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
31 changes: 31 additions & 0 deletions conformance/frames.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,37 @@
},
"wire_hex": "a564747970656e6d616e6167652d726571756573746573636f7065a2646b696e6464726f6f6d64706174687848313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131312f67656e6572616c65746f6b656e845828a2613126613458201111111111111111111111111111111111111111111111111111111111111111a059016ba86573636f7065a2646b696e6464726f6f6d64706174687848313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131312f67656e6572616c6662656172657258202222222222222222222222222222222222222222222222222222222222222222666973737565725820111111111111111111111111111111111111111111111111111111111111111167657870697265731b000001b8dac5b40068746f6b656e2d696450030303030303030303030303030303036a6361706162696c6974796b726f6f6d3a6d656d6265726a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb7564656c65676174696f6e732d72656d61696e696e67015840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff67636f6d6d616e64a264766572626b726f6f6d3a6d656d62657266706172616d73a164766572626c726f6f6d2e6d656d626572736a726571756573742d69640a"
},
{
"name": "manage_request_v1_capability_request_gated",
"message": {
"type": "manage-request",
"request-id": 21,
"command": {
"verb": "room:member",
"params": {
"verb": "capability.request",
"capability": "room:member"
}
},
"scope": {
"kind": "room",
"path": "1111111111111111111111111111111111111111111111111111111111111111/general"
},
"token": [
{
"hex": "a2613126613458201111111111111111111111111111111111111111111111111111111111111111"
},
{},
{
"hex": "a86573636f7065a1646b696e6464726f6f6d6662656172657258202222222222222222222222222222222222222222222222222222222222222222666973737565725820111111111111111111111111111111111111111111111111111111111111111167657870697265731b000001b8dac5b40068746f6b656e2d696450080808080808080808080808080808086a6361706162696c6974796e6d616e6167653a726571756573746a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb7372657175657374732d6361706162696c6974796b726f6f6d3a6d656d626572"
},
{
"hex": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
}
]
},
"wire_hex": "a564747970656e6d616e6167652d726571756573746573636f7065a2646b696e6464726f6f6d64706174687848313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131313131312f67656e6572616c65746f6b656e845828a2613126613458201111111111111111111111111111111111111111111111111111111111111111a0590128a86573636f7065a1646b696e6464726f6f6d6662656172657258202222222222222222222222222222222222222222222222222222222222222222666973737565725820111111111111111111111111111111111111111111111111111111111111111167657870697265731b000001b8dac5b40068746f6b656e2d696450080808080808080808080808080808086a6361706162696c6974796e6d616e6167653a726571756573746a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb7372657175657374732d6361706162696c6974796b726f6f6d3a6d656d6265725840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff67636f6d6d616e64a264766572626b726f6f6d3a6d656d62657266706172616d73a26476657262726361706162696c6974792e726571756573746a6361706162696c6974796b726f6f6d3a6d656d6265726a726571756573742d696415"
},
{
"name": "manage_request_v1_room_join_dm",
"message": {
Expand Down
18 changes: 17 additions & 1 deletion conformance/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import {
type Vector,
} from "@exadev/wire-mesh-conformance";
import { advertVectors } from "./adverts.ts";
import { grantTokenVectors, roomMemberTokens } from "./token-vectors.ts";
import {
grantTokenVectors,
manageRequestTokenVector,
roomMemberTokens,
} from "./token-vectors.ts";
import { writeVectorFiles } from "./vector-files.ts";

function wireHex(message: JsonWire): string {
Expand Down Expand Up @@ -242,6 +246,7 @@ const tokenVectors: Vector[] = [
rootTokenVector,
delegatedTokenVector,
...grantVectors,
manageRequestTokenVector(grantVectorContext),
...roomMemberVectors,
roomNoticeVector,
roomNoticeForwardVector,
Expand Down Expand Up @@ -520,6 +525,17 @@ const frameVectors: Vector[] = [
token: roomMember.rootToken,
}),
// room.join and room.invite are deliberately ungated (no token field) -- access control is a human's explicit approval in the receiving UI, not a pre-shared token, the first verbs in this spec to work that way. This join vector uses a DM room path (the sorted device-id pair), the shape a first, tokenless contact actually needs.
// wire-mesh#324: the same capability.request ask, presented by a requester holding a manage:request permission -- manage-request-frame's own token field carries it, exactly what a receiver that has opted into gating checks.
vector("manage_request_v1_capability_request_gated", {
type: "manage-request",
"request-id": 21,
command: {
verb: "room:member",
params: { verb: "capability.request", capability: "room:member" },
},
scope: { kind: "room", path: `${deviceAHex}/general` },
token: manageRequestTokenVector(grantVectorContext).message,
}),
vector("manage_request_v1_room_join_dm", {
type: "manage-request",
"request-id": 11,
Expand Down
32 changes: 32 additions & 0 deletions conformance/token-vectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,35 @@ export function roomMemberTokens(ctx: Readonly<TokenVectorContext>): {
delegatedToken: delegated.message,
};
}

/// wire-mesh#324's request-permission root: deviceA grants deviceB the right to ASK for
/// room:member over any room (a path-less room scope is the kind's whole root, so the
/// token's scope covers any room path a request might carry). Presented in
/// manage-request-frame's own token field; the gated frame vector below embeds exactly
/// these wire bytes.
export function manageRequestTokenVector(
ctx: Readonly<TokenVectorContext>,
): Vector {
const vector = (name: string, message: JsonWire): Vector => ({
name,
message,
wire_hex: ctx.wireHex(message),
});
return vector("capability_token_v1_manage_request_root", [
hex(ctx.protectedHeaderHex(ctx.deviceA)),
{},
hex(
ctx.wireHex({
"token-id": hex("08".repeat(ctx.tokenIdByteLength)),
issuer: ctx.deviceA,
"issuer-key": { alg: -7, "public-key": ctx.publicKeyEs256A },
bearer: ctx.deviceB,
capability: "manage:request",
scope: { kind: "room" },
expires: 1893456000000,
"requests-capability": "room:member",
}),
),
ctx.signatureFiller,
]);
}
16 changes: 16 additions & 0 deletions conformance/tokens.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@
],
"wire_hex": "845828a2613126613458202222222222222222222222222222222222222222222222222222222222222222a0590445aa6573636f7065a2646b696e6466666f6c6465726470617468652f776f726b6662656172657258203333333333333333333333333333333333333333333333333333333333333333666973737565725820222222222222222222222222222222222222222222222222222222222222222267657870697265731b000001b18314880068746f6b656e2d696450070707070707070707070707070707076a6361706162696c6974796c6d616e6167653a6772616e746a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd6d617574686f72697365642d62795902ef845828a2613126613458201111111111111111111111111111111111111111111111111111111111111111a059027ea96573636f7065a2646b696e6466666f6c6465726470617468652f776f726b6662656172657258202222222222222222222222222222222222222222222222222222222222222222666973737565725820111111111111111111111111111111111111111111111111111111111111111167657870697265731b000001b8dac5b40068746f6b656e2d696450050505050505050505050505050505056a6361706162696c6974796c6d616e6167653a6772616e746a636f6e646974696f6e7359014681a2646b696e64636e6f74676f706572616e64a3646b696e6463616e64646c656674a4626f70626571646b696e6467636f6d70617265646c656674a3646b696e646864656c65676174656673797374656d756772616e7465642d6361706162696c6974792d6973677061796c6f616468657865633a707479657269676874a2646b696e646e626f6f6c65616e4c69746572616c6576616c7565f5657269676874a4626f70626571646b696e6467636f6d70617265646c656674a3646b696e646864656c65676174656673797374656d6a6772616e7465652d6973677061796c6f6164784032323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232657269676874a2646b696e646e626f6f6c65616e4c69746572616c6576616c7565f56a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb7564656c65676174696f6e732d72656d61696e696e67015840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff716772616e74732d6361706162696c69747968657865633a7074797564656c65676174696f6e732d72656d61696e696e67005840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
},
{
"name": "capability_token_v1_manage_request_root",
"message": [
{
"hex": "a2613126613458201111111111111111111111111111111111111111111111111111111111111111"
},
{},
{
"hex": "a86573636f7065a1646b696e6464726f6f6d6662656172657258202222222222222222222222222222222222222222222222222222222222222222666973737565725820111111111111111111111111111111111111111111111111111111111111111167657870697265731b000001b8dac5b40068746f6b656e2d696450080808080808080808080808080808086a6361706162696c6974796e6d616e6167653a726571756573746a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb7372657175657374732d6361706162696c6974796b726f6f6d3a6d656d626572"
},
{
"hex": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
}
],
"wire_hex": "845828a2613126613458201111111111111111111111111111111111111111111111111111111111111111a0590128a86573636f7065a1646b696e6464726f6f6d6662656172657258202222222222222222222222222222222222222222222222222222222222222222666973737565725820111111111111111111111111111111111111111111111111111111111111111167657870697265731b000001b8dac5b40068746f6b656e2d696450080808080808080808080808080808086a6361706162696c6974796e6d616e6167653a726571756573746a6973737565722d6b6579a263616c67266a7075626c69632d6b6579584104aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb7372657175657374732d6361706162696c6974796b726f6f6d3a6d656d6265725840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
},
{
"name": "capability_token_v1_room_member_root_grant",
"message": [
Expand Down
2 changes: 2 additions & 0 deletions rust/crates/wire-mesh-core/src/domain/room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ mod tests {
delegations_remaining: None,
authorised_by: None,
grants_capability: None,
requests_capability: None,
extra: CanonicalMap::new(),
};
issuer
Expand Down Expand Up @@ -792,6 +793,7 @@ mod tests {
delegations_remaining: None,
authorised_by: None,
grants_capability: None,
requests_capability: None,
extra: CanonicalMap::new(),
};
let token = owner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ mod tests {
delegations_remaining: None,
authorised_by: None,
grants_capability: None,
requests_capability: None,
extra: CanonicalMap::new(),
};
issuer
Expand Down Expand Up @@ -345,6 +346,7 @@ mod tests {
delegations_remaining: None,
authorised_by: None,
grants_capability: None,
requests_capability: None,
extra: CanonicalMap::new(),
};
let token = owner
Expand Down
29 changes: 29 additions & 0 deletions rust/crates/wire-mesh-core/src/domain/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ mod tests {
delegations_remaining: None,
authorised_by: None,
grants_capability: None,
requests_capability: None,
extra: CanonicalMap::new(),
}
}
Expand Down Expand Up @@ -675,6 +676,7 @@ mod tests {
delegations_remaining: None,
authorised_by: None,
grants_capability: None,
requests_capability: None,
extra: CanonicalMap::new(),
}
}
Expand Down Expand Up @@ -2060,4 +2062,31 @@ mod tests {
other => panic!("expected Valid, got {other:?}"),
}
}

#[tokio::test]
async fn a_manage_request_token_verifies_as_an_ordinary_capability_token() {
// wire-mesh#324: a request-permission is just a capability token carrying the
// manage:request verb and optionally naming the verb it may ask for; the generic
// verifier treats it like any other, and the receiving-side gate (where it
// exists) is the one that reads requests-capability.
let issuer = NodeIdentity::generate_ed25519();
let bearer = NodeIdentity::generate_ed25519();
let mut claims = claims_for(&issuer, *bearer.device_id());
claims.capability = CapabilityVerb("manage:request".to_owned());
claims.requests_capability = Some(CapabilityVerb("room:member".to_owned()));
claims.scope = CapabilityScope {
kind: "room".to_owned(),
path: None,
};
let token = mint(&issuer, &claims).await;
match verify(&issuer, &RevocationView::new(), &token).await {
TokenVerdict::Valid { claims, .. } => {
assert_eq!(
claims.requests_capability.map(|v| v.0.clone()),
Some("room:member".to_owned())
);
}
other => panic!("expected Valid, got {other:?}"),
}
}
}
14 changes: 14 additions & 0 deletions rust/crates/wire-mesh-wire/src/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ pub struct TokenClaims {
/// On a manage:grant token: the single verb it authorises minting.
/// Absent means any verb within the token's own scope.
pub grants_capability: Option<CapabilityVerb>,
/// On a manage:request token (wire-mesh#324): the single verb it
/// authorises requesting. Absent means any verb within the token's own
/// scope.
pub requests_capability: Option<CapabilityVerb>,
pub extra: CanonicalMap<String, CborValue>,
}

Expand Down Expand Up @@ -566,6 +570,9 @@ impl Encode<()> for TokenClaims {
if let Some(grants) = &self.grants_capability {
builder.push("grants-capability", &grants.0.as_str());
}
if let Some(requests) = &self.requests_capability {
builder.push("requests-capability", &requests.0.as_str());
}
for (key, value) in self.extra.iter() {
let mut value_buf = Vec::new();
let mut value_enc = Encoder::new(&mut value_buf);
Expand Down Expand Up @@ -600,6 +607,7 @@ pub(crate) fn token_claims_from(d: &mut Decoder<'_>) -> Result<TokenClaims, Deco
let mut delegations_remaining: Option<u64> = None;
let mut authorised_by: Option<Vec<u8>> = None;
let mut grants_capability: Option<String> = None;
let mut requests_capability: Option<String> = None;
let mut extra = CanonicalMap::new();
while let Some(key) = map.next_key(d)? {
match key {
Expand All @@ -623,6 +631,9 @@ pub(crate) fn token_claims_from(d: &mut Decoder<'_>) -> Result<TokenClaims, Deco
"grants-capability" => {
strict::set_once(&mut grants_capability, strict::text_value(d)?)?
}
"requests-capability" => {
strict::set_once(&mut requests_capability, strict::text_value(d)?)?
}
other => {
let value = CborValue::decode_strict(d)?;
extra.insert(other.to_owned(), value)?;
Expand All @@ -644,6 +655,7 @@ pub(crate) fn token_claims_from(d: &mut Decoder<'_>) -> Result<TokenClaims, Deco
delegations_remaining,
authorised_by,
grants_capability: grants_capability.map(CapabilityVerb),
requests_capability: requests_capability.map(CapabilityVerb),
extra,
})
}
Expand Down Expand Up @@ -898,6 +910,7 @@ mod tests {
delegations_remaining: None,
authorised_by: None,
grants_capability: None,
requests_capability: None,
extra,
};
let bytes = claims.encode_to_vec();
Expand Down Expand Up @@ -936,6 +949,7 @@ mod tests {
authorised_by: Some(vec![5; 48]),
grants_capability: Some(CapabilityVerb("exec:pty".to_owned())),
extra: CanonicalMap::new(),
requests_capability: None,
};
let bytes = claims.encode_to_vec();
let decoded = TokenClaims::decode_bytes(&bytes).expect("decode");
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.
; 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.
; The right to request a capability is itself a permission (wire-mesh#324): `manage:request`, a grantable, condition-bearing token presented in manage-request-frame's own `token` field. The gate is OPTIONAL per verb and scope, on the receiving side: open by default, so a stranger's first contact (a first DM is a request with no token behind it) still reaches the receiver's own decision, and required exactly where a receiver opts in. This gate protects only what is reachable through an earlier relationship: a requester can only present a request-permission someone granted it, so it constrains known parties and pre-authorises a class of requesters without a human prompt; it does not close the stranger case, and a receiver that wants to hear from strangers leaves the gate open for them. Re-granting a request-permission goes through manage:grant like any other capability (`grants-capability: "manage:request"`); its own conditions (subject-mode or ordinary) bind exactly as tokens.cddl defines.
$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
Loading
Loading