@blamejs/pki 0.2.5 → 0.2.6

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/lib/webauthn.js CHANGED
@@ -29,7 +29,8 @@ var pkix = require("./schema-pkix");
29
29
  var oid = require("./oid");
30
30
  var webcrypto = require("./webcrypto");
31
31
  var constants = require("./constants");
32
- var ByteReader = require("./byte-reader");
32
+ var validator = require("./validator-all");
33
+ var edwardsPoint = require("./edwards-point");
33
34
  var nodeCrypto = require("crypto");
34
35
 
35
36
  var WebauthnError = frameworkError.WebauthnError;
@@ -61,15 +62,13 @@ function _ucmp(a, b) {
61
62
  // A decoded node is a primitive universal INTEGER (so `.content` is a real buffer,
62
63
  // not null as it is for a constructed node).
63
64
  function _isInteger(node) { return !!node && !node.constructed && node.tagClass === "universal" && node.tagNumber === asn1.TAGS.INTEGER; }
64
- // The minimal unsigned big-endian encoding of a non-negative integer (no sign octet,
65
- // no leading zeros) -- for an unsigned compare against a COSE fixed-width field.
66
- function _uintBytes(n) {
67
- var hex = n.toString(16); if (hex.length % 2) hex = "0" + hex;
68
- var b = Buffer.from(hex, "hex"); var i = 0; while (i < b.length - 1 && b[i] === 0) i++; return b.subarray(i);
69
- }
70
- // COSE signature algorithm -> the node hash name of its signature scheme (the digest
71
- // the TPM extraData / apple nonce is taken under).
72
- var COSE_ALG_HASH = { "-7": "sha256", "-257": "sha256", "-37": "sha256", "-35": "sha384", "-258": "sha384", "-36": "sha512", "-259": "sha512", "-65535": "sha1" };
65
+ // alg -> the digest a TPM attestation's certInfo.extraData is taken under (the ONLY
66
+ // consumer). Every ECDSA/RSA algorithm a TPM AIK may sign with -- including the RFC 9864
67
+ // fully-specified ECDSA ids (ESP256/384/512) -- MUST appear here, or the TPM extraData
68
+ // step rejects the attestation before the signature is evaluated. EdDSA (-8/-19/-53) is
69
+ // absent by design: a TPM 2.0 AIK never signs with EdDSA, so such an attestation is
70
+ // correctly refused.
71
+ var COSE_ALG_HASH = { "-7": "sha256", "-9": "sha256", "-257": "sha256", "-37": "sha256", "-35": "sha384", "-51": "sha384", "-258": "sha384", "-36": "sha512", "-52": "sha512", "-259": "sha512", "-65535": "sha1" };
73
72
  function _coseAlgHash(alg, E) {
74
73
  var h = COSE_ALG_HASH[String(alg)];
75
74
  if (!h) throw E("webauthn/unsupported-algorithm", "no hash mapping for COSE algorithm " + alg);
@@ -89,15 +88,6 @@ function _mapGet(node, key) {
89
88
  }
90
89
  return null;
91
90
  }
92
- // An integer-keyed entry (COSE_Key labels are ints), by its signed value.
93
- function _mapGetInt(node, key) {
94
- if (!node || node.majorType !== 5 || !node.children) return null;
95
- for (var i = 0; i < node.children.length; i++) {
96
- var k = node.children[i][0];
97
- if ((k.majorType === 0 || k.majorType === 1) && cbor.read.int(k) === BigInt(key)) return node.children[i][1];
98
- }
99
- return null;
100
- }
101
91
 
102
92
  // ---- authenticatorData bounded reader (WebAuthn sec. 6.1) --------------------
103
93
 
@@ -134,7 +124,7 @@ function _parseAuthData(buf, E) {
134
124
  try { keyNode = cbor.decode(buf.subarray(off), { allowTrailing: true }); }
135
125
  catch (e) { throw E("webauthn/bad-cose-key", "the credential public key is not well-formed CBOR", e); }
136
126
  out.credentialPublicKeyBytes = buf.subarray(off, off + keyNode.bytes.length);
137
- out.credentialPublicKey = _decodeCoseKey(keyNode, E);
127
+ out.credentialPublicKey = _decodeCoseKey(keyNode);
138
128
  off += keyNode.bytes.length;
139
129
  }
140
130
  if (out.flags.ed) {
@@ -157,71 +147,10 @@ function _parseAuthData(buf, E) {
157
147
 
158
148
  // ---- COSE_Key decode (WebAuthn sec. 6.5.1, RFC 9052 sec. 7) ------------------
159
149
 
160
- // COSE EC2 curve (label -1) -> the fixed field-element byte length its x/y carry,
161
- // and the named-curve OID a certificate on that curve declares.
162
- var EC2_CRV_LEN = { 1: 32, 2: 48, 3: 66 }; // P-256 / P-384 / P-521
163
- var EC2_CRV_OID = { 1: "prime256v1", 2: "secp384r1", 3: "secp521r1" };
164
- // COSE OKP curve (label -1) -> its RFC 8410 named-key OID + fixed public-key length.
165
- // Ed25519 (crv 6, 32-byte) and Ed448 (crv 7, 57-byte) both sign under COSE alg -8.
166
- var OKP_CRV = { 6: { oid: "Ed25519", len: 32 }, 7: { oid: "Ed448", len: 57 } };
167
- // The WebAuthn credential-key profile: a COSE signature algorithm (label 3) pins the
168
- // key type (label 1) and, for EC2, the curve -- so an EC2 key claiming EdDSA, or an
169
- // OKP key claiming ES256, is a profile violation (WebAuthn sec. 6.5.1, RFC 9052).
170
- var ALG_PROFILE = {
171
- "-7": { kty: 2, crv: 1 }, "-35": { kty: 2, crv: 2 }, "-36": { kty: 2, crv: 3 },
172
- "-8": { kty: 1 },
173
- "-257": { kty: 3 }, "-258": { kty: 3 }, "-259": { kty: 3 }, "-37": { kty: 3 }, "-65535": { kty: 3 },
174
- };
175
-
176
- // kty 1 / alg 3; EC2 (kty 2): crv -1, x -2, y -3; OKP (kty 1): crv -1, x -2;
177
- // RSA (kty 3): n -1, e -2. Surface the raw coordinate/modulus buffers.
178
- function _decodeCoseKey(node, E) {
179
- if (!node || node.majorType !== 5) throw E("webauthn/bad-cose-key", "a COSE_Key must be a CBOR map (RFC 9052 sec. 7)");
180
- // Every parameter read maps a wrong-type cbor/* fault to the webauthn domain -- a
181
- // wrong-typed COSE label (x as an integer, kty as a string) is bad-cose-key input,
182
- // not a leaked codec error.
183
- function ib(label) { var n = _mapGetInt(node, label); if (!n) return null; try { return cbor.read.byteString(n); } catch (e) { throw E("webauthn/bad-cose-key", "COSE_Key parameter " + label + " must be a byte string", e); } }
184
- function ii(label) { var n = _mapGetInt(node, label); if (!n) return null; try { return cbor.read.int(n); } catch (e) { throw E("webauthn/bad-cose-key", "COSE_Key parameter " + label + " must be an integer", e); } }
185
- var ktyN = _mapGetInt(node, 1), algN = _mapGetInt(node, 3);
186
- if (!ktyN) throw E("webauthn/bad-cose-key", "a COSE_Key is missing the kty (label 1) parameter");
187
- // The credential public key MUST declare its algorithm (label 3): a relying party
188
- // needs it to verify the later assertion signature (WebAuthn sec. 6.5.1, COSE sec. 7).
189
- if (!algN) throw E("webauthn/bad-cose-key", "a COSE_Key is missing the alg (label 3) parameter");
190
- var kty, algv;
191
- try { kty = cbor.read.int(ktyN); algv = cbor.read.int(algN); }
192
- catch (e) { throw E("webauthn/bad-cose-key", "COSE_Key kty (label 1) and alg (label 3) must be integers", e); }
193
- var key = { kty: Number(kty), alg: Number(algv) };
194
- // A credential key MUST carry every parameter its key type requires; an incomplete
195
- // key (or an unknown kty) is rejected at decode rather than surfaced with null
196
- // material a later binding check would have to defend against.
197
- if (kty === 2n) {
198
- key.crv = ii(-1) != null ? Number(ii(-1)) : null; key.x = ib(-2); key.y = ib(-3);
199
- if (key.crv == null || !key.x || !key.y) throw E("webauthn/bad-cose-key", "an EC2 COSE_Key must carry crv (-1), x (-2), and y (-3)");
200
- var el = EC2_CRV_LEN[key.crv];
201
- if (!el || key.x.length !== el || key.y.length !== el) throw E("webauthn/bad-cose-key", "an EC2 COSE_Key x/y length is inconsistent with its curve");
202
- } else if (kty === 1n) {
203
- key.crv = ii(-1) != null ? Number(ii(-1)) : null; key.x = ib(-2);
204
- var okp = OKP_CRV[key.crv];
205
- if (!okp || !key.x || key.x.length !== okp.len) throw E("webauthn/bad-cose-key", "an OKP COSE_Key must be Ed25519 (crv 6) or Ed448 (crv 7) with a matching-length x (-2)");
206
- } else if (kty === 3n) {
207
- key.n = ib(-1); key.e = ib(-2);
208
- if (!key.n || !key.n.length || !key.e || !key.e.length) throw E("webauthn/bad-cose-key", "an RSA COSE_Key must carry n (-1) and e (-2)");
209
- } else {
210
- throw E("webauthn/bad-cose-key", "unsupported COSE_Key kty " + Number(kty));
211
- }
212
- // A WebAuthn credential key is the CANONICAL CTAP2 COSE_Key for its type -- exactly
213
- // its defined parameters, nothing more: EC2 = { 1, 3, -1, -2, -3 } (5), OKP/RSA = 4.
214
- // Extra parameters are rejected (a padded key is a canonicalization ambiguity).
215
- var expectedParams = kty === 2n ? 5 : 4;
216
- if (node.children.length !== expectedParams) throw E("webauthn/bad-cose-key", "the COSE_Key carries parameters beyond the canonical set for its key type (WebAuthn sec. 6.5.1)");
217
- // Enforce the alg <-> kty/crv profile: the declared algorithm must match the key
218
- // type (and, for EC2, the curve) it is used with.
219
- var prof = ALG_PROFILE[String(key.alg)];
220
- if (!prof) throw E("webauthn/bad-cose-key", "unsupported credential key algorithm " + key.alg);
221
- if (prof.kty !== key.kty) throw E("webauthn/bad-cose-key", "credential key algorithm " + key.alg + " is inconsistent with key type " + key.kty);
222
- if (prof.crv != null && prof.crv !== key.crv) throw E("webauthn/bad-cose-key", "credential key algorithm " + key.alg + " requires a different curve");
223
- return key;
224
- }
150
+ // The complete COSE credential-key conformance rule set (kty/alg/crv/length/canonical/
151
+ // profile/on-curve) lives in validator-cose, composed here so every credential key
152
+ // routes through the one home -- never a per-format re-derivation of a partial subset.
153
+ function _decodeCoseKey(node) { return validator.cose.credentialKey(node, WebauthnError, "webauthn/bad-cose-key"); }
225
154
 
226
155
  // ---- signature verification bridge ------------------------------------------
227
156
 
@@ -233,6 +162,15 @@ var COSE_ALG = {
233
162
  "-35": { imp: { name: "ECDSA", namedCurve: "P-384" }, verify: { name: "ECDSA", hash: "SHA-384" }, ecdsa: 48 },
234
163
  "-36": { imp: { name: "ECDSA", namedCurve: "P-521" }, verify: { name: "ECDSA", hash: "SHA-512" }, ecdsa: 66 },
235
164
  "-8": { imp: { name: "Ed25519" }, verify: { name: "Ed25519" }, ecdsa: 0 },
165
+ // RFC 9864 fully-specified ids. WebAuthn recommends against them for credential creation,
166
+ // but a verifier MUST evaluate an assertion signed under one. ESP256/384/512 are the
167
+ // curve-pinned ECDSA twins of ES256/384/512; Ed25519(-19)/Ed448(-53) are fully-specified
168
+ // EdDSA (Ed448 is the ONLY WebAuthn path to Ed448 -- -8 is Ed25519 only).
169
+ "-9": { imp: { name: "ECDSA", namedCurve: "P-256" }, verify: { name: "ECDSA", hash: "SHA-256" }, ecdsa: 32 },
170
+ "-51": { imp: { name: "ECDSA", namedCurve: "P-384" }, verify: { name: "ECDSA", hash: "SHA-384" }, ecdsa: 48 },
171
+ "-52": { imp: { name: "ECDSA", namedCurve: "P-521" }, verify: { name: "ECDSA", hash: "SHA-512" }, ecdsa: 66 },
172
+ "-19": { imp: { name: "Ed25519" }, verify: { name: "Ed25519" }, ecdsa: 0 },
173
+ "-53": { imp: { name: "Ed448" }, verify: { name: "Ed448" }, ecdsa: 0 },
236
174
  "-257": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
237
175
  "-258": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
238
176
  "-259": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
@@ -244,33 +182,10 @@ var COSE_ALG = {
244
182
  "-65535": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-1" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
245
183
  };
246
184
 
247
- // DER ECDSA-Sig-Value SEQUENCE { r INTEGER, s INTEGER } -> raw r||s, each left-
248
- // padded to `coordLen` (the ieee-p1363 form WebCrypto verify expects).
249
- function _derEcdsaToRaw(der, coordLen, E) {
250
- var node;
251
- try { node = asn1.decode(der); } catch (e) { throw E("webauthn/bad-signature", "ECDSA signature is not a DER SEQUENCE", e); }
252
- if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length !== 2) {
253
- throw E("webauthn/bad-signature", "ECDSA signature must be a DER SEQUENCE { r, s }");
254
- }
255
- function coord(c) {
256
- // r and s MUST be primitive universal INTEGERs; a constructed child has a null
257
- // content buffer, so assert the shape before touching `.content` (else a crafted
258
- // SEQUENCE { SEQUENCE {}, SEQUENCE {} } would raw-throw instead of failing typed).
259
- if (c.constructed || c.tagClass !== "universal" || c.tagNumber !== asn1.TAGS.INTEGER) {
260
- throw E("webauthn/bad-signature", "ECDSA signature r/s must be a primitive INTEGER");
261
- }
262
- var b = c.content;
263
- // r and s MUST be positive: an empty INTEGER, or a DER-negative one (top bit of
264
- // the first content octet set, with no leading 0x00 sign octet), is not a valid
265
- // ECDSA signature coordinate.
266
- if (!b.length || (b[0] & 0x80)) throw E("webauthn/bad-signature", "ECDSA signature r/s must be a positive integer");
267
- while (b.length > 1 && b[0] === 0x00) b = b.subarray(1); // strip the DER sign octet
268
- if (b.length === 1 && b[0] === 0x00) throw E("webauthn/bad-signature", "ECDSA signature r/s must be a positive integer"); // reject zero
269
- if (b.length > coordLen) throw E("webauthn/bad-signature", "ECDSA signature coordinate exceeds the curve field size");
270
- var out = Buffer.alloc(coordLen); b.copy(out, coordLen - b.length); return out;
271
- }
272
- return Buffer.concat([coord(node.children[0]), coord(node.children[1])]);
273
- }
185
+ // DER ECDSA-Sig-Value { r, s } -> raw r||s (the ieee-p1363 form WebCrypto verify expects).
186
+ // validator-sig owns the complete strict-DER conformance (primitive, minimal, positive,
187
+ // bounded r/s), composed here.
188
+ function _derEcdsaToRaw(der, coordLen) { return validator.sig.ecdsaSigToRaw(der, coordLen, WebauthnError, "webauthn/bad-signature"); }
274
189
 
275
190
  // Verify `sig` over `message` with the SPKI public key `spkiBytes` under COSE `alg`.
276
191
  // A wrong signature resolves `false` from subtle.verify without throwing (a false
@@ -283,7 +198,12 @@ function _verifySig(alg, sig, spkiBytes, message, E) {
283
198
  // COSE alg -8 (EdDSA) covers Ed25519 and Ed448; the WebCrypto name follows the
284
199
  // signing key's own SPKI algorithm OID, not the (curve-agnostic) alg id.
285
200
  if (alg === -8) { var nm = _edName(spkiBytes, E); imp = { name: nm }; ver = { name: nm }; }
286
- var s = d.ecdsa ? _derEcdsaToRaw(sig, d.ecdsa, E) : sig;
201
+ // node/OpenSSL imports any Ed25519/Ed448 SPKI without validating the point, and a
202
+ // low-order (e.g. all-zeroes) key verifies a trivial signature -- so validate the OKP
203
+ // point before verify. This covers EVERY key that signs a WebAuthn statement: the x5c
204
+ // attestation-certificate key (packed/tpm/apple) AND the self-attestation credential key.
205
+ if (imp.name === "Ed25519" || imp.name === "Ed448") _requireValidEdPoint(spkiBytes, imp.name, E);
206
+ var s = d.ecdsa ? _derEcdsaToRaw(sig, d.ecdsa) : sig;
287
207
  return subtle.importKey("spki", spkiBytes, imp, false, ["verify"])
288
208
  .then(function (key) { return subtle.verify(ver, key, s, message); })
289
209
  .catch(function (e) { throw _err("webauthn/verify-error", "the attestation signature could not be evaluated", e); });
@@ -300,35 +220,24 @@ function _edName(spkiBytes, E) {
300
220
  if (!nm) throw E("webauthn/unsupported-algorithm", "unsupported EdDSA curve OID " + algOid);
301
221
  return nm;
302
222
  }
303
-
304
- // COSE EC2 / RSA credential key -> a self-contained SPKI the WebCrypto import
305
- // consumes, so a credential public key and a certificate key verify by one path.
306
- function _coseKeyToSpki(key, E) {
307
- if (key.kty === 2 && key.x && key.y) {
308
- var curveOid = key.crv === 1 ? "prime256v1" : key.crv === 2 ? "secp384r1" : key.crv === 3 ? "secp521r1" : null;
309
- if (!curveOid) throw E("webauthn/bad-cose-key", "unsupported EC2 curve " + key.crv);
310
- var b = asn1.build;
311
- return b.sequence([
312
- b.sequence([b.oid(oid.byName("ecPublicKey")), b.oid(oid.byName(curveOid))]),
313
- b.bitString(Buffer.concat([Buffer.from([0x04]), key.x, key.y])),
314
- ]);
315
- }
316
- if (key.kty === 3 && key.n && key.n.length && key.e && key.e.length) {
317
- var bb = asn1.build;
318
- return bb.sequence([
319
- bb.sequence([bb.oid(oid.byName("rsaEncryption")), bb.nullValue()]),
320
- bb.bitString(bb.sequence([bb.integer(BigInt("0x" + key.n.toString("hex"))), bb.integer(BigInt("0x" + key.e.toString("hex")))])),
321
- ]);
322
- }
323
- if (key.kty === 1 && OKP_CRV[key.crv] && key.x && key.x.length === OKP_CRV[key.crv].len) {
324
- // OKP Ed25519 / Ed448: the SPKI is AlgorithmIdentifier { id-Ed* } (no parameters)
325
- // over the raw public key -- RFC 8410.
326
- var eb = asn1.build;
327
- return eb.sequence([eb.sequence([eb.oid(oid.byName(OKP_CRV[key.crv].oid))]), eb.bitString(key.x)]);
223
+ // The raw Edwards point an OKP SPKI carries (its BIT STRING body, past the unused-bits
224
+ // octet) MUST be a valid, full-order point -- reject an off-curve or low-order key before it
225
+ // verifies a signature (WebCrypto import does not check it). Curve from the WebCrypto name.
226
+ function _requireValidEdPoint(spkiBytes, name, E) {
227
+ var content;
228
+ try { content = asn1.decode(spkiBytes).children[1].content; }
229
+ catch (e) { throw E("webauthn/bad-signature", "the EdDSA public key is not a well-formed SPKI", e); }
230
+ var point = content && content.length ? content.subarray(1) : Buffer.alloc(0);
231
+ if (!edwardsPoint.validate(point, name === "Ed25519" ? 6 : 7)) {
232
+ throw E("webauthn/bad-signature", "the EdDSA public key is not a valid, full-order Edwards point");
328
233
  }
329
- throw E("webauthn/bad-cose-key", "cannot build an SPKI for this COSE key type");
330
234
  }
331
235
 
236
+ // A validated COSE credential key -> a self-contained SPKI the WebCrypto import
237
+ // consumes, so a credential public key and a certificate key verify by one path.
238
+ // validator-cose owns the encoding (EC2 / RSA / OKP), composed here.
239
+ function _coseKeyToSpki(key) { return validator.cose.toSpki(key, WebauthnError, "webauthn/bad-cose-key"); }
240
+
332
241
  // The attestation-certificate subject public key MUST equal the credential public
333
242
  // key that authenticatorData carries (WebAuthn 8.4/8.8 item 30). Compare the raw
334
243
  // key material unsigned: an EC2 uncompressed point's X/Y vs the COSE x/y; an RSA
@@ -352,7 +261,7 @@ function _certPubKeyEqualsCose(cert, cose, E) {
352
261
  if (cose.kty === 2) {
353
262
  // The certificate's declared EC curve MUST equal the credential key's curve --
354
263
  // a curve substitution is a different key even if the coordinate bytes line up.
355
- var wantCurve = EC2_CRV_OID[cose.crv];
264
+ var wantCurve = validator.cose.EC2_CRV_OID[cose.crv];
356
265
  if (!wantCurve) throw E("webauthn/key-mismatch", "unsupported credential EC curve " + cose.crv);
357
266
  if (_certEcCurveOid(cert, E) !== oid.byName(wantCurve)) throw E("webauthn/key-mismatch", "the attestation certificate EC curve does not equal the credential key curve");
358
267
  if (raw.length < 1 || raw[0] !== 0x04) throw E("webauthn/key-mismatch", "the attestation certificate key is not an uncompressed EC point");
@@ -382,71 +291,13 @@ function _certPubKeyEqualsCose(cert, cose, E) {
382
291
  throw E("webauthn/key-mismatch", "unsupported credential key type for the certificate comparison");
383
292
  }
384
293
 
385
- // ---- TPM structure readers (WebAuthn 8.3; TCG TPM 2.0 Part 2) ----------------
386
- // Every field is unsigned big-endian, packed with no padding; every TPM2B_* is a
387
- // UINT16 size followed by exactly `size` bytes. The bare certInfo (TPMS_ATTEST) and
388
- // pubArea (TPMT_PUBLIC) carry NO outer TPM2B size prefix.
389
- var TPM_GENERATED_VALUE = 0xff544347;
390
- var TPM_ST_ATTEST_CERTIFY = 0x8017;
391
- var TPM_ALG = { RSA: 0x0001, SHA1: 0x0004, SHA256: 0x000b, SHA384: 0x000c, SHA512: 0x000d, NULL: 0x0010, ECC: 0x0023 };
392
- var TPM_ALG_HASH = {}; TPM_ALG_HASH[TPM_ALG.SHA1] = "sha1"; TPM_ALG_HASH[TPM_ALG.SHA256] = "sha256"; TPM_ALG_HASH[TPM_ALG.SHA384] = "sha384"; TPM_ALG_HASH[TPM_ALG.SHA512] = "sha512";
393
- // TPM_ECC_CURVE -> COSE crv (the codepoints differ; this is a mapping, not equality).
394
- var TPM_CURVE_TO_COSE = {}; TPM_CURVE_TO_COSE[0x0003] = 1; TPM_CURVE_TO_COSE[0x0004] = 2; TPM_CURVE_TO_COSE[0x0005] = 3;
395
-
396
- function _tpmReader(buf) { return new ByteReader(buf, 0, buf.length, WebauthnError, "webauthn/bad-tpm"); }
397
-
398
- // TPMT_PUBLIC (WebAuthn 8.3 item 17-20): decode type/nameAlg + the public key in
399
- // `unique`, walking past `parameters` per the algorithm union.
400
- function _parseTpmPubArea(buf) {
401
- var r = _tpmReader(buf);
402
- var type = r.u16(), nameAlg = r.u16();
403
- r.u32(); // objectAttributes (full policy validation deferred)
404
- r.vector(2, 0, null); // authPolicy TPM2B_DIGEST
405
- var pub = { type: type, nameAlg: nameAlg, nameAlgBytes: buf.subarray(2, 4) };
406
- if (type === TPM_ALG.RSA) {
407
- _tpmSymDef(r); // symmetric TPMT_SYM_DEF_OBJECT
408
- _tpmScheme(r); // scheme TPMT_RSA_SCHEME
409
- r.u16(); // keyBits
410
- var exp = r.u32(); // exponent (0 => default 65537)
411
- pub.exponent = exp === 0 ? 65537 : exp;
412
- pub.rsa = r.vector(2, 0, null); // unique TPM2B_PUBLIC_KEY_RSA (the modulus)
413
- } else if (type === TPM_ALG.ECC) {
414
- _tpmSymDef(r); // symmetric
415
- _tpmScheme(r); // scheme TPMT_ECC_SCHEME
416
- pub.curveId = r.u16(); // curveID TPMI_ECC_CURVE
417
- _tpmScheme(r); // kdf TPMT_KDF_SCHEME
418
- pub.x = r.vector(2, 0, null); // unique.ecc.x TPM2B_ECC_PARAMETER
419
- pub.y = r.vector(2, 0, null); // unique.ecc.y TPM2B_ECC_PARAMETER
420
- } else {
421
- throw _err("webauthn/bad-tpm", "unsupported TPMT_PUBLIC type 0x" + type.toString(16));
422
- }
423
- // TPMT_PUBLIC ends with `unique`; trailing bytes mean a malformed pubArea (and
424
- // would perturb the TPM Name hash), so fail closed rather than silently ignore them.
425
- if (!r.atEnd()) throw _err("webauthn/bad-tpm", "pubArea has trailing bytes after the unique field (WebAuthn 8.3)");
426
- return pub;
427
- }
428
- // TPMT_SYM_DEF_OBJECT: algorithm UINT16; when not NULL, keyBits+mode follow. An
429
- // attestation (restricted signing) key is NULL, so the non-NULL arm is defensive.
430
- function _tpmSymDef(r) { if (r.u16() !== TPM_ALG.NULL) { r.u16(); r.u16(); } }
431
- // TPMT_*_SCHEME / TPMT_KDF_SCHEME: scheme UINT16; when not NULL, a TPMS_SCHEME_HASH
432
- // (a single UINT16 hashAlg) follows for the signing/kdf schemes in scope.
433
- function _tpmScheme(r) { if (r.u16() !== TPM_ALG.NULL) { r.u16(); } }
434
-
435
- // TPMS_ATTEST (WebAuthn 8.3 item 13-15): magic/type + extraData + attested.name.
436
- // The whole certInfo is walked -- through qualifiedName and to the end -- so a
437
- // certInfo with trailing bytes past the TPMS_CERTIFY_INFO union fails closed.
438
- function _parseTpmCertInfo(buf) {
439
- var r = _tpmReader(buf);
440
- var magic = r.u32(), type = r.u16();
441
- r.vector(2, 0, null); // qualifiedSigner TPM2B_NAME
442
- var extraData = r.vector(2, 0, null); // extraData TPM2B_DATA
443
- r.u64(); r.u32(); r.u32(); r.u8(); // clockInfo TPMS_CLOCK_INFO (17 bytes)
444
- r.u64(); // firmwareVersion
445
- var name = r.vector(2, 0, null); // attested.name TPM2B_NAME (nameAlg||H)
446
- r.vector(2, 0, null); // attested.qualifiedName TPM2B_NAME
447
- if (!r.atEnd()) throw _err("webauthn/bad-tpm", "certInfo has trailing bytes after the attested structure (WebAuthn 8.3)");
448
- return { magic: magic, type: type, extraData: extraData, attestedName: name };
449
- }
294
+ // ---- TPM structure conformance (WebAuthn 8.3; TCG TPM 2.0 Part 2) -------------
295
+ // The TPMT_PUBLIC (pubArea) + TPMS_ATTEST (certInfo) decode + the pubArea-key binding
296
+ // live in validator-tpm, composed here. A malformed structure is bad-tpm; a key that
297
+ // does not equal the credential key is key-mismatch.
298
+ function _parseTpmPubArea(buf) { return validator.tpm.parsePubArea(buf, WebauthnError, "webauthn/bad-tpm"); }
299
+ function _parseTpmCertInfo(buf) { return validator.tpm.parseCertInfo(buf, WebauthnError, "webauthn/bad-tpm"); }
300
+ function _tpmPubKeyEqualsCose(pub, cose) { validator.tpm.pubKeyEqualsCose(pub, cose, WebauthnError, "webauthn/key-mismatch", "webauthn/bad-tpm"); }
450
301
 
451
302
  // ---- extension helpers -------------------------------------------------------
452
303
  // `oidName` is a registered name (byName resolves it at call time); an unregistered
@@ -455,17 +306,6 @@ function _findExt(cert, oidName) {
455
306
  var target = oid.byName(oidName);
456
307
  return (cert.extensions || []).filter(function (e) { return e.oid === target; })[0] || null;
457
308
  }
458
- // AuthorizationList (Android key attestation) is a SEQUENCE of [tag] EXPLICIT
459
- // OPTIONAL fields with non-contiguous tag numbers; return the EXPLICIT-unwrapped
460
- // value node for a tag, or null when absent (a tolerant, skip-unknown scan).
461
- function _alGet(seqNode, tag) {
462
- if (!seqNode || !seqNode.children) return null;
463
- for (var i = 0; i < seqNode.children.length; i++) {
464
- var c = seqNode.children[i];
465
- if (c.tagClass === "context" && c.tagNumber === tag) return c.children && c.children[0] ? c.children[0] : c;
466
- }
467
- return null;
468
- }
469
309
 
470
310
  // ---- public: parseAttestationObject -----------------------------------------
471
311
 
@@ -548,50 +388,15 @@ function _readX5c(attStmt) {
548
388
  try { return x509.parse(der); } catch (e) { throw _err("webauthn/bad-att-stmt", "an x5c certificate is not a well-formed X.509 certificate", e); }
549
389
  });
550
390
  }
551
- // Every attestation leaf cert MUST be an X.509 v3 certificate (WebAuthn 8.2.1 /
552
- // 8.3.1 / 8.4). The chain to a trust anchor is the caller's to run through
553
- // pki.path.validate with a supplied root; a bare chain is surfaced in trustPath.
554
- function _requireV3(cert) {
555
- if (cert.version !== 3) throw _err("webauthn/bad-att-cert", "an attestation certificate must be X.509 version 3");
556
- }
557
- // A packed / tpm attestation LEAF certificate MUST carry a basicConstraints
558
- // extension asserting cA=false (WebAuthn 8.2.1 / 8.3.1). Requiring the extension --
559
- // not merely rejecting a present cA=true -- is the gate that stops a genuine CA
560
- // certificate under the caller's pinned root, or a leaf that simply omits the
561
- // constraint, from being repurposed as an attestation leaf; RFC 5280 path validation
562
- // does not reject a CA-asserting or unconstrained leaf.
563
- function _assertNotCa(cert) {
564
- var bc = _decodeExt(cert, "basicConstraints");
565
- if (!bc) throw _err("webauthn/bad-att-cert", "an attestation leaf certificate MUST carry a basicConstraints extension (WebAuthn 8.2.1 / 8.3.1)");
566
- if (bc.value && bc.value.cA === true) throw _err("webauthn/bad-att-cert", "an attestation leaf certificate MUST NOT be a CA (basicConstraints cA=true)");
567
- }
568
- // Packed attestation certificate requirements (WebAuthn 8.2.1): v3, non-CA, and the
569
- // subject set to C (country), O (organization), OU = "Authenticator Attestation",
570
- // and CN (a vendor string).
571
- function _checkPackedCert(cert) {
572
- _requireV3(cert);
573
- _assertNotCa(cert);
574
- var have = {};
575
- (cert.subject.rdns || []).forEach(function (rdn) { rdn.forEach(function (a) { have[a.type] = a.value; }); });
576
- if (have[oid.byName("countryName")] == null || have[oid.byName("organizationName")] == null || have[oid.byName("commonName")] == null) {
577
- throw _err("webauthn/bad-att-cert", "the packed attestation certificate subject MUST set C, O, OU, and CN (WebAuthn 8.2.1)");
578
- }
579
- if (have[oid.byName("organizationalUnitName")] !== "Authenticator Attestation") {
580
- throw _err("webauthn/bad-att-cert", "the packed attestation certificate subject OU MUST be \"Authenticator Attestation\" (WebAuthn 8.2.1)");
581
- }
582
- }
583
- var AAGUID_EXT_OID = oid.byName("idFidoGenCeAaguid"); // a registered name, resolved at load
584
- // The id-fido-gen-ce-aaguid extension value (when present) MUST equal the
585
- // attestedCredentialData aaguid, and it MUST NOT be critical (WebAuthn 8.2.1).
586
- function _checkAaguidExt(cert, aaguid) {
587
- var ext = (cert.extensions || []).filter(function (e) { return e.oid === AAGUID_EXT_OID; })[0];
588
- if (!ext) return; // absence is tolerated; presence must match
589
- if (ext.critical) throw _err("webauthn/bad-att-cert", "the id-fido-gen-ce-aaguid extension MUST NOT be critical (WebAuthn 8.2.1)");
590
- var val;
591
- try { val = asn1.read.octetString(asn1.decode(ext.value)); }
592
- catch (e) { throw _err("webauthn/bad-att-cert", "the id-fido-gen-ce-aaguid extension value is not a valid OCTET STRING", e); }
593
- if (!val.equals(aaguid)) throw _err("webauthn/aaguid-mismatch", "the id-fido-gen-ce-aaguid extension value does not equal the authenticatorData aaguid");
594
- }
391
+ // The WebAuthn attestation-certificate profile checks (WebAuthn 8.2.1 packed / 8.3.1 TPM
392
+ // AIK / the id-fido-gen-ce-aaguid extension) live in validator-attcert, composed here. The
393
+ // extension-accessor object hands the validator this format's fail-closed extension
394
+ // decoders so it stays decoupled from the webauthn error namespace.
395
+ var _exts = { find: _findExt, decode: _decodeExt };
396
+ function _requireV3(cert) { validator.attcert.requireV3(cert, WebauthnError, "webauthn/bad-att-cert"); }
397
+ function _checkPackedCert(cert) { validator.attcert.packedCert(cert, _exts, WebauthnError, "webauthn/bad-att-cert"); }
398
+ function _checkAikCert(cert) { validator.attcert.aikCert(cert, _exts, WebauthnError, "webauthn/bad-att-cert"); }
399
+ function _checkAaguidExt(cert, aaguid) { validator.attcert.aaguidExt(cert, aaguid, _exts, WebauthnError, "webauthn/bad-att-cert", "webauthn/aaguid-mismatch"); }
595
400
 
596
401
  var VERIFIERS = {
597
402
  // packed (WebAuthn 8.2): the x5c arm (Basic/AttCA) or self-attestation.
@@ -612,7 +417,7 @@ var VERIFIERS = {
612
417
  // Self-attestation: the statement alg MUST equal the credential key's own alg
613
418
  // (WebAuthn 8.2), then sig verifies under the credential key itself.
614
419
  if (alg !== att.authData.credentialPublicKey.alg) throw _err("webauthn/bad-att-stmt", "the packed self-attestation alg does not match the credential public key algorithm (WebAuthn 8.2)");
615
- var spki = _coseKeyToSpki(att.authData.credentialPublicKey, _err);
420
+ var spki = _coseKeyToSpki(att.authData.credentialPublicKey);
616
421
  return _verifySig(alg, sig, spki, message, _err).then(function (ok) {
617
422
  if (!ok) throw _err("webauthn/verify-failed", "the packed self-attestation signature does not verify under the credential key");
618
423
  return _result("packed", "Self", [], att);
@@ -630,8 +435,10 @@ var VERIFIERS = {
630
435
  var leaf = chain[0];
631
436
  var sig = _sigOf(att.attStmt);
632
437
  var key = att.authData.credentialPublicKey;
633
- if (key.kty !== 2 || key.crv !== 1 || !key.x || !key.y || key.x.length !== 32 || key.y.length !== 32) {
634
- throw _err("webauthn/bad-att-stmt", "fido-u2f requires an EC2 P-256 credential public key (WebAuthn 8.6)");
438
+ // WebAuthn 8.6: the fido-u2f credential public key MUST be alg -7 (ES256) on EC2 P-256 --
439
+ // the newer ESP256 (-9) id, though the same curve, is not a valid fido-u2f credential.
440
+ if (key.alg !== -7 || key.kty !== 2 || key.crv !== 1 || !key.x || !key.y || key.x.length !== 32 || key.y.length !== 32) {
441
+ throw _err("webauthn/bad-att-stmt", "fido-u2f requires an ES256 (-7) EC2 P-256 credential public key (WebAuthn 8.6)");
635
442
  }
636
443
  var publicKeyU2F = Buffer.concat([Buffer.from([0x04]), key.x, key.y]);
637
444
  var verificationData = Buffer.concat([Buffer.from([0x00]), att.authData.rpIdHash, clientDataHash, att.authData.credentialId, publicKeyU2F]);
@@ -682,21 +489,20 @@ var VERIFIERS = {
682
489
  var pubAreaBytes = _attRead(att.attStmt, "pubArea", cbor.read.byteString, "a byte string");
683
490
  var certInfoBytes = _attRead(att.attStmt, "certInfo", cbor.read.byteString, "a byte string");
684
491
  var chain = _readX5c(att.attStmt), aik = chain[0];
685
- _requireV3(aik);
686
492
 
687
493
  var pub = _parseTpmPubArea(pubAreaBytes);
688
494
  _tpmPubKeyEqualsCose(pub, att.authData.credentialPublicKey); // 8.3 item 22
689
495
 
496
+ // validator-tpm decodes certInfo AND validates magic == TPM_GENERATED_VALUE / type ==
497
+ // TPM_ST_ATTEST_CERTIFY, so the returned structure is already self-consistent.
690
498
  var certInfo = _parseTpmCertInfo(certInfoBytes);
691
- if (certInfo.magic !== TPM_GENERATED_VALUE) throw _err("webauthn/bad-tpm", "certInfo magic is not TPM_GENERATED_VALUE (WebAuthn 8.3)");
692
- if (certInfo.type !== TPM_ST_ATTEST_CERTIFY) throw _err("webauthn/bad-tpm", "certInfo type is not TPM_ST_ATTEST_CERTIFY (WebAuthn 8.3)");
693
499
 
694
500
  // extraData == hash_alg(authData || clientDataHash) (bare digest, no method id).
695
501
  var attToBeSigned = Buffer.concat([att.authDataBytes, clientDataHash]);
696
502
  if (!certInfo.extraData.equals(_sha(_coseAlgHash(alg, _err), attToBeSigned))) throw _err("webauthn/verify-failed", "certInfo extraData does not equal the hash of authData || clientDataHash");
697
503
 
698
504
  // attested.name == nameAlg || H_nameAlg(pubArea).
699
- var nameHash = TPM_ALG_HASH[pub.nameAlg];
505
+ var nameHash = validator.tpm.TPM_ALG_HASH[pub.nameAlg];
700
506
  if (!nameHash) throw _err("webauthn/bad-tpm", "unsupported TPM nameAlg 0x" + pub.nameAlg.toString(16));
701
507
  var computedName = Buffer.concat([pub.nameAlgBytes, _sha(nameHash, pubAreaBytes)]);
702
508
  if (!certInfo.attestedName.equals(computedName)) throw _err("webauthn/verify-failed", "certInfo attested Name does not match the pubArea TPM Name");
@@ -736,27 +542,6 @@ function _result(fmt, attestationType, chain, att) {
736
542
  return { verified: true, fmt: fmt, attestationType: attestationType, trustPath: (chain || []).slice().reverse(), aaguid: att.authData.aaguid, credentialPublicKey: att.authData.credentialPublicKey };
737
543
  }
738
544
 
739
- // pubArea public key == credentialPublicKey (unsigned compare; WebAuthn 8.3 item 22).
740
- function _tpmPubKeyEqualsCose(pub, cose) {
741
- if (pub.type === TPM_ALG.ECC) {
742
- if (cose.kty !== 2 || TPM_CURVE_TO_COSE[pub.curveId] !== cose.crv || !_ucmp(pub.x, cose.x || Buffer.alloc(0)) || !_ucmp(pub.y, cose.y || Buffer.alloc(0))) {
743
- throw _err("webauthn/key-mismatch", "the TPM pubArea EC key does not equal the credential public key");
744
- }
745
- return;
746
- }
747
- if (pub.type === TPM_ALG.RSA) {
748
- // Compare the exponent as an unsigned integer over its FULL width (a UINT32 up to
749
- // 0xFFFFFFFF); a fixed 3-byte re-encode would silently truncate an exponent
750
- // > 0xFFFFFF and let a mismatched key pass (WebAuthn 8.3 item 22).
751
- var e = _uintBytes(pub.exponent >>> 0);
752
- if (cose.kty !== 3 || !_ucmp(pub.rsa, cose.n || Buffer.alloc(0)) || !_ucmp(e, cose.e || Buffer.alloc(0))) {
753
- throw _err("webauthn/key-mismatch", "the TPM pubArea RSA key does not equal the credential public key");
754
- }
755
- return;
756
- }
757
- throw _err("webauthn/bad-tpm", "unsupported TPM pubArea key type");
758
- }
759
-
760
545
  // Decode the Apple extension AppleAnonymousAttestation ::= SEQUENCE { nonce [1]
761
546
  // EXPLICIT OCTET STRING } (WebAuthn 8.8 item 29) and return the 32-byte nonce.
762
547
  function _appleNonce(extValue) {
@@ -769,73 +554,13 @@ function _appleNonce(extValue) {
769
554
  try { return asn1.read.octetString(tagged.children[0]); } catch (e) { throw _err("webauthn/bad-att-cert", "the apple attestation nonce is not an OCTET STRING", e); }
770
555
  }
771
556
 
772
- // The four Android KeyDescription checks (WebAuthn 8.4.1 item 28).
557
+ // The complete Android Key Attestation KeyDescription conformance (AOSP schema + WebAuthn
558
+ // 8.4.1) lives in validator-keydesc, composed here. A structural fault is bad-att-cert; a
559
+ // well-formed description that fails an 8.4.1 MUST is verify-failed.
773
560
  function _checkAndroidKeyDescription(cert, clientDataHash) {
774
- var ext = _findExt(cert, "keyDescription");
775
- if (!ext) throw _err("webauthn/bad-att-cert", "the android-key attestation certificate is missing the key-description extension (WebAuthn 8.4.1)");
776
- var kd;
777
- try { kd = asn1.decode(ext.value); } catch (e) { throw _err("webauthn/bad-att-cert", "the android KeyDescription is not decodable", e); }
778
- if (!kd.children || kd.children.length < 8) throw _err("webauthn/bad-att-cert", "the android KeyDescription is not a positional 8-field SEQUENCE");
779
- // (1) attestationChallenge (position 4) == clientDataHash.
780
- var challenge;
781
- try { challenge = asn1.read.octetString(kd.children[4]); } catch (e) { throw _err("webauthn/bad-att-cert", "the android attestationChallenge is not an OCTET STRING", e); }
782
- if (!challenge.equals(clientDataHash)) throw _err("webauthn/verify-failed", "the android attestationChallenge does not equal clientDataHash");
783
- var softwareEnforced = kd.children[6], hardwareEnforced = kd.children[7];
784
- // (2) [600] allApplications MUST be ABSENT in both lists.
785
- if (_alGet(softwareEnforced, 600) || _alGet(hardwareEnforced, 600)) throw _err("webauthn/verify-failed", "android allApplications MUST be absent (WebAuthn 8.4.1)");
786
- // (3)+(4) origin == KM_ORIGIN_GENERATED (0) and purpose == exactly {KM_PURPOSE_SIGN}
787
- // (2), evaluated over the UNION of the teeEnforced and softwareEnforced lists --
788
- // the WebAuthn 8.4.1 default (a teeEnforced-only policy is the caller's opt-in).
789
- // Origin: at least one list MUST declare it, and EVERY list that declares one MUST
790
- // say GENERATED -- a mixed teeEnforced=IMPORTED / softwareEnforced=GENERATED key is
791
- // contradictory and rejected, not accepted on the strength of one list.
792
- var origins = [_alGet(softwareEnforced, 702), _alGet(hardwareEnforced, 702)].filter(Boolean);
793
- if (!origins.length || !origins.every(function (o) { return _alInt(o) === 0n; })) {
794
- throw _err("webauthn/verify-failed", "android key origin is not KM_ORIGIN_GENERATED in every authorization list that declares it (WebAuthn 8.4.1)");
795
- }
796
- var purposes = _purposeUnion(softwareEnforced, hardwareEnforced);
797
- if (purposes.length !== 1 || purposes[0] !== 2n) {
798
- throw _err("webauthn/verify-failed", "android key purpose is not exactly KM_PURPOSE_SIGN (WebAuthn 8.4.1)");
799
- }
800
- }
801
- // Read an android KeyDescription authorization INTEGER, mapping a wrong-type asn1/*
802
- // fault to the webauthn domain (a malformed authorization value is bad-att-cert input
803
- // to this layer, not a leaked codec error).
804
- function _alInt(node) {
805
- try { return asn1.read.integer(node); }
806
- catch (e) { throw _err("webauthn/bad-att-cert", "an android KeyDescription authorization value is not an INTEGER", e); }
807
- }
808
- // The union of the [1] purpose (SET OF INTEGER) values across both authorization
809
- // lists -- an attestation key MUST be usable for SIGN and nothing else.
810
- function _purposeUnion(a, b) {
811
- var out = [];
812
- [a, b].forEach(function (list) {
813
- var p = _alGet(list, 1);
814
- if (p && p.children) p.children.forEach(function (c) { var v = _alInt(c); if (out.indexOf(v) === -1) out.push(v); });
815
- });
816
- return out;
561
+ validator.keydesc.androidKeyDescription(cert, clientDataHash, _exts, WebauthnError, "webauthn/bad-att-cert", "webauthn/verify-failed");
817
562
  }
818
563
 
819
- // AIK-certificate requirements (WebAuthn 8.3.1): non-CA, an empty subject, the tcg
820
- // AIK-certificate EKU, and the tcg tpmManufacturer/tpmModel/tpmVersion subjectAltName
821
- // directory attributes.
822
- function _checkAikCert(cert) {
823
- _assertNotCa(cert);
824
- if ((cert.subject.rdns || []).length !== 0) throw _err("webauthn/bad-att-cert", "the tpm AIK certificate subject MUST be empty (WebAuthn 8.3.1)");
825
- var eku = _decodeExt(cert, "extKeyUsage");
826
- if (!eku || !Array.isArray(eku.value) || eku.value.indexOf(oid.byName("tcgKpAikCertificate")) === -1) {
827
- throw _err("webauthn/bad-att-cert", "the tpm AIK certificate lacks the tcg-kp-AIKCertificate extended key purpose (WebAuthn 8.3.1)");
828
- }
829
- var san = _decodeExt(cert, "subjectAltName");
830
- var dirName = san && san.value && san.value.names && san.value.names.filter(function (n) { return n.tagNumber === 4; })[0];
831
- var types = {};
832
- if (dirName && dirName.value && dirName.value.rdns) {
833
- dirName.value.rdns.forEach(function (rdn) { rdn.forEach(function (a) { types[a.type] = true; }); });
834
- }
835
- if (!types[oid.byName("tpmManufacturer")] || !types[oid.byName("tpmModel")] || !types[oid.byName("tpmVersion")]) {
836
- throw _err("webauthn/bad-att-cert", "the tpm AIK certificate subjectAltName lacks the required tcg attributes (WebAuthn 8.3.1)");
837
- }
838
- }
839
564
 
840
565
  /**
841
566
  * @primitive pki.webauthn.verify
package/lib/webcrypto.js CHANGED
@@ -658,7 +658,11 @@ function _algFromImport(name, alg, keyObject) {
658
658
  }
659
659
 
660
660
  function _curveFromKey(ko) {
661
- try { var jwk = ko.export({ format: "jwk" }); for (var k in CURVE_NODE) { if (jwk.crv === k) return k; } } catch (_e) { /* best-effort */ }
661
+ try {
662
+ var jwk = ko.export({ format: "jwk" });
663
+ for (var k in CURVE_NODE) { if (jwk.crv === k) return k; }
664
+ }
665
+ catch (_e) { /* best-effort */ }
662
666
  return undefined;
663
667
  }
664
668
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -66,6 +66,8 @@
66
66
  "fuzz": "npm ci --prefix fuzz && npx --prefix fuzz jazzer fuzz/asn1-der.fuzz.js -- -max_total_time=60",
67
67
  "gates": "node test/layer-0-primitives/codebase-patterns.test.js && node scripts/validate-source-comment-blocks.js && node scripts/check-api-snapshot.js",
68
68
  "coverage": "c8 --include=lib/** --include=index.js --reporter=text-summary --reporter=lcov node test/smoke.js",
69
+ "check:swallows": "node scripts/check-swallow-coverage.js",
70
+ "coverage:gated": "npm run coverage && npm run check:swallows",
69
71
  "prepack": "node scripts/check-pack-against-gitignore.js",
70
72
  "check:vendor-currency": "node scripts/check-vendor-currency.js"
71
73
  },