Skip to content

fix(core): honor wildcard capabilities in Ucan::can, matching is_attenuated_by#200

Open
euxaristia wants to merge 2 commits into
Gitlawb:mainfrom
euxaristia:fix/ucan-can-wildcard-matching
Open

fix(core): honor wildcard capabilities in Ucan::can, matching is_attenuated_by#200
euxaristia wants to merge 2 commits into
Gitlawb:mainfrom
euxaristia:fix/ucan-can-wildcard-matching

Conversation

@euxaristia

Copy link
Copy Markdown

Summary

Capability::is_attenuated_by (crates/gitlawb-core/src/ucan.rs:52-57) treats with: "*" and can: "*" / "repo/admin" on a parent capability as wildcards during delegation-chain verification, so a valid UCAN can end with a capability like {"with": "*", "can": "git/push"}. But Ucan::can did strict equality on both fields, so that same capability could never match a concrete (resource, action) request at runtime.

Closes #199

Why this is a bug fix, not a security fix

The mismatch only produces false negatives: a validly-delegated wildcard capability gets denied, never something unauthorized getting granted. It's also dormant today — Ucan::can isn't called from any production authorization path yet (auth::caller_authorized_to_push is pure did_matches(...), with UCAN-capability checking noted as future Phase 2 work). This closes the gap before that wiring lands.

What changed

  • Ucan::can now mirrors is_attenuated_by's wildcard semantics from the matching side: the stored capability plays the "parent" role, so with == "*" matches any resource and can == "*" or "repo/admin" matches any action.
  • Two new tests: one for the resource wildcard, one for the repo/admin action wildcard.

How a reviewer can verify

cargo test -p gitlawb-core ucan
cargo test -p gl ucan
cargo fmt --all -- --check
cargo clippy -p gitlawb-core --all-targets -- -D warnings
cargo clippy -p gl --all-targets -- -D warnings
cargo clippy -p gitlawb-node --all-targets -- -D warnings

Kind of change

  • Bug fix
  • Feature
  • Security fix
  • Docs
  • Tests / CI
  • Refactor (no behavior change)
  • Breaking or protocol change

Protocol & signing impact

  • Touches DID / did:key, Ed25519 / RFC 9421 signatures, UCAN, ref certs, or P2P wire formats

No wire-format or signing change — can() is a local, in-process capability check over an already-verified UCAN's payload. Behavior only changes for capabilities that already contain a literal "*" or "repo/admin", which is_attenuated_by already treats as wildcards during chain verification.

…nuated_by

Ucan::can did strict equality on both resource and action, so a validly
delegated wildcard capability (with: "*", or can: "*" / "repo/admin") could
never match a concrete runtime request even though is_attenuated_by treats
those same values as wildcards during delegation-chain verification.

Not a security issue: the mismatch only produces false negatives (denying a
capability the delegation chain actually grants), never false positives.
Dormant today since no production code path calls can() yet — auth's
caller_authorized_to_push is pure did_matches(), with the UCAN-capability
check noted as future (Phase 2) work.

Makes can() mirror is_attenuated_by's semantics: the stored capability plays
the "parent" role, so with == "*" matches any resource and can == "*" or
"repo/admin" matches any action.

Closes Gitlawb#199
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@euxaristia, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 97cc1d37-609d-42e3-8403-3445eb789654

📥 Commits

Reviewing files that changed from the base of the PR and between ad7c2b2 and f5587e2.

📒 Files selected for processing (1)
  • crates/gitlawb-core/src/ucan.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@beardthelion beardthelion added crate:core gitlawb-core — identity, certs, encrypt, DID/UCAN kind:bug Defect fix — wrong or unsafe behavior subsystem:identity DID/UCAN, http-sig auth, push authorization labels Jul 14, 2026

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verified the core is correct by execution: can() is an exact mirror of is_attenuated_by with the stored capability as the parent, so it grants exactly what the delegation chain already accepts. I enumerated the full cap x resource x action matrix (including "*", "repo/admin", and empty strings) and found no case where can() grants beyond is_attenuated_by — only false negatives removed, no new grant. Reverting can() to the old strict equality turns both new tests red, and there's no production caller yet, so this is a correct, dormant fix. One gap before merge.

Findings

  • [P2] Add a test for the new cap.can == "*" branch
    crates/gitlawb-core/src/ucan.rs:198
    Of the three match alternatives the fix introduces, only with == "*" and repo/admin are exercised. Dropping just || cap.can == "*" leaves both can_honors_* tests green, so the full-action-wildcard branch has no coverage. On an authorization primitive every match branch should be load-bearing. One test closes it:

    let ucan = Ucan::issue(
        &issuer,
        audience,
        vec![Capability::new("gitlawb://repos/test/repo", "*")],
        None,
    )
    .unwrap();
    assert!(ucan.can("gitlawb://repos/test/repo", caps::GIT_PUSH));
    assert!(ucan.can("gitlawb://repos/test/repo", caps::PR_MERGE)); // two actions via can == "*"
    assert!(!ucan.can("gitlawb://repos/other/repo", caps::GIT_PUSH)); // wrong resource still denied
  • [P3, pre-existing] constraints (nb) is unenforced. Both can() and is_attenuated_by ignore the field, so a capability like {with, can: "repo/admin", nb: {...}} grants broadly regardless of the constraint. Not introduced here, but this PR widens where it matters once can() gets wired into the authorization path — worth settling constraint enforcement before that Phase 2 wiring lands. Note, not a blocker.

  • [P3, pre-existing] repo/admin is treated as a universal action wildcard (equal to "*"), so it grants network/join, agent/deploy, etc. on the resource, not just repo actions. This lives in is_attenuated_by; can() faithfully inherits it. Worth confirming that breadth is intended. Design note.

The retarget of the matching semantics to mirror is_attenuated_by is sound and correctly scoped. The only blocker is the untested can == "*" branch.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Add load-bearing coverage for the can == "*" wildcard branch
    crates/gitlawb-core/src/ucan.rs:198
    The implementation adds three wildcard alternatives, but the new tests exercise only with == "*" and can == "repo/admin". Removing just || cap.can == "*" leaves every added test green, so a regression would silently reintroduce false negatives for valid full-action-wildcard UCANs once this method is used for authorization. Add a test with a concrete resource and can: "*" that proves multiple actions match while the same action remains denied for a different resource.

@euxaristia

Copy link
Copy Markdown
Author

Pushed a fixup addressing beardthelion's and jatmn's shared finding: added can_honors_action_wildcard, a test with a concrete resource and can: "*" that asserts multiple actions match while the same action stays denied for a different resource. All 18 ucan tests pass.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@beardthelion
beardthelion dismissed their stale review July 15, 2026 19:57

Dismissing my stale CHANGES_REQUESTED — it was on cd96add, before the contributor pushed the load-bearing can=="*" test. Re-reviewed the current head; approving separately.

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The blocker from my last pass is closed. can_honors_action_wildcard is load-bearing: I dropped || cap.can == "*" from can() and it goes red (panic at the pr/merge assertion) while the other 17 ucan tests stay green, then restored and confirmed 18/18. That pins the exact branch jatmn and I flagged. The core is unchanged from my prior review and still correct: can() is a term-for-term mirror of is_attenuated_by with the stored capability in the parent slot, so it only removes false negatives and never grants past a real delegation. I re-checked the requested-side wildcard case too (can("*", ...) against a concrete stored cap denies, matching is_attenuated_by's "wildcards on self carry no meaning").

Confirmed dormant on this head: the only production authz path is verify_chain() at crates/gitlawb-node/src/auth/mod.rs:301; every .can() caller in the tree is test-only. LGTM.

One forward-looking note, not blocking this PR: can() trusts payload.att raw, with no signature/expiry/chain check of its own. That's the right split for a capability-matching primitive, but the wildcard change widens the blast radius if a future caller forgets to validate first (one ("*","repo/admin") cap now answers can(any, any) = true). When Phase 2 wires can() into the auth path, the caller must gate on verify_chain before calling it. Worth a one-line precondition on the doc comment while it's cheap; fine to fold into the Phase 2 change.

@kevincodex1 approved and ready to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crate:core gitlawb-core — identity, certs, encrypt, DID/UCAN kind:bug Defect fix — wrong or unsafe behavior subsystem:identity DID/UCAN, http-sig auth, push authorization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ucan::can does not honor wildcard capabilities that is_attenuated_by grants during delegation

3 participants