@blamejs/pki 0.4.14 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/MIGRATING.md +2 -2
  3. package/README.md +142 -137
  4. package/index.js +4 -0
  5. package/lib/acme.js +73 -1
  6. package/lib/asn1-der.js +2 -0
  7. package/lib/attrcert-sign.js +4 -0
  8. package/lib/cbor-det.js +32 -16
  9. package/lib/cmc-build.js +880 -0
  10. package/lib/cmc-verify.js +657 -0
  11. package/lib/cmp-build.js +8 -7
  12. package/lib/cmp-verify.js +11 -1
  13. package/lib/cms-sign.js +170 -8
  14. package/lib/cms-verify.js +80 -14
  15. package/lib/crl-sign.js +22 -0
  16. package/lib/crmf-sign.js +5 -2
  17. package/lib/csr-sign.js +3 -0
  18. package/lib/ct.js +72 -0
  19. package/lib/est.js +828 -32
  20. package/lib/framework-error.js +13 -0
  21. package/lib/guard-bytes.js +37 -1
  22. package/lib/guard-range.js +23 -1
  23. package/lib/http-transport.js +9 -3
  24. package/lib/inspect.js +28 -5
  25. package/lib/jose.js +15 -0
  26. package/lib/lint.js +4 -0
  27. package/lib/merkle.js +5 -5
  28. package/lib/ocsp.js +139 -11
  29. package/lib/oid.js +69 -1
  30. package/lib/path-validate.js +438 -100
  31. package/lib/pkcs12-build.js +12 -0
  32. package/lib/schema-all.js +19 -1
  33. package/lib/schema-attrcert.js +27 -0
  34. package/lib/schema-c509.js +6 -0
  35. package/lib/schema-cmc.js +791 -0
  36. package/lib/schema-cmp.js +25 -0
  37. package/lib/schema-cms.js +17 -1
  38. package/lib/schema-crl.js +23 -1
  39. package/lib/schema-crmf.js +13 -0
  40. package/lib/schema-csr.js +11 -0
  41. package/lib/schema-csrattrs.js +6 -0
  42. package/lib/schema-engine.js +6 -2
  43. package/lib/schema-ocsp.js +41 -0
  44. package/lib/schema-pkcs12.js +16 -0
  45. package/lib/schema-pkcs8.js +8 -0
  46. package/lib/schema-pkix.js +6 -0
  47. package/lib/schema-smime.js +4 -4
  48. package/lib/schema-tsp.js +32 -1
  49. package/lib/schema-x509.js +14 -1
  50. package/lib/shbs.js +12 -4
  51. package/lib/sigstore.js +4 -0
  52. package/lib/smime.js +28 -7
  53. package/lib/tls-cert-compress.js +15 -3
  54. package/lib/trust.js +27 -4
  55. package/lib/tsp-sign.js +41 -6
  56. package/lib/vendor/README.md +19 -19
  57. package/lib/webauthn.js +895 -26
  58. package/lib/x509-sign.js +3 -0
  59. package/package.json +1 -1
  60. package/sbom.cdx.json +6 -6
package/lib/webauthn.js CHANGED
@@ -304,7 +304,7 @@ function _findExt(cert, oidName) {
304
304
  * @primitive pki.webauthn.parseAttestationObject
305
305
  * @signature pki.webauthn.parseAttestationObject(bytes) -> { fmt, attStmt, authData, authDataBytes }
306
306
  * @since 0.2.5
307
- * @status experimental
307
+ * @status stable
308
308
  * @spec W3C WebAuthn Level 3 sec. 6.5.4 / 6.1
309
309
  * @related pki.webauthn.verify
310
310
  *
@@ -316,6 +316,8 @@ function _findExt(cert, oidName) {
316
316
  * A malformed object throws `webauthn/bad-attestation-object`.
317
317
  *
318
318
  * @example
319
+ * // requires: `attestationObject` -- the CBOR bytes a browser returns from
320
+ * // navigator.credentials.create(), i.e. credential.response.attestationObject
319
321
  * var att = pki.webauthn.parseAttestationObject(attestationObject);
320
322
  * att.fmt; // "packed"
321
323
  * att.authData.credentialPublicKey.kty; // 2 (EC2)
@@ -404,8 +406,279 @@ function _aaguidIsSigned(fmt) { return _AAGUID_SIGNED_BY_FMT[fmt] !== false; }
404
406
  // resolve to an inherited Object member and read as recognised.
405
407
  var _VERIFY_OPTS = Object.assign(Object.create(null), {
406
408
  time: 1, metadata: 1, tpmPolicy: 1, safetyNetRoots: 1, verifySafetyNetJws: 1, requireCtsProfileMatch: 1,
409
+ expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
410
+ rootCertificates: 1,
411
+ });
412
+
413
+ // Anchor an attestation's trust path to roots the CALLER pins. The metadata route
414
+ // resolves an authenticator's roots from the catalogue that registered it, which is
415
+ // the stronger source -- but it only reaches models the catalogue lists, and some
416
+ // are not there: Apple does not publish its authenticators to the FIDO Metadata
417
+ // Service, and the Google hardware-attestation roots are distributed by Google. For
418
+ // those formats the catalogue route resolves nothing, so without this there is no
419
+ // way to anchor them at all and a trust path comes back unchecked. (android-safetynet
420
+ // already had its own `safetyNetRoots` for the same reason; this is that carve-out
421
+ // made general.)
422
+ //
423
+ // PRECEDENCE, stated rather than implied: `metadata` governs when it is supplied,
424
+ // because a model's own registered roots are a stronger claim than a static pin, and
425
+ // its status reports can disqualify a model these roots would still accept.
426
+ // `rootCertificates` is the fallback for the models the catalogue does not cover.
427
+ // Supplying both is not an error -- it is the ordinary configuration for a relying
428
+ // party that accepts MDS-listed authenticators AND Apple.
429
+ // The caller's pinned roots are copied SYNCHRONOUSLY, at the entry point, because
430
+ // they are not read until the attestation verifier has resolved -- a later promise
431
+ // turn. Both the ARRAY and each DER buffer in it stay caller-owned across that gap,
432
+ // so a caller that recycles the array or overwrites a certificate's bytes in between
433
+ // would have the attestation anchored against the replacement roots while the verdict
434
+ // still reports `anchoredTo: "rootCertificates"` (CWE-367 reaching a wrong trust
435
+ // decision). Same defence, and the same reason, as the assertion input's snapshot.
436
+ // Shape faults are NOT raised here: the array is validated where it is consumed, so
437
+ // supplying a malformed pin alongside a metadata catalogue that answers keeps failing
438
+ // exactly where it did before.
439
+ // A parsed certificate is a tree of plain objects, arrays and Buffers, and the anchor
440
+ // comparison downstream reads its `subject` and `subjectPublicKeyInfo` -- both nested
441
+ // Buffers the caller still owns. Copying only the top-level array would leave those
442
+ // aliased, so the parsed form is deep-copied: every Buffer is duplicated and every
443
+ // container rebuilt, while the value types a parsed certificate carries (Date for a
444
+ // validity bound, BigInt for a serial) are preserved rather than flattened.
445
+ //
446
+ // The depth bound is a RECURSION BACKSTOP set far above any real certificate, never the
447
+ // precision mechanism -- a bound tuned to today's shapes silently rejects a legitimate root
448
+ // the moment a deeper one appears. A measured certificate reaches depth 5, and the deepest
449
+ // structure the profile allows -- a certificate policy whose qualifier carries a UserNotice
450
+ // with a noticeRef and its noticeNumbers -- lands near 12, so a cap of 12 would sit exactly
451
+ // on a conforming root. This is set well clear of that; it exists only so a cyclic or hostile
452
+ // object cannot recurse without end.
453
+ function _cloneParsed(v, depth) {
454
+ if (depth > 64) throw _err("webauthn/bad-input", "opts.rootCertificates[] is nested too deeply to be a parsed certificate");
455
+ if (Buffer.isBuffer(v) || v instanceof Uint8Array) return Buffer.from(v);
456
+ if (Array.isArray(v)) return v.map(function (x) { return _cloneParsed(x, depth + 1); });
457
+ if (v instanceof Date) return new Date(v.getTime());
458
+ if (v && typeof v === "object") {
459
+ var out = {};
460
+ Object.keys(v).forEach(function (k) { out[k] = _cloneParsed(v[k], depth + 1); });
461
+ return out;
462
+ }
463
+ return v; // string, number, boolean, bigint, null, undefined -- all immutable
464
+ }
465
+
466
+ // A caller-owned byte argument, copied so nothing downstream reads bytes the caller can still
467
+ // rewrite. Every byte form the parsers accept is covered -- an ArrayBuffer or a DataView arrives
468
+ // by the same door as a Buffer, and leaving those aliased would reopen the window for exactly the
469
+ // inputs that came in by the wider one. Anything else passes through untouched so this cannot
470
+ // change which inputs are accepted; the parser downstream still names a wrong type.
471
+ function _snapshotBytes(v, label) {
472
+ if (ArrayBuffer.isView(v) || v instanceof ArrayBuffer) {
473
+ return guard.bytes.snapshotSource(v, WebauthnError, "webauthn/bad-input", label);
474
+ }
475
+ return v;
476
+ }
477
+
478
+ function _snapshotRoots(supplied) {
479
+ if (!Array.isArray(supplied)) return supplied;
480
+ return supplied.map(function (root) {
481
+ if (Buffer.isBuffer(root) || root instanceof Uint8Array) {
482
+ return guard.bytes.snapshot(root, WebauthnError, "webauthn/bad-input", "opts.rootCertificates[]");
483
+ }
484
+ if (root && typeof root === "object") return _cloneParsed(root, 0);
485
+ return root; // a PEM string is immutable
486
+ });
487
+ }
488
+
489
+ // `supplied` is the SNAPSHOT taken at the entry point, never `opts.rootCertificates`
490
+ // read afresh here -- that re-read is the window this guard closes. `opts` is still
491
+ // threaded for the non-aliasing options (the validation time).
492
+ // `vopts` is `verify`'s NORMALIZED options object, never the caller's -- the parameter is
493
+ // named for that so a future caller cannot hand it the live one without noticing. Everything
494
+ // it reads was captured synchronously at the entry.
495
+ function _applyCallerRoots(res, supplied, vopts, onlyPaths) {
496
+ if (!Array.isArray(supplied) || !supplied.length) {
497
+ throw _err("webauthn/bad-input", "opts.rootCertificates must be a non-empty array of root certificates (DER, PEM, or parsed)");
498
+ }
499
+ // DER, PEM or an already-parsed certificate, decoded HERE -- the anchor comparison
500
+ // downstream reads a certificate's subject and key, so a raw buffer reaching it
501
+ // would fault on a field it does not have rather than naming the caller's mistake.
502
+ // The same three forms opts.safetyNetRoots takes, since it is the same question.
503
+ var roots = supplied.map(function (root, i) {
504
+ var cert;
505
+ try { cert = (Buffer.isBuffer(root) || typeof root === "string") ? x509.parse(root) : root; }
506
+ catch (e) { throw _err("webauthn/bad-input", "opts.rootCertificates[" + i + "] is not a decodable certificate", e); }
507
+ if (!cert || !cert.subject || !cert.subjectPublicKeyInfo) {
508
+ throw _err("webauthn/bad-input", "opts.rootCertificates[" + i + "] is not a certificate");
509
+ }
510
+ return cert;
511
+ });
512
+ // Same rule the metadata route applies: a compound element carrying no
513
+ // certificates makes no claim there is anything to anchor, so it is not a reason to
514
+ // refuse the statement -- but `anchoredTo` alone would then say "anchored" over a
515
+ // statement it did not wholly cover. The coverage is reported instead.
516
+ // `onlyPaths`, when given, is the set of paths the CATALOGUE did not cover -- the
517
+ // fallback is for exactly those. Applying it to the whole statement instead would
518
+ // re-judge an element the catalogue already governed against roots that have nothing
519
+ // to do with it: a compound holding one listed element (chaining to the root its own
520
+ // metadata entry registers) beside one unlisted element (chaining to a caller pin) would
521
+ // be refused, because the listed element does not chain to the pin. That is the
522
+ // documented "MDS-listed authenticators AND Apple" configuration, and each element is
523
+ // still anchored to something the caller trusts -- just not all to the same thing.
524
+ var paths = onlyPaths || ((res.fmt === "compound" && Array.isArray(res.compound))
525
+ ? res.compound.filter(function (el) { return el.trustPath && el.trustPath.length; })
526
+ .map(function (el) { return { tp: el.trustPath, at: el.chainValidatedAt }; })
527
+ : (res.trustPath && res.trustPath.length ? [{ tp: res.trustPath, at: res.chainValidatedAt }] : []));
528
+ // Coverage is ACCUMULATED across routes, not overwritten by whichever ran last. When
529
+ // `onlyPaths` is given the metadata route already anchored the rest and recorded that here,
530
+ // so reporting the fallback's own count would say 1 of 2 for a statement whose two elements
531
+ // were both anchored -- one by the catalogue, one by the pin. A caller enforcing
532
+ // `anchored === total` would then reject the mixed configuration this fallback exists to
533
+ // support, and the verdict would understate the trust evaluation it actually performed.
534
+ if (!onlyPaths) {
535
+ res.anchoredElements = { total: (res.fmt === "compound" && Array.isArray(res.compound)) ? res.compound.length : 1,
536
+ anchored: paths.length };
537
+ }
538
+ // `none` and a self-attestation carry no certificates, so there is nothing to
539
+ // anchor -- and a caller who asked for anchoring is told it could not be applied
540
+ // rather than handed a pass that looks like it was. Same rule the metadata route
541
+ // states, for the same reason.
542
+ if (!paths.length) {
543
+ throw _err("webauthn/anchor-not-applicable",
544
+ "opts.rootCertificates was supplied, but this attestation carries no trust path to anchor (format '" + res.fmt + "')");
545
+ }
546
+ var at = vopts.time !== undefined ? vopts.time : new Date();
547
+ // EVERY path, not merely one: a compound's elements are independent claims, and
548
+ // accepting the whole because one element anchored would let an unanchored element
549
+ // ride along on its neighbour's trust.
550
+ return paths.reduce(function (p, info) {
551
+ return p.then(function () {
552
+ var pathAt = vopts.time !== undefined ? vopts.time : (info.at || at);
553
+ return mds.chainToAnchor(info.tp.slice().reverse(), roots, pathAt,
554
+ "attestation trust path (against the roots supplied as opts.rootCertificates)");
555
+ });
556
+ }, Promise.resolve()).then(function () {
557
+ res.anchoredTo = _anchoredRoutes(res, "rootCertificates");
558
+ return res;
559
+ });
560
+ }
561
+
562
+ // android-safetynet anchors through a route of its own. The verifier REQUIRES
563
+ // `opts.safetyNetRoots` (8.5) and refuses the statement unless the x5c chain validates to
564
+ // one of them, so by the time a verdict is composed that element's path HAS been anchored --
565
+ // without either `metadata` or `rootCertificates` being involved. Counting only those two
566
+ // routes reports `anchoredTo: null` over it, whose documented meaning is that nobody checked
567
+ // the path, so a caller enforcing `anchoredTo !== null` would refuse an attestation this
568
+ // library did in fact anchor. Every route that contributed is named.
569
+ function _safetyNetAnchored(res) {
570
+ if (res.fmt === "android-safetynet") return 1;
571
+ if (res.fmt === "compound" && Array.isArray(res.compound)) {
572
+ return res.compound.filter(function (el) { return el && el.fmt === "android-safetynet"; }).length;
573
+ }
574
+ return 0;
575
+ }
576
+
577
+ // `base` is whichever of the two caller-selected routes ran, or null when neither did.
578
+ // Routes are named in a stable order so a caller may compare the string.
579
+ function _anchoredRoutes(res, base) {
580
+ var routes = base ? base.split("+") : [];
581
+ if (_safetyNetAnchored(res) && routes.indexOf("safetyNetRoots") < 0) routes.push("safetyNetRoots");
582
+ return routes.length ? routes.join("+") : null;
583
+ }
584
+
585
+ // ---- ceremony binding (WebAuthn sec. 7.1 / 7.2) ------------------------------
586
+ //
587
+ // The attestation and assertion procedures verify a SIGNATURE. What makes a
588
+ // response acceptable is a separate set of checks the relying party owns, because
589
+ // only it knows what it asked for: the challenge it issued, the origin the browser
590
+ // reported, the RP ID it operates under, and the user-presence / user-verification
591
+ // policy it requires. A verifier that answers only the first question, under a name
592
+ // that sounds like the second, is the shape of every phishing-resistant login that
593
+ // turns out not to be -- an attestation naming ANOTHER RP, with user presence
594
+ // clear, is a perfectly sound statement about a credential the caller must not
595
+ // accept.
596
+ //
597
+ // So the checks this layer CAN make are offered here, opt-in, and every verdict
598
+ // reports which of them actually ran. The challenge and the origin stay with the
599
+ // caller: they live in clientDataJSON, which the caller already holds and compares
600
+ // against state only it has.
601
+ var _BINDING_OPTS = Object.assign(Object.create(null), {
602
+ expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
407
603
  });
408
604
 
605
+ function _assertBool(v, name) {
606
+ if (typeof v !== "boolean") throw _err("webauthn/bad-input", "opts." + name + " must be a boolean");
607
+ }
608
+
609
+ // Runs the bindings the caller asked for and reports EACH one's status: `true` it
610
+ // was checked and holds, `false` it was not asked for. A caller reading the verdict
611
+ // can therefore tell a check that passed from one that never ran -- the distinction
612
+ // the single `verified` boolean could not express.
613
+ function _applyBindings(authData, coseKey, opts) {
614
+ var checked = { rpId: false, userPresence: false, userVerification: false, algorithm: false };
615
+ if (opts.expectedRpId !== undefined) {
616
+ if (typeof opts.expectedRpId !== "string" || !opts.expectedRpId.length) {
617
+ throw _err("webauthn/bad-input", "opts.expectedRpId must be a non-empty RP ID string");
618
+ }
619
+ // Compared in constant time and by full value, like every other identity
620
+ // comparison in this toolkit -- a prefix must not read as a match.
621
+ if (!guard.crypto.constantTimeEqual(_sha("sha256", Buffer.from(opts.expectedRpId, "utf8")),
622
+ Buffer.from(authData.rpIdHash))) {
623
+ throw _err("webauthn/rp-id-mismatch",
624
+ "the authenticatorData rpIdHash is not SHA-256 of opts.expectedRpId, so this response was " +
625
+ "produced for a different relying party (WebAuthn sec. 7.1 step 13 / sec. 7.2 step 15)");
626
+ }
627
+ checked.rpId = true;
628
+ }
629
+ if (opts.requireUserPresence !== undefined) {
630
+ _assertBool(opts.requireUserPresence, "requireUserPresence");
631
+ if (opts.requireUserPresence && !authData.flags.up) {
632
+ throw _err("webauthn/user-presence-required",
633
+ "opts.requireUserPresence is set and the authenticatorData User Present (UP) flag is clear " +
634
+ "(WebAuthn sec. 7.1 step 14 / sec. 7.2 step 16)");
635
+ }
636
+ checked.userPresence = opts.requireUserPresence;
637
+ }
638
+ if (opts.requireUserVerification !== undefined) {
639
+ _assertBool(opts.requireUserVerification, "requireUserVerification");
640
+ if (opts.requireUserVerification && !authData.flags.uv) {
641
+ throw _err("webauthn/user-verification-required",
642
+ "opts.requireUserVerification is set and the authenticatorData User Verified (UV) flag is clear " +
643
+ "(WebAuthn sec. 7.1 step 15 / sec. 7.2 step 17)");
644
+ }
645
+ checked.userVerification = opts.requireUserVerification;
646
+ }
647
+ // COSE alg -65535 is RSASSA-PKCS1-v1_5 with SHA-1, and a credential key may name
648
+ // it -- which would make SHA-1 the signature algorithm of every login that
649
+ // credential ever performs. Refused unless the caller lists it, so supporting a
650
+ // legacy authenticator is a decision someone wrote down rather than the default.
651
+ // (This is the CREDENTIAL key's algorithm. A legacy TPM attestation STATEMENT
652
+ // signed with RS1 is a different field and is still verified, since refusing it
653
+ // would reject the authenticator's own evidence about itself.)
654
+ var alg65535Allowed = Array.isArray(opts.allowedAlgorithms) && opts.allowedAlgorithms.indexOf(-65535) !== -1;
655
+ if (coseKey && coseKey.alg === -65535 && !alg65535Allowed) {
656
+ throw _err("webauthn/algorithm-not-allowed",
657
+ "the credential public key declares COSE algorithm -65535 (RSASSA-PKCS1-v1_5 with SHA-1); every " +
658
+ "signature made by this credential would use SHA-1, so it is refused unless opts.allowedAlgorithms " +
659
+ "names -65535 explicitly");
660
+ }
661
+ // The rest of algorithm policy is the RP's, declared to the browser as
662
+ // pubKeyCredParams, and nothing in the response proves what was offered -- so it
663
+ // is checked only when the caller states the list.
664
+ if (opts.allowedAlgorithms !== undefined) {
665
+ if (!Array.isArray(opts.allowedAlgorithms) || !opts.allowedAlgorithms.length ||
666
+ // isSafeInteger, not isInteger: a value above 2^53 is not held exactly as a
667
+ // Number, so one written that way is not the identifier the caller meant --
668
+ // and no COSE algorithm identifier lives out there anyway.
669
+ !opts.allowedAlgorithms.every(function (a) { return typeof a === "number" && Number.isSafeInteger(a); })) {
670
+ throw _err("webauthn/bad-input", "opts.allowedAlgorithms must be a non-empty array of COSE algorithm integers");
671
+ }
672
+ var alg = coseKey && coseKey.alg;
673
+ if (opts.allowedAlgorithms.indexOf(alg) === -1) {
674
+ throw _err("webauthn/algorithm-not-allowed",
675
+ "the credential public key declares COSE algorithm " + alg + ", which is not in opts.allowedAlgorithms");
676
+ }
677
+ checked.algorithm = true;
678
+ }
679
+ return checked;
680
+ }
681
+
409
682
  // One bound for every attestation certificate chain, wherever it arrives from -- an attStmt x5c
410
683
  // array or a JWS x5c header. Capping the bytes of a single entry does not bound the COUNT, and the
411
684
  // cost of an entry is a DER parse plus, downstream, a signature check or a path validation. Kept in
@@ -849,8 +1122,26 @@ function _safetyNetChainTrusted(chain, roots, time) {
849
1122
  // `chain` is the x5c order (leaf-first); trustPath is surfaced in pki.path.validate
850
1123
  // order (anchor-adjacent first, target/leaf last) so the caller passes it straight
851
1124
  // to the path validator without re-ordering. The input array is not mutated.
1125
+ // `attestationVerified`, not `verified`: this says the attestation STATEMENT is
1126
+ // sound, which is not the same claim as "this registration is acceptable". The
1127
+ // ceremony bindings the caller may also have asked for are reported separately, in
1128
+ // `bindingChecked`, so a field that passed can be told from one that never ran.
1129
+ // It also carries everything a relying party must STORE to finish registration and
1130
+ // run a later login: the credentialId it will look the credential up by, the
1131
+ // credential public key that will verify assertions, and the signCount that is the
1132
+ // starting point for the sec. 7.2 step 21 counter rule. Leaving those out sent the
1133
+ // caller back to parse the attestation object a second time for values this call
1134
+ // had already decoded -- and a login cannot be verified without them.
852
1135
  function _result(fmt, attestationType, chain, att) {
853
- return { verified: true, fmt: fmt, attestationType: attestationType, trustPath: (chain || []).slice().reverse(), aaguid: att.authData.aaguid, credentialPublicKey: att.authData.credentialPublicKey };
1136
+ return {
1137
+ attestationVerified: true, fmt: fmt, attestationType: attestationType,
1138
+ trustPath: (chain || []).slice().reverse(),
1139
+ aaguid: att.authData.aaguid,
1140
+ credentialId: att.authData.credentialId,
1141
+ credentialPublicKey: att.authData.credentialPublicKey,
1142
+ signCount: att.authData.signCount,
1143
+ flags: att.authData.flags,
1144
+ };
854
1145
  }
855
1146
 
856
1147
  // Decode the Apple extension AppleAnonymousAttestation ::= SEQUENCE { nonce [1]
@@ -858,9 +1149,19 @@ function _result(fmt, attestationType, chain, att) {
858
1149
  function _appleNonce(extValue) {
859
1150
  var seq;
860
1151
  try { seq = asn1.decode(extValue); } catch (e) { throw _err("webauthn/bad-att-cert", "the apple attestation extension is not decodable", e); }
861
- var tagged = seq.children && seq.children[0];
862
- if (!tagged || tagged.tagClass !== "context" || tagged.tagNumber !== 1 || !tagged.children || !tagged.children[0]) {
863
- throw _err("webauthn/bad-att-cert", "the apple attestation extension is not SEQUENCE { [1] OCTET STRING }");
1152
+ // ARITY IS PART OF THE DECLARED SHAPE. Reading the first child and ignoring the rest
1153
+ // accepts a certificate carrying a second, unchecked value beside the nonce -- and this
1154
+ // extension exists precisely to carry the value the attestation binds to, so an
1155
+ // ambiguous encoding of it is not a shape this verifier gets to pick a reading from.
1156
+ // The type is one SEQUENCE of exactly one field, and the EXPLICIT wrapper holds exactly
1157
+ // one value; anything else is refused rather than partially read.
1158
+ var isSeq = seq.tagClass === "universal" && seq.tagNumber === asn1.TAGS.SEQUENCE;
1159
+ if (!isSeq || !seq.children || seq.children.length !== 1) {
1160
+ throw _err("webauthn/bad-att-cert", "the apple attestation extension is not SEQUENCE { [1] OCTET STRING } (expected exactly one field)");
1161
+ }
1162
+ var tagged = seq.children[0];
1163
+ if (!tagged || tagged.tagClass !== "context" || tagged.tagNumber !== 1 || !tagged.children || tagged.children.length !== 1) {
1164
+ throw _err("webauthn/bad-att-cert", "the apple attestation extension is not SEQUENCE { [1] OCTET STRING } (expected one EXPLICIT [1] value)");
864
1165
  }
865
1166
  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); }
866
1167
  }
@@ -875,11 +1176,11 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
875
1176
 
876
1177
  /**
877
1178
  * @primitive pki.webauthn.verify
878
- * @signature pki.webauthn.verify(attestationObject, clientDataHash, opts) -> Promise<{ verified, fmt, attestationType, trustPath, aaguid, credentialPublicKey }>
1179
+ * @signature pki.webauthn.verify(attestationObject, clientDataHash, opts) -> Promise<{ attestationVerified, fmt, attestationType, trustPath, anchoredTo, anchoredElements, aaguid, credentialId, credentialPublicKey, signCount, flags, bindingChecked }>
879
1180
  * @since 0.2.5
880
- * @status experimental
881
- * @spec W3C WebAuthn Level 3 sec. 8
882
- * @related pki.webauthn.parseAttestationObject
1181
+ * @status stable
1182
+ * @spec W3C WebAuthn Level 3 sec. 8 / sec. 7.1
1183
+ * @related pki.webauthn.parseAttestationObject, pki.webauthn.parseClientData, pki.webauthn.verifyAssertion
883
1184
  *
884
1185
  * Verify a WebAuthn attestation statement: the attestation signature over
885
1186
  * `authenticatorData || clientDataHash` and (for the x5c formats) the format's
@@ -888,22 +1189,61 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
888
1189
  * throws a typed `webauthn/*` error; a signature that does not verify is a
889
1190
  * `webauthn/verify-failed` verdict, never a silent pass.
890
1191
  *
1192
+ * The verdict field is `attestationVerified`, and the name is the point: a sound
1193
+ * attestation statement is not the same claim as an acceptable registration. The
1194
+ * statement says nothing about WHICH relying party asked for it, or whether a user
1195
+ * was present -- so an attestation naming another origin's RP ID, with user presence
1196
+ * clear, is perfectly sound and must not be registered. Supply `expectedRpId`,
1197
+ * `requireUserPresence`, `requireUserVerification` and `allowedAlgorithms` and those
1198
+ * are checked here; `bindingChecked` reports which ran, so a check that passed can be
1199
+ * told from one that never happened. The CHALLENGE and the ORIGIN live in
1200
+ * clientDataJSON -- read them with `pki.webauthn.parseClientData`, which compares
1201
+ * them against the state only the relying party has.
1202
+ *
1203
+ * The verdict also carries what a relying party must STORE to run a later login:
1204
+ * `credentialId`, `credentialPublicKey` and the initial `signCount`.
1205
+ *
891
1206
  * @intro This verifies the attestation STATEMENT -- the signature and the format's
892
1207
  * structural bindings (the x5c leaf key == credential key, the apple nonce, the tpm
893
1208
  * certInfo Name/extraData, the android KeyDescription, the fido-u2f verificationData).
894
1209
  * Chaining the returned `trustPath` (the x5c certificates in `pki.path.validate`
895
- * order -- anchor-adjacent first, leaf last) to a trusted root is the caller's
896
- * step. Two ways to take it: anchor the path with `pki.path.validate` against roots
897
- * you pin, or pass `opts.metadata` -- a `pki.webauthn.verifyMetadataBlob` result --
898
- * and the authenticator's registered attestation roots are resolved from its aaguid
899
- * and the trust path is required to reach one, so an unlisted or revoked model is
900
- * refused rather than reported as verified.
1210
+ * order -- anchor-adjacent first, leaf last) to a trusted root is a separate step,
1211
+ * and there are three ways to take it. Pass `opts.metadata` -- a
1212
+ * `pki.webauthn.verifyMetadataBlob` result -- and the authenticator's registered
1213
+ * attestation roots are resolved from its own identifier and the trust path is
1214
+ * required to reach one, so an unlisted or revoked model is refused rather than
1215
+ * reported as verified. Pass `opts.rootCertificates` and the path is required to
1216
+ * validate to a root you pin: this is what anchors the formats the catalogue does
1217
+ * not cover, since Apple does not publish its authenticators to the FIDO Metadata
1218
+ * Service and the Google hardware-attestation roots come from Google. Or anchor
1219
+ * `trustPath` yourself with `pki.path.validate`. Supplying both options is the
1220
+ * ordinary configuration for a relying party that accepts MDS-listed authenticators
1221
+ * AND Apple; `metadata` governs when present, because a model's own registered
1222
+ * roots are a stronger claim than a static pin and its status reports can
1223
+ * disqualify a model those roots would still accept. `anchoredTo` reports which
1224
+ * route ran, naming every route that contributed and joining them with `+` when more
1225
+ * than one did -- `"metadata"`, `"rootCertificates"`, and `"safetyNetRoots"` for the
1226
+ * android-safetynet chain, which anchors through the roots that format requires whether
1227
+ * or not either other route was asked for. It is `null` only when nothing anchored the
1228
+ * path. On the mixed metadata/pinned-root route `metadata` still carries the
1229
+ * entries that governed, so the stronger half of the decision stays auditable rather
1230
+ * than being flattened into the pinned-root half. `anchoredElements` reports how much of the
1231
+ * statement it covered. That matters for a compound attestation (sec. 8.9), whose
1232
+ * elements are independent claims: an element carrying no certificates makes no
1233
+ * claim there is anything to anchor, so it is not a reason to refuse the statement,
1234
+ * but it does mean "anchored" covered fewer elements than the statement holds.
901
1235
  *
902
1236
  * @example
903
- * var res = await pki.webauthn.verify(attestationObject, clientDataHash, {});
904
- * res.verified; // true (statement signature + bindings hold)
905
- * res.attestationType; // "Basic"
906
- * // anchor res.trustPath to your pinned roots with pki.path.validate
1237
+ * // requires: `attestationObject` from navigator.credentials.create(), and
1238
+ * // `clientDataHash` = SHA-256 of the matching credential.response.clientDataJSON
1239
+ * var res = await pki.webauthn.verify(attestationObject, clientDataHash, {
1240
+ * expectedRpId: "example.com", requireUserPresence: true,
1241
+ * });
1242
+ * res.attestationVerified; // true (statement signature + bindings hold)
1243
+ * res.bindingChecked.rpId; // true -- this response names example.com
1244
+ * res.attestationType; // "Basic"
1245
+ * // store res.credentialId / res.credentialPublicKey / res.signCount for logins,
1246
+ * // and anchor res.trustPath to your pinned roots with pki.path.validate
907
1247
  */
908
1248
  function verify(attestationObject, clientDataHash, opts) {
909
1249
  opts = opts || {};
@@ -921,6 +1261,22 @@ function verify(attestationObject, clientDataHash, opts) {
921
1261
  return Promise.reject(_err("webauthn/bad-input", "clientDataHash must be a 32-byte SHA-256 digest"));
922
1262
  }
923
1263
  var att;
1264
+ // Both byte inputs are snapshotted BEFORE anything reads them, for the same reason the trust
1265
+ // anchors are: the attestation statement is not evaluated until a later promise turn, and both
1266
+ // stay caller-owned across that gap. The attestation object is copied before it is parsed, not
1267
+ // after, because the parse surfaces its byte fields as VIEWS -- the authenticator data the
1268
+ // signature covers, the statement, the credential key -- so copying afterwards would leave every
1269
+ // one of them pointing at bytes the caller can still rewrite. The clientDataHash is what binds
1270
+ // the statement to this ceremony; a caller who overwrote it in that gap would have the signature
1271
+ // checked against a challenge and origin nobody agreed to, and the verdict would still report a
1272
+ // sound attestation. A DataView or ArrayBuffer comes in by the same door and is copied too.
1273
+ var attBytes, cdh;
1274
+ try {
1275
+ attBytes = _snapshotBytes(attestationObject, "attestationObject");
1276
+ cdh = _snapshotBytes(clientDataHash, "clientDataHash");
1277
+ } catch (e) { return Promise.reject(e); }
1278
+ attestationObject = attBytes;
1279
+ clientDataHash = cdh;
924
1280
  try { att = parseAttestationObject(attestationObject); } catch (e) { return Promise.reject(e); }
925
1281
  // A registration attestation MUST carry attestedCredentialData (the AT flag): the
926
1282
  // whole point is to bind the attestation to a credential public key. Reject an
@@ -940,8 +1296,91 @@ function verify(attestationObject, clientDataHash, opts) {
940
1296
  if (opts.tpmPolicy !== undefined && !_formatCanSatisfyTpmPolicy(att)) {
941
1297
  return Promise.reject(_err("webauthn/tpm-policy", "opts.tpmPolicy requires a TPM attestation, but this attestation is format '" + att.fmt + "', which carries no TPM public area"));
942
1298
  }
943
- return Promise.resolve().then(function () { return verifier(att, clientDataHash, opts); })
944
- .then(function (res) { return opts.metadata === undefined ? res : _applyMetadata(res, att, opts); });
1299
+ // The ceremony bindings run BEFORE the statement is evaluated: a response
1300
+ // produced for another relying party, or without the user presence the caller
1301
+ // requires, is not a message this call should spend a signature verification on
1302
+ // -- and refusing it early keeps the "acceptable" question ahead of the "sound"
1303
+ // one, which is the order sec. 7.1 puts them in.
1304
+ // ONE normalized options object, built synchronously here. Nothing past this point reads
1305
+ // the caller's `opts` again -- not the values, and not the presence tests that pick which
1306
+ // trust policy runs.
1307
+ //
1308
+ // Fixing this per-field does not converge. The verifier resolves in a later turn, so every
1309
+ // field the deferred path touches stays caller-owned across that gap, and each repair that
1310
+ // snapshotted one more field left the next one aliased: the roots, then the safetynet
1311
+ // roots, then the routing test beside them, then the metadata routing test, then the
1312
+ // validation instant. They are not separate bugs, they are one window, and the way to
1313
+ // close a window is to stop reading through it rather than to copy one more thing across.
1314
+ //
1315
+ // `Object.assign` captures every own field by value at this instant, which fixes both the
1316
+ // values and the `!== undefined` presence tests derived from them. The two root arrays get
1317
+ // a deeper copy because their CONTENTS are caller-owned too. `metadata` is captured by
1318
+ // reference: a verified catalogue can hold thousands of entries and copying it per
1319
+ // registration is a real cost, so reassignment can no longer swap it, while mutation of
1320
+ // the object it points at remains open and is tracked separately.
1321
+ var bindingChecked, vopts;
1322
+ try {
1323
+ bindingChecked = _applyBindings(att.authData, att.authData.credentialPublicKey, opts);
1324
+ vopts = Object.assign({}, opts, {
1325
+ rootCertificates: _snapshotRoots(opts.rootCertificates),
1326
+ safetyNetRoots: _snapshotRoots(opts.safetyNetRoots),
1327
+ });
1328
+ } catch (e) { return Promise.reject(e); }
1329
+ return Promise.resolve().then(function () { return verifier(att, clientDataHash, vopts); })
1330
+ .then(function (res) {
1331
+ // Metadata governs when supplied; caller roots are the fallback for the models
1332
+ // the catalogue does not cover. Whichever ran, the verdict SAYS which -- so a
1333
+ // caller can tell an anchored trust path from one nobody checked, rather than
1334
+ // inferring it from which option they happened to pass.
1335
+ if (vopts.metadata === undefined) {
1336
+ if (vopts.rootCertificates !== undefined) return _applyCallerRoots(res, vopts.rootCertificates, vopts);
1337
+ res.anchoredTo = _anchoredRoutes(res, null);
1338
+ // Coverage is reported whenever anything WAS anchored, for the same reason the other
1339
+ // routes report it: `anchoredTo` alone cannot say how much of a compound it covered.
1340
+ var sn = _safetyNetAnchored(res);
1341
+ if (sn) {
1342
+ res.anchoredElements = { total: (res.fmt === "compound" && Array.isArray(res.compound)) ? res.compound.length : 1,
1343
+ anchored: sn };
1344
+ }
1345
+ return res;
1346
+ }
1347
+ return Promise.resolve().then(function () { return _applyMetadata(res, att, vopts); })
1348
+ .then(function (out) { out.anchoredTo = _anchoredRoutes(out, "metadata"); return out; }, function (e) {
1349
+ // A catalogue MISS is the case pinned roots exist for: the combined
1350
+ // configuration is exactly "MDS-listed authenticators AND Apple", and Apple
1351
+ // is in no catalogue, so failing here would make the documented pairing
1352
+ // unusable for the very models it was added to reach.
1353
+ //
1354
+ // A catalogue DENIAL is not a miss and never falls through. If the model IS
1355
+ // listed and its entry disqualifies it -- a revoked status report, a trust
1356
+ // path that does not reach the roots that model registered -- then the
1357
+ // catalogue has spoken about this authenticator, and letting a static pin
1358
+ // overrule it would turn the stronger source into the weaker one.
1359
+ if (!e || e.code !== "webauthn/metadata-not-found" || vopts.rootCertificates === undefined) throw e;
1360
+ // Only the elements the catalogue did not cover fall back. The listed ones were
1361
+ // already governed AND chain-validated to the roots their own entries register, so
1362
+ // re-judging them against an unrelated pin would refuse the combined configuration
1363
+ // this fallback exists to support.
1364
+ return Promise.resolve(_applyCallerRoots(res, vopts.rootCertificates, vopts, e.missedPaths))
1365
+ .then(function (out) {
1366
+ // Report BOTH routes. The pinned roots covered only the elements the catalogue
1367
+ // missed; the rest were governed by their metadata entries and chain-validated
1368
+ // against the roots those entries register. Saying "rootCertificates" alone
1369
+ // would attribute the whole evaluation to the weaker half and lose the entries
1370
+ // an auditor needs to see. `anchoredTo` names both, and the governed entries
1371
+ // are surfaced exactly as the pure-metadata route surfaces them.
1372
+ var applied = e.appliedEntries || [];
1373
+ if (!applied.length) return out;
1374
+ var primary = applied[0];
1375
+ out.metadata = { aaguid: primary.entry.aaguid, keyIdentifiers: primary.entry.keyIdentifiers || [],
1376
+ entry: primary.entry, entries: applied.map(function (a) { return a.entry; }),
1377
+ anchors: primary.anchors.length };
1378
+ out.anchoredTo = _anchoredRoutes(out, "metadata+rootCertificates");
1379
+ return out;
1380
+ });
1381
+ });
1382
+ })
1383
+ .then(function (res) { res.bindingChecked = bindingChecked; return res; });
945
1384
  }
946
1385
 
947
1386
  // WebAuthn sec. 7.1 step 22-23: having verified the attestation, look the authenticator model up in
@@ -968,10 +1407,19 @@ function _applyMetadata(res, att, opts) {
968
1407
  // single choice for the whole statement is wrong in both directions. It would push the packed
969
1408
  // element down the certificate-identifier path (where a conforming entry indexed only by its
970
1409
  // AAGUID is not found), or trust the u2f element's unsigned AAGUID.
1410
+ // A compound's elements are independent claims (sec. 8.9), and one carrying no
1411
+ // certificates -- a `none` or self element -- makes no attestation claim there is
1412
+ // anything to anchor. Refusing the whole statement over it would reject a
1413
+ // conforming compound whose certificate-bearing elements do chain; enforcing over
1414
+ // the rest and saying nothing would report catalogue enforcement over a statement
1415
+ // it did not wholly reach. So the elements that CAN be governed are, and the
1416
+ // coverage is reported (`anchoredElements`) rather than left to be assumed.
971
1417
  var paths = (res.fmt === "compound" && Array.isArray(res.compound))
972
1418
  ? res.compound.filter(function (el) { return el.trustPath && el.trustPath.length; })
973
1419
  .map(function (el) { return { tp: el.trustPath, fmt: el.fmt, at: el.chainValidatedAt }; })
974
1420
  : (res.trustPath && res.trustPath.length ? [{ tp: res.trustPath, fmt: res.fmt, at: res.chainValidatedAt }] : []);
1421
+ res.anchoredElements = { total: (res.fmt === "compound" && Array.isArray(res.compound)) ? res.compound.length : 1,
1422
+ anchored: paths.length };
975
1423
  // An attestation with no trust path at all (`none`, or a self-attestation) has nothing to anchor,
976
1424
  // so a caller who asked for metadata enforcement must be told it could not be applied rather than
977
1425
  // receiving a pass that looks like it was.
@@ -1046,9 +1494,45 @@ function _applyMetadata(res, att, opts) {
1046
1494
  // EVERY path must pass, not merely one of them: for a compound attestation each element is an
1047
1495
  // independent claim, and accepting the whole because one element anchored would let an
1048
1496
  // unanchored -- or revoked -- element ride along on its neighbour's trust.
1049
- return paths.reduce(function (p, info) {
1497
+ // Govern EVERY path BEFORE any is chain-validated, and let the SEVEREST outcome decide
1498
+ // rather than the first one reached. A compound's elements are independent claims and
1499
+ // its element order is NOT signed, so aborting on the first failure lets that order pick
1500
+ // which element the catalogue gets to speak about. The asymmetry that makes it exploitable
1501
+ // is that the errors are not equal: `metadata-not-found` is the one outcome the caller may
1502
+ // fall back to pinned roots on, and that fallback covers the WHOLE statement. So an element
1503
+ // the catalogue does not list, placed first, would raise the fallback error and a listed
1504
+ // sibling carrying a disqualifying status report would never be consulted -- the unlisted
1505
+ // element laundering the revoked one. A denial therefore outranks a miss wherever each sits:
1506
+ // a miss is only reported once every other path has been governed and none of them objected.
1507
+ var governed = [];
1508
+ var missed = null;
1509
+ var missedPaths = [];
1510
+ for (var gi = 0; gi < paths.length; gi++) {
1511
+ try {
1512
+ governed.push({ info: paths[gi], g: govern(paths[gi]) });
1513
+ } catch (ge) {
1514
+ // WHICH paths missed is carried with the error, not just THAT one did. The caller's
1515
+ // pinned-root fallback is for the elements the catalogue did not cover, and it cannot
1516
+ // tell which those were from the verdict alone -- every element looks the same there.
1517
+ if (ge && ge.code === "webauthn/metadata-not-found") { missed = missed || ge; missedPaths.push(paths[gi]); continue; }
1518
+ throw ge; // a status denial, a missing anchor, any other governance failure: terminal
1519
+ }
1520
+ }
1521
+ // The miss carries BOTH halves: which paths the catalogue did not cover, and which entries
1522
+ // it did govern. Without the second the fallback can only describe its own half, so a
1523
+ // mixed-route verdict would attribute the whole evaluation to the pinned roots and drop the
1524
+ // metadata-backed part entirely -- the stronger half, and the one an auditor most needs.
1525
+ if (missed) { missed.missedPaths = missedPaths; missed.appliedEntries = applied; }
1526
+ // The miss is NOT reported yet. Resolving an entry is only half of governance: the other
1527
+ // half is that the path must actually VALIDATE to the roots that entry registers, and that
1528
+ // runs below. Reporting the miss here would skip it for every listed element -- so a listed
1529
+ // element whose path reaches the caller's pinned roots but NOT its own registered roots
1530
+ // would ride out on the unlisted sibling's fallback, which is that same bypass moved one
1531
+ // phase down. Every listed element is chain-validated first, and only then may a miss be
1532
+ // raised.
1533
+ return governed.reduce(function (p, item) {
1050
1534
  return p.then(function () {
1051
- var g = govern(info);
1535
+ var g = item.g, info = item.info;
1052
1536
  // A path is re-validated at the instant its own format already judged it at, when the format
1053
1537
  // established one from signed data. An android-safetynet response carries its signing time and
1054
1538
  // its service chain has usually expired since; resetting to the current clock here would
@@ -1061,6 +1545,12 @@ function _applyMetadata(res, att, opts) {
1061
1545
  });
1062
1546
  }, Promise.resolve())
1063
1547
  .then(function () {
1548
+ // Every LISTED element has now been fully governed -- entry resolved, status checked, and its
1549
+ // path validated to that entry's own registered roots. A miss recorded above may finally be
1550
+ // reported, and the caller's pinned-roots fallback is reachable only from here: a statement
1551
+ // whose listed elements all satisfied their catalogue entries, with a sibling the catalogue
1552
+ // simply does not cover.
1553
+ if (missed) throw missed;
1064
1554
  // The ENTRY's aaguid, not the authenticator's raw field: for a U2F authenticator that field is
1065
1555
  // all zeroes, which means "no model identity" -- reporting it back as though it were one would
1066
1556
  // hand the caller a value that matches nothing and reads like an identifier. `entries` lists
@@ -1092,7 +1582,7 @@ void constants;
1092
1582
  * @primitive pki.webauthn.verifyMetadataBlob
1093
1583
  * @signature pki.webauthn.verifyMetadataBlob(blob, opts) -> Promise<{ no, nextUpdate, entries, byAaguid }>
1094
1584
  * @since 0.4.11
1095
- * @status experimental
1585
+ * @status stable
1096
1586
  * @spec FIDO Metadata Service v3.0 sec. 3.1, RFC 7515
1097
1587
  * @related pki.webauthn.verify, pki.webauthn.metadataFor
1098
1588
  *
@@ -1130,6 +1620,8 @@ void constants;
1130
1620
  * ignored rather than failed on.
1131
1621
  *
1132
1622
  * @example
1623
+ * // requires: `mdsBlobBytes` -- the signed BLOB from https://mds3.fidoalliance.org/
1624
+ * // -- and `fidoRootDer`, the FIDO Alliance root certificate it chains to
1133
1625
  * var md = await pki.webauthn.verifyMetadataBlob(mdsBlobBytes, {
1134
1626
  * rootCertificates: [fidoRootDer],
1135
1627
  * previousNo: 41, // refuse a replay of a BLOB you have already superseded
@@ -1144,7 +1636,7 @@ void constants;
1144
1636
  * @primitive pki.webauthn.metadataFor
1145
1637
  * @signature pki.webauthn.metadataFor(metadata, identifier) -> entry | null
1146
1638
  * @since 0.4.11
1147
- * @status experimental
1639
+ * @status stable
1148
1640
  * @spec FIDO Metadata Service v3.0 sec. 3.1.1
1149
1641
  * @related pki.webauthn.verifyMetadataBlob, pki.webauthn.metadataAnchors
1150
1642
  *
@@ -1160,6 +1652,8 @@ void constants;
1160
1652
  * declares no model identity" and matches nothing.
1161
1653
  *
1162
1654
  * @example
1655
+ * // requires: `mdsMetadata` -- a verifyMetadataBlob RESULT (never raw bytes, so a
1656
+ * // lookup cannot be answered out of an unverified BLOB) -- and the model's aaguid
1163
1657
  * var entry = pki.webauthn.metadataFor(mdsMetadata, mdsAaguid);
1164
1658
  * entry.statusReports[0].status; // "FIDO_CERTIFIED_L1"
1165
1659
  * pki.webauthn.metadataFor(mdsMetadata, "00000000-0000-0000-0000-000000000000"); // null
@@ -1169,7 +1663,7 @@ void constants;
1169
1663
  * @primitive pki.webauthn.metadataAnchors
1170
1664
  * @signature pki.webauthn.metadataAnchors(entry) -> [certificate]
1171
1665
  * @since 0.4.11
1172
- * @status experimental
1666
+ * @status stable
1173
1667
  * @spec FIDO Metadata Service v3.0 sec. 3.1.1
1174
1668
  * @related pki.webauthn.metadataFor, pki.path.validate
1175
1669
  *
@@ -1180,6 +1674,7 @@ void constants;
1180
1674
  * malformed root refuse the entire catalogue for every other authenticator in it.
1181
1675
  *
1182
1676
  * @example
1677
+ * // requires: `mdsEntry` -- one entry from a verified BLOB, as metadataFor returns
1183
1678
  * var anchors = pki.webauthn.metadataAnchors(mdsEntry);
1184
1679
  * anchors.length; // the attestation roots this model registered
1185
1680
  * anchors[0].subject; // the decoded root DN
@@ -1187,9 +1682,383 @@ void constants;
1187
1682
  * // await pki.path.validate(res.trustPath, { trustAnchors: anchors, time: mdsTime });
1188
1683
  */
1189
1684
 
1685
+ // ---- public: parseClientData -------------------------------------------------
1686
+
1687
+ // The two ceremony types, fixed by the spec -- `webauthn.create` for registration
1688
+ // and `webauthn.get` for authentication. This is NOT relying-party policy: a
1689
+ // registration response replayed into a login (or the reverse) is a real attack,
1690
+ // and which one a caller is finishing is known at the call site, never negotiated.
1691
+ var CLIENT_DATA_TYPE = Object.assign(Object.create(null), { "webauthn.create": 1, "webauthn.get": 1 });
1692
+
1693
+ /**
1694
+ * @primitive pki.webauthn.parseClientData
1695
+ * @signature pki.webauthn.parseClientData(bytes, opts?) -> { type, challenge, origin, crossOrigin, topOrigin, checked }
1696
+ * @since 0.5.0
1697
+ * @status experimental
1698
+ * @spec W3C WebAuthn Level 3 sec. 5.8.1 / 7.1 / 7.2
1699
+ * @defends webauthn-ceremony-confusion (CWE-345)
1700
+ * @related pki.webauthn.verify, pki.webauthn.verifyAssertion
1701
+ *
1702
+ * Decode the `clientDataJSON` a ceremony returns -- the half of a WebAuthn response
1703
+ * the signature covers by digest but that no signature check ever looks inside.
1704
+ * Parsed through the shared fail-closed JSON guard (bounded bytes and depth,
1705
+ * fatal UTF-8, duplicate members refused, no prototype pollution), because these
1706
+ * are bytes an attacker chose. `challenge` is returned DECODED from base64url as a
1707
+ * Buffer, so a caller compares raw bytes and never two spellings of the same
1708
+ * value; `type`, `origin`, `crossOrigin` and `topOrigin` come back as they were.
1709
+ *
1710
+ * Supply `expectedType`, `expectedChallenge` and `expectedOrigin` and each is
1711
+ * checked here -- the challenge in constant time and by full value. `checked`
1712
+ * reports which ran, so a check that passed is distinguishable from one that never
1713
+ * happened. `expectedType` is worth setting on every call: the ceremony a response
1714
+ * belongs to is fixed, and accepting a `webauthn.create` where a `webauthn.get` was
1715
+ * expected is a credential-registration response replayed as a login.
1716
+ *
1717
+ * @opts
1718
+ * expectedType -- "webauthn.create" or "webauthn.get"
1719
+ * expectedChallenge -- the challenge bytes this ceremony issued (Buffer)
1720
+ * expectedOrigin -- the origin string, or an array of acceptable origins
1721
+ *
1722
+ * @example
1723
+ * // requires: `clientDataJSON` -- credential.response.clientDataJSON;
1724
+ * // `issuedChallenge` -- the random bytes this server sent
1725
+ * var cd = pki.webauthn.parseClientData(clientDataJSON, {
1726
+ * expectedType: "webauthn.get",
1727
+ * expectedChallenge: issuedChallenge,
1728
+ * expectedOrigin: "https://example.com",
1729
+ * });
1730
+ * cd.checked.challenge; // true -- the issued challenge came back
1731
+ * cd.crossOrigin; // false
1732
+ */
1733
+ function parseClientData(bytes, opts) {
1734
+ opts = opts || {};
1735
+ if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
1736
+ guard.identifier.assertKnownKeys(opts, _CLIENT_DATA_OPTS, _err, "webauthn/bad-input", "opts has an unknown key ");
1737
+ if (!Buffer.isBuffer(bytes) && !(bytes instanceof Uint8Array)) {
1738
+ throw _err("webauthn/bad-input", "clientDataJSON must be the RAW Buffer or Uint8Array, not a parsed object");
1739
+ }
1740
+ var doc = guard.json.parse(Buffer.from(bytes), _err, {
1741
+ maxBytes: constants.LIMITS.JSON_MAX_BYTES, maxDepth: constants.LIMITS.JSON_MAX_DEPTH,
1742
+ badJson: "webauthn/bad-client-data", tooDeep: "webauthn/bad-client-data",
1743
+ duplicateMember: "webauthn/bad-client-data", tooLarge: "webauthn/bad-client-data",
1744
+ badInput: "webauthn/bad-input", label: "clientDataJSON",
1745
+ });
1746
+ if (!_isPlainObject(doc)) throw _err("webauthn/bad-client-data", "clientDataJSON is not a JSON object (WebAuthn sec. 5.8.1)");
1747
+ if (typeof doc.type !== "string" || CLIENT_DATA_TYPE[doc.type] !== 1) {
1748
+ throw _err("webauthn/bad-client-data",
1749
+ "clientDataJSON type must be \"webauthn.create\" or \"webauthn.get\", got " + JSON.stringify(doc.type) +
1750
+ " (WebAuthn sec. 5.8.1)");
1751
+ }
1752
+ if (typeof doc.origin !== "string" || !doc.origin.length) {
1753
+ throw _err("webauthn/bad-client-data", "clientDataJSON carries no origin (WebAuthn sec. 5.8.1)");
1754
+ }
1755
+ if (typeof doc.challenge !== "string" || !doc.challenge.length) {
1756
+ throw _err("webauthn/bad-client-data", "clientDataJSON carries no challenge (WebAuthn sec. 5.8.1)");
1757
+ }
1758
+ // base64url, and STRICTLY: the challenge is a comparison key, so an encoding the
1759
+ // decoder had to guess at would let two spellings of one value both "match".
1760
+ var challenge;
1761
+ try {
1762
+ challenge = guard.encoding.base64url(doc.challenge, constants.LIMITS.JSON_MAX_BYTES, _err,
1763
+ "webauthn/bad-client-data", "the clientDataJSON challenge");
1764
+ } catch (e) {
1765
+ if (e && e.code === "webauthn/bad-client-data") throw e;
1766
+ throw _err("webauthn/bad-client-data", "the clientDataJSON challenge is not base64url (WebAuthn sec. 5.8.1)", e);
1767
+ }
1768
+ if (doc.crossOrigin !== undefined && typeof doc.crossOrigin !== "boolean") {
1769
+ throw _err("webauthn/bad-client-data", "clientDataJSON crossOrigin must be a boolean when present (WebAuthn sec. 5.8.1)");
1770
+ }
1771
+ // A present-but-malformed topOrigin is refused rather than reported as absent. It
1772
+ // is the top-level origin of a cross-origin ceremony -- something a caller makes a
1773
+ // policy decision on -- so "the sender wrote something that is not an origin" and
1774
+ // "the sender said nothing" must not arrive as the same `null`.
1775
+ if (doc.topOrigin !== undefined && (typeof doc.topOrigin !== "string" || !doc.topOrigin.length)) {
1776
+ throw _err("webauthn/bad-client-data", "clientDataJSON topOrigin must be a non-empty string when present (WebAuthn sec. 5.8.1)");
1777
+ }
1778
+
1779
+ var checked = { type: false, challenge: false, origin: false };
1780
+ if (opts.expectedType !== undefined) {
1781
+ if (CLIENT_DATA_TYPE[opts.expectedType] !== 1) {
1782
+ throw _err("webauthn/bad-input", "opts.expectedType must be \"webauthn.create\" or \"webauthn.get\"");
1783
+ }
1784
+ if (doc.type !== opts.expectedType) {
1785
+ throw _err("webauthn/client-data-mismatch",
1786
+ "this is a " + doc.type + " response and a " + opts.expectedType + " one was expected -- a response " +
1787
+ "from the other ceremony (WebAuthn sec. 7.1 step 8 / sec. 7.2 step 11)");
1788
+ }
1789
+ checked.type = true;
1790
+ }
1791
+ if (opts.expectedChallenge !== undefined) {
1792
+ if (!Buffer.isBuffer(opts.expectedChallenge) && !(opts.expectedChallenge instanceof Uint8Array)) {
1793
+ throw _err("webauthn/bad-input", "opts.expectedChallenge must be the raw challenge bytes");
1794
+ }
1795
+ if (!guard.crypto.constantTimeEqual(Buffer.from(opts.expectedChallenge), challenge)) {
1796
+ throw _err("webauthn/client-data-mismatch",
1797
+ "the clientDataJSON challenge is not the one this ceremony issued (WebAuthn sec. 7.1 step 9 / sec. 7.2 step 12)");
1798
+ }
1799
+ checked.challenge = true;
1800
+ }
1801
+ if (opts.expectedOrigin !== undefined) {
1802
+ var allowed = Array.isArray(opts.expectedOrigin) ? opts.expectedOrigin : [opts.expectedOrigin];
1803
+ if (!allowed.length || !allowed.every(function (o) { return typeof o === "string" && o.length; })) {
1804
+ throw _err("webauthn/bad-input", "opts.expectedOrigin must be a non-empty origin string, or an array of them");
1805
+ }
1806
+ // Compared WHOLE and case-sensitively as the serialization it is. A prefix or
1807
+ // suffix test is how "https://example.com.attacker.tld" passes for
1808
+ // "https://example.com".
1809
+ if (allowed.indexOf(doc.origin) === -1) {
1810
+ throw _err("webauthn/client-data-mismatch",
1811
+ "the clientDataJSON origin " + JSON.stringify(doc.origin) + " is not one this relying party accepts " +
1812
+ "(WebAuthn sec. 7.1 step 10 / sec. 7.2 step 13)");
1813
+ }
1814
+ checked.origin = true;
1815
+ }
1816
+ return {
1817
+ type: doc.type, challenge: challenge, origin: doc.origin,
1818
+ crossOrigin: doc.crossOrigin === undefined ? false : doc.crossOrigin,
1819
+ topOrigin: doc.topOrigin === undefined ? null : doc.topOrigin,
1820
+ checked: checked,
1821
+ };
1822
+ }
1823
+ var _CLIENT_DATA_OPTS = Object.assign(Object.create(null), {
1824
+ expectedType: 1, expectedChallenge: 1, expectedOrigin: 1,
1825
+ });
1826
+
1827
+ // ---- public: parseAuthenticatorData / verifyAssertion ------------------------
1828
+
1829
+ /**
1830
+ * @primitive pki.webauthn.parseAuthenticatorData
1831
+ * @signature pki.webauthn.parseAuthenticatorData(bytes) -> { rpIdHash, flags, signCount, aaguid, credentialId, credentialPublicKey, extensions }
1832
+ * @since 0.5.0
1833
+ * @status experimental
1834
+ * @spec W3C WebAuthn Level 3 sec. 6.1
1835
+ * @related pki.webauthn.verifyAssertion, pki.webauthn.parseAttestationObject
1836
+ *
1837
+ * Decode a BARE authenticatorData, fail-closed -- the form an authentication
1838
+ * assertion returns, with no attestation-object wrapper around it. Same parser the
1839
+ * registration path uses: the 37-byte minimum, the reserved (RFU) flag bits, the
1840
+ * Backup State / Backup Eligibility rule, the 1..1023 credentialId bound, a
1841
+ * credential public key that must be one well-formed COSE_Key, and extensions that
1842
+ * must be exactly one CBOR map when the ED flag is set and absent when it is clear.
1843
+ * `flags` is decoded to `{ up, uv, be, bs, at, ed }`. An assertion normally has the
1844
+ * AT flag clear, so `aaguid` / `credentialId` / `credentialPublicKey` are null.
1845
+ * Malformed input throws `webauthn/bad-auth-data`.
1846
+ *
1847
+ * @example
1848
+ * // requires: `authenticatorData` -- credential.response.authenticatorData from
1849
+ * // navigator.credentials.get()
1850
+ * var ad = pki.webauthn.parseAuthenticatorData(authenticatorData);
1851
+ * ad.flags.up; // true when the user was present
1852
+ * ad.signCount; // the authenticator's counter for this credential
1853
+ */
1854
+ function parseAuthenticatorData(bytes) {
1855
+ if (!Buffer.isBuffer(bytes) && !(bytes instanceof Uint8Array)) {
1856
+ throw _err("webauthn/bad-input", "authenticatorData must be a Buffer or Uint8Array");
1857
+ }
1858
+ return _parseAuthData(guard.bytes.snapshotSource(bytes, WebauthnError, "webauthn/bad-input", "authenticatorData"), _err);
1859
+ }
1860
+
1861
+ // The options pki.webauthn.verifyAssertion recognises, null-prototype for the same
1862
+ // reason _VERIFY_OPTS is.
1863
+ var _ASSERT_OPTS = Object.assign(Object.create(null), {
1864
+ authenticatorData: 1, clientDataHash: 1, clientDataJSON: 1, signature: 1,
1865
+ credentialPublicKey: 1, previousSignCount: 1,
1866
+ expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
1867
+ expectedChallenge: 1, expectedOrigin: 1,
1868
+ });
1869
+
1870
+ /**
1871
+ * @primitive pki.webauthn.verifyAssertion
1872
+ * @signature pki.webauthn.verifyAssertion(input) -> Promise<{ signatureVerified, signCount, signCountChecked, flags, rpIdHash, bindingChecked }>
1873
+ * @since 0.5.0
1874
+ * @status experimental
1875
+ * @spec W3C WebAuthn Level 3 sec. 7.2
1876
+ * @defends webauthn-assertion-forgery (CWE-347)
1877
+ * @related pki.webauthn.parseAuthenticatorData, pki.webauthn.verify
1878
+ *
1879
+ * Verify an authentication assertion's signature: the authenticator signs
1880
+ * `authenticatorData || SHA-256(clientDataJSON)` as RAW bytes with the credential
1881
+ * key registered earlier -- no COSE_Sign1 wrapper, so a COSE message verifier is
1882
+ * the wrong tool and fails on structure before it ever reaches the signature. An
1883
+ * ES256 assertion signature is an ASN.1 DER `SEQUENCE { r, s }`, converted here
1884
+ * with the same order-aware reader the attestation path uses, so an r or s outside
1885
+ * `[1, n-1]` is refused rather than normalized.
1886
+ *
1887
+ * `signatureVerified`, not `verified`: this establishes that the holder of the
1888
+ * registered credential key produced this response. What makes the response
1889
+ * ACCEPTABLE is the sec. 7.2 binding, and the caller owns most of it. Supply
1890
+ * `expectedRpId`, `requireUserPresence`, `requireUserVerification` and
1891
+ * `allowedAlgorithms` and they are checked here -- `bindingChecked` reports which
1892
+ * ones ran, so a check that passed is distinguishable from one that never
1893
+ * happened. The CHALLENGE and the ORIGIN stay with the caller: both live in
1894
+ * clientDataJSON and are compared against state only the relying party has.
1895
+ *
1896
+ * Pass `previousSignCount` (the value stored at registration or the last login) and
1897
+ * the sec. 7.2 step 21 counter rule is applied: a counter that fails to advance is
1898
+ * a cloned authenticator and is refused, except for the `0`/`0` case an
1899
+ * authenticator that does not implement a counter reports. Without it the counter
1900
+ * is surfaced and not judged, and `signCountChecked` says so.
1901
+ *
1902
+ * @opts
1903
+ * authenticatorData -- the raw bytes from the assertion (Buffer)
1904
+ * clientDataJSON -- the raw clientDataJSON bytes; its SHA-256 is what the signature covers
1905
+ * clientDataHash -- the 32-byte digest instead, when the caller already has it
1906
+ * signature -- the assertion signature bytes
1907
+ * credentialPublicKey -- the stored COSE key (as parseAttestationObject surfaced it)
1908
+ * previousSignCount -- the stored counter, enabling the sec. 7.2 step 21 rule
1909
+ * expectedRpId, requireUserPresence, requireUserVerification, allowedAlgorithms -- the bindings above
1910
+ *
1911
+ * @example
1912
+ * // requires: `assertion` -- credential.response from navigator.credentials.get();
1913
+ * // `storedKey` -- the COSE credentialPublicKey kept at registration
1914
+ * var res = await pki.webauthn.verifyAssertion({
1915
+ * authenticatorData: assertion.authenticatorData,
1916
+ * clientDataJSON: assertion.clientDataJSON,
1917
+ * signature: assertion.signature,
1918
+ * credentialPublicKey: storedKey,
1919
+ * expectedRpId: "example.com", requireUserPresence: true,
1920
+ * });
1921
+ * res.signatureVerified; // true
1922
+ * res.bindingChecked.rpId; // true -- the rpIdHash matched example.com
1923
+ * // the challenge and origin in clientDataJSON are still yours to compare
1924
+ */
1925
+ // A private copy of the assertion inputs. Only what the caller can still write to
1926
+ // is copied: the byte fields, the descriptor itself, and the option arrays. The
1927
+ // COSE key is a decoded object the caller supplies from its own storage; it is
1928
+ // shallow-copied so a swapped field cannot reach the verification, which is the
1929
+ // mutation this can defend against without deep-copying an arbitrary object.
1930
+ function _snapshotAssertion(input) {
1931
+ if (!_isPlainObject(input)) throw _err("webauthn/bad-input", "pki.webauthn.verifyAssertion takes an options object");
1932
+ guard.identifier.assertKnownKeys(input, _ASSERT_OPTS, _err, "webauthn/bad-input", "verifyAssertion input has an unknown key ");
1933
+ var out = {}, k;
1934
+ for (k in input) { if (Object.prototype.hasOwnProperty.call(input, k)) out[k] = input[k]; }
1935
+ ["authenticatorData", "clientDataJSON", "clientDataHash", "signature", "expectedChallenge"].forEach(function (f) {
1936
+ if (Buffer.isBuffer(out[f]) || out[f] instanceof Uint8Array || out[f] instanceof ArrayBuffer) {
1937
+ out[f] = guard.bytes.snapshotSource(out[f], WebauthnError, "webauthn/bad-input", f);
1938
+ }
1939
+ });
1940
+ if (_isPlainObject(out.credentialPublicKey)) {
1941
+ var key = {}, kk;
1942
+ for (kk in out.credentialPublicKey) {
1943
+ if (!Object.prototype.hasOwnProperty.call(out.credentialPublicKey, kk)) continue;
1944
+ var v = out.credentialPublicKey[kk];
1945
+ key[kk] = (Buffer.isBuffer(v) || v instanceof Uint8Array) ? Buffer.from(v) : v;
1946
+ }
1947
+ out.credentialPublicKey = key;
1948
+ }
1949
+ if (Array.isArray(out.allowedAlgorithms)) out.allowedAlgorithms = out.allowedAlgorithms.slice();
1950
+ if (Array.isArray(out.expectedOrigin)) out.expectedOrigin = out.expectedOrigin.slice();
1951
+ return out;
1952
+ }
1953
+
1954
+ function verifyAssertion(input) {
1955
+ // Every byte this verdict depends on is COPIED here, synchronously, before
1956
+ // anything defers. The descriptor is the caller's object and the buffers in it
1957
+ // are the caller's memory: a caller that reuses or zeroizes them on the line
1958
+ // after this call has already changed them before the deferred body would run,
1959
+ // and an assertion that did not verify could be replaced by one that does. The
1960
+ // same rule pki.cmc.verify applies to a response, for the same reason.
1961
+ var frozen;
1962
+ try { frozen = _snapshotAssertion(input); } catch (e) { return Promise.reject(e); }
1963
+ return Promise.resolve().then(function () {
1964
+ input = frozen;
1965
+ var authData = parseAuthenticatorData(input.authenticatorData);
1966
+ if (!Buffer.isBuffer(input.signature) && !(input.signature instanceof Uint8Array)) {
1967
+ throw _err("webauthn/bad-input", "signature must be a Buffer or Uint8Array");
1968
+ }
1969
+ // The two forms of the same input, and NEITHER is inferred from the other's
1970
+ // absence: supplying both invites them to disagree, and picking one would make
1971
+ // the signature cover something the caller did not mean.
1972
+ var haveJson = input.clientDataJSON !== undefined, haveHash = input.clientDataHash !== undefined;
1973
+ if (haveJson === haveHash) {
1974
+ throw _err("webauthn/bad-input", "verifyAssertion takes exactly one of clientDataJSON or clientDataHash");
1975
+ }
1976
+ var clientDataHash, clientData = null;
1977
+ if (haveJson) {
1978
+ if (!Buffer.isBuffer(input.clientDataJSON) && !(input.clientDataJSON instanceof Uint8Array)) {
1979
+ throw _err("webauthn/bad-input", "clientDataJSON must be a Buffer or Uint8Array (the RAW bytes, not a parsed object)");
1980
+ }
1981
+ // Given the JSON, this verb reads it -- the ceremony TYPE is checked
1982
+ // unconditionally, because which ceremony a response belongs to is fixed by
1983
+ // the spec rather than chosen by the caller, and a registration response
1984
+ // replayed as a login is exactly what that check stops. The challenge and
1985
+ // origin are checked when the caller supplies what it issued.
1986
+ clientData = parseClientData(input.clientDataJSON, {
1987
+ expectedType: "webauthn.get",
1988
+ expectedChallenge: input.expectedChallenge,
1989
+ expectedOrigin: input.expectedOrigin,
1990
+ });
1991
+ clientDataHash = _sha("sha256", Buffer.from(input.clientDataJSON));
1992
+ } else {
1993
+ if (input.expectedChallenge !== undefined || input.expectedOrigin !== undefined) {
1994
+ throw _err("webauthn/bad-input",
1995
+ "expectedChallenge / expectedOrigin are checked against clientDataJSON, which this call did not " +
1996
+ "supply -- pass clientDataJSON instead of clientDataHash, or check them yourself");
1997
+ }
1998
+ if (!Buffer.isBuffer(input.clientDataHash) || input.clientDataHash.length !== 32) {
1999
+ throw _err("webauthn/bad-input", "clientDataHash must be a 32-byte SHA-256 digest");
2000
+ }
2001
+ clientDataHash = Buffer.from(input.clientDataHash);
2002
+ }
2003
+ var coseKey = input.credentialPublicKey;
2004
+ if (!_isPlainObject(coseKey)) {
2005
+ throw _err("webauthn/bad-input", "credentialPublicKey must be the stored COSE key object");
2006
+ }
2007
+ var bindingChecked = _applyBindings(authData, coseKey, input);
2008
+ // The counter's SHAPE is a config-time question and is answered here; whether it
2009
+ // advanced is a question about the message, and that is asked only after the
2010
+ // signature holds -- see below.
2011
+ var prev = input.previousSignCount;
2012
+ if (prev !== undefined && (typeof prev !== "number" || !Number.isSafeInteger(prev) || prev < 0 || prev > 0xFFFFFFFF)) {
2013
+ throw _err("webauthn/bad-input", "previousSignCount must be an integer in 0..4294967295");
2014
+ }
2015
+ // The signed message is the CONCATENATION, in that order, of the
2016
+ // authenticatorData exactly as it arrived and the clientDataJSON digest.
2017
+ var message = Buffer.concat([Buffer.from(input.authenticatorData), clientDataHash]);
2018
+ var spki = _coseKeyToSpki(coseKey);
2019
+ return _verifySig(coseKey.alg, Buffer.from(input.signature), spki, message, _err).then(function (ok) {
2020
+ if (!ok) {
2021
+ throw _err("webauthn/bad-signature",
2022
+ "the assertion signature does not verify under the stored credential public key (WebAuthn sec. 7.2 step 20)");
2023
+ }
2024
+ // sec. 7.2 step 21, and only NOW. A counter that fails to advance means two
2025
+ // authenticators hold one credential -- an alarm a relying party acts on, by
2026
+ // locking the account or revoking the credential. Judging it before the
2027
+ // signature would let anyone raise that alarm with arbitrary bytes, so the
2028
+ // counter is read only out of an assertion that proved to be authentic. The
2029
+ // 0/0 case is an authenticator that implements no counter, which is permitted.
2030
+ var signCountChecked = false;
2031
+ if (prev !== undefined) {
2032
+ if (!(prev === 0 && authData.signCount === 0) && authData.signCount <= prev) {
2033
+ throw _err("webauthn/sign-count-not-advanced",
2034
+ "the assertion signCount " + authData.signCount + " does not advance past the stored " + prev +
2035
+ ", which is the signal of a cloned authenticator (WebAuthn sec. 7.2 step 21)");
2036
+ }
2037
+ signCountChecked = true;
2038
+ }
2039
+ return {
2040
+ signatureVerified: true,
2041
+ signCount: authData.signCount,
2042
+ signCountChecked: signCountChecked,
2043
+ flags: authData.flags,
2044
+ rpIdHash: authData.rpIdHash,
2045
+ extensions: authData.extensions,
2046
+ // The decoded clientData when this call was given the JSON, so a caller that
2047
+ // still owes itself a comparison has the values without re-parsing; null
2048
+ // when only the digest was supplied and there was nothing to read.
2049
+ clientData: clientData,
2050
+ bindingChecked: bindingChecked,
2051
+ };
2052
+ });
2053
+ });
2054
+ }
2055
+
1190
2056
  module.exports = {
1191
2057
  parseAttestationObject: parseAttestationObject,
2058
+ parseAuthenticatorData: parseAuthenticatorData,
2059
+ parseClientData: parseClientData,
1192
2060
  verify: verify,
2061
+ verifyAssertion: verifyAssertion,
1193
2062
  verifyMetadataBlob: mds.verifyMetadataBlob,
1194
2063
  metadataFor: mds.metadataFor,
1195
2064
  metadataAnchors: mds.metadataAnchors,