@blamejs/pki 0.5.2 → 0.5.4

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
 
@@ -180,10 +179,7 @@ function _decodeCoseKey(node) {
180
179
  * // pass `stored` straight as its credentialPublicKey.
181
180
  */
182
181
  function parseCoseKey(bytes) {
183
- var buf = _snapshotBytes(bytes, "the COSE key");
184
- if (!Buffer.isBuffer(buf)) {
185
- throw _err("webauthn/bad-input", "parseCoseKey takes the stored COSE key bytes (a Buffer, TypedArray or ArrayBuffer)");
186
- }
182
+ var buf = _bytesArg(bytes, "the COSE key");
187
183
  var node;
188
184
  try { node = cbor.decode(buf); }
189
185
  catch (e) { throw _err("webauthn/bad-cose-key", "the stored credential key is not decodable CBOR", e); }
@@ -244,7 +240,7 @@ function _verifySig(alg, sig, spkiBytes, message, E) {
244
240
  // low-order (e.g. all-zeroes) key verifies a trivial signature -- so validate the OKP
245
241
  // point before verify. This covers EVERY key that signs a WebAuthn statement: the x5c
246
242
  // attestation-certificate key (packed/tpm/apple) AND the self-attestation credential key.
247
- if (imp.name === "Ed25519" || imp.name === "Ed448") _requireValidEdPoint(spkiBytes, imp.name, E);
243
+ if (imp.name === "Ed25519" || imp.name === "Ed448") _requireValidEdPoint(spkiBytes, imp.name);
248
244
  var s = d.ecdsa ? _derEcdsaToRaw(sig, d.imp.namedCurve) : sig;
249
245
  return subtle.importKey("spki", spkiBytes, imp, false, ["verify"])
250
246
  .then(function (key) { return subtle.verify(ver, key, s, message); })
@@ -262,17 +258,14 @@ function _edName(spkiBytes, E) {
262
258
  if (!nm) throw E("webauthn/unsupported-algorithm", "unsupported EdDSA curve OID " + algOid);
263
259
  return nm;
264
260
  }
265
- // The raw Edwards point an OKP SPKI carries (its BIT STRING body, past the unused-bits
266
- // octet) MUST be a valid, full-order point -- reject an off-curve or low-order key before it
267
- // verifies a signature (WebCrypto import does not check it). Curve from the WebCrypto name.
268
- function _requireValidEdPoint(spkiBytes, name, E) {
269
- var content;
270
- try { content = asn1.decode(spkiBytes).children[1].content; }
271
- catch (e) { throw E("webauthn/bad-signature", "the EdDSA public key is not a well-formed SPKI", e); }
272
- var point = content && content.length ? content.subarray(1) : Buffer.alloc(0);
273
- if (!edwardsPoint.validate(point, name === "Ed25519" ? 6 : 7)) {
274
- throw E("webauthn/bad-signature", "the EdDSA public key is not a valid, full-order Edwards point");
275
- }
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");
276
269
  }
277
270
 
278
271
  // A validated COSE credential key -> a self-contained SPKI the WebCrypto import
@@ -295,11 +288,39 @@ function _certEcCurveOid(cert, E) {
295
288
  try { return asn1.read.oid(asn1.decode(params)); }
296
289
  catch (e) { throw E("webauthn/key-mismatch", "the attestation certificate EC curve is not a valid OBJECT IDENTIFIER", e); }
297
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
+ }
298
318
  // A void assert: throws webauthn/key-mismatch on any inequality, returns nothing on
299
319
  // success (called for its throw side-effect, like the other _check* asserts).
300
320
  function _certPubKeyEqualsCose(cert, cose, E) {
301
321
  var raw = cert.subjectPublicKeyInfo && cert.subjectPublicKeyInfo.publicKey && cert.subjectPublicKeyInfo.publicKey.bytes;
302
322
  if (!raw) throw E("webauthn/key-mismatch", "the attestation certificate exposes no public key");
323
+ _assertCertKeyAlgorithm(cert, cose, E);
303
324
  if (cose.kty === 2) {
304
325
  // The certificate's declared EC curve MUST equal the credential key's curve --
305
326
  // a curve substitution is a different key even if the coordinate bytes line up.
@@ -459,8 +480,15 @@ var _VERIFY_OPTS = Object.assign(Object.create(null), {
459
480
  time: 1, metadata: 1, tpmPolicy: 1, safetyNetRoots: 1, verifySafetyNetJws: 1, requireCtsProfileMatch: 1,
460
481
  expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
461
482
  rootCertificates: 1,
483
+ clientDataJSON: 1, expectedChallenge: 1, expectedOrigin: 1, expectedTopOrigin: 1,
462
484
  });
463
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
+
464
492
  // Anchor an attestation's trust path to roots the CALLER pins. The metadata route
465
493
  // resolves an authenticator's roots from the catalogue that registered it, which is
466
494
  // the stronger source -- but it only reaches models the catalogue lists, and some
@@ -515,22 +543,34 @@ function _cloneParsed(v, depth) {
515
543
  }
516
544
 
517
545
  // A caller-owned byte argument, copied so nothing downstream reads bytes the caller can still
518
- // rewrite. Every byte form the parsers accept is covered -- an ArrayBuffer or a DataView arrives
519
- // by the same door as a Buffer, and leaving those aliased would reopen the window for exactly the
520
- // inputs that came in by the wider one. Anything else passes through untouched so this cannot
521
- // change which inputs are accepted; the parser downstream still names a wrong type.
522
- function _snapshotBytes(v, label) {
523
- 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)) {
524
560
  return guard.bytes.snapshotSource(v, WebauthnError, "webauthn/bad-input", label);
525
561
  }
526
- return v;
562
+ throw _err("webauthn/bad-input", label + " must be a BufferSource (a Buffer, a typed-array view, or an ArrayBuffer)");
527
563
  }
528
564
 
529
565
  function _snapshotRoots(supplied) {
530
566
  if (!Array.isArray(supplied)) return supplied;
531
567
  return supplied.map(function (root) {
532
- if (Buffer.isBuffer(root) || root instanceof Uint8Array) {
533
- 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[]");
534
574
  }
535
575
  if (root && typeof root === "object") return _cloneParsed(root, 0);
536
576
  return root; // a PEM string is immutable
@@ -553,7 +593,7 @@ function _applyCallerRoots(res, supplied, vopts, onlyPaths) {
553
593
  // The same three forms opts.safetyNetRoots takes, since it is the same question.
554
594
  var roots = supplied.map(function (root, i) {
555
595
  var cert;
556
- 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); }
557
597
  catch (e) { throw _err("webauthn/bad-input", "opts.rootCertificates[" + i + "] is not a decodable certificate", e); }
558
598
  if (!cert || !cert.subject || !cert.subjectPublicKeyInfo) {
559
599
  throw _err("webauthn/bad-input", "opts.rootCertificates[" + i + "] is not a certificate");
@@ -649,10 +689,6 @@ function _anchoredRoutes(res, base) {
649
689
  // reports which of them actually ran. The challenge and the origin stay with the
650
690
  // caller: they live in clientDataJSON, which the caller already holds and compares
651
691
  // against state only it has.
652
- var _BINDING_OPTS = Object.assign(Object.create(null), {
653
- expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
654
- });
655
-
656
692
  function _assertBool(v, name) {
657
693
  if (typeof v !== "boolean") throw _err("webauthn/bad-input", "opts." + name + " must be a boolean");
658
694
  }
@@ -905,10 +941,8 @@ var VERIFIERS = {
905
941
  // existed. There is no bundled root and no trust-on-first-use.
906
942
  "android-safetynet": function (att, clientDataHash, opts) {
907
943
  opts = opts || {};
944
+ // The type is settled at the entry point, so what remains here is the opt-in itself.
908
945
  if (opts.verifySafetyNetJws !== true) {
909
- if (opts.verifySafetyNetJws !== undefined && typeof opts.verifySafetyNetJws !== "boolean") {
910
- throw _err("webauthn/bad-input", "opts.verifySafetyNetJws must be a boolean");
911
- }
912
946
  throw _err("webauthn/unsupported-format", "attestation statement format 'android-safetynet' is not supported");
913
947
  }
914
948
  var roots = opts.safetyNetRoots;
@@ -992,9 +1026,6 @@ var VERIFIERS = {
992
1026
  if (opts.requireCtsProfileMatch === true && signals.ctsProfileMatch !== true) {
993
1027
  throw _err("webauthn/safetynet-cts-profile", "the android-safetynet response reports ctsProfileMatch " + JSON.stringify(signals.ctsProfileMatch) + ", and opts.requireCtsProfileMatch demands true");
994
1028
  }
995
- if (opts.requireCtsProfileMatch !== undefined && typeof opts.requireCtsProfileMatch !== "boolean") {
996
- throw _err("webauthn/bad-input", "opts.requireCtsProfileMatch must be a boolean");
997
- }
998
1029
 
999
1030
  return _verifySig(-257, sigBytes, leaf.subjectPublicKeyInfo.bytes,
1000
1031
  Buffer.from(segs[0] + "." + segs[1], "ascii"), _err).then(function (ok) {
@@ -1133,41 +1164,28 @@ function _safetyNetHostnameOk(leaf) {
1133
1164
  // rotation; the first that validates wins, and if none does the attestation is refused. The chain
1134
1165
  // goes through the full path validator rather than a signature-only walk, so an expired, revoked-by-
1135
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.
1136
1174
  function _safetyNetChainTrusted(chain, roots, time) {
1137
- var ordered = chain.slice().reverse(); // path.validate takes anchor-adjacent first
1138
- var when = time === undefined ? new Date() : time;
1139
- var attempts = roots.map(function (root, i) {
1140
- return function () {
1175
+ var anchors;
1176
+ try {
1177
+ anchors = roots.map(function (root, i) {
1141
1178
  var anchorCert;
1142
- 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); }
1143
1180
  catch (e) { throw _err("webauthn/bad-input", "opts.safetyNetRoots[" + i + "] is not a decodable certificate", e); }
1144
1181
  if (!anchorCert || !anchorCert.subject || !anchorCert.subjectPublicKeyInfo) {
1145
1182
  throw _err("webauthn/bad-input", "opts.safetyNetRoots[" + i + "] is not a certificate");
1146
1183
  }
1147
- // An x5c chain conventionally carries the root as its last entry. The anchor is supplied
1148
- // separately and is what establishes trust, so drop a trailing self-issued certificate that
1149
- // IS this anchor rather than validating it against itself as a path element.
1150
- var path = ordered.slice();
1151
- if (path.length > 1 && guard.name.dnEqual(path[0].subject, anchorCert.subject) &&
1152
- guard.name.dnEqual(path[0].issuer, path[0].subject)) {
1153
- path = path.slice(1);
1154
- }
1155
- return pathValidate.validate(path, {
1156
- time: when,
1157
- // The anchor's own KEY algorithm and its parameters, not the algorithm its issuer signed it
1158
- // with: the validator carries these forward as the working public key, and a certificate
1159
- // below the anchor may inherit its key parameters from them.
1160
- trustAnchor: { name: anchorCert.subject, publicKey: anchorCert.subjectPublicKeyInfo.bytes,
1161
- algorithm: anchorCert.subjectPublicKeyInfo.algorithm.oid,
1162
- parameters: anchorCert.subjectPublicKeyInfo.algorithm.parameters },
1163
- }).then(function (r) { return !!(r && r.valid); }, function () { return false; });
1164
- };
1165
- });
1166
- return attempts.reduce(function (p, next) {
1167
- return p.then(function (done) { return done ? true : next(); });
1168
- }, Promise.resolve(false)).then(function (trusted) {
1169
- if (!trusted) throw _err("webauthn/safetynet-cert-untrusted", "the android-safetynet x5c chain does not validate to any supplied root (opts.safetyNetRoots)");
1170
- });
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");
1171
1189
  }
1172
1190
 
1173
1191
  // `chain` is the x5c order (leaf-first); trustPath is surfaced in pki.path.validate
@@ -1197,6 +1215,16 @@ function _result(fmt, attestationType, chain, att) {
1197
1215
  credentialPublicKeyBytes: att.authData.credentialPublicKeyBytes,
1198
1216
  signCount: att.authData.signCount,
1199
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,
1200
1228
  };
1201
1229
  }
1202
1230
 
@@ -1232,7 +1260,7 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
1232
1260
 
1233
1261
  /**
1234
1262
  * @primitive pki.webauthn.verify
1235
- * @signature pki.webauthn.verify(attestationObject, clientDataHash, opts) -> Promise<{ attestationVerified, fmt, attestationType, trustPath, anchoredTo, anchoredElements, aaguid, credentialId, credentialPublicKey, credentialPublicKeyBytes, 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 }>
1236
1264
  * @since 0.2.5
1237
1265
  * @status stable
1238
1266
  * @spec W3C WebAuthn Level 3 sec. 8 / sec. 7.1
@@ -1240,11 +1268,19 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
1240
1268
  *
1241
1269
  * Verify a WebAuthn attestation statement: the attestation signature over
1242
1270
  * `authenticatorData || clientDataHash` and (for the x5c formats) the format's
1243
- * certificate requirements. `clientDataHash` is the SHA-256 of the serialized client
1244
- * data, supplied by the relying party. Resolves the attestation type + trust path or
1271
+ * certificate requirements. Resolves the attestation type + trust path or
1245
1272
  * throws a typed `webauthn/*` error; a signature that does not verify is a
1246
1273
  * `webauthn/verify-failed` verdict, never a silent pass.
1247
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
+ *
1248
1284
  * The verdict field is `attestationVerified`, and the name is the point: a sound
1249
1285
  * attestation statement is not the same claim as an acceptable registration. The
1250
1286
  * statement says nothing about WHICH relying party asked for it, or whether a user
@@ -1252,9 +1288,12 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
1252
1288
  * clear, is perfectly sound and must not be registered. Supply `expectedRpId`,
1253
1289
  * `requireUserPresence`, `requireUserVerification` and `allowedAlgorithms` and those
1254
1290
  * are checked here; `bindingChecked` reports which ran, so a check that passed can be
1255
- * told from one that never happened. The CHALLENGE and the ORIGIN live in
1256
- * clientDataJSON -- read them with `pki.webauthn.parseClientData`, which compares
1257
- * 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.
1258
1297
  *
1259
1298
  * The verdict also carries what a relying party must STORE to run a later login:
1260
1299
  * `credentialId`, `credentialPublicKey` and the initial `signCount`. The credential key
@@ -1294,19 +1333,48 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
1294
1333
  * claim there is anything to anchor, so it is not a reason to refuse the statement,
1295
1334
  * but it does mean "anchored" covered fewer elements than the statement holds.
1296
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
+ *
1297
1353
  * @example
1298
- * // requires: `attestationObject` from navigator.credentials.create(), and
1299
- * // `clientDataHash` = SHA-256 of the matching credential.response.clientDataJSON
1300
- * 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",
1301
1360
  * expectedRpId: "example.com", requireUserPresence: true,
1302
1361
  * });
1303
- * res.attestationVerified; // true (statement signature + bindings hold)
1304
- * res.bindingChecked.rpId; // true -- this response names example.com
1305
- * res.attestationType; // "Basic"
1306
- * // 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,
1307
1367
  * // and anchor res.trustPath to your pinned roots with pki.path.validate
1308
1368
  */
1309
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
+ }
1310
1378
  opts = opts || {};
1311
1379
  // Every option here either GATES the verdict or supplies the trust material a gate needs, so a
1312
1380
  // misspelled key is not a harmless no-op: `metdata` leaves the metadata gate switched off and the
@@ -1316,11 +1384,25 @@ function verify(attestationObject, clientDataHash, opts) {
1316
1384
  try {
1317
1385
  if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
1318
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);
1319
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
+ });
1320
1405
  } catch (e) { return Promise.reject(e); }
1321
- if (!Buffer.isBuffer(clientDataHash) || clientDataHash.length !== 32) {
1322
- return Promise.reject(_err("webauthn/bad-input", "clientDataHash must be a 32-byte SHA-256 digest"));
1323
- }
1324
1406
  var att;
1325
1407
  // Both byte inputs are snapshotted BEFORE anything reads them, for the same reason the trust
1326
1408
  // anchors are: the attestation statement is not evaluated until a later promise turn, and both
@@ -1331,10 +1413,49 @@ function verify(attestationObject, clientDataHash, opts) {
1331
1413
  // the statement to this ceremony; a caller who overwrote it in that gap would have the signature
1332
1414
  // checked against a challenge and origin nobody agreed to, and the verdict would still report a
1333
1415
  // sound attestation. A DataView or ArrayBuffer comes in by the same door and is copied too.
1334
- var attBytes, cdh;
1416
+ var attBytes, cdh, clientData = null;
1335
1417
  try {
1336
- attBytes = _snapshotBytes(attestationObject, "attestationObject");
1337
- 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
+ }
1338
1459
  } catch (e) { return Promise.reject(e); }
1339
1460
  attestationObject = attBytes;
1340
1461
  clientDataHash = cdh;
@@ -1354,9 +1475,17 @@ function verify(attestationObject, clientDataHash, opts) {
1354
1475
  // demanded a TPM-bound key would accept a `none` attestation instead. The requirement therefore
1355
1476
  // belongs at the dispatch, where it can refuse a format that cannot satisfy it, not in the arm
1356
1477
  // that only runs once that format was already chosen.
1357
- if (opts.tpmPolicy !== undefined && !_formatCanSatisfyTpmPolicy(att)) {
1478
+ if (opts.tpmPolicy !== undefined && !_formatCarries(att, "tpm")) {
1358
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"));
1359
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
+ }
1360
1489
  // The ceremony bindings run BEFORE the statement is evaluated: a response
1361
1490
  // produced for another relying party, or without the user presence the caller
1362
1491
  // requires, is not a message this call should spend a signature verification on
@@ -1388,6 +1517,14 @@ function verify(attestationObject, clientDataHash, opts) {
1388
1517
  });
1389
1518
  } catch (e) { return Promise.reject(e); }
1390
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
+ })
1391
1528
  .then(function (res) {
1392
1529
  // Metadata governs when supplied; caller roots are the fallback for the models
1393
1530
  // the catalogue does not cover. Whichever ran, the verdict SAYS which -- so a
@@ -1528,7 +1665,10 @@ function _applyMetadata(res, att, opts) {
1528
1665
  if (mds.statusDenied(entry, md, tp[tp.length - 1], at)) {
1529
1666
  throw _err("webauthn/metadata-status", "the metadata entry for " + identifier + " carries a disqualifying status report");
1530
1667
  }
1531
- 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] });
1532
1672
  if (!anchors.length) throw _err("webauthn/metadata-no-anchor", "the metadata entry for " + identifier + " supplies no attestation root certificate");
1533
1673
  applied.push({ entry: entry, anchors: anchors, identifier: identifier });
1534
1674
  return { anchors: anchors, identifier: identifier };
@@ -1625,15 +1765,20 @@ function _applyMetadata(res, att, opts) {
1625
1765
  });
1626
1766
  }
1627
1767
 
1628
- // Only an attestation that actually carries a TPM public area can satisfy a TPM policy: the tpm
1629
- // format directly, or a compound holding at least one tpm element (whose own arm applies it).
1630
- function _formatCanSatisfyTpmPolicy(att) {
1631
- 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;
1632
1777
  if (att.fmt !== "compound") return false;
1633
1778
  return (att.attStmt.children || []).some(function (el) {
1634
1779
  if (!el || el.majorType !== 5) return false;
1635
1780
  var fN = cbor.read.mapGet(el, "fmt");
1636
- return !!fN && fN.majorType === 3 && cbor.read.textString(fN) === "tpm";
1781
+ return !!fN && fN.majorType === 3 && cbor.read.textString(fN) === fmt;
1637
1782
  });
1638
1783
  }
1639
1784
 
@@ -1641,7 +1786,7 @@ void constants;
1641
1786
 
1642
1787
  /**
1643
1788
  * @primitive pki.webauthn.verifyMetadataBlob
1644
- * @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 }>
1645
1790
  * @since 0.4.11
1646
1791
  * @status stable
1647
1792
  * @spec FIDO Metadata Service v3.0 sec. 3.1, RFC 7515
@@ -1659,6 +1804,12 @@ void constants;
1659
1804
  * (freshness). Every failure is a typed `webauthn/metadata-*` throw, never a partial
1660
1805
  * result.
1661
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
+ *
1662
1813
  * @intro No FIDO root ships with this toolkit and there is no trust-on-first-use:
1663
1814
  * which metadata authority to trust is the operator's decision, exactly as a root
1664
1815
  * store is for `pki.path.validate`. Supply the FIDO Alliance root you pin.
@@ -1722,21 +1873,40 @@ void constants;
1722
1873
 
1723
1874
  /**
1724
1875
  * @primitive pki.webauthn.metadataAnchors
1725
- * @signature pki.webauthn.metadataAnchors(entry) -> [certificate]
1876
+ * @signature pki.webauthn.metadataAnchors(entry, opts?) -> [certificate]
1726
1877
  * @since 0.4.11
1727
1878
  * @status stable
1728
1879
  * @spec FIDO Metadata Service v3.0 sec. 3.1.1
1880
+ * @defends webauthn-revoked-authenticator-accepted (CWE-299)
1729
1881
  * @related pki.webauthn.metadataFor, pki.path.validate
1730
1882
  *
1731
1883
  * The parsed attestation root certificates a metadata entry registers -- the anchors an
1732
- * 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
1733
1897
  * whole BLOB on purpose: a handful of certificates in the live metadata do not parse
1734
1898
  * under a strict decoder, and decoding everything up front would let one vendor's
1735
1899
  * malformed root refuse the entire catalogue for every other authenticator in it.
1736
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
+ *
1737
1906
  * @example
1738
- * // requires: `mdsEntry` -- one entry from a verified BLOB, as metadataFor returns
1739
- * 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 });
1740
1910
  * anchors.length; // the attestation roots this model registered
1741
1911
  * anchors[0].subject; // the decoded root DN
1742
1912
  * // chain an attestation's trustPath to them:
@@ -1768,17 +1938,29 @@ var CLIENT_DATA_TYPE = Object.assign(Object.create(null), { "webauthn.create": 1
1768
1938
  * Buffer, so a caller compares raw bytes and never two spellings of the same
1769
1939
  * value; `type`, `origin`, `crossOrigin` and `topOrigin` come back as they were.
1770
1940
  *
1771
- * Supply `expectedType`, `expectedChallenge` and `expectedOrigin` and each is
1772
- * 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`
1773
1944
  * reports which ran, so a check that passed is distinguishable from one that never
1774
1945
  * happened. `expectedType` is worth setting on every call: the ceremony a response
1775
1946
  * belongs to is fixed, and accepting a `webauthn.create` where a `webauthn.get` was
1776
1947
  * expected is a credential-registration response replayed as a login.
1777
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
+ *
1778
1958
  * @opts
1779
1959
  * expectedType -- "webauthn.create" or "webauthn.get"
1780
- * expectedChallenge -- the challenge bytes this ceremony issued (Buffer)
1960
+ * expectedChallenge -- the challenge bytes this ceremony issued (BufferSource)
1781
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
1782
1964
  *
1783
1965
  * @example
1784
1966
  * // requires: `clientDataJSON` -- credential.response.clientDataJSON;
@@ -1795,9 +1977,9 @@ function parseClientData(bytes, opts) {
1795
1977
  opts = opts || {};
1796
1978
  if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
1797
1979
  guard.identifier.assertKnownKeys(opts, _CLIENT_DATA_OPTS, _err, "webauthn/bad-input", "opts has an unknown key ");
1798
- if (!Buffer.isBuffer(bytes) && !(bytes instanceof Uint8Array)) {
1799
- throw _err("webauthn/bad-input", "clientDataJSON must be the RAW Buffer or Uint8Array, not a parsed object");
1800
- }
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");
1801
1983
  var doc = guard.json.parse(Buffer.from(bytes), _err, {
1802
1984
  maxBytes: constants.LIMITS.JSON_MAX_BYTES, maxDepth: constants.LIMITS.JSON_MAX_DEPTH,
1803
1985
  badJson: "webauthn/bad-client-data", tooDeep: "webauthn/bad-client-data",
@@ -1837,7 +2019,7 @@ function parseClientData(bytes, opts) {
1837
2019
  throw _err("webauthn/bad-client-data", "clientDataJSON topOrigin must be a non-empty string when present (WebAuthn sec. 5.8.1)");
1838
2020
  }
1839
2021
 
1840
- var checked = { type: false, challenge: false, origin: false };
2022
+ var checked = { type: false, challenge: false, origin: false, topOrigin: false };
1841
2023
  if (opts.expectedType !== undefined) {
1842
2024
  if (CLIENT_DATA_TYPE[opts.expectedType] !== 1) {
1843
2025
  throw _err("webauthn/bad-input", "opts.expectedType must be \"webauthn.create\" or \"webauthn.get\"");
@@ -1850,10 +2032,7 @@ function parseClientData(bytes, opts) {
1850
2032
  checked.type = true;
1851
2033
  }
1852
2034
  if (opts.expectedChallenge !== undefined) {
1853
- if (!Buffer.isBuffer(opts.expectedChallenge) && !(opts.expectedChallenge instanceof Uint8Array)) {
1854
- throw _err("webauthn/bad-input", "opts.expectedChallenge must be the raw challenge bytes");
1855
- }
1856
- if (!guard.crypto.constantTimeEqual(Buffer.from(opts.expectedChallenge), challenge)) {
2035
+ if (!guard.crypto.constantTimeEqual(_bytesArg(opts.expectedChallenge, "opts.expectedChallenge"), challenge)) {
1857
2036
  throw _err("webauthn/client-data-mismatch",
1858
2037
  "the clientDataJSON challenge is not the one this ceremony issued (WebAuthn sec. 7.1 step 9 / sec. 7.2 step 12)");
1859
2038
  }
@@ -1874,6 +2053,48 @@ function parseClientData(bytes, opts) {
1874
2053
  }
1875
2054
  checked.origin = true;
1876
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
+ }
1877
2098
  return {
1878
2099
  type: doc.type, challenge: challenge, origin: doc.origin,
1879
2100
  crossOrigin: doc.crossOrigin === undefined ? false : doc.crossOrigin,
@@ -1882,14 +2103,14 @@ function parseClientData(bytes, opts) {
1882
2103
  };
1883
2104
  }
1884
2105
  var _CLIENT_DATA_OPTS = Object.assign(Object.create(null), {
1885
- expectedType: 1, expectedChallenge: 1, expectedOrigin: 1,
2106
+ expectedType: 1, expectedChallenge: 1, expectedOrigin: 1, expectedTopOrigin: 1,
1886
2107
  });
1887
2108
 
1888
2109
  // ---- public: parseAuthenticatorData / verifyAssertion ------------------------
1889
2110
 
1890
2111
  /**
1891
2112
  * @primitive pki.webauthn.parseAuthenticatorData
1892
- * @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 }
1893
2114
  * @since 0.5.0
1894
2115
  * @status experimental
1895
2116
  * @spec W3C WebAuthn Level 3 sec. 6.1
@@ -1913,10 +2134,7 @@ var _CLIENT_DATA_OPTS = Object.assign(Object.create(null), {
1913
2134
  * ad.signCount; // the authenticator's counter for this credential
1914
2135
  */
1915
2136
  function parseAuthenticatorData(bytes) {
1916
- if (!Buffer.isBuffer(bytes) && !(bytes instanceof Uint8Array)) {
1917
- throw _err("webauthn/bad-input", "authenticatorData must be a Buffer or Uint8Array");
1918
- }
1919
- return _parseAuthData(guard.bytes.snapshotSource(bytes, WebauthnError, "webauthn/bad-input", "authenticatorData"), _err);
2137
+ return _parseAuthData(_bytesArg(bytes, "authenticatorData"), _err);
1920
2138
  }
1921
2139
 
1922
2140
  // The options pki.webauthn.verifyAssertion recognises, null-prototype for the same
@@ -1925,12 +2143,12 @@ var _ASSERT_OPTS = Object.assign(Object.create(null), {
1925
2143
  authenticatorData: 1, clientDataHash: 1, clientDataJSON: 1, signature: 1,
1926
2144
  credentialPublicKey: 1, previousSignCount: 1,
1927
2145
  expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
1928
- expectedChallenge: 1, expectedOrigin: 1,
2146
+ expectedChallenge: 1, expectedOrigin: 1, expectedTopOrigin: 1,
1929
2147
  });
1930
2148
 
1931
2149
  /**
1932
2150
  * @primitive pki.webauthn.verifyAssertion
1933
- * @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 }>
1934
2152
  * @since 0.5.0
1935
2153
  * @status experimental
1936
2154
  * @spec W3C WebAuthn Level 3 sec. 7.2
@@ -1993,17 +2211,17 @@ function _snapshotAssertion(input) {
1993
2211
  guard.identifier.assertKnownKeys(input, _ASSERT_OPTS, _err, "webauthn/bad-input", "verifyAssertion input has an unknown key ");
1994
2212
  var out = {}, k;
1995
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.
1996
2217
  ["authenticatorData", "clientDataJSON", "clientDataHash", "signature", "expectedChallenge"].forEach(function (f) {
1997
- if (Buffer.isBuffer(out[f]) || out[f] instanceof Uint8Array || out[f] instanceof ArrayBuffer) {
1998
- out[f] = guard.bytes.snapshotSource(out[f], WebauthnError, "webauthn/bad-input", f);
1999
- }
2218
+ if (_isBufferSource(out[f])) out[f] = guard.bytes.snapshotSource(out[f], WebauthnError, "webauthn/bad-input", f);
2000
2219
  });
2001
2220
  // The BYTES form first. A Buffer satisfies the plain-object test below, so leaving it to that
2002
2221
  // branch copies its numeric indices into a `{0:.., 1:..}` object that is no longer a key at all
2003
2222
  // -- the stored credential silently becoming something the SPKI builder cannot read. It is
2004
2223
  // snapshotted here for the same reason the other byte inputs are: it is read after a yield.
2005
- if (Buffer.isBuffer(out.credentialPublicKey) || ArrayBuffer.isView(out.credentialPublicKey) ||
2006
- out.credentialPublicKey instanceof ArrayBuffer) {
2224
+ if (_isBufferSource(out.credentialPublicKey)) {
2007
2225
  out.credentialPublicKey = guard.bytes.snapshotSource(out.credentialPublicKey, WebauthnError,
2008
2226
  "webauthn/bad-input", "credentialPublicKey");
2009
2227
  } else if (_isPlainObject(out.credentialPublicKey)) {
@@ -2011,12 +2229,13 @@ function _snapshotAssertion(input) {
2011
2229
  for (kk in out.credentialPublicKey) {
2012
2230
  if (!Object.prototype.hasOwnProperty.call(out.credentialPublicKey, kk)) continue;
2013
2231
  var v = out.credentialPublicKey[kk];
2014
- 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;
2015
2233
  }
2016
2234
  out.credentialPublicKey = key;
2017
2235
  }
2018
2236
  if (Array.isArray(out.allowedAlgorithms)) out.allowedAlgorithms = out.allowedAlgorithms.slice();
2019
2237
  if (Array.isArray(out.expectedOrigin)) out.expectedOrigin = out.expectedOrigin.slice();
2238
+ if (Array.isArray(out.expectedTopOrigin)) out.expectedTopOrigin = out.expectedTopOrigin.slice();
2020
2239
  return out;
2021
2240
  }
2022
2241
 
@@ -2032,9 +2251,7 @@ function verifyAssertion(input) {
2032
2251
  return Promise.resolve().then(function () {
2033
2252
  input = frozen;
2034
2253
  var authData = parseAuthenticatorData(input.authenticatorData);
2035
- if (!Buffer.isBuffer(input.signature) && !(input.signature instanceof Uint8Array)) {
2036
- throw _err("webauthn/bad-input", "signature must be a Buffer or Uint8Array");
2037
- }
2254
+ input.signature = _bytesArg(input.signature, "signature");
2038
2255
  // The two forms of the same input, and NEITHER is inferred from the other's
2039
2256
  // absence: supplying both invites them to disagree, and picking one would make
2040
2257
  // the signature cover something the caller did not mean.
@@ -2044,9 +2261,7 @@ function verifyAssertion(input) {
2044
2261
  }
2045
2262
  var clientDataHash, clientData = null;
2046
2263
  if (haveJson) {
2047
- if (!Buffer.isBuffer(input.clientDataJSON) && !(input.clientDataJSON instanceof Uint8Array)) {
2048
- throw _err("webauthn/bad-input", "clientDataJSON must be a Buffer or Uint8Array (the RAW bytes, not a parsed object)");
2049
- }
2264
+ input.clientDataJSON = _bytesArg(input.clientDataJSON, "clientDataJSON");
2050
2265
  // Given the JSON, this verb reads it -- the ceremony TYPE is checked
2051
2266
  // unconditionally, because which ceremony a response belongs to is fixed by
2052
2267
  // the spec rather than chosen by the caller, and a registration response
@@ -2056,18 +2271,21 @@ function verifyAssertion(input) {
2056
2271
  expectedType: "webauthn.get",
2057
2272
  expectedChallenge: input.expectedChallenge,
2058
2273
  expectedOrigin: input.expectedOrigin,
2274
+ expectedTopOrigin: input.expectedTopOrigin,
2059
2275
  });
2060
2276
  clientDataHash = _sha("sha256", Buffer.from(input.clientDataJSON));
2061
2277
  } else {
2062
- 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) {
2063
2283
  throw _err("webauthn/bad-input",
2064
- "expectedChallenge / expectedOrigin are checked against clientDataJSON, which this call did not " +
2065
- "supply -- pass clientDataJSON instead of clientDataHash, or check them yourself");
2066
- }
2067
- if (!Buffer.isBuffer(input.clientDataHash) || input.clientDataHash.length !== 32) {
2068
- 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");
2069
2286
  }
2070
- 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");
2071
2289
  }
2072
2290
  // Either form a relying party can be holding. `verify` hands back the parsed object, but the
2073
2291
  // durable form is BYTES: the object carries Buffers, so a JSON round trip through a datastore
@@ -2103,14 +2321,26 @@ function verifyAssertion(input) {
2103
2321
  // signature would let anyone raise that alarm with arbitrary bytes, so the
2104
2322
  // counter is read only out of an assertion that proved to be authentic. The
2105
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.
2106
2332
  var signCountChecked = false;
2107
2333
  if (prev !== undefined) {
2108
- if (!(prev === 0 && authData.signCount === 0) && authData.signCount <= prev) {
2109
- throw _err("webauthn/sign-count-not-advanced",
2110
- "the assertion signCount " + authData.signCount + " does not advance past the stored " + prev +
2111
- ", 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;
2112
2343
  }
2113
- signCountChecked = true;
2114
2344
  }
2115
2345
  return {
2116
2346
  signatureVerified: true,