@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/README.md +57 -107
- package/dist/cbor.d.ts +87 -0
- package/dist/cbor.d.ts.map +1 -0
- package/dist/cbor.js +326 -0
- package/dist/cbor.js.map +1 -0
- package/dist/clauses.d.ts +70 -0
- package/dist/clauses.d.ts.map +1 -1
- package/dist/clauses.js +77 -0
- package/dist/clauses.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/quorum.d.ts +245 -0
- package/dist/quorum.d.ts.map +1 -0
- package/dist/quorum.js +566 -0
- package/dist/quorum.js.map +1 -0
- package/dist/sig.d.ts +57 -0
- package/dist/sig.d.ts.map +1 -0
- package/dist/sig.js +127 -0
- package/dist/sig.js.map +1 -0
- package/dist/testkeys.d.ts +33 -0
- package/dist/testkeys.d.ts.map +1 -1
- package/dist/testkeys.js +100 -0
- package/dist/testkeys.js.map +1 -1
- package/dist/verify.d.ts +44 -20
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +55 -146
- package/dist/verify.js.map +1 -1
- package/dist/webauthn.d.ts +148 -0
- package/dist/webauthn.d.ts.map +1 -0
- package/dist/webauthn.js +414 -0
- package/dist/webauthn.js.map +1 -0
- package/dist/wire.d.ts +44 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +90 -0
- package/dist/wire.js.map +1 -0
- package/package.json +12 -9
package/dist/webauthn.js
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §8.6c HM-1..HM-7 -- a human attester's WebAuthn assertion, verified in the
|
|
3
|
+
* customer's own process.
|
|
4
|
+
*
|
|
5
|
+
* The third writing of `crates/acp-crypto/src/webauthn.rs` and
|
|
6
|
+
* `reference/src/acp_crypto.py`, and the mirrored corpus
|
|
7
|
+
* `fixtures/refused_webauthn.json` is what holds the three to one rule: 18
|
|
8
|
+
* vectors, two ACCEPT and one refusal per HM-4 step, replayed by
|
|
9
|
+
* `webauthn.test.ts` against this file.
|
|
10
|
+
*
|
|
11
|
+
* # What a human leg is NOT (HM-6, CR-8)
|
|
12
|
+
*
|
|
13
|
+
* **CLASSICAL, and this function must never be read as though it were more.**
|
|
14
|
+
* No passkey, platform authenticator or security key in existence produces a
|
|
15
|
+
* post-quantum signature, so a `webauthn` entry is never compared against the
|
|
16
|
+
* bundle's `min_suite` (CR-8) -- the absence of a floor check here IS the
|
|
17
|
+
* clause, not an omission. The post-quantum binding of a human's decision is
|
|
18
|
+
* the receipt AB-1 digests the whole entry into and the §11 anchor over it.
|
|
19
|
+
*
|
|
20
|
+
* # The order is normative and the NAMES are the product (HM-4)
|
|
21
|
+
*
|
|
22
|
+
* HM-4 states six checks in order and says no step is skipped for a smaller one
|
|
23
|
+
* that already failed. What the implementations must agree on is not "refused"
|
|
24
|
+
* but WHICH step refused: `WebauthnOrigin` is a phishing finding and
|
|
25
|
+
* `WebauthnChallenge` is a replay, and one name for both would make them one
|
|
26
|
+
* event in an audit record. That is why this throws a named
|
|
27
|
+
* {@link WebauthnRefusal} rather than returning a boolean.
|
|
28
|
+
*
|
|
29
|
+
* Step (f), the signature counter, is NOT here: comparing it needs the stored
|
|
30
|
+
* value for the credential, which is Consumption Ledger state. What this
|
|
31
|
+
* returns is the counter the assertion carried; `quorum.ts` compares it against
|
|
32
|
+
* what the caller stored, and discloses that this package holds no ledger.
|
|
33
|
+
*/
|
|
34
|
+
import { createHash } from 'node:crypto';
|
|
35
|
+
import { p256 } from '@noble/curves/nist.js';
|
|
36
|
+
import { cborAsBytes, cborGetInt, cborGetText, cborIsMap, cborKeys, decodeCanonical } from './cbor.js';
|
|
37
|
+
import { ED25519_PK_LEN, ED25519_SIG_LEN, ed25519IsSmallOrder, verifyEd25519Strict } from './ed25519.js';
|
|
38
|
+
/**
|
|
39
|
+
* HM-1 / CR-8: the two names an entry's `alg` may take.
|
|
40
|
+
*
|
|
41
|
+
* ATTESTATION-ENTRY SUITES ONLY. `suite.ts`'s table is the machine one and
|
|
42
|
+
* these are deliberately absent from it, because CR-8 forbids either as a
|
|
43
|
+
* bundle, receipt or door suite -- a name present in both tables is a name
|
|
44
|
+
* whose meaning depends on where it was read.
|
|
45
|
+
*/
|
|
46
|
+
export const WEBAUTHN_ALGS = ['webauthn-es256', 'webauthn-ed25519'];
|
|
47
|
+
/** The one primitive name a human entry's `sig` map carries (HM-3). */
|
|
48
|
+
export const WEBAUTHN_PRIMITIVE = 'webauthn';
|
|
49
|
+
/**
|
|
50
|
+
* HM-3's three fields.
|
|
51
|
+
*
|
|
52
|
+
* **This is not their canonical ORDER**, and the difference looks like one.
|
|
53
|
+
* RFC 8949 §4.2.1 sorts map keys by their ENCODED bytes and a text key's head
|
|
54
|
+
* carries its length, so the wire order is by length first: `signature` (9),
|
|
55
|
+
* `client_data_json` (16), `authenticator_data` (18). The decoder gets that
|
|
56
|
+
* right by construction; a reader who assumed this array's order was the wire
|
|
57
|
+
* order would write a decoder that refuses every real assertion.
|
|
58
|
+
*/
|
|
59
|
+
export const ASSERTION_FIELDS = [
|
|
60
|
+
'authenticator_data',
|
|
61
|
+
'client_data_json',
|
|
62
|
+
'signature',
|
|
63
|
+
];
|
|
64
|
+
/** The registry entry, the COSE_Key or the assertion is not the declared shape. */
|
|
65
|
+
export const MALFORMED = 'Malformed';
|
|
66
|
+
/** PB-9 / HM-5, at enrolment: an off-curve ES256 key or a small-order Ed25519 one. */
|
|
67
|
+
export const REGISTRY_KEY_WEAK = 'RegistryKeyWeak';
|
|
68
|
+
/** HM-4 (a). */
|
|
69
|
+
export const WEBAUTHN_TYPE = 'WebauthnType';
|
|
70
|
+
/** HM-4 (b). */
|
|
71
|
+
export const WEBAUTHN_CHALLENGE = 'WebauthnChallenge';
|
|
72
|
+
/** HM-4 (c). */
|
|
73
|
+
export const WEBAUTHN_ORIGIN = 'WebauthnOrigin';
|
|
74
|
+
/** HM-4 (d). */
|
|
75
|
+
export const WEBAUTHN_AUTHENTICATOR_DATA = 'WebauthnAuthenticatorData';
|
|
76
|
+
/** HM-4 (e). */
|
|
77
|
+
export const WEBAUTHN_SIGNATURE = 'WebauthnSignature';
|
|
78
|
+
/**
|
|
79
|
+
* One HM-4 step refused, named.
|
|
80
|
+
*
|
|
81
|
+
* `name` is the STEP and is what the cross-implementation comparison is on; the
|
|
82
|
+
* clause is HM-4 for all of them, and six failures under one clause id would be
|
|
83
|
+
* one name for six objects. Deliberately NOT a {@link import('./refusal.js').Refusal}:
|
|
84
|
+
* `quorum.ts` maps these to §9.3's vocabulary at its own boundary, and a type
|
|
85
|
+
* that could travel straight out would let an unmapped name reach a caller.
|
|
86
|
+
*/
|
|
87
|
+
export class WebauthnRefusal extends Error {
|
|
88
|
+
refusalName;
|
|
89
|
+
detail;
|
|
90
|
+
constructor(refusalName, detail) {
|
|
91
|
+
super(`${refusalName}: ${detail}`);
|
|
92
|
+
this.name = 'WebauthnRefusal';
|
|
93
|
+
this.refusalName = refusalName;
|
|
94
|
+
this.detail = detail;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// COSE_Key labels (RFC 9052 §7). NAMED, because `1` and `-2` mean nothing at a
|
|
98
|
+
// call site and a transposed pair of integers is a defect no reader would see.
|
|
99
|
+
const COSE_KTY = 1n;
|
|
100
|
+
const COSE_CRV = -1n;
|
|
101
|
+
const COSE_X = -2n;
|
|
102
|
+
const COSE_Y = -3n;
|
|
103
|
+
/**
|
|
104
|
+
* HM-4 (d): the flags byte of `authenticator_data`. UP is "a person touched the
|
|
105
|
+
* authenticator", UV is "the authenticator verified WHO". Both required -- UP
|
|
106
|
+
* alone is possession, and HM-1 says possession is not a person.
|
|
107
|
+
*/
|
|
108
|
+
const AD_FLAG_UP = 0x01;
|
|
109
|
+
const AD_FLAG_UV = 0x04;
|
|
110
|
+
/** 32 bytes rpIdHash + 1 flags + 4 counter. */
|
|
111
|
+
const AD_MIN_LEN = 37;
|
|
112
|
+
/**
|
|
113
|
+
* The FIXED shape of a credential's public key, per `alg`.
|
|
114
|
+
*
|
|
115
|
+
* EXACTLY THESE LABELS AND NOTHING ELSE, which is stricter than "whatever the
|
|
116
|
+
* authenticator returned" and is taken deliberately: an optional label gives one
|
|
117
|
+
* key two canonical encodings, hence two `public_key` strings for one
|
|
118
|
+
* credential, and PB-7's distinctness -- which HM-5 takes over exactly that
|
|
119
|
+
* string -- is a comparison over it. Two spellings of one credential enrolled
|
|
120
|
+
* under two names is one holder satisfying k=2 alone.
|
|
121
|
+
*
|
|
122
|
+
* What that costs, disclosed rather than buried: a real authenticator's
|
|
123
|
+
* COSE_Key also carries label 3 (`alg`), so the enrolling service must drop it
|
|
124
|
+
* before writing the registry entry. The registry's own `alg` field is the one
|
|
125
|
+
* definition of which key type HM-4 (e) verifies under, and a second copy inside
|
|
126
|
+
* the key bytes is a second definition CR-5 does not cover. The engine reports
|
|
127
|
+
* this as a spec wording problem; this implementation inherits the reading
|
|
128
|
+
* rather than taking a different one.
|
|
129
|
+
*/
|
|
130
|
+
const COSE_SHAPES = {
|
|
131
|
+
'webauthn-es256': { kty: 2n, crv: 1n, labels: [COSE_KTY, COSE_CRV, COSE_X, COSE_Y] },
|
|
132
|
+
'webauthn-ed25519': { kty: 1n, crv: 6n, labels: [COSE_KTY, COSE_CRV, COSE_X] },
|
|
133
|
+
};
|
|
134
|
+
function sortedLabelText(labels) {
|
|
135
|
+
return labels
|
|
136
|
+
.map((l) => (typeof l === 'bigint' ? l.toString() : JSON.stringify(l)))
|
|
137
|
+
.sort()
|
|
138
|
+
.join(',');
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* HM-1 / HM-5: decode a credential's COSE_Key.
|
|
142
|
+
*
|
|
143
|
+
* The decode is CANONICAL AND VALIDATING (AT-8a) because the registry entry is
|
|
144
|
+
* signed policy: a key that decodes from two different byte strings is a
|
|
145
|
+
* credential with two names.
|
|
146
|
+
*
|
|
147
|
+
* WEAKNESS IS REFUSED HERE, at enrolment, not left to the verifier: an ES256
|
|
148
|
+
* point off the curve and an Ed25519 point of small order are both
|
|
149
|
+
* `RegistryKeyWeak` (HM-5, PB-9). The curve-membership question is asked of
|
|
150
|
+
* `p256`'s own construction -- the same one the verifier will use -- rather than
|
|
151
|
+
* of a second copy of the curve equation, which would be a second definition of
|
|
152
|
+
* the curve. {@link ed25519IsSmallOrder} is the predicate `verifyEd25519Strict`
|
|
153
|
+
* already applies: one rule, two callers (ACP-109).
|
|
154
|
+
*/
|
|
155
|
+
export function coseKeyParse(alg, raw) {
|
|
156
|
+
const shape = COSE_SHAPES[alg];
|
|
157
|
+
if (shape === undefined) {
|
|
158
|
+
throw new WebauthnRefusal(MALFORMED, `no WebAuthn alg ${JSON.stringify(alg)}`);
|
|
159
|
+
}
|
|
160
|
+
let key;
|
|
161
|
+
try {
|
|
162
|
+
key = decodeCanonical(raw);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
const why = e instanceof Error ? e.message : 'not canonical CBOR';
|
|
166
|
+
throw new WebauthnRefusal(MALFORMED, `COSE_Key: ${why}`);
|
|
167
|
+
}
|
|
168
|
+
if (!cborIsMap(key))
|
|
169
|
+
throw new WebauthnRefusal(MALFORMED, 'COSE_Key is not a map');
|
|
170
|
+
// Compared as SETS. The decoder has already refused a duplicate key and a key
|
|
171
|
+
// out of canonical order, so what is left is membership -- and an extra label
|
|
172
|
+
// and a missing one are one refusal, because "this is not the key shape" is
|
|
173
|
+
// one fact.
|
|
174
|
+
const got = sortedLabelText(cborKeys(key));
|
|
175
|
+
const want = sortedLabelText([...shape.labels]);
|
|
176
|
+
if (got !== want) {
|
|
177
|
+
throw new WebauthnRefusal(MALFORMED, `COSE_Key labels {${got}} are not {${want}}`);
|
|
178
|
+
}
|
|
179
|
+
if (cborGetInt(key, COSE_KTY) !== shape.kty || cborGetInt(key, COSE_CRV) !== shape.crv) {
|
|
180
|
+
throw new WebauthnRefusal(MALFORMED, `COSE_Key kty/crv is not ${shape.kty}/${shape.crv} for ${alg}`);
|
|
181
|
+
}
|
|
182
|
+
const coord = (label) => {
|
|
183
|
+
const b = cborAsBytes(cborGetInt(key, label));
|
|
184
|
+
if (b === null)
|
|
185
|
+
throw new WebauthnRefusal(MALFORMED, 'COSE_Key coordinate is not bytes');
|
|
186
|
+
// Fixed width, short encodings forbidden: COSE writes P-256 and Ed25519
|
|
187
|
+
// coordinates at 32 bytes, and a short encoding is a second spelling of one
|
|
188
|
+
// integer.
|
|
189
|
+
if (b.length !== 32) {
|
|
190
|
+
throw new WebauthnRefusal(MALFORMED, 'COSE_Key coordinate is not 32 bytes');
|
|
191
|
+
}
|
|
192
|
+
return b;
|
|
193
|
+
};
|
|
194
|
+
if (alg === 'webauthn-es256') {
|
|
195
|
+
const x = coord(COSE_X);
|
|
196
|
+
const y = coord(COSE_Y);
|
|
197
|
+
// SEC1 uncompressed, which is what `p256` decodes and what the verifier
|
|
198
|
+
// below hands it: one encoding of the point, built once.
|
|
199
|
+
const sec1 = new Uint8Array(65);
|
|
200
|
+
sec1[0] = 0x04;
|
|
201
|
+
sec1.set(x, 1);
|
|
202
|
+
sec1.set(y, 33);
|
|
203
|
+
if (!p256.utils.isValidPublicKey(sec1, false)) {
|
|
204
|
+
throw new WebauthnRefusal(REGISTRY_KEY_WEAK, 'ES256 public key is not a point on secp256r1');
|
|
205
|
+
}
|
|
206
|
+
return { kind: 'es256', sec1 };
|
|
207
|
+
}
|
|
208
|
+
const x = coord(COSE_X);
|
|
209
|
+
if (x.length !== ED25519_PK_LEN || ed25519IsSmallOrder(x)) {
|
|
210
|
+
// PB-9 / ACP-106, one clause over: under a small-order key the Ed25519
|
|
211
|
+
// equation stops mentioning the message and ONE signature verifies every
|
|
212
|
+
// message. Enrolled as a human approver, that identity is a forgery oracle
|
|
213
|
+
// for anyone who has seen the public key.
|
|
214
|
+
throw new WebauthnRefusal(REGISTRY_KEY_WEAK, 'Ed25519 credential key is of small order');
|
|
215
|
+
}
|
|
216
|
+
return { kind: 'ed25519', raw: x };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* HM-3: the `webauthn` primitive's value, decoded under a FIXED SHAPE.
|
|
220
|
+
*
|
|
221
|
+
* A general CBOR decoder is the wrong tool and is deliberately not offered.
|
|
222
|
+
* What arrives here is attacker-supplied on the transport path, so the decoder
|
|
223
|
+
* must accept exactly one shape -- three text keys, each a byte string -- and
|
|
224
|
+
* refuse everything else as `Malformed`. A tag, a float, a nested map, an extra
|
|
225
|
+
* key and a missing key are all the same answer, because a decoder that reports
|
|
226
|
+
* them differently is a parser an attacker can interrogate.
|
|
227
|
+
*/
|
|
228
|
+
export function decodeAssertion(raw) {
|
|
229
|
+
let obj;
|
|
230
|
+
try {
|
|
231
|
+
obj = decodeCanonical(raw);
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
const why = e instanceof Error ? e.message : 'not canonical CBOR';
|
|
235
|
+
throw new WebauthnRefusal(MALFORMED, `assertion: ${why}`);
|
|
236
|
+
}
|
|
237
|
+
if (!cborIsMap(obj) || sortedLabelText(cborKeys(obj)) !== sortedLabelText([...ASSERTION_FIELDS])) {
|
|
238
|
+
throw new WebauthnRefusal(MALFORMED, 'assertion is not the HM-3 map');
|
|
239
|
+
}
|
|
240
|
+
const field = (name) => {
|
|
241
|
+
const b = cborAsBytes(cborGetText(obj, name));
|
|
242
|
+
if (b === null) {
|
|
243
|
+
throw new WebauthnRefusal(MALFORMED, `assertion ${name} is not a byte string`);
|
|
244
|
+
}
|
|
245
|
+
return b;
|
|
246
|
+
};
|
|
247
|
+
return {
|
|
248
|
+
authenticatorData: field('authenticator_data'),
|
|
249
|
+
clientDataJson: field('client_data_json'),
|
|
250
|
+
signature: field('signature'),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* HM-2: the challenge is base64url WITHOUT padding of the message's bytes.
|
|
255
|
+
*
|
|
256
|
+
* ONE definition, so "the challenge encoding" cannot mean two things. Note what
|
|
257
|
+
* the verifier does with it: it RECOMPUTES this from an id it derived itself and
|
|
258
|
+
* compares (RES-8). It never decodes the challenge the assertion carries and
|
|
259
|
+
* treats the result as an identifier -- a transmitted id is a name for a
|
|
260
|
+
* binding, not evidence of one.
|
|
261
|
+
*/
|
|
262
|
+
export function webauthnChallenge(messageIdBytes) {
|
|
263
|
+
const A = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
264
|
+
let s = '';
|
|
265
|
+
for (let i = 0; i < messageIdBytes.length; i += 3) {
|
|
266
|
+
const b0 = messageIdBytes[i] ?? 0;
|
|
267
|
+
const b1 = messageIdBytes[i + 1];
|
|
268
|
+
const b2 = messageIdBytes[i + 2];
|
|
269
|
+
const v = (b0 << 16) | ((b1 ?? 0) << 8) | (b2 ?? 0);
|
|
270
|
+
s += A[(v >> 18) & 63];
|
|
271
|
+
s += A[(v >> 12) & 63];
|
|
272
|
+
if (b1 !== undefined)
|
|
273
|
+
s += A[(v >> 6) & 63];
|
|
274
|
+
if (b2 !== undefined)
|
|
275
|
+
s += A[v & 63];
|
|
276
|
+
}
|
|
277
|
+
return s;
|
|
278
|
+
}
|
|
279
|
+
function sha256(bytes) {
|
|
280
|
+
return Uint8Array.from(createHash('sha256').update(bytes).digest());
|
|
281
|
+
}
|
|
282
|
+
function bytesEqual(a, b) {
|
|
283
|
+
return a.length === b.length && a.every((x, i) => x === b[i]);
|
|
284
|
+
}
|
|
285
|
+
function strProp(v, key) {
|
|
286
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
287
|
+
return null;
|
|
288
|
+
const raw = Object.prototype.hasOwnProperty.call(v, key)
|
|
289
|
+
? Object.getOwnPropertyDescriptor(v, key)?.value
|
|
290
|
+
: undefined;
|
|
291
|
+
return typeof raw === 'string' ? raw : null;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* HM-4 (a)-(e), IN ORDER, returning the assertion's 32-bit signature counter.
|
|
295
|
+
*
|
|
296
|
+
* `messageIdBytes` is the UTF-8 bytes of a **recomputed** id -- the attestation
|
|
297
|
+
* id at §9.3 step 7b(v). Never an id read off the message being verified
|
|
298
|
+
* (HM-2, RES-8). `rpId` is the entry's SIGNED relying-party id: signed policy,
|
|
299
|
+
* never a value the assertion supplies, which is the whole of HM-4 (c).
|
|
300
|
+
*
|
|
301
|
+
* NO STEP IS SKIPPED FOR AN EARLIER ONE, and the order is normative: an
|
|
302
|
+
* assertion wrong in two ways must produce the same refusal name in every
|
|
303
|
+
* implementation, or the corpus is comparing luck.
|
|
304
|
+
*/
|
|
305
|
+
export function verifyWebauthn(alg, coseKey, messageIdBytes, rpId, assertion) {
|
|
306
|
+
const key = coseKeyParse(alg, coseKey);
|
|
307
|
+
const a = decodeAssertion(assertion);
|
|
308
|
+
// (a) A parse failure and a wrong `type` are ONE refusal, because the clause
|
|
309
|
+
// states them as one condition: what it demands is a `webauthn.get`
|
|
310
|
+
// assertion, and bytes that are not JSON are not one. A `webauthn.create`
|
|
311
|
+
// document here is a REGISTRATION ceremony replayed as an approval -- the
|
|
312
|
+
// person consented to enrolling a key, not to the action.
|
|
313
|
+
let client;
|
|
314
|
+
try {
|
|
315
|
+
client = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(a.clientDataJson));
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
throw new WebauthnRefusal(WEBAUTHN_TYPE, 'HM-4 (a): clientDataJSON is not JSON');
|
|
319
|
+
}
|
|
320
|
+
const type = strProp(client, 'type');
|
|
321
|
+
if (type !== 'webauthn.get') {
|
|
322
|
+
throw new WebauthnRefusal(WEBAUTHN_TYPE, `HM-4 (a): clientDataJSON type is ${JSON.stringify(type)}, not 'webauthn.get'`);
|
|
323
|
+
}
|
|
324
|
+
// (b) HM-4 (b) / HM-2. RECOMPUTED and compared; never read as a value.
|
|
325
|
+
if (strProp(client, 'challenge') !== webauthnChallenge(messageIdBytes)) {
|
|
326
|
+
throw new WebauthnRefusal(WEBAUTHN_CHALLENGE, 'HM-4 (b): the assertion answers a different challenge than the recomputed id');
|
|
327
|
+
}
|
|
328
|
+
// (c) EXACTLY `https://` + the entry's signed `rp_id`, and nothing else -- no
|
|
329
|
+
// port, no path, no scheme substitution. An assertion made for another origin
|
|
330
|
+
// is an assertion made on another SITE, which is the entire phishing case
|
|
331
|
+
// this binding exists to refuse: the person really did touch their key, on a
|
|
332
|
+
// page an attacker served.
|
|
333
|
+
const wantOrigin = `https://${rpId}`;
|
|
334
|
+
const origin = strProp(client, 'origin');
|
|
335
|
+
if (origin !== wantOrigin) {
|
|
336
|
+
throw new WebauthnRefusal(WEBAUTHN_ORIGIN, `HM-4 (c): origin ${JSON.stringify(origin)} is not ${JSON.stringify(wantOrigin)}`);
|
|
337
|
+
}
|
|
338
|
+
// (d) Length, rpIdHash and the two flags are one refusal name because they
|
|
339
|
+
// are one question: is this authenticator data for this relying party,
|
|
340
|
+
// produced with the person present and verified? UV is the half that makes
|
|
341
|
+
// the level AT-10 records mean anything -- without it the entry is possession
|
|
342
|
+
// of a device, and HM-1 says possession is not a person.
|
|
343
|
+
if (a.authenticatorData.length < AD_MIN_LEN) {
|
|
344
|
+
throw new WebauthnRefusal(WEBAUTHN_AUTHENTICATOR_DATA, `HM-4 (d): authenticator_data is ${a.authenticatorData.length} bytes, under the ${AD_MIN_LEN}-byte minimum`);
|
|
345
|
+
}
|
|
346
|
+
if (!bytesEqual(a.authenticatorData.subarray(0, 32), sha256(new TextEncoder().encode(rpId)))) {
|
|
347
|
+
throw new WebauthnRefusal(WEBAUTHN_AUTHENTICATOR_DATA, "HM-4 (d): rpIdHash is not SHA-256 of the entry's rp_id");
|
|
348
|
+
}
|
|
349
|
+
const flags = a.authenticatorData[32] ?? 0;
|
|
350
|
+
if ((flags & AD_FLAG_UP) === 0) {
|
|
351
|
+
throw new WebauthnRefusal(WEBAUTHN_AUTHENTICATOR_DATA, 'HM-4 (d): the user-present flag is clear');
|
|
352
|
+
}
|
|
353
|
+
if ((flags & AD_FLAG_UV) === 0) {
|
|
354
|
+
throw new WebauthnRefusal(WEBAUTHN_AUTHENTICATOR_DATA, 'HM-4 (d): the user-verified flag is clear');
|
|
355
|
+
}
|
|
356
|
+
// (e) The signed message is the authenticator's own bytes followed by the
|
|
357
|
+
// hash of the client data -- NOT the challenge, and not the id. That is what
|
|
358
|
+
// binds (a)-(d) to the signature: tamper with the origin and the client-data
|
|
359
|
+
// hash moves, so an attacker who wants to pass (c) must re-sign, which needs
|
|
360
|
+
// the credential.
|
|
361
|
+
const signed = new Uint8Array(a.authenticatorData.length + 32);
|
|
362
|
+
signed.set(a.authenticatorData, 0);
|
|
363
|
+
signed.set(sha256(a.clientDataJson), a.authenticatorData.length);
|
|
364
|
+
if (key.kind === 'es256') {
|
|
365
|
+
let ok;
|
|
366
|
+
try {
|
|
367
|
+
// DER, as WebAuthn's ES256 is defined and as every authenticator emits.
|
|
368
|
+
// The fixed-width form is a different encoding of the same pair and is
|
|
369
|
+
// NOT accepted: two encodings of one signature is two entry digests for
|
|
370
|
+
// one human decision (AB-1).
|
|
371
|
+
//
|
|
372
|
+
// `lowS: false` is a PIN, not a default left alone. noble refuses a
|
|
373
|
+
// high-S signature unless told otherwise; neither engine side does --
|
|
374
|
+
// RustCrypto's `VerifyingKey::verify` and `cryptography`'s
|
|
375
|
+
// `ECDSA(SHA256())` both accept one -- so leaving noble's default in
|
|
376
|
+
// place would make this verifier refuse an assertion the engine accepts.
|
|
377
|
+
// A guard on one side only converts a closed defect into a divergence
|
|
378
|
+
// (ACP-106's lesson, one primitive over), and malleability buys an
|
|
379
|
+
// attacker nothing here: the receipt commits to the exact entry bytes
|
|
380
|
+
// under AB-1, so a re-encoded signature is a different entry the receipt
|
|
381
|
+
// never authorised.
|
|
382
|
+
ok = p256.verify(a.signature, signed, key.sec1, {
|
|
383
|
+
format: 'der',
|
|
384
|
+
prehash: true,
|
|
385
|
+
lowS: false,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
ok = false;
|
|
390
|
+
}
|
|
391
|
+
if (!ok) {
|
|
392
|
+
throw new WebauthnRefusal(WEBAUTHN_SIGNATURE, 'HM-4 (e): ES256 assertion signature is not DER, or does not verify');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// ACP-106's strict rule, on the human leg. `coseKeyParse` has already
|
|
397
|
+
// refused a small-order public key; `verifyEd25519Strict` refuses a
|
|
398
|
+
// small-order `R` as well -- the half that lets a signer emit a signature a
|
|
399
|
+
// non-strict verifier accepts over a message it never committed to. The two
|
|
400
|
+
// halves are one rule, and a human leg verified with only half of it would
|
|
401
|
+
// be the ACP-106 defect reopened for the one signature class where the
|
|
402
|
+
// signer is a person.
|
|
403
|
+
if (a.signature.length !== ED25519_SIG_LEN ||
|
|
404
|
+
!verifyEd25519Strict(key.raw, signed, a.signature)) {
|
|
405
|
+
throw new WebauthnRefusal(WEBAUTHN_SIGNATURE, 'HM-4 (e): Ed25519 assertion signature is malformed, non-canonical (ACP-106) or does not verify');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// HM-4 (f)'s INPUT. Big-endian, bytes 33..37, per the WebAuthn
|
|
409
|
+
// authenticator-data layout. The comparison against the stored value is the
|
|
410
|
+
// caller's -- see the module header on why it is not here.
|
|
411
|
+
const c = a.authenticatorData;
|
|
412
|
+
return (((c[33] ?? 0) << 24) | ((c[34] ?? 0) << 16) | ((c[35] ?? 0) << 8) | (c[36] ?? 0)) >>> 0;
|
|
413
|
+
}
|
|
414
|
+
//# sourceMappingURL=webauthn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webauthn.js","sourceRoot":"","sources":["../src/webauthn.ts"],"names":[],"mappings":"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"}
|
package/dist/wire.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-value predicates shared by the receipt gate and the §9.3 step 7b quorum.
|
|
3
|
+
*
|
|
4
|
+
* They live here rather than in `verify.ts` for one reason: `quorum.ts` needs
|
|
5
|
+
* every one of them, `verify.ts` imports `quorum.ts`, and a second copy in the
|
|
6
|
+
* quorum would be two definitions of WE-4 in one package -- the encoding-split
|
|
7
|
+
* defect at the source level, in the exact rule whose whole point is that one
|
|
8
|
+
* nonce has one spelling. `verify.ts` re-exports these so the published surface
|
|
9
|
+
* is unchanged.
|
|
10
|
+
*/
|
|
11
|
+
/** The one wire nonce size, 128-bit (`Nonce128`, cited by AT-1 and L-17). */
|
|
12
|
+
export declare const NONCE128_BYTES = 16;
|
|
13
|
+
/**
|
|
14
|
+
* `"b64:"` plus base64 of 16 bytes: 4 + 24.
|
|
15
|
+
*
|
|
16
|
+
* Counted, never decoded -- a rule whose behaviour on bad input is "throw" is
|
|
17
|
+
* not a refusal, and the reference learned that the hard way (its first
|
|
18
|
+
* spelling decoded the body, which raises on the URL-safe alphabet).
|
|
19
|
+
*/
|
|
20
|
+
export declare const NONCE128_LEN: number;
|
|
21
|
+
/**
|
|
22
|
+
* WE-4: is this the ASCII string `b64:` followed by RFC 4648 §4 base64, WITH
|
|
23
|
+
* padding? Ported from `quorum.rs::is_we4_b64` -- the body is whole
|
|
24
|
+
* four-character groups, the alphabet is §4's (`+` and `/`, never §5's `-` and
|
|
25
|
+
* `_`), and `=` appears only as one or two characters at the very end.
|
|
26
|
+
*
|
|
27
|
+
* Rejected, never normalised: normalising would make this verifier accept two
|
|
28
|
+
* spellings of one value, which is how one nonce comes to hold two ledger slots
|
|
29
|
+
* (ACP-87) and how one attestation comes to hold two ids.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isWe4B64(s: string): boolean;
|
|
32
|
+
/** A parsed JSON object, narrowed by a predicate rather than an `as` cast. */
|
|
33
|
+
export declare function isRecord(v: unknown): v is Record<string, unknown>;
|
|
34
|
+
/** A string field of a parsed object, or null -- never a coerced value. */
|
|
35
|
+
export declare function strField(v: Record<string, unknown>, key: string): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* RFC 4648 §4 base64 with padding, decoded. The carrier WE-4 declares.
|
|
38
|
+
*
|
|
39
|
+
* Returns null rather than throwing, and is called only after
|
|
40
|
+
* {@link isWe4B64} has accepted the string: the type check is the refusal, and
|
|
41
|
+
* this is the decode that follows it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function b64Decode(s: string): Uint8Array | null;
|
|
44
|
+
//# sourceMappingURL=wire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../src/wire.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,QAAwC,CAAC;AAElE;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAc3C;AAED,8EAA8E;AAC9E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEjE;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG/E;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAoBtD"}
|
package/dist/wire.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-value predicates shared by the receipt gate and the §9.3 step 7b quorum.
|
|
3
|
+
*
|
|
4
|
+
* They live here rather than in `verify.ts` for one reason: `quorum.ts` needs
|
|
5
|
+
* every one of them, `verify.ts` imports `quorum.ts`, and a second copy in the
|
|
6
|
+
* quorum would be two definitions of WE-4 in one package -- the encoding-split
|
|
7
|
+
* defect at the source level, in the exact rule whose whole point is that one
|
|
8
|
+
* nonce has one spelling. `verify.ts` re-exports these so the published surface
|
|
9
|
+
* is unchanged.
|
|
10
|
+
*/
|
|
11
|
+
/** The one wire nonce size, 128-bit (`Nonce128`, cited by AT-1 and L-17). */
|
|
12
|
+
export const NONCE128_BYTES = 16;
|
|
13
|
+
/**
|
|
14
|
+
* `"b64:"` plus base64 of 16 bytes: 4 + 24.
|
|
15
|
+
*
|
|
16
|
+
* Counted, never decoded -- a rule whose behaviour on bad input is "throw" is
|
|
17
|
+
* not a refusal, and the reference learned that the hard way (its first
|
|
18
|
+
* spelling decoded the body, which raises on the URL-safe alphabet).
|
|
19
|
+
*/
|
|
20
|
+
export const NONCE128_LEN = 4 + Math.ceil(NONCE128_BYTES / 3) * 4;
|
|
21
|
+
/**
|
|
22
|
+
* WE-4: is this the ASCII string `b64:` followed by RFC 4648 §4 base64, WITH
|
|
23
|
+
* padding? Ported from `quorum.rs::is_we4_b64` -- the body is whole
|
|
24
|
+
* four-character groups, the alphabet is §4's (`+` and `/`, never §5's `-` and
|
|
25
|
+
* `_`), and `=` appears only as one or two characters at the very end.
|
|
26
|
+
*
|
|
27
|
+
* Rejected, never normalised: normalising would make this verifier accept two
|
|
28
|
+
* spellings of one value, which is how one nonce comes to hold two ledger slots
|
|
29
|
+
* (ACP-87) and how one attestation comes to hold two ids.
|
|
30
|
+
*/
|
|
31
|
+
export function isWe4B64(s) {
|
|
32
|
+
if (!s.startsWith('b64:'))
|
|
33
|
+
return false;
|
|
34
|
+
const body = s.slice(4);
|
|
35
|
+
if (body.length % 4 !== 0)
|
|
36
|
+
return false;
|
|
37
|
+
let pad = 0;
|
|
38
|
+
for (let i = body.length - 1; i >= 0 && body[i] === '='; i -= 1)
|
|
39
|
+
pad += 1;
|
|
40
|
+
if (pad > 2)
|
|
41
|
+
return false;
|
|
42
|
+
for (let i = 0; i < body.length - pad; i += 1) {
|
|
43
|
+
const c = body.charCodeAt(i);
|
|
44
|
+
const alnum = (c >= 0x30 && c <= 0x39) || (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
|
|
45
|
+
if (!alnum && c !== 0x2b /* + */ && c !== 0x2f /* / */)
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/** A parsed JSON object, narrowed by a predicate rather than an `as` cast. */
|
|
51
|
+
export function isRecord(v) {
|
|
52
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
53
|
+
}
|
|
54
|
+
/** A string field of a parsed object, or null -- never a coerced value. */
|
|
55
|
+
export function strField(v, key) {
|
|
56
|
+
const raw = v[key];
|
|
57
|
+
return typeof raw === 'string' ? raw : null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* RFC 4648 §4 base64 with padding, decoded. The carrier WE-4 declares.
|
|
61
|
+
*
|
|
62
|
+
* Returns null rather than throwing, and is called only after
|
|
63
|
+
* {@link isWe4B64} has accepted the string: the type check is the refusal, and
|
|
64
|
+
* this is the decode that follows it.
|
|
65
|
+
*/
|
|
66
|
+
export function b64Decode(s) {
|
|
67
|
+
const A = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
68
|
+
if (s.length === 0 || s.length % 4 !== 0)
|
|
69
|
+
return null;
|
|
70
|
+
let pad = 0;
|
|
71
|
+
while (pad < 2 && s[s.length - 1 - pad] === '=')
|
|
72
|
+
pad += 1;
|
|
73
|
+
const out = [];
|
|
74
|
+
let acc = 0;
|
|
75
|
+
let bits = 0;
|
|
76
|
+
for (let i = 0; i < s.length - pad; i += 1) {
|
|
77
|
+
const v = A.indexOf(s[i] ?? '');
|
|
78
|
+
if (v < 0)
|
|
79
|
+
return null;
|
|
80
|
+
acc = (acc << 6) | v;
|
|
81
|
+
bits += 6;
|
|
82
|
+
if (bits >= 8) {
|
|
83
|
+
bits -= 8;
|
|
84
|
+
out.push((acc >> bits) & 0xff);
|
|
85
|
+
acc &= (1 << bits) - 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return Uint8Array.from(out);
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=wire.js.map
|
package/dist/wire.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.js","sourceRoot":"","sources":["../src/wire.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziffer-io/verify",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Verify a ZIFFER decision receipt in your own process, against a key you configured.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ziffer",
|
|
7
|
+
"agent",
|
|
8
|
+
"ai-agent",
|
|
9
|
+
"guardrail",
|
|
10
|
+
"approval",
|
|
11
|
+
"receipt"
|
|
12
|
+
],
|
|
5
13
|
"author": "code75 SASU",
|
|
6
14
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
-
"comment-license": "The SDK is proprietary (code75 SASU). \"license\" is the SPDX escape hatch for exactly this case: there is no SPDX identifier for these terms, so the field points at the file that states them, and LICENSE ships in the tarball beside THIRD-PARTY-NOTICES. Do not put an OSI identifier here -- Apache-2.0 stood in these four files until ACP-214 and was wrong the whole time.",
|
|
8
15
|
"type": "module",
|
|
9
16
|
"main": "./dist/index.js",
|
|
10
17
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"comment-exports": "The `./dist/*` entry is not laziness and removing it breaks a caller. `exports` is a gate as well as a map: declaring only `.` makes every other path unreachable, and packages/mcp/src/tools.test.ts imports `@ziffer-io/verify/dist/testkeys.js` ON PURPOSE -- testkeys is deliberately absent from index.ts so it cannot be reached by accident, and the deep import is how the MCP package's tests get a signing key without a second definition of one identity. That import resolves today because there is no exports map; adding a `.`-only one would break it at publish time, which is the class of defect an exports map introduces rather than prevents. So the dist is declared addressable, and the reason is here rather than in a reviewer's memory.",
|
|
12
18
|
"exports": {
|
|
13
19
|
".": {
|
|
14
20
|
"types": "./dist/index.d.ts",
|
|
@@ -34,12 +40,9 @@
|
|
|
34
40
|
"publishConfig": {
|
|
35
41
|
"access": "public"
|
|
36
42
|
},
|
|
37
|
-
"comment-no-provenance": "There is deliberately no \"provenance\": true here, and it was removed rather than never added. npm generates a provenance attestation only from a recognised CI runner, and its documentation states plainly that provenance is NOT SUPPORTED for private repositories (docs.npmjs.com/trusted-publishers, read 2026-09-03) -- ziffer-hq/ziffer is private (`gh api` says so). Setting the flag does not degrade to a warning: npm attempts the attestation and the publish FAILS, so the field would have broken the operator's very first publish from a laptop and every CI publish after it. tools/release-npm.sh asserts the field stays absent, and that assertion is the thing to delete on the day this repository becomes public -- at which point trusted publishing generates provenance on its own, with no flag at all.",
|
|
38
43
|
"ziffer": {
|
|
39
|
-
"enginePin": "
|
|
44
|
+
"enginePin": "7fd84dbe1a430e6deea4b241ebe1ba641103ffbe"
|
|
40
45
|
},
|
|
41
|
-
"comment-enginePin": "The engine commit whose clause spellings, canon and mirrored corpus this implementation is bound to. A COPY of the rev in Cargo.toml, which is the one authority tools/guard.sh reads; tools/release-npm.sh refuses to release when the two differ, by name. tools/bump-pin.sh does not move this field -- see packages/types/package.json for why and what closing it costs.",
|
|
42
|
-
"comment-deps": "Exactly the two signature primitives, at the versions services/approval already pins -- the DR-2 permitted common set minus the wire types, which this package does not consume: it verifies raw parsed JSON, and typing the wire is the client's (@ziffer-io/client) business. Adding anything else needs written justification (ACP-197 runbook section 5).",
|
|
43
46
|
"dependencies": {
|
|
44
47
|
"@noble/curves": "2.3.0",
|
|
45
48
|
"@noble/post-quantum": "0.7.0"
|
|
@@ -51,7 +54,7 @@
|
|
|
51
54
|
"scripts": {
|
|
52
55
|
"build": "tsc -b",
|
|
53
56
|
"typecheck": "tsc -b",
|
|
54
|
-
"pretest": "tsc -b && if ! find dist -name '*.test.js' -print -quit | grep -q .; then echo '
|
|
57
|
+
"pretest": "tsc -b && if ! find dist -name '*.test.js' -print -quit | grep -q .; then echo 'no compiled test file under dist/ -- node --test reports 0 tests and exits 0, so this package would go green having run nothing' >&2; exit 1; fi",
|
|
55
58
|
"test": "tsc -b && node --test \"dist/**/*.test.js\""
|
|
56
59
|
}
|
|
57
60
|
}
|