@openlfcp/wire 0.2.2 → 0.3.1

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.
@@ -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
@@ -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