@blamejs/pki 0.8.55 → 0.8.56

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/lib/acme.js +2 -2
  3. package/lib/asn1-der.js +17 -5
  4. package/lib/attrcert-verify.js +1 -1
  5. package/lib/cmc-verify.js +1 -1
  6. package/lib/cmp-session.js +2 -2
  7. package/lib/cmp-verify.js +2 -2
  8. package/lib/cms-decrypt.js +4 -1
  9. package/lib/cms-encrypt.js +4 -1
  10. package/lib/cms-sign.js +26 -12
  11. package/lib/cms-verify.js +1 -1
  12. package/lib/composite-kem.js +30 -12
  13. package/lib/composite-sig.js +42 -23
  14. package/lib/constants.js +7 -2
  15. package/lib/crl-verify.js +1 -1
  16. package/lib/crmf-sign.js +72 -43
  17. package/lib/crmf-verify.js +1 -1
  18. package/lib/csr-verify.js +1 -1
  19. package/lib/ct.js +2 -2
  20. package/lib/edwards-point.js +1 -1
  21. package/lib/est.js +3 -3
  22. package/lib/guard-async.js +22 -1
  23. package/lib/guard-bytes.js +6 -3
  24. package/lib/guard-identifier.js +1 -1
  25. package/lib/guard-intrinsic.js +474 -0
  26. package/lib/http-digest.js +146 -62
  27. package/lib/http-transport.js +82 -17
  28. package/lib/identity-match.js +2 -2
  29. package/lib/merkle.js +2 -2
  30. package/lib/ocsp-request-verify.js +1 -1
  31. package/lib/ocsp.js +46 -22
  32. package/lib/path-validate.js +169 -76
  33. package/lib/pbes2.js +14 -7
  34. package/lib/schema-attrcert.js +2 -2
  35. package/lib/schema-cmc.js +2 -2
  36. package/lib/schema-cmp.js +2 -2
  37. package/lib/schema-cms.js +2 -2
  38. package/lib/schema-crmf.js +2 -2
  39. package/lib/schema-csr.js +2 -2
  40. package/lib/schema-pkix.js +3 -3
  41. package/lib/schema-x509.js +2 -2
  42. package/lib/sigstore.js +1 -1
  43. package/lib/tlog.js +7 -4
  44. package/lib/trust.js +187 -104
  45. package/lib/tsp-sign.js +1 -1
  46. package/lib/webauthn-mds.js +257 -121
  47. package/lib/webauthn.js +327 -164
  48. package/lib/webcrypto.js +10 -3
  49. package/package.json +1 -1
  50. package/sbom.cdx.json +6 -6
package/lib/webauthn.js CHANGED
@@ -38,6 +38,10 @@ var intrinsic = require("./guard-intrinsic");
38
38
  var _hasOwn = intrinsic.hasOwn;
39
39
  var _reverse = intrinsic.reverse;
40
40
  var _arrSlice = intrinsic.arraySlice;
41
+ /** @internal The split that cuts an android-safetynet JWS into its three segments decides which bytes
42
+ * are the signed input and which are the signature, so a replacement returning segments of its own
43
+ * choosing has the attestation verified over bytes the signer never covered. */
44
+ var _strSplit = intrinsic.uncurry(String.prototype.split);
41
45
  /** @internal `resolve` and `reject` build their promise from the RECEIVER, so reading the global
42
46
  * binding at the call hands the construction to whatever that binding holds. Two shapes here decide a
43
47
  * verdict: the seeds the attestation trust-path reductions fold over, where a replacement settling
@@ -67,32 +71,57 @@ var subtle = webcrypto.webcrypto.subtle;
67
71
  /** @internal The verify operation, captured at load. One answering true accepts an assertion or an
68
72
  * attestation statement that the credential's key never signed, which is the whole verdict. */
69
73
  var _subtleVerify = intrinsic.uncurry(subtle.verify);
70
-
71
- function _sha(name, buf) { return nodeCrypto.createHash(name).update(buf).digest(); }
74
+ /** @internal The import is captured with it. It produces the key the verify above runs under, so a
75
+ * replacement answering with a key of its own accepts a signature made by whoever holds that key
76
+ * while the bytes handed in were never the credential's. */
77
+ var _subtleImportKey = intrinsic.uncurry(subtle.importKey);
78
+
79
+ /** @internal The hash constructor is captured, and so are the methods of the object it returns. Every
80
+ * attestation comparison in this module rests on a digest, so a replaced `createHash`, or a replaced
81
+ * `update` or `digest` on the object it hands back, chooses what the comparison sees. */
82
+ var _createHash = nodeCrypto.createHash;
83
+ function _sha(name, buf) {
84
+ var h = _createHash(name);
85
+ return _hashDigest(_hashUpdate(h, buf));
86
+ }
87
+ var _hashUpdate = intrinsic.uncurry(nodeCrypto.createHash("sha256").update);
88
+ var _hashDigest = intrinsic.uncurry(nodeCrypto.createHash("sha256").digest);
72
89
  /** @internal The comparison goes through the captured operation. This is what decides that an
73
90
  * attestation statement's key is the credential's key, so a replacement answering zero makes a
74
91
  * different key match. */
75
92
  function _ucmp(a, b) {
76
- function strip(x) { var i = 0; while (i < x.length - 1 && x[i] === 0) i++; return x.subarray(i); }
93
+ function strip(x) { var i = 0; while (i < x.length - 1 && x[i] === 0) i++; return intrinsic.byteSlice(x, i); }
77
94
  return intrinsic.compare(strip(a), strip(b)) === 0;
78
95
  }
79
96
  function _isInteger(node) { return !!node && !node.constructed && node.tagClass === "universal" && node.tagNumber === asn1.TAGS.INTEGER; }
80
97
  var COSE_ALG_HASH = intrinsic.assign(intrinsic.create(null), { "-7": "sha256", "-9": "sha256", "-257": "sha256", "-37": "sha256", "-35": "sha384", "-51": "sha384", "-258": "sha384", "-38": "sha384", "-36": "sha512", "-52": "sha512", "-259": "sha512", "-39": "sha512", "-65535": "sha1" });
98
+ /** @internal The conversion that builds the registry KEY is captured. It selects which hash a
99
+ * signature is checked under, so a replacement answering with another algorithm's key picks another
100
+ * hash for the same declared algorithm. */
81
101
  function _coseAlgHash(alg, E) {
82
- var h = COSE_ALG_HASH[String(alg)];
102
+ var h = COSE_ALG_HASH[intrinsic.String(alg)];
83
103
  if (!h) throw E("webauthn/unsupported-algorithm", "no hash mapping for COSE algorithm " + alg);
84
104
  return h;
85
105
  }
86
106
 
87
107
 
88
108
  var AAGUID_LEN = 16;
109
+ /** @internal Every slice this takes of authenticatorData goes through the capture that consults no
110
+ * construction protocol, along with the classification that admits the buffer in the first place.
111
+ * The slices ARE the fields the ceremony is checked against: the RP ID hash, the AAGUID, the
112
+ * credential id and the bytes the COSE key is read from, so a replaced slice answers the checks
113
+ * below about bytes the authenticator never sent. Capturing `subarray` is not enough on its own,
114
+ * because it runs TypedArraySpeciesCreate and so builds its result through a constructor read off
115
+ * the receiver: one answering with the expected RP ID hash for a 32-byte slice made an assertion
116
+ * produced for another relying party satisfy `expectedRpId`, while the signature was still verified
117
+ * over the original bytes. */
89
118
  function _parseAuthData(buf, E) {
90
- if (!Buffer.isBuffer(buf) || buf.length < 37) throw E("webauthn/bad-auth-data", "authenticatorData is shorter than the 37-byte minimum (RFC WebAuthn sec. 6.1)");
119
+ if (!intrinsic.isBuffer(buf) || buf.length < 37) throw E("webauthn/bad-auth-data", "authenticatorData is shorter than the 37-byte minimum (RFC WebAuthn sec. 6.1)");
91
120
  var flags = buf[32];
92
121
  var out = {
93
- rpIdHash: buf.subarray(0, 32),
122
+ rpIdHash: intrinsic.byteSlice(buf, 0, 32),
94
123
  flags: { up: !!(flags & 0x01), uv: !!(flags & 0x04), be: !!(flags & 0x08), bs: !!(flags & 0x10), at: !!(flags & 0x40), ed: !!(flags & 0x80) },
95
- signCount: buf.readUInt32BE(33),
124
+ signCount: intrinsic.readUInt32BE(buf, 33),
96
125
  aaguid: null, credentialId: null, credentialPublicKey: null, credentialPublicKeyBytes: null, extensions: null,
97
126
  };
98
127
  if (out.flags.bs && !out.flags.be) throw E("webauthn/bad-auth-data", "authenticatorData sets Backup State (BS) without Backup Eligibility (BE) (WebAuthn sec. 6.1)");
@@ -100,24 +129,24 @@ function _parseAuthData(buf, E) {
100
129
  var off = 37;
101
130
  if (out.flags.at) {
102
131
  if (buf.length < off + AAGUID_LEN + 2) throw E("webauthn/bad-auth-data", "attestedCredentialData is truncated before the credentialId length");
103
- out.aaguid = buf.subarray(off, off + AAGUID_LEN); off += AAGUID_LEN;
104
- var credLen = buf.readUInt16BE(off); off += 2;
132
+ out.aaguid = intrinsic.byteSlice(buf, off, off + AAGUID_LEN); off += AAGUID_LEN;
133
+ var credLen = intrinsic.readUInt16BE(buf, off); off += 2;
105
134
  if (credLen < 1 || credLen > 1023) throw E("webauthn/bad-credential-id", "credentialIdLength " + credLen + " is outside 1..1023 (RFC WebAuthn sec. 6.1)");
106
135
  if (buf.length < off + credLen) throw E("webauthn/bad-auth-data", "credentialId overruns authenticatorData");
107
- out.credentialId = buf.subarray(off, off + credLen); off += credLen;
136
+ out.credentialId = intrinsic.byteSlice(buf, off, off + credLen); off += credLen;
108
137
  var keyNode;
109
- try { keyNode = cbor.decode(buf.subarray(off), { allowTrailing: true }); }
138
+ try { keyNode = cbor.decode(intrinsic.byteSlice(buf, off), { allowTrailing: true }); }
110
139
  catch (e) { throw E("webauthn/bad-cose-key", "the credential public key is not well-formed CBOR", e); }
111
- out.credentialPublicKeyBytes = buf.subarray(off, off + keyNode.bytes.length);
140
+ out.credentialPublicKeyBytes = intrinsic.byteSlice(buf, off, off + keyNode.bytes.length);
112
141
  out.credentialPublicKey = _decodeCoseKey(keyNode);
113
142
  off += keyNode.bytes.length;
114
143
  }
115
144
  if (out.flags.ed) {
116
145
  var extNode;
117
- try { extNode = cbor.decode(buf.subarray(off)); }
146
+ try { extNode = cbor.decode(intrinsic.byteSlice(buf, off)); }
118
147
  catch (e) { throw E("webauthn/bad-auth-data", "the authenticatorData extensions are not a single well-formed CBOR map", e); }
119
148
  if (extNode.majorType !== 5) throw E("webauthn/bad-auth-data", "the authenticatorData extensions must be a CBOR map");
120
- out.extensions = buf.subarray(off);
149
+ out.extensions = intrinsic.byteSlice(buf, off);
121
150
  } else if (off < buf.length) {
122
151
  throw E("webauthn/bad-auth-data", "authenticatorData has trailing bytes after attestedCredentialData with the ED flag clear");
123
152
  }
@@ -193,16 +222,25 @@ var COSE_ALG = intrinsic.assign(intrinsic.create(null), {
193
222
 
194
223
  function _derEcdsaToRaw(der, curve) { return validator.sig.ecdsaDerToP1363(der, curve, WebauthnError, "webauthn/bad-signature"); }
195
224
 
225
+ /** @internal Same registry-key conversion, and this one selects the import and the verify routine a
226
+ * signature is checked with, which is the binding between a declared algorithm and the operation
227
+ * applied to it. */
196
228
  function _verifySig(alg, sig, spkiBytes, message, E) {
197
- var d = COSE_ALG[String(alg)];
229
+ var d = COSE_ALG[intrinsic.String(alg)];
198
230
  if (!d) throw _err("webauthn/unsupported-algorithm", "unsupported COSE algorithm " + alg);
199
231
  var imp = d.imp, ver = d.verify;
200
232
  if (alg === -8) { var nm = _edName(spkiBytes, E); imp = { name: nm }; ver = { name: nm }; }
201
233
  if (imp.name === "Ed25519" || imp.name === "Ed448") _requireValidEdPoint(spkiBytes, imp.name);
202
234
  var s = d.ecdsa ? _derEcdsaToRaw(sig, d.imp.namedCurve) : sig;
203
- return subtle.importKey("spki", spkiBytes, imp, false, ["verify"])
204
- .then(function (key) { return _subtleVerify(subtle, ver, key, s, message); })
205
- .catch(function (e) { throw _err("webauthn/verify-error", "the attestation signature could not be evaluated", e); });
235
+ /** @internal Every continuation on this route is taken through the captured `then`. `then` is read
236
+ * off the promise at the call, so a replacement settles the chain without running the callback:
237
+ * settling here hands back a value in place of the signature check, and each caller below reads
238
+ * that value as the verdict. The whole attestation route is continued this way for that reason. */
239
+ var imported = _subtleImportKey(subtle, "spki", spkiBytes, imp, false, ["verify"]);
240
+ var checked = intrinsic.chain(imported, function (key) { return _subtleVerify(subtle, ver, key, s, message); });
241
+ return intrinsic.chain(checked, undefined, function (e) {
242
+ throw _err("webauthn/verify-error", "the attestation signature could not be evaluated", e);
243
+ });
206
244
  }
207
245
  var ED_OID_NAME = intrinsic.create(null);
208
246
  ED_OID_NAME[oid.byName("Ed25519")] = "Ed25519";
@@ -223,7 +261,7 @@ function _coseKeyToSpki(key) { return validator.cose.toSpki(key, WebauthnError,
223
261
 
224
262
  function _certEcCurveOid(cert, E) {
225
263
  var params = cert.subjectPublicKeyInfo && cert.subjectPublicKeyInfo.algorithm && cert.subjectPublicKeyInfo.algorithm.parameters;
226
- if (!Buffer.isBuffer(params)) throw E("webauthn/key-mismatch", "the attestation certificate EC key carries no named-curve parameters");
264
+ if (!intrinsic.isBuffer(params)) throw E("webauthn/key-mismatch", "the attestation certificate EC key carries no named-curve parameters");
227
265
  try { return asn1.read.oid(asn1.decode(params)); }
228
266
  catch (e) { throw E("webauthn/key-mismatch", "the attestation certificate EC curve is not a valid OBJECT IDENTIFIER", e); }
229
267
  }
@@ -239,8 +277,8 @@ function _certKeyAlgNames(cose) {
239
277
  function _assertCertKeyAlgorithm(cert, cose, E) {
240
278
  var alg = (cert.subjectPublicKeyInfo && cert.subjectPublicKeyInfo.algorithm) || {};
241
279
  var want = _certKeyAlgNames(cose);
242
- if (!want.length || want.indexOf(alg.name) < 0) {
243
- throw E("webauthn/key-mismatch", "the attestation certificate carries a " + JSON.stringify(alg.name) +
280
+ if (!want.length || intrinsic.indexOf(want, alg.name) < 0) {
281
+ throw E("webauthn/key-mismatch", "the attestation certificate carries a " + intrinsic.stringify(alg.name) +
244
282
  " key, which is not the kind of key the credential public key declares");
245
283
  }
246
284
  }
@@ -254,7 +292,7 @@ function _certPubKeyEqualsCose(cert, cose, E) {
254
292
  if (_certEcCurveOid(cert, E) !== oid.byName(wantCurve)) throw E("webauthn/key-mismatch", "the attestation certificate EC curve does not equal the credential key curve");
255
293
  if (raw.length < 1 || raw[0] !== 0x04) throw E("webauthn/key-mismatch", "the attestation certificate key is not an uncompressed EC point");
256
294
  var coordLen = (raw.length - 1) >> 1;
257
- var cx = raw.subarray(1, 1 + coordLen), cy = raw.subarray(1 + coordLen);
295
+ var cx = intrinsic.byteSlice(raw, 1, 1 + coordLen), cy = intrinsic.byteSlice(raw, 1 + coordLen);
258
296
  if (!cose.x || !cose.y || !_ucmp(cx, cose.x) || !_ucmp(cy, cose.y)) throw E("webauthn/key-mismatch", "the attestation certificate EC key does not equal the credential public key");
259
297
  return;
260
298
  }
@@ -269,7 +307,7 @@ function _certPubKeyEqualsCose(cert, cose, E) {
269
307
  return;
270
308
  }
271
309
  if (cose.kty === 1) {
272
- if (!cose.x || !Buffer.from(raw).equals(cose.x)) throw E("webauthn/key-mismatch", "the attestation certificate OKP key does not equal the credential public key");
310
+ if (!cose.x || !intrinsic.bufferEquals(intrinsic.bufferFrom(raw), cose.x)) throw E("webauthn/key-mismatch", "the attestation certificate OKP key does not equal the credential public key");
273
311
  return;
274
312
  }
275
313
  throw E("webauthn/key-mismatch", "unsupported credential key type for the certificate comparison");
@@ -279,9 +317,24 @@ function _parseTpmPubArea(buf) { return validator.tpm.parsePubArea(buf, Webauthn
279
317
  function _parseTpmCertInfo(buf) { return validator.tpm.parseCertInfo(buf, WebauthnError, "webauthn/bad-tpm"); }
280
318
  function _tpmPubKeyEqualsCose(pub, cose) { validator.tpm.pubKeyEqualsCose(pub, cose, WebauthnError, "webauthn/key-mismatch", "webauthn/bad-tpm"); }
281
319
 
320
+ /** @internal The selection that FINDS an extension is captured. Every check that reads an extension
321
+ * value reads whatever this returns, the android-safetynet hostname requirement among them, so a
322
+ * replacement answering with another entry hands the check the wrong value to compare. */
323
+ /** @internal An index loop rather than a selection, because a captured `filter` still runs
324
+ * ArraySpeciesCreate and builds its result through a constructor read off the receiver: one
325
+ * returning `{ length: 0 }` takes the match as property `0` while the length stays zero, so the
326
+ * extension reads as ABSENT. Absent is not inert here, since the hostname check falls back from the
327
+ * subject alternative name to the common name, and a leaf whose authoritative name is another host
328
+ * passes on a matching CN. The loop also avoids indexing an empty result, where index `0` reads
329
+ * through to `Array.prototype[0]` and a value installed there answers for an extension the
330
+ * certificate does not carry. */
282
331
  function _findExt(cert, oidName) {
283
332
  var target = oid.byName(oidName);
284
- return (cert.extensions || []).filter(function (e) { return e.oid === target; })[0] || null;
333
+ var exts = cert.extensions || [];
334
+ for (var i = 0; i < exts.length; i++) {
335
+ if (exts[i] && exts[i].oid === target) return exts[i];
336
+ }
337
+ return null;
285
338
  }
286
339
 
287
340
 
@@ -317,7 +370,7 @@ function parseAttestationObject(bytes) {
317
370
  var wantMajor = ATT_STMT_MAJOR[cbor.read.textString(fmtN)];
318
371
  if (wantMajor === undefined) wantMajor = 5;
319
372
  if (attStmtN.majorType !== wantMajor) {
320
- throw _err("webauthn/bad-attestation-object", "attestation object 'attStmt' must be a CBOR " + (wantMajor === 4 ? "array" : "map") + " for format " + JSON.stringify(cbor.read.textString(fmtN)));
373
+ throw _err("webauthn/bad-attestation-object", "attestation object 'attStmt' must be a CBOR " + (wantMajor === 4 ? "array" : "map") + " for format " + intrinsic.stringify(cbor.read.textString(fmtN)));
321
374
  }
322
375
  if (authDataN.majorType !== 2) throw _err("webauthn/bad-attestation-object", "attestation object 'authData' must be a byte string");
323
376
  var authDataBytes = cbor.read.byteString(authDataN);
@@ -340,23 +393,31 @@ function _attRead(map, key, reader, what) {
340
393
  try { return reader(n); }
341
394
  catch (e) { throw _err("webauthn/bad-att-stmt", "the attestation statement '" + key + "' must be " + what, e); }
342
395
  }
343
- function _algOf(attStmt) { return Number(_attRead(attStmt, "alg", cbor.read.int, "an integer")); }
396
+ /** @internal The numeric conversion comes from the capture. This value is the algorithm the
397
+ * attestation DECLARES, and it is compared against the credential key's own algorithm, so a
398
+ * conversion that answered with a different identifier would let one algorithm's signature be
399
+ * checked as another's. */
400
+ function _algOf(attStmt) { return intrinsic.Number(_attRead(attStmt, "alg", cbor.read.int, "an integer")); }
344
401
  function _sigOf(attStmt) { return _attRead(attStmt, "sig", cbor.read.byteString, "a byte string"); }
345
402
  function _requireAttShape(attStmt, allowed, required) {
346
403
  var have = intrinsic.create(null);
347
- cbor.read.map(attStmt).forEach(function (kv) {
404
+ /** @internal The walks and the membership test are captured. These are REFUSALS: the first rejects
405
+ * a field the format does not allow and the second a required field that is absent, so a walk that
406
+ * skips an entry or a membership test answering differently admits a statement neither rule would
407
+ * have passed. */
408
+ intrinsic.forEach(cbor.read.map(attStmt), function (kv) {
348
409
  if (kv[0].majorType !== 3) throw _err("webauthn/bad-att-stmt", "the attestation statement has a non-text-string field key");
349
410
  have[cbor.read.textString(kv[0])] = true;
350
411
  });
351
- Object.keys(have).forEach(function (k) { if (allowed.indexOf(k) === -1) throw _err("webauthn/bad-att-stmt", "the attestation statement carries an unexpected field '" + k + "'"); });
352
- required.forEach(function (k) { if (!have[k]) throw _err("webauthn/bad-att-stmt", "the attestation statement is missing the '" + k + "' field"); });
412
+ intrinsic.forEach(intrinsic.keys(have), function (k) { if (intrinsic.indexOf(allowed, k) === -1) throw _err("webauthn/bad-att-stmt", "the attestation statement carries an unexpected field '" + k + "'"); });
413
+ intrinsic.forEach(required, function (k) { if (!have[k]) throw _err("webauthn/bad-att-stmt", "the attestation statement is missing the '" + k + "' field"); });
353
414
  }
354
415
  var ATT_STMT_MAJOR = intrinsic.assign(intrinsic.create(null), { compound: 4 });
355
416
 
356
- var _AAGUID_SIGNED_BY_FMT = Object.assign(Object.create(null), { "fido-u2f": false });
417
+ var _AAGUID_SIGNED_BY_FMT = intrinsic.assign(intrinsic.create(null), { "fido-u2f": false });
357
418
  function _aaguidIsSigned(fmt) { return _AAGUID_SIGNED_BY_FMT[fmt] !== false; }
358
419
 
359
- var _VERIFY_OPTS = Object.assign(Object.create(null), {
420
+ var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), {
360
421
  time: 1, metadata: 1, tpmPolicy: 1, safetyNetRoots: 1, verifySafetyNetJws: 1, requireCtsProfileMatch: 1,
361
422
  expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
362
423
  rootCertificates: 1,
@@ -368,11 +429,14 @@ var _FORMAT_SCOPED_BOOLEAN_OPTS = ["verifySafetyNetJws", "requireCtsProfileMatch
368
429
  function _cloneParsed(v, depth) {
369
430
  if (depth > 64) throw _err("webauthn/bad-input", "opts.rootCertificates[] is nested too deeply to be a parsed certificate");
370
431
  if (guard.bytes.isByteSource(v)) return guard.bytes.snapshotSource(v, _err, "webauthn/bad-input", "a byte field of opts.rootCertificates[]");
371
- if (Array.isArray(v)) return guard.list.copyMap(v, function (x) { return _cloneParsed(x, depth + 1); });
372
- if (guard.time.isDate(v)) return new Date(guard.time.instantOf(v));
432
+ if (intrinsic.isArray(v)) return guard.list.copyMap(v, function (x) { return _cloneParsed(x, depth + 1); });
433
+ if (guard.time.isDate(v)) return new intrinsic.Date(guard.time.instantOf(v));
373
434
  if (v && typeof v === "object") {
374
435
  var out = intrinsic.create(null);
375
- Object.keys(v).forEach(function (k) { out[k] = _cloneParsed(v[k], depth + 1); });
436
+ /** @internal The key enumeration and the walk over it are captured: this is the copy that stops a
437
+ * caller's root certificate changing after it was validated, and an enumeration that omits a key
438
+ * leaves that field reading through to the caller's own object. */
439
+ intrinsic.forEach(intrinsic.keys(v), function (k) { out[k] = _cloneParsed(v[k], depth + 1); });
376
440
  return out;
377
441
  }
378
442
  return v;
@@ -386,9 +450,13 @@ function _bytesArg(v, label) {
386
450
  throw _err("webauthn/bad-input", label + " must be a BufferSource (a Buffer, a typed-array view, or an ArrayBuffer)");
387
451
  }
388
452
 
453
+ /** @internal The classification and the walk are captured. These produce the root set an attestation
454
+ * path is anchored to, so a walk answering with entries of its own supplies the anchors rather than
455
+ * copying the caller's, and a classification answering `false` hands the caller's live array through
456
+ * with no snapshot taken. */
389
457
  function _snapshotRoots(supplied) {
390
- if (!Array.isArray(supplied)) return supplied;
391
- return supplied.map(function (root) {
458
+ if (!intrinsic.isArray(supplied)) return supplied;
459
+ return intrinsic.mapList(supplied, function (root) {
392
460
  if (_isBufferSource(root)) {
393
461
  return guard.bytes.snapshotSource(root, WebauthnError, "webauthn/bad-input", "opts.rootCertificates[]");
394
462
  }
@@ -397,10 +465,10 @@ function _snapshotRoots(supplied) {
397
465
  }
398
466
 
399
467
  function _applyCallerRoots(res, supplied, vopts, onlyPaths) {
400
- if (!Array.isArray(supplied) || !supplied.length) {
468
+ if (!intrinsic.isArray(supplied) || !supplied.length) {
401
469
  throw _err("webauthn/bad-input", "opts.rootCertificates must be a non-empty array of root certificates (DER, PEM, or parsed)");
402
470
  }
403
- var roots = supplied.map(function (root, i) {
471
+ var roots = intrinsic.mapList(supplied, function (root, i) {
404
472
  var label = "opts.rootCertificates[" + i + "]";
405
473
  return guard.parsed.acceptDerived(root, "certificate", function (bytes) {
406
474
  try {
@@ -408,42 +476,46 @@ function _applyCallerRoots(res, supplied, vopts, onlyPaths) {
408
476
  } catch (e) { throw _err("webauthn/bad-input", label + " is not a decodable certificate", e); }
409
477
  }, _err, "webauthn/bad-input", label);
410
478
  });
411
- var paths = onlyPaths || ((res.fmt === "compound" && Array.isArray(res.compound))
412
- ? res.compound.filter(function (el) { return el.trustPath && el.trustPath.length; })
413
- .map(function (el) { return { tp: el.trustPath, at: el.chainValidatedAt }; })
479
+ /** @internal The selection of trust paths to anchor is captured. A selection that drops an element
480
+ * leaves that element's path unanchored while the refusal below sees a non-empty list, so the
481
+ * verdict reports the whole attestation anchored on the strength of the paths that survived. */
482
+ var paths = onlyPaths || ((res.fmt === "compound" && intrinsic.isArray(res.compound))
483
+ ? intrinsic.mapList(intrinsic.selectList(res.compound, function (el) { return el.trustPath && el.trustPath.length; }),
484
+ function (el) { return { tp: el.trustPath, at: el.chainValidatedAt }; })
414
485
  : (res.trustPath && res.trustPath.length ? [{ tp: res.trustPath, at: res.chainValidatedAt }] : []));
415
486
  if (!onlyPaths) {
416
- res = guard.verdict.of(res, { anchoredElements: { total: (res.fmt === "compound" && Array.isArray(res.compound)) ? res.compound.length : 1,
487
+ res = guard.verdict.of(res, { anchoredElements: { total: (res.fmt === "compound" && intrinsic.isArray(res.compound)) ? res.compound.length : 1,
417
488
  anchored: paths.length } });
418
489
  }
419
490
  if (!paths.length) {
420
491
  throw _err("webauthn/anchor-not-applicable",
421
492
  "opts.rootCertificates was supplied, but this attestation carries no trust path to anchor (format '" + res.fmt + "')");
422
493
  }
423
- var at = vopts.time !== undefined ? vopts.time : new Date();
424
- return paths.reduce(function (p, info) {
425
- return p.then(function () {
494
+ var at = vopts.time !== undefined ? vopts.time : new intrinsic.Date();
495
+ var anchoredAll = intrinsic.reduce(paths, function (p, info) {
496
+ return intrinsic.chain(p, function () {
426
497
  var pathAt = vopts.time !== undefined ? vopts.time : (info.at || at);
427
- return mds.chainToAnchor(_reverse(_arrSlice(info.tp)), roots, pathAt,
498
+ return mds.chainToAnchor(_reverse(intrinsic.copyList(info.tp)), roots, pathAt,
428
499
  "attestation trust path (against the roots supplied as opts.rootCertificates)");
429
500
  });
430
- }, _promiseResolve(_Promise, undefined)).then(function () {
501
+ }, _promiseResolve(_Promise, undefined));
502
+ return intrinsic.chain(anchoredAll, function () {
431
503
  return guard.verdict.of(res, { anchoredTo: _anchoredRoutes(res, "rootCertificates") });
432
504
  });
433
505
  }
434
506
 
435
507
  function _safetyNetAnchored(res) {
436
508
  if (res.fmt === "android-safetynet") return 1;
437
- if (res.fmt === "compound" && Array.isArray(res.compound)) {
438
- return res.compound.filter(function (el) { return el && el.fmt === "android-safetynet"; }).length;
509
+ if (res.fmt === "compound" && intrinsic.isArray(res.compound)) {
510
+ return intrinsic.selectList(res.compound, function (el) { return el && el.fmt === "android-safetynet"; }).length;
439
511
  }
440
512
  return 0;
441
513
  }
442
514
 
443
515
  function _anchoredRoutes(res, base) {
444
- var routes = base ? base.split("+") : [];
445
- if (_safetyNetAnchored(res) && routes.indexOf("safetyNetRoots") < 0) guard.list.append(routes, "safetyNetRoots");
446
- return routes.length ? routes.join("+") : null;
516
+ var routes = base ? intrinsic.splitChar(base, 43) : [];
517
+ if (_safetyNetAnchored(res) && intrinsic.indexOf(routes, "safetyNetRoots") < 0) guard.list.append(routes, "safetyNetRoots");
518
+ return routes.length ? intrinsic.join(routes, "+") : null;
447
519
  }
448
520
 
449
521
  function _assertBool(v, name) {
@@ -456,8 +528,11 @@ function _applyBindings(authData, coseKey, opts) {
456
528
  if (typeof opts.expectedRpId !== "string" || !opts.expectedRpId.length) {
457
529
  throw _err("webauthn/bad-input", "opts.expectedRpId must be a non-empty RP ID string");
458
530
  }
459
- if (!guard.crypto.constantTimeEqual(_sha("sha256", Buffer.from(opts.expectedRpId, "utf8")),
460
- Buffer.from(authData.rpIdHash))) {
531
+ /** @internal Both operands of the RP ID comparison are built with captured operations. This is
532
+ * what decides the response was produced for the relying party the caller named, so an operation
533
+ * answering with bytes of its own matches a response made for another party. */
534
+ if (!guard.crypto.constantTimeEqual(_sha("sha256", intrinsic.bufferFrom(opts.expectedRpId, "utf8")),
535
+ intrinsic.bufferFrom(authData.rpIdHash))) {
461
536
  throw _err("webauthn/rp-id-mismatch",
462
537
  "the authenticatorData rpIdHash is not SHA-256 of opts.expectedRpId, so this response was " +
463
538
  "produced for a different relying party (WebAuthn sec. 7.1 step 13 / sec. 7.2 step 15)");
@@ -482,7 +557,9 @@ function _applyBindings(authData, coseKey, opts) {
482
557
  }
483
558
  checked.userVerification = opts.requireUserVerification;
484
559
  }
485
- var alg65535Allowed = Array.isArray(opts.allowedAlgorithms) && opts.allowedAlgorithms.indexOf(-65535) !== -1;
560
+ /** @internal The SHA-1 opt-in is read through captures. This membership test ADMITS COSE -65535, so
561
+ * a test answering that -65535 is present waives the refusal for a caller who never named it. */
562
+ var alg65535Allowed = intrinsic.isArray(opts.allowedAlgorithms) && intrinsic.ownIndexOf(opts.allowedAlgorithms, -65535) !== -1;
486
563
  if (coseKey && coseKey.alg === -65535 && !alg65535Allowed) {
487
564
  throw _err("webauthn/algorithm-not-allowed",
488
565
  "the credential public key declares COSE algorithm -65535 (RSASSA-PKCS1-v1_5 with SHA-1); every " +
@@ -490,12 +567,17 @@ function _applyBindings(authData, coseKey, opts) {
490
567
  "names -65535 explicitly");
491
568
  }
492
569
  if (opts.allowedAlgorithms !== undefined) {
493
- if (!Array.isArray(opts.allowedAlgorithms) || !opts.allowedAlgorithms.length ||
494
- !opts.allowedAlgorithms.every(function (a) { return typeof a === "number" && Number.isSafeInteger(a); })) {
570
+ /** @internal The entries are required to be the list's OWN. A hole answers from the prototype at
571
+ * that index, and the universal test skips it, so a sparse list with a number installed on
572
+ * `Array.prototype` passes this door and then admits that algorithm below. */
573
+ if (!intrinsic.isArray(opts.allowedAlgorithms) || !opts.allowedAlgorithms.length ||
574
+ !intrinsic.everyOwn(opts.allowedAlgorithms, function (a) { return typeof a === "number" && intrinsic.isSafeInteger(a); })) {
495
575
  throw _err("webauthn/bad-input", "opts.allowedAlgorithms must be a non-empty array of COSE algorithm integers");
496
576
  }
497
577
  var alg = coseKey && coseKey.alg;
498
- if (opts.allowedAlgorithms.indexOf(alg) === -1) {
578
+ /** @internal The membership test comes from the capture. It REFUSES an algorithm the caller did
579
+ * not name, so one answering that any algorithm is present admits every algorithm. */
580
+ if (intrinsic.ownIndexOf(opts.allowedAlgorithms, alg) === -1) {
499
581
  throw _err("webauthn/algorithm-not-allowed",
500
582
  "the credential public key declares COSE algorithm " + alg + ", which is not in opts.allowedAlgorithms");
501
583
  }
@@ -513,7 +595,10 @@ function _readX5c(attStmt) {
513
595
  var x5cN = cbor.read.mapGet(attStmt, "x5c");
514
596
  if (!x5cN || x5cN.majorType !== 4 || !x5cN.children || !x5cN.children.length) throw _err("webauthn/bad-att-stmt", "x5c must be a non-empty array of certificates");
515
597
  _requireX5cCount(x5cN.children.length);
516
- return x5cN.children.map(function (c) {
598
+ /** @internal The walk over the x5c entries is captured. It produces the chain the attestation is
599
+ * verified against, so a walk answering with certificates of its own replaces the chain whose
600
+ * signatures are then checked. */
601
+ return intrinsic.mapList(x5cN.children, function (c) {
517
602
  var der;
518
603
  try { der = cbor.read.byteString(c); } catch (e) { throw _err("webauthn/bad-att-stmt", "an x5c entry must be a byte string", e); }
519
604
  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); }
@@ -530,11 +615,15 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
530
615
  var isX5c = !!cbor.read.mapGet(att.attStmt, "x5c");
531
616
  _requireAttShape(att.attStmt, isX5c ? ["alg", "sig", "x5c"] : ["alg", "sig"], isX5c ? ["alg", "sig", "x5c"] : ["alg", "sig"]);
532
617
  var alg = _algOf(att.attStmt), sig = _sigOf(att.attStmt);
533
- var message = Buffer.concat([att.authDataBytes, clientDataHash]);
618
+ /** @internal The signed input is assembled with the captured operation. Every verifier below does
619
+ * the same, because the assembly decides WHICH bytes the signature is checked over: an operation
620
+ * answering with bytes the attestation statement does carry a signature for verifies a statement
621
+ * that was never made about this ceremony's authenticatorData and client data. */
622
+ var message = intrinsic.bufferConcat([att.authDataBytes, clientDataHash]);
534
623
  if (isX5c) {
535
624
  var chain = _readX5c(att.attStmt), leaf = chain[0];
536
625
  _checkPackedCert(leaf);
537
- return _verifySig(alg, sig, leaf.subjectPublicKeyInfo.bytes, message, _err).then(function (ok) {
626
+ return intrinsic.chain(_verifySig(alg, sig, leaf.subjectPublicKeyInfo.bytes, message, _err), function (ok) {
538
627
  if (!ok) throw _err("webauthn/verify-failed", "the packed attestation signature does not verify under the x5c leaf key");
539
628
  _checkAaguidExt(leaf, att.authData.aaguid);
540
629
  return _result("packed", "Basic", chain, att);
@@ -542,7 +631,7 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
542
631
  }
543
632
  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)");
544
633
  var spki = _coseKeyToSpki(att.authData.credentialPublicKey);
545
- return _verifySig(alg, sig, spki, message, _err).then(function (ok) {
634
+ return intrinsic.chain(_verifySig(alg, sig, spki, message, _err), function (ok) {
546
635
  if (!ok) throw _err("webauthn/verify-failed", "the packed self-attestation signature does not verify under the credential key");
547
636
  return _result("packed", "Self", [], att);
548
637
  });
@@ -558,9 +647,9 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
558
647
  if (key.alg !== -7 || key.kty !== 2 || key.crv !== 1 || !key.x || !key.y || key.x.length !== 32 || key.y.length !== 32) {
559
648
  throw _err("webauthn/bad-att-stmt", "fido-u2f requires an ES256 (-7) EC2 P-256 credential public key (WebAuthn 8.6)");
560
649
  }
561
- var publicKeyU2F = Buffer.concat([Buffer.from([0x04]), key.x, key.y]);
562
- var verificationData = Buffer.concat([Buffer.from([0x00]), att.authData.rpIdHash, clientDataHash, att.authData.credentialId, publicKeyU2F]);
563
- return _verifySig(-7, sig, leaf.subjectPublicKeyInfo.bytes, verificationData, _err).then(function (ok) {
650
+ var publicKeyU2F = intrinsic.bufferConcat([intrinsic.bufferFrom([0x04]), key.x, key.y]);
651
+ var verificationData = intrinsic.bufferConcat([intrinsic.bufferFrom([0x00]), att.authData.rpIdHash, clientDataHash, att.authData.credentialId, publicKeyU2F]);
652
+ return intrinsic.chain(_verifySig(-7, sig, leaf.subjectPublicKeyInfo.bytes, verificationData, _err), function (ok) {
564
653
  if (!ok) throw _err("webauthn/verify-failed", "the fido-u2f attestation signature does not verify under the x5c leaf key");
565
654
  return _result("fido-u2f", "Basic", chain, att);
566
655
  });
@@ -570,11 +659,11 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
570
659
  _requireAttShape(att.attStmt, ["alg", "x5c"], ["x5c"]);
571
660
  var chain = _readX5c(att.attStmt), leaf = chain[0];
572
661
  _requireV3(leaf);
573
- var nonce = _sha("sha256", Buffer.concat([att.authDataBytes, clientDataHash]));
662
+ var nonce = _sha("sha256", intrinsic.bufferConcat([att.authDataBytes, clientDataHash]));
574
663
  var ext = _findExt(leaf, "appleAnonymousAttestation");
575
664
  if (!ext) throw _err("webauthn/bad-att-cert", "the apple attestation certificate is missing the anonymous-attestation extension (WebAuthn 8.8)");
576
665
  var embedded = _appleNonce(ext.value);
577
- if (!embedded.equals(nonce)) throw _err("webauthn/verify-failed", "the apple attestation nonce does not equal SHA-256(authData || clientDataHash)");
666
+ if (!intrinsic.bufferEquals(embedded, nonce)) throw _err("webauthn/verify-failed", "the apple attestation nonce does not equal SHA-256(authData || clientDataHash)");
578
667
  _certPubKeyEqualsCose(leaf, att.authData.credentialPublicKey, _err);
579
668
  return _promiseResolve(_Promise, _result("apple", "AnonCA", chain, att));
580
669
  },
@@ -584,8 +673,8 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
584
673
  var chain = _readX5c(att.attStmt), leaf = chain[0];
585
674
  _requireV3(leaf);
586
675
  var alg = _algOf(att.attStmt), sig = _sigOf(att.attStmt);
587
- var message = Buffer.concat([att.authDataBytes, clientDataHash]);
588
- return _verifySig(alg, sig, leaf.subjectPublicKeyInfo.bytes, message, _err).then(function (ok) {
676
+ var message = intrinsic.bufferConcat([att.authDataBytes, clientDataHash]);
677
+ return intrinsic.chain(_verifySig(alg, sig, leaf.subjectPublicKeyInfo.bytes, message, _err), function (ok) {
589
678
  if (!ok) throw _err("webauthn/verify-failed", "the android-key attestation signature does not verify under the x5c leaf key");
590
679
  _certPubKeyEqualsCose(leaf, att.authData.credentialPublicKey, _err);
591
680
  _checkAndroidKeyDescription(leaf, clientDataHash);
@@ -594,7 +683,7 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
594
683
  },
595
684
 
596
685
  tpm: function (att, clientDataHash, opts) {
597
- var tpmPolicy = validator.tpm.normalizeObjectAttributePolicy((opts || {}).tpmPolicy, WebauthnError, "webauthn/bad-input");
686
+ var tpmPolicy = validator.tpm.normalizeObjectAttributePolicy((opts || intrinsic.create(null)).tpmPolicy, WebauthnError, "webauthn/bad-input");
598
687
  _requireAttShape(att.attStmt, ["ver", "alg", "sig", "certInfo", "pubArea", "x5c"], ["ver", "alg", "sig", "certInfo", "pubArea", "x5c"]);
599
688
  var verN = cbor.read.mapGet(att.attStmt, "ver");
600
689
  if (!verN || verN.majorType !== 3 || cbor.read.textString(verN) !== "2.0") throw _err("webauthn/bad-att-stmt", "tpm attestation 'ver' MUST be \"2.0\" (WebAuthn 8.3)");
@@ -608,15 +697,18 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
608
697
 
609
698
  var certInfo = _parseTpmCertInfo(certInfoBytes);
610
699
 
611
- var attToBeSigned = Buffer.concat([att.authDataBytes, clientDataHash]);
612
- if (!certInfo.extraData.equals(_sha(_coseAlgHash(alg, _err), attToBeSigned))) throw _err("webauthn/verify-failed", "certInfo extraData does not equal the hash of authData || clientDataHash");
700
+ var attToBeSigned = intrinsic.bufferConcat([att.authDataBytes, clientDataHash]);
701
+ /** @internal Both TPM comparisons go through the capture. `extraData` is what binds the quote to
702
+ * this ceremony and the attested Name is what binds it to the pubArea key, so one answering true
703
+ * accepts a genuine TPM quote taken over another ceremony or over a different key. */
704
+ if (!intrinsic.bufferEquals(certInfo.extraData, _sha(_coseAlgHash(alg, _err), attToBeSigned))) throw _err("webauthn/verify-failed", "certInfo extraData does not equal the hash of authData || clientDataHash");
613
705
 
614
706
  var nameHash = validator.tpm.TPM_ALG_HASH[pub.nameAlg];
615
- if (!nameHash) throw _err("webauthn/bad-tpm", "unsupported TPM nameAlg 0x" + pub.nameAlg.toString(16));
616
- var computedName = Buffer.concat([pub.nameAlgBytes, _sha(nameHash, pubAreaBytes)]);
617
- if (!certInfo.attestedName.equals(computedName)) throw _err("webauthn/verify-failed", "certInfo attested Name does not match the pubArea TPM Name");
707
+ if (!nameHash) throw _err("webauthn/bad-tpm", "unsupported TPM nameAlg 0x" + intrinsic.numberToString(pub.nameAlg, 16));
708
+ var computedName = intrinsic.bufferConcat([pub.nameAlgBytes, _sha(nameHash, pubAreaBytes)]);
709
+ if (!intrinsic.bufferEquals(certInfo.attestedName, computedName)) throw _err("webauthn/verify-failed", "certInfo attested Name does not match the pubArea TPM Name");
618
710
 
619
- return _verifySig(alg, sig, aik.subjectPublicKeyInfo.bytes, certInfoBytes, _err).then(function (ok) {
711
+ return intrinsic.chain(_verifySig(alg, sig, aik.subjectPublicKeyInfo.bytes, certInfoBytes, _err), function (ok) {
620
712
  if (!ok) throw _err("webauthn/verify-failed", "the tpm attestation signature does not verify over certInfo under the AIK");
621
713
  _checkAikCert(aik);
622
714
  _checkAaguidExt(aik, att.authData.aaguid);
@@ -627,12 +719,12 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
627
719
  },
628
720
 
629
721
  "android-safetynet": function (att, clientDataHash, opts) {
630
- opts = opts || {};
722
+ opts = opts || intrinsic.create(null);
631
723
  if (opts.verifySafetyNetJws !== true) {
632
724
  throw _err("webauthn/unsupported-format", "attestation statement format 'android-safetynet' is not supported");
633
725
  }
634
726
  var roots = opts.safetyNetRoots;
635
- if (!Array.isArray(roots) || roots.length === 0) {
727
+ if (!intrinsic.isArray(roots) || roots.length === 0) {
636
728
  throw _err("webauthn/safetynet-no-root", "verifying an android-safetynet attestation requires opts.safetyNetRoots -- the Google root(s) to anchor the x5c chain to; this library bundles none (WebAuthn 8.5)");
637
729
  }
638
730
  if (opts.time !== undefined) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
@@ -642,24 +734,26 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
642
734
  var responseBytes = _attRead(att.attStmt, "response", cbor.read.byteString, "a byte string");
643
735
  if (!responseBytes.length) throw _err("webauthn/bad-att-stmt", "the android-safetynet response is empty");
644
736
 
645
- var segs = guard.text.decode(responseBytes, constants.LIMITS.SAFETYNET_JWS_MAX_BYTES, WebauthnError, "webauthn/bad-att-stmt", "the android-safetynet response").split(".");
737
+ /** @internal The split that divides the JWS is captured: it decides which bytes are the signed
738
+ * input and which are the signature, so a replacement chooses what the verification covers. */
739
+ var segs = intrinsic.splitChar(guard.text.decode(responseBytes, constants.LIMITS.SAFETYNET_JWS_MAX_BYTES, WebauthnError, "webauthn/bad-att-stmt", "the android-safetynet response"), 46);
646
740
  if (segs.length !== 3) throw _err("webauthn/bad-att-stmt", "the android-safetynet response is not a three-part JWS compact serialization (RFC 7515 sec. 3.1)");
647
741
  var header, payload, sigBytes;
648
742
  try {
649
743
  header = jose.parseJson(jose.base64url.decode(segs[0]));
650
744
  payload = jose.parseJson(jose.base64url.decode(segs[1]));
651
- sigBytes = Buffer.from(jose.base64url.decode(segs[2]));
745
+ sigBytes = intrinsic.bufferFrom(jose.base64url.decode(segs[2]));
652
746
  } catch (e) { throw _err("webauthn/bad-att-stmt", "the android-safetynet response is not a decodable JWS", e); }
653
747
  if (!_isPlainObject(header) || !_isPlainObject(payload)) {
654
748
  throw _err("webauthn/bad-att-stmt", "the android-safetynet JWS header and payload must each be a JSON object");
655
749
  }
656
750
 
657
- if (header.alg !== "RS256") throw _err("webauthn/unsupported-algorithm", "the android-safetynet JWS alg must be RS256, got " + JSON.stringify(header.alg));
658
- if (!Array.isArray(header.x5c) || header.x5c.length === 0) {
751
+ if (header.alg !== "RS256") throw _err("webauthn/unsupported-algorithm", "the android-safetynet JWS alg must be RS256, got " + intrinsic.stringify(header.alg));
752
+ if (!intrinsic.isArray(header.x5c) || header.x5c.length === 0) {
659
753
  throw _err("webauthn/bad-att-stmt", "the android-safetynet JWS header carries no x5c certificate chain (RFC 7515 sec. 4.1.6)");
660
754
  }
661
755
  _requireX5cCount(header.x5c.length);
662
- var chain = header.x5c.map(function (entry, i) {
756
+ var chain = intrinsic.mapList(header.x5c, function (entry, i) {
663
757
  if (typeof entry !== "string") throw _err("webauthn/bad-att-stmt", "the android-safetynet x5c entry " + i + " is not a string");
664
758
  var der;
665
759
  try { der = guard.encoding.base64(entry, constants.LIMITS.SAFETYNET_CERT_MAX_BYTES, _err, "webauthn/bad-att-stmt", "an android-safetynet x5c entry"); }
@@ -669,8 +763,13 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
669
763
  });
670
764
  var leaf = chain[0];
671
765
 
672
- var wantNonce = _sha("sha256", Buffer.concat([att.authDataBytes, clientDataHash])).toString("base64");
673
- if (typeof payload.nonce !== "string" || !guard.crypto.constantTimeEqual(Buffer.from(payload.nonce, "utf8"), Buffer.from(wantNonce, "utf8"))) {
766
+ /** @internal The digest is taken with the captured hash, and the encoding that turns it back into
767
+ * text is captured too, along with the construction of both comparison operands. The nonce is what
768
+ * binds this response to this ceremony's authenticatorData and clientDataHash, so an operation that
769
+ * chooses either operand makes a correctly signed response replay against different data. */
770
+ var wantNonce = intrinsic.bufToString(_sha("sha256", intrinsic.bufferConcat([att.authDataBytes, clientDataHash])), "base64");
771
+ if (typeof payload.nonce !== "string" || !guard.crypto.constantTimeEqual(
772
+ intrinsic.bufferFrom(payload.nonce, "utf8"), intrinsic.bufferFrom(wantNonce, "utf8"))) {
674
773
  throw _err("webauthn/safetynet-nonce-mismatch", "the android-safetynet nonce does not bind this authenticatorData and clientDataHash (WebAuthn 8.5)");
675
774
  }
676
775
 
@@ -685,17 +784,18 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
685
784
  apkCertificateDigestSha256: payload.apkCertificateDigestSha256, advice: payload.advice,
686
785
  };
687
786
  if (opts.requireCtsProfileMatch === true && signals.ctsProfileMatch !== true) {
688
- throw _err("webauthn/safetynet-cts-profile", "the android-safetynet response reports ctsProfileMatch " + JSON.stringify(signals.ctsProfileMatch) + ", and opts.requireCtsProfileMatch demands true");
787
+ throw _err("webauthn/safetynet-cts-profile", "the android-safetynet response reports ctsProfileMatch " + intrinsic.stringify(signals.ctsProfileMatch) + ", and opts.requireCtsProfileMatch demands true");
689
788
  }
690
789
 
691
- return _verifySig(-257, sigBytes, leaf.subjectPublicKeyInfo.bytes,
692
- Buffer.from(segs[0] + "." + segs[1], "ascii"), _err).then(function (ok) {
790
+ var jwsTrusted = intrinsic.chain(_verifySig(-257, sigBytes, leaf.subjectPublicKeyInfo.bytes,
791
+ intrinsic.bufferFrom(segs[0] + "." + segs[1], "ascii"), _err), function (ok) {
693
792
  if (!ok) throw _err("webauthn/verify-failed", "the android-safetynet JWS signature does not verify under the x5c leaf key");
694
793
  chainAt = opts.time !== undefined ? opts.time
695
- : (typeof payload.timestampMs === "number" && isFinite(payload.timestampMs) && payload.timestampMs > 0
696
- ? new Date(payload.timestampMs) : undefined);
794
+ : (typeof payload.timestampMs === "number" && intrinsic.isFinite(payload.timestampMs) && payload.timestampMs > 0
795
+ ? new intrinsic.Date(payload.timestampMs) : undefined);
697
796
  return _safetyNetChainTrusted(chain, roots, chainAt);
698
- }).then(function () {
797
+ });
798
+ return intrinsic.chain(jwsTrusted, function () {
699
799
  var res = guard.verdict.of(_result("android-safetynet", "Basic", chain, att), { safetyNet: signals });
700
800
  if (chainAt !== undefined) res = guard.verdict.of(res, { chainValidatedAt: chainAt });
701
801
  return res;
@@ -708,7 +808,11 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
708
808
  if (kids.length > constants.LIMITS.WEBAUTHN_COMPOUND_MAX_STATEMENTS) {
709
809
  throw _err("webauthn/bad-att-stmt", "a compound attestation statement carries " + kids.length + " nested statements, above the " + constants.LIMITS.WEBAUTHN_COMPOUND_MAX_STATEMENTS + " this toolkit will verify");
710
810
  }
711
- var elements = kids.map(function (el, i) {
811
+ /** @internal The walk that builds the element list and the fold that verifies them are both
812
+ * captured. A walk that drops an element leaves it unverified while the compound verdict still
813
+ * reports, and a fold that returns its seed without visiting an element answers for a statement
814
+ * nothing checked. */
815
+ var elements = intrinsic.mapList(kids, function (el, i) {
712
816
  if (!el || el.majorType !== 5) throw _err("webauthn/bad-att-stmt", "compound element " + i + " must be a CBOR map { fmt, attStmt } (WebAuthn 8.9)");
713
817
  var fN = cbor.read.mapGet(el, "fmt"), sN = cbor.read.mapGet(el, "attStmt");
714
818
  if (el.children.length !== 2 || !fN || !sN) throw _err("webauthn/bad-att-stmt", "compound element " + i + " must be exactly { fmt, attStmt } (WebAuthn 8.9)");
@@ -722,14 +826,16 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
722
826
  return { fmt: f, index: i, att: { fmt: f, attStmt: sN, authData: att.authData, authDataBytes: att.authDataBytes } };
723
827
  });
724
828
  var out = [];
725
- return elements.reduce(function (p, e) {
726
- return p.then(function () {
727
- return _promiseResolve(_Promise, undefined).then(function () { return VERIFIERS[e.fmt](e.att, clientDataHash, opts); })
728
- .then(function (r) { guard.list.append(out, r); }, function (err) {
729
- throw _err("webauthn/compound-element-failed", "compound element " + e.index + " (format '" + e.fmt + "') did not verify", err);
730
- });
829
+ var allVerified = intrinsic.reduce(elements, function (p, e) {
830
+ return intrinsic.chain(p, function () {
831
+ var ran = intrinsic.chain(_promiseResolve(_Promise, undefined),
832
+ function () { return VERIFIERS[e.fmt](e.att, clientDataHash, opts); });
833
+ return intrinsic.chain(ran, function (r) { guard.list.append(out, r); }, function (err) {
834
+ throw _err("webauthn/compound-element-failed", "compound element " + e.index + " (format '" + e.fmt + "') did not verify", err);
835
+ });
731
836
  });
732
- }, _promiseResolve(_Promise, undefined)).then(function () {
837
+ }, _promiseResolve(_Promise, undefined));
838
+ return intrinsic.chain(allVerified, function () {
733
839
  return guard.verdict.of(_result("compound", "Compound", [], att), { compound: out });
734
840
  });
735
841
  },
@@ -742,17 +848,40 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
742
848
  },
743
849
  });
744
850
 
745
- function _isPlainObject(v) { return !!v && typeof v === "object" && !Array.isArray(v); }
851
+ function _isPlainObject(v) { return !!v && typeof v === "object" && !intrinsic.isArray(v); }
746
852
 
853
+ /** @internal The case fold comes from the load-time capture, not the live prototype. This is an
854
+ * ADMISSION gate: it answers `true` when the leaf is issued to the one name WebAuthn 8.5 requires, so
855
+ * a replacement that answers with that name admits ANY leaf. Measured, with the fold replaced for the
856
+ * leaf's own name alone, `pki.webauthn.verify` ACCEPTED an android-safetynet attestation whose x5c
857
+ * leaf names `other.example`. Replacing the fold for every string instead breaks an earlier step and
858
+ * hides it, which is why the vector narrows the replacement to one value. */
747
859
  function _safetyNetHostnameOk(leaf) {
748
860
  var want = "attest.android.com";
749
861
  var san = _decodeExt(leaf, "subjectAltName");
750
- var entries = san && Array.isArray(san.value) ? san.value : [];
751
- var dns = entries.filter(function (gn) { return gn && gn.type === "dNSName" && typeof gn.value === "string"; });
752
- if (dns.length) return dns.some(function (gn) { return gn.value.toLowerCase() === want; });
753
- return leaf.subject.rdns.some(function (rdn) {
754
- return rdn.some(function (atv) {
755
- return atv.name === "commonName" && typeof atv.value === "string" && atv.value.toLowerCase() === want;
862
+ /** @internal The entries come from the decoded record's `names`, and a dNSName is GeneralName
863
+ * `[2]`, which is how `path-validate` and `identity-match` read the same structure. Read as a bare
864
+ * array under a `type` field, this branch selected nothing at all: the decoder returns
865
+ * `{ names, bytes }` and each entry carries `tagNumber`, so every leaf fell through to the
866
+ * commonName fallback. That accepted a leaf whose authoritative SAN named another host while its
867
+ * commonName named the wanted one, and refused a SAN-only leaf whose commonName differed. */
868
+ var entries = (san && san.value && intrinsic.isArray(san.value.names)) ? san.value.names : [];
869
+ /** @internal The WALKS are captured as well as the fold. The fold alone is not the gate: a replaced
870
+ * `some` answering `true` passes the requirement whatever the names compare as, and a selection
871
+ * that drops every dNSName entry sends this down the commonName fallback instead. Both reach the
872
+ * same `true` this function exists to withhold. */
873
+ var dns = intrinsic.selectList(entries, function (gn) { return gn && gn.tagNumber === 2 && typeof gn.value === "string"; });
874
+ /** @internal A subjectAltName that is PRESENT is authoritative, whichever name forms it carries,
875
+ * so the commonName is consulted only for a leaf that carries no such extension at all. Gating the
876
+ * fallback on "no dNSName was selected" instead let a leaf whose SAN holds only a URI or an
877
+ * address fall through to its commonName, and a rooted leaf naming another host in the one place
878
+ * that binds it was accepted on a commonName that read `attest.android.com`. A leaf with no SAN is
879
+ * still read by commonName, which is how the service's own certificates were issued. */
880
+ if (san) return intrinsic.some(dns, function (gn) { return intrinsic.toLowerCase(gn.value) === want; });
881
+ return intrinsic.some(leaf.subject.rdns, function (rdn) {
882
+ return intrinsic.some(rdn, function (atv) {
883
+ return atv.name === "commonName" && typeof atv.value === "string" &&
884
+ intrinsic.toLowerCase(atv.value) === want;
756
885
  });
757
886
  });
758
887
  }
@@ -760,7 +889,7 @@ function _safetyNetHostnameOk(leaf) {
760
889
  function _safetyNetChainTrusted(chain, roots, time) {
761
890
  var anchors;
762
891
  try {
763
- anchors = roots.map(function (root, i) {
892
+ anchors = intrinsic.mapList(roots, function (root, i) {
764
893
  var label = "opts.safetyNetRoots[" + i + "]";
765
894
  return guard.parsed.acceptDerived(root, "certificate", function (bytes) {
766
895
  try {
@@ -769,14 +898,14 @@ function _safetyNetChainTrusted(chain, roots, time) {
769
898
  }, _err, "webauthn/bad-input", label);
770
899
  });
771
900
  } catch (e) { return _promiseReject(_Promise, e); }
772
- return mds.chainToAnchor(chain, anchors, time === undefined ? new Date() : time,
901
+ return mds.chainToAnchor(chain, anchors, time === undefined ? new intrinsic.Date() : time,
773
902
  "android-safetynet x5c certificate chain", "webauthn/safetynet-cert-untrusted");
774
903
  }
775
904
 
776
905
  function _result(fmt, attestationType, chain, att) {
777
906
  return guard.verdict.of({
778
907
  valid: true, attestationVerified: true, fmt: fmt, attestationType: attestationType,
779
- trustPath: _reverse(_arrSlice(chain || [])),
908
+ trustPath: _reverse(intrinsic.copyList(chain || [])),
780
909
  aaguid: att.authData.aaguid,
781
910
  credentialId: att.authData.credentialId,
782
911
  credentialPublicKey: att.authData.credentialPublicKey,
@@ -920,13 +1049,18 @@ function verify(attestationObject, clientDataHash, opts) {
920
1049
  !_isBufferSource(clientDataHash) && _isPlainObject(clientDataHash)) {
921
1050
  opts = clientDataHash; clientDataHash = undefined;
922
1051
  }
923
- opts = opts || {};
1052
+ /** @internal A caller who passes no options gets a record with NO prototype. Defaulted to `{}` it
1053
+ * inherits, so a property installed on `Object.prototype` reads as an option this call never set. */
1054
+ opts = opts || intrinsic.create(null);
924
1055
  try {
925
1056
  if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
926
1057
  guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "webauthn/bad-input", "opts has an unknown key ");
927
1058
  opts = guard.identifier.ownOptions(opts);
928
1059
  if (opts.time !== undefined) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
929
- _FORMAT_SCOPED_BOOLEAN_OPTS.forEach(function (k) {
1060
+ /** @internal The walk comes from the capture. Each pass of it is a REFUSAL of a mistyped option,
1061
+ * and a mistyped one that survives is read for truthiness later where the rule wants exactly
1062
+ * `true`, so a walk that visits nothing admits `"true"` as the opt-in. */
1063
+ intrinsic.forEach(_FORMAT_SCOPED_BOOLEAN_OPTS, function (k) {
930
1064
  if (opts[k] !== undefined && typeof opts[k] !== "boolean") throw _err("webauthn/bad-input", "opts." + k + " must be a boolean");
931
1065
  });
932
1066
  } catch (e) { return _promiseReject(_Promise, e); }
@@ -979,27 +1113,30 @@ function verify(attestationObject, clientDataHash, opts) {
979
1113
  vopts.rootCertificates = _snapshotRoots(opts.rootCertificates);
980
1114
  vopts.safetyNetRoots = _snapshotRoots(opts.safetyNetRoots);
981
1115
  } catch (e) { return _promiseReject(_Promise, e); }
982
- return _promiseResolve(_Promise, undefined).then(function () { return verifier(att, clientDataHash, vopts); })
983
- .then(function (res) {
984
- return guard.verdict.of(res, { clientData: clientData });
985
- })
986
- .then(function (res) {
1116
+ var ranVerifier = intrinsic.chain(_promiseResolve(_Promise, undefined),
1117
+ function () { return verifier(att, clientDataHash, vopts); });
1118
+ var withClientData = intrinsic.chain(ranVerifier, function (res) {
1119
+ return guard.verdict.of(res, { clientData: clientData });
1120
+ });
1121
+ var anchoredRes = intrinsic.chain(withClientData, function (res) {
987
1122
  if (vopts.metadata === undefined) {
988
1123
  if (vopts.rootCertificates !== undefined) return _applyCallerRoots(res, vopts.rootCertificates, vopts);
989
1124
  var anchored = guard.verdict.of(res, { anchoredTo: _anchoredRoutes(res, null) });
990
1125
  var sn = _safetyNetAnchored(anchored);
991
1126
  if (sn) {
992
1127
  anchored = guard.verdict.of(anchored, { anchoredElements: {
993
- total: (anchored.fmt === "compound" && Array.isArray(anchored.compound)) ? anchored.compound.length : 1,
1128
+ total: (anchored.fmt === "compound" && intrinsic.isArray(anchored.compound)) ? anchored.compound.length : 1,
994
1129
  anchored: sn } });
995
1130
  }
996
1131
  return anchored;
997
1132
  }
998
- return _promiseResolve(_Promise, undefined).then(function () { return _applyMetadata(res, att, vopts); })
999
- .then(function (out) { return guard.verdict.of(out, { anchoredTo: _anchoredRoutes(out, "metadata") }); }, function (e) {
1133
+ var applyMd = intrinsic.chain(_promiseResolve(_Promise, undefined),
1134
+ function () { return _applyMetadata(res, att, vopts); });
1135
+ return intrinsic.chain(applyMd,
1136
+ function (out) { return guard.verdict.of(out, { anchoredTo: _anchoredRoutes(out, "metadata") }); }, function (e) {
1000
1137
  if (!e || e.code !== "webauthn/metadata-not-found" || vopts.rootCertificates === undefined) throw e;
1001
- return _promiseResolve(_Promise, _applyCallerRoots(res, vopts.rootCertificates, vopts, e.missedPaths))
1002
- .then(function (withRoots) {
1138
+ return intrinsic.chain(_promiseResolve(_Promise, _applyCallerRoots(res, vopts.rootCertificates, vopts, e.missedPaths)),
1139
+ function (withRoots) {
1003
1140
  /** @internal The element coverage the metadata attempt measured is the whole
1004
1141
  * statement's, so it travels on the error rather than being recomputed from the
1005
1142
  * fallback's own paths. */
@@ -1009,13 +1146,13 @@ function verify(attestationObject, clientDataHash, opts) {
1009
1146
  var primary = applied[0];
1010
1147
  var withMd = guard.verdict.of(out, { metadata: { aaguid: primary.entry.aaguid,
1011
1148
  keyIdentifiers: primary.entry.keyIdentifiers || [],
1012
- entry: primary.entry, entries: applied.map(function (a) { return a.entry; }),
1149
+ entry: primary.entry, entries: intrinsic.mapList(applied, function (a) { return a.entry; }),
1013
1150
  anchors: primary.anchors.length } });
1014
1151
  return guard.verdict.of(withMd, { anchoredTo: _anchoredRoutes(withMd, "metadata+rootCertificates") });
1015
1152
  });
1016
1153
  });
1017
- })
1018
- .then(function (res) { return guard.verdict.of(res, { bindingChecked: bindingChecked }); });
1154
+ });
1155
+ return intrinsic.chain(anchoredRes, function (res) { return guard.verdict.of(res, { bindingChecked: bindingChecked }); });
1019
1156
  }
1020
1157
 
1021
1158
  function _applyMetadata(res, att, opts) {
@@ -1023,12 +1160,12 @@ function _applyMetadata(res, att, opts) {
1023
1160
  if (!mds.isVerifiedResult(md)) {
1024
1161
  throw _err("webauthn/bad-input", "opts.metadata must be a pki.webauthn.verifyMetadataBlob result, not a raw BLOB");
1025
1162
  }
1026
- var paths = (res.fmt === "compound" && Array.isArray(res.compound))
1027
- ? res.compound.filter(function (el) { return el.trustPath && el.trustPath.length; })
1028
- .map(function (el) { return { tp: el.trustPath, fmt: el.fmt, at: el.chainValidatedAt }; })
1163
+ var paths = (res.fmt === "compound" && intrinsic.isArray(res.compound))
1164
+ ? intrinsic.mapList(intrinsic.selectList(res.compound, function (el) { return el.trustPath && el.trustPath.length; }),
1165
+ function (el) { return { tp: el.trustPath, fmt: el.fmt, at: el.chainValidatedAt }; })
1029
1166
  : (res.trustPath && res.trustPath.length ? [{ tp: res.trustPath, fmt: res.fmt, at: res.chainValidatedAt }] : []);
1030
1167
  res = guard.verdict.of(res, { anchoredElements: {
1031
- total: (res.fmt === "compound" && Array.isArray(res.compound)) ? res.compound.length : 1,
1168
+ total: (res.fmt === "compound" && intrinsic.isArray(res.compound)) ? res.compound.length : 1,
1032
1169
  anchored: paths.length } });
1033
1170
  if (!paths.length) {
1034
1171
  throw _err("webauthn/metadata-not-applicable", "opts.metadata was supplied, but this attestation carries no trust path to anchor (format '" + res.fmt + "')");
@@ -1056,7 +1193,7 @@ function _applyMetadata(res, att, opts) {
1056
1193
  guard.list.append(applied, { entry: entry, anchors: anchors, identifier: identifier });
1057
1194
  return { anchors: anchors, identifier: identifier };
1058
1195
  }
1059
- var at = opts.time !== undefined ? opts.time : new Date();
1196
+ var at = opts.time !== undefined ? opts.time : new intrinsic.Date();
1060
1197
  mds.assertFresh(md, at, "the metadata supplied as opts.metadata");
1061
1198
  var governed = [];
1062
1199
  var missed = null;
@@ -1074,20 +1211,21 @@ function _applyMetadata(res, att, opts) {
1074
1211
  guard.verdict.set(missed, "appliedEntries", applied);
1075
1212
  guard.verdict.set(missed, "anchoredElements", res.anchoredElements);
1076
1213
  }
1077
- return governed.reduce(function (p, item) {
1078
- return p.then(function () {
1214
+ var governedAnchored = intrinsic.reduce(governed, function (p, item) {
1215
+ return intrinsic.chain(p, function () {
1079
1216
  var g = item.g, info = item.info;
1080
1217
  var pathAt = opts.time !== undefined ? opts.time : (info.at || at);
1081
- return mds.chainToAnchor(_reverse(_arrSlice(info.tp)), g.anchors, pathAt,
1218
+ return mds.chainToAnchor(_reverse(intrinsic.copyList(info.tp)), g.anchors, pathAt,
1082
1219
  "attestation trust path for " + g.identifier + " (against the roots its metadata entry registers)");
1083
1220
  });
1084
- }, _promiseResolve(_Promise, undefined))
1085
- .then(function () {
1221
+ }, _promiseResolve(_Promise, undefined));
1222
+ return intrinsic.chain(governedAnchored,
1223
+ function () {
1086
1224
  if (missed) throw missed;
1087
1225
  var primary = applied[0];
1088
1226
  return guard.verdict.of(res, { metadata: { aaguid: primary.entry.aaguid,
1089
1227
  keyIdentifiers: primary.entry.keyIdentifiers || [],
1090
- entry: primary.entry, entries: applied.map(function (a) { return a.entry; }),
1228
+ entry: primary.entry, entries: intrinsic.mapList(applied, function (a) { return a.entry; }),
1091
1229
  anchors: primary.anchors.length } });
1092
1230
  });
1093
1231
  }
@@ -1095,7 +1233,11 @@ function _applyMetadata(res, att, opts) {
1095
1233
  function _formatCarries(att, fmt) {
1096
1234
  if (att.fmt === fmt) return true;
1097
1235
  if (att.fmt !== "compound") return false;
1098
- return (att.attStmt.children || []).some(function (el) {
1236
+ /** @internal The membership walk is captured. It decides whether a format-scoped option applies to
1237
+ * a compound statement, so one answering `false` for a format the statement does carry leaves that
1238
+ * element's option unapplied, and one answering `true` applies an option to a statement that has
1239
+ * no element of that format. */
1240
+ return intrinsic.some(att.attStmt.children || [], function (el) {
1099
1241
  if (!el || el.majorType !== 5) return false;
1100
1242
  var fN = cbor.read.mapGet(el, "fmt");
1101
1243
  return !!fN && fN.majorType === 3 && cbor.read.textString(fN) === fmt;
@@ -1234,7 +1376,7 @@ void constants;
1234
1376
  */
1235
1377
 
1236
1378
 
1237
- var CLIENT_DATA_TYPE = Object.assign(Object.create(null), { "webauthn.create": 1, "webauthn.get": 1 });
1379
+ var CLIENT_DATA_TYPE = intrinsic.assign(intrinsic.create(null), { "webauthn.create": 1, "webauthn.get": 1 });
1238
1380
 
1239
1381
  /**
1240
1382
  * @primitive pki.webauthn.parseClientData
@@ -1289,7 +1431,7 @@ var CLIENT_DATA_TYPE = Object.assign(Object.create(null), { "webauthn.create": 1
1289
1431
  * cd.crossOrigin; // false
1290
1432
  */
1291
1433
  function parseClientData(bytes, opts) {
1292
- opts = opts || {};
1434
+ opts = opts || intrinsic.create(null);
1293
1435
  if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
1294
1436
  guard.identifier.assertKnownKeys(opts, _CLIENT_DATA_OPTS, _err, "webauthn/bad-input", "opts has an unknown key ");
1295
1437
  /** @internal Each expected* option is read to decide whether to compare and read again for the
@@ -1298,7 +1440,7 @@ function parseClientData(bytes, opts) {
1298
1440
  * the caller never supplied. Take each option once. */
1299
1441
  opts = guard.identifier.snapshotOptions(opts, _CLIENT_DATA_OPTS);
1300
1442
  bytes = _bytesArg(bytes, "clientDataJSON");
1301
- var doc = guard.json.parse(Buffer.from(bytes), _err, {
1443
+ var doc = guard.json.parse(intrinsic.bufferFrom(bytes), _err, {
1302
1444
  maxBytes: constants.LIMITS.JSON_MAX_BYTES, maxDepth: constants.LIMITS.JSON_MAX_DEPTH,
1303
1445
  badJson: "webauthn/bad-client-data", tooDeep: "webauthn/bad-client-data",
1304
1446
  duplicateMember: "webauthn/bad-client-data", tooLarge: "webauthn/bad-client-data",
@@ -1307,7 +1449,7 @@ function parseClientData(bytes, opts) {
1307
1449
  if (!_isPlainObject(doc)) throw _err("webauthn/bad-client-data", "clientDataJSON is not a JSON object (WebAuthn sec. 5.8.1)");
1308
1450
  if (typeof doc.type !== "string" || CLIENT_DATA_TYPE[doc.type] !== 1) {
1309
1451
  throw _err("webauthn/bad-client-data",
1310
- "clientDataJSON type must be \"webauthn.create\" or \"webauthn.get\", got " + JSON.stringify(doc.type) +
1452
+ "clientDataJSON type must be \"webauthn.create\" or \"webauthn.get\", got " + intrinsic.stringify(doc.type) +
1311
1453
  " (WebAuthn sec. 5.8.1)");
1312
1454
  }
1313
1455
  if (typeof doc.origin !== "string" || !doc.origin.length) {
@@ -1351,13 +1493,23 @@ function parseClientData(bytes, opts) {
1351
1493
  checked.challenge = true;
1352
1494
  }
1353
1495
  if (opts.expectedOrigin !== undefined) {
1354
- var allowed = Array.isArray(opts.expectedOrigin) ? opts.expectedOrigin : [opts.expectedOrigin];
1355
- if (!allowed.length || !allowed.every(function (o) { return typeof o === "string" && o.length; })) {
1496
+ /** @internal The origin allow-list is read through captures end to end. The membership test is
1497
+ * what REFUSES a response produced at an origin this relying party does not accept, so one
1498
+ * answering that any origin is present admits a ceremony completed on another site, and the
1499
+ * classification above it decides whether the caller's single origin or their whole list is the
1500
+ * set that gets tested. */
1501
+ var allowed = intrinsic.isArray(opts.expectedOrigin) ? opts.expectedOrigin : [opts.expectedOrigin];
1502
+ /** @internal The entries are required to be the list's OWN, and the membership test reads own
1503
+ * indexes. A hole answers from the prototype at that index while the universal test skips it, so
1504
+ * a one-element sparse list carrying no origin of its own passed this door and then admitted a
1505
+ * response whose origin had been installed on `Array.prototype`, with the origin reported as
1506
+ * checked although the caller listed none. */
1507
+ if (!allowed.length || !intrinsic.everyOwn(allowed, function (o) { return typeof o === "string" && o.length; })) {
1356
1508
  throw _err("webauthn/bad-input", "opts.expectedOrigin must be a non-empty origin string, or an array of them");
1357
1509
  }
1358
- if (allowed.indexOf(doc.origin) === -1) {
1510
+ if (intrinsic.ownIndexOf(allowed, doc.origin) === -1) {
1359
1511
  throw _err("webauthn/client-data-mismatch",
1360
- "the clientDataJSON origin " + JSON.stringify(doc.origin) + " is not one this relying party accepts " +
1512
+ "the clientDataJSON origin " + intrinsic.stringify(doc.origin) + " is not one this relying party accepts " +
1361
1513
  "(WebAuthn sec. 7.1 step 10 / sec. 7.2 step 13)");
1362
1514
  }
1363
1515
  checked.origin = true;
@@ -1368,19 +1520,19 @@ function parseClientData(bytes, opts) {
1368
1520
  if (wantTop === null) {
1369
1521
  if (framed || doc.topOrigin !== undefined) {
1370
1522
  throw _err("webauthn/client-data-mismatch",
1371
- "the clientDataJSON describes a cross-origin ceremony (crossOrigin " + JSON.stringify(doc.crossOrigin) +
1372
- ", topOrigin " + JSON.stringify(doc.topOrigin === undefined ? null : doc.topOrigin) +
1523
+ "the clientDataJSON describes a cross-origin ceremony (crossOrigin " + intrinsic.stringify(doc.crossOrigin) +
1524
+ ", topOrigin " + intrinsic.stringify(doc.topOrigin === undefined ? null : doc.topOrigin) +
1373
1525
  "), and this relying party requires an unframed one (WebAuthn sec. 5.8.1)");
1374
1526
  }
1375
1527
  } else {
1376
- var allowedTop = Array.isArray(wantTop) ? wantTop : [wantTop];
1377
- if (!allowedTop.length || !allowedTop.every(function (o) { return typeof o === "string" && o.length; })) {
1528
+ var allowedTop = intrinsic.isArray(wantTop) ? wantTop : [wantTop];
1529
+ if (!allowedTop.length || !intrinsic.everyOwn(allowedTop, function (o) { return typeof o === "string" && o.length; })) {
1378
1530
  throw _err("webauthn/bad-input", "opts.expectedTopOrigin must be null, a non-empty origin string, or an array of them");
1379
1531
  }
1380
- if (!framed || allowedTop.indexOf(doc.topOrigin) === -1) {
1532
+ if (!framed || intrinsic.ownIndexOf(allowedTop, doc.topOrigin) === -1) {
1381
1533
  throw _err("webauthn/client-data-mismatch",
1382
- "the clientDataJSON topOrigin " + JSON.stringify(doc.topOrigin === undefined ? null : doc.topOrigin) +
1383
- " (crossOrigin " + JSON.stringify(doc.crossOrigin) + ") is not a framing this relying party accepts (WebAuthn sec. 5.8.1)");
1534
+ "the clientDataJSON topOrigin " + intrinsic.stringify(doc.topOrigin === undefined ? null : doc.topOrigin) +
1535
+ " (crossOrigin " + intrinsic.stringify(doc.crossOrigin) + ") is not a framing this relying party accepts (WebAuthn sec. 5.8.1)");
1384
1536
  }
1385
1537
  }
1386
1538
  checked.topOrigin = true;
@@ -1392,7 +1544,7 @@ function parseClientData(bytes, opts) {
1392
1544
  checked: checked,
1393
1545
  };
1394
1546
  }
1395
- var _CLIENT_DATA_OPTS = Object.assign(Object.create(null), {
1547
+ var _CLIENT_DATA_OPTS = intrinsic.assign(intrinsic.create(null), {
1396
1548
  expectedType: 1, expectedChallenge: 1, expectedOrigin: 1, expectedTopOrigin: 1,
1397
1549
  });
1398
1550
 
@@ -1426,7 +1578,7 @@ function parseAuthenticatorData(bytes) {
1426
1578
  return _parseAuthData(_bytesArg(bytes, "authenticatorData"), _err);
1427
1579
  }
1428
1580
 
1429
- var _ASSERT_OPTS = Object.assign(Object.create(null), {
1581
+ var _ASSERT_OPTS = intrinsic.assign(intrinsic.create(null), {
1430
1582
  authenticatorData: 1, clientDataHash: 1, clientDataJSON: 1, signature: 1,
1431
1583
  credentialPublicKey: 1, previousSignCount: 1,
1432
1584
  expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
@@ -1496,7 +1648,11 @@ function _snapshotAssertion(input) {
1496
1648
  if (!_isPlainObject(input)) throw _err("webauthn/bad-input", "pki.webauthn.verifyAssertion takes an options object");
1497
1649
  guard.identifier.assertKnownKeys(input, _ASSERT_OPTS, _err, "webauthn/bad-input", "verifyAssertion input has an unknown key ");
1498
1650
  var out = guard.identifier.ownOptions(input);
1499
- ["authenticatorData", "clientDataJSON", "clientDataHash", "signature", "expectedChallenge"].forEach(function (f) {
1651
+ /** @internal The walk comes from the capture. Each pass of it SNAPSHOTS one of the byte fields the
1652
+ * verdict is computed over, so a walk that visits nothing leaves the caller's own buffers in place
1653
+ * and they can be rewritten between this call returning its promise and the verification running:
1654
+ * substituted authenticatorData with a signature that matches it then verifies. */
1655
+ intrinsic.forEach(["authenticatorData", "clientDataJSON", "clientDataHash", "signature", "expectedChallenge"], function (f) {
1500
1656
  if (_isBufferSource(out[f])) out[f] = guard.bytes.snapshotSource(out[f], WebauthnError, "webauthn/bad-input", f);
1501
1657
  });
1502
1658
  if (_isBufferSource(out.credentialPublicKey)) {
@@ -1514,16 +1670,19 @@ function _snapshotAssertion(input) {
1514
1670
  }
1515
1671
  out.credentialPublicKey = key;
1516
1672
  }
1517
- if (Array.isArray(out.allowedAlgorithms)) out.allowedAlgorithms = guard.list.snapshot(out.allowedAlgorithms);
1518
- if (Array.isArray(out.expectedOrigin)) out.expectedOrigin = guard.list.snapshot(out.expectedOrigin);
1519
- if (Array.isArray(out.expectedTopOrigin)) out.expectedTopOrigin = guard.list.snapshot(out.expectedTopOrigin);
1673
+ /** @internal Each classification decides whether the list is snapshotted. One answering `false`
1674
+ * leaves the caller's own array in place, so the allow-list the checks run against can still change
1675
+ * after this point. */
1676
+ if (intrinsic.isArray(out.allowedAlgorithms)) out.allowedAlgorithms = guard.list.snapshot(out.allowedAlgorithms);
1677
+ if (intrinsic.isArray(out.expectedOrigin)) out.expectedOrigin = guard.list.snapshot(out.expectedOrigin);
1678
+ if (intrinsic.isArray(out.expectedTopOrigin)) out.expectedTopOrigin = guard.list.snapshot(out.expectedTopOrigin);
1520
1679
  return out;
1521
1680
  }
1522
1681
 
1523
1682
  function verifyAssertion(input) {
1524
1683
  var frozen;
1525
1684
  try { frozen = _snapshotAssertion(input); } catch (e) { return _promiseReject(_Promise, e); }
1526
- return _promiseResolve(_Promise, undefined).then(function () {
1685
+ return intrinsic.chain(_promiseResolve(_Promise, undefined), function () {
1527
1686
  input = frozen;
1528
1687
  var authData = parseAuthenticatorData(input.authenticatorData);
1529
1688
  input.signature = _bytesArg(input.signature, "signature");
@@ -1540,7 +1699,7 @@ function verifyAssertion(input) {
1540
1699
  expectedOrigin: input.expectedOrigin,
1541
1700
  expectedTopOrigin: input.expectedTopOrigin,
1542
1701
  });
1543
- clientDataHash = _sha("sha256", Buffer.from(input.clientDataJSON));
1702
+ clientDataHash = _sha("sha256", intrinsic.bufferFrom(input.clientDataJSON));
1544
1703
  } else {
1545
1704
  if (input.expectedChallenge !== undefined || input.expectedOrigin !== undefined ||
1546
1705
  input.expectedTopOrigin !== undefined) {
@@ -1561,12 +1720,14 @@ function verifyAssertion(input) {
1561
1720
  }
1562
1721
  var bindingChecked = _applyBindings(authData, coseKey, input);
1563
1722
  var prev = input.previousSignCount;
1564
- if (prev !== undefined && (typeof prev !== "number" || !Number.isSafeInteger(prev) || prev < 0 || prev > 0xFFFFFFFF)) {
1723
+ if (prev !== undefined && (typeof prev !== "number" || !intrinsic.isSafeInteger(prev) || prev < 0 || prev > 0xFFFFFFFF)) {
1565
1724
  throw _err("webauthn/bad-input", "previousSignCount must be an integer in 0..4294967295");
1566
1725
  }
1567
- var message = Buffer.concat([Buffer.from(input.authenticatorData), clientDataHash]);
1726
+ /** @internal The signed input and the signature are both assembled with captures, which is what
1727
+ * decides the bytes this assertion is checked over and the bytes it is checked against. */
1728
+ var message = intrinsic.bufferConcat([intrinsic.bufferFrom(input.authenticatorData), clientDataHash]);
1568
1729
  var spki = _coseKeyToSpki(coseKey);
1569
- return _verifySig(coseKey.alg, Buffer.from(input.signature), spki, message, _err).then(function (ok) {
1730
+ return intrinsic.chain(_verifySig(coseKey.alg, intrinsic.bufferFrom(input.signature), spki, message, _err), function (ok) {
1570
1731
  if (!ok) {
1571
1732
  throw _err("webauthn/bad-signature",
1572
1733
  "the assertion signature does not verify under the stored credential public key (WebAuthn sec. 7.2 step 20)");
@@ -1599,7 +1760,9 @@ function verifyAssertion(input) {
1599
1760
  });
1600
1761
  }
1601
1762
 
1602
- module.exports = {
1763
+ /** @internal Frozen, so a consumer that reads one of these off this object at the call cannot be
1764
+ * handed a replacement in place of it. */
1765
+ module.exports = intrinsic.freeze({
1603
1766
  parseAttestationObject: parseAttestationObject,
1604
1767
  parseAuthenticatorData: parseAuthenticatorData,
1605
1768
  parseClientData: parseClientData,
@@ -1609,4 +1772,4 @@ module.exports = {
1609
1772
  verifyMetadataBlob: mds.verifyMetadataBlob,
1610
1773
  metadataFor: mds.metadataFor,
1611
1774
  metadataAnchors: mds.metadataAnchors,
1612
- };
1775
+ });