Skip to content

fix: cross-check the minted ascent link and classify rotation authoring refusals - #1070

Merged
FSM1 merged 3 commits into
mainfrom
fix/1057-produce-side-ascent-link-check-and-refusal-classes
Aug 5, 2026
Merged

fix: cross-check the minted ascent link and classify rotation authoring refusals#1070
FSM1 merged 3 commits into
mainfrom
fix/1057-produce-side-ascent-link-check-and-refusal-classes

Conversation

@FSM1

@FSM1 FSM1 commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Two produce-side rotation-authoring correctness fixes from the #1053/#1058 review round.

1057 — mirror the ascent-link cross-check on the produce side of a re-seal

The adoption gate's stage 3 opens a scope root's ascent link under the reader's cached ancestor node seed and fail-closes when the payload's epoch is not the envelope's or the recovered seed does not derive this node's read key. net/author.rs::check_scope_root cannot mirror that half — EnvelopeAuthoring carries no ancestor node seed — but the re-seal does hold one, and it is the path that mints the link.

reseal_scope_root now reopens the link it just sealed, exactly as an ancestor reader does, and returns ResealError::AscentLinkMismatch unless it carries the seed and epoch this re-seal publishes at. The expected pair is taken from ResealSeeds, never from the payload under test, so the ascent arm cannot drift from the seed and epoch the rest of the section is minted at. The guard returns Err — never a debug_assert! — and its reject rows are covered by a test that fires in a --release build, per security rule 8.

Where the mirror stops, stated rather than overclaimed. An earlier revision of this PR justified the guard by claiming seal_ascent_link and open_ascent_link derive the ascent keypair independently. They do not — both call kdf::ascent_keypair on the same parent_node_seed. Both review gates caught it. So the guard cannot authenticate the threaded ancestor seed itself: a caller that hands the re-seal a stale or wrong parent seed seals and reopens symmetrically and passes, and is caught only by a reader deriving its own from cached state. That limit is now written on the function, and the real check — which needs a second, independently sourced derivation, in the cascade or a post-publish readback — is filed as #1076, blocked by this work.

ResealError::AscentLinkMismatch is non-retryable in both consumers without any change: CascadeError::is_retryable retries only ResealError::Entropy, and SweepError::is_retryable treats every re-seal rejection as fatal. Correct — the guard's inputs are all owner-derived, so it can only fire on an internal bug, never on adversarial input.

1055 — classify rotation-path authoring refusals

publish_scope_root mapped every AuthorError to ScopeRootPublishError::NotPublished, which is_retryable reports as retryable. The four produce-side trust refusals are this build's own gate verdict on bytes it is about to sign, reached before the PUT; re-authoring the same section reaches the same verdict, so a rotation whose section cannot author retried forever without ever being classified. They now route to Rejected, the non-retryable arm the rotation plane already uses.

The issue's item 1 is settled by deleting the duplicate commitment-signature verify from check_publishable: check_scope_root verifies the same signature (and its name binding) inside the authoring call, and with the mapping above both paths now reach Rejected. The write-scope-seed extraction moved to after the authoring call so a trust verdict still precedes the capability check — otherwise a record that is both trust-invalid and write-keyless would have been reported as the retryable verdict.

Abort vs drop, decided deliberately

The issue asked to check that a newly non-retryable verdict "drops the node from the work list rather than aborting a cascade mid-way". Neither consumer drops today: rotation/sweep.rs drops only LostRace and aborts the pass on every other publish error, and rotation/cascade.rs::rekey_one aborts on every publish error including LostRace (a revocation must install the fresh seed). Introducing Rejected therefore changes no abort-vs-drop behaviour at all — only is_retryable, from an infinite retry that could never converge to an honest fatal verdict. Both consumers already have coverage for a Rejected publish verdict aborting non-retryably, so no error-arm edits were needed in sweep.rs, cascade.rs or rotate.rs.

Denial-of-revocation reasoning

Turning a tolerated input into a hard failure on an adversary-reachable path is how a committed grantee blocks an owner's revocation permanently. Neither refusal reclassified here is adversary-reachable:

  • the commitment and its signature come from the descendant's gate-passed record, which stage 2 already verified under the anchored owner identity and bound to that exact ipnsName, so CommitmentSignatureInvalid and CommitmentNameMismatch cannot be reached from network bytes;
  • structure signatures are minted by the re-seal under the pseudonym key reseal_scope_root refuses to deviate from, and recomputed at the same (scope, epoch) the envelope is authored at, so SectionSignatureInvalid cannot either;
  • the ascent-link guard's inputs (parent node seed, override seed, read epoch, scope id) are all owner-derived; an attacker has no lever on any of them.

And in the shape where a refusal did somehow occur, the prior behaviour aborted the cascade too — retryably, spinning forever without converging. The change makes an already-failing revocation legible rather than adding a new way to stall one.

The reviewers pushed the other way on the non-trust arm: HeadTooLarge and Seal(_) reproduce on every pass here, so retrying them stalls a revocation indefinitely, and one gate recommended making them fatal. Deliberately not taken — that is precisely the denial-of-revocation vector. A party who can pad a scope root's preserved fields could then abort the owner's cascade permanently, where today it merely stalls. The size axis is the right place to fix it, filed as #1077.

Verification

cargo test -p cipherbox-engine -p cipherbox-core: 1239 passed, 0 failed. Release run (--release): 1239 passed, 0 failed, with all three new tests confirmed executing under --release. cargo fmt --all --check, cargo clippy --all-targets, cargo check -p cipherbox-wasm --target wasm32-unknown-unknown and pnpm lint:tracker-refs all clean.

Review gates run on this diff: /simplify (named the verdict mapper, merged two duplicate publish tests, dropped a doc paragraph defending code that left the function), /security-review and /crypto-privacy-review (both above, findings folded in; follow-ups #1076 and #1077 filed with dependency edges).

Issue-body corrections

#1055 says check_scope_root verifies the commitment "nine lines later" than check_publishable; it is roughly 130 lines later, inside the authoring call. Its item 2 also asks whether a non-retryable verdict "drops the node rather than aborting" — neither consumer drops on a non-LostRace publish error today, so there is no drop-vs-abort choice to make; see above.

Closes #1057
Closes #1055

Note

Fix ascent link cross-check in reseal and classify rotation authoring refusals as rejected

  • reseal_scope_root in reseal.rs now reopens the freshly sealed ascent link and verifies the recovered override seed and epoch match expected values; mismatches return a new AscentLinkMismatch error instead of producing a bad section.
  • A new author_verdict helper in rotation.rs maps AuthorError variants so trust refusals become non-retryable Rejected responses, while codec/size failures remain retryable NotPublished.
  • OwnerRotationNet.publish_scope_root now uses author_verdict for error classification and defers write_scope_seed retrieval until after successful authoring.
  • OwnerRotationNet.check_publishable no longer verifies the commitment signature; only floor checks (read epoch vs. revocation floor, write epoch vs. write floor) remain.
  • Behavioral Change: sections with trust violations during publish_scope_root now return Rejected (non-retryable) instead of NotPublished.

Macroscope summarized d7acbfd.

Summary by CodeRabbit

  • New Features

    • Re-sealing now validates ascent-link information more strictly, helping ensure links are reopened and checked during rotation.
  • Bug Fixes

    • Improved handling of publish failures so trust-related rejections are treated differently from retryable format or size issues.
    • Invalid, mismatched, or wrongly contextualized links now fail with a clear, consistent error.
    • Added safeguards so corrupted related data is rejected during publishing and re-sealing.

…ng refusals

Mirror the adoption gate's stage-3 ascent-link cross-check on the produce
side of a re-seal, where the ancestor node seed lives: reopen the freshly
sealed link exactly as an ancestor reader does and refuse, release-active,
a link that does not yield this epoch's override seed. seal_ascent_link and
open_ascent_link derive the ascent keypair independently, so nothing else
stopped a re-seal signing an interior scope root every ancestor reader
rejects whole-record.

Route a produce-side trust refusal from author_scope_root_with_section to
ScopeRootPublishError::Rejected instead of NotPublished, so the sweep and
cascade stop retrying this build's own gate verdict as if it were a stalled
endpoint. Drop the duplicate commitment-signature verify from
check_publishable now that the authoring call reaches the same Rejected
verdict, and take the write-scope seed after authoring so a trust verdict
still precedes the capability check.

Closes #1057
Closes #1055
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

The re-seal path now validates newly sealed ascent links against the parent seed and epoch. Rotation authoring now distinguishes permanent trust refusals from retryable codec and size failures. Publishability checks durable epoch floors without duplicating commitment verification.

Reseal validation and publication classification

Layer / File(s) Summary
Ascent-link re-seal validation
crates/engine/src/net/author.rs, crates/engine/src/rotation/reseal.rs
Re-sealing reopens ascent links and validates their seed, epoch, context, structure, and version. Mismatches use ResealError::AscentLinkMismatch. Tests cover valid and invalid links plus owner-blob agreement.
Authoring verdict and publication flow
crates/engine/src/net/rotation.rs
Rotation maps trust refusals to Rejected and codec or size failures to NotPublished. Publishability checks durable epoch floors, and corruption tests verify permanent rejection without publication.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • FSM1/cipher-box issue 1076: Concerns ascent-link validation in rotation/reseal.rs with the ancestor seed.
  • FSM1/cipher-box issue 1077: Concerns author_verdict handling for retryable authoring failures.

Possibly related PRs

  • FSM1/cipher-box#1058: Modifies authoring trust failures and commitment-signature classification in the same rotation paths.
  • FSM1/cipher-box#1049: Addresses ascent-link validation in author.rs and rotation/reseal.rs.
  • FSM1/cipher-box#1027: Introduces the rotation and re-sealing paths extended by these changes.

Suggested labels: comp:engine

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the ascent-link cross-check and trust-refusal classification required by [#1057] and [#1055], with release-active rejection tests.
Out of Scope Changes check ✅ Passed The documented boundary, error classification, verification removal, seed ordering, and tests are all related to the linked issue objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: ascent-link cross-checking and classification of rotation authoring refusals.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1057-produce-side-ascent-link-check-and-refusal-classes

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

FSM1 added 2 commits August 5, 2026 12:51
…ard rationale

Fold the /simplify pass: extract the AuthorError routing into a named
author_verdict beside publish_verdict, drop the doc paragraph defending the
commitment verify that left check_publishable, and merge the two near-identical
gate-mirror publish tests into one table.

Correct the ascent-link guard's doc: seal_ascent_link and open_ascent_link both
derive the keypair through kdf::ascent_keypair, so the guard's value is that it
takes its expected seed and epoch from ResealSeeds rather than from the payload
under test. Its positive row now runs end-to-end through reseal_scope_root
instead of a hand-sealed link.
…wner blob

Fold the security and crypto-privacy passes. Both flagged that the guard's doc
claimed coverage it does not have: seal and open derive the ascent keypair from
the one parent_node_seed the re-seal is handed, so a mis-threaded ancestor seed
passes. State that limit instead of overclaiming, and file the real check as a
follow-up.

Add the reject rows that survive a refactor of the guard's placement — three AAD
transplants and a substituted ascent public half — and a test that the minted
ascent link and owner blob carry one seed, which is the binding the publish arm
relies on when it keys the record's read body off the owner blob.

Also state honestly why a codec or size refusal stays retryable in
author_verdict: a permanent verdict there would let anyone who can grow the
record block the owner's revocation for good.
@FSM1
FSM1 marked this pull request as ready for review August 5, 2026 14:53
Comment thread crates/engine/src/rotation/reseal.rs
@FSM1
FSM1 marked this pull request as draft August 5, 2026 15:00
@FSM1
FSM1 marked this pull request as ready for review August 5, 2026 15:18
@FSM1
FSM1 merged commit 04d84c9 into main Aug 5, 2026
35 checks passed
@FSM1
FSM1 deleted the fix/1057-produce-side-ascent-link-check-and-refusal-classes branch August 5, 2026 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant