@blamejs/pki 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/webauthn.js CHANGED
@@ -34,7 +34,6 @@ var validator = require("./validator-all");
34
34
  var edwardsPoint = require("./edwards-point");
35
35
  var guard = require("./guard-all");
36
36
  var jose = require("./jose");
37
- var pathValidate = require("./path-validate");
38
37
  var mds = require("./webauthn-mds");
39
38
  var nodeCrypto = require("crypto");
40
39
 
@@ -73,7 +72,7 @@ function _isInteger(node) { return !!node && !node.constructed && node.tagClass
73
72
  // step rejects the attestation before the signature is evaluated. EdDSA (-8/-19/-53) is
74
73
  // absent by design: a TPM 2.0 AIK never signs with EdDSA, so such an attestation is
75
74
  // correctly refused.
76
- var COSE_ALG_HASH = { "-7": "sha256", "-9": "sha256", "-257": "sha256", "-37": "sha256", "-35": "sha384", "-51": "sha384", "-258": "sha384", "-36": "sha512", "-52": "sha512", "-259": "sha512", "-65535": "sha1" };
75
+ var COSE_ALG_HASH = { "-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" };
77
76
  function _coseAlgHash(alg, E) {
78
77
  var h = COSE_ALG_HASH[String(alg)];
79
78
  if (!h) throw E("webauthn/unsupported-algorithm", "no hash mapping for COSE algorithm " + alg);
@@ -141,7 +140,51 @@ function _parseAuthData(buf, E) {
141
140
  // The complete COSE credential-key conformance rule set (kty/alg/crv/length/canonical/
142
141
  // profile/on-curve) lives in validator-cose, composed here so every credential key
143
142
  // routes through the one home -- never a per-format re-derivation of a partial subset.
144
- function _decodeCoseKey(node) { return validator.cose.credentialKey(node, WebauthnError, "webauthn/bad-cose-key"); }
143
+ function _decodeCoseKey(node) {
144
+ return validator.cose.credentialKey(node, WebauthnError, "webauthn/bad-cose-key", "webauthn/unsupported-algorithm");
145
+ }
146
+
147
+ /**
148
+ * @primitive pki.webauthn.parseCoseKey
149
+ * @signature pki.webauthn.parseCoseKey(bytes) -> object
150
+ * @since 0.5.2
151
+ * @status stable
152
+ * @spec RFC 9052, W3C WebAuthn Level 3 sec. 6.5.1
153
+ * @related pki.webauthn.verify, pki.webauthn.verifyAssertion
154
+ *
155
+ * Decode a bare COSE_Key -- the credential public key a relying party stored at
156
+ * registration -- back into the object `verifyAssertion` takes. `pki.webauthn.verify`
157
+ * returns that object, but the durable form is bytes: the object carries `Buffer`
158
+ * values, so a JSON round trip through a datastore yields
159
+ * `{"type":"Buffer","data":[...]}` rather than the object that went in, and existing
160
+ * credential stores already hold COSE bytes whoever wrote them. Without this the only
161
+ * routes into the decoder were `parseAttestationObject` and `parseAuthenticatorData`,
162
+ * both of which parse a CONTAINING structure -- so recovering a stored key meant
163
+ * fabricating an authenticatorData that never existed.
164
+ *
165
+ * The same validation the attestation path applies: the key type, the algorithm, the
166
+ * curve, and the coordinates are checked, and anything that is not a credential COSE
167
+ * key is refused with `webauthn/bad-cose-key`. `verifyAssertion` accepts either form
168
+ * for `credentialPublicKey`, so calling this first is a convenience rather than a step.
169
+ *
170
+ * @example
171
+ * // requires: `attestationObject` / `clientDataHash` -- what a browser returns from a
172
+ * // registration ceremony
173
+ * var reg = await pki.webauthn.verify(attestationObject, clientDataHash, {});
174
+ * var stored = reg.credentialPublicKeyBytes; // the form a credential row holds
175
+ * // ... at a login months later, read it back:
176
+ * var key = pki.webauthn.parseCoseKey(stored);
177
+ * key.alg; // -> -7 for ES256
178
+ * // verifyAssertion takes either form, so this parse is a convenience, not a step:
179
+ * // pass `stored` straight as its credentialPublicKey.
180
+ */
181
+ function parseCoseKey(bytes) {
182
+ var buf = _bytesArg(bytes, "the COSE key");
183
+ var node;
184
+ try { node = cbor.decode(buf); }
185
+ catch (e) { throw _err("webauthn/bad-cose-key", "the stored credential key is not decodable CBOR", e); }
186
+ return _decodeCoseKey(node);
187
+ }
145
188
 
146
189
  // ---- signature verification bridge ------------------------------------------
147
190
 
@@ -165,7 +208,11 @@ var COSE_ALG = {
165
208
  "-257": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
166
209
  "-258": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
167
210
  "-259": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
211
+ // RSASSA-PSS. The salt length is the hash length, the profile WebCrypto verifies and the one
212
+ // RFC 8230 sec. 2 fixes for the COSE PS* identifiers -- 32 / 48 / 64 bytes for SHA-256/384/512.
168
213
  "-37": { imp: { name: "RSA-PSS", hash: "SHA-256" }, verify: { name: "RSA-PSS", saltLength: 32 }, ecdsa: 0 },
214
+ "-38": { imp: { name: "RSA-PSS", hash: "SHA-384" }, verify: { name: "RSA-PSS", saltLength: 48 }, ecdsa: 0 },
215
+ "-39": { imp: { name: "RSA-PSS", hash: "SHA-512" }, verify: { name: "RSA-PSS", saltLength: 64 }, ecdsa: 0 },
169
216
  // RS1 (RSASSA-PKCS1-v1_5 / SHA-1): a legacy COSE algorithm real Windows Hello TPM
170
217
  // authenticators emit in their attestation statement. VERIFY-only support -- the
171
218
  // toolkit never signs with SHA-1; it must still evaluate the attestations that
@@ -193,7 +240,7 @@ function _verifySig(alg, sig, spkiBytes, message, E) {
193
240
  // low-order (e.g. all-zeroes) key verifies a trivial signature -- so validate the OKP
194
241
  // point before verify. This covers EVERY key that signs a WebAuthn statement: the x5c
195
242
  // attestation-certificate key (packed/tpm/apple) AND the self-attestation credential key.
196
- if (imp.name === "Ed25519" || imp.name === "Ed448") _requireValidEdPoint(spkiBytes, imp.name, E);
243
+ if (imp.name === "Ed25519" || imp.name === "Ed448") _requireValidEdPoint(spkiBytes, imp.name);
197
244
  var s = d.ecdsa ? _derEcdsaToRaw(sig, d.imp.namedCurve) : sig;
198
245
  return subtle.importKey("spki", spkiBytes, imp, false, ["verify"])
199
246
  .then(function (key) { return subtle.verify(ver, key, s, message); })
@@ -211,17 +258,14 @@ function _edName(spkiBytes, E) {
211
258
  if (!nm) throw E("webauthn/unsupported-algorithm", "unsupported EdDSA curve OID " + algOid);
212
259
  return nm;
213
260
  }
214
- // The raw Edwards point an OKP SPKI carries (its BIT STRING body, past the unused-bits
215
- // octet) MUST be a valid, full-order point -- reject an off-curve or low-order key before it
216
- // verifies a signature (WebCrypto import does not check it). Curve from the WebCrypto name.
217
- function _requireValidEdPoint(spkiBytes, name, E) {
218
- var content;
219
- try { content = asn1.decode(spkiBytes).children[1].content; }
220
- catch (e) { throw E("webauthn/bad-signature", "the EdDSA public key is not a well-formed SPKI", e); }
221
- var point = content && content.length ? content.subarray(1) : Buffer.alloc(0);
222
- if (!edwardsPoint.validate(point, name === "Ed25519" ? 6 : 7)) {
223
- throw E("webauthn/bad-signature", "the EdDSA public key is not a valid, full-order Edwards point");
224
- }
261
+ // The raw Edwards point an OKP SPKI carries MUST be a valid, full-order point -- reject an
262
+ // off-curve or low-order key before it verifies a signature, which WebCrypto import will not do.
263
+ // Routed through edwards-point's own SPKI entry rather than extracting the point here: that module
264
+ // documents itself as the one home every EdDSA verify path takes an SPKI through, and this was the
265
+ // path that had its own copy. Keeping a second extraction is how one route comes to be guarded and
266
+ // the next one written is not.
267
+ function _requireValidEdPoint(spkiBytes, name) {
268
+ edwardsPoint.validateSpki(spkiBytes, name === "Ed25519" ? 6 : 7, WebauthnError, "webauthn/bad-signature");
225
269
  }
226
270
 
227
271
  // A validated COSE credential key -> a self-contained SPKI the WebCrypto import
@@ -244,11 +288,39 @@ function _certEcCurveOid(cert, E) {
244
288
  try { return asn1.read.oid(asn1.decode(params)); }
245
289
  catch (e) { throw E("webauthn/key-mismatch", "the attestation certificate EC curve is not a valid OBJECT IDENTIFIER", e); }
246
290
  }
291
+ // Which certificate key ALGORITHMS a credential key of each COSE type may be carried by. Two keys
292
+ // are the same key only if they are the same KIND of key: the bytes alone do not say what a key is,
293
+ // and for the Edwards curves they cannot -- an X25519 key-agreement key and an Ed25519 signing key
294
+ // are both 32 raw bytes, so a certificate declaring the former would otherwise compare equal to a
295
+ // credential key declaring the latter and be accepted as its attestation certificate. The EC2
296
+ // branch already asked this question as a curve check; asking it only there left the two key types
297
+ // whose confusion is undetectable from the material to be decided on the material alone.
298
+ // A row per COSE key type, keyed the way every other algorithm decision in this toolkit is.
299
+ function _certKeyAlgNames(cose) {
300
+ if (cose.kty === 2) return ["ecPublicKey"];
301
+ // An RSA key may be carried under the general OID or under id-RSASSA-PSS, which restricts it to
302
+ // PSS but is the same key (RFC 4055 sec. 1.2).
303
+ if (cose.kty === 3) return ["rsaEncryption", "rsassaPss"];
304
+ if (cose.kty === 1) {
305
+ var okp = validator.cose.OKP_CRV[cose.crv];
306
+ return okp ? [okp.oid] : [];
307
+ }
308
+ return [];
309
+ }
310
+ function _assertCertKeyAlgorithm(cert, cose, E) {
311
+ var alg = (cert.subjectPublicKeyInfo && cert.subjectPublicKeyInfo.algorithm) || {};
312
+ var want = _certKeyAlgNames(cose);
313
+ if (!want.length || want.indexOf(alg.name) < 0) {
314
+ throw E("webauthn/key-mismatch", "the attestation certificate carries a " + JSON.stringify(alg.name) +
315
+ " key, which is not the kind of key the credential public key declares");
316
+ }
317
+ }
247
318
  // A void assert: throws webauthn/key-mismatch on any inequality, returns nothing on
248
319
  // success (called for its throw side-effect, like the other _check* asserts).
249
320
  function _certPubKeyEqualsCose(cert, cose, E) {
250
321
  var raw = cert.subjectPublicKeyInfo && cert.subjectPublicKeyInfo.publicKey && cert.subjectPublicKeyInfo.publicKey.bytes;
251
322
  if (!raw) throw E("webauthn/key-mismatch", "the attestation certificate exposes no public key");
323
+ _assertCertKeyAlgorithm(cert, cose, E);
252
324
  if (cose.kty === 2) {
253
325
  // The certificate's declared EC curve MUST equal the credential key's curve --
254
326
  // a curve substitution is a different key even if the coordinate bytes line up.
@@ -408,8 +480,15 @@ var _VERIFY_OPTS = Object.assign(Object.create(null), {
408
480
  time: 1, metadata: 1, tpmPolicy: 1, safetyNetRoots: 1, verifySafetyNetJws: 1, requireCtsProfileMatch: 1,
409
481
  expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
410
482
  rootCertificates: 1,
483
+ clientDataJSON: 1, expectedChallenge: 1, expectedOrigin: 1, expectedTopOrigin: 1,
411
484
  });
412
485
 
486
+ // The boolean switches whose only reader sits inside ONE format's arm, so nothing else would ever
487
+ // examine them. The binding switches are absent because _applyBindings owns their type and runs on
488
+ // every format and on the assertion path too -- a second copy of that rule here would be a second
489
+ // place for it to drift.
490
+ var _FORMAT_SCOPED_BOOLEAN_OPTS = ["verifySafetyNetJws", "requireCtsProfileMatch"];
491
+
413
492
  // Anchor an attestation's trust path to roots the CALLER pins. The metadata route
414
493
  // resolves an authenticator's roots from the catalogue that registered it, which is
415
494
  // the stronger source -- but it only reaches models the catalogue lists, and some
@@ -464,22 +543,34 @@ function _cloneParsed(v, depth) {
464
543
  }
465
544
 
466
545
  // 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) {
546
+ // rewrite, and accepted in every form the W3C BufferSource contract defines.
547
+ //
548
+ // ONE accepted set, for every byte argument on this namespace. The forms differed per argument
549
+ // before -- an attestation object took an ArrayBuffer while the clientDataHash beside it took only
550
+ // a Buffer -- and the caller producing both is the same one: `crypto.subtle.digest` returns an
551
+ // ArrayBuffer, so the natural way to compute that hash produced a value this verb refused. Which
552
+ // forms an argument accepts is not a per-argument decision; it is the namespace's contract, and it
553
+ // is made once here.
554
+ //
555
+ // Anything that is not a BufferSource is refused BY NAME, rather than passed down to be described
556
+ // by whichever parser reaches it first.
557
+ function _isBufferSource(v) { return ArrayBuffer.isView(v) || v instanceof ArrayBuffer; }
558
+ function _bytesArg(v, label) {
559
+ if (_isBufferSource(v)) {
473
560
  return guard.bytes.snapshotSource(v, WebauthnError, "webauthn/bad-input", label);
474
561
  }
475
- return v;
562
+ throw _err("webauthn/bad-input", label + " must be a BufferSource (a Buffer, a typed-array view, or an ArrayBuffer)");
476
563
  }
477
564
 
478
565
  function _snapshotRoots(supplied) {
479
566
  if (!Array.isArray(supplied)) return supplied;
480
567
  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[]");
568
+ // Every byte form, tested BEFORE the parsed-object branch: a DataView is an object too, so a
569
+ // narrower byte test does not merely miss the copy -- it sends those bytes down the branch for
570
+ // parsed certificates, which deep-copies a typed array field by field into something that is no
571
+ // longer a certificate at all.
572
+ if (_isBufferSource(root)) {
573
+ return guard.bytes.snapshotSource(root, WebauthnError, "webauthn/bad-input", "opts.rootCertificates[]");
483
574
  }
484
575
  if (root && typeof root === "object") return _cloneParsed(root, 0);
485
576
  return root; // a PEM string is immutable
@@ -502,7 +593,7 @@ function _applyCallerRoots(res, supplied, vopts, onlyPaths) {
502
593
  // The same three forms opts.safetyNetRoots takes, since it is the same question.
503
594
  var roots = supplied.map(function (root, i) {
504
595
  var cert;
505
- try { cert = (Buffer.isBuffer(root) || typeof root === "string") ? x509.parse(root) : root; }
596
+ try { cert = _isBufferSource(root) ? x509.parse(guard.bytes.source(root, WebauthnError, "webauthn/bad-input", "opts.rootCertificates[" + i + "]")) : (typeof root === "string" ? x509.parse(root) : root); }
506
597
  catch (e) { throw _err("webauthn/bad-input", "opts.rootCertificates[" + i + "] is not a decodable certificate", e); }
507
598
  if (!cert || !cert.subject || !cert.subjectPublicKeyInfo) {
508
599
  throw _err("webauthn/bad-input", "opts.rootCertificates[" + i + "] is not a certificate");
@@ -598,10 +689,6 @@ function _anchoredRoutes(res, base) {
598
689
  // reports which of them actually ran. The challenge and the origin stay with the
599
690
  // caller: they live in clientDataJSON, which the caller already holds and compares
600
691
  // against state only it has.
601
- var _BINDING_OPTS = Object.assign(Object.create(null), {
602
- expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
603
- });
604
-
605
692
  function _assertBool(v, name) {
606
693
  if (typeof v !== "boolean") throw _err("webauthn/bad-input", "opts." + name + " must be a boolean");
607
694
  }
@@ -854,10 +941,8 @@ var VERIFIERS = {
854
941
  // existed. There is no bundled root and no trust-on-first-use.
855
942
  "android-safetynet": function (att, clientDataHash, opts) {
856
943
  opts = opts || {};
944
+ // The type is settled at the entry point, so what remains here is the opt-in itself.
857
945
  if (opts.verifySafetyNetJws !== true) {
858
- if (opts.verifySafetyNetJws !== undefined && typeof opts.verifySafetyNetJws !== "boolean") {
859
- throw _err("webauthn/bad-input", "opts.verifySafetyNetJws must be a boolean");
860
- }
861
946
  throw _err("webauthn/unsupported-format", "attestation statement format 'android-safetynet' is not supported");
862
947
  }
863
948
  var roots = opts.safetyNetRoots;
@@ -941,9 +1026,6 @@ var VERIFIERS = {
941
1026
  if (opts.requireCtsProfileMatch === true && signals.ctsProfileMatch !== true) {
942
1027
  throw _err("webauthn/safetynet-cts-profile", "the android-safetynet response reports ctsProfileMatch " + JSON.stringify(signals.ctsProfileMatch) + ", and opts.requireCtsProfileMatch demands true");
943
1028
  }
944
- if (opts.requireCtsProfileMatch !== undefined && typeof opts.requireCtsProfileMatch !== "boolean") {
945
- throw _err("webauthn/bad-input", "opts.requireCtsProfileMatch must be a boolean");
946
- }
947
1029
 
948
1030
  return _verifySig(-257, sigBytes, leaf.subjectPublicKeyInfo.bytes,
949
1031
  Buffer.from(segs[0] + "." + segs[1], "ascii"), _err).then(function (ok) {
@@ -1082,41 +1164,28 @@ function _safetyNetHostnameOk(leaf) {
1082
1164
  // rotation; the first that validates wins, and if none does the attestation is refused. The chain
1083
1165
  // goes through the full path validator rather than a signature-only walk, so an expired, revoked-by-
1084
1166
  // policy, or otherwise non-conforming intermediate cannot slip past on a signature alone.
1167
+ // The caller's roots are the only thing this function owns: their SHAPE is its own config-time
1168
+ // contract, so a root that is not a decodable certificate is named by its index here. Anchoring
1169
+ // itself is the namespace's one walk (webauthn-mds), which every other chain in this module already
1170
+ // reaches its anchors through -- including the anchor-stripping rule, which this had a slightly
1171
+ // different and weaker copy of. Each root is resolved BEFORE the walk starts, so a malformed entry
1172
+ // is a config fault whatever position it sits in, rather than one that only surfaces when the roots
1173
+ // before it happen to fail.
1085
1174
  function _safetyNetChainTrusted(chain, roots, time) {
1086
- var ordered = chain.slice().reverse(); // path.validate takes anchor-adjacent first
1087
- var when = time === undefined ? new Date() : time;
1088
- var attempts = roots.map(function (root, i) {
1089
- return function () {
1175
+ var anchors;
1176
+ try {
1177
+ anchors = roots.map(function (root, i) {
1090
1178
  var anchorCert;
1091
- try { anchorCert = Buffer.isBuffer(root) || typeof root === "string" ? x509.parse(root) : root; }
1179
+ try { anchorCert = _isBufferSource(root) ? x509.parse(guard.bytes.source(root, WebauthnError, "webauthn/bad-input", "opts.safetyNetRoots[" + i + "]")) : (typeof root === "string" ? x509.parse(root) : root); }
1092
1180
  catch (e) { throw _err("webauthn/bad-input", "opts.safetyNetRoots[" + i + "] is not a decodable certificate", e); }
1093
1181
  if (!anchorCert || !anchorCert.subject || !anchorCert.subjectPublicKeyInfo) {
1094
1182
  throw _err("webauthn/bad-input", "opts.safetyNetRoots[" + i + "] is not a certificate");
1095
1183
  }
1096
- // An x5c chain conventionally carries the root as its last entry. The anchor is supplied
1097
- // separately and is what establishes trust, so drop a trailing self-issued certificate that
1098
- // IS this anchor rather than validating it against itself as a path element.
1099
- var path = ordered.slice();
1100
- if (path.length > 1 && guard.name.dnEqual(path[0].subject, anchorCert.subject) &&
1101
- guard.name.dnEqual(path[0].issuer, path[0].subject)) {
1102
- path = path.slice(1);
1103
- }
1104
- return pathValidate.validate(path, {
1105
- time: when,
1106
- // The anchor's own KEY algorithm and its parameters, not the algorithm its issuer signed it
1107
- // with: the validator carries these forward as the working public key, and a certificate
1108
- // below the anchor may inherit its key parameters from them.
1109
- trustAnchor: { name: anchorCert.subject, publicKey: anchorCert.subjectPublicKeyInfo.bytes,
1110
- algorithm: anchorCert.subjectPublicKeyInfo.algorithm.oid,
1111
- parameters: anchorCert.subjectPublicKeyInfo.algorithm.parameters },
1112
- }).then(function (r) { return !!(r && r.valid); }, function () { return false; });
1113
- };
1114
- });
1115
- return attempts.reduce(function (p, next) {
1116
- return p.then(function (done) { return done ? true : next(); });
1117
- }, Promise.resolve(false)).then(function (trusted) {
1118
- if (!trusted) throw _err("webauthn/safetynet-cert-untrusted", "the android-safetynet x5c chain does not validate to any supplied root (opts.safetyNetRoots)");
1119
- });
1184
+ return anchorCert;
1185
+ });
1186
+ } catch (e) { return Promise.reject(e); }
1187
+ return mds.chainToAnchor(chain, anchors, time === undefined ? new Date() : time,
1188
+ "android-safetynet x5c certificate chain", "webauthn/safetynet-cert-untrusted");
1120
1189
  }
1121
1190
 
1122
1191
  // `chain` is the x5c order (leaf-first); trustPath is surfaced in pki.path.validate
@@ -1139,8 +1208,23 @@ function _result(fmt, attestationType, chain, att) {
1139
1208
  aaguid: att.authData.aaguid,
1140
1209
  credentialId: att.authData.credentialId,
1141
1210
  credentialPublicKey: att.authData.credentialPublicKey,
1211
+ // The same key in the form that SURVIVES STORAGE. The decoded object carries Buffers, so a JSON
1212
+ // round trip through a datastore returns {"type":"Buffer","data":[...]} rather than what went
1213
+ // in; the COSE bytes are what a credential row actually holds. Returning only the object left
1214
+ // the caller re-parsing the attestation object to recover bytes this call had already isolated.
1215
+ credentialPublicKeyBytes: att.authData.credentialPublicKeyBytes,
1142
1216
  signCount: att.authData.signCount,
1143
1217
  flags: att.authData.flags,
1218
+ // The RP ID hash and the authenticator extension outputs, for the same reason the key bytes
1219
+ // above are here: this call decoded them and the caller needs them. Extension outputs arrive
1220
+ // at REGISTRATION and nowhere else -- credProtect (whether this credential is
1221
+ // user-verification-required for the rest of its life), credProps.rk (whether it is
1222
+ // discoverable), credBlob, minPinLength, hmac-secret/prf -- so a relying party that must
1223
+ // persist them had to re-parse the attestation object to read values already in hand.
1224
+ // `verifyAssertion` returns both; the registration verdict returning less made the two halves
1225
+ // of one lifecycle disagree about what they hand back.
1226
+ rpIdHash: att.authData.rpIdHash,
1227
+ extensions: att.authData.extensions,
1144
1228
  };
1145
1229
  }
1146
1230
 
@@ -1176,7 +1260,7 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
1176
1260
 
1177
1261
  /**
1178
1262
  * @primitive pki.webauthn.verify
1179
- * @signature pki.webauthn.verify(attestationObject, clientDataHash, opts) -> Promise<{ attestationVerified, fmt, attestationType, trustPath, anchoredTo, anchoredElements, aaguid, credentialId, credentialPublicKey, signCount, flags, bindingChecked }>
1263
+ * @signature pki.webauthn.verify(attestationObject, clientDataHash?, opts?) -> Promise<{ attestationVerified, fmt, attestationType, trustPath, anchoredTo, aaguid, credentialId, credentialPublicKey, credentialPublicKeyBytes, signCount, flags, rpIdHash, extensions, bindingChecked, clientData }>
1180
1264
  * @since 0.2.5
1181
1265
  * @status stable
1182
1266
  * @spec W3C WebAuthn Level 3 sec. 8 / sec. 7.1
@@ -1184,11 +1268,19 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
1184
1268
  *
1185
1269
  * Verify a WebAuthn attestation statement: the attestation signature over
1186
1270
  * `authenticatorData || clientDataHash` and (for the x5c formats) the format's
1187
- * certificate requirements. `clientDataHash` is the SHA-256 of the serialized client
1188
- * data, supplied by the relying party. Resolves the attestation type + trust path or
1271
+ * certificate requirements. Resolves the attestation type + trust path or
1189
1272
  * throws a typed `webauthn/*` error; a signature that does not verify is a
1190
1273
  * `webauthn/verify-failed` verdict, never a silent pass.
1191
1274
  *
1275
+ * Give it the client data in exactly one of the two forms, neither inferred from the
1276
+ * other's absence: the raw `opts.clientDataJSON`, or the SHA-256 digest of it as the
1277
+ * second argument. Given the JSON, this reads it -- the ceremony TYPE is checked
1278
+ * unconditionally, because which ceremony a response belongs to is fixed by the
1279
+ * specification rather than chosen by a caller, and a login response replayed into a
1280
+ * registration is exactly what that check stops. The challenge, origin and top-level
1281
+ * origin are checked when you supply what you issued, and `clientData.checked` reports
1282
+ * which ran. Given only the digest, nothing reads it and `clientData` is null.
1283
+ *
1192
1284
  * The verdict field is `attestationVerified`, and the name is the point: a sound
1193
1285
  * attestation statement is not the same claim as an acceptable registration. The
1194
1286
  * statement says nothing about WHICH relying party asked for it, or whether a user
@@ -1196,12 +1288,20 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
1196
1288
  * clear, is perfectly sound and must not be registered. Supply `expectedRpId`,
1197
1289
  * `requireUserPresence`, `requireUserVerification` and `allowedAlgorithms` and those
1198
1290
  * 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.
1291
+ * told from one that never happened.
1292
+ *
1293
+ * Four more fields appear only where they mean something, rather than as nulls on every
1294
+ * verdict: `metadata` when the catalogue governed, `anchoredElements` when a trust path
1295
+ * was anchored, `compound` for a sec. 8.9 statement's per-element results, and
1296
+ * `safetyNet` with `chainValidatedAt` for an android-safetynet response.
1202
1297
  *
1203
1298
  * The verdict also carries what a relying party must STORE to run a later login:
1204
- * `credentialId`, `credentialPublicKey` and the initial `signCount`.
1299
+ * `credentialId`, `credentialPublicKey` and the initial `signCount`. The credential key
1300
+ * comes back in both forms: the decoded object, and `credentialPublicKeyBytes`, which is
1301
+ * what a credential row should hold -- the object carries `Buffer` values, so a JSON round
1302
+ * trip through a datastore returns `{"type":"Buffer","data":[...]}` rather than the object
1303
+ * that went in. `pki.webauthn.parseCoseKey` reads those bytes back, and
1304
+ * `verifyAssertion` accepts either form.
1205
1305
  *
1206
1306
  * @intro This verifies the attestation STATEMENT -- the signature and the format's
1207
1307
  * structural bindings (the x5c leaf key == credential key, the apple nonce, the tpm
@@ -1233,19 +1333,48 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
1233
1333
  * claim there is anything to anchor, so it is not a reason to refuse the statement,
1234
1334
  * but it does mean "anchored" covered fewer elements than the statement holds.
1235
1335
  *
1336
+ * @opts
1337
+ * clientDataJSON -- the RAW clientDataJSON bytes; supply this OR the digest argument
1338
+ * expectedChallenge -- the challenge bytes this ceremony issued (needs clientDataJSON)
1339
+ * expectedOrigin -- the origin string, or an array of acceptable origins
1340
+ * expectedTopOrigin -- the acceptable top-level origin(s), or null to require an
1341
+ * unframed ceremony
1342
+ * expectedRpId -- the RP ID whose SHA-256 the authenticatorData must carry
1343
+ * requireUserPresence / requireUserVerification -- the flags this registration requires
1344
+ * allowedAlgorithms -- the COSE algorithms this relying party accepts
1345
+ * metadata -- a verifyMetadataBlob result; the model's own roots govern
1346
+ * rootCertificates -- trust anchors you pin, for the models no catalogue lists
1347
+ * time -- the instant certificate validity is judged at
1348
+ * tpmPolicy -- required TPM key properties; refuses a non-TPM attestation
1349
+ * safetyNetRoots / verifySafetyNetJws / requireCtsProfileMatch -- the
1350
+ * android-safetynet opt-in, its Google roots, and the
1351
+ * device-integrity demand
1352
+ *
1236
1353
  * @example
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, {
1354
+ * // requires: `attestationObject` and `clientDataJSON` from
1355
+ * // navigator.credentials.create(), and `issuedChallenge` -- the bytes you sent
1356
+ * var res = await pki.webauthn.verify(attestationObject, {
1357
+ * clientDataJSON: clientDataJSON,
1358
+ * expectedChallenge: issuedChallenge,
1359
+ * expectedOrigin: "https://example.com",
1240
1360
  * expectedRpId: "example.com", requireUserPresence: true,
1241
1361
  * });
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,
1362
+ * res.attestationVerified; // true (statement signature + bindings hold)
1363
+ * res.clientData.checked.type; // true -- this really is a registration response
1364
+ * res.bindingChecked.rpId; // true -- this response names example.com
1365
+ * res.attestationType; // "Basic"
1366
+ * // store res.credentialId / res.credentialPublicKeyBytes / res.signCount for logins,
1246
1367
  * // and anchor res.trustPath to your pinned roots with pki.path.validate
1247
1368
  */
1248
1369
  function verify(attestationObject, clientDataHash, opts) {
1370
+ // Two call shapes, disjoint by shape so neither is guessed at: the digest positionally, or an
1371
+ // options object carrying the clientDataJSON it is computed from. A Buffer IS an object, so the
1372
+ // question has to be asked as "is this not bytes?" -- the disjointness is between a BufferSource
1373
+ // and everything else, and testing only the second half swallows the ordinary two-argument call.
1374
+ if (opts === undefined && clientDataHash !== undefined &&
1375
+ !_isBufferSource(clientDataHash) && _isPlainObject(clientDataHash)) {
1376
+ opts = clientDataHash; clientDataHash = undefined;
1377
+ }
1249
1378
  opts = opts || {};
1250
1379
  // Every option here either GATES the verdict or supplies the trust material a gate needs, so a
1251
1380
  // misspelled key is not a harmless no-op: `metdata` leaves the metadata gate switched off and the
@@ -1255,11 +1384,25 @@ function verify(attestationObject, clientDataHash, opts) {
1255
1384
  try {
1256
1385
  if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
1257
1386
  guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "webauthn/bad-input", "opts has an unknown key ");
1387
+ // ONE read of the caller's object, HERE, and nothing below ever touches it again. Not a
1388
+ // precaution about mutation across the deferred verification -- that was already the reason for
1389
+ // the copy this replaces -- but about the reads themselves: a gate that reads `opts.x` to decide
1390
+ // whether a demand was made, and a later step that reads `opts.x` again to act on it, are two
1391
+ // reads of a value the caller still owns, and an accessor makes them disagree. The value that
1392
+ // was CHECKED must be the value that is USED, which means checking and using the same copy.
1393
+ // `Object.assign` takes every own enumerable field by value at this instant, which resolves any
1394
+ // accessor exactly once.
1395
+ opts = Object.assign({}, opts);
1258
1396
  if (opts.time !== undefined) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
1397
+ // The boolean switches are typed HERE rather than in the arm that reads them. Validated only
1398
+ // inside the android-safetynet arm, `requireCtsProfileMatch: "true"` reaching any other format
1399
+ // is never examined at all -- a truthy string that demands nothing, silently accepted. A
1400
+ // recognised key with an unusable value is the same class of caller mistake as an unrecognised
1401
+ // key, and belongs at the same boundary.
1402
+ _FORMAT_SCOPED_BOOLEAN_OPTS.forEach(function (k) {
1403
+ if (opts[k] !== undefined && typeof opts[k] !== "boolean") throw _err("webauthn/bad-input", "opts." + k + " must be a boolean");
1404
+ });
1259
1405
  } catch (e) { return Promise.reject(e); }
1260
- if (!Buffer.isBuffer(clientDataHash) || clientDataHash.length !== 32) {
1261
- return Promise.reject(_err("webauthn/bad-input", "clientDataHash must be a 32-byte SHA-256 digest"));
1262
- }
1263
1406
  var att;
1264
1407
  // Both byte inputs are snapshotted BEFORE anything reads them, for the same reason the trust
1265
1408
  // anchors are: the attestation statement is not evaluated until a later promise turn, and both
@@ -1270,10 +1413,49 @@ function verify(attestationObject, clientDataHash, opts) {
1270
1413
  // the statement to this ceremony; a caller who overwrote it in that gap would have the signature
1271
1414
  // checked against a challenge and origin nobody agreed to, and the verdict would still report a
1272
1415
  // sound attestation. A DataView or ArrayBuffer comes in by the same door and is copied too.
1273
- var attBytes, cdh;
1416
+ var attBytes, cdh, clientData = null;
1274
1417
  try {
1275
- attBytes = _snapshotBytes(attestationObject, "attestationObject");
1276
- cdh = _snapshotBytes(clientDataHash, "clientDataHash");
1418
+ attBytes = _bytesArg(attestationObject, "attestationObject");
1419
+ // The two forms of the same input, and NEITHER is inferred from the other's absence -- the
1420
+ // same rule verifyAssertion applies, because it is the same question. Supplying both invites
1421
+ // them to disagree, and picking one would make the attestation cover something the caller did
1422
+ // not mean.
1423
+ var haveJson = opts.clientDataJSON !== undefined, haveHash = clientDataHash !== undefined;
1424
+ if (haveJson === haveHash) {
1425
+ throw _err("webauthn/bad-input", "verify takes exactly one of clientDataJSON or the clientDataHash argument");
1426
+ }
1427
+ if (haveJson) {
1428
+ // The bytes are taken ONCE and both the reading and the hashing use that copy. Checking one
1429
+ // value and hashing another would bind the attestation to client data the ceremony checks
1430
+ // never saw -- the check and the use have to be of the same bytes.
1431
+ var cdjBytes = _bytesArg(opts.clientDataJSON, "opts.clientDataJSON");
1432
+ // Given the JSON, the ceremony TYPE is checked unconditionally. Which ceremony a response
1433
+ // belongs to is fixed by the specification rather than chosen by the caller, and a login
1434
+ // response replayed into a registration is exactly what that check stops -- so registration
1435
+ // gets the same non-negotiable rule the login path already had. Without this door there was
1436
+ // no way to apply it at registration at all: the hash is opaque, and the caller was left to
1437
+ // compare a value the attestation never bound.
1438
+ clientData = parseClientData(cdjBytes, {
1439
+ expectedType: "webauthn.create",
1440
+ expectedChallenge: opts.expectedChallenge,
1441
+ expectedOrigin: opts.expectedOrigin,
1442
+ expectedTopOrigin: opts.expectedTopOrigin,
1443
+ });
1444
+ cdh = _sha("sha256", cdjBytes);
1445
+ } else {
1446
+ // Every expectation the clientData reader would have answered is unanswerable from a bare
1447
+ // digest, so each is refused rather than left silently uncompared.
1448
+ if (opts.expectedChallenge !== undefined || opts.expectedOrigin !== undefined ||
1449
+ opts.expectedTopOrigin !== undefined) {
1450
+ throw _err("webauthn/bad-input",
1451
+ "expectedChallenge / expectedOrigin / expectedTopOrigin are checked against clientDataJSON, which this call " +
1452
+ "did not supply -- pass opts.clientDataJSON instead of the clientDataHash argument, or check them yourself");
1453
+ }
1454
+ cdh = _bytesArg(clientDataHash, "clientDataHash");
1455
+ // The LENGTH is checked on the normalized copy, so the digest is 32 bytes whichever form it
1456
+ // arrived in -- not only when it arrived as a Buffer.
1457
+ if (cdh.length !== 32) throw _err("webauthn/bad-input", "clientDataHash must be a 32-byte SHA-256 digest");
1458
+ }
1277
1459
  } catch (e) { return Promise.reject(e); }
1278
1460
  attestationObject = attBytes;
1279
1461
  clientDataHash = cdh;
@@ -1293,9 +1475,17 @@ function verify(attestationObject, clientDataHash, opts) {
1293
1475
  // demanded a TPM-bound key would accept a `none` attestation instead. The requirement therefore
1294
1476
  // belongs at the dispatch, where it can refuse a format that cannot satisfy it, not in the arm
1295
1477
  // that only runs once that format was already chosen.
1296
- if (opts.tpmPolicy !== undefined && !_formatCanSatisfyTpmPolicy(att)) {
1478
+ if (opts.tpmPolicy !== undefined && !_formatCarries(att, "tpm")) {
1297
1479
  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"));
1298
1480
  }
1481
+ // The same rule, and the same reason, for the one other option that DEMANDS rather than supplies:
1482
+ // ctsProfileMatch is an android-safetynet device-integrity signal, so no other format's statement
1483
+ // carries one to test. Checked only inside that arm, a caller who demanded a CTS-matching device
1484
+ // would get a pass from a `packed` or `none` attestation that was never asked the question.
1485
+ // A caller who passes `false` demands nothing and is not refused.
1486
+ if (opts.requireCtsProfileMatch === true && !_formatCarries(att, "android-safetynet")) {
1487
+ return Promise.reject(_err("webauthn/safetynet-cts-profile", "opts.requireCtsProfileMatch requires an android-safetynet attestation, but this attestation is format '" + att.fmt + "', whose statement carries no device-integrity signals"));
1488
+ }
1299
1489
  // The ceremony bindings run BEFORE the statement is evaluated: a response
1300
1490
  // produced for another relying party, or without the user presence the caller
1301
1491
  // requires, is not a message this call should spend a signature verification on
@@ -1327,6 +1517,14 @@ function verify(attestationObject, clientDataHash, opts) {
1327
1517
  });
1328
1518
  } catch (e) { return Promise.reject(e); }
1329
1519
  return Promise.resolve().then(function () { return verifier(att, clientDataHash, vopts); })
1520
+ .then(function (res) {
1521
+ // What the clientData reader found, on the verdict rather than left for the caller to
1522
+ // recompute -- the same field the login verdict carries, for the same reason: `checked` is
1523
+ // where a comparison that ran is told from one that never did. Null when the caller supplied
1524
+ // only the digest, which is the honest answer: nothing read it.
1525
+ res.clientData = clientData;
1526
+ return res;
1527
+ })
1330
1528
  .then(function (res) {
1331
1529
  // Metadata governs when supplied; caller roots are the fallback for the models
1332
1530
  // the catalogue does not cover. Whichever ran, the verdict SAYS which -- so a
@@ -1467,7 +1665,10 @@ function _applyMetadata(res, att, opts) {
1467
1665
  if (mds.statusDenied(entry, md, tp[tp.length - 1], at)) {
1468
1666
  throw _err("webauthn/metadata-status", "the metadata entry for " + identifier + " carries a disqualifying status report");
1469
1667
  }
1470
- var anchors = mds.metadataAnchors(entry);
1668
+ // The same three inputs the status check above used. metadataAnchors makes the same decision
1669
+ // for the callers who reach it directly, so handing it different inputs is how the two readings
1670
+ // would come to disagree about the same entry.
1671
+ var anchors = mds.metadataAnchors(entry, { metadata: md, time: at, certificate: tp[tp.length - 1] });
1471
1672
  if (!anchors.length) throw _err("webauthn/metadata-no-anchor", "the metadata entry for " + identifier + " supplies no attestation root certificate");
1472
1673
  applied.push({ entry: entry, anchors: anchors, identifier: identifier });
1473
1674
  return { anchors: anchors, identifier: identifier };
@@ -1564,15 +1765,20 @@ function _applyMetadata(res, att, opts) {
1564
1765
  });
1565
1766
  }
1566
1767
 
1567
- // Only an attestation that actually carries a TPM public area can satisfy a TPM policy: the tpm
1568
- // format directly, or a compound holding at least one tpm element (whose own arm applies it).
1569
- function _formatCanSatisfyTpmPolicy(att) {
1570
- if (att.fmt === "tpm") return true;
1768
+ // Does this attestation carry a statement of the named format -- as the whole statement, or as one
1769
+ // element of a compound (sec. 8.9), whose own arm would apply a policy about that format?
1770
+ //
1771
+ // Every option that DEMANDS something only one format can produce asks this same question, so it is
1772
+ // asked in one place: a demand about a TPM public area and a demand about an android-safetynet
1773
+ // device-integrity signal differ only in the format they name. Answering it per option is how the
1774
+ // second one comes to be answered only for the simple case and not for a compound.
1775
+ function _formatCarries(att, fmt) {
1776
+ if (att.fmt === fmt) return true;
1571
1777
  if (att.fmt !== "compound") return false;
1572
1778
  return (att.attStmt.children || []).some(function (el) {
1573
1779
  if (!el || el.majorType !== 5) return false;
1574
1780
  var fN = cbor.read.mapGet(el, "fmt");
1575
- return !!fN && fN.majorType === 3 && cbor.read.textString(fN) === "tpm";
1781
+ return !!fN && fN.majorType === 3 && cbor.read.textString(fN) === fmt;
1576
1782
  });
1577
1783
  }
1578
1784
 
@@ -1580,7 +1786,7 @@ void constants;
1580
1786
 
1581
1787
  /**
1582
1788
  * @primitive pki.webauthn.verifyMetadataBlob
1583
- * @signature pki.webauthn.verifyMetadataBlob(blob, opts) -> Promise<{ no, nextUpdate, entries, byAaguid }>
1789
+ * @signature pki.webauthn.verifyMetadataBlob(blob, opts) -> Promise<{ no, legalHeader, nextUpdate, stale, allowStale, rollbackChecked, previousNo, entries, byAaguid, byKeyIdentifier, statusPolicy, rejectUnknownStatus }>
1584
1790
  * @since 0.4.11
1585
1791
  * @status stable
1586
1792
  * @spec FIDO Metadata Service v3.0 sec. 3.1, RFC 7515
@@ -1598,6 +1804,12 @@ void constants;
1598
1804
  * (freshness). Every failure is a typed `webauthn/metadata-*` throw, never a partial
1599
1805
  * result.
1600
1806
  *
1807
+ * The result says which of those rules actually ran, so a catalogue held for a while can
1808
+ * still answer for itself: `stale` and `allowStale` for freshness, `rollbackChecked` and
1809
+ * the `previousNo` it was compared against for rollback, `statusPolicy` and
1810
+ * `rejectUnknownStatus` for the status reading every later lookup will use. A rule that
1811
+ * did not run reads as not-run rather than as passed.
1812
+ *
1601
1813
  * @intro No FIDO root ships with this toolkit and there is no trust-on-first-use:
1602
1814
  * which metadata authority to trust is the operator's decision, exactly as a root
1603
1815
  * store is for `pki.path.validate`. Supply the FIDO Alliance root you pin.
@@ -1661,21 +1873,40 @@ void constants;
1661
1873
 
1662
1874
  /**
1663
1875
  * @primitive pki.webauthn.metadataAnchors
1664
- * @signature pki.webauthn.metadataAnchors(entry) -> [certificate]
1876
+ * @signature pki.webauthn.metadataAnchors(entry, opts?) -> [certificate]
1665
1877
  * @since 0.4.11
1666
1878
  * @status stable
1667
1879
  * @spec FIDO Metadata Service v3.0 sec. 3.1.1
1880
+ * @defends webauthn-revoked-authenticator-accepted (CWE-299)
1668
1881
  * @related pki.webauthn.metadataFor, pki.path.validate
1669
1882
  *
1670
1883
  * The parsed attestation root certificates a metadata entry registers -- the anchors an
1671
- * attestation from that model must chain to. Decoding is per entry rather than for the
1884
+ * attestation from that model must chain to. An entry whose status reports disqualify
1885
+ * the model registers none: the catalogue exists to say which authenticators are still
1886
+ * trusted, so handing back the roots of one it has revoked would answer a different
1887
+ * question than the caller asked. That refusal is `webauthn/metadata-status`.
1888
+ *
1889
+ * The judgement uses whatever the caller supplies and the strictest reading of what it
1890
+ * does not: pass the verified `metadata` and its own `statusPolicy` governs and its
1891
+ * freshness is re-checked, pass `time` and reports are judged as of that instant, pass
1892
+ * the `certificate` an attestation actually presented and a report naming a single
1893
+ * certificate is judged against that one rather than denying every device the entry
1894
+ * covers. With none of them: any disqualifying report denies, judged now.
1895
+ *
1896
+ * Decoding is per entry rather than for the
1672
1897
  * whole BLOB on purpose: a handful of certificates in the live metadata do not parse
1673
1898
  * under a strict decoder, and decoding everything up front would let one vendor's
1674
1899
  * malformed root refuse the entire catalogue for every other authenticator in it.
1675
1900
  *
1901
+ * @opts
1902
+ * metadata -- the verifyMetadataBlob result the entry came from
1903
+ * time -- the instant the status reports are judged at (default: now)
1904
+ * certificate -- the attestation certificate presented, for a report that names one
1905
+ *
1676
1906
  * @example
1677
- * // requires: `mdsEntry` -- one entry from a verified BLOB, as metadataFor returns
1678
- * var anchors = pki.webauthn.metadataAnchors(mdsEntry);
1907
+ * // requires: `mdsMetadata` -- a verifyMetadataBlob result; `mdsEntry` -- one of its
1908
+ * // entries, as metadataFor returns; `mdsTime` -- the instant to judge at
1909
+ * var anchors = pki.webauthn.metadataAnchors(mdsEntry, { metadata: mdsMetadata, time: mdsTime });
1679
1910
  * anchors.length; // the attestation roots this model registered
1680
1911
  * anchors[0].subject; // the decoded root DN
1681
1912
  * // chain an attestation's trustPath to them:
@@ -1707,17 +1938,29 @@ var CLIENT_DATA_TYPE = Object.assign(Object.create(null), { "webauthn.create": 1
1707
1938
  * Buffer, so a caller compares raw bytes and never two spellings of the same
1708
1939
  * value; `type`, `origin`, `crossOrigin` and `topOrigin` come back as they were.
1709
1940
  *
1710
- * Supply `expectedType`, `expectedChallenge` and `expectedOrigin` and each is
1711
- * checked here -- the challenge in constant time and by full value. `checked`
1941
+ * Supply `expectedType`, `expectedChallenge`, `expectedOrigin` and
1942
+ * `expectedTopOrigin` and each is checked here -- the challenge in constant time
1943
+ * and by full value, the origins whole and case-sensitively. `checked`
1712
1944
  * reports which ran, so a check that passed is distinguishable from one that never
1713
1945
  * happened. `expectedType` is worth setting on every call: the ceremony a response
1714
1946
  * belongs to is fixed, and accepting a `webauthn.create` where a `webauthn.get` was
1715
1947
  * expected is a credential-registration response replayed as a login.
1716
1948
  *
1949
+ * In a cross-origin ceremony `origin` is the framed document's and `topOrigin` is
1950
+ * the page that framed it, so a relying party that allows framing at all should say
1951
+ * which pages may do it. `expectedTopOrigin: null` requires an unframed ceremony,
1952
+ * which an origin list cannot express. Whether a ceremony was framed is stated by
1953
+ * BOTH `crossOrigin` and `topOrigin` and is only usable when they agree: a response
1954
+ * declaring itself cross-origin does not satisfy `null` by omitting the origin, and
1955
+ * one that does not declare itself cross-origin makes no framing claim for an origin
1956
+ * list to accept.
1957
+ *
1717
1958
  * @opts
1718
1959
  * expectedType -- "webauthn.create" or "webauthn.get"
1719
- * expectedChallenge -- the challenge bytes this ceremony issued (Buffer)
1960
+ * expectedChallenge -- the challenge bytes this ceremony issued (BufferSource)
1720
1961
  * expectedOrigin -- the origin string, or an array of acceptable origins
1962
+ * expectedTopOrigin -- the acceptable top-level origin(s), or null to require an
1963
+ * unframed ceremony
1721
1964
  *
1722
1965
  * @example
1723
1966
  * // requires: `clientDataJSON` -- credential.response.clientDataJSON;
@@ -1734,9 +1977,9 @@ function parseClientData(bytes, opts) {
1734
1977
  opts = opts || {};
1735
1978
  if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
1736
1979
  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
- }
1980
+ // The RAW bytes, not a parsed object: this reader decides on the exact octets the authenticator
1981
+ // signed over, and re-serializing a parsed object would decide on different ones.
1982
+ bytes = _bytesArg(bytes, "clientDataJSON");
1740
1983
  var doc = guard.json.parse(Buffer.from(bytes), _err, {
1741
1984
  maxBytes: constants.LIMITS.JSON_MAX_BYTES, maxDepth: constants.LIMITS.JSON_MAX_DEPTH,
1742
1985
  badJson: "webauthn/bad-client-data", tooDeep: "webauthn/bad-client-data",
@@ -1776,7 +2019,7 @@ function parseClientData(bytes, opts) {
1776
2019
  throw _err("webauthn/bad-client-data", "clientDataJSON topOrigin must be a non-empty string when present (WebAuthn sec. 5.8.1)");
1777
2020
  }
1778
2021
 
1779
- var checked = { type: false, challenge: false, origin: false };
2022
+ var checked = { type: false, challenge: false, origin: false, topOrigin: false };
1780
2023
  if (opts.expectedType !== undefined) {
1781
2024
  if (CLIENT_DATA_TYPE[opts.expectedType] !== 1) {
1782
2025
  throw _err("webauthn/bad-input", "opts.expectedType must be \"webauthn.create\" or \"webauthn.get\"");
@@ -1789,10 +2032,7 @@ function parseClientData(bytes, opts) {
1789
2032
  checked.type = true;
1790
2033
  }
1791
2034
  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)) {
2035
+ if (!guard.crypto.constantTimeEqual(_bytesArg(opts.expectedChallenge, "opts.expectedChallenge"), challenge)) {
1796
2036
  throw _err("webauthn/client-data-mismatch",
1797
2037
  "the clientDataJSON challenge is not the one this ceremony issued (WebAuthn sec. 7.1 step 9 / sec. 7.2 step 12)");
1798
2038
  }
@@ -1813,6 +2053,48 @@ function parseClientData(bytes, opts) {
1813
2053
  }
1814
2054
  checked.origin = true;
1815
2055
  }
2056
+ // The top-level origin of a cross-origin ceremony. It is surfaced either way, but a value a
2057
+ // relying party only READS is a value nobody compares: `origin` in a cross-origin ceremony is the
2058
+ // iframe's, and the top-level page that framed it is the one the security decision is actually
2059
+ // about. Given what it accepts, this compares it the same way as `origin` -- whole, case
2060
+ // sensitive, against a list -- so the two cannot drift into different matching rules.
2061
+ //
2062
+ // The three cases are kept apart rather than collapsed. An expectation of `null` means "this
2063
+ // ceremony must NOT be framed", which is a real policy and cannot be expressed by an origin list;
2064
+ // a response carrying no topOrigin against a list of acceptable ones is not framed at all, so the
2065
+ // list has nothing to accept and the ceremony is refused rather than passed.
2066
+ if (opts.expectedTopOrigin !== undefined) {
2067
+ var wantTop = opts.expectedTopOrigin;
2068
+ // Whether the ceremony was framed is stated by TWO fields, and the answer is only usable when
2069
+ // they agree. `crossOrigin` says a framing happened; `topOrigin` names the page that did it.
2070
+ // Reading one and not the other lets a response that declares itself cross-origin, but omits
2071
+ // the origin, satisfy a policy of "must not be framed" -- it answers the caller's question with
2072
+ // the field it left out rather than the one it filled in.
2073
+ var framed = doc.crossOrigin === true;
2074
+ if (wantTop === null) {
2075
+ if (framed || doc.topOrigin !== undefined) {
2076
+ throw _err("webauthn/client-data-mismatch",
2077
+ "the clientDataJSON describes a cross-origin ceremony (crossOrigin " + JSON.stringify(doc.crossOrigin) +
2078
+ ", topOrigin " + JSON.stringify(doc.topOrigin === undefined ? null : doc.topOrigin) +
2079
+ "), and this relying party requires an unframed one (WebAuthn sec. 5.8.1)");
2080
+ }
2081
+ } else {
2082
+ var allowedTop = Array.isArray(wantTop) ? wantTop : [wantTop];
2083
+ if (!allowedTop.length || !allowedTop.every(function (o) { return typeof o === "string" && o.length; })) {
2084
+ throw _err("webauthn/bad-input", "opts.expectedTopOrigin must be null, a non-empty origin string, or an array of them");
2085
+ }
2086
+ // Allow-listing the pages that may frame this ceremony is a statement about a framed one. A
2087
+ // response that does not say it was framed makes no such claim to match against, so the list
2088
+ // has nothing to accept -- and treating its absent topOrigin as a pass would report a check
2089
+ // that decided nothing.
2090
+ if (!framed || allowedTop.indexOf(doc.topOrigin) === -1) {
2091
+ throw _err("webauthn/client-data-mismatch",
2092
+ "the clientDataJSON topOrigin " + JSON.stringify(doc.topOrigin === undefined ? null : doc.topOrigin) +
2093
+ " (crossOrigin " + JSON.stringify(doc.crossOrigin) + ") is not a framing this relying party accepts (WebAuthn sec. 5.8.1)");
2094
+ }
2095
+ }
2096
+ checked.topOrigin = true;
2097
+ }
1816
2098
  return {
1817
2099
  type: doc.type, challenge: challenge, origin: doc.origin,
1818
2100
  crossOrigin: doc.crossOrigin === undefined ? false : doc.crossOrigin,
@@ -1821,14 +2103,14 @@ function parseClientData(bytes, opts) {
1821
2103
  };
1822
2104
  }
1823
2105
  var _CLIENT_DATA_OPTS = Object.assign(Object.create(null), {
1824
- expectedType: 1, expectedChallenge: 1, expectedOrigin: 1,
2106
+ expectedType: 1, expectedChallenge: 1, expectedOrigin: 1, expectedTopOrigin: 1,
1825
2107
  });
1826
2108
 
1827
2109
  // ---- public: parseAuthenticatorData / verifyAssertion ------------------------
1828
2110
 
1829
2111
  /**
1830
2112
  * @primitive pki.webauthn.parseAuthenticatorData
1831
- * @signature pki.webauthn.parseAuthenticatorData(bytes) -> { rpIdHash, flags, signCount, aaguid, credentialId, credentialPublicKey, extensions }
2113
+ * @signature pki.webauthn.parseAuthenticatorData(bytes) -> { rpIdHash, flags, signCount, aaguid, credentialId, credentialPublicKey, credentialPublicKeyBytes, extensions }
1832
2114
  * @since 0.5.0
1833
2115
  * @status experimental
1834
2116
  * @spec W3C WebAuthn Level 3 sec. 6.1
@@ -1852,10 +2134,7 @@ var _CLIENT_DATA_OPTS = Object.assign(Object.create(null), {
1852
2134
  * ad.signCount; // the authenticator's counter for this credential
1853
2135
  */
1854
2136
  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);
2137
+ return _parseAuthData(_bytesArg(bytes, "authenticatorData"), _err);
1859
2138
  }
1860
2139
 
1861
2140
  // The options pki.webauthn.verifyAssertion recognises, null-prototype for the same
@@ -1864,12 +2143,12 @@ var _ASSERT_OPTS = Object.assign(Object.create(null), {
1864
2143
  authenticatorData: 1, clientDataHash: 1, clientDataJSON: 1, signature: 1,
1865
2144
  credentialPublicKey: 1, previousSignCount: 1,
1866
2145
  expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
1867
- expectedChallenge: 1, expectedOrigin: 1,
2146
+ expectedChallenge: 1, expectedOrigin: 1, expectedTopOrigin: 1,
1868
2147
  });
1869
2148
 
1870
2149
  /**
1871
2150
  * @primitive pki.webauthn.verifyAssertion
1872
- * @signature pki.webauthn.verifyAssertion(input) -> Promise<{ signatureVerified, signCount, signCountChecked, flags, rpIdHash, bindingChecked }>
2151
+ * @signature pki.webauthn.verifyAssertion(input) -> Promise<{ signatureVerified, signCount, signCountChecked, flags, rpIdHash, extensions, bindingChecked, clientData }>
1873
2152
  * @since 0.5.0
1874
2153
  * @status experimental
1875
2154
  * @spec W3C WebAuthn Level 3 sec. 7.2
@@ -1932,22 +2211,31 @@ function _snapshotAssertion(input) {
1932
2211
  guard.identifier.assertKnownKeys(input, _ASSERT_OPTS, _err, "webauthn/bad-input", "verifyAssertion input has an unknown key ");
1933
2212
  var out = {}, k;
1934
2213
  for (k in input) { if (Object.prototype.hasOwnProperty.call(input, k)) out[k] = input[k]; }
2214
+ // The SAME set of byte forms the arguments accept. A snapshot list narrower than the accept list
2215
+ // leaves whatever falls in the gap aliased across the yield -- which is the one input a widened
2216
+ // accept list newly admits, so the two must be written from one predicate.
1935
2217
  ["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
- }
2218
+ if (_isBufferSource(out[f])) out[f] = guard.bytes.snapshotSource(out[f], WebauthnError, "webauthn/bad-input", f);
1939
2219
  });
1940
- if (_isPlainObject(out.credentialPublicKey)) {
2220
+ // The BYTES form first. A Buffer satisfies the plain-object test below, so leaving it to that
2221
+ // branch copies its numeric indices into a `{0:.., 1:..}` object that is no longer a key at all
2222
+ // -- the stored credential silently becoming something the SPKI builder cannot read. It is
2223
+ // snapshotted here for the same reason the other byte inputs are: it is read after a yield.
2224
+ if (_isBufferSource(out.credentialPublicKey)) {
2225
+ out.credentialPublicKey = guard.bytes.snapshotSource(out.credentialPublicKey, WebauthnError,
2226
+ "webauthn/bad-input", "credentialPublicKey");
2227
+ } else if (_isPlainObject(out.credentialPublicKey)) {
1941
2228
  var key = {}, kk;
1942
2229
  for (kk in out.credentialPublicKey) {
1943
2230
  if (!Object.prototype.hasOwnProperty.call(out.credentialPublicKey, kk)) continue;
1944
2231
  var v = out.credentialPublicKey[kk];
1945
- key[kk] = (Buffer.isBuffer(v) || v instanceof Uint8Array) ? Buffer.from(v) : v;
2232
+ key[kk] = _isBufferSource(v) ? guard.bytes.snapshotSource(v, WebauthnError, "webauthn/bad-input", "credentialPublicKey." + kk) : v;
1946
2233
  }
1947
2234
  out.credentialPublicKey = key;
1948
2235
  }
1949
2236
  if (Array.isArray(out.allowedAlgorithms)) out.allowedAlgorithms = out.allowedAlgorithms.slice();
1950
2237
  if (Array.isArray(out.expectedOrigin)) out.expectedOrigin = out.expectedOrigin.slice();
2238
+ if (Array.isArray(out.expectedTopOrigin)) out.expectedTopOrigin = out.expectedTopOrigin.slice();
1951
2239
  return out;
1952
2240
  }
1953
2241
 
@@ -1963,9 +2251,7 @@ function verifyAssertion(input) {
1963
2251
  return Promise.resolve().then(function () {
1964
2252
  input = frozen;
1965
2253
  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
- }
2254
+ input.signature = _bytesArg(input.signature, "signature");
1969
2255
  // The two forms of the same input, and NEITHER is inferred from the other's
1970
2256
  // absence: supplying both invites them to disagree, and picking one would make
1971
2257
  // the signature cover something the caller did not mean.
@@ -1975,9 +2261,7 @@ function verifyAssertion(input) {
1975
2261
  }
1976
2262
  var clientDataHash, clientData = null;
1977
2263
  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
- }
2264
+ input.clientDataJSON = _bytesArg(input.clientDataJSON, "clientDataJSON");
1981
2265
  // Given the JSON, this verb reads it -- the ceremony TYPE is checked
1982
2266
  // unconditionally, because which ceremony a response belongs to is fixed by
1983
2267
  // the spec rather than chosen by the caller, and a registration response
@@ -1987,22 +2271,32 @@ function verifyAssertion(input) {
1987
2271
  expectedType: "webauthn.get",
1988
2272
  expectedChallenge: input.expectedChallenge,
1989
2273
  expectedOrigin: input.expectedOrigin,
2274
+ expectedTopOrigin: input.expectedTopOrigin,
1990
2275
  });
1991
2276
  clientDataHash = _sha("sha256", Buffer.from(input.clientDataJSON));
1992
2277
  } else {
1993
- if (input.expectedChallenge !== undefined || input.expectedOrigin !== undefined) {
2278
+ // Every expectation this verb forwards to the clientData reader is unanswerable without the
2279
+ // JSON, so each is refused here rather than silently going uncompared. The list is the
2280
+ // forwarded set above; a new expectation added there is added here too.
2281
+ if (input.expectedChallenge !== undefined || input.expectedOrigin !== undefined ||
2282
+ input.expectedTopOrigin !== undefined) {
1994
2283
  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");
2284
+ "expectedChallenge / expectedOrigin / expectedTopOrigin are checked against clientDataJSON, which this call " +
2285
+ "did not supply -- pass clientDataJSON instead of clientDataHash, or check them yourself");
2000
2286
  }
2001
- clientDataHash = Buffer.from(input.clientDataHash);
2287
+ clientDataHash = _bytesArg(input.clientDataHash, "clientDataHash");
2288
+ if (clientDataHash.length !== 32) throw _err("webauthn/bad-input", "clientDataHash must be a 32-byte SHA-256 digest");
2002
2289
  }
2290
+ // Either form a relying party can be holding. `verify` hands back the parsed object, but the
2291
+ // durable form is BYTES: the object carries Buffers, so a JSON round trip through a datastore
2292
+ // returns {"type":"Buffer","data":[...]} rather than what went in, and every existing credential
2293
+ // store already holds the COSE bytes. Accepting only the object made a caller fabricate an
2294
+ // authenticatorData that never existed just to reach their own key.
2003
2295
  var coseKey = input.credentialPublicKey;
2004
- if (!_isPlainObject(coseKey)) {
2005
- throw _err("webauthn/bad-input", "credentialPublicKey must be the stored COSE key object");
2296
+ if (Buffer.isBuffer(coseKey) || ArrayBuffer.isView(coseKey) || coseKey instanceof ArrayBuffer) {
2297
+ coseKey = parseCoseKey(coseKey);
2298
+ } else if (!_isPlainObject(coseKey)) {
2299
+ throw _err("webauthn/bad-input", "credentialPublicKey must be the stored COSE key -- the object pki.webauthn.verify returned, or its COSE bytes");
2006
2300
  }
2007
2301
  var bindingChecked = _applyBindings(authData, coseKey, input);
2008
2302
  // The counter's SHAPE is a config-time question and is answered here; whether it
@@ -2027,14 +2321,26 @@ function verifyAssertion(input) {
2027
2321
  // signature would let anyone raise that alarm with arbitrary bytes, so the
2028
2322
  // counter is read only out of an assertion that proved to be authentic. The
2029
2323
  // 0/0 case is an authenticator that implements no counter, which is permitted.
2324
+ // THREE outcomes, kept apart, because this field's only purpose is to tell a relying party
2325
+ // whether it has cloned-authenticator detection on this credential:
2326
+ // false -- not requested; no previousSignCount was supplied
2327
+ // "not-supported" -- requested, but the authenticator implements no counter (the 0/0 case
2328
+ // sec. 7.2 permits), so the comparison was deliberately skipped
2329
+ // true -- requested and performed
2330
+ // Reporting `true` for the waived case claimed a detection that cannot happen, and a bare
2331
+ // `false` there would be indistinguishable from never having asked.
2030
2332
  var signCountChecked = false;
2031
2333
  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)");
2334
+ if (prev === 0 && authData.signCount === 0) {
2335
+ signCountChecked = "not-supported";
2336
+ } else {
2337
+ if (authData.signCount <= prev) {
2338
+ throw _err("webauthn/sign-count-not-advanced",
2339
+ "the assertion signCount " + authData.signCount + " does not advance past the stored " + prev +
2340
+ ", which is the signal of a cloned authenticator (WebAuthn sec. 7.2 step 21)");
2341
+ }
2342
+ signCountChecked = true;
2036
2343
  }
2037
- signCountChecked = true;
2038
2344
  }
2039
2345
  return {
2040
2346
  signatureVerified: true,
@@ -2057,6 +2363,7 @@ module.exports = {
2057
2363
  parseAttestationObject: parseAttestationObject,
2058
2364
  parseAuthenticatorData: parseAuthenticatorData,
2059
2365
  parseClientData: parseClientData,
2366
+ parseCoseKey: parseCoseKey,
2060
2367
  verify: verify,
2061
2368
  verifyAssertion: verifyAssertion,
2062
2369
  verifyMetadataBlob: mds.verifyMetadataBlob,