@openlfcp/wire 0.2.1 → 0.3.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/capability.d.ts +34 -6
- package/dist/capability.js +143 -45
- package/dist/chain.d.ts +10 -4
- package/dist/chain.js +153 -29
- package/dist/control.d.ts +37 -5
- package/dist/control.js +148 -18
- package/dist/device-scan.d.ts +13 -0
- package/dist/device-scan.js +268 -0
- package/dist/device-state.d.ts +154 -0
- package/dist/device-state.js +318 -0
- package/dist/device.d.ts +231 -0
- package/dist/device.js +427 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/invite.d.ts +44 -8
- package/dist/invite.js +130 -45
- package/dist/objects.d.ts +12 -2
- package/dist/objects.js +18 -4
- package/dist/persistent-map.d.ts +42 -0
- package/dist/persistent-map.js +147 -0
- package/dist/revoked-reader.d.ts +29 -0
- package/dist/revoked-reader.js +39 -0
- package/package.json +3 -3
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { bytesEqual, controlRecordId, toHex, } from "@openlfcp/core";
|
|
2
|
+
import { ABILITY, isGrantActive } from "./capability.js";
|
|
3
|
+
import { checkChainAuthority, checkChainState, DEVICE_ABILITY, DEVICE_KIND, } from "./device.js";
|
|
4
|
+
import { PersistentMap, put } from "./persistent-map.js";
|
|
5
|
+
export const EMPTY_DEVICE_STATE = Object.freeze({
|
|
6
|
+
roles: PersistentMap.empty(),
|
|
7
|
+
devices: PersistentMap.empty(),
|
|
8
|
+
revoked: PersistentMap.empty(),
|
|
9
|
+
liveBound: PersistentMap.empty(),
|
|
10
|
+
preemptive: PersistentMap.empty(),
|
|
11
|
+
preemptiveRoots: PersistentMap.empty(),
|
|
12
|
+
});
|
|
13
|
+
/** §17.4 rule 8: live Principals bound to one root in a Resource. */
|
|
14
|
+
export const MAX_LIVE_BOUND = 32;
|
|
15
|
+
/** §17.4 rule 8: accepted Device Grants per (root, leaf) pair. */
|
|
16
|
+
export const MAX_DEVICE_GRANTS = 8;
|
|
17
|
+
/** §17.5 rule 5: pre-emptive revocations of unbound Principals per root. */
|
|
18
|
+
export const MAX_PREEMPTIVE_REVOCATIONS = 64;
|
|
19
|
+
const count = (map, key) => map.get(key) ?? 0;
|
|
20
|
+
/** The key of `revoked` for the pair (root, device). */
|
|
21
|
+
export const revocationKey = (root, device) => `${toHex(root)}:${toHex(device)}`;
|
|
22
|
+
const hasCode = (mask, code) => mask.includes(code);
|
|
23
|
+
/** `bound(P)` (§80.1): P appeared as a device, at any level, in an accepted chain. */
|
|
24
|
+
export const isBound = (state, p) => state.devices.has(toHex(p));
|
|
25
|
+
/** `revoked[(root, device)]`, if any. */
|
|
26
|
+
export const revocationOf = (state, root, device) => state.revoked.get(revocationKey(root, device));
|
|
27
|
+
const isOwner = (state, p) => bytesEqual(state.owner.principalId, p);
|
|
28
|
+
/**
|
|
29
|
+
* `rootStanding(R)` (§17.5): R is the owner, or the subject of at least one
|
|
30
|
+
* active grant that does not list `invite/claim`.
|
|
31
|
+
*/
|
|
32
|
+
export function rootStanding(state, root) {
|
|
33
|
+
if (isOwner(state, root))
|
|
34
|
+
return true;
|
|
35
|
+
for (const g of state.grants.values())
|
|
36
|
+
if (bytesEqual(g.subject, root) &&
|
|
37
|
+
!g.abilities.includes(ABILITY.INVITE_CLAIM) &&
|
|
38
|
+
isGrantActive(state, g.id))
|
|
39
|
+
return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `activeDevice(P)` (§17.5): registered, `(root, P)` not revoked, no
|
|
44
|
+
* revoked issuer in its current chain (the cascade), and its root in good
|
|
45
|
+
* standing. Undefined otherwise.
|
|
46
|
+
*/
|
|
47
|
+
export function activeDevice(state, p) {
|
|
48
|
+
const d = state.devices.get(toHex(p));
|
|
49
|
+
if (d === undefined || !d.registered)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (revocationOf(state, d.root, p) !== undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (d.issuers.some((e) => revocationOf(state, d.root, e) !== undefined))
|
|
54
|
+
return undefined;
|
|
55
|
+
return rootStanding(state, d.root) ? d : undefined;
|
|
56
|
+
}
|
|
57
|
+
/** `actsFor(P)` (§80.1): the root of an active device, else P itself. */
|
|
58
|
+
export const actsFor = (state, p) => activeDevice(state, p)?.root ?? p;
|
|
59
|
+
/**
|
|
60
|
+
* `rootOf(P)` (§80.1): the root of a bound P (registered, suspended,
|
|
61
|
+
* revoked or intermediate), else P itself. For attribution, never for
|
|
62
|
+
* authority.
|
|
63
|
+
*/
|
|
64
|
+
export const rootOf = (state, p) => state.devices.get(toHex(p))?.root ?? p;
|
|
65
|
+
/**
|
|
66
|
+
* `ownerAction(P)` (§80.1): for a bound P, an active device of the owner
|
|
67
|
+
* with `owner/act` in its mask; otherwise P is the owner.
|
|
68
|
+
*/
|
|
69
|
+
export function ownerAction(state, p) {
|
|
70
|
+
if (!isBound(state, p))
|
|
71
|
+
return isOwner(state, p);
|
|
72
|
+
const d = activeDevice(state, p);
|
|
73
|
+
return d !== undefined && isOwner(state, d.root) && hasCode(d.mask, DEVICE_ABILITY.OWNER_ACT);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The descriptor a transfer offer with `kid` is verified under (§62 class
|
|
77
|
+
* 4, §23.3): the kid's, when it is the owner or a device bound to the
|
|
78
|
+
* owner, active or not. Any other kid, known here or not, is neither a
|
|
79
|
+
* dependency nor signature-checked: the offer fails §23.3 item 1 in the
|
|
80
|
+
* authority class (ownerAction), AUTHORIZATION_FAILED.
|
|
81
|
+
*/
|
|
82
|
+
export function offerSigner(state, kid) {
|
|
83
|
+
if (isOwner(state, kid))
|
|
84
|
+
return state.owner;
|
|
85
|
+
const d = state.devices.get(toHex(kid));
|
|
86
|
+
return d !== undefined && isOwner(state, d.root) ? state.principals.get(toHex(kid)) : undefined;
|
|
87
|
+
}
|
|
88
|
+
/** `sameRoot(a, b)` (§25.2): the same `actsFor`, and at least one of them an active device. */
|
|
89
|
+
export const sameRoot = (state, a, b) => bytesEqual(actsFor(state, a), actsFor(state, b)) &&
|
|
90
|
+
(activeDevice(state, a) !== undefined || activeDevice(state, b) !== undefined);
|
|
91
|
+
const sameSet = (a, b) => a.length === b.length && new Set([...a, ...b]).size === new Set(a).size;
|
|
92
|
+
/** §7.3 step 4.2–4.4 against `state`, for verifyChain. */
|
|
93
|
+
export function deviceChainChecks(state) {
|
|
94
|
+
return {
|
|
95
|
+
rootAllowed: (root) => state.roles.get(toHex(root)) !== "device",
|
|
96
|
+
bindingMatches: (p) => {
|
|
97
|
+
const id = toHex(p.device.principalId);
|
|
98
|
+
if (state.roles.get(id) === "non-device")
|
|
99
|
+
return false;
|
|
100
|
+
const d = state.devices.get(id);
|
|
101
|
+
return (d === undefined ||
|
|
102
|
+
(bytesEqual(d.root, p.root) &&
|
|
103
|
+
d.kind === p.kind &&
|
|
104
|
+
sameSet(d.mask.map(String), p.mask.map(String)) &&
|
|
105
|
+
sameSet(d.scope.map(toHex), p.scope.map(toHex))));
|
|
106
|
+
},
|
|
107
|
+
isRevoked: (root, device) => revocationOf(state, root, device) !== undefined,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Authority (class 5 of §62), after every dependency and signature
|
|
112
|
+
const ALLOW = Object.freeze({ allowed: true });
|
|
113
|
+
const deny = (reason) => Object.freeze({ allowed: false, reason });
|
|
114
|
+
/** (c3), (c4), (c6) then step 4.2–4.4 of verifyChain at `state`; `inScope` on success. */
|
|
115
|
+
function chainAuthority(state, chain) {
|
|
116
|
+
const intrinsic = checkChainAuthority(chain, state.resourceId);
|
|
117
|
+
if (!intrinsic.valid)
|
|
118
|
+
return deny(intrinsic.reason);
|
|
119
|
+
const s = checkChainState(chain, deviceChainChecks(state));
|
|
120
|
+
if (!s.valid)
|
|
121
|
+
return deny(s.reason);
|
|
122
|
+
return { allowed: true, inScope: intrinsic.inScope };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The device chain of a Genesis (field 5) or a transfer commit (field 2):
|
|
126
|
+
* verifyChain step 4 at `state`, then the leaf is a personal device with
|
|
127
|
+
* `owner/act` (§15, §23.3).
|
|
128
|
+
*/
|
|
129
|
+
export function authorizeOwnerChain(state, chain) {
|
|
130
|
+
const a = chainAuthority(state, chain);
|
|
131
|
+
if (!a.allowed)
|
|
132
|
+
return a;
|
|
133
|
+
const leaf = chain.leaf.payload;
|
|
134
|
+
if (leaf.kind !== DEVICE_KIND.PERSONAL || !hasCode(leaf.mask, DEVICE_ABILITY.OWNER_ACT))
|
|
135
|
+
return deny("the leaf is not a personal device with owner/act (§15, §23.3)");
|
|
136
|
+
return ALLOW;
|
|
137
|
+
}
|
|
138
|
+
/** An active device of `root` with `device/manage`. */
|
|
139
|
+
const manages = (state, issuer, root) => {
|
|
140
|
+
const d = activeDevice(state, issuer);
|
|
141
|
+
return (d !== undefined && bytesEqual(d.root, root) && hasCode(d.mask, DEVICE_ABILITY.DEVICE_MANAGE));
|
|
142
|
+
};
|
|
143
|
+
/** §17.4 rules 4–7, in order (rules 1–3 are structure, dependencies and signatures). */
|
|
144
|
+
export function authorizeDeviceGrant(state, issuer, body) {
|
|
145
|
+
const root = body.root;
|
|
146
|
+
// Rule 4.
|
|
147
|
+
if (!rootStanding(state, root))
|
|
148
|
+
return deny("the root has no standing (§17.4 rule 4)");
|
|
149
|
+
// Rule 5: verifyChain step 4, then a repeated leaf certificate is not a transition.
|
|
150
|
+
const a = chainAuthority(state, body.chain);
|
|
151
|
+
if (!a.allowed)
|
|
152
|
+
return a;
|
|
153
|
+
const leaf = body.chain.leaf;
|
|
154
|
+
const leafId = leaf.payload.device.principalId;
|
|
155
|
+
const existing = state.devices.get(toHex(leafId));
|
|
156
|
+
if (existing?.leafId !== null &&
|
|
157
|
+
existing?.leafId !== undefined &&
|
|
158
|
+
bytesEqual(existing.leafId, leaf.id))
|
|
159
|
+
return deny("the leaf certificate is already the device's current one (§17.4 rule 5)");
|
|
160
|
+
// Rule 6. A personal device submits only its first registration: once
|
|
161
|
+
// it has a chain it never re-certifies itself, not even with device/manage
|
|
162
|
+
// or owner/act, so a device suspended by the cascade waits for the root
|
|
163
|
+
// or another managing device (spec#2 7d75f7d).
|
|
164
|
+
const personal = leaf.payload.kind === DEVICE_KIND.PERSONAL;
|
|
165
|
+
const self = personal && bytesEqual(issuer, leafId);
|
|
166
|
+
if (self && existing !== undefined && existing.chain !== null)
|
|
167
|
+
return deny("a registered device never re-certifies itself (§17.4 rule 6)");
|
|
168
|
+
const allowed = bytesEqual(issuer, root) ||
|
|
169
|
+
self ||
|
|
170
|
+
(personal && body.chain.issuers.some((e) => bytesEqual(e, issuer))) ||
|
|
171
|
+
manages(state, issuer, root) ||
|
|
172
|
+
(isOwner(state, root) && ownerAction(state, issuer));
|
|
173
|
+
if (!allowed)
|
|
174
|
+
return deny("the issuer may not bind or register this device (§17.4 rule 6)");
|
|
175
|
+
// Rule 7.
|
|
176
|
+
if (body.pairingDevice !== undefined) {
|
|
177
|
+
const session = state.devices.get(toHex(body.pairingDevice));
|
|
178
|
+
const consumed = revocationOf(state, root, body.pairingDevice);
|
|
179
|
+
if (session === undefined ||
|
|
180
|
+
session.kind !== DEVICE_KIND.PAIRING ||
|
|
181
|
+
!bytesEqual(session.root, root) ||
|
|
182
|
+
consumed === undefined ||
|
|
183
|
+
consumed.reason !== 3n ||
|
|
184
|
+
consumed.approval === null ||
|
|
185
|
+
!bytesEqual(consumed.approval, leaf.payloadDigest))
|
|
186
|
+
return deny("no consumed pairing approval names this leaf (§17.4 rule 7)");
|
|
187
|
+
}
|
|
188
|
+
// Rule 8 (spec#2 bc03b0f): live bindings per root, grants per pair.
|
|
189
|
+
const fresh = body.chain.certificates.filter((c) => !state.devices.has(toHex(c.payload.device.principalId))).length;
|
|
190
|
+
if (count(state.liveBound, toHex(root)) + fresh > MAX_LIVE_BOUND)
|
|
191
|
+
return deny(`more than ${MAX_LIVE_BOUND} live devices would be bound to the root (§17.4 rule 8)`);
|
|
192
|
+
if ((existing?.grantsAccepted ?? 0) >= MAX_DEVICE_GRANTS)
|
|
193
|
+
return deny(`the device has had ${MAX_DEVICE_GRANTS} Device Grants for its root (§17.4 rule 8)`);
|
|
194
|
+
return ALLOW;
|
|
195
|
+
}
|
|
196
|
+
/** §17.5 rules 3–5, in order. */
|
|
197
|
+
export function authorizeDeviceRevoke(state, issuer, body) {
|
|
198
|
+
const { root, device } = body;
|
|
199
|
+
const binding = state.devices.get(toHex(device));
|
|
200
|
+
// Rule 3: consistency.
|
|
201
|
+
if (binding !== undefined && !bytesEqual(binding.root, root))
|
|
202
|
+
return deny("the device is bound to another root (§17.5 rule 3)");
|
|
203
|
+
if (state.roles.get(toHex(device)) === "non-device")
|
|
204
|
+
return deny("the device is a non-device Principal here (§17.5 rule 3)");
|
|
205
|
+
if (state.roles.get(toHex(root)) === "device")
|
|
206
|
+
return deny("the root is bound as a device here (§17.5 rule 3)");
|
|
207
|
+
if (body.reason === 3n && binding?.kind !== DEVICE_KIND.PAIRING)
|
|
208
|
+
return deny("only a bound pairing device is consumed (§17.5 rule 3)");
|
|
209
|
+
// Rule 4.
|
|
210
|
+
const existing = revocationOf(state, root, device) !== undefined;
|
|
211
|
+
const allowed = (bytesEqual(issuer, root) && rootStanding(state, root)) ||
|
|
212
|
+
manages(state, issuer, root) ||
|
|
213
|
+
ownerAction(state, issuer) ||
|
|
214
|
+
(bytesEqual(issuer, device) && body.reason === 0n && binding?.registered === true && !existing);
|
|
215
|
+
if (!allowed)
|
|
216
|
+
return deny("the issuer may not revoke this device (§17.5 rule 4)");
|
|
217
|
+
// Rule 5 (spec#2 bc03b0f): only a pre-emptive revocation is limited.
|
|
218
|
+
if (existing)
|
|
219
|
+
return deny("the pair is already revoked (§17.5 rule 5)");
|
|
220
|
+
if (binding === undefined && count(state.preemptive, toHex(root)) >= MAX_PREEMPTIVE_REVOCATIONS)
|
|
221
|
+
return deny(`the root has ${MAX_PREEMPTIVE_REVOCATIONS} revocations of unbound Principals (§17.5 rule 5)`);
|
|
222
|
+
return ALLOW;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Binds every device of an accepted chain to its root and records the
|
|
226
|
+
* leaf's chain (§17.4 rule 9, §15, §23.3). Intermediates keep their own
|
|
227
|
+
* registration and current chains. `keepSameLeaf`: a transfer commit
|
|
228
|
+
* leaves a leaf registered with the same certificate ID unchanged.
|
|
229
|
+
* `grant`: a Device Grant, which counts towards its leaf's eight.
|
|
230
|
+
*/
|
|
231
|
+
export function bindChain(state, chain, inScope, keepSameLeaf, grant = false) {
|
|
232
|
+
let { roles, devices, principals, liveBound, preemptive } = state;
|
|
233
|
+
for (const c of chain.certificates) {
|
|
234
|
+
const p = c.payload;
|
|
235
|
+
const id = toHex(p.device.principalId);
|
|
236
|
+
if (!devices.has(id)) {
|
|
237
|
+
devices = put(devices, id, Object.freeze({
|
|
238
|
+
root: p.root,
|
|
239
|
+
kind: p.kind,
|
|
240
|
+
mask: p.mask,
|
|
241
|
+
scope: p.scope,
|
|
242
|
+
registered: false,
|
|
243
|
+
chain: null,
|
|
244
|
+
leafId: null,
|
|
245
|
+
issuers: Object.freeze([]),
|
|
246
|
+
grantsAccepted: 0,
|
|
247
|
+
}));
|
|
248
|
+
// A chain never holds a revoked pair (§7.3 step 4): the binding is live.
|
|
249
|
+
liveBound = put(liveBound, toHex(p.root), count(liveBound, toHex(p.root)) + 1);
|
|
250
|
+
// Its pre-emptive revocations no longer count: it is bound now.
|
|
251
|
+
for (const r of state.preemptiveRoots.get(id)?.keys() ?? [])
|
|
252
|
+
preemptive = put(preemptive, r, count(preemptive, r) - 1);
|
|
253
|
+
}
|
|
254
|
+
if (roles.get(id) !== "device")
|
|
255
|
+
roles = put(roles, id, "device");
|
|
256
|
+
if (principals.get(id) !== p.device)
|
|
257
|
+
principals = put(principals, id, p.device);
|
|
258
|
+
}
|
|
259
|
+
const leafKey = toHex(chain.leaf.payload.device.principalId);
|
|
260
|
+
const leaf = devices.get(leafKey);
|
|
261
|
+
const grantsAccepted = leaf.grantsAccepted + (grant ? 1 : 0);
|
|
262
|
+
if (keepSameLeaf &&
|
|
263
|
+
leaf.registered &&
|
|
264
|
+
leaf.leafId !== null &&
|
|
265
|
+
bytesEqual(leaf.leafId, chain.leaf.id)) {
|
|
266
|
+
if (grant)
|
|
267
|
+
devices = put(devices, leafKey, Object.freeze({ ...leaf, grantsAccepted }));
|
|
268
|
+
}
|
|
269
|
+
else
|
|
270
|
+
devices = put(devices, leafKey, Object.freeze({
|
|
271
|
+
...leaf,
|
|
272
|
+
chain,
|
|
273
|
+
leafId: chain.leaf.id,
|
|
274
|
+
issuers: chain.issuers,
|
|
275
|
+
registered: leaf.registered || inScope,
|
|
276
|
+
grantsAccepted,
|
|
277
|
+
}));
|
|
278
|
+
return { ...state, roles, devices, principals, liveBound, preemptive };
|
|
279
|
+
}
|
|
280
|
+
/** The in-scope result of an accepted DEVICE_GRANT's chain at the state it was accepted on. */
|
|
281
|
+
export const grantInScope = (state, body) => {
|
|
282
|
+
const a = checkChainAuthority(body.chain, state.resourceId);
|
|
283
|
+
return a.valid && a.inScope;
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* Records the revocation of an accepted DEVICE_REVOKE (§17.5 rule 6): a
|
|
287
|
+
* bound device releases its live place (§17.4 rule 8), an unbound one
|
|
288
|
+
* spends one of the root's pre-emptive revocations (§17.5 rule 5).
|
|
289
|
+
*/
|
|
290
|
+
export function recordRevocation(state, record, body) {
|
|
291
|
+
const root = toHex(body.root);
|
|
292
|
+
const device = toHex(body.device);
|
|
293
|
+
const revoked = put(state.revoked, revocationKey(body.root, body.device), Object.freeze({
|
|
294
|
+
root: body.root,
|
|
295
|
+
device: body.device,
|
|
296
|
+
reason: body.reason,
|
|
297
|
+
approval: body.approval ?? null,
|
|
298
|
+
revokedBy: controlRecordId(record.signed.id),
|
|
299
|
+
}));
|
|
300
|
+
if (state.devices.has(device))
|
|
301
|
+
return {
|
|
302
|
+
revoked,
|
|
303
|
+
liveBound: put(state.liveBound, root, count(state.liveBound, root) - 1),
|
|
304
|
+
preemptive: state.preemptive,
|
|
305
|
+
preemptiveRoots: state.preemptiveRoots,
|
|
306
|
+
};
|
|
307
|
+
return {
|
|
308
|
+
revoked,
|
|
309
|
+
liveBound: state.liveBound,
|
|
310
|
+
preemptive: put(state.preemptive, root, count(state.preemptive, root) + 1),
|
|
311
|
+
preemptiveRoots: put(state.preemptiveRoots, device, put(state.preemptiveRoots.get(device) ?? PersistentMap.empty(), root, true)),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/** Assigns the `non-device` role (§79). */
|
|
315
|
+
export function nonDevice(roles, p) {
|
|
316
|
+
return put(roles, toHex(p), "non-device");
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=device-state.js.map
|
package/dist/device.d.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { type Hash32, type PrincipalId, type ResourceId } from "@openlfcp/core";
|
|
2
|
+
import { type CborValue } from "./cbor/index.js";
|
|
3
|
+
import { type SignedObject, type Signer } from "./cose.js";
|
|
4
|
+
import { type PrincipalDescriptor } from "./principal.js";
|
|
5
|
+
/**
|
|
6
|
+
* Device Certificates and certificate chains (LFCP-WIRE-01 §7.2, §7.3,
|
|
7
|
+
* IDENTITY-01): the codec, the structural rules, and `verifyChain` with
|
|
8
|
+
* the class order of §62 (structure, dependencies, signatures, authority
|
|
9
|
+
* and state).
|
|
10
|
+
*
|
|
11
|
+
* Structure only decides what can be decided from the bytes: every
|
|
12
|
+
* failure of §7.3 rules 1–5, (c1), (c5) and (c7) is INVALID_STRUCTURE
|
|
13
|
+
* (MALFORMED_MESSAGE on the wire), thrown while decoding. The state
|
|
14
|
+
* questions of §7.3 step 4.2–4.4 (roles, bindings, revocations) are asked
|
|
15
|
+
* through the read-only `ChainStateChecks`; this module stores no state
|
|
16
|
+
* and applies no transition.
|
|
17
|
+
*
|
|
18
|
+
* Unknown mask codes (and 9, 11) are kept as received, in received order,
|
|
19
|
+
* and never promoted to abilities. Exact certificate bytes are kept: a
|
|
20
|
+
* chain re-encodes to the byte strings it was read from.
|
|
21
|
+
*/
|
|
22
|
+
/** §7.3 rule 1: the domain and version label, payload element 0. */
|
|
23
|
+
export declare const DEVICE_CERT_LABEL = "LFCP-DEVICE-CERT-v1";
|
|
24
|
+
/** §7.3: `certificate-chain = [1*4 bstr]`. */
|
|
25
|
+
export declare const MAX_CHAIN_LENGTH = 4;
|
|
26
|
+
/** §7.3 (c7): a chain whose leaf lists `device/manage` has at most three certificates. */
|
|
27
|
+
export declare const MAX_MANAGING_CHAIN_LENGTH = 3;
|
|
28
|
+
/** §7.3 rule 4: `device-mask = [1*32 uint]`. */
|
|
29
|
+
export declare const MAX_MASK_ENTRIES = 32;
|
|
30
|
+
/** §7.3: `resource-scope = [*64 resource-id]`. */
|
|
31
|
+
export declare const MAX_SCOPE_ENTRIES = 64;
|
|
32
|
+
/** §7.3: `bstr .size 16`, the serial. */
|
|
33
|
+
export declare const SERIAL_LENGTH = 16;
|
|
34
|
+
/** §17.1 codes that have meaning only inside a Device Certificate mask. */
|
|
35
|
+
export declare const DEVICE_ABILITY: Readonly<{
|
|
36
|
+
/** `device/manage`: may sign Device Certificates and issue device records for its root. */
|
|
37
|
+
DEVICE_MANAGE: 12n;
|
|
38
|
+
/** `owner/act`: in a Resource its root owns, holds every standard ability. */
|
|
39
|
+
OWNER_ACT: 13n;
|
|
40
|
+
}>;
|
|
41
|
+
/** §7.2 kinds: 0 personal, 1 service, 2 pairing. 3 and above are reserved. */
|
|
42
|
+
export declare const DEVICE_KIND: Readonly<{
|
|
43
|
+
readonly PERSONAL: 0;
|
|
44
|
+
readonly SERVICE: 1;
|
|
45
|
+
readonly PAIRING: 2;
|
|
46
|
+
}>;
|
|
47
|
+
export type DeviceKind = (typeof DEVICE_KIND)[keyof typeof DEVICE_KIND];
|
|
48
|
+
/** `device-certificate-payload` (§7.3). */
|
|
49
|
+
export interface DeviceCertificatePayload {
|
|
50
|
+
/** Field 1: the root the device acts for. */
|
|
51
|
+
readonly root: PrincipalId;
|
|
52
|
+
/** Field 2: the device (self-certifying, §7). */
|
|
53
|
+
readonly device: PrincipalDescriptor;
|
|
54
|
+
readonly kind: DeviceKind;
|
|
55
|
+
/** Field 4: 1–32 distinct codes, in received order. Unknown codes confer nothing. */
|
|
56
|
+
readonly mask: readonly bigint[];
|
|
57
|
+
/** Field 5: distinct Resource IDs, subject to the kind rules. */
|
|
58
|
+
readonly scope: readonly ResourceId[];
|
|
59
|
+
/** Field 6: 16 random bytes; no order, no time. */
|
|
60
|
+
readonly serial: Uint8Array;
|
|
61
|
+
}
|
|
62
|
+
/** A Device Certificate: the exact signed object and its payload. */
|
|
63
|
+
export interface DeviceCertificate {
|
|
64
|
+
readonly signed: SignedObject;
|
|
65
|
+
readonly payload: DeviceCertificatePayload;
|
|
66
|
+
/** §7.3 rule 6: the §10.6 object ID of the exact COSE bytes. */
|
|
67
|
+
readonly id: Hash32;
|
|
68
|
+
/** §7.3 rule 6: SHA-256 of the exact payload bytes; what a pairing approval commits to. */
|
|
69
|
+
readonly payloadDigest: Hash32;
|
|
70
|
+
}
|
|
71
|
+
/** A chain well-formed for `root` (§7.3 (c1), (c5), (c7)): leaf first, issued by the root last. */
|
|
72
|
+
export interface CertificateChain {
|
|
73
|
+
readonly root: PrincipalId;
|
|
74
|
+
readonly certificates: readonly DeviceCertificate[];
|
|
75
|
+
readonly leaf: DeviceCertificate;
|
|
76
|
+
/** The devices that issued inside the chain (`c_1 .. c_{n-1}`), leaf's issuer first. */
|
|
77
|
+
readonly issuers: readonly PrincipalId[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* §7.3 rules 2–5 on a payload: a 32-byte root, a valid device descriptor
|
|
81
|
+
* (§7) that is not the root; a mask of 1–32
|
|
82
|
+
* distinct unsigned 64-bit codes, with `1` and `2` whenever it lists `12`; at most 64
|
|
83
|
+
* distinct scope entries; and the kind rules for scope and mask. Throws
|
|
84
|
+
* INVALID_STRUCTURE. Writers and readers apply the same rules.
|
|
85
|
+
*/
|
|
86
|
+
export declare function checkDeviceCertificatePayload(p: DeviceCertificatePayload): void;
|
|
87
|
+
/**
|
|
88
|
+
* Decodes `device-certificate-payload` (§7.3): a seven-element array
|
|
89
|
+
* labelled `LFCP-DEVICE-CERT-v1`, then rules 2–5. Array lengths are
|
|
90
|
+
* checked before their entries are decoded.
|
|
91
|
+
*/
|
|
92
|
+
export declare function deviceCertificatePayloadFromCbor(value: CborValue): DeviceCertificatePayload;
|
|
93
|
+
/** The payload as the §7.3 array. */
|
|
94
|
+
export declare function deviceCertificatePayloadToCbor(p: DeviceCertificatePayload): CborValue;
|
|
95
|
+
/** Deterministic CBOR of a payload that satisfies §7.3 rules 1–5 (INVALID_STRUCTURE otherwise). */
|
|
96
|
+
export declare function encodeDeviceCertificatePayload(p: DeviceCertificatePayload): Uint8Array;
|
|
97
|
+
/**
|
|
98
|
+
* §7.3 rule 6: the certificate payload digest, SHA-256 of the payload's
|
|
99
|
+
* deterministic CBOR. Computed before the certificate exists, it is what a
|
|
100
|
+
* `DEVICE_REVOKE` with reason 3 commits to (§17.5, invariant N9).
|
|
101
|
+
*/
|
|
102
|
+
export declare const deviceCertificatePayloadDigest: (p: DeviceCertificatePayload) => Hash32;
|
|
103
|
+
/** A fresh random serial (§7.3 field 6, §5.4). */
|
|
104
|
+
export declare const newDeviceCertificateSerial: () => Uint8Array;
|
|
105
|
+
/**
|
|
106
|
+
* Parses a Device Certificate from its exact COSE bytes: the canonical
|
|
107
|
+
* COSE_Sign1 shape with the §5.2 canonical-bytes check of its protected
|
|
108
|
+
* header and payload (cose.ts), then the payload rules. The signature is
|
|
109
|
+
* not verified here; its signer is decided by chain position (§7.3 (c2)).
|
|
110
|
+
*/
|
|
111
|
+
export declare function parseDeviceCertificate(bytes: Uint8Array): DeviceCertificate;
|
|
112
|
+
/**
|
|
113
|
+
* Signs a Device Certificate. `signer` is the root or a managing device of
|
|
114
|
+
* the root; which one is valid is decided by the chain it is placed in.
|
|
115
|
+
* The payload must satisfy §7.3 rules 1–5 (INVALID_STRUCTURE otherwise).
|
|
116
|
+
*/
|
|
117
|
+
export declare function signDeviceCertificate(payload: DeviceCertificatePayload, signer: Signer): DeviceCertificate;
|
|
118
|
+
/** Builds a chain (leaf first) for `root`, applying (c1), (c5) and (c7). INVALID_STRUCTURE otherwise. */
|
|
119
|
+
export declare const certificateChain: (certificates: readonly DeviceCertificate[], root: PrincipalId) => CertificateChain;
|
|
120
|
+
/**
|
|
121
|
+
* Decodes a received `certificate-chain` for `root`: an array of one to
|
|
122
|
+
* four byte strings (checked before any certificate is parsed), each a
|
|
123
|
+
* Device Certificate, then (c1), (c5), (c7). Every certificate is parsed
|
|
124
|
+
* in full before any chain rule, so every structural failure of every
|
|
125
|
+
* certificate is reported as structure (§62 class 1).
|
|
126
|
+
*/
|
|
127
|
+
export declare function certificateChainFromCbor(value: CborValue, root: PrincipalId): CertificateChain;
|
|
128
|
+
/** The chain as the §7.3 array of exact certificate bytes (never re-encoded). */
|
|
129
|
+
export declare const certificateChainToCbor: (chain: CertificateChain) => CborValue;
|
|
130
|
+
/** The rule that decided a chain verdict, for diagnostics and tests. */
|
|
131
|
+
export type DeviceChainRule =
|
|
132
|
+
/** (c1): the chain is for another root than the one the record names. */
|
|
133
|
+
"c1-root"
|
|
134
|
+
/** (c1), (c5), (c7) or rules 1–5 on a prospective certificate (preflight). */
|
|
135
|
+
| "structure"
|
|
136
|
+
/** §7.3 step 2: the root's descriptor is not available. */
|
|
137
|
+
| "root-descriptor"
|
|
138
|
+
/** (c2): a certificate's kid is not the Principal its position requires. */
|
|
139
|
+
| "c2-kid"
|
|
140
|
+
/** (c2): a certificate's signature does not verify. */
|
|
141
|
+
| "c2-signature"
|
|
142
|
+
/** (c3): an issuer is not a managing personal device. */
|
|
143
|
+
| "c3"
|
|
144
|
+
/** (c4): a mask is not a subset of its issuer's mask. */
|
|
145
|
+
| "c4"
|
|
146
|
+
/** (c6): a pairing leaf whose scope does not list the Resource. */
|
|
147
|
+
| "c6"
|
|
148
|
+
/** step 4.2: `roles[R]` is `device`. */
|
|
149
|
+
| "root-role"
|
|
150
|
+
/** step 4.3: a device's role or existing binding conflicts with this chain (N8). */
|
|
151
|
+
| "binding"
|
|
152
|
+
/** step 4.4: `(R, E)` is revoked. */
|
|
153
|
+
| "revoked";
|
|
154
|
+
export type ChainWireCode = "MALFORMED_MESSAGE" | "MISSING_DEPENDENCY" | "INVALID_SIGNATURE" | "AUTHORIZATION_FAILED";
|
|
155
|
+
/** A refusal: the first failing rule in §62 class order and its wire code. */
|
|
156
|
+
export interface ChainRefusal {
|
|
157
|
+
readonly valid: false;
|
|
158
|
+
readonly rule: DeviceChainRule;
|
|
159
|
+
readonly wireCode: ChainWireCode;
|
|
160
|
+
readonly reason: string;
|
|
161
|
+
}
|
|
162
|
+
/** An accepted chain (§7.3 result): evidence for the state validator, not a transition. */
|
|
163
|
+
export interface VerifiedChain {
|
|
164
|
+
readonly valid: true;
|
|
165
|
+
readonly chain: CertificateChain;
|
|
166
|
+
/** The leaf's payload: its descriptor, kind, mask and scope. */
|
|
167
|
+
readonly leaf: DeviceCertificatePayload;
|
|
168
|
+
readonly leafId: Hash32;
|
|
169
|
+
readonly issuers: readonly PrincipalId[];
|
|
170
|
+
/** False only for a kind-1 leaf whose scope does not list the Resource (bound, never registered). */
|
|
171
|
+
readonly inScope: boolean;
|
|
172
|
+
/** Every certificate's payload, leaf first: the bindings to record (§79). */
|
|
173
|
+
readonly bindings: readonly DeviceCertificatePayload[];
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Read-only view of the previous Control state for §7.3 step 4.2–4.4.
|
|
177
|
+
* Mask and scope are compared as sets; serial and issuer may differ.
|
|
178
|
+
*/
|
|
179
|
+
export interface ChainStateChecks {
|
|
180
|
+
/** Step 4.2: `roles[root]` is absent or `non-device`. */
|
|
181
|
+
rootAllowed(root: PrincipalId): boolean;
|
|
182
|
+
/**
|
|
183
|
+
* Step 4.3: `roles[device]` is absent or `device`, and an existing
|
|
184
|
+
* `devices[device]` has this root and the certificate's kind, mask and scope.
|
|
185
|
+
*/
|
|
186
|
+
bindingMatches(certificate: DeviceCertificatePayload): boolean;
|
|
187
|
+
/** Step 4.4: whether `(root, device)` is in `revoked`. */
|
|
188
|
+
isRevoked(root: PrincipalId, device: PrincipalId): boolean;
|
|
189
|
+
}
|
|
190
|
+
/** The empty state of a Genesis (§15: `S = empty`); nothing is bound or revoked. */
|
|
191
|
+
export declare const EMPTY_CHAIN_STATE: ChainStateChecks;
|
|
192
|
+
/**
|
|
193
|
+
* §7.3 step 3 only (class 4): (c2) for every certificate, leaf first.
|
|
194
|
+
* A validator of a record that embeds a chain resolves every dependency
|
|
195
|
+
* of the record (outer issuer and chain root) before any signature, so
|
|
196
|
+
* this is separate from the descriptor lookup (§62).
|
|
197
|
+
*/
|
|
198
|
+
export declare function verifyChainSignatures(chain: CertificateChain, rootDescriptor: PrincipalDescriptor): ChainRefusal | {
|
|
199
|
+
readonly valid: true;
|
|
200
|
+
};
|
|
201
|
+
/** §7.3 step 4.1 (class 5): (c3), (c4), (c6). `inScope` as in VerifiedChain. */
|
|
202
|
+
export declare function checkChainAuthority(chain: CertificateChain, resource: ResourceId): ChainRefusal | {
|
|
203
|
+
readonly valid: true;
|
|
204
|
+
readonly inScope: boolean;
|
|
205
|
+
};
|
|
206
|
+
/** §7.3 step 4.2–4.4 (class 5): the root's role, then every binding, then every revocation. */
|
|
207
|
+
export declare function checkChainState(chain: CertificateChain, state: ChainStateChecks): ChainRefusal | {
|
|
208
|
+
readonly valid: true;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* `verifyChain(chain, R, R_descriptor, S)` (§7.3) in Resource `resource`:
|
|
212
|
+
* one pass of the §62 classes. Structure was checked when the chain was
|
|
213
|
+
* decoded; here (c1) is checked against `root`, then the root descriptor
|
|
214
|
+
* (MISSING_DEPENDENCY when undefined), then every signature, then (c3),
|
|
215
|
+
* (c4), (c6), then the state predicates. Pure; reads no clock.
|
|
216
|
+
*/
|
|
217
|
+
export declare function verifyChain(chain: CertificateChain, root: PrincipalId, rootDescriptor: PrincipalDescriptor | undefined, resource: ResourceId, state: ChainStateChecks): VerifiedChain | ChainRefusal;
|
|
218
|
+
/**
|
|
219
|
+
* Checks a prospective leaf before it is signed, against the issuing
|
|
220
|
+
* device's chain (`issuers`, leaf-first; empty when the root issues): the
|
|
221
|
+
* structure of the chain the leaf would form, the root descriptor, the
|
|
222
|
+
* issuers' signatures, then (c3), (c4), (c6). Lets a managing installation
|
|
223
|
+
* refuse an issuance (managing depth 4, an empty service scope) before it
|
|
224
|
+
* consumes a pairing session (§7.3 N9, IDENTITY-PROFILE-01 §5). Current
|
|
225
|
+
* state (standing, activity) is the caller's to check.
|
|
226
|
+
*/
|
|
227
|
+
export declare function preflightIssuance(leaf: DeviceCertificatePayload, issuers: readonly DeviceCertificate[], rootDescriptor: PrincipalDescriptor | undefined, resource: ResourceId): ChainRefusal | {
|
|
228
|
+
readonly valid: true;
|
|
229
|
+
readonly inScope: boolean;
|
|
230
|
+
};
|
|
231
|
+
//# sourceMappingURL=device.d.ts.map
|