fix(attest): make verify_all lenient under RequireAll (#359) - #365
fix(attest): make verify_all lenient under RequireAll (#359)#365andreolf wants to merge 1 commit into
Conversation
Registry::verify_all propagated every per-attestation error via ?, so a single malformed or unknown attestation aborted the whole batch. Under Policy::RequireAll that is a DoS vector: any third party can attach an attestation, so an unrelated attacker/spam/v1 could block an otherwise valid cert. Under RequireAll, drop entries that fail to verify instead of aborting; the required_types check remains the gate and only counts fully_verified entries, so a required type present only as a malformed attestation still fails with RequiredMissing. AcceptKnown and RejectUnknown stay strict. Adds tests for both: a malformed extra is dropped without aborting, and a malformed-only required type is still rejected. Closes Gitlawb#359
|
Warning Review limit reached
Next review available in: 52 minutes Limit details: You’ve used all 1 included review currently available under your plan. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. 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: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
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 |
beardthelion
left a comment
There was a problem hiding this comment.
Verified the fix does what it claims and does not weaken the gate. Reverting the policy branch to the pre-fix strict loop turns require_all_drops_a_malformed_extra_and_still_accepts red, and disabling the required_types check turns require_all_rejects_when_only_a_malformed_required_type_is_present red, so both new tests bind real behavior rather than passing vacuously. The vector is reachable as described: cert_hash() strips attestations before hashing and attach() is public, so any relaying party can append junk without invalidating a signature. Direction is right, the gate holds, and the blast radius is one caller. Two asks, both in lines this PR adds.
Unrelated to this PR: cargo audit is red on main too, RUSTSEC-2026-0258 in h2 0.4.13. Not yours, and I am not holding this on it.
Findings
-
[P2] Lock the non-
RequireAllpaths with an error-propagation test
crates/gitlawb-attest/src/verifier.rs:166
The new docstring promisesAcceptKnownandRejectUnknownstill surface the first error, but nothing asserts it. Replacing the strict branch's?with a lenientif let Ok(v)leaves the whole crate green; gutting its return does go red, so the branch executes and it is specifically the error propagation that is unbound. A later edit applying the lenient loop to every policy would pass every test here while silently turning defaultAcceptKnownbatch verification into drop-on-error. This closes it, and I confirmed it reddens under exactly that mutation:#[test] fn strict_policies_surface_the_first_batch_error() { let sk = fresh(); let cert_hash = sample_hash(); let bad = signed_demo(&sk, other_hash(), "ok"); let mut accept = Registry::new().with_policy(Policy::AcceptKnown); accept.register(DemoVerifier); let err = accept.verify_all(&[bad.clone()], cert_hash).expect_err("must not drop"); assert!(matches!(err, Error::CertHashMismatch { .. }), "got {err:?}"); let mut reject = Registry::new().with_policy(Policy::RejectUnknown); reject.register(DemoVerifier); let err = reject.verify_all(&[bad], cert_hash).expect_err("must not drop"); assert!(matches!(err, Error::CertHashMismatch { .. }), "got {err:?}"); }
-
[P3] Correct the docstring: unknown types are not dropped
crates/gitlawb-attest/src/verifier.rs:149
The added text lists "a type with no verifier" among entries dropped from the result. UnderRequireAll,verifyreturnsOkwithfully_verified = falsefor an unregistered type, sofilter_map(.ok())keeps it, andrequire_all_is_lenient_on_unknown_types_in_the_batchalready asserts the entry survives withlen == 2. Only the genuineErrpaths are dropped. The PR summary carries the same slip, though the Motivation section states it correctly.
Summary
Registry::verify_allaborted the whole batch on any per-attestation error, violating its documentedRequireAllleniency. UnderPolicy::RequireAll, a single malformed or unknown attestation now drops out of the result instead of failing the batch. Fixes #359.Motivation & context
Closes #359
The module docstring promises that under
RequireAllthe registry "never short-circuits on unknown types — any attestation can be attached by any third party, so blocking the cert because an attacker added an unrelatedattacker/spam/v1would be a denial-of-service vector." Butverify_allpropagated every error via?:So a bad signature, cert-hash mismatch, or malformed payload on any attached attestation aborted the entire batch — exactly the DoS the docstring warns about.
Kind of change
What changed
Crate touched:
gitlawb-attest(src/verifier.rs).Policy::RequireAll,verify_allnow drops entries that fail to verify (filter_map(.ok())) instead of aborting. Therequired_typespresence check remains the gate, and it only countsfully_verifiedentries — so a required type present only as a malformed attestation still fails withRequiredMissing. Leniency does not open a hole.AcceptKnownandRejectUnknownare unchanged: they stay strict and surface the first error.verify_alldocstring to state the per-policy contract.How a reviewer can verify
cargo test -p gitlawb-attest --lib verifier cargo clippy -p gitlawb-attest --all-targets -- -D warningsTwo new tests:
require_all_drops_a_malformed_extra_and_still_accepts— a malformed extra (signed against the wrong cert hash) alongside a valid required attestation no longer aborts; the junk is dropped and the cert verifies. Fails on the pre-fix code.require_all_rejects_when_only_a_malformed_required_type_is_present— when the sole copy of a required type is malformed,verify_allstill returnsRequiredMissing.Before you request review
cargo test --workspacepasses locally (rangitlawb-attest: 52 tests pass)cargo fmt --allandcargo clippy --workspace --all-targets -- -D warningsare cleanfix(attest): ...).env.exampleupdated if behavior or config changed (N/A)Protocol & signing impact
did:key, Ed25519 / RFC 9421 signatures, UCAN, ref certs, or P2P wire formats — attestation verification for ref-update certs. No wire-format or signature-scheme change; only the batch error-handling policy underRequireAll. Behavior forAcceptKnown/RejectUnknownis unchanged.RequireAll, and only for attestations that would otherwise have hard-failed the batch).Notes for reviewers
gitlawb-core'scert.rs; deliberately out of scope here to keep this PR to one crate/one change. Happy to follow up on that separately.