@ziffer-io/verify 0.1.0 → 0.2.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/quorum.js ADDED
@@ -0,0 +1,566 @@
1
+ /**
2
+ * §9.3 step 7b -- the AT-* quorum and §8.6b's AB-*, run in the CUSTOMER's own
3
+ * process against the CUSTOMER's own signed bundle.
4
+ *
5
+ * # Why this exists (ACP-406 gap 1)
6
+ *
7
+ * Until this module, a customer verifying a ZIFFER receipt checked the
8
+ * signature and the bindings and **trusted us for the approvals**: the header
9
+ * table in `verify.ts` said "7b -- NO, needs the attester registry". A
10
+ * compromised receipt-signing substrate could therefore emit a floor-HIGH
11
+ * receipt carrying fabricated `attestation_digests`, and every customer-side
12
+ * verifier would accept it. The registry is in the customer's OWN signed
13
+ * bundle -- the tree they already load for `policy_bundle_hash` -- so nothing
14
+ * about that trust was necessary. This is the third implementation of the
15
+ * step, after `reference/src/acp_executor.py::_verify_quorum` and
16
+ * `crates/acp-decision/src/quorum.rs`, and the clause names are theirs.
17
+ *
18
+ * # AT-9 is TWO requirements and they fail closed on disjoint inputs
19
+ *
20
+ * 1. The threshold is **recomputed** from the signed bundle's `quorum_k`
21
+ * (PB-6) and never read from an attestation. Through v1.3.14 the reference
22
+ * read `entries[0].required_count` -- asking the party under verification
23
+ * how many signatures to demand -- and one compromised attester key signing
24
+ * one object carrying `required_count: 1` executed a floor-HIGH action.
25
+ * 2. **Every** entry's `required_count` must equal `quorum_k`. That is a
26
+ * CONSENT check (AT-3), not a threshold one: two humans approving while both
27
+ * objects say `3` means they approved believing a third reviewer existed.
28
+ *
29
+ * Deleting (2) cannot lower a quorum; deleting (1) cannot detect an attester
30
+ * who signed under a different stated threshold. Keeping only (1) satisfies
31
+ * INV-1-HIGH while silently executing actions no attester agreed to.
32
+ *
33
+ * # What this CANNOT check customer-side, and why -- read before trusting a pass
34
+ *
35
+ * These are absences, not agreements. Each one is a step of §9.3 this package
36
+ * does not run, and each is disclosed here rather than approximated:
37
+ *
38
+ * - **Whether a quorum was required at all.** §9.3 step 7 recomputes the
39
+ * floor-only risk from the bundle's `floors.json` and `risk_functions.json`;
40
+ * this package does not grade, so it cannot know that a receipt carrying an
41
+ * EMPTY attestation list should have carried a quorum. A receipt with no
42
+ * attestations passes 7b vacuously (AB-1's own note covers the legitimate
43
+ * LOW/MEDIUM case) and {@link verifyQuorum} returns `null` so the caller can
44
+ * see that nothing was verified. **Reading `risk_level_floor_only` off the
45
+ * receipt instead would be X1 -- the defect where a compromised issuer
46
+ * asserting `LOW` suppressed attestation entirely** -- so it is not read.
47
+ * - **`obj.floor_only_risk` against the recomputed grade** (7b(iii), TR-8), for
48
+ * the same reason: there is no recomputed grade here to compare against.
49
+ * - **Single-use of the recomputed `attestation_id`** (7b(v), CL-3). That needs
50
+ * a linearizable Consumption Ledger and this package holds none. Replay
51
+ * protection that forgets is not replay protection, so it is named rather
52
+ * than half-built.
53
+ * - **HM-4 (f)'s stored signature counter** is `T` at this boundary: the caller
54
+ * hands in what it stored and MUST persist what {@link QuorumOutcome}
55
+ * returns. A caller passing an empty map every time has switched (f) off for
56
+ * a credential that has signed before.
57
+ *
58
+ * # One deliberate NARROWING beyond both engine implementations
59
+ *
60
+ * AT-1 says role membership "is resolved against the attester registry", and
61
+ * neither engine implementation resolves it: `quorum.rs` records that
62
+ * `AttesterRole` is generated and read by nothing, and the Python reference has
63
+ * no role field at all. This one does, because the customer's registry carries
64
+ * `role` and their bundle is the authority on who may approve: an entry counts
65
+ * toward the quorum only if its attester's REGISTRY role is named in the
66
+ * object's `required_roles`. An empty list means the rule demanded a quorum and
67
+ * named no role, so any enrolled attester satisfies it (AT-1 as amended in
68
+ * v1.3.32, ACP-308) -- which is byte-for-byte the engine's behaviour, and is the
69
+ * case every ZIFFER deployment is in today except where the policy engine sets
70
+ * `["approver"]`.
71
+ *
72
+ * This is a NARROWER acceptance, never a wider one, and it is stated because a
73
+ * third implementation that refuses what the other two accept is a divergence
74
+ * even when it is the safer side. Its strength is bounded and that is disclosed
75
+ * too: `required_roles` is read from the OBJECT (class **B** -- signed by the
76
+ * attester, evidence of what they were shown), because the bundle names no
77
+ * per-rule role list for it to be recomputed from. A compromised issuer that
78
+ * writes an empty list degrades this check to the engine's behaviour and no
79
+ * further.
80
+ *
81
+ * # Suite 12 classification -- R / B / T for every control input read here
82
+ *
83
+ * | input | class | why |
84
+ * | --- | :---: | --- |
85
+ * | `quorumK` | **R** | from the signed bundle, never an attestation (AT-9) |
86
+ * | attester keys and credentials | **R** | read out of the signed registry after the bundle's own signature checked; `rpId` above all, because HM-4 (c) compares the assertion's origin against THIS and an origin taken from the assertion would compare a value with itself |
87
+ * | `floor` | **R** | the signed manifest's `min_suite` (CR-4) |
88
+ * | `policyBundleHash`, `bundleEpoch` | **R** | the bundle the caller trusts; the object's copies are compared against them (7b(iii)) |
89
+ * | the eleven AT-1 fields | **B** | the attester signature covers the derived id, which covers the whole object |
90
+ * | `obj.proposal_hash` | **B** | and compared against the hash this verifier recomputed from the caller's own proposal bytes (Y1) |
91
+ * | `obj.required_count` | **B** | and compared against `quorumK` (AT-9's consent half) |
92
+ * | `obj.required_roles` | **B** | signed, and resolved against the registry; see the narrowing above for what it does NOT prove |
93
+ * | `entry.attester` | **B** | a name SELECTS a key; the signature establishes identity (ACK-4) |
94
+ * | `entry.attestation_id` | **R** | derived; a transmitted value is compared, never used (Y1b) |
95
+ * | `attestation_digests` | **B** | inside the signed receipt body (AB-5), and each entry's digest is recomputed from the received bytes (AB-1) |
96
+ * | `entry.kind` | **B** | bound by the entry digest under the receipt signature (AB-1). **NARROWED RESIDUAL, not closed**: the attester does not sign `kind`, so the ISSUER still chooses per entry whose signature counts as an approval |
97
+ * | `webauthnCounters` | **T** | the caller's stored counters; this package holds no ledger and says so above |
98
+ */
99
+ import { createHash } from 'node:crypto';
100
+ import { canon } from './canon.js';
101
+ import { CLAUSE_ASSURANCE, CLAUSE_ATTESTATION_SUITE, CLAUSE_ATTESTER_SIG, CLAUSE_ATT_NONCE_SIZE, CLAUSE_BINDING, CLAUSE_CARDINALITY, CLAUSE_CONSENT, CLAUSE_DERIVED_ID, CLAUSE_DIGEST_DUP, CLAUSE_DIGEST_ORDER, CLAUSE_MALFORMED, CLAUSE_MEMBERSHIP, CLAUSE_NO_ATTESTATIONS, CLAUSE_NO_OBJECT, CLAUSE_OBJECT_SCHEMA, CLAUSE_OPERATOR_DISAGREE, CLAUSE_OPERATOR_SELF, CLAUSE_POLICY_BASIS, CLAUSE_QUORUM, CLAUSE_WEBAUTHN_ALG, CLAUSE_WEBAUTHN_COUNTER, CLAUSE_WIRE_TYPE, } from './clauses.js';
102
+ import { parseInstant } from './instant.js';
103
+ import { Refusal } from './refusal.js';
104
+ import { hex, parseSig, verifyUnderSuite } from './sig.js';
105
+ import { parseSuite, satisfiesFloor } from './suite.js';
106
+ import { WEBAUTHN_ALGS, WEBAUTHN_PRIMITIVE, WebauthnRefusal, WEBAUTHN_AUTHENTICATOR_DATA, WEBAUTHN_CHALLENGE, WEBAUTHN_ORIGIN, WEBAUTHN_SIGNATURE, WEBAUTHN_TYPE, verifyWebauthn, } from './webauthn.js';
107
+ import { b64Decode, isRecord, isWe4B64, NONCE128_BYTES, NONCE128_LEN, strField } from './wire.js';
108
+ /**
109
+ * AT-8b: the Attestation Object schema is CLOSED. Exactly this field set, no
110
+ * more and no less.
111
+ *
112
+ * An unknown field is refused rather than ignored and a missing one is never
113
+ * defaulted (Z4). Normalising either would let an issuer add a field a future
114
+ * verifier reads, or drop one this verifier checks, without invalidating a
115
+ * signature that covers only what is present.
116
+ */
117
+ export const AT1_FIELDS = [
118
+ 'alg',
119
+ 'att_nonce',
120
+ 'bundle_epoch',
121
+ 'context_snapshot_hash',
122
+ 'expires_at',
123
+ 'floor_only_risk',
124
+ 'operator',
125
+ 'policy_bundle_hash',
126
+ 'proposal_hash',
127
+ 'required_count',
128
+ 'required_roles',
129
+ ];
130
+ /** The four fields AB-1's preimage must carry, plus the one it may. */
131
+ const ENTRY_REQUIRED = ['obj', 'kind', 'attester', 'sig'];
132
+ const ENTRY_ALLOWED = [...ENTRY_REQUIRED, 'attestation_id'];
133
+ /** AT-10's ladder. Ranked, unlike suites, whose floor is containment (CR-4). */
134
+ const ASSURANCE_ORDER = ['AS0', 'AS1', 'AS2'];
135
+ /**
136
+ * AT-8a -- `sha256:` over the canonical bytes, the reference's `h()`.
137
+ *
138
+ * The canonical encoding is `canon.ts`'s, which is the engine's
139
+ * `acp_executor.canon` / `receipt.rs::canon`. AT-8a names canonical CBOR and
140
+ * both engine implementations use canonical JSON here; this package reproduces
141
+ * what the engine actually signs, because a verifier that hashed a different
142
+ * preimage would refuse every genuine attestation. The divergence is the
143
+ * engine's and is recorded in `acp_executor.canon`'s own docstring.
144
+ */
145
+ function digestOf(value) {
146
+ return `sha256:${hex(Uint8Array.from(createHash('sha256').update(canon(value)).digest()))}`;
147
+ }
148
+ /**
149
+ * AT-8a -- derive an Attestation Object's id from its canonical bytes.
150
+ * **Derived, never read**: the id names a binding, and a transmitted name for a
151
+ * binding is not evidence of one (Y1b).
152
+ */
153
+ export function attestationId(obj) {
154
+ return digestOf(obj);
155
+ }
156
+ /**
157
+ * AB-1 -- the receipt's commitment to ONE attestation entry.
158
+ *
159
+ * ENTRY-SCOPED, NEVER OBJECT-SCOPED, and this is the whole trap.
160
+ * {@link attestationId} already exists and is `h(obj)` -- reusing it here is the
161
+ * obvious move and it is wrong. {@link AT1_FIELDS} is closed over the OBJECT: it
162
+ * contains no `kind`, no `attester` and no `sig`. Quorum composition reads
163
+ * `kind` and `attester` from the ENTRY and the attester signs neither, so an
164
+ * object-scoped digest lets an attacker on the transport path flip `kind` from
165
+ * `confirmation` to `approval` while every digest still matches.
166
+ *
167
+ * The two identifiers MUST NOT be merged: `attestation_id` is what a ledger
168
+ * consumes for single-use under CL-3, because the same object replayed inside a
169
+ * different entry is still a replay. This digest is what the receipt authorised,
170
+ * byte for byte.
171
+ */
172
+ export function entryDigest(entry) {
173
+ if (!isRecord(entry)) {
174
+ throw new Refusal(CLAUSE_MEMBERSHIP, 'attestation entry is not a map');
175
+ }
176
+ const present = Object.keys(entry);
177
+ const missing = ENTRY_REQUIRED.filter((k) => !present.includes(k));
178
+ const extra = present.filter((k) => !ENTRY_ALLOWED.includes(k));
179
+ if (missing.length > 0 || extra.length > 0) {
180
+ throw new Refusal(CLAUSE_MEMBERSHIP, `entry schema violation missing=${JSON.stringify(missing)} extra=${JSON.stringify(extra)}`);
181
+ }
182
+ return digestOf(entry);
183
+ }
184
+ function intField(obj, name, clause) {
185
+ const raw = obj[name];
186
+ if (typeof raw !== 'number' || !Number.isSafeInteger(raw) || raw < 0) {
187
+ throw new Refusal(clause, `attestation object field ${name} is not an integer`);
188
+ }
189
+ return raw;
190
+ }
191
+ function requiredStr(obj, name, clause) {
192
+ const v = strField(obj, name);
193
+ if (v === null) {
194
+ throw new Refusal(clause, `attestation object field ${name} is not a string`);
195
+ }
196
+ return v;
197
+ }
198
+ /** The HM-4 step names that travel out as the refusal's clause. */
199
+ const HM4_STEPS = [
200
+ WEBAUTHN_TYPE,
201
+ WEBAUTHN_CHALLENGE,
202
+ WEBAUTHN_ORIGIN,
203
+ WEBAUTHN_AUTHENTICATOR_DATA,
204
+ WEBAUTHN_SIGNATURE,
205
+ ];
206
+ /**
207
+ * HM-3 and HM-4 (a)-(e) over one entry, returning the counter (f) compares.
208
+ *
209
+ * **HM-3 IS CHECKED BEFORE ANY BYTE OF THE ASSERTION IS EXAMINED.** `alg` is an
210
+ * AT-1 field and therefore signature-covered (CR-5), but that only proves the
211
+ * ISSUER chose it: the registry is what says which kind of key this identity
212
+ * holds. Parsing attacker-supplied bytes under the wrong algorithm is work done
213
+ * on the attacker's behalf.
214
+ */
215
+ function verifyWebauthnEntry(cred, objAlg, sig, messageId) {
216
+ if (objAlg !== cred.alg) {
217
+ throw new Refusal(CLAUSE_WEBAUTHN_ALG, `HM-3: attestation alg ${objAlg} is not the registered alg ${cred.alg}`);
218
+ }
219
+ // HM-3: EXACTLY the one primitive. A map carrying `webauthn` beside
220
+ // `classical` would be a human leg with a machine leg stapled to it, which is
221
+ // not a stronger signature -- it is two different claims about who decided,
222
+ // and CR-3's conjunction has nothing to say about a pair that names two
223
+ // holders.
224
+ const keys = isRecord(sig) ? Object.keys(sig) : [];
225
+ const encoded = isRecord(sig) ? sig[WEBAUTHN_PRIMITIVE] : undefined;
226
+ if (keys.length !== 1 || keys[0] !== WEBAUTHN_PRIMITIVE || typeof encoded !== 'string') {
227
+ throw new Refusal(CLAUSE_MALFORMED, 'HM-3: a webauthn entry carries exactly the one primitive `webauthn`');
228
+ }
229
+ // WE-4, and the `b64:` prefix is PART OF THE VALUE -- the same rule
230
+ // `att_nonce` is held to, one field over.
231
+ if (!isWe4B64(encoded)) {
232
+ throw new Refusal(CLAUSE_MALFORMED, 'HM-3/WE-4: the assertion is not `b64:` + RFC 4648 sec 4 base64 with padding');
233
+ }
234
+ const raw = b64Decode(encoded.slice('b64:'.length));
235
+ if (raw === null) {
236
+ throw new Refusal(CLAUSE_MALFORMED, 'HM-3: assertion is not base64');
237
+ }
238
+ try {
239
+ return verifyWebauthn(cred.alg, cred.publicKey, new TextEncoder().encode(messageId), cred.rpId, raw);
240
+ }
241
+ catch (e) {
242
+ if (!(e instanceof WebauthnRefusal))
243
+ throw e;
244
+ // The step's NAME becomes the clause, and HM-4 travels in the message. One
245
+ // clause for all six failures would give six distinguishable faults one
246
+ // name, and the cross-implementation comparison is on exactly this field.
247
+ //
248
+ // `Malformed` and `RegistryKeyWeak` fold together into `Malformed`: the
249
+ // assertion or the credential is not this shape at all. `RegistryKeyWeak`
250
+ // should be unreachable here because the bundle loader refuses such a key at
251
+ // enrolment -- it is folded in rather than left to fall through, because a
252
+ // branch that produced no refusal would let the caller continue as though
253
+ // the leg had verified.
254
+ if (HM4_STEPS.includes(e.refusalName))
255
+ throw new Refusal(e.refusalName, e.detail);
256
+ throw new Refusal(CLAUSE_MALFORMED, `${e.refusalName}: ${e.detail}`);
257
+ }
258
+ }
259
+ /** The registry's `role` for an identity, or null. */
260
+ function roleOf(cred) {
261
+ return cred.role;
262
+ }
263
+ /**
264
+ * §9.3 step 7b over the attestations travelling beside one receipt.
265
+ *
266
+ * @param policy the customer's own signed-bundle quorum policy.
267
+ * @param basis the values recomputed by `verifyReceipt` before this runs.
268
+ * @param entries `receipt.attestations`, as parsed JSON.
269
+ * @param digests `receipt.attestation_digests`, as parsed JSON.
270
+ * @returns the outcome, or `null` when no attestations travelled and none were
271
+ * committed to -- the vacuous LOW/MEDIUM case of AB-1's own note.
272
+ * `null` means "nothing was verified", never "a quorum held": see the
273
+ * module header on why this package cannot tell that a quorum was
274
+ * REQUIRED.
275
+ * @throws {@link Refusal} with the engine's clause names.
276
+ */
277
+ export function verifyQuorum(policy, basis, entries, digests) {
278
+ // The shapes first, so a malformed carrier is named rather than iterated.
279
+ // The engine tolerates a non-list `attestations` by falling back to `[]`
280
+ // (`receipt.get("attestations") or []`); refusing it is a NARROWER
281
+ // acceptance and is pinned by a test, because a truthy non-list there would
282
+ // be iterated by the reference into per-character entries.
283
+ if (entries !== undefined && entries !== null && !Array.isArray(entries)) {
284
+ throw new Refusal(CLAUSE_MEMBERSHIP, 'attestations is not a list');
285
+ }
286
+ const list = Array.isArray(entries) ? entries : [];
287
+ const digestsAbsent = digests === undefined || digests === null;
288
+ // The vacuous case, and it is AB-1's own note: a LOW/MEDIUM receipt carries
289
+ // no quorum and commits to none. Checked BEFORE the digest type check only
290
+ // when the field is genuinely absent -- a field that is PRESENT and not a
291
+ // list of strings is a malformed receipt whatever its length would have been.
292
+ if (list.length === 0 && digestsAbsent)
293
+ return null;
294
+ let signed;
295
+ if (Array.isArray(digests) && digests.every((d) => typeof d === 'string')) {
296
+ signed = digests.filter((d) => typeof d === 'string');
297
+ }
298
+ else {
299
+ // An absent list refuses under AB-1's TYPE check exactly as the reference's
300
+ // `isinstance` does: `None` never short-circuits to "no digests to check".
301
+ throw new Refusal(CLAUSE_MEMBERSHIP, 'attestation_digests is not a list of strings');
302
+ }
303
+ if (list.length === 0 && signed.length === 0)
304
+ return null;
305
+ if (list.length === 0) {
306
+ // An empty entry list beside a populated digest list is a MISSING QUORUM,
307
+ // not a cardinality mismatch, and the operator must be told which. The
308
+ // engine puts this first for the same reason.
309
+ throw new Refusal(CLAUSE_NO_ATTESTATIONS, 'receipt commits to attestations that did not arrive');
310
+ }
311
+ // AB-3: duplicates refused HERE and explicitly, BEFORE the ordering check --
312
+ // the reference's order, which matters on an input wrong in both ways. In an
313
+ // Executor holding a Consumption Ledger this check is masked by CL-3 and
314
+ // kills no mutant (a positive-path obligation); this package has no ledger,
315
+ // and here it is the only thing standing between a duplicated digest and a
316
+ // quorum counted twice.
317
+ const distinct = new Set(signed);
318
+ if (distinct.size !== signed.length) {
319
+ throw new Refusal(CLAUSE_DIGEST_DUP, 'duplicate attestation digests');
320
+ }
321
+ // AB-2: ONE canonical ordering, strictly ascending over the UTF-8 bytes of
322
+ // the digest strings INCLUDING the `sha256:` prefix -- the prefix is part of
323
+ // the value, never decoration. Without it the same quorum yields two
324
+ // canonical encodings of one receipt, hence two receipt identities.
325
+ for (let i = 1; i < signed.length; i += 1) {
326
+ const prev = signed[i - 1];
327
+ const cur = signed[i];
328
+ if (prev === undefined || cur === undefined)
329
+ continue; // unreachable; index in range
330
+ if (!(prev < cur)) {
331
+ throw new Refusal(CLAUSE_DIGEST_ORDER, 'attestation_digests is not sorted');
332
+ }
333
+ }
334
+ // AB-4: cardinality in BOTH directions. Membership alone lets an attacker
335
+ // WITHHOLD entries -- every digest they do send still matches, and the quorum
336
+ // silently shrinks. Counting from the digest list instead would be reading
337
+ // the size of the quorum off the receipt: AT-3's defect, one layer down.
338
+ if (list.length !== signed.length) {
339
+ throw new Refusal(CLAUSE_CARDINALITY, `${list.length} attestations received against ${signed.length} signed digests`);
340
+ }
341
+ // HM-4 (f)'s working copy, seeded from what the caller stored and advanced as
342
+ // entries are accepted, so two entries in ONE receipt made by one credential
343
+ // are held to the same "strictly greater" rule that two receipts are. Without
344
+ // the local copy the second would be compared against the stored value the
345
+ // first already passed, and a replayed assertion beside its original would
346
+ // count twice toward the quorum.
347
+ const counters = new Map(policy.webauthnCounters ?? new Map());
348
+ const claimed = new Map();
349
+ const approvals = new Set();
350
+ const operators = new Set();
351
+ for (const raw of list) {
352
+ // AB-1: membership, checked FIRST and recomputed from the received bytes
353
+ // (§9.3 step 7b(0)). Before the signature, so an entry the receipt never
354
+ // committed to is refused before the verifier spends work on
355
+ // attacker-supplied material.
356
+ const digest = entryDigest(raw);
357
+ if (!distinct.has(digest)) {
358
+ throw new Refusal(CLAUSE_MEMBERSHIP, "attestation is not in the receipt's signed digest list");
359
+ }
360
+ // `entryDigest` has already proved this is a record with the four fields.
361
+ const entry = isRecord(raw) ? raw : {};
362
+ const obj = entry['obj'];
363
+ if (!isRecord(obj)) {
364
+ throw new Refusal(CLAUSE_NO_OBJECT, 'attestation carries no object (v1.3.2 form)');
365
+ }
366
+ // AT-8b: CLOSED schema. Exact field set, never normalised.
367
+ const presentFields = Object.keys(obj);
368
+ const missing = AT1_FIELDS.filter((k) => !presentFields.includes(k));
369
+ const extra = presentFields.filter((k) => !AT1_FIELDS.includes(k));
370
+ if (missing.length > 0 || extra.length > 0) {
371
+ throw new Refusal(CLAUSE_OBJECT_SCHEMA, `object schema violation missing=${JSON.stringify(missing)} extra=${JSON.stringify(extra)}`);
372
+ }
373
+ // WE-4 then AT-1, in that order and never the other way round: an
374
+ // `att_nonce` wrong in both ways must stop at the same rule in every
375
+ // implementation. AT-8b above closes the field SET and says nothing about
376
+ // the spelling of a value, so this sits one layer beneath it -- and the id
377
+ // derived below is SHA-256 over the canonical bytes of the whole object, so
378
+ // `b64:AAAA==` and `AAAA==` would be TWO IDS FOR ONE OBJECT.
379
+ const nonce = strField(obj, 'att_nonce') ?? '';
380
+ if (!isWe4B64(nonce)) {
381
+ throw new Refusal(CLAUSE_WIRE_TYPE, `att_nonce ${JSON.stringify(obj['att_nonce'])} is not b64: + RFC 4648 sec 4 base64 with padding`);
382
+ }
383
+ if (nonce.length !== NONCE128_LEN) {
384
+ throw new Refusal(CLAUSE_ATT_NONCE_SIZE, `att_nonce is not ${NONCE128_BYTES * 8}-bit`);
385
+ }
386
+ // (i) The id is DERIVED from the canonical object, and it is also the
387
+ // message the attester signed. Deriving it before the signature check is
388
+ // what makes the signature cover the whole object rather than a digest
389
+ // somebody else chose.
390
+ const aid = attestationId(obj);
391
+ // CR-5: `alg` is an AT-1 field, so the suite is signature-covered.
392
+ const alg = requiredStr(obj, 'alg', CLAUSE_ATTESTATION_SUITE);
393
+ const attester = strField(entry, 'attester') ?? '';
394
+ const registered = policy.attesters.get(attester);
395
+ // §8.6c: IS THIS LEG A PERSON'S? Answered from the registry entry OR from
396
+ // the object's declared suite, and then the two are required to agree
397
+ // (HM-3), because either one alone decides nothing. Taking it from the
398
+ // object would let a compromised issuer route a machine's entry down the
399
+ // human path; taking it from the registry would let an object claiming
400
+ // `hybrid-ed25519-mldsa65` be verified as an assertion. The DISAGREEMENT is
401
+ // the refusal.
402
+ const human = registered?.kind === 'webauthn' || WEBAUTHN_ALGS.includes(alg);
403
+ if (human) {
404
+ if (registered === undefined || registered.kind !== 'webauthn') {
405
+ throw new Refusal(CLAUSE_WEBAUTHN_ALG, `HM-3: ${attester} is not a registered human attester, but the object claims ${alg}`);
406
+ }
407
+ // CR-8: NO SUITE FLOOR HERE, and its absence IS the clause rather than an
408
+ // omission. `min_suite` is CR-4 containment over MACHINE suites, and no
409
+ // authenticator in existence produces a post-quantum signature -- so
410
+ // comparing a human leg against a hybrid floor would refuse every person
411
+ // §8.6c exists to admit. What HM-6 says the classical leg costs is paid
412
+ // elsewhere: AB-1 digests this whole entry, assertion bytes included, into
413
+ // the receipt body a hybrid key signs, and §11 anchors that.
414
+ const counter = verifyWebauthnEntry(registered, alg, entry['sig'], aid);
415
+ // HM-4 (f). Strictly greater whenever EITHER is non-zero, so a credential
416
+ // whose authenticator does not implement a counter (both zero, forever)
417
+ // still verifies -- the WebAuthn specification permits that, and refusing
418
+ // it would refuse a class of real hardware.
419
+ const stored = counters.get(registered.credentialId) ?? 0;
420
+ if ((stored !== 0 || counter !== 0) && counter <= stored) {
421
+ throw new Refusal(CLAUSE_WEBAUTHN_COUNTER, `HM-4 (f): signature counter ${counter} for credential ${registered.credentialId.slice(0, 16)} did not advance past ${stored}: the credential is cloned, or the assertion is a replay`);
422
+ }
423
+ counters.set(registered.credentialId, counter);
424
+ claimed.set(registered.credentialId, counter);
425
+ }
426
+ else {
427
+ // CR-4 by CONTAINMENT. An UNKNOWN suite refuses here under CR-4 rather
428
+ // than CR-1, matching `Bundle.suite_ok`, which returns False for a name it
429
+ // does not know; the receipt path spells the same condition CR-1 and the
430
+ // comparison across implementations is on refusal NAMES, so the reference
431
+ // wins.
432
+ const suite = parseSuite(alg);
433
+ if (suite === null || !satisfiesFloor(suite, basis.floor)) {
434
+ throw new Refusal(CLAUSE_ATTESTATION_SUITE, `attestation suite ${alg} below bundle floor`);
435
+ }
436
+ if (registered === undefined || registered.kind !== 'hybrid') {
437
+ // An identity absent from the signed registry is not an attester
438
+ // (ACK-4): a name nobody enrolled must not be able to satisfy
439
+ // INV-1-HIGH.
440
+ throw new Refusal(CLAUSE_ATTESTER_SIG, 'attester signature invalid');
441
+ }
442
+ let bad = true;
443
+ try {
444
+ // The machine parse HERE and not earlier: the shape a signature must
445
+ // have is the shape its SUITE requires, and the suite is only settled
446
+ // once the branch above has established that this leg is a machine's.
447
+ const parts = parseSig(entry['sig'], alg);
448
+ bad = verifyUnderSuite(suite, registered, new TextEncoder().encode(aid), parts) !== null;
449
+ }
450
+ catch (e) {
451
+ // A shape complaint from `parseSig` is a bad ATTESTER signature here,
452
+ // not a bad receipt signature: the engine remaps the same way, and a
453
+ // `9.3-1` escaping this loop would name the wrong artifact.
454
+ if (!(e instanceof Refusal))
455
+ throw e;
456
+ bad = true;
457
+ }
458
+ if (bad)
459
+ throw new Refusal(CLAUSE_ATTESTER_SIG, 'attester signature invalid');
460
+ }
461
+ // (ii) THE BINDING. Y1: a genuine signature over an object bound to a
462
+ // DIFFERENT proposal is a valid attestation to something else.
463
+ if (requiredStr(obj, 'proposal_hash', CLAUSE_BINDING) !== basis.proposalHash) {
464
+ throw new Refusal(CLAUSE_BINDING, 'attestation bound to a DIFFERENT proposal');
465
+ }
466
+ // (iii) The policy basis the attester was shown, against the bundle the
467
+ // CALLER trusts. `floor_only_risk` is NOT compared -- see the module header:
468
+ // there is no recomputed grade in this package to compare it against, and
469
+ // comparing it against the receipt's own assertion would be X1.
470
+ if (requiredStr(obj, 'policy_bundle_hash', CLAUSE_POLICY_BASIS) !== policy.policyBundleHash ||
471
+ intField(obj, 'bundle_epoch', CLAUSE_POLICY_BASIS) !== policy.bundleEpoch) {
472
+ throw new Refusal(CLAUSE_POLICY_BASIS, 'attestation policy basis mismatch');
473
+ }
474
+ // (iii) AT-9's SECOND requirement: CONSENT, not threshold. This is not how
475
+ // the quorum size is obtained -- that is `quorumK` below, and this field is
476
+ // never consulted for it. A first pass at the engine's fix DELETED this
477
+ // check, arguing the threshold is already bound transitively through
478
+ // `policy_bundle_hash`. The argument was sound and the conclusion wrong: it
479
+ // considered only attacks that LOWER the threshold, and raising the stated
480
+ // count is an attack on consent, not on the invariant.
481
+ const stated = intField(obj, 'required_count', CLAUSE_CONSENT);
482
+ if (stated !== policy.quorumK) {
483
+ throw new Refusal(CLAUSE_CONSENT, `attester signed for quorum ${stated}, bundle requires ${policy.quorumK}`);
484
+ }
485
+ // (iii) Freshness of the object itself, against the receipt's issuance.
486
+ // PARSED, never compared as strings: two RFC 3339 strings compare
487
+ // lexicographically and would agree with this check for every canonical
488
+ // value and then disagree on the first one that was not (ACP-167).
489
+ const objExp = parseInstant(requiredStr(obj, 'expires_at', CLAUSE_POLICY_BASIS));
490
+ if (objExp === null) {
491
+ throw new Refusal(CLAUSE_POLICY_BASIS, 'attestation object field expires_at is not an RFC 3339 UTC instant');
492
+ }
493
+ if (objExp < basis.receiptIssuedAt) {
494
+ throw new Refusal(CLAUSE_POLICY_BASIS, 'attestation expired before issuance');
495
+ }
496
+ // (iii-a) The operator comes from the VERIFIED object (Y4), never from the
497
+ // receipt body -- a body/object disagreement must not be resolved in favour
498
+ // of the body.
499
+ operators.add(requiredStr(obj, 'operator', CLAUSE_POLICY_BASIS));
500
+ // (v) Y1b: the derived id is authoritative. A transmitted id is permitted to
501
+ // be absent and refused when it disagrees. Still load-bearing against a
502
+ // COMPROMISED issuer, which chooses the digest list: a garbage
503
+ // `attestation_id` inside a correctly-digested entry passes membership, and
504
+ // only this recomputation catches it.
505
+ const transmitted = entry['attestation_id'];
506
+ if (transmitted !== undefined && transmitted !== aid) {
507
+ throw new Refusal(CLAUSE_DERIVED_ID, 'transmitted attestation_id != derived id');
508
+ }
509
+ // (iv) AT-10: the key that signed is held the way approver keys must be
510
+ // held -- both sides from the SIGNED BUNDLE, never from the entry. Every
511
+ // check above this line verifies a signature, and a signature says only that
512
+ // a key was used: a private key in a config file on a build host satisfied
513
+ // all of them. Before any count, approvals and confirmations alike, matching
514
+ // the engine's position exactly so a multi-defect entry refuses at the same
515
+ // clause in every implementation.
516
+ const level = policy.assurances.get(attester) ?? 'AS0';
517
+ const have = ASSURANCE_ORDER.indexOf(level);
518
+ const need = ASSURANCE_ORDER.indexOf(policy.minAssurance);
519
+ if (have < 0 || need < 0) {
520
+ // P-4 applied to the ladder itself, not only to its members: a level
521
+ // outside the ladder has no rank, and ranking it anywhere would be
522
+ // guessing on the permissive side half the time.
523
+ throw new Refusal(CLAUSE_ASSURANCE, `assurance level outside the AS0..AS2 ladder: enrolled ${level}, floor ${policy.minAssurance}`);
524
+ }
525
+ if (have < need) {
526
+ throw new Refusal(CLAUSE_ASSURANCE, `attester ${attester} enrolled at ${level}, bundle floor is ${policy.minAssurance}`);
527
+ }
528
+ // The `kind` routing disclosed in the module header. Distinct ATTESTERS,
529
+ // not distinct entries: the set is what AT-3 counts.
530
+ //
531
+ // AT-1's role resolution is the narrowing the header states: an empty
532
+ // `required_roles` means any enrolled attester satisfies it (v1.3.32), and a
533
+ // non-empty one is resolved against the REGISTRY's role for this identity,
534
+ // never against a role the entry supplies.
535
+ if (entry['kind'] === 'approval') {
536
+ const wanted = obj['required_roles'];
537
+ if (!Array.isArray(wanted)) {
538
+ throw new Refusal(CLAUSE_OBJECT_SCHEMA, 'attestation object field required_roles is not a list');
539
+ }
540
+ const role = roleOf(registered);
541
+ if (wanted.length === 0 || (role !== null && wanted.includes(role))) {
542
+ approvals.add(attester);
543
+ }
544
+ }
545
+ }
546
+ if (operators.size !== 1) {
547
+ throw new Refusal(CLAUSE_OPERATOR_DISAGREE, 'attestations disagree on operator');
548
+ }
549
+ const operator = [...operators][0];
550
+ if (operator === undefined) {
551
+ throw new Refusal(CLAUSE_OPERATOR_DISAGREE, 'attestations disagree on operator');
552
+ }
553
+ // AT-9's FIRST requirement. Reading the threshold from the signed bundle
554
+ // rather than from an entry raises nothing, so no mutant can score this line
555
+ // -- it is DEFENCE IN DEPTH, and the engine records the same thing rather
556
+ // than dressing it up as a control. The branch that CAN refuse is below.
557
+ if (approvals.size < policy.quorumK) {
558
+ throw new Refusal(CLAUSE_QUORUM, `quorum ${approvals.size} < ${policy.quorumK}`);
559
+ }
560
+ // AT-2 distinctness: the proposer never counts toward their own quorum.
561
+ if (approvals.has(operator)) {
562
+ throw new Refusal(CLAUSE_OPERATOR_SELF, 'operator counted toward own quorum');
563
+ }
564
+ return { operator, approvals: [...approvals].sort(), webauthnCounters: claimed };
565
+ }
566
+ //# sourceMappingURL=quorum.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quorum.js","sourceRoot":"","sources":["../src/quorum.ts"],"names":[],"mappings":"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"}
package/dist/sig.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The CR-2 signature carrier and the CR-3 conjunctive verification, shared by
3
+ * the receipt gate (`verify.ts`) and the attestation loop (`quorum.ts`).
4
+ *
5
+ * One home, for the reason `quorum.rs` gives when it calls
6
+ * `crate::decide::parse_sig` rather than writing its own: the shape a signature
7
+ * must have is the shape its SUITE requires, and a second parser is a second
8
+ * answer to "which primitives satisfy this suite". The refusal each function
9
+ * raises is the RECEIPT's clause (`9.3-1`); the quorum catches it and re-raises
10
+ * under `9.3-7b-i`, exactly as the engine does, because a bad attester
11
+ * signature and a bad receipt signature are different findings.
12
+ */
13
+ import { type HybridError, type Primitive, type PrimitiveVerdict, type Suite } from './suite.js';
14
+ /** FIPS 204 ML-DSA-65 public key length. */
15
+ export declare const MLDSA65_PK_LEN = 1952;
16
+ /** FIPS 204 ML-DSA-65 signature length. */
17
+ export declare const MLDSA65_SIG_LEN = 3309;
18
+ /** Lowercase hex of a byte string. */
19
+ export declare function hex(bytes: Uint8Array): string;
20
+ /** Decode a hex signature leg, refusing exactly as `decide.rs`'s `unhex`. */
21
+ export declare function unhex(s: string): Uint8Array;
22
+ /** One primitive's signature as presented on the wire. */
23
+ export interface SignaturePart {
24
+ readonly primitive: Primitive;
25
+ readonly bytes: Uint8Array;
26
+ }
27
+ /**
28
+ * Parse a wire `sig` object into the parts the CR-3 combiner verifies --
29
+ * `decide.rs::parse_sig`, including its rule that the key set must be EXACTLY
30
+ * the suite's. Three attacks live in the difference: a scalar `sig` (format
31
+ * confusion is a downgrade in disguise), a missing primitive (the stripped
32
+ * leg), and an extra undeclared primitive (an accepted code path the attacker
33
+ * chose). An unknown primitive name is refused rather than dropped -- a dropped
34
+ * part would let a suite be satisfied by the parts that remain.
35
+ */
36
+ export declare function parseSig(sig: unknown, alg: string): SignaturePart[];
37
+ /**
38
+ * Verify one ML-DSA-65 signature. Never throws; length checks first, exactly as
39
+ * `primitives.rs` -- a malformed input is `invalid`, never `unsupported`: one is
40
+ * a statement about the signature, the other about this build, and reporting one
41
+ * as the other sends the investigation to the wrong place.
42
+ */
43
+ export declare function verifyMlDsa65(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): PrimitiveVerdict;
44
+ /** A hybrid identity's two public halves -- both, because CR-3 is conjunctive. */
45
+ export interface HybridPublicKey {
46
+ readonly classical: Uint8Array;
47
+ readonly pq: Uint8Array;
48
+ }
49
+ /**
50
+ * CR-3 over a declared suite: every primitive the suite names must be present
51
+ * and must verify. Returns null on success, the `HybridError` name on refusal.
52
+ *
53
+ * The suite is a PARAMETER, exactly as in Rust: a caller that decides how many
54
+ * primitives a hybrid signature needs can be persuaded to decide "one".
55
+ */
56
+ export declare function verifyUnderSuite(suite: Suite, key: HybridPublicKey, message: Uint8Array, parts: readonly SignaturePart[]): HybridError | null;
57
+ //# sourceMappingURL=sig.d.ts.map