Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions rust/crates/wire-mesh-core/src/domain/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1051,9 +1051,10 @@ mod tests {

#[tokio::test]
async fn a_delegator_may_name_itself_as_the_bearer_of_its_own_delegation() {
// The right to delegate a capability always includes using it: nothing
// stops a delegator minting a child that names itself, so a
// delegate-only permission would not separate the two.
// Today's unconstrained behaviour: nothing stops a delegator minting
// a child that names itself. Whether a grant-capability whose
// conditions bind the grants it authorises should constrain this is
// the open design in wire-mesh#323.
let parent = NodeIdentity::generate_ed25519();
let (_, self_delegated, claims) = mint_delegated(&parent, |child| {
child.bearer = child.issuer;
Expand Down
2 changes: 1 addition & 1 deletion spec/management.cddl
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ manage-response-frame = { type: "manage-response", request-id: uint, outcome: ma
; same message/token-lifetime concept the plan's own section E already
; names as its own open item, rather than inventing a second TTL field.
; The outer manage-command.verb for a capability-request MUST be the capability string itself -- the same value as this map's own `capability` field below -- never a generic literal: routing by capability (the same pattern room.send/room.join already share under one shared room:member outer verb) is how a receiver's own per-capability handler knows which grant this ask is even for.
; Why there is no "may request" capability (wire-mesh#324): a request already carries an optional `token` field, and what a receiver does with it is the receiver's own policy, not a wire rule. A receiver that wants to pre-authorise a class of requesters, or to refuse the rest without putting a prompt in front of a human, accepts a request that presents a token it already honours (a `group:member` token for its own group, say) and applies its own rate limits and block lists to the rest. That reuses identity and tokens as they are, so a dedicated request capability would be a wrapper around them. It would also not close the stranger case: a requester can only hold such a token if someone granted it one, and a first contact has none, so a receiver that wants to hear from strangers must be reachable without one either way.
; Whether requesting a capability needs a permission of its own (wire-mesh#324, open): a request already carries an optional `token` field, and a receiver can apply its own policy to it, accepting a request that presents a token it already honours (a `group:member` token for its own group, say) and rate-limiting or blocking the rest. The open design treats the right to request as a grantable permission under the same owner-rooted model as #323, so a receiver can also delegate who may ask. Whichever way it lands, a stranger's first contact holds no token, so a receiver that wants to hear from strangers must stay reachable without one; the spec must keep saying so plainly.
$manage-command-params /= capability-request
capability-request = { verb: "capability.request", capability: tstr, ? valid-until: uint, * tstr => any }
capability-grant-ok = { result: "ok", "granted-token": capability-token, * tstr => any }
Expand Down
4 changes: 2 additions & 2 deletions spec/protocol.cddl
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ manage-response-frame = { type: "manage-response", request-id: uint, outcome: ma
; same message/token-lifetime concept the plan's own section E already
; names as its own open item, rather than inventing a second TTL field.
; The outer manage-command.verb for a capability-request MUST be the capability string itself -- the same value as this map's own `capability` field below -- never a generic literal: routing by capability (the same pattern room.send/room.join already share under one shared room:member outer verb) is how a receiver's own per-capability handler knows which grant this ask is even for.
; Why there is no "may request" capability (wire-mesh#324): a request already carries an optional `token` field, and what a receiver does with it is the receiver's own policy, not a wire rule. A receiver that wants to pre-authorise a class of requesters, or to refuse the rest without putting a prompt in front of a human, accepts a request that presents a token it already honours (a `group:member` token for its own group, say) and applies its own rate limits and block lists to the rest. That reuses identity and tokens as they are, so a dedicated request capability would be a wrapper around them. It would also not close the stranger case: a requester can only hold such a token if someone granted it one, and a first contact has none, so a receiver that wants to hear from strangers must be reachable without one either way.
; Whether requesting a capability needs a permission of its own (wire-mesh#324, open): a request already carries an optional `token` field, and a receiver can apply its own policy to it, accepting a request that presents a token it already honours (a `group:member` token for its own group, say) and rate-limiting or blocking the rest. The open design treats the right to request as a grantable permission under the same owner-rooted model as #323, so a receiver can also delegate who may ask. Whichever way it lands, a stranger's first contact holds no token, so a receiver that wants to hear from strangers must stay reachable without one; the spec must keep saying so plainly.
$manage-command-params /= capability-request
capability-request = { verb: "capability.request", capability: tstr, ? valid-until: uint, * tstr => any }
capability-grant-ok = { result: "ok", "granted-token": capability-token, * tstr => any }
Expand Down Expand Up @@ -1446,7 +1446,7 @@ capability-token = cose-sign1
; `0` on every grant it issues directly, so every member's token was issued
; by the owner and is therefore directly revocable with zero collateral,
; rather than only revocable by tearing down an entire invited subtree.
; Why there is no "may delegate but may not use" permission (wire-mesh#323): a delegation's bearer is unconstrained, so whoever may mint a child of a token may mint one naming themselves as bearer. The right to delegate a capability therefore always includes the right to use it, and a separate delegate-only capability would not separate the two unless a token could also restrict who its children name as bearer, an inheritance of restrictions that `conditions` deliberately does not have. Authority over other tokens that does not include using them is `manage:revoke`'s case, and it works because revoking confers nothing the revoker could grant itself. A holder that must not use a capability is given no token for it; the owner of the resource mints root-level grants directly.
; Today's behaviour, pending the grant-permission design (wire-mesh#323): a delegation's bearer is unconstrained, so whoever may mint a child of a token may mint one naming themselves as bearer; the right to delegate a capability, as the model stands, includes the right to use it. `conditions` on a token are evaluated against that token's own claims and deliberately do not inherit, so nothing today constrains who a minted child may name as bearer. Whether a grant-capability (grantability as a permission of its own, rooted at the resource's owner) whose trilean conditions bind the grants it authorises, letting an issuer bar its holder from granting to itself among other constraints, should close that is the open design in #323.
token-claims = {
token-id: bstr,
issuer: device-id,
Expand Down
2 changes: 1 addition & 1 deletion spec/tokens.cddl
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ capability-token = cose-sign1
; `0` on every grant it issues directly, so every member's token was issued
; by the owner and is therefore directly revocable with zero collateral,
; rather than only revocable by tearing down an entire invited subtree.
; Why there is no "may delegate but may not use" permission (wire-mesh#323): a delegation's bearer is unconstrained, so whoever may mint a child of a token may mint one naming themselves as bearer. The right to delegate a capability therefore always includes the right to use it, and a separate delegate-only capability would not separate the two unless a token could also restrict who its children name as bearer, an inheritance of restrictions that `conditions` deliberately does not have. Authority over other tokens that does not include using them is `manage:revoke`'s case, and it works because revoking confers nothing the revoker could grant itself. A holder that must not use a capability is given no token for it; the owner of the resource mints root-level grants directly.
; Today's behaviour, pending the grant-permission design (wire-mesh#323): a delegation's bearer is unconstrained, so whoever may mint a child of a token may mint one naming themselves as bearer; the right to delegate a capability, as the model stands, includes the right to use it. `conditions` on a token are evaluated against that token's own claims and deliberately do not inherit, so nothing today constrains who a minted child may name as bearer. Whether a grant-capability (grantability as a permission of its own, rooted at the resource's owner) whose trilean conditions bind the grants it authorises, letting an issuer bar its holder from granting to itself among other constraints, should close that is the open design in #323.
token-claims = {
token-id: bstr,
issuer: device-id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ describe("verifyCapabilityToken - delegation narrowing", () => {
expect(verdict.ok).toBe(true);
});

it("accepts a delegation whose bearer is the delegator itself, so the right to delegate a capability always includes using it", async () => {
it("accepts a delegation whose bearer is the delegator itself, the model's unconstrained behaviour today", async () => {
const root = await signToken(issuer, {
tokenId: nextTokenId(),
bearer: bearerDeviceId,
Expand Down
Loading