wire-mesh-core 3.7.2 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/byte-stream-connection.d.cts +2 -2
- package/dist/adapters/byte-stream-connection.d.mts +2 -2
- package/dist/adapters/frame-codec.d.cts +1 -1
- package/dist/adapters/frame-codec.d.mts +1 -1
- package/dist/adapters/node-identity.d.cts +2 -2
- package/dist/adapters/node-identity.d.mts +2 -2
- package/dist/adapters/tcp-transport.d.cts +1 -1
- package/dist/adapters/tcp-transport.d.mts +1 -1
- package/dist/adapters/threshold-identity.d.cts +2 -2
- package/dist/adapters/threshold-identity.d.mts +2 -2
- package/dist/adapters/threshold-wasm.d.cts +1 -1
- package/dist/adapters/threshold-wasm.d.mts +1 -1
- package/dist/adapters/tls-transport.d.cts +1 -1
- package/dist/adapters/tls-transport.d.mts +1 -1
- package/dist/domain/advert-extension-policy.d.cts +1 -1
- package/dist/domain/advert-extension-policy.d.mts +1 -1
- package/dist/domain/bulk.d.cts +2 -2
- package/dist/domain/bulk.d.mts +2 -2
- package/dist/domain/capability-grant.cjs +4 -4
- package/dist/domain/capability-grant.d.cts +3 -3
- package/dist/domain/capability-grant.d.mts +3 -3
- package/dist/domain/capability-grant.mjs +2 -2
- package/dist/domain/capability-request.cjs +66 -6
- package/dist/domain/capability-request.d.cts +21 -3
- package/dist/domain/capability-request.d.mts +21 -3
- package/dist/domain/capability-request.mjs +64 -6
- package/dist/domain/coordinator-election.d.cts +1 -1
- package/dist/domain/coordinator-election.d.mts +1 -1
- package/dist/domain/data-sync.d.cts +2 -2
- package/dist/domain/data-sync.d.mts +2 -2
- package/dist/domain/device-id.d.cts +1 -1
- package/dist/domain/device-id.d.mts +1 -1
- package/dist/domain/direct-manage-request.d.cts +2 -2
- package/dist/domain/direct-manage-request.d.mts +2 -2
- package/dist/domain/gossip-expansion.d.cts +1 -1
- package/dist/domain/gossip-expansion.d.mts +1 -1
- package/dist/domain/grant-candidates.cjs +2 -2
- package/dist/domain/grant-candidates.d.cts +1 -1
- package/dist/domain/grant-candidates.d.mts +1 -1
- package/dist/domain/grant-candidates.mjs +1 -1
- package/dist/domain/handshake.d.cts +1 -1
- package/dist/domain/handshake.d.mts +1 -1
- package/dist/domain/hub-mailbox.d.cts +1 -1
- package/dist/domain/hub-mailbox.d.mts +1 -1
- package/dist/domain/mesh-session.cjs +3 -3
- package/dist/domain/mesh-session.d.cts +3 -3
- package/dist/domain/mesh-session.d.mts +3 -3
- package/dist/domain/mesh-session.mjs +2 -2
- package/dist/domain/notice-board.d.cts +3 -3
- package/dist/domain/notice-board.d.mts +3 -3
- package/dist/domain/path-trace.d.cts +1 -1
- package/dist/domain/path-trace.d.mts +1 -1
- package/dist/domain/peer-advert.cjs +2 -2
- package/dist/domain/peer-advert.d.cts +2 -2
- package/dist/domain/peer-advert.d.mts +2 -2
- package/dist/domain/peer-advert.mjs +1 -1
- package/dist/domain/relay-advert.d.cts +1 -1
- package/dist/domain/relay-advert.d.mts +1 -1
- package/dist/domain/relay-hub.d.cts +2 -2
- package/dist/domain/relay-hub.d.mts +2 -2
- package/dist/domain/relay-use-gate.d.cts +2 -2
- package/dist/domain/relay-use-gate.d.mts +2 -2
- package/dist/domain/revocation-view.cjs +2 -2
- package/dist/domain/revocation-view.d.cts +2 -2
- package/dist/domain/revocation-view.d.mts +2 -2
- package/dist/domain/revocation-view.mjs +1 -1
- package/dist/domain/room-rekey.d.cts +3 -3
- package/dist/domain/room-rekey.d.mts +3 -3
- package/dist/domain/room-token-verification.cjs +2 -2
- package/dist/domain/room-token-verification.d.cts +1 -1
- package/dist/domain/room-token-verification.d.mts +1 -1
- package/dist/domain/room-token-verification.mjs +1 -1
- package/dist/domain/room.d.cts +4 -4
- package/dist/domain/room.d.mts +4 -4
- package/dist/domain/shard-manifest.d.cts +1 -1
- package/dist/domain/shard-manifest.d.mts +1 -1
- package/dist/domain/threshold-subject.cjs +2 -2
- package/dist/domain/threshold-subject.mjs +1 -1
- package/dist/domain/token-predicates.cjs +184 -0
- package/dist/domain/token-predicates.d.cts +2 -0
- package/dist/domain/token-predicates.d.mts +2 -0
- package/dist/domain/token-predicates.mjs +173 -0
- package/dist/{token-scope-CxHXTT3u.cjs → domain/token-scope.cjs} +3 -12
- package/dist/domain/token-scope.d.cts +11 -0
- package/dist/domain/token-scope.d.mts +11 -0
- package/dist/{token-scope-Z4bmci4M.mjs → domain/token-scope.mjs} +1 -1
- package/dist/domain/tokens.cjs +466 -7
- package/dist/domain/tokens.d.cts +131 -2
- package/dist/domain/tokens.d.mts +131 -2
- package/dist/domain/tokens.mjs +459 -2
- package/dist/domain/topology-snapshot.d.cts +1 -1
- package/dist/domain/topology-snapshot.d.mts +1 -1
- package/dist/domain/topology.d.cts +1 -1
- package/dist/domain/topology.d.mts +1 -1
- package/dist/domain/webrtc-signaling.cjs +2 -2
- package/dist/domain/webrtc-signaling.d.cts +2 -2
- package/dist/domain/webrtc-signaling.d.mts +2 -2
- package/dist/domain/webrtc-signaling.mjs +1 -1
- package/dist/generated/protocol.cjs +4 -1
- package/dist/generated/protocol.d.cts +1 -1
- package/dist/generated/protocol.d.mts +1 -1
- package/dist/generated/protocol.mjs +4 -1
- package/dist/{identity-DVCNuFLG.d.cts → identity-DJbr96J3.d.cts} +1 -1
- package/dist/{identity-DcaYgjXT.d.mts → identity-ViYdZrIU.d.mts} +1 -1
- package/dist/ports/identity.d.cts +1 -1
- package/dist/ports/identity.d.mts +1 -1
- package/dist/ports/transport.d.cts +1 -1
- package/dist/ports/transport.d.mts +1 -1
- package/dist/{protocol-DCDae3z4.d.cts → protocol-BgR1e_NI.d.cts} +3 -0
- package/dist/{protocol-DCDae3z4.d.mts → protocol-BgR1e_NI.d.mts} +3 -0
- package/dist/{room-token-verification-CkGnFuQc.d.mts → room-token-verification-BpQ6yJtj.d.cts} +2 -2
- package/dist/{room-token-verification-PSv4eQ1x.d.cts → room-token-verification-CTQSihUO.d.mts} +2 -2
- package/dist/token-predicates-C-aIe1DT.d.cts +72 -0
- package/dist/token-predicates-DqvyS7Y8.d.mts +72 -0
- package/dist/{transport-DIXjoQKn.d.cts → transport-DP3hdehk.d.cts} +1 -1
- package/dist/{transport-B0GywEzG.d.mts → transport-Dv_LTCsF.d.mts} +1 -1
- package/package.json +9 -1
- package/dist/tokens-CBiSYuzp.d.mts +0 -126
- package/dist/tokens-CMdWiRBb.mjs +0 -459
- package/dist/tokens-DSG3SDr8.cjs +0 -494
- package/dist/tokens-DqFu7iMX.d.cts +0 -126
package/dist/tokens-DSG3SDr8.cjs
DELETED
|
@@ -1,494 +0,0 @@
|
|
|
1
|
-
const require_generated_protocol = require("./generated/protocol.cjs");
|
|
2
|
-
const require_token_scope = require("./token-scope-CxHXTT3u.cjs");
|
|
3
|
-
let cbor2 = require("cbor2");
|
|
4
|
-
let zod = require("zod");
|
|
5
|
-
let trilean = require("trilean");
|
|
6
|
-
//#region src/domain/token-predicates.ts
|
|
7
|
-
/** The wire shape of `token-claims.conditions` once CBOR-decoded: trilean's own PredicateNodeSchema is the single source of truth for what a condition entry may contain, re-validated here rather than trusted from a CDDL-generated shadow schema (see tokens.cddl's own comment on why `conditions` is an opaque bstr, not a native CDDL type) -- a token from an untrusted peer must pass trilean's real schema before any of its conditions are evaluated. Explicitly annotated: trilean's PredicateNodeSchema is a deeply recursive z.lazy() type whose inferred shape is too large for tsdown's declaration-file generator to serialise (TS7056) without this. */
|
|
8
|
-
const conditionsListSchema = zod.z.array(trilean.PredicateNodeSchema);
|
|
9
|
-
/**
|
|
10
|
-
* tokens.cddl's mandated core predicate-op vocabulary (issue #85): the five narrowing checks a verifier previously enforced as five hardcoded `if`s, now expressed as `delegate` systems every conforming verifier registers a handler for. Order matches the historical check order in verifyTokenChain/checkNarrowing, preserved so a caller mapping a failing system to its own reason vocabulary (MintRefusalReason's five distinct values; verifyTokenChain's single collapsed "delegation_exceeds_parent") sees the same check run first that always ran first.
|
|
11
|
-
*/
|
|
12
|
-
const BEARER_IS = "bearer-is";
|
|
13
|
-
const EXPIRES_AT = "expires-at";
|
|
14
|
-
const SCOPE_NARROWS = "scope-narrows";
|
|
15
|
-
const CAPABILITY_IS = "capability-is";
|
|
16
|
-
const DEPTH_REMAINING = "depth-remaining";
|
|
17
|
-
const NARROWING_SYSTEMS = [
|
|
18
|
-
BEARER_IS,
|
|
19
|
-
EXPIRES_AT,
|
|
20
|
-
SCOPE_NARROWS,
|
|
21
|
-
CAPABILITY_IS,
|
|
22
|
-
DEPTH_REMAINING
|
|
23
|
-
];
|
|
24
|
-
function isNarrowingSystem(system) {
|
|
25
|
-
return system === "bearer-is" || system === "expires-at" || system === "scope-narrows" || system === "capability-is" || system === "depth-remaining";
|
|
26
|
-
}
|
|
27
|
-
function booleanFound(value) {
|
|
28
|
-
return {
|
|
29
|
-
found: true,
|
|
30
|
-
value: {
|
|
31
|
-
kind: "boolean",
|
|
32
|
-
value
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/** The five mandatory narrowing ops, each reading only from TokenPredicateContext -- never the delegate node's own payload, since every fact each op needs (which parent, which candidate) already travels via the context the caller constructs per evaluation, not via wire-encoded payload data. A context with no parentClaims (narrowing is being checked with nothing to narrow against) resolves every op to `false` rather than throwing -- fail-closed, matching how an indeterminate result already refuses a token. */
|
|
37
|
-
const narrowingHandlers = {
|
|
38
|
-
[BEARER_IS]: (_payload, context) => {
|
|
39
|
-
if (context.parentClaims === void 0) return booleanFound(false);
|
|
40
|
-
return booleanFound(require_token_scope.bytesEqual(context.parentClaims.bearer, context.childIssuer));
|
|
41
|
-
},
|
|
42
|
-
[EXPIRES_AT]: (_payload, context) => {
|
|
43
|
-
if (context.parentClaims === void 0) return booleanFound(false);
|
|
44
|
-
return booleanFound(context.candidate.expires <= context.parentClaims.expires);
|
|
45
|
-
},
|
|
46
|
-
[SCOPE_NARROWS]: (_payload, context) => {
|
|
47
|
-
if (context.parentClaims === void 0) return booleanFound(false);
|
|
48
|
-
return booleanFound(require_token_scope.scopeNarrows(context.parentClaims.scope, context.candidate.scope));
|
|
49
|
-
},
|
|
50
|
-
[CAPABILITY_IS]: (_payload, context) => {
|
|
51
|
-
if (context.parentClaims === void 0) return booleanFound(false);
|
|
52
|
-
return booleanFound(context.parentClaims.capability === context.candidate.capability);
|
|
53
|
-
},
|
|
54
|
-
[DEPTH_REMAINING]: (_payload, context) => {
|
|
55
|
-
if (context.parentClaims === void 0) return booleanFound(false);
|
|
56
|
-
const parentRemaining = context.parentClaims["delegations-remaining"];
|
|
57
|
-
if (parentRemaining === void 0) return booleanFound(true);
|
|
58
|
-
return booleanFound(context.candidate.delegationsRemaining !== void 0 && context.candidate.delegationsRemaining < parentRemaining);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
/** `{kind:"compare", op:"eq", left:{kind:"delegate", system, payload:null}, right:{kind:"booleanLiteral", value:true}}` -- delegate is an ExpressionNode, not itself a PredicateNode (trilean's PredicateNodeSchema has no `delegate` member), so every predicate op is expressed this way: resolve the delegate to a boolean ComputedValue, then compare it against the literal `true`. */
|
|
62
|
-
function delegateIsTrue(system) {
|
|
63
|
-
return {
|
|
64
|
-
kind: "compare",
|
|
65
|
-
op: "eq",
|
|
66
|
-
left: {
|
|
67
|
-
kind: "delegate",
|
|
68
|
-
system,
|
|
69
|
-
payload: null
|
|
70
|
-
},
|
|
71
|
-
right: {
|
|
72
|
-
kind: "booleanLiteral",
|
|
73
|
-
value: true
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
/** trilean's three required resolvers are never exercised by any node this module constructs (narrowing/conditions nodes are built entirely from `compare`+`delegate`+`booleanLiteral`, never `reference`/`lookup`/`fold`/`some`/`every`) -- these stubs exist only because `Resolvers` requires them structurally. */
|
|
78
|
-
const unusedResolveValue = async () => Promise.resolve({ found: false });
|
|
79
|
-
const unusedResolveLookup = async () => Promise.resolve({ found: false });
|
|
80
|
-
const unusedResolveCollection = async () => Promise.resolve([]);
|
|
81
|
-
/**
|
|
82
|
-
* Evaluates the five mandatory narrowing predicates against parentClaims/candidate, in their historical check order, short-circuiting on and returning the first that does not hold. Returns undefined when every check holds. This is the single mechanism both mintCapabilityToken's own pre-mint check and verifyTokenChain's own delegation-chain walk call -- the two can never silently drift into different ideas of what "narrows" means, because there is now exactly one implementation of each check, not two hardcoded copies.
|
|
83
|
-
*/
|
|
84
|
-
async function evaluateNarrowing(parentClaims, childIssuer, candidate) {
|
|
85
|
-
const context = {
|
|
86
|
-
childIssuer,
|
|
87
|
-
candidate,
|
|
88
|
-
parentClaims
|
|
89
|
-
};
|
|
90
|
-
const resolvers = {
|
|
91
|
-
resolveValue: unusedResolveValue,
|
|
92
|
-
resolveLookup: unusedResolveLookup,
|
|
93
|
-
resolveCollection: unusedResolveCollection,
|
|
94
|
-
resolveDelegate: async (system, payload) => {
|
|
95
|
-
if (!isNarrowingSystem(system)) return { found: false };
|
|
96
|
-
return narrowingHandlers[system](payload, context);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
for (const system of NARROWING_SYSTEMS) {
|
|
100
|
-
const result = await (0, trilean.evaluatePredicate)(delegateIsTrue(system), context, resolvers);
|
|
101
|
-
if (result.status !== "definite" || !result.value) return system;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Evaluates token-claims' own `conditions` field (issue #85): additional, issuer-chosen predicates strictly additive to the five mandatory narrowing checks above, which are computed structurally (from a delegated token's own claims and its parent's) and never read from this field -- the five built-ins are inherently about a parent/child relationship, so they are not meaningfully reusable inside a bare, parent-less conditions evaluation, unlike a genuine domain-specific condition (a message TTL, #84's future revoke-authorization check) which reasons about the presented token's own claims directly. `extraHandlers` is the registration point for those: none exist yet, so a `conditions` entry naming any system with no registered handler resolves `{found: false}` -- indeterminate, fail-closed. Absent `nodes` (an empty array, the caller's own signal for "no conditions field present") is trivially `{ok: true}` -- no extra restrictions, exactly today's behaviour. Every entry MUST evaluate to a definite `true`; the loop short-circuits on the first indeterminate or false result.
|
|
106
|
-
*/
|
|
107
|
-
async function evaluateConditions(nodes, claims, clock, extraHandlers = {}) {
|
|
108
|
-
const context = {
|
|
109
|
-
clock,
|
|
110
|
-
claims
|
|
111
|
-
};
|
|
112
|
-
const resolvers = {
|
|
113
|
-
resolveValue: unusedResolveValue,
|
|
114
|
-
resolveLookup: unusedResolveLookup,
|
|
115
|
-
resolveCollection: unusedResolveCollection,
|
|
116
|
-
resolveDelegate: async (system, payload) => {
|
|
117
|
-
const handler = Object.hasOwn(extraHandlers, system) ? extraHandlers[system] : void 0;
|
|
118
|
-
if (handler === void 0) return { found: false };
|
|
119
|
-
try {
|
|
120
|
-
return await handler(payload, context);
|
|
121
|
-
} catch {
|
|
122
|
-
return { found: false };
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
for (const node of nodes) {
|
|
127
|
-
const result = await (0, trilean.evaluatePredicate)(node, context, resolvers);
|
|
128
|
-
if (result.status !== "definite" || !result.value) return {
|
|
129
|
-
ok: false,
|
|
130
|
-
reason: "not_satisfied"
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
return { ok: true };
|
|
134
|
-
}
|
|
135
|
-
//#endregion
|
|
136
|
-
//#region src/domain/tokens.ts
|
|
137
|
-
/** RFC 9052 §4.4 Sig_structure for a COSE_Sign1 with no external AAD: ["Signature1", protected, external_aad, payload]. Exported (not just used internally by verifyTokenChain) so any other self-certifying COSE_Sign1 construction in this codebase -- e.g. threshold-subject.ts's own to-be-signed bytes for a FROST-signed capability-token/revocation-entry/handle-record/room-notice -- reuses this single construction rather than a second hand-rolled one. */
|
|
138
|
-
function sig1ToBeSigned(protectedHeader, payload) {
|
|
139
|
-
return (0, cbor2.encode)([
|
|
140
|
-
"Signature1",
|
|
141
|
-
protectedHeader,
|
|
142
|
-
/* @__PURE__ */ new Uint8Array(0),
|
|
143
|
-
payload
|
|
144
|
-
], cbor2.cdeEncodeOptions);
|
|
145
|
-
}
|
|
146
|
-
/** Normalises cbor2's encode() (and any other Uint8Array<ArrayBufferLike>-typed construction) to a fresh, non-shared, whole-buffer Uint8Array<ArrayBuffer> -- what the generated schemas' concrete-typed fields require. */
|
|
147
|
-
function buf(bytes) {
|
|
148
|
-
return Uint8Array.from(bytes);
|
|
149
|
-
}
|
|
150
|
-
function encodeBuf(value) {
|
|
151
|
-
return buf((0, cbor2.encode)(value, cbor2.cdeEncodeOptions));
|
|
152
|
-
}
|
|
153
|
-
/** The COSE protected header every capability-token/revocation-entry envelope in this codebase actually signs over: label 1 (alg) and label 4 (kid, the issuer's own device-id) -- matching the frozen conformance vectors, not the empty header a token merely needs to verify against itself. */
|
|
154
|
-
function protectedHeaderFor(identity) {
|
|
155
|
-
return encodeBuf({
|
|
156
|
-
1: identity.identityKey.alg,
|
|
157
|
-
4: identity.deviceId
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
/** Decodes and validates a parent token's own claims from its raw CapabilityToken tuple -- the same decode `verifyTokenChain` performs on `claims.parent`, extracted here so mint can check narrowing against a parent's real claims without duplicating the CBOR/schema plumbing. Returns undefined for anything that doesn't parse; the caller turns that into its own refusal reason since "malformed" means something different at mint time than at verify time. */
|
|
161
|
-
function decodeTokenClaims(token) {
|
|
162
|
-
const [, , payload] = token;
|
|
163
|
-
if (payload === null) return void 0;
|
|
164
|
-
let decoded;
|
|
165
|
-
try {
|
|
166
|
-
decoded = (0, cbor2.decode)(payload, cbor2.cdeDecodeOptions);
|
|
167
|
-
} catch {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
const result = require_generated_protocol.tokenClaimsSchema.safeParse(decoded);
|
|
171
|
-
return result.success ? result.data : void 0;
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Verifies one capability token per tokens.cddl's own documented rules: the token is a well-formed COSE_Sign1 whose signature actually verifies against its own embedded issuer-key, that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id -- no shared secret needed to check this), the token is currently valid (not expired, not before not-before, not revoked by its own issuer), and -- recursively -- any parent delegation narrows rather than widens across all three axes of authority: the parent's bearer must be this token's issuer (the delegation chain is unbroken), this token's expiry must not exceed its parent's, and this token's scope must narrow its parent's (same kind; equal-or-descendant path when the parent carries one) with an identical capability verb (the capability-verb grammar has no sub-verb relation, so a different verb is a different authority, not a narrower one). Undecodable payload bytes return "malformed" and undecodable parent bytes return "parent_invalid" -- hostile input produces a verdict, never a throw.
|
|
175
|
-
*/
|
|
176
|
-
async function verifyCapabilityToken(token, options) {
|
|
177
|
-
const verdict = await verifyTokenChain(token, {
|
|
178
|
-
identity: options.identity,
|
|
179
|
-
clock: options.clock,
|
|
180
|
-
revocation: options.revocation,
|
|
181
|
-
...options.extraPredicateResolvers !== void 0 ? { extraPredicateResolvers: options.extraPredicateResolvers } : {}
|
|
182
|
-
});
|
|
183
|
-
if (!verdict.ok) return verdict;
|
|
184
|
-
if (options.expectedBearer !== void 0 && !require_token_scope.bytesEqual(verdict.claims.bearer, options.expectedBearer)) return {
|
|
185
|
-
ok: false,
|
|
186
|
-
reason: "bearer_mismatch"
|
|
187
|
-
};
|
|
188
|
-
return verdict;
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Does one recorded revocation-claims entry actually revoke targetClaims, per management.cddl's own additive obligation? Valid when EITHER the entry's own issuer equals the target token's own issuer (the original, unconditional rule -- only a token's own issuer may revoke it), OR the entry carries an `authorization` that independently verifies as an ordinary capability-token -- with `expectedBearer` set to THIS entry's own `issuer`, proving the authorization was actually granted to the party submitting this revocation, not merely referenced from someone else's -- whose own `capability` is `manage:revoke` (spec/registry/core-capabilities.md) and whose own `scope` narrows targetClaims' scope. An authorization that fails any part of this (wrong capability, scope doesn't narrow, fails ordinary verification -- expired, revoked, bad signature, bearer mismatch) makes the entry no more valid than if `authorization` were absent; it never falls back to weakening the issuer-match rule.
|
|
192
|
-
*/
|
|
193
|
-
async function revocationEntryGrantsRevoke(entry, targetClaims, options) {
|
|
194
|
-
if (require_token_scope.bytesEqual(entry.issuer, targetClaims.issuer)) return true;
|
|
195
|
-
if (entry.authorization === void 0) return false;
|
|
196
|
-
let decodedAuthorization;
|
|
197
|
-
try {
|
|
198
|
-
decodedAuthorization = (0, cbor2.decode)(entry.authorization, cbor2.cdeDecodeOptions);
|
|
199
|
-
} catch {
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
const authorizationResult = require_generated_protocol.capabilityTokenSchema.safeParse(decodedAuthorization);
|
|
203
|
-
if (!authorizationResult.success) return false;
|
|
204
|
-
const authorizationVerdict = await verifyCapabilityToken(authorizationResult.data, {
|
|
205
|
-
...options,
|
|
206
|
-
expectedBearer: entry.issuer
|
|
207
|
-
});
|
|
208
|
-
return authorizationVerdict.ok && authorizationVerdict.claims.capability === "manage:revoke" && require_token_scope.scopeNarrows(authorizationVerdict.claims.scope, targetClaims.scope);
|
|
209
|
-
}
|
|
210
|
-
async function verifyTokenChain(token, options) {
|
|
211
|
-
const [protectedHeader, , payload, signature] = token;
|
|
212
|
-
if (payload === null) return {
|
|
213
|
-
ok: false,
|
|
214
|
-
reason: "malformed"
|
|
215
|
-
};
|
|
216
|
-
let decodedClaims;
|
|
217
|
-
try {
|
|
218
|
-
decodedClaims = (0, cbor2.decode)(payload, cbor2.cdeDecodeOptions);
|
|
219
|
-
} catch {
|
|
220
|
-
return {
|
|
221
|
-
ok: false,
|
|
222
|
-
reason: "malformed"
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
const claimsResult = require_generated_protocol.tokenClaimsSchema.safeParse(decodedClaims);
|
|
226
|
-
if (!claimsResult.success) return {
|
|
227
|
-
ok: false,
|
|
228
|
-
reason: "malformed"
|
|
229
|
-
};
|
|
230
|
-
const claims = claimsResult.data;
|
|
231
|
-
if (!await options.identity.verify(claims["issuer-key"], sig1ToBeSigned(protectedHeader, payload), signature)) return {
|
|
232
|
-
ok: false,
|
|
233
|
-
reason: "bad_signature"
|
|
234
|
-
};
|
|
235
|
-
const derivedIssuerId = await options.identity.deriveDeviceId(claims["issuer-key"]["public-key"]);
|
|
236
|
-
if (!require_token_scope.bytesEqual(derivedIssuerId, claims.issuer)) return {
|
|
237
|
-
ok: false,
|
|
238
|
-
reason: "wrong_issuer"
|
|
239
|
-
};
|
|
240
|
-
const now = options.clock.now();
|
|
241
|
-
if (claims.expires <= now) return {
|
|
242
|
-
ok: false,
|
|
243
|
-
reason: "expired"
|
|
244
|
-
};
|
|
245
|
-
if (claims["not-before"] !== void 0 && claims["not-before"] > now) return {
|
|
246
|
-
ok: false,
|
|
247
|
-
reason: "not_yet_valid"
|
|
248
|
-
};
|
|
249
|
-
const validUntil = claims["valid-until"];
|
|
250
|
-
if (validUntil !== void 0 && validUntil <= now) return {
|
|
251
|
-
ok: false,
|
|
252
|
-
reason: "content_expired"
|
|
253
|
-
};
|
|
254
|
-
const revocationEntries = await options.revocation.entriesFor(claims["token-id"]);
|
|
255
|
-
for (const entry of revocationEntries) if (await revocationEntryGrantsRevoke(entry, claims, options)) return {
|
|
256
|
-
ok: false,
|
|
257
|
-
reason: "revoked"
|
|
258
|
-
};
|
|
259
|
-
if (claims.conditions !== void 0) {
|
|
260
|
-
let decodedConditions;
|
|
261
|
-
try {
|
|
262
|
-
decodedConditions = (0, cbor2.decode)(claims.conditions, cbor2.cdeDecodeOptions);
|
|
263
|
-
} catch {
|
|
264
|
-
return {
|
|
265
|
-
ok: false,
|
|
266
|
-
reason: "conditions_invalid"
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
const conditionsResult = conditionsListSchema.safeParse(decodedConditions);
|
|
270
|
-
if (!conditionsResult.success) return {
|
|
271
|
-
ok: false,
|
|
272
|
-
reason: "conditions_invalid"
|
|
273
|
-
};
|
|
274
|
-
if (!(await evaluateConditions(conditionsResult.data, claims, options.clock, options.extraPredicateResolvers)).ok) return {
|
|
275
|
-
ok: false,
|
|
276
|
-
reason: "conditions_not_satisfied"
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
if (claims.parent !== void 0) {
|
|
280
|
-
let decodedParent;
|
|
281
|
-
try {
|
|
282
|
-
decodedParent = (0, cbor2.decode)(claims.parent, cbor2.cdeDecodeOptions);
|
|
283
|
-
} catch {
|
|
284
|
-
return {
|
|
285
|
-
ok: false,
|
|
286
|
-
reason: "parent_invalid"
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
const parentResult = require_generated_protocol.capabilityTokenSchema.safeParse(decodedParent);
|
|
290
|
-
if (!parentResult.success) return {
|
|
291
|
-
ok: false,
|
|
292
|
-
reason: "parent_invalid"
|
|
293
|
-
};
|
|
294
|
-
const parentVerdict = await verifyTokenChain(parentResult.data, options);
|
|
295
|
-
if (!parentVerdict.ok) return {
|
|
296
|
-
ok: false,
|
|
297
|
-
reason: "parent_invalid"
|
|
298
|
-
};
|
|
299
|
-
const candidate = {
|
|
300
|
-
capability: claims.capability,
|
|
301
|
-
scope: claims.scope,
|
|
302
|
-
expires: claims.expires,
|
|
303
|
-
...claims["delegations-remaining"] !== void 0 ? { delegationsRemaining: claims["delegations-remaining"] } : {}
|
|
304
|
-
};
|
|
305
|
-
if (await evaluateNarrowing(parentVerdict.claims, claims.issuer, candidate) !== void 0) return {
|
|
306
|
-
ok: false,
|
|
307
|
-
reason: "delegation_exceeds_parent"
|
|
308
|
-
};
|
|
309
|
-
return {
|
|
310
|
-
ok: true,
|
|
311
|
-
claims,
|
|
312
|
-
rootIssuer: parentVerdict.rootIssuer,
|
|
313
|
-
rootIssuerKey: parentVerdict.rootIssuerKey,
|
|
314
|
-
depth: parentVerdict.depth + 1
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
return {
|
|
318
|
-
ok: true,
|
|
319
|
-
claims,
|
|
320
|
-
rootIssuer: claims.issuer,
|
|
321
|
-
rootIssuerKey: claims["issuer-key"],
|
|
322
|
-
depth: 0
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Verifies one gossiped revocation-entry (management.cddl): a well-formed COSE_Sign1 whose signature verifies against its own embedded issuer-key, where that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id). A verifier that ingests a revocation-announce frame runs each entry through this before recording it in its revocation view; entries failing here are dropped, not stored. The issuer-match against a specific token's own issuer (only a token's own issuer may revoke it) is deliberately NOT checked here -- it happens at lookup time in RevocationCheck, against whichever token is being verified.
|
|
327
|
-
*/
|
|
328
|
-
async function verifyRevocationEntry(entry, options) {
|
|
329
|
-
const [protectedHeader, , payload, signature] = entry;
|
|
330
|
-
if (payload === null) return {
|
|
331
|
-
ok: false,
|
|
332
|
-
reason: "malformed"
|
|
333
|
-
};
|
|
334
|
-
let decodedClaims;
|
|
335
|
-
try {
|
|
336
|
-
decodedClaims = (0, cbor2.decode)(payload, cbor2.cdeDecodeOptions);
|
|
337
|
-
} catch {
|
|
338
|
-
return {
|
|
339
|
-
ok: false,
|
|
340
|
-
reason: "malformed"
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
const claimsResult = require_generated_protocol.revocationClaimsSchema.safeParse(decodedClaims);
|
|
344
|
-
if (!claimsResult.success) return {
|
|
345
|
-
ok: false,
|
|
346
|
-
reason: "malformed"
|
|
347
|
-
};
|
|
348
|
-
const claims = claimsResult.data;
|
|
349
|
-
if (!await options.identity.verify(claims["issuer-key"], sig1ToBeSigned(protectedHeader, payload), signature)) return {
|
|
350
|
-
ok: false,
|
|
351
|
-
reason: "bad_signature"
|
|
352
|
-
};
|
|
353
|
-
const derivedIssuerId = await options.identity.deriveDeviceId(claims["issuer-key"]["public-key"]);
|
|
354
|
-
if (!require_token_scope.bytesEqual(derivedIssuerId, claims.issuer)) return {
|
|
355
|
-
ok: false,
|
|
356
|
-
reason: "wrong_issuer"
|
|
357
|
-
};
|
|
358
|
-
return {
|
|
359
|
-
ok: true,
|
|
360
|
-
claims
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
/** Maps evaluateNarrowing's own NarrowingSystem (the first predicate op that did not hold) to mintCapabilityToken's five distinct refusal reasons -- verifyTokenChain collapses every narrowing failure to a single "delegation_exceeds_parent", but mint has always reported which specific rule was violated, since a caller building the candidate itself benefits from knowing exactly what to fix. */
|
|
364
|
-
const NARROWING_SYSTEM_TO_MINT_REFUSAL = {
|
|
365
|
-
"bearer-is": "parent_bearer_mismatch",
|
|
366
|
-
"expires-at": "expires_exceeds_parent",
|
|
367
|
-
"scope-narrows": "scope_does_not_narrow",
|
|
368
|
-
"capability-is": "capability_mismatch",
|
|
369
|
-
"depth-remaining": "delegation_exceeds_parent"
|
|
370
|
-
};
|
|
371
|
-
/** The narrowing arithmetic tokens.cddl's own delegation obligations require (bearer match, expiry within the parent's, scope narrows, same capability, delegations-remaining strictly less than the parent's) -- shared between mintCapabilityToken (which additionally builds and signs the resulting token) and canGrant (a pure query with no minting side effect at all), so the two can never silently drift into two different ideas of what "narrows" means. Delegates the actual check to evaluateNarrowing (token-predicates.ts), the same generic evaluator verifyTokenChain's own delegation-chain walk uses, so mint and verify share one implementation of each of the five checks rather than two hardcoded copies. Returns the specific refusal reason, or undefined when every rule is satisfied. */
|
|
372
|
-
async function checkNarrowing(parentClaims, granterDeviceId, candidate) {
|
|
373
|
-
const failedSystem = await evaluateNarrowing(parentClaims, granterDeviceId, candidate);
|
|
374
|
-
return failedSystem === void 0 ? void 0 : NARROWING_SYSTEM_TO_MINT_REFUSAL[failedSystem];
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* A pure query: could deviceId, presenting heldToken as its own delegation authority, successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
|
|
378
|
-
*
|
|
379
|
-
* Deliberately narrower than a full mint attempt in one respect: this checks only the narrowing rules tokens.cddl's own delegation obligations require (bearer match, expiry, scope, capability, delegations-remaining), the same scope mintCapabilityToken itself checks a *parent* against -- it does not verify heldToken's own signature or revocation status, exactly as mintCapabilityToken never re-verifies its own parent's signature either. A caller that also needs heldToken's cryptographic validity confirmed calls verifyCapabilityToken separately.
|
|
380
|
-
*
|
|
381
|
-
* Async as of the predicate-list evaluator (issue #85): checkNarrowing now routes through trilean's own evaluatePredicate, which is asynchronous throughout -- there is no synchronous path through a real evaluator call, so this is a genuine, deliberate breaking change to what was previously a synchronous pure function. No existing consumer of wire-mesh-core calls canGrant (confirmed against agent-comms, the only other repo depending on this package), so there is nothing to migrate.
|
|
382
|
-
*/
|
|
383
|
-
async function canGrant(heldToken, deviceId, candidate, now) {
|
|
384
|
-
if (candidate.expires <= now) return false;
|
|
385
|
-
const heldClaims = decodeTokenClaims(heldToken);
|
|
386
|
-
if (heldClaims === void 0) return false;
|
|
387
|
-
return await checkNarrowing(heldClaims, deviceId, candidate) === void 0;
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Mints one capability token: builds token-claims from the given fields, signs it as a COSE_Sign1 under `identity`'s own key, with a protected header matching what the frozen conformance vectors actually encode (`{1: alg, 4: issuer device-id}`, not the empty header a token merely needs to verify against itself).
|
|
391
|
-
*
|
|
392
|
-
* When `parent` is given, every one of `tokens.cddl`'s own narrowing obligations is enforced here, at issuance, rather than left for the far end to discover minutes or hours later as a bare `delegation_exceeds_parent` from `verifyCapabilityToken` -- the same "fail loudly, fail early" reasoning that governs every other boundary in this codebase. An issuer minting an invalid delegation is a bug in the caller; this function refuses rather than producing a token indistinguishable from a valid one until someone else verifies it.
|
|
393
|
-
*/
|
|
394
|
-
async function mintCapabilityToken(options) {
|
|
395
|
-
if (options.expires <= options.clock.now()) return {
|
|
396
|
-
ok: false,
|
|
397
|
-
reason: "already_expired"
|
|
398
|
-
};
|
|
399
|
-
let parentBytes;
|
|
400
|
-
if (options.parent !== void 0) {
|
|
401
|
-
const parentClaims = decodeTokenClaims(options.parent);
|
|
402
|
-
if (parentClaims === void 0) return {
|
|
403
|
-
ok: false,
|
|
404
|
-
reason: "parent_malformed"
|
|
405
|
-
};
|
|
406
|
-
const refusal = await checkNarrowing(parentClaims, options.identity.deviceId, {
|
|
407
|
-
capability: options.capability,
|
|
408
|
-
scope: options.scope,
|
|
409
|
-
expires: options.expires,
|
|
410
|
-
...options.delegationsRemaining !== void 0 ? { delegationsRemaining: options.delegationsRemaining } : {}
|
|
411
|
-
});
|
|
412
|
-
if (refusal !== void 0) return {
|
|
413
|
-
ok: false,
|
|
414
|
-
reason: refusal
|
|
415
|
-
};
|
|
416
|
-
parentBytes = encodeBuf(options.parent);
|
|
417
|
-
}
|
|
418
|
-
const payload = encodeBuf({
|
|
419
|
-
"token-id": options.tokenId,
|
|
420
|
-
issuer: options.identity.deviceId,
|
|
421
|
-
"issuer-key": options.identity.identityKey,
|
|
422
|
-
bearer: options.bearer,
|
|
423
|
-
capability: options.capability,
|
|
424
|
-
scope: options.scope,
|
|
425
|
-
expires: options.expires,
|
|
426
|
-
...options.notBefore !== void 0 ? { "not-before": options.notBefore } : {},
|
|
427
|
-
...parentBytes !== void 0 ? { parent: parentBytes } : {},
|
|
428
|
-
...options.delegationsRemaining !== void 0 ? { "delegations-remaining": options.delegationsRemaining } : {},
|
|
429
|
-
...options.conditions !== void 0 ? { conditions: encodeBuf(options.conditions) } : {}
|
|
430
|
-
});
|
|
431
|
-
const protectedHeader = protectedHeaderFor(options.identity);
|
|
432
|
-
return {
|
|
433
|
-
ok: true,
|
|
434
|
-
token: [
|
|
435
|
-
protectedHeader,
|
|
436
|
-
{},
|
|
437
|
-
payload,
|
|
438
|
-
await options.identity.sign(sig1ToBeSigned(protectedHeader, payload))
|
|
439
|
-
]
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
/** Mints one revocation-entry (management.cddl): a COSE_Sign1 over revocation-claims, signed the same way mintCapabilityToken signs a token. No narrowing chain to check -- a revocation entry has no parent and cannot fail to be issuable the way a delegated token can, so this returns the entry directly rather than a verdict. */
|
|
443
|
-
async function mintRevocationEntry(options) {
|
|
444
|
-
const payload = encodeBuf({
|
|
445
|
-
"token-id": options.tokenId,
|
|
446
|
-
issuer: options.identity.deviceId,
|
|
447
|
-
"issuer-key": options.identity.identityKey,
|
|
448
|
-
"revoked-at": options.revokedAt
|
|
449
|
-
});
|
|
450
|
-
const protectedHeader = protectedHeaderFor(options.identity);
|
|
451
|
-
return [
|
|
452
|
-
protectedHeader,
|
|
453
|
-
{},
|
|
454
|
-
payload,
|
|
455
|
-
await options.identity.sign(sig1ToBeSigned(protectedHeader, payload))
|
|
456
|
-
];
|
|
457
|
-
}
|
|
458
|
-
//#endregion
|
|
459
|
-
Object.defineProperty(exports, "canGrant", {
|
|
460
|
-
enumerable: true,
|
|
461
|
-
get: function() {
|
|
462
|
-
return canGrant;
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
Object.defineProperty(exports, "mintCapabilityToken", {
|
|
466
|
-
enumerable: true,
|
|
467
|
-
get: function() {
|
|
468
|
-
return mintCapabilityToken;
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
Object.defineProperty(exports, "mintRevocationEntry", {
|
|
472
|
-
enumerable: true,
|
|
473
|
-
get: function() {
|
|
474
|
-
return mintRevocationEntry;
|
|
475
|
-
}
|
|
476
|
-
});
|
|
477
|
-
Object.defineProperty(exports, "sig1ToBeSigned", {
|
|
478
|
-
enumerable: true,
|
|
479
|
-
get: function() {
|
|
480
|
-
return sig1ToBeSigned;
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
Object.defineProperty(exports, "verifyCapabilityToken", {
|
|
484
|
-
enumerable: true,
|
|
485
|
-
get: function() {
|
|
486
|
-
return verifyCapabilityToken;
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
Object.defineProperty(exports, "verifyRevocationEntry", {
|
|
490
|
-
enumerable: true,
|
|
491
|
-
get: function() {
|
|
492
|
-
return verifyRevocationEntry;
|
|
493
|
-
}
|
|
494
|
-
});
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { B as IdentityKey, D as DeviceId, bt as RevocationEntry, en as TokenClaims, p as CapabilityToken, yt as RevocationClaims } from "./protocol-DCDae3z4.cjs";
|
|
2
|
-
import { t as IdentityPort } from "./identity-DVCNuFLG.cjs";
|
|
3
|
-
import { t as Clock } from "./clock-DiSx-WKM.cjs";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { JsonValue, PredicateNode, Resolution } from "trilean";
|
|
6
|
-
//#region src/domain/token-predicates.d.ts
|
|
7
|
-
/** Everything needed to check candidate against parent, restated from tokens.ts's own NarrowingCandidate/checkNarrowing shape rather than requiring a full TokenClaims for the not-yet-minted child -- the same reasoning mintCapabilityToken's own checkNarrowing already applies (a delegation candidate has no token-id/issuer-key/bearer of its own yet). camelCase field names, matching NarrowingCandidate's own convention, deliberately distinct from TokenClaims' hyphenated wire field names. */
|
|
8
|
-
interface NarrowingCandidate {
|
|
9
|
-
capability: TokenClaims["capability"];
|
|
10
|
-
scope: TokenClaims["scope"];
|
|
11
|
-
expires: number;
|
|
12
|
-
delegationsRemaining?: number;
|
|
13
|
-
}
|
|
14
|
-
/** A `conditions` entry's own generic evaluation context: no parent/candidate distinction, since a domain-specific condition (a message TTL, a future revoke-authorization check) reasons about the presented token's own claims directly, not a not-yet-minted delegation candidate. */
|
|
15
|
-
interface ConditionsContext {
|
|
16
|
-
readonly clock: Clock;
|
|
17
|
-
readonly claims: Readonly<TokenClaims>;
|
|
18
|
-
}
|
|
19
|
-
/** One delegate system's own handler: given the delegate node's payload and the concrete context it was invoked with, resolves to a boolean-valued Resolution. Handlers never throw for a data-quality problem -- an op that cannot make sense of its own payload/context returns `{found: false}`, which the evaluator turns into a fail-closed `indeterminate`. */
|
|
20
|
-
type TokenDelegateHandler<TContext> = (payload: JsonValue, context: TContext) => Resolution | Promise<Resolution>;
|
|
21
|
-
//#endregion
|
|
22
|
-
//#region src/domain/tokens.d.ts
|
|
23
|
-
/**
|
|
24
|
-
* The revocation view a verifier consults. Returns every recorded, already-signature-verified revocation-claims for tokenId, across every issuer that has ever submitted one -- unfiltered by the store itself. The actual verifier obligation (an entry counts against a token when its own issuer matches the token's own issuer, OR its optional `authorization` grants delegated revoke authority -- management.cddl) is checked by the caller (verifyTokenChain), not here, since that check needs the target token's own scope plus identity/clock to verify a nested authorization token, none of which a store constructed ahead of time has access to. Implementations ingest gossiped revocation-announce frames via verifyRevocationEntry (which enforces each entry's own signature and self-certification) and key the resulting claims by token-id alone -- a token-id can legitimately carry multiple recorded entries from different issuers.
|
|
25
|
-
*/
|
|
26
|
-
interface RevocationCheck {
|
|
27
|
-
entriesFor: (tokenId: Uint8Array) => Promise<readonly RevocationClaims[]>;
|
|
28
|
-
}
|
|
29
|
-
type TokenVerdictReason = "malformed" | "bad_signature" | "wrong_issuer" | "bearer_mismatch" | "expired" | "not_yet_valid" | "content_expired" | "revoked" | "delegation_exceeds_parent" | "parent_invalid" |
|
|
30
|
-
/** claims.conditions is present but its bstr fails CBOR decode, or decodes to something that is not a JSON array of valid trilean PredicateNodes -- fail-closed per CONVENTIONS.md's verifier-obligations glossary, the same treatment "malformed" already gives an undecodable payload. */
|
|
31
|
-
"conditions_invalid" |
|
|
32
|
-
/** claims.conditions decoded and validated, but at least one entry did not evaluate to a definite `true` (indeterminate or false) -- the issuer's own additional restriction was not met. */
|
|
33
|
-
"conditions_not_satisfied";
|
|
34
|
-
type TokenVerdict = {
|
|
35
|
-
ok: true;
|
|
36
|
-
claims: TokenClaims;
|
|
37
|
-
/** The device-id at the root of this token's delegation chain: its own issuer when it carries no parent, otherwise the root of its parent's chain. Lets a caller (e.g. core/room's obligation that a chain must terminate at the path's own owner, or the verifier itself for a DM) check the chain's root with one equality comparison instead of re-walking the parent chain a second time. */
|
|
38
|
-
rootIssuer: DeviceId;
|
|
39
|
-
/** The root ancestor's own self-certifying issuer-key -- already decoded during the walk (every TokenClaims carries its own issuer-key), just threaded up rather than re-derived. Lets a caller compute an ECDH shared secret against the chain's root (e.g. room.rekey's own sender, for a named room whose token chain terminates at the owner) without a separate, out-of-band way to learn that issuer's public key. */
|
|
40
|
-
rootIssuerKey: IdentityKey;
|
|
41
|
-
/** How many delegation hops this token is from its own root -- 0 for a root grant. Costs nothing extra once rootIssuer is being tracked, and makes the delegation bound observable for diagnostics. */
|
|
42
|
-
depth: number;
|
|
43
|
-
} | {
|
|
44
|
-
ok: false;
|
|
45
|
-
reason: TokenVerdictReason;
|
|
46
|
-
};
|
|
47
|
-
interface VerifyCapabilityTokenOptions {
|
|
48
|
-
identity: IdentityPort;
|
|
49
|
-
clock: Clock;
|
|
50
|
-
revocation: RevocationCheck;
|
|
51
|
-
/** When given, the token must bear this device -- the caller presenting a token to authorise itself, not someone else. */
|
|
52
|
-
expectedBearer?: DeviceId;
|
|
53
|
-
/** Registers domain-specific delegate systems a presented token's own `conditions` entries may name, beyond the five mandatory narrowing ops (which are never reachable from `conditions` -- see evaluateConditions's own doc comment). None are registered by wire-mesh-core itself; a domain (a message TTL, #84's future revoke-authorization check) supplies its own here. A `conditions` entry naming a system absent from this map is indeterminate, and therefore fails the whole token -- fail-closed, not a silent no-op. */
|
|
54
|
-
extraPredicateResolvers?: Readonly<Record<string, TokenDelegateHandler<ConditionsContext>>>;
|
|
55
|
-
}
|
|
56
|
-
/** RFC 9052 §4.4 Sig_structure for a COSE_Sign1 with no external AAD: ["Signature1", protected, external_aad, payload]. Exported (not just used internally by verifyTokenChain) so any other self-certifying COSE_Sign1 construction in this codebase -- e.g. threshold-subject.ts's own to-be-signed bytes for a FROST-signed capability-token/revocation-entry/handle-record/room-notice -- reuses this single construction rather than a second hand-rolled one. */
|
|
57
|
-
declare function sig1ToBeSigned(protectedHeader: Uint8Array, payload: Uint8Array): Uint8Array;
|
|
58
|
-
/**
|
|
59
|
-
* Verifies one capability token per tokens.cddl's own documented rules: the token is a well-formed COSE_Sign1 whose signature actually verifies against its own embedded issuer-key, that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id -- no shared secret needed to check this), the token is currently valid (not expired, not before not-before, not revoked by its own issuer), and -- recursively -- any parent delegation narrows rather than widens across all three axes of authority: the parent's bearer must be this token's issuer (the delegation chain is unbroken), this token's expiry must not exceed its parent's, and this token's scope must narrow its parent's (same kind; equal-or-descendant path when the parent carries one) with an identical capability verb (the capability-verb grammar has no sub-verb relation, so a different verb is a different authority, not a narrower one). Undecodable payload bytes return "malformed" and undecodable parent bytes return "parent_invalid" -- hostile input produces a verdict, never a throw.
|
|
60
|
-
*/
|
|
61
|
-
declare function verifyCapabilityToken(token: CapabilityToken, options: VerifyCapabilityTokenOptions): Promise<TokenVerdict>;
|
|
62
|
-
type RevocationEntryVerdictReason = "malformed" | "bad_signature" | "wrong_issuer";
|
|
63
|
-
type RevocationEntryVerdict = {
|
|
64
|
-
ok: true;
|
|
65
|
-
claims: RevocationClaims;
|
|
66
|
-
} | {
|
|
67
|
-
ok: false;
|
|
68
|
-
reason: RevocationEntryVerdictReason;
|
|
69
|
-
};
|
|
70
|
-
interface VerifyRevocationEntryOptions {
|
|
71
|
-
/** Crypto primitives only -- any IdentityPort instance can verify any entry, since everything needed to check one travels inside the entry itself. */
|
|
72
|
-
identity: IdentityPort;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Verifies one gossiped revocation-entry (management.cddl): a well-formed COSE_Sign1 whose signature verifies against its own embedded issuer-key, where that issuer-key is self-certifying (sha256(issuer-key.public-key) equals the claimed issuer device-id). A verifier that ingests a revocation-announce frame runs each entry through this before recording it in its revocation view; entries failing here are dropped, not stored. The issuer-match against a specific token's own issuer (only a token's own issuer may revoke it) is deliberately NOT checked here -- it happens at lookup time in RevocationCheck, against whichever token is being verified.
|
|
76
|
-
*/
|
|
77
|
-
declare function verifyRevocationEntry(entry: RevocationEntry, options: VerifyRevocationEntryOptions): Promise<RevocationEntryVerdict>;
|
|
78
|
-
type MintRefusalReason = "already_expired" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent";
|
|
79
|
-
type MintVerdict = {
|
|
80
|
-
ok: true;
|
|
81
|
-
token: CapabilityToken;
|
|
82
|
-
} | {
|
|
83
|
-
ok: false;
|
|
84
|
-
reason: MintRefusalReason;
|
|
85
|
-
};
|
|
86
|
-
/**
|
|
87
|
-
* A pure query: could deviceId, presenting heldToken as its own delegation authority, successfully mint a delegation matching candidate right now -- without attempting (and potentially failing) a real mint just to find out. Reuses mintCapabilityToken's own narrowing arithmetic via checkNarrowing, so the two can never silently drift into different ideas of what "narrows" means.
|
|
88
|
-
*
|
|
89
|
-
* Deliberately narrower than a full mint attempt in one respect: this checks only the narrowing rules tokens.cddl's own delegation obligations require (bearer match, expiry, scope, capability, delegations-remaining), the same scope mintCapabilityToken itself checks a *parent* against -- it does not verify heldToken's own signature or revocation status, exactly as mintCapabilityToken never re-verifies its own parent's signature either. A caller that also needs heldToken's cryptographic validity confirmed calls verifyCapabilityToken separately.
|
|
90
|
-
*
|
|
91
|
-
* Async as of the predicate-list evaluator (issue #85): checkNarrowing now routes through trilean's own evaluatePredicate, which is asynchronous throughout -- there is no synchronous path through a real evaluator call, so this is a genuine, deliberate breaking change to what was previously a synchronous pure function. No existing consumer of wire-mesh-core calls canGrant (confirmed against agent-comms, the only other repo depending on this package), so there is nothing to migrate.
|
|
92
|
-
*/
|
|
93
|
-
declare function canGrant(heldToken: CapabilityToken, deviceId: DeviceId, candidate: Readonly<NarrowingCandidate>, now: number): Promise<boolean>;
|
|
94
|
-
interface MintCapabilityTokenOptions {
|
|
95
|
-
/** The issuer -- signs the token, and supplies the self-certifying issuer/issuer-key claims. */
|
|
96
|
-
identity: IdentityPort;
|
|
97
|
-
clock: Clock;
|
|
98
|
-
tokenId: Uint8Array<ArrayBuffer>;
|
|
99
|
-
bearer: DeviceId;
|
|
100
|
-
capability: TokenClaims["capability"];
|
|
101
|
-
scope: TokenClaims["scope"];
|
|
102
|
-
expires: number;
|
|
103
|
-
notBefore?: number;
|
|
104
|
-
/** How many further delegation hops the *resulting* token itself permits below it -- unrelated to, and never a bound on, whether `identity` may mint further tokens of its own at the root level for other bearers. Those are two different facts: a token minted with `delegationsRemaining: 0` genuinely cannot itself be re-delegated (correct -- e.g. a room owner's own self-signed root grant, which should never be handed onward), but that same `0` says nothing about the issuer's own separate, ordinary authority to mint additional independent root-level grants (naming no `parent` at all) for other bearers. Root-level minting for a second bearer is never blocked by any existing token's own `delegationsRemaining`, because it uses no `parent` in the first place -- there is no narrowing check to run. Confirmed live in agent-comms' own room-membership implementation (`ExaDev/agent-comms` PR #72): each member's own join/invite grant is minted as its own independent, parent-less, root-level token precisely because the room owner's `delegationsRemaining: 0` self-grant cannot parent anything -- correct by this same reasoning, not a workaround. */
|
|
105
|
-
delegationsRemaining?: number;
|
|
106
|
-
/** The issuer's own token, when this is a delegation rather than a root grant. Its claims are checked against every narrowing rule below -- mint refuses rather than producing a token verifyCapabilityToken would reject anyway. Omit entirely for a root-level grant (including a second, independent root-level grant for a different bearer under the same capability/scope this issuer already grants elsewhere) -- there is no bound on how many such root grants an issuer may mint, since none of them narrows any other. */
|
|
107
|
-
parent?: CapabilityToken;
|
|
108
|
-
/** Additional, issuer-chosen restrictions beyond the five mandatory narrowing checks (issue #85) -- CBOR-encoded into claims.conditions verbatim, evaluated by every verifier via evaluateConditions. Strictly additive: has no bearing on narrowing, which mint enforces separately above regardless of what's given here. Not re-validated against trilean's own schema before encoding -- the TS type already guarantees a well-formed PredicateNode[] at this call site, unlike the bytes a verifier decodes from an untrusted wire token, which always are. */
|
|
109
|
-
conditions?: PredicateNode[];
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Mints one capability token: builds token-claims from the given fields, signs it as a COSE_Sign1 under `identity`'s own key, with a protected header matching what the frozen conformance vectors actually encode (`{1: alg, 4: issuer device-id}`, not the empty header a token merely needs to verify against itself).
|
|
113
|
-
*
|
|
114
|
-
* When `parent` is given, every one of `tokens.cddl`'s own narrowing obligations is enforced here, at issuance, rather than left for the far end to discover minutes or hours later as a bare `delegation_exceeds_parent` from `verifyCapabilityToken` -- the same "fail loudly, fail early" reasoning that governs every other boundary in this codebase. An issuer minting an invalid delegation is a bug in the caller; this function refuses rather than producing a token indistinguishable from a valid one until someone else verifies it.
|
|
115
|
-
*/
|
|
116
|
-
declare function mintCapabilityToken(options: MintCapabilityTokenOptions): Promise<MintVerdict>;
|
|
117
|
-
interface MintRevocationEntryOptions {
|
|
118
|
-
/** The token's own issuer -- only a token's own issuer may revoke it (management.cddl), so this must be the same identity that minted the token being revoked. */
|
|
119
|
-
identity: IdentityPort;
|
|
120
|
-
tokenId: Uint8Array<ArrayBuffer>;
|
|
121
|
-
revokedAt: number;
|
|
122
|
-
}
|
|
123
|
-
/** Mints one revocation-entry (management.cddl): a COSE_Sign1 over revocation-claims, signed the same way mintCapabilityToken signs a token. No narrowing chain to check -- a revocation entry has no parent and cannot fail to be issuable the way a delegated token can, so this returns the entry directly rather than a verdict. */
|
|
124
|
-
declare function mintRevocationEntry(options: MintRevocationEntryOptions): Promise<RevocationEntry>;
|
|
125
|
-
//#endregion
|
|
126
|
-
export { verifyRevocationEntry as _, RevocationCheck as a, TokenVerdict as c, VerifyRevocationEntryOptions as d, canGrant as f, verifyCapabilityToken as g, sig1ToBeSigned as h, MintVerdict as i, TokenVerdictReason as l, mintRevocationEntry as m, MintRefusalReason as n, RevocationEntryVerdict as o, mintCapabilityToken as p, MintRevocationEntryOptions as r, RevocationEntryVerdictReason as s, MintCapabilityTokenOptions as t, VerifyCapabilityTokenOptions as u };
|