fix: cross-check the minted ascent link and classify rotation authoring refusals - #1070
Merged
FSM1 merged 3 commits intoAug 5, 2026
Merged
Conversation
…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
WalkthroughChangesThe 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
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
…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
marked this pull request as ready for review
August 5, 2026 14:53
FSM1
marked this pull request as draft
August 5, 2026 15:00
FSM1
marked this pull request as ready for review
August 5, 2026 15:18
FSM1
deleted the
fix/1057-produce-side-ascent-link-check-and-refusal-classes
branch
August 5, 2026 15:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_rootcannot mirror that half —EnvelopeAuthoringcarries no ancestor node seed — but the re-seal does hold one, and it is the path that mints the link.reseal_scope_rootnow reopens the link it just sealed, exactly as an ancestor reader does, and returnsResealError::AscentLinkMismatchunless it carries the seed and epoch this re-seal publishes at. The expected pair is taken fromResealSeeds, 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 returnsErr— never adebug_assert!— and its reject rows are covered by a test that fires in a--releasebuild, 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_linkandopen_ascent_linkderive the ascent keypair independently. They do not — both callkdf::ascent_keypairon the sameparent_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::AscentLinkMismatchis non-retryable in both consumers without any change:CascadeError::is_retryableretries onlyResealError::Entropy, andSweepError::is_retryabletreats 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_rootmapped everyAuthorErrortoScopeRootPublishError::NotPublished, whichis_retryablereports 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 toRejected, 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_rootverifies the same signature (and its name binding) inside the authoring call, and with the mapping above both paths now reachRejected. 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.rsdrops onlyLostRaceand aborts the pass on every other publish error, androtation/cascade.rs::rekey_oneaborts on every publish error includingLostRace(a revocation must install the fresh seed). IntroducingRejectedtherefore changes no abort-vs-drop behaviour at all — onlyis_retryable, from an infinite retry that could never converge to an honest fatal verdict. Both consumers already have coverage for aRejectedpublish verdict aborting non-retryably, so no error-arm edits were needed insweep.rs,cascade.rsorrotate.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:
ipnsName, soCommitmentSignatureInvalidandCommitmentNameMismatchcannot be reached from network bytes;reseal_scope_rootrefuses to deviate from, and recomputed at the same(scope, epoch)the envelope is authored at, soSectionSignatureInvalidcannot either;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:
HeadTooLargeandSeal(_)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-unknownandpnpm lint:tracker-refsall 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-reviewand/crypto-privacy-review(both above, findings folded in; follow-ups #1076 and #1077 filed with dependency edges).Issue-body corrections
#1055sayscheck_scope_rootverifies the commitment "nine lines later" thancheck_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-LostRacepublish 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_rootin reseal.rs now reopens the freshly sealed ascent link and verifies the recovered override seed and epoch match expected values; mismatches return a newAscentLinkMismatcherror instead of producing a bad section.author_verdicthelper in rotation.rs mapsAuthorErrorvariants so trust refusals become non-retryableRejectedresponses, while codec/size failures remain retryableNotPublished.OwnerRotationNet.publish_scope_rootnow usesauthor_verdictfor error classification and deferswrite_scope_seedretrieval until after successful authoring.OwnerRotationNet.check_publishableno longer verifies the commitment signature; only floor checks (read epoch vs. revocation floor, write epoch vs. write floor) remain.publish_scope_rootnow returnRejected(non-retryable) instead ofNotPublished.Macroscope summarized d7acbfd.
Summary by CodeRabbit
New Features
Bug Fixes