fix: cross-language IPNS and node-codec verification parity#608
Conversation
Entire-Checkpoint: 28cad9348085
Entire-Checkpoint: e739f36a0c16
Entire-Checkpoint: 0198826aa4cd
…ases - 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
Entire-Checkpoint: 07e263ae724b
- 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
…odec 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
…c 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
Entire-Checkpoint: 142c93154f2c
- 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
- 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
- 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
- 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
- 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
Entire-Checkpoint: 12fdaeb7318e
- 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
- 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
- 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
- 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
- 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
Entire-Checkpoint: bf3c6f7ccd53
- 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
- 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
Entire-Checkpoint: 5112a3fb234e
Entire-Checkpoint: 36c3ccaf9bc2
Entire-Checkpoint: a2cb16d0454e
Entire-Checkpoint: 4025885d9fe0
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
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
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
|
Warning Review limit reached
Next review available in: 46 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (43)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Z3zThQZPm6bVk2476WURL Entire-Checkpoint: 01f9fb37c55b
Release Preview
Cascade Details
|
Greptile SummaryThis PR aligns Rust and TypeScript verification behavior for IPNS records and node-codec vectors. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (2): Last reviewed commit: "fix(75): reject any duplicate CBOR key i..." | Re-trigger Greptile |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #608 +/- ##
==========================================
+ Coverage 70.16% 77.68% +7.52%
==========================================
Files 157 197 +40
Lines 15425 32874 +17449
Branches 1952 1952
==========================================
+ Hits 10823 25539 +14716
- Misses 4348 7081 +2733
Partials 254 254
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
…arity 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
Phase 75 — Cross-Language IPNS and Node-Codec Verification Parity
Makes the Rust and TypeScript verification paths reach identical fail-closed
verdicts for every IPNS record and node-codec body they inspect. All
divergences here are Ed25519-signature-gated, so these are soundness/parity
concerns (a split-brain where one language accepts what the other rejects), not
key or plaintext exposure.
What shipped (SC1–SC3)
real-signed invalid cases (expired, wrong
ValidityType, two malformedRFC3339). TS
resolveIpnsRecordnow parses RFC3339 with a strictbranch-for-branch mirror of the Rust
parse_rfc3339_to_unix_secsand gates onValidityType === 0; Rustbind_verifiedgates onValidityType == 0.fileIvencoding lock. KAT samples switched toencoding-unambiguous base64 with a decode-and-assert-length check on both
languages, so a hex decoder substituted for base64 fails the KAT.
build_node_aadand TSuuidToBytestightened to canonical-only, locked by a cross-language oracle.Parity hardening from the ship-time crypto/privacy review
The review (
.planning/security/REVIEW-2026-07-11-phase75-parity.md) surfacedverdict divergences beyond the original scope; the two material ones are fixed
here on both sides:
cborDecodedefaulted to last-wins, so asigned record with e.g.
ValidityType:[1,0]decoded to0and was acceptedwhile the Rust decoder rejects duplicate keys. TS now decodes with
rejectDuplicateMapKeys: true; locked by an integration test.parse::<T>()accepted a leading+/-and overflowing years; TS did not enforce the 4-digit year. Both now use a
fixed-width digit check (year 4, others 2), rejecting leading signs and
non-canonical widths identically. Locked by parser tests on both languages.
One LOW benign-direction divergence (a CBOR float
0.0ValidityType— stillmeans EOL — passes the TS gate but the Rust integer-major-type check rejects it)
is deferred:
.planning/todos/pending/2026-07-11-ts-validitytype-float-vs-integer-cbor-parity.md.CodeRabbit CLI review
5 findings, all fixed. The material one was another SC3 parity gap:
uuidToBytesaccepted a trailing-newline UUID because JS
$(nomflag) also matchesbefore a final
\n, while Rust'sis_canonical_uuid_formrejects it onbytes.len() != 36. Fixed with alength === 36guard and a\nregression caseadded to the shared UUID oracle (run by both languages). The rest were test-rigor
(specific reject-verdict assertions on both sides) and STATE.md/review-doc
bookkeeping. Triage tally: 8 fixed, 1 todo, 1 discarded (
fileIvbase64confirm — already locked by the SC2 KAT).
Verification
cipherbox-fuse ipns_verify_vectors,cipherbox-core node_codec_vectors,cipherbox-crypto cross_language,cipherbox-api-client --lib ipns— all green.@cipherbox/sdk-core389 passed,@cipherbox/core203,@cipherbox/crypto200.before and after the resolve-path change (no false-reject of real records). The
2
tee-republishfailures are a pre-existing TEE-worker precondition(
tee_key_state is empty) unrelated to this phase — no TEE code is touched.75-SECURITY.md: SECURED.75-VALIDATION.md: nyquist-compliant.Also in this branch (bookkeeping)
Closed 3 stranded
resolves_phase: 74todos that phase 74 (merged) actuallyresolved but the GSD auto-close left in
pending— verified each against themerged code before moving. See
docs(74)commit.🤖 Generated with Claude Code