wire-mesh-core 0.0.0 → 1.0.2
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/README.md +32 -0
- package/dist/adapters/frame-codec.cjs +37 -0
- package/dist/adapters/frame-codec.d.cts +12 -0
- package/dist/adapters/frame-codec.d.mts +12 -0
- package/dist/adapters/frame-codec.mjs +33 -0
- package/dist/adapters/memory-storage.cjs +14 -0
- package/dist/adapters/memory-storage.d.cts +5 -0
- package/dist/adapters/memory-storage.d.mts +5 -0
- package/dist/adapters/memory-storage.mjs +13 -0
- package/dist/adapters/node-identity.cjs +55 -0
- package/dist/adapters/node-identity.d.cts +11 -0
- package/dist/adapters/node-identity.d.mts +11 -0
- package/dist/adapters/node-identity.mjs +52 -0
- package/dist/adapters/system-clock.cjs +8 -0
- package/dist/adapters/system-clock.d.cts +5 -0
- package/dist/adapters/system-clock.d.mts +5 -0
- package/dist/adapters/system-clock.mjs +7 -0
- package/dist/adapters/tcp-transport.cjs +154 -0
- package/dist/adapters/tcp-transport.d.cts +5 -0
- package/dist/adapters/tcp-transport.d.mts +5 -0
- package/dist/adapters/tcp-transport.mjs +153 -0
- package/dist/adapters/tls-transport.cjs +178 -0
- package/dist/adapters/tls-transport.d.cts +9 -0
- package/dist/adapters/tls-transport.d.mts +9 -0
- package/dist/adapters/tls-transport.mjs +177 -0
- package/dist/clock-DiSx-WKM.d.cts +7 -0
- package/dist/clock-DiSx-WKM.d.mts +7 -0
- package/dist/domain/device-id.cjs +27 -0
- package/dist/domain/device-id.d.cts +9 -0
- package/dist/domain/device-id.d.mts +9 -0
- package/dist/domain/device-id.mjs +24 -0
- package/dist/domain/handshake.cjs +23 -0
- package/dist/domain/handshake.d.cts +16 -0
- package/dist/domain/handshake.d.mts +16 -0
- package/dist/domain/handshake.mjs +21 -0
- package/dist/domain/mesh-session.cjs +439 -0
- package/dist/domain/mesh-session.d.cts +101 -0
- package/dist/domain/mesh-session.d.mts +101 -0
- package/dist/domain/mesh-session.mjs +436 -0
- package/dist/domain/relay-hub.cjs +99 -0
- package/dist/domain/relay-hub.d.cts +10 -0
- package/dist/domain/relay-hub.d.mts +10 -0
- package/dist/domain/relay-hub.mjs +98 -0
- package/dist/domain/revocation-view.cjs +22 -0
- package/dist/domain/revocation-view.d.cts +12 -0
- package/dist/domain/revocation-view.d.mts +12 -0
- package/dist/domain/revocation-view.mjs +21 -0
- package/dist/domain/tokens.cjs +300 -0
- package/dist/domain/tokens.d.cts +86 -0
- package/dist/domain/tokens.d.mts +86 -0
- package/dist/domain/tokens.mjs +296 -0
- package/dist/generated/protocol.cjs +466 -0
- package/dist/generated/protocol.d.cts +2 -0
- package/dist/generated/protocol.d.mts +2 -0
- package/dist/generated/protocol.mjs +382 -0
- package/dist/generated/runtime.cjs +8 -0
- package/dist/generated/runtime.d.cts +2 -0
- package/dist/generated/runtime.d.mts +2 -0
- package/dist/generated/runtime.mjs +2 -0
- package/dist/identity-BRLEUfVY.d.cts +17 -0
- package/dist/identity-qNkmGytv.d.mts +17 -0
- package/dist/ports/clock.cjs +0 -0
- package/dist/ports/clock.d.cts +2 -0
- package/dist/ports/clock.d.mts +2 -0
- package/dist/ports/clock.mjs +1 -0
- package/dist/ports/identity.cjs +0 -0
- package/dist/ports/identity.d.cts +2 -0
- package/dist/ports/identity.d.mts +2 -0
- package/dist/ports/identity.mjs +1 -0
- package/dist/ports/storage.cjs +0 -0
- package/dist/ports/storage.d.cts +9 -0
- package/dist/ports/storage.d.mts +9 -0
- package/dist/ports/storage.mjs +1 -0
- package/dist/ports/transport.cjs +0 -0
- package/dist/ports/transport.d.cts +29 -0
- package/dist/ports/transport.d.mts +29 -0
- package/dist/ports/transport.mjs +1 -0
- package/dist/protocol-B26-5VX7.d.cts +1112 -0
- package/dist/protocol-B26-5VX7.d.mts +1112 -0
- package/package.json +130 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//#region src/domain/relay-hub.ts
|
|
2
|
+
const HEX_RADIX = 16;
|
|
3
|
+
const HEX_DIGITS_PER_BYTE = 2;
|
|
4
|
+
function deviceKey(device) {
|
|
5
|
+
let key = "";
|
|
6
|
+
for (const byte of device) key += byte.toString(HEX_RADIX).padStart(HEX_DIGITS_PER_BYTE, "0");
|
|
7
|
+
return key;
|
|
8
|
+
}
|
|
9
|
+
function createRelayHub() {
|
|
10
|
+
const devices = /* @__PURE__ */ new Map();
|
|
11
|
+
const connectionDevice = /* @__PURE__ */ new Map();
|
|
12
|
+
const pairings = /* @__PURE__ */ new Map();
|
|
13
|
+
const mostRecentPairing = /* @__PURE__ */ new Map();
|
|
14
|
+
function pairingsOf(connection) {
|
|
15
|
+
const existing = pairings.get(connection);
|
|
16
|
+
if (existing) return existing;
|
|
17
|
+
const created = /* @__PURE__ */ new Map();
|
|
18
|
+
pairings.set(connection, created);
|
|
19
|
+
return created;
|
|
20
|
+
}
|
|
21
|
+
function addPairing(a, aDevice, b, bDevice) {
|
|
22
|
+
pairingsOf(a).set(deviceKey(bDevice), b);
|
|
23
|
+
pairingsOf(b).set(deviceKey(aDevice), a);
|
|
24
|
+
mostRecentPairing.set(a, b);
|
|
25
|
+
mostRecentPairing.set(b, a);
|
|
26
|
+
}
|
|
27
|
+
function forgetConnection(connection) {
|
|
28
|
+
for (const [key, registration] of devices) if (registration.connection === connection) devices.delete(key);
|
|
29
|
+
const ownDevice = connectionDevice.get(connection);
|
|
30
|
+
connectionDevice.delete(connection);
|
|
31
|
+
mostRecentPairing.delete(connection);
|
|
32
|
+
const own = pairings.get(connection);
|
|
33
|
+
if (own) {
|
|
34
|
+
for (const peer of own.values()) {
|
|
35
|
+
const peerOwn = pairings.get(peer);
|
|
36
|
+
if (peerOwn && ownDevice !== void 0) peerOwn.delete(deviceKey(ownDevice));
|
|
37
|
+
if (mostRecentPairing.get(peer) === connection) mostRecentPairing.delete(peer);
|
|
38
|
+
}
|
|
39
|
+
pairings.delete(connection);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function handleFrame(connection, frame) {
|
|
43
|
+
if (frame.type === "gossip") {
|
|
44
|
+
for (const advert of frame.peers) devices.set(deviceKey(advert.device), {
|
|
45
|
+
connection,
|
|
46
|
+
device: advert.device
|
|
47
|
+
});
|
|
48
|
+
for (const registration of devices.values()) if (registration.connection === connection) {
|
|
49
|
+
connectionDevice.set(connection, registration.device);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (frame.type === "relay-connect") {
|
|
55
|
+
const registration = devices.get(deviceKey(frame["target-device"]));
|
|
56
|
+
if (!registration || registration.connection === connection) return;
|
|
57
|
+
const initiatorDevice = connectionDevice.get(connection);
|
|
58
|
+
if (initiatorDevice === void 0) return;
|
|
59
|
+
addPairing(connection, initiatorDevice, registration.connection, registration.device);
|
|
60
|
+
await registration.connection.send({
|
|
61
|
+
type: "relay-inbound",
|
|
62
|
+
"source-device": initiatorDevice
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (frame.type === "relay-data") {
|
|
67
|
+
const own = pairings.get(connection);
|
|
68
|
+
const toDevice = frame["to-device"];
|
|
69
|
+
const peer = toDevice !== void 0 ? own?.get(deviceKey(toDevice)) : mostRecentPairing.get(connection);
|
|
70
|
+
if (!peer) return;
|
|
71
|
+
const senderDevice = connectionDevice.get(connection);
|
|
72
|
+
if (senderDevice === void 0) return;
|
|
73
|
+
await peer.send({
|
|
74
|
+
type: "relay-data",
|
|
75
|
+
payload: frame.payload,
|
|
76
|
+
"from-device": senderDevice
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
async handleConnection(connection) {
|
|
83
|
+
try {
|
|
84
|
+
for await (const frame of connection.receive()) await handleFrame(connection, frame);
|
|
85
|
+
} catch {} finally {
|
|
86
|
+
forgetConnection(connection);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
stop() {
|
|
90
|
+
devices.clear();
|
|
91
|
+
connectionDevice.clear();
|
|
92
|
+
pairings.clear();
|
|
93
|
+
mostRecentPairing.clear();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { createRelayHub };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_domain_device_id = require("./device-id.cjs");
|
|
3
|
+
const require_domain_tokens = require("./tokens.cjs");
|
|
4
|
+
//#region src/domain/revocation-view.ts
|
|
5
|
+
function revocationKey(tokenId, issuer) {
|
|
6
|
+
return `${require_domain_device_id.bytesToHex(tokenId)}:${require_domain_device_id.deviceIdToHex(issuer)}`;
|
|
7
|
+
}
|
|
8
|
+
function createRevocationView() {
|
|
9
|
+
const revoked = /* @__PURE__ */ new Set();
|
|
10
|
+
return {
|
|
11
|
+
async isRevoked(tokenId, issuer) {
|
|
12
|
+
return Promise.resolve(revoked.has(revocationKey(tokenId, issuer)));
|
|
13
|
+
},
|
|
14
|
+
async record(entry, options) {
|
|
15
|
+
const verdict = await require_domain_tokens.verifyRevocationEntry(entry, options);
|
|
16
|
+
if (verdict.ok) revoked.add(revocationKey(verdict.claims["token-id"], verdict.claims.issuer));
|
|
17
|
+
return verdict;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
exports.createRevocationView = createRevocationView;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ct as RevocationEntry } from "../protocol-B26-5VX7.cjs";
|
|
2
|
+
import { RevocationCheck, RevocationEntryVerdict, VerifyRevocationEntryOptions } from "./tokens.cjs";
|
|
3
|
+
//#region src/domain/revocation-view.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* An in-memory RevocationCheck fed by ingested revocation-announce frames. Keyed by (token-id, issuer) per management.cddl's contract: an entry counts against a token only when both match, so a third party's entry for someone else's token-id is stored (it is a well-formed, self-certifying entry) but never matches a lookup for the token it does not actually govern.
|
|
6
|
+
*/
|
|
7
|
+
export interface RevocationView extends RevocationCheck {
|
|
8
|
+
/** Verifies one gossiped revocation-entry via verifyRevocationEntry and, if it verifies, records it. An entry that fails verification is dropped, not stored -- the returned verdict lets a caller log or otherwise report the refusal. */
|
|
9
|
+
record: (entry: RevocationEntry, options: VerifyRevocationEntryOptions) => Promise<RevocationEntryVerdict>;
|
|
10
|
+
}
|
|
11
|
+
export declare function createRevocationView(): RevocationView;
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ct as RevocationEntry } from "../protocol-B26-5VX7.mjs";
|
|
2
|
+
import { RevocationCheck, RevocationEntryVerdict, VerifyRevocationEntryOptions } from "./tokens.mjs";
|
|
3
|
+
//#region src/domain/revocation-view.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* An in-memory RevocationCheck fed by ingested revocation-announce frames. Keyed by (token-id, issuer) per management.cddl's contract: an entry counts against a token only when both match, so a third party's entry for someone else's token-id is stored (it is a well-formed, self-certifying entry) but never matches a lookup for the token it does not actually govern.
|
|
6
|
+
*/
|
|
7
|
+
export interface RevocationView extends RevocationCheck {
|
|
8
|
+
/** Verifies one gossiped revocation-entry via verifyRevocationEntry and, if it verifies, records it. An entry that fails verification is dropped, not stored -- the returned verdict lets a caller log or otherwise report the refusal. */
|
|
9
|
+
record: (entry: RevocationEntry, options: VerifyRevocationEntryOptions) => Promise<RevocationEntryVerdict>;
|
|
10
|
+
}
|
|
11
|
+
export declare function createRevocationView(): RevocationView;
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { bytesToHex, deviceIdToHex } from "./device-id.mjs";
|
|
2
|
+
import { verifyRevocationEntry } from "./tokens.mjs";
|
|
3
|
+
//#region src/domain/revocation-view.ts
|
|
4
|
+
function revocationKey(tokenId, issuer) {
|
|
5
|
+
return `${bytesToHex(tokenId)}:${deviceIdToHex(issuer)}`;
|
|
6
|
+
}
|
|
7
|
+
function createRevocationView() {
|
|
8
|
+
const revoked = /* @__PURE__ */ new Set();
|
|
9
|
+
return {
|
|
10
|
+
async isRevoked(tokenId, issuer) {
|
|
11
|
+
return Promise.resolve(revoked.has(revocationKey(tokenId, issuer)));
|
|
12
|
+
},
|
|
13
|
+
async record(entry, options) {
|
|
14
|
+
const verdict = await verifyRevocationEntry(entry, options);
|
|
15
|
+
if (verdict.ok) revoked.add(revocationKey(verdict.claims["token-id"], verdict.claims.issuer));
|
|
16
|
+
return verdict;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { createRevocationView };
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_generated_protocol = require("../generated/protocol.cjs");
|
|
3
|
+
let cbor2 = require("cbor2");
|
|
4
|
+
//#region src/domain/tokens.ts
|
|
5
|
+
function bytesEqual(a, b) {
|
|
6
|
+
if (a.length !== b.length) return false;
|
|
7
|
+
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
/** True when the path contains a "." or ".." segment. Purely lexical prefix comparison would let "/work/../org" pass under "/work" -- a path that normalises outside the parent -- so any relative segment fails the narrowing comparison wholesale: fail-closed rather than reimplementing path normalisation, consistent with how empty, case-different, and non-boundary-prefixed paths already behave. */
|
|
11
|
+
function hasRelativeSegment(path) {
|
|
12
|
+
return path.split("/").some((segment) => segment === "." || segment === "..");
|
|
13
|
+
}
|
|
14
|
+
/** True when childPath is parentPath or a descendant of it, compared on "/"-segment boundaries: "/work/sub" narrows "/work", but "/workbook" does NOT narrow "/work" despite the string prefix, because "book" continues the same segment. Paths containing "." or ".." segments never narrow anything (see hasRelativeSegment). */
|
|
15
|
+
function pathNarrows(childPath, parentPath) {
|
|
16
|
+
if (hasRelativeSegment(childPath) || hasRelativeSegment(parentPath)) return false;
|
|
17
|
+
if (childPath === parentPath) return true;
|
|
18
|
+
if (!childPath.startsWith(parentPath)) return false;
|
|
19
|
+
if (parentPath.endsWith("/")) return true;
|
|
20
|
+
return childPath.charAt(parentPath.length) === "/";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* True when childScope narrows parentScope per tokens.cddl ("each hop can only narrow authority, never widen it"): the kind must be identical (a different kind is a different kind of authority, not a narrower one), and a parent with a path requires the child to carry an equal-or-descendant path -- an absent child path means the kind's whole-scope root, which is wider than any path-narrowed parent. A parent with no path (whole-scope root) lets any child path under the same kind through.
|
|
24
|
+
*/
|
|
25
|
+
function scopeNarrows(parent, child) {
|
|
26
|
+
if (parent.kind !== child.kind) return false;
|
|
27
|
+
if (parent.path === void 0) return true;
|
|
28
|
+
if (child.path === void 0) return false;
|
|
29
|
+
return pathNarrows(child.path, parent.path);
|
|
30
|
+
}
|
|
31
|
+
/** RFC 9052 §4.4 Sig_structure for a COSE_Sign1 with no external AAD: ["Signature1", protected, external_aad, payload]. */
|
|
32
|
+
function sig1ToBeSigned(protectedHeader, payload) {
|
|
33
|
+
return (0, cbor2.encode)([
|
|
34
|
+
"Signature1",
|
|
35
|
+
protectedHeader,
|
|
36
|
+
/* @__PURE__ */ new Uint8Array(0),
|
|
37
|
+
payload
|
|
38
|
+
], cbor2.cdeEncodeOptions);
|
|
39
|
+
}
|
|
40
|
+
/** 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. */
|
|
41
|
+
function buf(bytes) {
|
|
42
|
+
return Uint8Array.from(bytes);
|
|
43
|
+
}
|
|
44
|
+
function encodeBuf(value) {
|
|
45
|
+
return buf((0, cbor2.encode)(value, cbor2.cdeEncodeOptions));
|
|
46
|
+
}
|
|
47
|
+
/** 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. */
|
|
48
|
+
function protectedHeaderFor(identity) {
|
|
49
|
+
return encodeBuf({
|
|
50
|
+
1: identity.identityKey.alg,
|
|
51
|
+
4: identity.deviceId
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** 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. */
|
|
55
|
+
function decodeTokenClaims(token) {
|
|
56
|
+
const [, , payload] = token;
|
|
57
|
+
if (payload === null) return void 0;
|
|
58
|
+
let decoded;
|
|
59
|
+
try {
|
|
60
|
+
decoded = (0, cbor2.decode)(payload, cbor2.cdeDecodeOptions);
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const result = require_generated_protocol.tokenClaimsSchema.safeParse(decoded);
|
|
65
|
+
return result.success ? result.data : void 0;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 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.
|
|
69
|
+
*/
|
|
70
|
+
async function verifyCapabilityToken(token, options) {
|
|
71
|
+
const verdict = await verifyTokenChain(token, {
|
|
72
|
+
identity: options.identity,
|
|
73
|
+
clock: options.clock,
|
|
74
|
+
revocation: options.revocation
|
|
75
|
+
});
|
|
76
|
+
if (!verdict.ok) return verdict;
|
|
77
|
+
if (options.expectedBearer !== void 0 && !bytesEqual(verdict.claims.bearer, options.expectedBearer)) return {
|
|
78
|
+
ok: false,
|
|
79
|
+
reason: "bearer_mismatch"
|
|
80
|
+
};
|
|
81
|
+
return verdict;
|
|
82
|
+
}
|
|
83
|
+
async function verifyTokenChain(token, options) {
|
|
84
|
+
const [protectedHeader, , payload, signature] = token;
|
|
85
|
+
if (payload === null) return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: "malformed"
|
|
88
|
+
};
|
|
89
|
+
let decodedClaims;
|
|
90
|
+
try {
|
|
91
|
+
decodedClaims = (0, cbor2.decode)(payload, cbor2.cdeDecodeOptions);
|
|
92
|
+
} catch {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: "malformed"
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const claimsResult = require_generated_protocol.tokenClaimsSchema.safeParse(decodedClaims);
|
|
99
|
+
if (!claimsResult.success) return {
|
|
100
|
+
ok: false,
|
|
101
|
+
reason: "malformed"
|
|
102
|
+
};
|
|
103
|
+
const claims = claimsResult.data;
|
|
104
|
+
if (!await options.identity.verify(claims["issuer-key"], sig1ToBeSigned(protectedHeader, payload), signature)) return {
|
|
105
|
+
ok: false,
|
|
106
|
+
reason: "bad_signature"
|
|
107
|
+
};
|
|
108
|
+
if (!bytesEqual(await options.identity.deriveDeviceId(claims["issuer-key"]["public-key"]), claims.issuer)) return {
|
|
109
|
+
ok: false,
|
|
110
|
+
reason: "wrong_issuer"
|
|
111
|
+
};
|
|
112
|
+
const now = options.clock.now();
|
|
113
|
+
if (claims.expires <= now) return {
|
|
114
|
+
ok: false,
|
|
115
|
+
reason: "expired"
|
|
116
|
+
};
|
|
117
|
+
if (claims["not-before"] !== void 0 && claims["not-before"] > now) return {
|
|
118
|
+
ok: false,
|
|
119
|
+
reason: "not_yet_valid"
|
|
120
|
+
};
|
|
121
|
+
if (await options.revocation.isRevoked(claims["token-id"], claims.issuer)) return {
|
|
122
|
+
ok: false,
|
|
123
|
+
reason: "revoked"
|
|
124
|
+
};
|
|
125
|
+
if (claims.parent !== void 0) {
|
|
126
|
+
let decodedParent;
|
|
127
|
+
try {
|
|
128
|
+
decodedParent = (0, cbor2.decode)(claims.parent, cbor2.cdeDecodeOptions);
|
|
129
|
+
} catch {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
reason: "parent_invalid"
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const parentResult = require_generated_protocol.capabilityTokenSchema.safeParse(decodedParent);
|
|
136
|
+
if (!parentResult.success) return {
|
|
137
|
+
ok: false,
|
|
138
|
+
reason: "parent_invalid"
|
|
139
|
+
};
|
|
140
|
+
const parentVerdict = await verifyTokenChain(parentResult.data, options);
|
|
141
|
+
if (!parentVerdict.ok) return {
|
|
142
|
+
ok: false,
|
|
143
|
+
reason: "parent_invalid"
|
|
144
|
+
};
|
|
145
|
+
if (!bytesEqual(parentVerdict.claims.bearer, claims.issuer)) return {
|
|
146
|
+
ok: false,
|
|
147
|
+
reason: "delegation_exceeds_parent"
|
|
148
|
+
};
|
|
149
|
+
if (claims.expires > parentVerdict.claims.expires) return {
|
|
150
|
+
ok: false,
|
|
151
|
+
reason: "delegation_exceeds_parent"
|
|
152
|
+
};
|
|
153
|
+
if (!scopeNarrows(parentVerdict.claims.scope, claims.scope)) return {
|
|
154
|
+
ok: false,
|
|
155
|
+
reason: "delegation_exceeds_parent"
|
|
156
|
+
};
|
|
157
|
+
if (parentVerdict.claims.capability !== claims.capability) return {
|
|
158
|
+
ok: false,
|
|
159
|
+
reason: "delegation_exceeds_parent"
|
|
160
|
+
};
|
|
161
|
+
const parentRemaining = parentVerdict.claims["delegations-remaining"];
|
|
162
|
+
if (parentRemaining !== void 0 && (claims["delegations-remaining"] === void 0 || claims["delegations-remaining"] >= parentRemaining)) return {
|
|
163
|
+
ok: false,
|
|
164
|
+
reason: "delegation_exceeds_parent"
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
claims,
|
|
169
|
+
rootIssuer: parentVerdict.rootIssuer,
|
|
170
|
+
depth: parentVerdict.depth + 1
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
claims,
|
|
176
|
+
rootIssuer: claims.issuer,
|
|
177
|
+
depth: 0
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 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.
|
|
182
|
+
*/
|
|
183
|
+
async function verifyRevocationEntry(entry, options) {
|
|
184
|
+
const [protectedHeader, , payload, signature] = entry;
|
|
185
|
+
if (payload === null) return {
|
|
186
|
+
ok: false,
|
|
187
|
+
reason: "malformed"
|
|
188
|
+
};
|
|
189
|
+
let decodedClaims;
|
|
190
|
+
try {
|
|
191
|
+
decodedClaims = (0, cbor2.decode)(payload, cbor2.cdeDecodeOptions);
|
|
192
|
+
} catch {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
reason: "malformed"
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const claimsResult = require_generated_protocol.revocationClaimsSchema.safeParse(decodedClaims);
|
|
199
|
+
if (!claimsResult.success) return {
|
|
200
|
+
ok: false,
|
|
201
|
+
reason: "malformed"
|
|
202
|
+
};
|
|
203
|
+
const claims = claimsResult.data;
|
|
204
|
+
if (!await options.identity.verify(claims["issuer-key"], sig1ToBeSigned(protectedHeader, payload), signature)) return {
|
|
205
|
+
ok: false,
|
|
206
|
+
reason: "bad_signature"
|
|
207
|
+
};
|
|
208
|
+
if (!bytesEqual(await options.identity.deriveDeviceId(claims["issuer-key"]["public-key"]), claims.issuer)) return {
|
|
209
|
+
ok: false,
|
|
210
|
+
reason: "wrong_issuer"
|
|
211
|
+
};
|
|
212
|
+
return {
|
|
213
|
+
ok: true,
|
|
214
|
+
claims
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 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).
|
|
219
|
+
*
|
|
220
|
+
* 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.
|
|
221
|
+
*/
|
|
222
|
+
async function mintCapabilityToken(options) {
|
|
223
|
+
if (options.expires <= options.clock.now()) return {
|
|
224
|
+
ok: false,
|
|
225
|
+
reason: "already_expired"
|
|
226
|
+
};
|
|
227
|
+
let parentBytes;
|
|
228
|
+
if (options.parent !== void 0) {
|
|
229
|
+
const parentClaims = decodeTokenClaims(options.parent);
|
|
230
|
+
if (parentClaims === void 0) return {
|
|
231
|
+
ok: false,
|
|
232
|
+
reason: "parent_malformed"
|
|
233
|
+
};
|
|
234
|
+
if (!bytesEqual(parentClaims.bearer, options.identity.deviceId)) return {
|
|
235
|
+
ok: false,
|
|
236
|
+
reason: "parent_bearer_mismatch"
|
|
237
|
+
};
|
|
238
|
+
if (options.expires > parentClaims.expires) return {
|
|
239
|
+
ok: false,
|
|
240
|
+
reason: "expires_exceeds_parent"
|
|
241
|
+
};
|
|
242
|
+
if (!scopeNarrows(parentClaims.scope, options.scope)) return {
|
|
243
|
+
ok: false,
|
|
244
|
+
reason: "scope_does_not_narrow"
|
|
245
|
+
};
|
|
246
|
+
if (parentClaims.capability !== options.capability) return {
|
|
247
|
+
ok: false,
|
|
248
|
+
reason: "capability_mismatch"
|
|
249
|
+
};
|
|
250
|
+
const parentRemaining = parentClaims["delegations-remaining"];
|
|
251
|
+
if (parentRemaining !== void 0 && (options.delegationsRemaining === void 0 || options.delegationsRemaining >= parentRemaining)) return {
|
|
252
|
+
ok: false,
|
|
253
|
+
reason: "delegation_exceeds_parent"
|
|
254
|
+
};
|
|
255
|
+
parentBytes = encodeBuf(options.parent);
|
|
256
|
+
}
|
|
257
|
+
const payload = encodeBuf({
|
|
258
|
+
"token-id": options.tokenId,
|
|
259
|
+
issuer: options.identity.deviceId,
|
|
260
|
+
"issuer-key": options.identity.identityKey,
|
|
261
|
+
bearer: options.bearer,
|
|
262
|
+
capability: options.capability,
|
|
263
|
+
scope: options.scope,
|
|
264
|
+
expires: options.expires,
|
|
265
|
+
...options.notBefore !== void 0 ? { "not-before": options.notBefore } : {},
|
|
266
|
+
...parentBytes !== void 0 ? { parent: parentBytes } : {},
|
|
267
|
+
...options.delegationsRemaining !== void 0 ? { "delegations-remaining": options.delegationsRemaining } : {}
|
|
268
|
+
});
|
|
269
|
+
const protectedHeader = protectedHeaderFor(options.identity);
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
token: [
|
|
273
|
+
protectedHeader,
|
|
274
|
+
{},
|
|
275
|
+
payload,
|
|
276
|
+
await options.identity.sign(sig1ToBeSigned(protectedHeader, payload))
|
|
277
|
+
]
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/** 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. */
|
|
281
|
+
async function mintRevocationEntry(options) {
|
|
282
|
+
const payload = encodeBuf({
|
|
283
|
+
"token-id": options.tokenId,
|
|
284
|
+
issuer: options.identity.deviceId,
|
|
285
|
+
"issuer-key": options.identity.identityKey,
|
|
286
|
+
"revoked-at": options.revokedAt
|
|
287
|
+
});
|
|
288
|
+
const protectedHeader = protectedHeaderFor(options.identity);
|
|
289
|
+
return [
|
|
290
|
+
protectedHeader,
|
|
291
|
+
{},
|
|
292
|
+
payload,
|
|
293
|
+
await options.identity.sign(sig1ToBeSigned(protectedHeader, payload))
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
exports.mintCapabilityToken = mintCapabilityToken;
|
|
298
|
+
exports.mintRevocationEntry = mintRevocationEntry;
|
|
299
|
+
exports.verifyCapabilityToken = verifyCapabilityToken;
|
|
300
|
+
exports.verifyRevocationEntry = verifyRevocationEntry;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Et as TokenClaims, ct as RevocationEntry, i as CapabilityToken, st as RevocationClaims, v as DeviceId } from "../protocol-B26-5VX7.cjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-BRLEUfVY.cjs";
|
|
3
|
+
import { t as Clock } from "../clock-DiSx-WKM.cjs";
|
|
4
|
+
//#region src/domain/tokens.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The revocation view a verifier consults. Contract per management.cddl: an entry counts against a token only when BOTH its token-id and its issuer match the token's own -- only a token's own issuer may revoke it, so a third party's entry for someone else's token-id must be ignored. 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 + issuer.
|
|
7
|
+
*/
|
|
8
|
+
export interface RevocationCheck {
|
|
9
|
+
isRevoked: (tokenId: Uint8Array, issuer: DeviceId) => Promise<boolean>;
|
|
10
|
+
}
|
|
11
|
+
export type TokenVerdictReason = "malformed" | "bad_signature" | "wrong_issuer" | "bearer_mismatch" | "expired" | "not_yet_valid" | "revoked" | "delegation_exceeds_parent" | "parent_invalid";
|
|
12
|
+
export type TokenVerdict = {
|
|
13
|
+
ok: true;
|
|
14
|
+
claims: TokenClaims;
|
|
15
|
+
/** 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. */
|
|
16
|
+
rootIssuer: DeviceId;
|
|
17
|
+
/** 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. */
|
|
18
|
+
depth: number;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
reason: TokenVerdictReason;
|
|
22
|
+
};
|
|
23
|
+
export interface VerifyCapabilityTokenOptions {
|
|
24
|
+
identity: IdentityPort;
|
|
25
|
+
clock: Clock;
|
|
26
|
+
revocation: RevocationCheck;
|
|
27
|
+
/** When given, the token must bear this device -- the caller presenting a token to authorise itself, not someone else. */
|
|
28
|
+
expectedBearer?: DeviceId;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 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.
|
|
32
|
+
*/
|
|
33
|
+
export declare function verifyCapabilityToken(token: CapabilityToken, options: VerifyCapabilityTokenOptions): Promise<TokenVerdict>;
|
|
34
|
+
export type RevocationEntryVerdictReason = "malformed" | "bad_signature" | "wrong_issuer";
|
|
35
|
+
export type RevocationEntryVerdict = {
|
|
36
|
+
ok: true;
|
|
37
|
+
claims: RevocationClaims;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
reason: RevocationEntryVerdictReason;
|
|
41
|
+
};
|
|
42
|
+
export interface VerifyRevocationEntryOptions {
|
|
43
|
+
/** Crypto primitives only -- any IdentityPort instance can verify any entry, since everything needed to check one travels inside the entry itself. */
|
|
44
|
+
identity: IdentityPort;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 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.
|
|
48
|
+
*/
|
|
49
|
+
export declare function verifyRevocationEntry(entry: RevocationEntry, options: VerifyRevocationEntryOptions): Promise<RevocationEntryVerdict>;
|
|
50
|
+
export type MintRefusalReason = "already_expired" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent";
|
|
51
|
+
export type MintVerdict = {
|
|
52
|
+
ok: true;
|
|
53
|
+
token: CapabilityToken;
|
|
54
|
+
} | {
|
|
55
|
+
ok: false;
|
|
56
|
+
reason: MintRefusalReason;
|
|
57
|
+
};
|
|
58
|
+
export interface MintCapabilityTokenOptions {
|
|
59
|
+
/** The issuer -- signs the token, and supplies the self-certifying issuer/issuer-key claims. */
|
|
60
|
+
identity: IdentityPort;
|
|
61
|
+
clock: Clock;
|
|
62
|
+
tokenId: Uint8Array<ArrayBuffer>;
|
|
63
|
+
bearer: DeviceId;
|
|
64
|
+
capability: TokenClaims["capability"];
|
|
65
|
+
scope: TokenClaims["scope"];
|
|
66
|
+
expires: number;
|
|
67
|
+
notBefore?: number;
|
|
68
|
+
delegationsRemaining?: number;
|
|
69
|
+
/** 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. */
|
|
70
|
+
parent?: CapabilityToken;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 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).
|
|
74
|
+
*
|
|
75
|
+
* 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.
|
|
76
|
+
*/
|
|
77
|
+
export declare function mintCapabilityToken(options: MintCapabilityTokenOptions): Promise<MintVerdict>;
|
|
78
|
+
export interface MintRevocationEntryOptions {
|
|
79
|
+
/** 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. */
|
|
80
|
+
identity: IdentityPort;
|
|
81
|
+
tokenId: Uint8Array<ArrayBuffer>;
|
|
82
|
+
revokedAt: number;
|
|
83
|
+
}
|
|
84
|
+
/** 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. */
|
|
85
|
+
export declare function mintRevocationEntry(options: MintRevocationEntryOptions): Promise<RevocationEntry>;
|
|
86
|
+
//#endregion
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Et as TokenClaims, ct as RevocationEntry, i as CapabilityToken, st as RevocationClaims, v as DeviceId } from "../protocol-B26-5VX7.mjs";
|
|
2
|
+
import { t as IdentityPort } from "../identity-qNkmGytv.mjs";
|
|
3
|
+
import { t as Clock } from "../clock-DiSx-WKM.mjs";
|
|
4
|
+
//#region src/domain/tokens.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The revocation view a verifier consults. Contract per management.cddl: an entry counts against a token only when BOTH its token-id and its issuer match the token's own -- only a token's own issuer may revoke it, so a third party's entry for someone else's token-id must be ignored. 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 + issuer.
|
|
7
|
+
*/
|
|
8
|
+
export interface RevocationCheck {
|
|
9
|
+
isRevoked: (tokenId: Uint8Array, issuer: DeviceId) => Promise<boolean>;
|
|
10
|
+
}
|
|
11
|
+
export type TokenVerdictReason = "malformed" | "bad_signature" | "wrong_issuer" | "bearer_mismatch" | "expired" | "not_yet_valid" | "revoked" | "delegation_exceeds_parent" | "parent_invalid";
|
|
12
|
+
export type TokenVerdict = {
|
|
13
|
+
ok: true;
|
|
14
|
+
claims: TokenClaims;
|
|
15
|
+
/** 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. */
|
|
16
|
+
rootIssuer: DeviceId;
|
|
17
|
+
/** 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. */
|
|
18
|
+
depth: number;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
reason: TokenVerdictReason;
|
|
22
|
+
};
|
|
23
|
+
export interface VerifyCapabilityTokenOptions {
|
|
24
|
+
identity: IdentityPort;
|
|
25
|
+
clock: Clock;
|
|
26
|
+
revocation: RevocationCheck;
|
|
27
|
+
/** When given, the token must bear this device -- the caller presenting a token to authorise itself, not someone else. */
|
|
28
|
+
expectedBearer?: DeviceId;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 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.
|
|
32
|
+
*/
|
|
33
|
+
export declare function verifyCapabilityToken(token: CapabilityToken, options: VerifyCapabilityTokenOptions): Promise<TokenVerdict>;
|
|
34
|
+
export type RevocationEntryVerdictReason = "malformed" | "bad_signature" | "wrong_issuer";
|
|
35
|
+
export type RevocationEntryVerdict = {
|
|
36
|
+
ok: true;
|
|
37
|
+
claims: RevocationClaims;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
reason: RevocationEntryVerdictReason;
|
|
41
|
+
};
|
|
42
|
+
export interface VerifyRevocationEntryOptions {
|
|
43
|
+
/** Crypto primitives only -- any IdentityPort instance can verify any entry, since everything needed to check one travels inside the entry itself. */
|
|
44
|
+
identity: IdentityPort;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 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.
|
|
48
|
+
*/
|
|
49
|
+
export declare function verifyRevocationEntry(entry: RevocationEntry, options: VerifyRevocationEntryOptions): Promise<RevocationEntryVerdict>;
|
|
50
|
+
export type MintRefusalReason = "already_expired" | "parent_malformed" | "parent_bearer_mismatch" | "expires_exceeds_parent" | "scope_does_not_narrow" | "capability_mismatch" | "delegation_exceeds_parent";
|
|
51
|
+
export type MintVerdict = {
|
|
52
|
+
ok: true;
|
|
53
|
+
token: CapabilityToken;
|
|
54
|
+
} | {
|
|
55
|
+
ok: false;
|
|
56
|
+
reason: MintRefusalReason;
|
|
57
|
+
};
|
|
58
|
+
export interface MintCapabilityTokenOptions {
|
|
59
|
+
/** The issuer -- signs the token, and supplies the self-certifying issuer/issuer-key claims. */
|
|
60
|
+
identity: IdentityPort;
|
|
61
|
+
clock: Clock;
|
|
62
|
+
tokenId: Uint8Array<ArrayBuffer>;
|
|
63
|
+
bearer: DeviceId;
|
|
64
|
+
capability: TokenClaims["capability"];
|
|
65
|
+
scope: TokenClaims["scope"];
|
|
66
|
+
expires: number;
|
|
67
|
+
notBefore?: number;
|
|
68
|
+
delegationsRemaining?: number;
|
|
69
|
+
/** 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. */
|
|
70
|
+
parent?: CapabilityToken;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 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).
|
|
74
|
+
*
|
|
75
|
+
* 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.
|
|
76
|
+
*/
|
|
77
|
+
export declare function mintCapabilityToken(options: MintCapabilityTokenOptions): Promise<MintVerdict>;
|
|
78
|
+
export interface MintRevocationEntryOptions {
|
|
79
|
+
/** 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. */
|
|
80
|
+
identity: IdentityPort;
|
|
81
|
+
tokenId: Uint8Array<ArrayBuffer>;
|
|
82
|
+
revokedAt: number;
|
|
83
|
+
}
|
|
84
|
+
/** 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. */
|
|
85
|
+
export declare function mintRevocationEntry(options: MintRevocationEntryOptions): Promise<RevocationEntry>;
|
|
86
|
+
//#endregion
|