@blamejs/pki 0.6.13 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/README.md +1 -1
  3. package/index.js +1 -1
  4. package/lib/acme.js +59 -815
  5. package/lib/asn1-der.js +7 -245
  6. package/lib/attrcert-sign.js +12 -260
  7. package/lib/attrcert-verify.js +1 -7
  8. package/lib/byte-reader.js +0 -8
  9. package/lib/byte-writer.js +0 -11
  10. package/lib/cbor-det.js +0 -54
  11. package/lib/cmc-build.js +15 -357
  12. package/lib/cmc-verify.js +6 -239
  13. package/lib/cmp-build.js +41 -317
  14. package/lib/cmp-session.js +73 -760
  15. package/lib/cmp-verify.js +32 -488
  16. package/lib/cms-compress.js +3 -32
  17. package/lib/cms-decrypt.js +29 -294
  18. package/lib/cms-encrypt.js +14 -234
  19. package/lib/cms-sign.js +29 -324
  20. package/lib/cms-verify.js +9 -500
  21. package/lib/composite-kem.js +2 -102
  22. package/lib/composite-sig.js +1 -77
  23. package/lib/constants.js +0 -249
  24. package/lib/crl-sign.js +9 -237
  25. package/lib/crl-verify.js +1 -10
  26. package/lib/crmf-sign.js +19 -198
  27. package/lib/crmf-verify.js +1 -12
  28. package/lib/csr-sign.js +3 -34
  29. package/lib/csr-verify.js +1 -12
  30. package/lib/ct.js +28 -253
  31. package/lib/edwards-point.js +10 -44
  32. package/lib/est.js +28 -764
  33. package/lib/framework-error.js +4 -182
  34. package/lib/guard-all.js +1 -58
  35. package/lib/guard-async.js +1 -36
  36. package/lib/guard-bytes.js +6 -650
  37. package/lib/guard-compress.js +5 -102
  38. package/lib/guard-crypto.js +1 -40
  39. package/lib/guard-der.js +2 -93
  40. package/lib/guard-encoding.js +2 -60
  41. package/lib/guard-header.js +1 -36
  42. package/lib/guard-identifier.js +7 -531
  43. package/lib/guard-intrinsic.js +1 -150
  44. package/lib/guard-json.js +10 -73
  45. package/lib/guard-limits.js +1 -77
  46. package/lib/guard-list.js +1 -49
  47. package/lib/guard-name.js +3 -209
  48. package/lib/guard-parsed.js +7 -335
  49. package/lib/guard-range.js +1 -71
  50. package/lib/guard-secret.js +1 -102
  51. package/lib/guard-text.js +1 -64
  52. package/lib/guard-time.js +1 -97
  53. package/lib/hpke.js +3 -105
  54. package/lib/http-digest.js +14 -142
  55. package/lib/http-retry-after.js +1 -47
  56. package/lib/http-transport.js +21 -131
  57. package/lib/inspect.js +30 -284
  58. package/lib/ip-utils.js +6 -47
  59. package/lib/jose.js +7 -142
  60. package/lib/key.js +2 -67
  61. package/lib/lint.js +9 -194
  62. package/lib/merkle.js +1 -27
  63. package/lib/mime.js +9 -82
  64. package/lib/ocsp-request-verify.js +1 -14
  65. package/lib/ocsp-verify.js +1 -82
  66. package/lib/ocsp.js +11 -169
  67. package/lib/oid.js +5 -337
  68. package/lib/path-validate.js +103 -1492
  69. package/lib/pbes2.js +2 -89
  70. package/lib/pkcs12-build.js +31 -379
  71. package/lib/pki-build.js +64 -290
  72. package/lib/rc2.js +2 -18
  73. package/lib/rfc3339.js +4 -27
  74. package/lib/scep.js +59 -233
  75. package/lib/schema-all.js +0 -122
  76. package/lib/schema-attrcert.js +4 -146
  77. package/lib/schema-c509.js +153 -802
  78. package/lib/schema-cmc.js +5 -220
  79. package/lib/schema-cmp.js +2 -223
  80. package/lib/schema-cms.js +13 -427
  81. package/lib/schema-crl.js +5 -68
  82. package/lib/schema-crmf.js +6 -167
  83. package/lib/schema-csr.js +1 -54
  84. package/lib/schema-csrattrs.js +2 -87
  85. package/lib/schema-engine.js +0 -152
  86. package/lib/schema-ocsp.js +0 -117
  87. package/lib/schema-pkcs12.js +2 -93
  88. package/lib/schema-pkcs8.js +3 -35
  89. package/lib/schema-pkix.js +24 -655
  90. package/lib/schema-smime.js +0 -54
  91. package/lib/schema-tsp.js +3 -99
  92. package/lib/schema-x509.js +4 -63
  93. package/lib/shbs.js +11 -95
  94. package/lib/sign-scheme.js +2 -97
  95. package/lib/sigstore.js +9 -169
  96. package/lib/sleep.js +2 -8
  97. package/lib/smime.js +15 -408
  98. package/lib/tls-cert-compress.js +0 -62
  99. package/lib/trust.js +19 -185
  100. package/lib/tsp-sign.js +16 -270
  101. package/lib/validator-all.js +1 -30
  102. package/lib/validator-attcert.js +2 -46
  103. package/lib/validator-cose.js +2 -153
  104. package/lib/validator-keydesc.js +1 -37
  105. package/lib/validator-sig.js +1 -46
  106. package/lib/validator-tls.js +1 -88
  107. package/lib/validator-tpm.js +19 -122
  108. package/lib/verify-seam.js +1 -24
  109. package/lib/webauthn-mds.js +3 -400
  110. package/lib/webauthn.js +12 -833
  111. package/lib/webcrypto.js +5 -485
  112. package/lib/x509-sign.js +4 -108
  113. package/package.json +1 -1
  114. package/sbom.cdx.json +6 -6
package/lib/webauthn.js CHANGED
@@ -35,8 +35,6 @@ var validator = require("./validator-all");
35
35
  var edwardsPoint = require("./edwards-point");
36
36
  var guard = require("./guard-all");
37
37
  var intrinsic = require("./guard-intrinsic");
38
- // Own-key membership through an operation taken at load: written out it reads two replaceable
39
- // properties to answer one question, and either answering true admits a name the table never had.
40
38
  var _hasOwn = intrinsic.hasOwn;
41
39
  var jose = require("./jose");
42
40
  var mds = require("./webauthn-mds");
@@ -44,9 +42,6 @@ var nodeCrypto = require("crypto");
44
42
 
45
43
  var WebauthnError = frameworkError.WebauthnError;
46
44
  function _err(code, message, cause) { return new WebauthnError(code, message, cause); }
47
- // The shared certificate-extension decoder registry (keyed by OID), so an
48
- // attestation-certificate extension (extKeyUsage, subjectAltName) is decoded by the
49
- // same fail-closed pkix decoder every other format uses, not a local hand-roll.
50
45
  var NS = pkix.makeNS("webauthn", WebauthnError, oid);
51
46
  var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
52
47
  function _decodeExt(cert, oidName) {
@@ -58,25 +53,12 @@ function _decodeExt(cert, oidName) {
58
53
  }
59
54
  var subtle = webcrypto.webcrypto.subtle;
60
55
 
61
- // A one-shot digest over a bounded buffer (the attestation nonce / TPM Name / TPM
62
- // extraData preimages), by the node hash name the caller has already resolved.
63
56
  function _sha(name, buf) { return nodeCrypto.createHash(name).update(buf).digest(); }
64
- // Unsigned big-endian magnitude compare: strip leading zero octets, then byte-equal.
65
- // TPM2B buffers and COSE fixed-length coordinates can differ by a leading 0x00, so a
66
- // raw memcmp would spuriously reject an equal key (WebAuthn 8.3, item 22).
67
57
  function _ucmp(a, b) {
68
58
  function strip(x) { var i = 0; while (i < x.length - 1 && x[i] === 0) i++; return x.subarray(i); }
69
59
  return Buffer.compare(strip(a), strip(b)) === 0;
70
60
  }
71
- // A decoded node is a primitive universal INTEGER (so `.content` is a real buffer,
72
- // not null as it is for a constructed node).
73
61
  function _isInteger(node) { return !!node && !node.constructed && node.tagClass === "universal" && node.tagNumber === asn1.TAGS.INTEGER; }
74
- // alg -> the digest a TPM attestation's certInfo.extraData is taken under (its only
75
- // consumer). Every ECDSA/RSA algorithm a TPM AIK may sign with, including the RFC 9864
76
- // fully-specified ECDSA ids (ESP256/384/512) -- MUST appear here, or the TPM extraData
77
- // step rejects the attestation before the signature is evaluated. EdDSA (-8/-19/-53) is
78
- // absent by design: a TPM 2.0 AIK never signs with EdDSA, so such an attestation is
79
- // correctly refused.
80
62
  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" };
81
63
  function _coseAlgHash(alg, E) {
82
64
  var h = COSE_ALG_HASH[String(alg)];
@@ -84,11 +66,7 @@ function _coseAlgHash(alg, E) {
84
66
  return h;
85
67
  }
86
68
 
87
- // ---- authenticatorData bounded reader (WebAuthn sec. 6.1) --------------------
88
69
 
89
- // authData = rpIdHash[32] || flags[1] || signCount[4 BE] || (AT? attestedCredentialData) || (ED? extensions CBOR).
90
- // A bounded big-endian read: every slice is length-checked before it is taken, so a
91
- // truncated / oversize field fails closed and never reads past the buffer.
92
70
  var AAGUID_LEN = 16;
93
71
  function _parseAuthData(buf, E) {
94
72
  if (!Buffer.isBuffer(buf) || buf.length < 37) throw E("webauthn/bad-auth-data", "authenticatorData is shorter than the 37-byte minimum (RFC WebAuthn sec. 6.1)");
@@ -99,12 +77,7 @@ function _parseAuthData(buf, E) {
99
77
  signCount: buf.readUInt32BE(33),
100
78
  aaguid: null, credentialId: null, credentialPublicKey: null, credentialPublicKeyBytes: null, extensions: null,
101
79
  };
102
- // Backup State is only valid when Backup Eligibility is set: a credential cannot be
103
- // "backed up" if it is not "backup eligible" (WebAuthn sec. 6.1).
104
80
  if (out.flags.bs && !out.flags.be) throw E("webauthn/bad-auth-data", "authenticatorData sets Backup State (BS) without Backup Eligibility (BE) (WebAuthn sec. 6.1)");
105
- // The reserved flag bits (bit 1 = 0x02, bit 5 = 0x20) are undefined; a conforming
106
- // authenticator leaves them 0. Reject a set reserved bit; never ignore an
107
- // unknown flag (WebAuthn sec. 6.1, fail-closed on undefined structure).
108
81
  if (flags & 0x22) throw E("webauthn/bad-auth-data", "authenticatorData sets a reserved (RFU) flag bit (WebAuthn sec. 6.1)");
109
82
  var off = 37;
110
83
  if (out.flags.at) {
@@ -114,7 +87,6 @@ function _parseAuthData(buf, E) {
114
87
  if (credLen < 1 || credLen > 1023) throw E("webauthn/bad-credential-id", "credentialIdLength " + credLen + " is outside 1..1023 (RFC WebAuthn sec. 6.1)");
115
88
  if (buf.length < off + credLen) throw E("webauthn/bad-auth-data", "credentialId overruns authenticatorData");
116
89
  out.credentialId = buf.subarray(off, off + credLen); off += credLen;
117
- // credentialPublicKey is a COSE_Key: a single CBOR map occupying [off, its end).
118
90
  var keyNode;
119
91
  try { keyNode = cbor.decode(buf.subarray(off), { allowTrailing: true }); }
120
92
  catch (e) { throw E("webauthn/bad-cose-key", "the credential public key is not well-formed CBOR", e); }
@@ -123,28 +95,18 @@ function _parseAuthData(buf, E) {
123
95
  off += keyNode.bytes.length;
124
96
  }
125
97
  if (out.flags.ed) {
126
- // With the ED flag set, the remainder MUST be exactly one well-formed CBOR map
127
- // (the extensions); the strict decoder rejects a non-map, malformed bytes, an
128
- // empty remainder, or trailing bytes (RFC WebAuthn sec. 6.1).
129
98
  var extNode;
130
99
  try { extNode = cbor.decode(buf.subarray(off)); }
131
100
  catch (e) { throw E("webauthn/bad-auth-data", "the authenticatorData extensions are not a single well-formed CBOR map", e); }
132
101
  if (extNode.majorType !== 5) throw E("webauthn/bad-auth-data", "the authenticatorData extensions must be a CBOR map");
133
102
  out.extensions = buf.subarray(off);
134
103
  } else if (off < buf.length) {
135
- // authenticatorData is fixed-layout: with the ED flag clear there MUST be no
136
- // bytes after the attestedCredentialData (RFC WebAuthn sec. 6.1). Trailing
137
- // bytes are a malformed structure, so fail closed and never ignore them.
138
104
  throw E("webauthn/bad-auth-data", "authenticatorData has trailing bytes after attestedCredentialData with the ED flag clear");
139
105
  }
140
106
  return out;
141
107
  }
142
108
 
143
- // ---- COSE_Key decode (WebAuthn sec. 6.5.1, RFC 9052 sec. 7) ------------------
144
109
 
145
- // The complete COSE credential-key conformance rule set (kty/alg/crv/length/canonical/
146
- // profile/on-curve) lives in validator-cose, composed here so every credential key
147
- // routes through the one home -- never a per-format re-derivation of a partial subset.
148
110
  function _decodeCoseKey(node) {
149
111
  return validator.cose.credentialKey(node, WebauthnError, "webauthn/bad-cose-key", "webauthn/unsupported-algorithm");
150
112
  }
@@ -191,20 +153,12 @@ function parseCoseKey(bytes) {
191
153
  return _decodeCoseKey(node);
192
154
  }
193
155
 
194
- // ---- signature verification bridge ------------------------------------------
195
156
 
196
- // COSE algorithm id -> the WebCrypto import + verify descriptor. WebAuthn packed /
197
- // android-key / tpm ECDSA signatures are DER ECDSA-Sig-Value, so they are converted
198
- // to the raw r||s form pki.webcrypto (ieee-p1363) expects.
199
157
  var COSE_ALG = {
200
158
  "-7": { imp: { name: "ECDSA", namedCurve: "P-256" }, verify: { name: "ECDSA", hash: "SHA-256" }, ecdsa: 32 },
201
159
  "-35": { imp: { name: "ECDSA", namedCurve: "P-384" }, verify: { name: "ECDSA", hash: "SHA-384" }, ecdsa: 48 },
202
160
  "-36": { imp: { name: "ECDSA", namedCurve: "P-521" }, verify: { name: "ECDSA", hash: "SHA-512" }, ecdsa: 66 },
203
161
  "-8": { imp: { name: "Ed25519" }, verify: { name: "Ed25519" }, ecdsa: 0 },
204
- // RFC 9864 fully-specified ids. WebAuthn recommends against them for credential creation,
205
- // but a verifier MUST evaluate an assertion signed under one. ESP256/384/512 are the
206
- // curve-pinned ECDSA twins of ES256/384/512; Ed25519(-19)/Ed448(-53) are fully-specified
207
- // EdDSA (Ed448 is the ONLY WebAuthn path to Ed448 -- -8 is Ed25519 only).
208
162
  "-9": { imp: { name: "ECDSA", namedCurve: "P-256" }, verify: { name: "ECDSA", hash: "SHA-256" }, ecdsa: 32 },
209
163
  "-51": { imp: { name: "ECDSA", namedCurve: "P-384" }, verify: { name: "ECDSA", hash: "SHA-384" }, ecdsa: 48 },
210
164
  "-52": { imp: { name: "ECDSA", namedCurve: "P-521" }, verify: { name: "ECDSA", hash: "SHA-512" }, ecdsa: 66 },
@@ -213,45 +167,25 @@ var COSE_ALG = {
213
167
  "-257": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
214
168
  "-258": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
215
169
  "-259": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
216
- // RSASSA-PSS. The salt length is the hash length, the profile WebCrypto verifies and the one
217
- // RFC 8230 sec. 2 fixes for the COSE PS* identifiers -- 32 / 48 / 64 bytes for SHA-256/384/512.
218
170
  "-37": { imp: { name: "RSA-PSS", hash: "SHA-256" }, verify: { name: "RSA-PSS", saltLength: 32 }, ecdsa: 0 },
219
171
  "-38": { imp: { name: "RSA-PSS", hash: "SHA-384" }, verify: { name: "RSA-PSS", saltLength: 48 }, ecdsa: 0 },
220
172
  "-39": { imp: { name: "RSA-PSS", hash: "SHA-512" }, verify: { name: "RSA-PSS", saltLength: 64 }, ecdsa: 0 },
221
- // RS1 (RSASSA-PKCS1-v1_5 / SHA-1): a legacy COSE algorithm real Windows Hello TPM
222
- // authenticators emit in their attestation statement. VERIFY-only support -- the
223
- // toolkit never signs with SHA-1; it must still evaluate the attestations that
224
- // ship using it, or a large class of TPM authenticators cannot be verified.
225
173
  "-65535": { imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-1" }, verify: { name: "RSASSA-PKCS1-v1_5" }, ecdsa: 0 },
226
174
  };
227
175
 
228
- // DER ECDSA-Sig-Value { r, s } -> raw r||s (the ieee-p1363 form WebCrypto verify expects).
229
- // validator-sig owns the complete strict-DER conformance PLUS the FIPS 186-5 sec. 6.4.2 order bound
230
- // (r,s in [1, n-1] -- the order-aware gate a curve-aware verifier MUST use), composed here.
231
176
  function _derEcdsaToRaw(der, curve) { return validator.sig.ecdsaDerToP1363(der, curve, WebauthnError, "webauthn/bad-signature"); }
232
177
 
233
- // Verify `sig` over `message` with the SPKI public key `spkiBytes` under COSE `alg`.
234
- // A wrong signature resolves `false` from subtle.verify without throwing (a false
235
- // verdict is a verdict); a structural failure -- an unimportable key, a bad
236
- // descriptor -- is re-thrown as a typed fail-closed error, never masked as false.
237
178
  function _verifySig(alg, sig, spkiBytes, message, E) {
238
179
  var d = COSE_ALG[String(alg)];
239
180
  if (!d) throw _err("webauthn/unsupported-algorithm", "unsupported COSE algorithm " + alg);
240
181
  var imp = d.imp, ver = d.verify;
241
- // COSE alg -8 (EdDSA) covers Ed25519 and Ed448; the WebCrypto name follows the
242
- // signing key's own SPKI algorithm OID, not the (curve-agnostic) alg id.
243
182
  if (alg === -8) { var nm = _edName(spkiBytes, E); imp = { name: nm }; ver = { name: nm }; }
244
- // node/OpenSSL imports any Ed25519/Ed448 SPKI without validating the point, and a
245
- // low-order (e.g. all-zeroes) key verifies a trivial signature -- so validate the OKP
246
- // point before verify. This covers EVERY key that signs a WebAuthn statement: the x5c
247
- // attestation-certificate key (packed/tpm/apple) AND the self-attestation credential key.
248
183
  if (imp.name === "Ed25519" || imp.name === "Ed448") _requireValidEdPoint(spkiBytes, imp.name);
249
184
  var s = d.ecdsa ? _derEcdsaToRaw(sig, d.imp.namedCurve) : sig;
250
185
  return subtle.importKey("spki", spkiBytes, imp, false, ["verify"])
251
186
  .then(function (key) { return subtle.verify(ver, key, s, message); })
252
187
  .catch(function (e) { throw _err("webauthn/verify-error", "the attestation signature could not be evaluated", e); });
253
188
  }
254
- // The WebCrypto EdDSA name (Ed25519 / Ed448) an SPKI declares via its algorithm OID.
255
189
  var ED_OID_NAME = {};
256
190
  ED_OID_NAME[oid.byName("Ed25519")] = "Ed25519";
257
191
  ED_OID_NAME[oid.byName("Ed448")] = "Ed448";
@@ -263,48 +197,20 @@ function _edName(spkiBytes, E) {
263
197
  if (!nm) throw E("webauthn/unsupported-algorithm", "unsupported EdDSA curve OID " + algOid);
264
198
  return nm;
265
199
  }
266
- // The raw Edwards point an OKP SPKI carries MUST be a valid, full-order point -- reject an
267
- // off-curve or low-order key before it verifies a signature, which WebCrypto import will not do.
268
- // Routed through edwards-point's own SPKI entry rather than extracting the point here: that module
269
- // documents itself as the one home every EdDSA verify path takes an SPKI through, and this was the
270
- // path that had its own copy. Keeping a second extraction is how one route comes to be guarded and
271
- // the next one written is not.
272
200
  function _requireValidEdPoint(spkiBytes, name) {
273
201
  edwardsPoint.validateSpki(spkiBytes, name === "Ed25519" ? 6 : 7, WebauthnError, "webauthn/bad-signature");
274
202
  }
275
203
 
276
- // A validated COSE credential key -> a self-contained SPKI the WebCrypto import
277
- // consumes, so a credential public key and a certificate key verify by one path.
278
- // validator-cose owns the encoding (EC2 / RSA / OKP), composed here.
279
204
  function _coseKeyToSpki(key) { return validator.cose.toSpki(key, WebauthnError, "webauthn/bad-cose-key"); }
280
205
 
281
- // The attestation-certificate subject public key MUST equal the credential public
282
- // key that authenticatorData carries (WebAuthn 8.4/8.8 item 30). Compare the raw
283
- // key material unsigned: an EC2 uncompressed point's X/Y vs the COSE x/y; an RSA
284
- // modulus/exponent vs the COSE n/e. `cert.subjectPublicKeyInfo.publicKey.bytes` is
285
- // the BIT STRING key material the SPKI carries.
286
- // The named-curve OID a certificate's EC SubjectPublicKeyInfo declares (the DER
287
- // OBJECT IDENTIFIER in the algorithm parameters). Fail-closed: absent or malformed
288
- // curve parameters throw a typed key-mismatch -- never swallowed to a null the
289
- // caller would compare as "no match by default".
290
206
  function _certEcCurveOid(cert, E) {
291
207
  var params = cert.subjectPublicKeyInfo && cert.subjectPublicKeyInfo.algorithm && cert.subjectPublicKeyInfo.algorithm.parameters;
292
208
  if (!Buffer.isBuffer(params)) throw E("webauthn/key-mismatch", "the attestation certificate EC key carries no named-curve parameters");
293
209
  try { return asn1.read.oid(asn1.decode(params)); }
294
210
  catch (e) { throw E("webauthn/key-mismatch", "the attestation certificate EC curve is not a valid OBJECT IDENTIFIER", e); }
295
211
  }
296
- // Which certificate key ALGORITHMS a credential key of each COSE type may be carried by. Two keys
297
- // are the same key only if they are the same KIND of key: the bytes alone do not say what a key is,
298
- // and for the Edwards curves they cannot -- an X25519 key-agreement key and an Ed25519 signing key
299
- // are both 32 raw bytes, so a certificate declaring the former would otherwise compare equal to a
300
- // credential key declaring the latter and be accepted as its attestation certificate. The EC2
301
- // branch already asked this question as a curve check; asking it only there left the two key types
302
- // whose confusion is undetectable from the material to be decided on the material alone.
303
- // A row per COSE key type, keyed the way every other algorithm decision in this toolkit is.
304
212
  function _certKeyAlgNames(cose) {
305
213
  if (cose.kty === 2) return ["ecPublicKey"];
306
- // An RSA key may be carried under the general OID or under id-RSASSA-PSS, which restricts it to
307
- // PSS but is the same key (RFC 4055 sec. 1.2).
308
214
  if (cose.kty === 3) return ["rsaEncryption", "rsassaPss"];
309
215
  if (cose.kty === 1) {
310
216
  var okp = validator.cose.OKP_CRV[cose.crv];
@@ -320,15 +226,11 @@ function _assertCertKeyAlgorithm(cert, cose, E) {
320
226
  " key, which is not the kind of key the credential public key declares");
321
227
  }
322
228
  }
323
- // A void assert: throws webauthn/key-mismatch on any inequality, returns nothing on
324
- // success (called for its throw side-effect, like the other _check* asserts).
325
229
  function _certPubKeyEqualsCose(cert, cose, E) {
326
230
  var raw = cert.subjectPublicKeyInfo && cert.subjectPublicKeyInfo.publicKey && cert.subjectPublicKeyInfo.publicKey.bytes;
327
231
  if (!raw) throw E("webauthn/key-mismatch", "the attestation certificate exposes no public key");
328
232
  _assertCertKeyAlgorithm(cert, cose, E);
329
233
  if (cose.kty === 2) {
330
- // The certificate's declared EC curve MUST equal the credential key's curve --
331
- // a curve substitution is a different key even if the coordinate bytes line up.
332
234
  var wantCurve = validator.cose.EC2_CRV_OID[cose.crv];
333
235
  if (!wantCurve) throw E("webauthn/key-mismatch", "unsupported credential EC curve " + cose.crv);
334
236
  if (_certEcCurveOid(cert, E) !== oid.byName(wantCurve)) throw E("webauthn/key-mismatch", "the attestation certificate EC curve does not equal the credential key curve");
@@ -341,8 +243,6 @@ function _certPubKeyEqualsCose(cert, cose, E) {
341
243
  if (cose.kty === 3) {
342
244
  var seq;
343
245
  try { seq = asn1.decode(raw); } catch (e) { throw E("webauthn/key-mismatch", "the attestation certificate RSA key is not decodable", e); }
344
- // RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }; both
345
- // MUST be primitive INTEGERs (a constructed child has a null content buffer).
346
246
  if (!seq.children || seq.children.length !== 2 || !_isInteger(seq.children[0]) || !_isInteger(seq.children[1])) {
347
247
  throw E("webauthn/key-mismatch", "the attestation certificate RSA key is malformed");
348
248
  }
@@ -351,31 +251,21 @@ function _certPubKeyEqualsCose(cert, cose, E) {
351
251
  return;
352
252
  }
353
253
  if (cose.kty === 1) {
354
- // OKP Ed25519 / Ed448: the SPKI BIT STRING body IS the raw public key (fixed
355
- // width, so a byte-exact compare, not a leading-zero-stripping unsigned compare).
356
254
  if (!cose.x || !Buffer.from(raw).equals(cose.x)) throw E("webauthn/key-mismatch", "the attestation certificate OKP key does not equal the credential public key");
357
255
  return;
358
256
  }
359
257
  throw E("webauthn/key-mismatch", "unsupported credential key type for the certificate comparison");
360
258
  }
361
259
 
362
- // ---- TPM structure conformance (WebAuthn 8.3; TCG TPM 2.0 Part 2) -------------
363
- // The TPMT_PUBLIC (pubArea) + TPMS_ATTEST (certInfo) decode + the pubArea-key binding
364
- // live in validator-tpm, composed here. A malformed structure is bad-tpm; a key that
365
- // does not equal the credential key is key-mismatch.
366
260
  function _parseTpmPubArea(buf) { return validator.tpm.parsePubArea(buf, WebauthnError, "webauthn/bad-tpm"); }
367
261
  function _parseTpmCertInfo(buf) { return validator.tpm.parseCertInfo(buf, WebauthnError, "webauthn/bad-tpm"); }
368
262
  function _tpmPubKeyEqualsCose(pub, cose) { validator.tpm.pubKeyEqualsCose(pub, cose, WebauthnError, "webauthn/key-mismatch", "webauthn/bad-tpm"); }
369
263
 
370
- // ---- extension helpers -------------------------------------------------------
371
- // `oidName` is a registered name (byName resolves it at call time); an unregistered
372
- // name is a programming error that surfaces as an undefined target, never swallowed.
373
264
  function _findExt(cert, oidName) {
374
265
  var target = oid.byName(oidName);
375
266
  return (cert.extensions || []).filter(function (e) { return e.oid === target; })[0] || null;
376
267
  }
377
268
 
378
- // ---- public: parseAttestationObject -----------------------------------------
379
269
 
380
270
  /**
381
271
  * @primitive pki.webauthn.parseAttestationObject
@@ -404,15 +294,8 @@ function parseAttestationObject(bytes) {
404
294
  try { root = cbor.decode(bytes); } catch (e) { throw _err("webauthn/bad-attestation-object", "the attestation object is not well-formed CBOR", e); }
405
295
  if (root.majorType !== 5) throw _err("webauthn/bad-attestation-object", "the attestation object must be a CBOR map { fmt, attStmt, authData }");
406
296
  var fmtN = cbor.read.mapGet(root, "fmt"), attStmtN = cbor.read.mapGet(root, "attStmt"), authDataN = cbor.read.mapGet(root, "authData");
407
- // The attestation object is EXACTLY { fmt, attStmt, authData } -- no more, no fewer
408
- // (WebAuthn 6.5.4); an extra top-level key is a non-canonical envelope, rejected.
409
297
  if (root.children.length !== 3 || !fmtN || !attStmtN || !authDataN) throw _err("webauthn/bad-attestation-object", "the attestation object must be exactly { fmt, attStmt, authData }");
410
298
  if (fmtN.majorType !== 3) throw _err("webauthn/bad-attestation-object", "attestation object 'fmt' must be a text string");
411
- // attStmt is the attestation statement, a CBOR map keyed by field name (WebAuthn 6.5.4) for every
412
- // format but one: sec. 8.9 gives compound an ARRAY of nested statements. Which shape a format
413
- // takes is a registry row, not a branch, so adding a format cannot silently widen the envelope
414
- // for the others -- a non-map value (whose children are single nodes, not { key, value } pairs)
415
- // must never reach the per-field statement walk of a format that expects a map.
416
299
  var wantMajor = ATT_STMT_MAJOR[cbor.read.textString(fmtN)];
417
300
  if (wantMajor === undefined) wantMajor = 5;
418
301
  if (attStmtN.majorType !== wantMajor) {
@@ -428,16 +311,12 @@ function parseAttestationObject(bytes) {
428
311
  };
429
312
  }
430
313
 
431
- // ---- attestation-format verifiers -------------------------------------------
432
314
 
433
315
  function _reqAttr(map, key) {
434
316
  var n = cbor.read.mapGet(map, key);
435
317
  if (!n) throw _err("webauthn/bad-att-stmt", "the attestation statement is missing the '" + key + "' field");
436
318
  return n;
437
319
  }
438
- // Read a required attStmt field of an expected CBOR type, mapping a wrong-type
439
- // cbor/* fault to the webauthn domain (an attStmt field is not-well-formed input to
440
- // this layer, so it is a webauthn/bad-att-stmt verdict, not a leaked codec error).
441
320
  function _attRead(map, key, reader, what) {
442
321
  var n = _reqAttr(map, key);
443
322
  try { return reader(n); }
@@ -445,42 +324,20 @@ function _attRead(map, key, reader, what) {
445
324
  }
446
325
  function _algOf(attStmt) { return Number(_attRead(attStmt, "alg", cbor.read.int, "an integer")); }
447
326
  function _sigOf(attStmt) { return _attRead(attStmt, "sig", cbor.read.byteString, "a byte string"); }
448
- // A format's attStmt is its CANONICAL field set: every present key must be in
449
- // `allowed`, and every key in `required` must be present. An unexpected field is a
450
- // non-canonical statement, rejected before any field is trusted (WebAuthn sec. 8.*).
451
327
  function _requireAttShape(attStmt, allowed, required) {
452
328
  var have = {};
453
- // attStmt is the attestation-statement map (parseAttestationObject established it is a CBOR
454
- // map). cbor.read.map asserts the major type and yields the { key, value } pairs -- never a
455
- // raw .children walk, whose entries are single nodes (not pairs) for a non-map, so a pair
456
- // index would read undefined and throw a raw TypeError instead of failing closed.
457
329
  cbor.read.map(attStmt).forEach(function (kv) {
458
- // attStmt keys are text strings; a non-text key is a malformed statement,
459
- // rejected (not silently skipped, which would evade the unexpected-field check).
460
330
  if (kv[0].majorType !== 3) throw _err("webauthn/bad-att-stmt", "the attestation statement has a non-text-string field key");
461
331
  have[cbor.read.textString(kv[0])] = true;
462
332
  });
463
333
  Object.keys(have).forEach(function (k) { if (allowed.indexOf(k) === -1) throw _err("webauthn/bad-att-stmt", "the attestation statement carries an unexpected field '" + k + "'"); });
464
334
  required.forEach(function (k) { if (!have[k]) throw _err("webauthn/bad-att-stmt", "the attestation statement is missing the '" + k + "' field"); });
465
335
  }
466
- // The CBOR major type each format's attStmt takes. Every format uses a map (5) except compound,
467
- // whose sec. 8.9 syntax is an array of nested statements. A data row rather than a branch, so a
468
- // future format declares its shape here instead of loosening the shared envelope check.
469
336
  var ATT_STMT_MAJOR = { compound: 4 };
470
337
 
471
- // Which attestation formats put the AAGUID under their signature. A registry row rather than a
472
- // branch, so adding a format forces the question to be answered for it rather than inheriting the
473
- // permissive default. Every format signs over authenticatorData (which contains the AAGUID) except
474
- // fido-u2f, whose sec. 8.6 verificationData is assembled from named fields and omits it.
475
- // The question is asked PER FORMAT, and answered for the element being judged -- never once for a
476
- // whole statement. A compound mixes formats, and a single answer would be wrong in both directions:
477
- // it would either trust a u2f element's unsigned AAGUID, or refuse to look a packed element up by
478
- // the very AAGUID its own signature covers.
479
338
  var _AAGUID_SIGNED_BY_FMT = Object.assign(Object.create(null), { "fido-u2f": false });
480
339
  function _aaguidIsSigned(fmt) { return _AAGUID_SIGNED_BY_FMT[fmt] !== false; }
481
340
 
482
- // The options pki.webauthn.verify recognizes. Null-prototype, so a caller-supplied key cannot
483
- // resolve to an inherited Object member and read as recognized.
484
341
  var _VERIFY_OPTS = Object.assign(Object.create(null), {
485
342
  time: 1, metadata: 1, tpmPolicy: 1, safetyNetRoots: 1, verifySafetyNetJws: 1, requireCtsProfileMatch: 1,
486
343
  expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
@@ -488,52 +345,8 @@ var _VERIFY_OPTS = Object.assign(Object.create(null), {
488
345
  clientDataJSON: 1, expectedChallenge: 1, expectedOrigin: 1, expectedTopOrigin: 1,
489
346
  });
490
347
 
491
- // The boolean switches whose only reader sits inside ONE format's arm, so nothing else would ever
492
- // examine them. The binding switches are absent because _applyBindings owns their type and runs on
493
- // every format and on the assertion path too -- a second copy of that rule here would be a second
494
- // place for it to drift.
495
348
  var _FORMAT_SCOPED_BOOLEAN_OPTS = ["verifySafetyNetJws", "requireCtsProfileMatch"];
496
349
 
497
- // Anchor an attestation's trust path to roots the CALLER pins. The metadata route
498
- // resolves an authenticator's roots from the catalogue that registered it, which is
499
- // the stronger source -- but it only reaches models the catalogue lists, and some
500
- // are not there: Apple does not publish its authenticators to the FIDO Metadata
501
- // Service, and the Google hardware-attestation roots are distributed by Google. For
502
- // those formats the catalogue route resolves nothing, so without this there is no
503
- // way to anchor them at all and a trust path comes back unchecked. (android-safetynet
504
- // already had its own `safetyNetRoots` for the same reason; this is that carve-out
505
- // made general.)
506
- //
507
- // PRECEDENCE, stated rather than implied: `metadata` governs when it is supplied,
508
- // because a model's own registered roots are a stronger claim than a static pin, and
509
- // its status reports can disqualify a model these roots would still accept.
510
- // `rootCertificates` is the fallback for the models the catalogue does not cover.
511
- // Supplying both is not an error -- it is the ordinary configuration for a relying
512
- // party that accepts MDS-listed authenticators AND Apple.
513
- // The caller's pinned roots are copied SYNCHRONOUSLY, at the entry point, because
514
- // they are not read until the attestation verifier has resolved -- a later promise
515
- // turn. Both the ARRAY and each DER buffer in it stay caller-owned across that gap,
516
- // so a caller that recycles the array or overwrites a certificate's bytes in between
517
- // would have the attestation anchored against the replacement roots while the verdict
518
- // still reports `anchoredTo: "rootCertificates"` (CWE-367 reaching a wrong trust
519
- // decision). Same defense, and the same reason, as the assertion input's snapshot.
520
- // Shape faults are NOT raised here: the array is validated where it is consumed, so
521
- // supplying a malformed pin alongside a metadata catalogue that answers keeps failing
522
- // exactly where it did before.
523
- // A parsed certificate is a tree of plain objects, arrays and Buffers, and the anchor
524
- // comparison downstream reads its `subject` and `subjectPublicKeyInfo` -- both nested
525
- // Buffers the caller still owns. Copying only the top-level array would leave those
526
- // aliased, so the parsed form is deep-copied: every Buffer is duplicated and every
527
- // container rebuilt, while the value types a parsed certificate carries (Date for a
528
- // validity bound, BigInt for a serial) are preserved rather than flattened.
529
- //
530
- // The depth bound is a RECURSION BACKSTOP set far above any real certificate, never the
531
- // precision mechanism -- a bound tuned to today's shapes silently rejects a legitimate root
532
- // the moment a deeper one appears. A measured certificate reaches depth 5, and the deepest
533
- // structure the profile allows -- a certificate policy whose qualifier carries a UserNotice
534
- // with a noticeRef and its noticeNumbers -- lands near 12, so a cap of 12 would sit exactly
535
- // on a conforming root. This is set well clear of that; it exists only so a cyclic or hostile
536
- // object cannot recurse without end.
537
350
  function _cloneParsed(v, depth) {
538
351
  if (depth > 64) throw _err("webauthn/bad-input", "opts.rootCertificates[] is nested too deeply to be a parsed certificate");
539
352
  if (guard.bytes.isByteSource(v)) return guard.bytes.snapshotSource(v, _err, "webauthn/bad-input", "a byte field of opts.rootCertificates[]");
@@ -544,21 +357,9 @@ function _cloneParsed(v, depth) {
544
357
  Object.keys(v).forEach(function (k) { out[k] = _cloneParsed(v[k], depth + 1); });
545
358
  return out;
546
359
  }
547
- return v; // string, number, boolean, bigint, null, undefined -- all immutable
360
+ return v;
548
361
  }
549
362
 
550
- // A caller-owned byte argument, copied so nothing downstream reads bytes the caller can still
551
- // rewrite, and accepted in every form the W3C BufferSource contract defines.
552
- //
553
- // ONE accepted set, for every byte argument on this namespace. The forms differed per argument
554
- // before -- an attestation object took an ArrayBuffer while the clientDataHash beside it took only
555
- // a Buffer -- and the caller producing both is the same one: `crypto.subtle.digest` returns an
556
- // ArrayBuffer, so the natural way to compute that hash produced a value this verb refused. Which
557
- // forms an argument accepts is not a per-argument decision; it is the namespace's contract, and it
558
- // is made once here.
559
- //
560
- // Anything that is not a BufferSource is refused BY NAME, rather than passed down to be described
561
- // by whichever parser reaches it first.
562
363
  function _isBufferSource(v) { return guard.bytes.isByteSource(v); }
563
364
  function _bytesArg(v, label) {
564
365
  if (_isBufferSource(v)) {
@@ -570,87 +371,38 @@ function _bytesArg(v, label) {
570
371
  function _snapshotRoots(supplied) {
571
372
  if (!Array.isArray(supplied)) return supplied;
572
373
  return supplied.map(function (root) {
573
- // Every byte form, tested BEFORE the parsed-object branch: a DataView is an object too, so a
574
- // narrower byte test does not merely miss the copy -- it sends those bytes down the branch for
575
- // parsed certificates, which deep-copies a typed array field by field into something that is no
576
- // longer a certificate at all.
577
374
  if (_isBufferSource(root)) {
578
375
  return guard.bytes.snapshotSource(root, WebauthnError, "webauthn/bad-input", "opts.rootCertificates[]");
579
376
  }
580
- // A PARSED certificate is kept AS IT IS, not deep-copied. The copy was this function's way of
581
- // stopping a caller mutating a root after it was accepted, and it solved that by making a
582
- // detached twin -- which loses the parser's record, so the door below could no longer re-derive
583
- // the anchor from the bytes it was read from. The record is the stronger form of the same
584
- // protection: the anchor is re-parsed from those bytes, so an edit made afterwards, at any depth,
585
- // is discarded rather than copied. Cloning would trade that for a snapshot of a mutable object.
586
- return root; // a PEM string is immutable; a parsed certificate carries its own provenance
377
+ return root;
587
378
  });
588
379
  }
589
380
 
590
- // `supplied` is the SNAPSHOT taken at the entry point, never `opts.rootCertificates`
591
- // read afresh here -- that re-read is the window this guard closes. `opts` is still
592
- // threaded for the non-aliasing options (the validation time).
593
- // `vopts` is `verify`'s NORMALIZED options object, never the caller's -- the parameter is
594
- // named for that so a future caller cannot hand it the live one without noticing. Everything
595
- // it reads was captured synchronously at the entry.
596
381
  function _applyCallerRoots(res, supplied, vopts, onlyPaths) {
597
382
  if (!Array.isArray(supplied) || !supplied.length) {
598
383
  throw _err("webauthn/bad-input", "opts.rootCertificates must be a non-empty array of root certificates (DER, PEM, or parsed)");
599
384
  }
600
- // DER, PEM or an already-parsed certificate, decoded HERE -- the anchor comparison
601
- // downstream reads a certificate's subject and key, so a raw buffer reaching it
602
- // would fault on a field it does not have rather than naming the caller's mistake.
603
- // The same three forms opts.safetyNetRoots takes, since it is the same question.
604
385
  var roots = supplied.map(function (root, i) {
605
386
  var label = "opts.rootCertificates[" + i + "]";
606
- // These BECOME trust anchors, which is the sharpest form of the rule: the anchor's key is what
607
- // the whole attestation chain is judged against, so a caller-assembled certificate carrying a
608
- // real root's name beside a substituted key would anchor an attacker's chain. Re-derived from
609
- // the bytes its parser read, exactly as at every other certificate door.
610
387
  return guard.parsed.acceptDerived(root, "certificate", function (bytes) {
611
388
  try {
612
389
  return x509.parse(_isBufferSource(bytes) ? guard.bytes.source(bytes, WebauthnError, "webauthn/bad-input", label) : bytes);
613
390
  } catch (e) { throw _err("webauthn/bad-input", label + " is not a decodable certificate", e); }
614
391
  }, _err, "webauthn/bad-input", label);
615
392
  });
616
- // Same rule the metadata route applies: a compound element carrying no
617
- // certificates makes no claim there is anything to anchor, so it is not a reason to
618
- // refuse the statement -- but `anchoredTo` alone would then say "anchored" over a
619
- // statement it did not wholly cover. The coverage is reported instead.
620
- // `onlyPaths`, when given, is the set of paths the CATALOGUE did not cover -- the
621
- // fallback is for exactly those. Applying it to the whole statement instead would
622
- // re-judge an element the catalogue already governed against roots that have nothing
623
- // to do with it: a compound holding one listed element (chaining to the root its own
624
- // metadata entry registers) beside one unlisted element (chaining to a caller pin) would
625
- // be refused, because the listed element does not chain to the pin. That is the
626
- // documented "MDS-listed authenticators AND Apple" configuration, and each element is
627
- // still anchored to something the caller trusts -- just not all to the same thing.
628
393
  var paths = onlyPaths || ((res.fmt === "compound" && Array.isArray(res.compound))
629
394
  ? res.compound.filter(function (el) { return el.trustPath && el.trustPath.length; })
630
395
  .map(function (el) { return { tp: el.trustPath, at: el.chainValidatedAt }; })
631
396
  : (res.trustPath && res.trustPath.length ? [{ tp: res.trustPath, at: res.chainValidatedAt }] : []));
632
- // Coverage is ACCUMULATED across routes, not overwritten by whichever ran last. When
633
- // `onlyPaths` is given the metadata route already anchored the rest and recorded that here,
634
- // so reporting the fallback's own count would say 1 of 2 for a statement whose two elements
635
- // were both anchored -- one by the catalogue, one by the pin. A caller enforcing
636
- // `anchored === total` would then reject the mixed configuration this fallback exists to
637
- // support, and the verdict would understate the trust evaluation it actually performed.
638
397
  if (!onlyPaths) {
639
398
  res.anchoredElements = { total: (res.fmt === "compound" && Array.isArray(res.compound)) ? res.compound.length : 1,
640
399
  anchored: paths.length };
641
400
  }
642
- // `none` and a self-attestation carry no certificates, so there is nothing to
643
- // anchor -- and a caller who asked for anchoring is told it could not be applied
644
- // rather than handed a pass that looks like it was. Same rule the metadata route
645
- // states, for the same reason.
646
401
  if (!paths.length) {
647
402
  throw _err("webauthn/anchor-not-applicable",
648
403
  "opts.rootCertificates was supplied, but this attestation carries no trust path to anchor (format '" + res.fmt + "')");
649
404
  }
650
405
  var at = vopts.time !== undefined ? vopts.time : new Date();
651
- // EVERY path, not merely one: a compound's elements are independent claims, and
652
- // accepting the whole because one element anchored would let an unanchored element
653
- // ride along on its neighbor's trust.
654
406
  return paths.reduce(function (p, info) {
655
407
  return p.then(function () {
656
408
  var pathAt = vopts.time !== undefined ? vopts.time : (info.at || at);
@@ -663,13 +415,6 @@ function _applyCallerRoots(res, supplied, vopts, onlyPaths) {
663
415
  });
664
416
  }
665
417
 
666
- // android-safetynet anchors through a route of its own. The verifier REQUIRES
667
- // `opts.safetyNetRoots` (8.5) and refuses the statement unless the x5c chain validates to
668
- // one of them, so by the time a verdict is composed that element's path HAS been anchored --
669
- // without either `metadata` or `rootCertificates` being involved. Counting only those two
670
- // routes reports `anchoredTo: null` over it, whose documented meaning is that nobody checked
671
- // the path, so a caller enforcing `anchoredTo !== null` would refuse an attestation this
672
- // library did in fact anchor. Every route that contributed is named.
673
418
  function _safetyNetAnchored(res) {
674
419
  if (res.fmt === "android-safetynet") return 1;
675
420
  if (res.fmt === "compound" && Array.isArray(res.compound)) {
@@ -678,46 +423,22 @@ function _safetyNetAnchored(res) {
678
423
  return 0;
679
424
  }
680
425
 
681
- // `base` is whichever of the two caller-selected routes ran, or null when neither did.
682
- // Routes are named in a stable order so a caller may compare the string.
683
426
  function _anchoredRoutes(res, base) {
684
427
  var routes = base ? base.split("+") : [];
685
428
  if (_safetyNetAnchored(res) && routes.indexOf("safetyNetRoots") < 0) routes.push("safetyNetRoots");
686
429
  return routes.length ? routes.join("+") : null;
687
430
  }
688
431
 
689
- // ---- ceremony binding (WebAuthn sec. 7.1 / 7.2) ------------------------------
690
- //
691
- // The attestation and assertion procedures verify a SIGNATURE. What makes a
692
- // response acceptable is a separate set of checks the relying party owns, because
693
- // only it knows what it asked for: the challenge it issued, the origin the browser
694
- // reported, the RP ID it operates under, and the user-presence / user-verification
695
- // policy it requires. A verifier that answers only the first question, under a name
696
- // that sounds like the second, is the shape of every phishing-resistant login that
697
- // turns out not to be -- an attestation naming ANOTHER RP, with user presence
698
- // clear, is a perfectly sound statement about a credential the caller must not
699
- // accept.
700
- //
701
- // So the checks this layer CAN make are offered here, opt-in, and every verdict
702
- // reports which of them actually ran. The challenge and the origin stay with the
703
- // caller: they live in clientDataJSON, which the caller already holds and compares
704
- // against state only it has.
705
432
  function _assertBool(v, name) {
706
433
  if (typeof v !== "boolean") throw _err("webauthn/bad-input", "opts." + name + " must be a boolean");
707
434
  }
708
435
 
709
- // Runs the bindings the caller asked for and reports EACH one's status: `true` it
710
- // was checked and holds, `false` it was not asked for. A caller reading the verdict
711
- // can therefore tell a check that passed from one that never ran -- the distinction
712
- // the single `verified` boolean could not express.
713
436
  function _applyBindings(authData, coseKey, opts) {
714
437
  var checked = { rpId: false, userPresence: false, userVerification: false, algorithm: false };
715
438
  if (opts.expectedRpId !== undefined) {
716
439
  if (typeof opts.expectedRpId !== "string" || !opts.expectedRpId.length) {
717
440
  throw _err("webauthn/bad-input", "opts.expectedRpId must be a non-empty RP ID string");
718
441
  }
719
- // Compared in constant time and by full value, like every other identity
720
- // comparison in this toolkit -- a prefix must not read as a match.
721
442
  if (!guard.crypto.constantTimeEqual(_sha("sha256", Buffer.from(opts.expectedRpId, "utf8")),
722
443
  Buffer.from(authData.rpIdHash))) {
723
444
  throw _err("webauthn/rp-id-mismatch",
@@ -744,13 +465,6 @@ function _applyBindings(authData, coseKey, opts) {
744
465
  }
745
466
  checked.userVerification = opts.requireUserVerification;
746
467
  }
747
- // COSE alg -65535 is RSASSA-PKCS1-v1_5 with SHA-1, and a credential key may name
748
- // it -- which would make SHA-1 the signature algorithm of every login that
749
- // credential ever performs. Refused unless the caller lists it, so supporting a
750
- // legacy authenticator is a decision someone wrote down rather than the default.
751
- // (This is the CREDENTIAL key's algorithm. A legacy TPM attestation STATEMENT
752
- // signed with RS1 is a different field and is still verified, since refusing it
753
- // would reject the authenticator's own evidence about itself.)
754
468
  var alg65535Allowed = Array.isArray(opts.allowedAlgorithms) && opts.allowedAlgorithms.indexOf(-65535) !== -1;
755
469
  if (coseKey && coseKey.alg === -65535 && !alg65535Allowed) {
756
470
  throw _err("webauthn/algorithm-not-allowed",
@@ -758,14 +472,8 @@ function _applyBindings(authData, coseKey, opts) {
758
472
  "signature made by this credential would use SHA-1, so it is refused unless opts.allowedAlgorithms " +
759
473
  "names -65535 explicitly");
760
474
  }
761
- // The rest of algorithm policy is the RP's, declared to the browser as
762
- // pubKeyCredParams, and nothing in the response proves what was offered -- so it
763
- // is checked only when the caller states the list.
764
475
  if (opts.allowedAlgorithms !== undefined) {
765
476
  if (!Array.isArray(opts.allowedAlgorithms) || !opts.allowedAlgorithms.length ||
766
- // isSafeInteger, not isInteger: a value above 2^53 is not held exactly as a
767
- // Number, so one written that way is not the identifier the caller meant --
768
- // and no COSE algorithm identifier lives out there anyway.
769
477
  !opts.allowedAlgorithms.every(function (a) { return typeof a === "number" && Number.isSafeInteger(a); })) {
770
478
  throw _err("webauthn/bad-input", "opts.allowedAlgorithms must be a non-empty array of COSE algorithm integers");
771
479
  }
@@ -779,10 +487,6 @@ function _applyBindings(authData, coseKey, opts) {
779
487
  return checked;
780
488
  }
781
489
 
782
- // One bound for every attestation certificate chain, wherever it arrives from -- an attStmt x5c
783
- // array or a JWS x5c header. Capping the bytes of a single entry does not bound the COUNT, and the
784
- // cost of an entry is a DER parse plus, downstream, a signature check or a path validation. Kept in
785
- // one place so a new chain-bearing format cannot reintroduce the unbounded fanout.
786
490
  function _requireX5cCount(n) {
787
491
  if (n > constants.LIMITS.WEBAUTHN_X5C_MAX_CERTS) {
788
492
  throw _err("webauthn/bad-att-stmt", "an attestation certificate chain carries " + n + " certificates, above the " + constants.LIMITS.WEBAUTHN_X5C_MAX_CERTS + " this toolkit will parse");
@@ -798,10 +502,6 @@ function _readX5c(attStmt) {
798
502
  try { return x509.parse(der); } catch (e) { throw _err("webauthn/bad-att-stmt", "an x5c certificate is not a well-formed X.509 certificate", e); }
799
503
  });
800
504
  }
801
- // The WebAuthn attestation-certificate profile checks (WebAuthn 8.2.1 packed / 8.3.1 TPM
802
- // AIK / the id-fido-gen-ce-aaguid extension) live in validator-attcert, composed here. The
803
- // extension-accessor object hands the validator this format's fail-closed extension
804
- // decoders so it stays decoupled from the webauthn error namespace.
805
505
  var _exts = { find: _findExt, decode: _decodeExt };
806
506
  function _requireV3(cert) { validator.attcert.requireV3(cert, WebauthnError, "webauthn/bad-att-cert"); }
807
507
  function _checkPackedCert(cert) { validator.attcert.packedCert(cert, _exts, WebauthnError, "webauthn/bad-att-cert"); }
@@ -809,7 +509,6 @@ function _checkAikCert(cert) { validator.attcert.aikCert(cert, _exts, WebauthnEr
809
509
  function _checkAaguidExt(cert, aaguid) { validator.attcert.aaguidExt(cert, aaguid, _exts, WebauthnError, "webauthn/bad-att-cert", "webauthn/aaguid-mismatch"); }
810
510
 
811
511
  var VERIFIERS = {
812
- // packed (WebAuthn 8.2): the x5c arm (Basic/AttCA) or self-attestation.
813
512
  packed: function (att, clientDataHash) {
814
513
  var isX5c = !!cbor.read.mapGet(att.attStmt, "x5c");
815
514
  _requireAttShape(att.attStmt, isX5c ? ["alg", "sig", "x5c"] : ["alg", "sig"], isX5c ? ["alg", "sig", "x5c"] : ["alg", "sig"]);
@@ -817,15 +516,13 @@ var VERIFIERS = {
817
516
  var message = Buffer.concat([att.authDataBytes, clientDataHash]);
818
517
  if (isX5c) {
819
518
  var chain = _readX5c(att.attStmt), leaf = chain[0];
820
- _checkPackedCert(leaf); // 8.2.1: v3, non-CA, OU=Authenticator Attestation
519
+ _checkPackedCert(leaf);
821
520
  return _verifySig(alg, sig, leaf.subjectPublicKeyInfo.bytes, message, _err).then(function (ok) {
822
521
  if (!ok) throw _err("webauthn/verify-failed", "the packed attestation signature does not verify under the x5c leaf key");
823
522
  _checkAaguidExt(leaf, att.authData.aaguid);
824
523
  return _result("packed", "Basic", chain, att);
825
524
  });
826
525
  }
827
- // Self-attestation: the statement alg MUST equal the credential key's own alg
828
- // (WebAuthn 8.2), then sig verifies under the credential key itself.
829
526
  if (alg !== att.authData.credentialPublicKey.alg) throw _err("webauthn/bad-att-stmt", "the packed self-attestation alg does not match the credential public key algorithm (WebAuthn 8.2)");
830
527
  var spki = _coseKeyToSpki(att.authData.credentialPublicKey);
831
528
  return _verifySig(alg, sig, spki, message, _err).then(function (ok) {
@@ -834,19 +531,13 @@ var VERIFIERS = {
834
531
  });
835
532
  },
836
533
 
837
- // fido-u2f (WebAuthn 8.6): reconstruct the U2F verificationData and verify with
838
- // the single x5c cert. The credential key MUST be EC2/P-256.
839
534
  "fido-u2f": function (att, clientDataHash) {
840
535
  _requireAttShape(att.attStmt, ["sig", "x5c"], ["sig", "x5c"]);
841
536
  var chain = _readX5c(att.attStmt);
842
537
  if (chain.length !== 1) throw _err("webauthn/bad-att-stmt", "fido-u2f x5c MUST contain exactly one certificate (WebAuthn 8.6)");
843
- // WebAuthn 8.6 does not require a version-3 certificate for fido-u2f (unlike the
844
- // packed 8.2.1 / tpm 8.3.1 leaves), so a legacy v1 U2F attestation cert is valid.
845
538
  var leaf = chain[0];
846
539
  var sig = _sigOf(att.attStmt);
847
540
  var key = att.authData.credentialPublicKey;
848
- // WebAuthn 8.6: the fido-u2f credential public key MUST be alg -7 (ES256) on EC2 P-256 --
849
- // the newer ESP256 (-9) id, though the same curve, is not a valid fido-u2f credential.
850
541
  if (key.alg !== -7 || key.kty !== 2 || key.crv !== 1 || !key.x || !key.y || key.x.length !== 32 || key.y.length !== 32) {
851
542
  throw _err("webauthn/bad-att-stmt", "fido-u2f requires an ES256 (-7) EC2 P-256 credential public key (WebAuthn 8.6)");
852
543
  }
@@ -858,10 +549,8 @@ var VERIFIERS = {
858
549
  });
859
550
  },
860
551
 
861
- // apple (WebAuthn 8.8): the binding is the SHA-256 nonce over authData ||
862
- // clientDataHash embedded in the leaf cert; there is no signature field.
863
552
  apple: function (att, clientDataHash) {
864
- _requireAttShape(att.attStmt, ["alg", "x5c"], ["x5c"]); // alg optional, ignored
553
+ _requireAttShape(att.attStmt, ["alg", "x5c"], ["x5c"]);
865
554
  var chain = _readX5c(att.attStmt), leaf = chain[0];
866
555
  _requireV3(leaf);
867
556
  var nonce = _sha("sha256", Buffer.concat([att.authDataBytes, clientDataHash]));
@@ -869,12 +558,10 @@ var VERIFIERS = {
869
558
  if (!ext) throw _err("webauthn/bad-att-cert", "the apple attestation certificate is missing the anonymous-attestation extension (WebAuthn 8.8)");
870
559
  var embedded = _appleNonce(ext.value);
871
560
  if (!embedded.equals(nonce)) throw _err("webauthn/verify-failed", "the apple attestation nonce does not equal SHA-256(authData || clientDataHash)");
872
- _certPubKeyEqualsCose(leaf, att.authData.credentialPublicKey, _err); // 8.8 item 30
561
+ _certPubKeyEqualsCose(leaf, att.authData.credentialPublicKey, _err);
873
562
  return Promise.resolve(_result("apple", "AnonCA", chain, att));
874
563
  },
875
564
 
876
- // android-key (WebAuthn 8.4): verify sig with the leaf key, bind the leaf key to
877
- // the credential key, and enforce the four KeyDescription checks (8.4.1).
878
565
  "android-key": function (att, clientDataHash) {
879
566
  _requireAttShape(att.attStmt, ["alg", "sig", "x5c"], ["alg", "sig", "x5c"]);
880
567
  var chain = _readX5c(att.attStmt), leaf = chain[0];
@@ -884,16 +571,12 @@ var VERIFIERS = {
884
571
  return _verifySig(alg, sig, leaf.subjectPublicKeyInfo.bytes, message, _err).then(function (ok) {
885
572
  if (!ok) throw _err("webauthn/verify-failed", "the android-key attestation signature does not verify under the x5c leaf key");
886
573
  _certPubKeyEqualsCose(leaf, att.authData.credentialPublicKey, _err);
887
- _checkAndroidKeyDescription(leaf, clientDataHash); // 8.4.1
574
+ _checkAndroidKeyDescription(leaf, clientDataHash);
888
575
  return _result("android-key", "Basic", chain, att);
889
576
  });
890
577
  },
891
578
 
892
- // tpm (WebAuthn 8.3): decode certInfo/pubArea, enforce magic/type/extraData/Name,
893
- // bind pubArea to the credential key, and verify sig over certInfo with the AIK.
894
579
  tpm: function (att, clientDataHash, opts) {
895
- // Config-time: a malformed or mistyped tpmPolicy is a caller error, caught before any parsing so
896
- // a typo cannot silently disable the check the caller believes they enabled.
897
580
  var tpmPolicy = validator.tpm.normalizeObjectAttributePolicy((opts || {}).tpmPolicy, WebauthnError, "webauthn/bad-input");
898
581
  _requireAttShape(att.attStmt, ["ver", "alg", "sig", "certInfo", "pubArea", "x5c"], ["ver", "alg", "sig", "certInfo", "pubArea", "x5c"]);
899
582
  var verN = cbor.read.mapGet(att.attStmt, "ver");
@@ -904,38 +587,22 @@ var VERIFIERS = {
904
587
  var chain = _readX5c(att.attStmt), aik = chain[0];
905
588
 
906
589
  var pub = _parseTpmPubArea(pubAreaBytes);
907
- _tpmPubKeyEqualsCose(pub, att.authData.credentialPublicKey); // 8.3 item 22
590
+ _tpmPubKeyEqualsCose(pub, att.authData.credentialPublicKey);
908
591
 
909
- // validator-tpm decodes certInfo AND validates magic == TPM_GENERATED_VALUE / type ==
910
- // TPM_ST_ATTEST_CERTIFY, so the returned structure is already self-consistent.
911
592
  var certInfo = _parseTpmCertInfo(certInfoBytes);
912
593
 
913
- // extraData == hash_alg(authData || clientDataHash) (bare digest, no method id).
914
594
  var attToBeSigned = Buffer.concat([att.authDataBytes, clientDataHash]);
915
595
  if (!certInfo.extraData.equals(_sha(_coseAlgHash(alg, _err), attToBeSigned))) throw _err("webauthn/verify-failed", "certInfo extraData does not equal the hash of authData || clientDataHash");
916
596
 
917
- // attested.name == nameAlg || H_nameAlg(pubArea).
918
597
  var nameHash = validator.tpm.TPM_ALG_HASH[pub.nameAlg];
919
598
  if (!nameHash) throw _err("webauthn/bad-tpm", "unsupported TPM nameAlg 0x" + pub.nameAlg.toString(16));
920
599
  var computedName = Buffer.concat([pub.nameAlgBytes, _sha(nameHash, pubAreaBytes)]);
921
600
  if (!certInfo.attestedName.equals(computedName)) throw _err("webauthn/verify-failed", "certInfo attested Name does not match the pubArea TPM Name");
922
601
 
923
- // WebAuthn 8.3 verification step: "Verify the sig is a valid signature over
924
- // certInfo ... with the algorithm specified in alg" -- sig is verified DIRECTLY
925
- // with alg, not parsed as a TPMT_SIGNATURE and unwrapped. The attStmt-syntax
926
- // "in the form of a TPMT_SIGNATURE" is a description of the byte string; real
927
- // authenticators put the raw signature here (the interop KAT's tpm sig is a bare
928
- // 256-byte RSASSA signature that verifies as-is), and the reference verifier
929
- // (py_webauthn) verifies it directly with alg the same way.
930
602
  return _verifySig(alg, sig, aik.subjectPublicKeyInfo.bytes, certInfoBytes, _err).then(function (ok) {
931
603
  if (!ok) throw _err("webauthn/verify-failed", "the tpm attestation signature does not verify over certInfo under the AIK");
932
- _checkAikCert(aik); // 8.3.1
933
- _checkAaguidExt(aik, att.authData.aaguid); // 8.3.1: aaguid ext, if present, MUST match
934
- // The TPMT_PUBLIC object attributes and authPolicy are properties of the credential key that
935
- // sec. 8.3 does not constrain -- it bounds only pubArea's `parameters` and `unique`. They are
936
- // surfaced for relying-party policy, and gated only when the caller supplies opts.tpmPolicy.
937
- // Applied AFTER the signature and Name checks, so the bytes being judged are ones the AIK
938
- // signature already covers rather than attacker-chosen input.
604
+ _checkAikCert(aik);
605
+ _checkAaguidExt(aik, att.authData.aaguid);
939
606
  validator.tpm.assertObjectAttributePolicy(pub, tpmPolicy, WebauthnError, "webauthn/tpm-policy", "webauthn/bad-tpm");
940
607
  var tpmRes = _result("tpm", "AttCA", chain, att);
941
608
  tpmRes.tpm = { objectAttributes: pub.objectAttributes, attributes: pub.attributes, authPolicy: pub.authPolicy };
@@ -943,18 +610,8 @@ var VERIFIERS = {
943
610
  });
944
611
  },
945
612
 
946
- // android-safetynet (WebAuthn 8.5): the attStmt carries a SafetyNet JWS ("response") whose payload
947
- // binds a nonce to this registration and whose x5c header chains to a Google root.
948
- //
949
- // OFF BY DEFAULT, and anchored only by the caller. Google retired the SafetyNet Attestation API, so
950
- // nothing mints these any more -- the surviving use is a relying party re-checking attestations it
951
- // stored years ago. Enabling a format whose producer is gone, against a root this library chose,
952
- // would widen what every caller trusts for no live benefit; so the caller opts in AND supplies the
953
- // root. With the opt off the verdict is byte-identical to the one this format had before the arm
954
- // existed. There is no bundled root and no trust-on-first-use.
955
613
  "android-safetynet": function (att, clientDataHash, opts) {
956
614
  opts = opts || {};
957
- // The type is settled at the entry point, so what remains here is the opt-in itself.
958
615
  if (opts.verifySafetyNetJws !== true) {
959
616
  throw _err("webauthn/unsupported-format", "attestation statement format 'android-safetynet' is not supported");
960
617
  }
@@ -962,19 +619,13 @@ var VERIFIERS = {
962
619
  if (!Array.isArray(roots) || roots.length === 0) {
963
620
  throw _err("webauthn/safetynet-no-root", "verifying an android-safetynet attestation requires opts.safetyNetRoots -- the Google root(s) to anchor the x5c chain to; this library bundles none (WebAuthn 8.5)");
964
621
  }
965
- // The guard rejects through a (code, message) FACTORY. Handing it the error CLASS makes the
966
- // reject path raise "class constructor cannot be invoked without new" -- a raw, untyped throw
967
- // escaping a public verb, on the branch a valid-input test never takes.
968
622
  if (opts.time !== undefined) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
969
623
 
970
- // 8.5 attStmt syntax: safetynetStmtFormat = { ver: text, response: bytes }. `ver` is READ but
971
- // never gated on -- 8.5 states it is reserved for future use.
972
624
  _requireAttShape(att.attStmt, ["ver", "response"], ["ver", "response"]);
973
625
  _attRead(att.attStmt, "ver", cbor.read.textString, "a text string");
974
626
  var responseBytes = _attRead(att.attStmt, "response", cbor.read.byteString, "a byte string");
975
627
  if (!responseBytes.length) throw _err("webauthn/bad-att-stmt", "the android-safetynet response is empty");
976
628
 
977
- // RFC 7515 sec. 3.1 Compact Serialization: exactly three base64url segments.
978
629
  var segs = guard.text.decode(responseBytes, constants.LIMITS.SAFETYNET_JWS_MAX_BYTES, WebauthnError, "webauthn/bad-att-stmt", "the android-safetynet response").split(".");
979
630
  if (segs.length !== 3) throw _err("webauthn/bad-att-stmt", "the android-safetynet response is not a three-part JWS compact serialization (RFC 7515 sec. 3.1)");
980
631
  var header, payload, sigBytes;
@@ -983,53 +634,34 @@ var VERIFIERS = {
983
634
  payload = jose.parseJson(jose.base64url.decode(segs[1]));
984
635
  sigBytes = Buffer.from(jose.base64url.decode(segs[2]));
985
636
  } catch (e) { throw _err("webauthn/bad-att-stmt", "the android-safetynet response is not a decodable JWS", e); }
986
- // A JWS segment must decode to a JSON OBJECT. `null`, a number, a string and an array are all
987
- // valid JSON, so the parse succeeds and every later field read would be a raw TypeError escaping
988
- // this module's typed contract -- the caller's error handling would never see a webauthn/* code.
989
637
  if (!_isPlainObject(header) || !_isPlainObject(payload)) {
990
638
  throw _err("webauthn/bad-att-stmt", "the android-safetynet JWS header and payload must each be a JSON object");
991
639
  }
992
640
 
993
- // Pin the algorithm rather than reading it from the token: an attacker-chosen alg is the JWS
994
- // algorithm-confusion class, and SafetyNet only ever signed RS256.
995
641
  if (header.alg !== "RS256") throw _err("webauthn/unsupported-algorithm", "the android-safetynet JWS alg must be RS256, got " + JSON.stringify(header.alg));
996
642
  if (!Array.isArray(header.x5c) || header.x5c.length === 0) {
997
643
  throw _err("webauthn/bad-att-stmt", "the android-safetynet JWS header carries no x5c certificate chain (RFC 7515 sec. 4.1.6)");
998
644
  }
999
645
  _requireX5cCount(header.x5c.length);
1000
- // x5c entries are STANDARD base64 (RFC 7515 sec. 4.1.6), not base64url like the segments.
1001
646
  var chain = header.x5c.map(function (entry, i) {
1002
647
  if (typeof entry !== "string") throw _err("webauthn/bad-att-stmt", "the android-safetynet x5c entry " + i + " is not a string");
1003
648
  var der;
1004
- try { der = guard.encoding.base64(entry, constants.LIMITS.SAFETYNET_CERT_MAX_BYTES, WebauthnError, "webauthn/bad-att-stmt", "an android-safetynet x5c entry"); }
649
+ try { der = guard.encoding.base64(entry, constants.LIMITS.SAFETYNET_CERT_MAX_BYTES, _err, "webauthn/bad-att-stmt", "an android-safetynet x5c entry"); }
1005
650
  catch (e) { throw _err("webauthn/bad-att-stmt", "the android-safetynet x5c entry " + i + " is not canonical base64", e); }
1006
651
  try { return x509.parse(der); }
1007
652
  catch (e) { throw _err("webauthn/bad-att-cert", "the android-safetynet x5c entry " + i + " is not a decodable certificate", e); }
1008
653
  });
1009
654
  var leaf = chain[0];
1010
655
 
1011
- // 8.5 bullet 3: nonce == STANDARD Base64 of SHA-256(authenticatorData || clientDataHash). Note
1012
- // standard base64 (+/=), NOT base64url -- and the digest is over the raw concatenation.
1013
656
  var wantNonce = _sha("sha256", Buffer.concat([att.authDataBytes, clientDataHash])).toString("base64");
1014
657
  if (typeof payload.nonce !== "string" || !guard.crypto.constantTimeEqual(Buffer.from(payload.nonce, "utf8"), Buffer.from(wantNonce, "utf8"))) {
1015
658
  throw _err("webauthn/safetynet-nonce-mismatch", "the android-safetynet nonce does not bind this authenticatorData and clientDataHash (WebAuthn 8.5)");
1016
659
  }
1017
660
 
1018
- // 8.5 bullet 4 (via the SafetyNet documentation): the response must come from the SafetyNet
1019
- // service, which is established by the leaf being issued to attest.android.com AND the chain
1020
- // validating to a Google root. The hostname alone proves nothing until the chain is anchored.
1021
661
  if (!_safetyNetHostnameOk(leaf)) {
1022
662
  throw _err("webauthn/safetynet-bad-hostname", "the android-safetynet x5c leaf is not issued to attest.android.com (WebAuthn 8.5)");
1023
663
  }
1024
664
 
1025
- // The device-integrity signals are NOT part of the 8.5 verification procedure -- its five bullets
1026
- // never mention them -- so gating the attestation verdict on them would invent a requirement the
1027
- // specification does not state. They are relying-party policy, so they are surfaced on the result
1028
- // for a caller to act on, and enforced here only when the caller explicitly asks. A caller that
1029
- // asks and finds them missing or false gets a refusal, never a silent pass.
1030
- // The instant the service chain is judged at, resolved below once the signature has authenticated
1031
- // the timestamp it comes from, and carried out on the result so a later check of the same path
1032
- // uses the same one rather than resetting to the current clock.
1033
665
  var chainAt;
1034
666
  var signals = {
1035
667
  ctsProfileMatch: payload.ctsProfileMatch, basicIntegrity: payload.basicIntegrity,
@@ -1043,100 +675,53 @@ var VERIFIERS = {
1043
675
  return _verifySig(-257, sigBytes, leaf.subjectPublicKeyInfo.bytes,
1044
676
  Buffer.from(segs[0] + "." + segs[1], "ascii"), _err).then(function (ok) {
1045
677
  if (!ok) throw _err("webauthn/verify-failed", "the android-safetynet JWS signature does not verify under the x5c leaf key");
1046
- // WHEN to judge the chain. These attestations are historical by construction -- the service is
1047
- // retired -- so a leaf that was valid when the response was signed is routinely expired now,
1048
- // and judging it against the current clock would refuse every genuine stored registration.
1049
- // The response carries its own signing time, and by this point that value is covered by the
1050
- // signature just verified under the leaf, so it is authenticated rather than caller-asserted.
1051
- // Precedence: an explicit opts.time (the caller knows when the registration happened) beats
1052
- // the signed timestamp, which beats now (a response that carries no usable timestamp).
1053
678
  chainAt = opts.time !== undefined ? opts.time
1054
679
  : (typeof payload.timestampMs === "number" && isFinite(payload.timestampMs) && payload.timestampMs > 0
1055
680
  ? new Date(payload.timestampMs) : undefined);
1056
681
  return _safetyNetChainTrusted(chain, roots, chainAt);
1057
682
  }).then(function () {
1058
- // 8.5 bullet 5: attestation type Basic, trust path x5c.
1059
683
  var res = _result("android-safetynet", "Basic", chain, att);
1060
684
  res.safetyNet = signals;
1061
- // The instant this chain was judged at, surfaced so any later check of the SAME path uses the
1062
- // same one. A stored response's service chain has usually expired by now, and re-validating
1063
- // it against the current clock would refuse a registration that was valid when it was made.
1064
685
  if (chainAt !== undefined) res.chainValidatedAt = chainAt;
1065
686
  return res;
1066
687
  });
1067
688
  },
1068
689
 
1069
- // compound (WebAuthn 8.9): the attStmt is an ARRAY of nested attestation statements, each
1070
- // verified over the SAME authenticatorData and clientDataHash as the outer object. sec. 8.9
1071
- // leaves the acceptance threshold to relying-party policy ("if validation fails for one or more
1072
- // subStmt, decide the appropriate result based on RP policy"); this toolkit's policy is
1073
- // fail-closed -- EVERY element must verify, because accepting a compound whose strong element
1074
- // failed and whose `none` element passed would let a wrapper launder a failed attestation.
1075
690
  compound: function (att, clientDataHash, opts) {
1076
691
  var kids = att.attStmt.children || [];
1077
- // sec. 8.9 syntax `2*`: at least two nested statements, or it is not a compound.
1078
692
  if (kids.length < 2) throw _err("webauthn/bad-att-stmt", "a compound attestation statement must carry at least two nested statements (WebAuthn 8.9)");
1079
- // Not a spec rule: a resource bound this toolkit chooses. The CBOR caps bound the PARSE; they
1080
- // do not bound the crypto, and each element costs a signature verify plus, for some formats, a
1081
- // certificate-chain path validation.
1082
693
  if (kids.length > constants.LIMITS.WEBAUTHN_COMPOUND_MAX_STATEMENTS) {
1083
694
  throw _err("webauthn/bad-att-stmt", "a compound attestation statement carries " + kids.length + " nested statements, above the " + constants.LIMITS.WEBAUTHN_COMPOUND_MAX_STATEMENTS + " this toolkit will verify");
1084
695
  }
1085
696
  var elements = kids.map(function (el, i) {
1086
- // sec. 8.9: nonCompoundAttStmt = { $$attStmtType } -- each element is exactly { fmt, attStmt }.
1087
697
  if (!el || el.majorType !== 5) throw _err("webauthn/bad-att-stmt", "compound element " + i + " must be a CBOR map { fmt, attStmt } (WebAuthn 8.9)");
1088
698
  var fN = cbor.read.mapGet(el, "fmt"), sN = cbor.read.mapGet(el, "attStmt");
1089
699
  if (el.children.length !== 2 || !fN || !sN) throw _err("webauthn/bad-att-stmt", "compound element " + i + " must be exactly { fmt, attStmt } (WebAuthn 8.9)");
1090
700
  if (fN.majorType !== 3) throw _err("webauthn/bad-att-stmt", "compound element " + i + " 'fmt' must be a text string");
1091
701
  var f = cbor.read.textString(fN);
1092
- // sec. 8.9 spells the element type `.ne "compound"`: nesting is forbidden by the syntax
1093
- // itself, which is what fixes the evaluation depth at one. No depth parameter is needed --
1094
- // and adding one would imply a nesting this format does not have.
1095
702
  if (f === "compound") throw _err("webauthn/bad-att-stmt", "a compound attestation statement must not nest another compound (WebAuthn 8.9)");
1096
- // sec. 8.1: identifiers match case-sensitively, which the registry lookup already is.
1097
703
  var v = VERIFIERS[f];
1098
704
  if (!v) throw _err("webauthn/unsupported-format", "compound element " + i + " uses unsupported attestation statement format '" + f + "'");
1099
705
  var wantMajor = ATT_STMT_MAJOR[f] === undefined ? 5 : ATT_STMT_MAJOR[f];
1100
706
  if (sN.majorType !== wantMajor) throw _err("webauthn/bad-att-stmt", "compound element " + i + " 'attStmt' has the wrong CBOR shape for format '" + f + "'");
1101
- // Each element verifies against the OUTER authenticatorData: sec. 8.9 passes the same
1102
- // verification-procedure inputs down, so an element cannot bind a different credential.
1103
707
  return { fmt: f, index: i, att: { fmt: f, attStmt: sN, authData: att.authData, authDataBytes: att.authDataBytes } };
1104
708
  });
1105
- // Sequential, not Promise.all: a compound may hold many elements, and each can cost a
1106
- // signature verify plus a full path validation. Fanning them out concurrently would turn one
1107
- // attestation into a burst of crypto work.
1108
709
  var out = [];
1109
710
  return elements.reduce(function (p, e) {
1110
711
  return p.then(function () {
1111
- // The verifier is CALLED inside the promise chain, not evaluated as an argument to
1112
- // Promise.resolve: most arms do their structural checks synchronously, so an argument-
1113
- // position call would let those throws escape the handler below and reach the caller
1114
- // bare -- the same failure reported with the element's context or without it, depending
1115
- // only on whether it happened before or after the first await.
1116
712
  return Promise.resolve().then(function () { return VERIFIERS[e.fmt](e.att, clientDataHash, opts); })
1117
713
  .then(function (r) { out.push(r); }, function (err) {
1118
714
  throw _err("webauthn/compound-element-failed", "compound element " + e.index + " (format '" + e.fmt + "') did not verify", err);
1119
715
  });
1120
716
  });
1121
717
  }, Promise.resolve()).then(function () {
1122
- // sec. 8.9 lists the supported attestation type as "Any" and authorizes returning
1123
- // "implementation-specific values representing any combination of outputs". A distinct type
1124
- // rather than a merge: collapsing to the strongest element would let a wrapper upgrade a
1125
- // caller's attestationType check, and collapsing to the weakest would spuriously fail one.
1126
- // The trust path is empty because two elements yield two independent chains and there is no
1127
- // single ordered path -- each element's own path is on its entry in `compound`.
1128
718
  var res = _result("compound", "Compound", [], att);
1129
719
  res.compound = out;
1130
720
  return res;
1131
721
  });
1132
722
  },
1133
723
 
1134
- // none (WebAuthn 8.7): the authenticator provides no attestation. attStmt MUST be
1135
- // an empty map; there is no statement to verify, so the result carries no trust
1136
- // path. The credential public key still binds via authenticatorData (AT flag).
1137
724
  none: function (att) {
1138
- // parseAttestationObject has already established attStmt is a CBOR map; the none format
1139
- // additionally requires it be EMPTY -- there is no statement to verify (WebAuthn 8.7).
1140
725
  if (att.attStmt.children.length !== 0) {
1141
726
  throw _err("webauthn/bad-att-stmt", "the none attestation statement MUST be an empty map (WebAuthn 8.7)");
1142
727
  }
@@ -1144,27 +729,14 @@ var VERIFIERS = {
1144
729
  },
1145
730
  };
1146
731
 
1147
- // A decoded JSON value that is safe to read named members off. JSON.parse yields null, numbers,
1148
- // strings and arrays too, and a member read on any of those would leave this module's typed error
1149
- // contract as a raw TypeError.
1150
732
  function _isPlainObject(v) { return !!v && typeof v === "object" && !Array.isArray(v); }
1151
733
 
1152
- // WebAuthn 8.5 (via the SafetyNet documentation): the JWS leaf is issued to attest.android.com.
1153
- // Checked on the SAN dNSName entries first -- the name a TLS-style certificate is actually issued
1154
- // to -- falling back to the commonName only when the certificate carries no SAN at all, the way a
1155
- // hostname match has been specified since RFC 6125. Compared case-insensitively (a DNS name is
1156
- // case-insensitive) and exactly: no wildcard, no suffix match, so attest.android.com.evil.test
1157
- // cannot pass.
1158
734
  function _safetyNetHostnameOk(leaf) {
1159
735
  var want = "attest.android.com";
1160
- // Decoded through the SAME shared pkix extension decoder every other format here uses, so the
1161
- // general-name parse cannot drift from the rest of the toolkit.
1162
736
  var san = _decodeExt(leaf, "subjectAltName");
1163
737
  var entries = san && Array.isArray(san.value) ? san.value : [];
1164
738
  var dns = entries.filter(function (gn) { return gn && gn.type === "dNSName" && typeof gn.value === "string"; });
1165
- // A SAN carrying dNSName entries is authoritative: the commonName is not consulted at all.
1166
739
  if (dns.length) return dns.some(function (gn) { return gn.value.toLowerCase() === want; });
1167
- // rdns is a sequence of RDNs, each a set of attribute/value pairs -- hence the nested walk.
1168
740
  return leaf.subject.rdns.some(function (rdn) {
1169
741
  return rdn.some(function (atv) {
1170
742
  return atv.name === "commonName" && typeof atv.value === "string" && atv.value.toLowerCase() === want;
@@ -1172,23 +744,9 @@ function _safetyNetHostnameOk(leaf) {
1172
744
  });
1173
745
  }
1174
746
 
1175
- // WebAuthn 8.5 (via the SafetyNet documentation): the x5c chain must validate to a Google root the
1176
- // CALLER supplied. Every anchor is tried because a caller may hold several Google roots across a
1177
- // rotation; the first that validates wins, and if none does the attestation is refused. The chain
1178
- // goes through the full path validator rather than a signature-only walk, so an expired, revoked-by-
1179
- // policy, or otherwise non-conforming intermediate cannot slip past on a signature alone.
1180
- // The caller's roots are the only thing this function owns: their SHAPE is its own config-time
1181
- // contract, so a root that is not a decodable certificate is named by its index here. Anchoring
1182
- // itself is the namespace's one walk (webauthn-mds), which every other chain in this module already
1183
- // reaches its anchors through -- including the anchor-stripping rule, which this had a slightly
1184
- // different and weaker copy of. Each root is resolved BEFORE the walk starts, so a malformed entry
1185
- // is a config fault whatever position it sits in, rather than one that only surfaces when the roots
1186
- // before it happen to fail.
1187
747
  function _safetyNetChainTrusted(chain, roots, time) {
1188
748
  var anchors;
1189
749
  try {
1190
- // The same door opts.rootCertificates goes through, for the same reason: these become trust
1191
- // anchors, and an anchor's key is what the chain is judged against.
1192
750
  anchors = roots.map(function (root, i) {
1193
751
  var label = "opts.safetyNetRoots[" + i + "]";
1194
752
  return guard.parsed.acceptDerived(root, "certificate", function (bytes) {
@@ -1202,60 +760,24 @@ function _safetyNetChainTrusted(chain, roots, time) {
1202
760
  "android-safetynet x5c certificate chain", "webauthn/safetynet-cert-untrusted");
1203
761
  }
1204
762
 
1205
- // `chain` is the x5c order (leaf-first); trustPath is surfaced in pki.path.validate
1206
- // order (anchor-adjacent first, target/leaf last) so the caller passes it straight
1207
- // to the path validator without re-ordering. The input array is not mutated.
1208
- // `attestationVerified`, not `verified`: this says the attestation STATEMENT is
1209
- // sound, which is not the same claim as "this registration is acceptable". The
1210
- // ceremony bindings the caller may also have asked for are reported separately, in
1211
- // `bindingChecked`, so a field that passed can be told from one that never ran.
1212
- // It also carries everything a relying party must STORE to finish registration and
1213
- // run a later login: the credentialId it will look the credential up by, the
1214
- // credential public key that will verify assertions, and the signCount that is the
1215
- // starting point for the sec. 7.2 step 21 counter rule. Leaving those out sent the
1216
- // caller back to parse the attestation object a second time for values this call
1217
- // had already decoded -- and a login cannot be verified without them.
1218
763
  function _result(fmt, attestationType, chain, att) {
1219
764
  return {
1220
- // `valid` / `attestationVerified` say the attestation format was validly processed, which the `none`
1221
- // format (WebAuthn 8.7, no attestation) satisfies too. The ASSURANCE level is `attestationType`
1222
- // (`None` for none): a relying party that requires cryptographic attestation gates on it, not on `valid`.
1223
765
  valid: true, attestationVerified: true, fmt: fmt, attestationType: attestationType,
1224
766
  trustPath: (chain || []).slice().reverse(),
1225
767
  aaguid: att.authData.aaguid,
1226
768
  credentialId: att.authData.credentialId,
1227
769
  credentialPublicKey: att.authData.credentialPublicKey,
1228
- // The same key in the form that SURVIVES STORAGE. The decoded object carries Buffers, so a JSON
1229
- // round trip through a datastore returns {"type":"Buffer","data":[...]} rather than what went
1230
- // in; the COSE bytes are what a credential row actually holds. Returning only the object left
1231
- // the caller re-parsing the attestation object to recover bytes this call had already isolated.
1232
770
  credentialPublicKeyBytes: att.authData.credentialPublicKeyBytes,
1233
771
  signCount: att.authData.signCount,
1234
772
  flags: att.authData.flags,
1235
- // The RP ID hash and the authenticator extension outputs, for the same reason the key bytes
1236
- // above are here: this call decoded them and the caller needs them. Extension outputs arrive
1237
- // at REGISTRATION and nowhere else -- credProtect (whether this credential is
1238
- // user-verification-required for the rest of its life), credProps.rk (whether it is
1239
- // discoverable), credBlob, minPinLength, hmac-secret/prf -- so a relying party that must
1240
- // persist them had to re-parse the attestation object to read values already in hand.
1241
- // `verifyAssertion` returns both; the registration verdict returning less made the two halves
1242
- // of one lifecycle disagree about what they hand back.
1243
773
  rpIdHash: att.authData.rpIdHash,
1244
774
  extensions: att.authData.extensions,
1245
775
  };
1246
776
  }
1247
777
 
1248
- // Decode the Apple extension AppleAnonymousAttestation ::= SEQUENCE { nonce [1]
1249
- // EXPLICIT OCTET STRING } (WebAuthn 8.8 item 29) and return the 32-byte nonce.
1250
778
  function _appleNonce(extValue) {
1251
779
  var seq;
1252
780
  try { seq = asn1.decode(extValue); } catch (e) { throw _err("webauthn/bad-att-cert", "the apple attestation extension is not decodable", e); }
1253
- // ARITY IS PART OF THE DECLARED SHAPE. Reading the first child and ignoring the rest
1254
- // accepts a certificate carrying a second, unchecked value beside the nonce -- and this
1255
- // extension exists precisely to carry the value the attestation binds to, so an
1256
- // ambiguous encoding of it is not a shape this verifier gets to pick a reading from.
1257
- // The type is one SEQUENCE of exactly one field, and the EXPLICIT wrapper holds exactly
1258
- // one value; anything else is refused rather than partially read.
1259
781
  var isSeq = seq.tagClass === "universal" && seq.tagNumber === asn1.TAGS.SEQUENCE;
1260
782
  if (!isSeq || !seq.children || seq.children.length !== 1) {
1261
783
  throw _err("webauthn/bad-att-cert", "the apple attestation extension is not SEQUENCE { [1] OCTET STRING } (expected exactly one field)");
@@ -1267,9 +789,6 @@ function _appleNonce(extValue) {
1267
789
  try { return asn1.read.octetString(tagged.children[0]); } catch (e) { throw _err("webauthn/bad-att-cert", "the apple attestation nonce is not an OCTET STRING", e); }
1268
790
  }
1269
791
 
1270
- // The complete Android Key Attestation KeyDescription conformance (AOSP schema + WebAuthn
1271
- // 8.4.1) lives in validator-keydesc, composed here. A structural fault is bad-att-cert; a
1272
- // well-formed description that fails an 8.4.1 MUST is verify-failed.
1273
792
  function _checkAndroidKeyDescription(cert, clientDataHash) {
1274
793
  validator.keydesc.androidKeyDescription(cert, clientDataHash, _exts, WebauthnError, "webauthn/bad-att-cert", "webauthn/verify-failed");
1275
794
  }
@@ -1384,74 +903,30 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
1384
903
  * // and anchor res.trustPath to your pinned roots with pki.path.validate
1385
904
  */
1386
905
  function verify(attestationObject, clientDataHash, opts) {
1387
- // Two call shapes, disjoint by shape so neither is guessed at: the digest positionally, or an
1388
- // options object carrying the clientDataJSON it is computed from. A Buffer IS an object, so the
1389
- // question has to be asked as "is this not bytes?" -- the disjointness is between a BufferSource
1390
- // and everything else, and testing only the second half swallows the ordinary two-argument call.
1391
906
  if (opts === undefined && clientDataHash !== undefined &&
1392
907
  !_isBufferSource(clientDataHash) && _isPlainObject(clientDataHash)) {
1393
908
  opts = clientDataHash; clientDataHash = undefined;
1394
909
  }
1395
910
  opts = opts || {};
1396
- // Every option here either GATES the verdict or supplies the trust material a gate needs, so a
1397
- // misspelled key is not a harmless no-op: `metdata` leaves the metadata gate switched off and the
1398
- // call returns a pass the caller believes was checked against the catalogue. Reject an
1399
- // unrecognized key at the boundary, and validate the one shared option every arm reads, so a bad
1400
- // instant is a config fault here rather than a trust failure reported from deep inside a chain.
1401
911
  try {
1402
912
  if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
1403
913
  guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "webauthn/bad-input", "opts has an unknown key ");
1404
- // ONE read of the caller's object, HERE, and nothing below ever touches it again. Not a
1405
- // precaution about mutation across the deferred verification -- that was already the reason for
1406
- // the copy this replaces -- but about the reads themselves: a gate that reads `opts.x` to decide
1407
- // whether a demand was made, and a later step that reads `opts.x` again to act on it, are two
1408
- // reads of a value the caller still owns, and an accessor makes them disagree. The value that
1409
- // was CHECKED must be the value that is USED, which means checking and using the same copy.
1410
- // `Object.assign` takes every own enumerable field by value at this instant, which resolves any
1411
- // accessor exactly once.
1412
914
  opts = Object.assign({}, opts);
1413
915
  if (opts.time !== undefined) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
1414
- // The boolean switches are typed HERE rather than in the arm that reads them. Validated only
1415
- // inside the android-safetynet arm, `requireCtsProfileMatch: "true"` reaching any other format
1416
- // is never examined at all -- a truthy string that demands nothing, silently accepted. A
1417
- // recognized key with an unusable value is the same class of caller mistake as an unrecognized
1418
- // key, and belongs at the same boundary.
1419
916
  _FORMAT_SCOPED_BOOLEAN_OPTS.forEach(function (k) {
1420
917
  if (opts[k] !== undefined && typeof opts[k] !== "boolean") throw _err("webauthn/bad-input", "opts." + k + " must be a boolean");
1421
918
  });
1422
919
  } catch (e) { return Promise.reject(e); }
1423
920
  var att;
1424
- // Both byte inputs are snapshotted BEFORE anything reads them, for the same reason the trust
1425
- // anchors are: the attestation statement is not evaluated until a later promise turn, and both
1426
- // stay caller-owned across that gap. The attestation object is copied before it is parsed, not
1427
- // after, because the parse surfaces its byte fields as VIEWS -- the authenticator data the
1428
- // signature covers, the statement, the credential key -- so copying afterwards would leave every
1429
- // one of them pointing at bytes the caller can still rewrite. The clientDataHash is what binds
1430
- // the statement to this ceremony; a caller who overwrote it in that gap would have the signature
1431
- // checked against a challenge and origin nobody agreed to, and the verdict would still report a
1432
- // sound attestation. A DataView or ArrayBuffer comes in by the same door and is copied too.
1433
921
  var attBytes, cdh, clientData = null;
1434
922
  try {
1435
923
  attBytes = _bytesArg(attestationObject, "attestationObject");
1436
- // The two forms of the same input, and NEITHER is inferred from the other's absence -- the
1437
- // same rule verifyAssertion applies, because it is the same question. Supplying both invites
1438
- // them to disagree, and picking one would make the attestation cover something the caller did
1439
- // not mean.
1440
924
  var haveJson = opts.clientDataJSON !== undefined, haveHash = clientDataHash !== undefined;
1441
925
  if (haveJson === haveHash) {
1442
926
  throw _err("webauthn/bad-input", "verify takes exactly one of clientDataJSON or the clientDataHash argument");
1443
927
  }
1444
928
  if (haveJson) {
1445
- // The bytes are taken ONCE and both the reading and the hashing use that copy. Checking one
1446
- // value and hashing another would bind the attestation to client data the ceremony checks
1447
- // never saw -- the check and the use have to be of the same bytes.
1448
929
  var cdjBytes = _bytesArg(opts.clientDataJSON, "opts.clientDataJSON");
1449
- // Given the JSON, the ceremony TYPE is checked unconditionally. Which ceremony a response
1450
- // belongs to is fixed by the specification rather than chosen by the caller, and a login
1451
- // response replayed into a registration is exactly what that check stops -- so registration
1452
- // gets the same non-negotiable rule the login path already had. Without this door there was
1453
- // no way to apply it at registration at all: the hash is opaque, and the caller was left to
1454
- // compare a value the attestation never bound.
1455
930
  clientData = parseClientData(cdjBytes, {
1456
931
  expectedType: "webauthn.create",
1457
932
  expectedChallenge: opts.expectedChallenge,
@@ -1460,8 +935,6 @@ function verify(attestationObject, clientDataHash, opts) {
1460
935
  });
1461
936
  cdh = _sha("sha256", cdjBytes);
1462
937
  } else {
1463
- // Every expectation the clientData reader would have answered is unanswerable from a bare
1464
- // digest, so each is refused rather than left silently uncompared.
1465
938
  if (opts.expectedChallenge !== undefined || opts.expectedOrigin !== undefined ||
1466
939
  opts.expectedTopOrigin !== undefined) {
1467
940
  throw _err("webauthn/bad-input",
@@ -1469,62 +942,23 @@ function verify(attestationObject, clientDataHash, opts) {
1469
942
  "did not supply -- pass opts.clientDataJSON instead of the clientDataHash argument, or check them yourself");
1470
943
  }
1471
944
  cdh = _bytesArg(clientDataHash, "clientDataHash");
1472
- // The LENGTH is checked on the normalized copy, so the digest is 32 bytes whichever form it
1473
- // arrived in -- not only when it arrived as a Buffer.
1474
945
  if (cdh.length !== 32) throw _err("webauthn/bad-input", "clientDataHash must be a 32-byte SHA-256 digest");
1475
946
  }
1476
947
  } catch (e) { return Promise.reject(e); }
1477
948
  attestationObject = attBytes;
1478
949
  clientDataHash = cdh;
1479
950
  try { att = parseAttestationObject(attestationObject); } catch (e) { return Promise.reject(e); }
1480
- // A registration attestation MUST carry attestedCredentialData (the AT flag): the
1481
- // whole point is to bind the attestation to a credential public key. Reject an
1482
- // AT-clear authenticatorData up front -- else the packed x5c arm could resolve a
1483
- // positive verdict bound to NO credential, and every arm would dereference the
1484
- // null credential key with a raw (untyped) throw (WebAuthn 6.1 / 7.1).
1485
951
  if (!att.authData.flags.at || !att.authData.credentialPublicKey) {
1486
952
  return Promise.reject(_err("webauthn/bad-auth-data", "attestation requires attestedCredentialData (the AT flag must be set)"));
1487
953
  }
1488
954
  var verifier = VERIFIERS[att.fmt];
1489
955
  if (!verifier) return Promise.reject(_err("webauthn/unsupported-format", "attestation statement format '" + att.fmt + "' is not supported"));
1490
- // A policy about TPM key properties is checked INSIDE the tpm arm, so an attestation in any other
1491
- // format would never reach it and the policy would silently apply to nothing -- a caller who
1492
- // demanded a TPM-bound key would accept a `none` attestation instead. The requirement therefore
1493
- // belongs at the dispatch, where it can refuse a format that cannot satisfy it, not in the arm
1494
- // that only runs once that format was already chosen.
1495
956
  if (opts.tpmPolicy !== undefined && !_formatCarries(att, "tpm")) {
1496
957
  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"));
1497
958
  }
1498
- // The same rule, and the same reason, for the one other option that DEMANDS rather than supplies:
1499
- // ctsProfileMatch is an android-safetynet device-integrity signal, so no other format's statement
1500
- // carries one to test. Checked only inside that arm, a caller who demanded a CTS-matching device
1501
- // would get a pass from a `packed` or `none` attestation that was never asked the question.
1502
- // A caller who passes `false` demands nothing and is not refused.
1503
959
  if (opts.requireCtsProfileMatch === true && !_formatCarries(att, "android-safetynet")) {
1504
960
  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"));
1505
961
  }
1506
- // The ceremony bindings run BEFORE the statement is evaluated: a response
1507
- // produced for another relying party, or without the user presence the caller
1508
- // requires, is not a message this call should spend a signature verification on
1509
- // -- and refusing it early keeps the "acceptable" question ahead of the "sound"
1510
- // one, which is the order sec. 7.1 puts them in.
1511
- // ONE normalized options object, built synchronously here. Nothing past this point reads
1512
- // the caller's `opts` again -- not the values, and not the presence tests that pick which
1513
- // trust policy runs.
1514
- //
1515
- // Fixing this per-field does not converge. The verifier resolves in a later turn, so every
1516
- // field the deferred path touches stays caller-owned across that gap, and each repair that
1517
- // snapshotted one more field left the next one aliased: the roots, then the safetynet
1518
- // roots, then the routing test beside them, then the metadata routing test, then the
1519
- // validation instant. They are not separate bugs, they are one window, and the way to
1520
- // close a window is to stop reading through it rather than to copy one more thing across.
1521
- //
1522
- // `Object.assign` captures every own field by value at this instant, which fixes both the
1523
- // values and the `!== undefined` presence tests derived from them. The two root arrays get
1524
- // a deeper copy because their CONTENTS are caller-owned too. `metadata` is captured by
1525
- // reference: a verified catalogue can hold thousands of entries and copying it per
1526
- // registration is a real cost, so reassignment can no longer swap it, while mutation of
1527
- // the object it points at remains open and is tracked separately.
1528
962
  var bindingChecked, vopts;
1529
963
  try {
1530
964
  bindingChecked = _applyBindings(att.authData, att.authData.credentialPublicKey, opts);
@@ -1535,23 +969,13 @@ function verify(attestationObject, clientDataHash, opts) {
1535
969
  } catch (e) { return Promise.reject(e); }
1536
970
  return Promise.resolve().then(function () { return verifier(att, clientDataHash, vopts); })
1537
971
  .then(function (res) {
1538
- // What the clientData reader found, on the verdict rather than left for the caller to
1539
- // recompute -- the same field the login verdict carries, for the same reason: `checked` is
1540
- // where a comparison that ran is told from one that never did. Null when the caller supplied
1541
- // only the digest, which is the honest answer: nothing read it.
1542
972
  res.clientData = clientData;
1543
973
  return res;
1544
974
  })
1545
975
  .then(function (res) {
1546
- // Metadata governs when supplied; caller roots are the fallback for the models
1547
- // the catalogue does not cover. Whichever ran, the verdict SAYS which -- so a
1548
- // caller can tell an anchored trust path from one nobody checked, rather than
1549
- // inferring it from which option they happened to pass.
1550
976
  if (vopts.metadata === undefined) {
1551
977
  if (vopts.rootCertificates !== undefined) return _applyCallerRoots(res, vopts.rootCertificates, vopts);
1552
978
  res.anchoredTo = _anchoredRoutes(res, null);
1553
- // Coverage is reported whenever anything WAS anchored, for the same reason the other
1554
- // routes report it: `anchoredTo` alone cannot say how much of a compound it covered.
1555
979
  var sn = _safetyNetAnchored(res);
1556
980
  if (sn) {
1557
981
  res.anchoredElements = { total: (res.fmt === "compound" && Array.isArray(res.compound)) ? res.compound.length : 1,
@@ -1561,29 +985,9 @@ function verify(attestationObject, clientDataHash, opts) {
1561
985
  }
1562
986
  return Promise.resolve().then(function () { return _applyMetadata(res, att, vopts); })
1563
987
  .then(function (out) { out.anchoredTo = _anchoredRoutes(out, "metadata"); return out; }, function (e) {
1564
- // A catalogue MISS is the case pinned roots exist for: the combined
1565
- // configuration is exactly "MDS-listed authenticators AND Apple", and Apple
1566
- // is in no catalogue, so failing here would make the documented pairing
1567
- // unusable for the very models it was added to reach.
1568
- //
1569
- // A catalogue DENIAL is not a miss and never falls through. If the model IS
1570
- // listed and its entry disqualifies it -- a revoked status report, a trust
1571
- // path that does not reach the roots that model registered -- then the
1572
- // catalogue has spoken about this authenticator, and letting a static pin
1573
- // overrule it would turn the stronger source into the weaker one.
1574
988
  if (!e || e.code !== "webauthn/metadata-not-found" || vopts.rootCertificates === undefined) throw e;
1575
- // Only the elements the catalogue did not cover fall back. The listed ones were
1576
- // already governed AND chain-validated to the roots their own entries register, so
1577
- // re-judging them against an unrelated pin would refuse the combined configuration
1578
- // this fallback exists to support.
1579
989
  return Promise.resolve(_applyCallerRoots(res, vopts.rootCertificates, vopts, e.missedPaths))
1580
990
  .then(function (out) {
1581
- // Report BOTH routes. The pinned roots covered only the elements the catalogue
1582
- // missed; the rest were governed by their metadata entries and chain-validated
1583
- // against the roots those entries register. Saying "rootCertificates" alone
1584
- // would attribute the whole evaluation to the weaker half and lose the entries
1585
- // an auditor needs to see. `anchoredTo` names both, and the governed entries
1586
- // are surfaced exactly as the pure-metadata route surfaces them.
1587
991
  var applied = e.appliedEntries || [];
1588
992
  if (!applied.length) return out;
1589
993
  var primary = applied[0];
@@ -1598,68 +1002,21 @@ function verify(attestationObject, clientDataHash, opts) {
1598
1002
  .then(function (res) { res.bindingChecked = bindingChecked; return res; });
1599
1003
  }
1600
1004
 
1601
- // WebAuthn sec. 7.1 step 22-23: having verified the attestation, look the authenticator model up in
1602
- // the metadata and require its trust path to chain to one of the roots that model is registered
1603
- // with. The metadata must be a verifyMetadataBlob RESULT, never raw bytes -- accepting bytes here
1604
- // would let an unverified BLOB decide which roots are acceptable, which is the whole question.
1605
1005
  function _applyMetadata(res, att, opts) {
1606
1006
  var md = opts.metadata;
1607
- // Provenance, not shape: only a catalogue this toolkit actually verified may decide which roots
1608
- // an authenticator is allowed to chain to. Recognizing it by its property names would accept a
1609
- // hand-built object, or one restored from a cache an attacker can write -- neither of which has
1610
- // been through the signature and chain checks that give the catalogue its authority.
1611
1007
  if (!mds.isVerifiedResult(md)) {
1612
1008
  throw _err("webauthn/bad-input", "opts.metadata must be a pki.webauthn.verifyMetadataBlob result, not a raw BLOB");
1613
1009
  }
1614
- // A compound attestation (sec. 8.9) deliberately carries an EMPTY top-level trust path, because
1615
- // its elements yield independent chains with no single ordered path -- each element's own path is
1616
- // on its entry in `compound`. Treating that empty path as "nothing to anchor" would refuse every
1617
- // compound attestation outright, including ones whose certificate-bearing elements all chain to
1618
- // the model's registered roots. So the paths to check are the elements'.
1619
- // Each path travels WITH the format that produced it, because the format is what decides which
1620
- // identifier may name its entry -- and a compound mixes formats. A compound's elements are
1621
- // independent claims: a packed element's AAGUID is signed while a fido-u2f element's is not, so a
1622
- // single choice for the whole statement is wrong in both directions. It would push the packed
1623
- // element down the certificate-identifier path (where a conforming entry indexed only by its
1624
- // AAGUID is not found), or trust the u2f element's unsigned AAGUID.
1625
- // A compound's elements are independent claims (sec. 8.9), and one carrying no
1626
- // certificates -- a `none` or self element -- makes no attestation claim there is
1627
- // anything to anchor. Refusing the whole statement over it would reject a
1628
- // conforming compound whose certificate-bearing elements do chain; enforcing over
1629
- // the rest and saying nothing would report catalogue enforcement over a statement
1630
- // it did not wholly reach. So the elements that CAN be governed are, and the
1631
- // coverage is reported (`anchoredElements`) rather than left to be assumed.
1632
1010
  var paths = (res.fmt === "compound" && Array.isArray(res.compound))
1633
1011
  ? res.compound.filter(function (el) { return el.trustPath && el.trustPath.length; })
1634
1012
  .map(function (el) { return { tp: el.trustPath, fmt: el.fmt, at: el.chainValidatedAt }; })
1635
1013
  : (res.trustPath && res.trustPath.length ? [{ tp: res.trustPath, fmt: res.fmt, at: res.chainValidatedAt }] : []);
1636
1014
  res.anchoredElements = { total: (res.fmt === "compound" && Array.isArray(res.compound)) ? res.compound.length : 1,
1637
1015
  anchored: paths.length };
1638
- // An attestation with no trust path at all (`none`, or a self-attestation) has nothing to anchor,
1639
- // so a caller who asked for metadata enforcement must be told it could not be applied rather than
1640
- // receiving a pass that looks like it was.
1641
1016
  if (!paths.length) {
1642
1017
  throw _err("webauthn/metadata-not-applicable", "opts.metadata was supplied, but this attestation carries no trust path to anchor (format '" + res.fmt + "')");
1643
1018
  }
1644
- // The catalogue's two key spaces are DISJOINT, and which one applies is decided by what the
1645
- // authenticator declares -- never by trying one and falling back to the other. An authenticator
1646
- // that declares a signed model identity is looked up by it and by nothing else, so a model the
1647
- // catalogue does not list is refused instead of being resolved out of the U2F key space under
1648
- // some other authenticator's entry.
1649
1019
  var aaguid = mds.aaguidToString(att.authData.aaguid);
1650
- // The aaguid may only select the entry when the attestation SIGNATURE covers it. For fido-u2f it
1651
- // does not -- sec. 8.6 signs 0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F,
1652
- // which excludes the field entirely -- so those 16 bytes are attacker-editable. Trusting them
1653
- // there does not merely misreport a model: setting them to a LISTED model that shares the
1654
- // vendor's registered root makes the forged statement resolve to that model's entry, skipping the
1655
- // real U2F entry's own key-identifier lookup and, with it, its status reports. A revoked
1656
- // authenticator would present itself as its healthy sibling. So for that format the certificate
1657
- // key identifier decides, whatever the field says.
1658
- // Every path is resolved and enforced against ITS OWN entry, by the identifier ITS OWN format
1659
- // allows. A compound carries independent claims and its element order is not signed, so choosing
1660
- // one entry for the whole statement would let that order decide whose status report is consulted:
1661
- // a healthy element listed first would suppress a revoked sibling's report while both still
1662
- // anchor successfully. Each path answers for itself, and all of them must pass.
1663
1020
  var applied = [];
1664
1021
  function govern(info) {
1665
1022
  var tp = info.tp;
@@ -1669,59 +1026,21 @@ function _applyMetadata(res, att, opts) {
1669
1026
  entry = mds.metadataFor(md, aaguid);
1670
1027
  identifier = "aaguid " + aaguid;
1671
1028
  } else {
1672
- // No signed model identity: keyed by the attestation certificate instead. A path is
1673
- // anchor-adjacent first, so that certificate is its LAST element -- the one whose key the
1674
- // catalogue identifies, not the root it chains to.
1675
1029
  var keyId = mds.certKeyIdentifier(tp[tp.length - 1]);
1676
1030
  entry = mds.metadataForKeyIdentifier(md, keyId);
1677
1031
  identifier = "attestation certificate key identifier " + keyId;
1678
1032
  }
1679
1033
  if (!entry) throw _err("webauthn/metadata-not-found", "no metadata entry matches this authenticator (" + identifier + ")");
1680
- // The attestation certificate is passed in so a report that names a single certificate is
1681
- // judged against the one actually presented, rather than denying every device the entry covers.
1682
1034
  if (mds.statusDenied(entry, md, tp[tp.length - 1], at)) {
1683
1035
  throw _err("webauthn/metadata-status", "the metadata entry for " + identifier + " carries a disqualifying status report");
1684
1036
  }
1685
- // The same three inputs the status check above used. metadataAnchors makes the same decision
1686
- // for the callers who reach it directly, so handing it different inputs is how the two readings
1687
- // would come to disagree about the same entry.
1688
1037
  var anchors = mds.metadataAnchors(entry, { metadata: md, time: at, certificate: tp[tp.length - 1] });
1689
1038
  if (!anchors.length) throw _err("webauthn/metadata-no-anchor", "the metadata entry for " + identifier + " supplies no attestation root certificate");
1690
1039
  applied.push({ entry: entry, anchors: anchors, identifier: identifier });
1691
1040
  return { anchors: anchors, identifier: identifier };
1692
1041
  }
1693
- // A path must VALIDATE to one of the roots its own model registered -- signature chaining,
1694
- // validity, constraints -- not merely resemble one. A name comparison against the top of the path
1695
- // would accept any certificate asserting the registered issuer's name, which is the assertion an
1696
- // attacker controls; and certificate equality is not the question either, since a registered root
1697
- // normally ISSUES the attestation certificate rather than being it. So this composes the same
1698
- // path validation the BLOB's own chain goes through. chainToAnchor takes leaf-first and a trust
1699
- // path is anchor-adjacent first, so the order is reversed on the way in.
1700
- //
1701
- // opts.time was validated at the entry, so this instant is usable: an unusable one reaching the
1702
- // path validator would be absorbed by the chain walk and reported as an authenticator trust
1703
- // failure, which is the wrong verdict for a caller's configuration mistake.
1704
1042
  var at = opts.time !== undefined ? opts.time : new Date();
1705
- // The catalogue's own freshness is re-established HERE, at the instant it is being used to decide
1706
- // trust -- not only at the instant it was parsed. A verified result is a plain object a caller may
1707
- // hold and reuse indefinitely, so a catalogue fetched before its nextUpdate and then reused a
1708
- // month afterwards would otherwise keep authorizing an authenticator whose status reports have
1709
- // since revoked it, which is exactly what nextUpdate exists to prevent. The caller's original
1710
- // allowStale decision rides on the result, so opting out stays opted out.
1711
1043
  mds.assertFresh(md, at, "the metadata supplied as opts.metadata");
1712
- // EVERY path must pass, not merely one of them: for a compound attestation each element is an
1713
- // independent claim, and accepting the whole because one element anchored would let an
1714
- // unanchored -- or revoked -- element ride along on its neighbor's trust.
1715
- // Govern EVERY path BEFORE any is chain-validated, and let the SEVEREST outcome decide
1716
- // rather than the first one reached. A compound's elements are independent claims and
1717
- // its element order is NOT signed, so aborting on the first failure lets that order pick
1718
- // which element the catalogue gets to speak about. The asymmetry that makes it exploitable
1719
- // is that the errors are not equal: `metadata-not-found` is the one outcome the caller may
1720
- // fall back to pinned roots on, and that fallback covers the WHOLE statement. So an element
1721
- // the catalogue does not list, placed first, would raise the fallback error and a listed
1722
- // sibling carrying a disqualifying status report would never be consulted -- the unlisted
1723
- // element laundering the revoked one. A denial therefore outranks a miss wherever each sits:
1724
- // a miss is only reported once every other path has been governed and none of them objected.
1725
1044
  var governed = [];
1726
1045
  var missed = null;
1727
1046
  var missedPaths = [];
@@ -1729,51 +1048,21 @@ function _applyMetadata(res, att, opts) {
1729
1048
  try {
1730
1049
  governed.push({ info: paths[gi], g: govern(paths[gi]) });
1731
1050
  } catch (ge) {
1732
- // WHICH paths missed is carried with the error, not just THAT one did. The caller's
1733
- // pinned-root fallback is for the elements the catalogue did not cover, and it cannot
1734
- // tell which those were from the verdict alone -- every element looks the same there.
1735
1051
  if (ge && ge.code === "webauthn/metadata-not-found") { missed = missed || ge; missedPaths.push(paths[gi]); continue; }
1736
- throw ge; // a status denial, a missing anchor, any other governance failure: terminal
1052
+ throw ge;
1737
1053
  }
1738
1054
  }
1739
- // The miss carries BOTH halves: which paths the catalogue did not cover, and which entries
1740
- // it did govern. Without the second the fallback can only describe its own half, so a
1741
- // mixed-route verdict would attribute the whole evaluation to the pinned roots and drop the
1742
- // metadata-backed part entirely -- the stronger half, and the one an auditor most needs.
1743
1055
  if (missed) { missed.missedPaths = missedPaths; missed.appliedEntries = applied; }
1744
- // The miss is NOT reported yet. Resolving an entry is only half of governance: the other
1745
- // half is that the path must actually VALIDATE to the roots that entry registers, and that
1746
- // runs below. Reporting the miss here would skip it for every listed element -- so a listed
1747
- // element whose path reaches the caller's pinned roots but NOT its own registered roots
1748
- // would ride out on the unlisted sibling's fallback, which is that same bypass moved one
1749
- // phase down. Every listed element is chain-validated first, and only then may a miss be
1750
- // raised.
1751
1056
  return governed.reduce(function (p, item) {
1752
1057
  return p.then(function () {
1753
1058
  var g = item.g, info = item.info;
1754
- // A path is re-validated at the instant its own format already judged it at, when the format
1755
- // established one from signed data. An android-safetynet response carries its signing time and
1756
- // its service chain has usually expired since; resetting to the current clock here would
1757
- // refuse the very registration the format verifier just accepted. An explicit opts.time still
1758
- // wins, since the caller knows when the registration happened. The catalogue's own expiry is a
1759
- // separate question and is judged above, against the caller's instant rather than this one.
1760
1059
  var pathAt = opts.time !== undefined ? opts.time : (info.at || at);
1761
1060
  return mds.chainToAnchor(info.tp.slice().reverse(), g.anchors, pathAt,
1762
1061
  "attestation trust path for " + g.identifier + " (against the roots its metadata entry registers)");
1763
1062
  });
1764
1063
  }, Promise.resolve())
1765
1064
  .then(function () {
1766
- // Every LISTED element has now been fully governed -- entry resolved, status checked, and its
1767
- // path validated to that entry's own registered roots. A miss recorded above may finally be
1768
- // reported, and the caller's pinned-roots fallback is reachable only from here: a statement
1769
- // whose listed elements all satisfied their catalogue entries, with a sibling the catalogue
1770
- // simply does not cover.
1771
1065
  if (missed) throw missed;
1772
- // The ENTRY's aaguid, not the authenticator's raw field: for a U2F authenticator that field is
1773
- // all zeroes, which means "no model identity" -- reporting it back as though it were one would
1774
- // hand the caller a value that matches nothing and reads like an identifier. `entries` lists
1775
- // every entry that governed a path, which for a compound is one per certificate-bearing
1776
- // element; `entry` is the first, and equals the only one for every other format.
1777
1066
  var primary = applied[0];
1778
1067
  res.metadata = { aaguid: primary.entry.aaguid, keyIdentifiers: primary.entry.keyIdentifiers || [],
1779
1068
  entry: primary.entry, entries: applied.map(function (a) { return a.entry; }),
@@ -1782,13 +1071,6 @@ function _applyMetadata(res, att, opts) {
1782
1071
  });
1783
1072
  }
1784
1073
 
1785
- // Does this attestation carry a statement of the named format -- as the whole statement, or as one
1786
- // element of a compound (sec. 8.9), whose own arm would apply a policy about that format?
1787
- //
1788
- // Every option that DEMANDS something only one format can produce asks this same question, so it is
1789
- // asked in one place: a demand about a TPM public area and a demand about an android-safetynet
1790
- // device-integrity signal differ only in the format they name. Answering it per option is how the
1791
- // second one comes to be answered only for the simple case and not for a compound.
1792
1074
  function _formatCarries(att, fmt) {
1793
1075
  if (att.fmt === fmt) return true;
1794
1076
  if (att.fmt !== "compound") return false;
@@ -1930,12 +1212,7 @@ void constants;
1930
1212
  * // await pki.path.validate(res.trustPath, { trustAnchors: anchors, time: mdsTime });
1931
1213
  */
1932
1214
 
1933
- // ---- public: parseClientData -------------------------------------------------
1934
1215
 
1935
- // The two ceremony types, fixed by the spec -- `webauthn.create` for registration
1936
- // and `webauthn.get` for authentication. This is NOT relying-party policy: a
1937
- // registration response replayed into a login (or the reverse) is a real attack,
1938
- // and which one a caller is finishing is known at the call site, never negotiated.
1939
1216
  var CLIENT_DATA_TYPE = Object.assign(Object.create(null), { "webauthn.create": 1, "webauthn.get": 1 });
1940
1217
 
1941
1218
  /**
@@ -1994,8 +1271,6 @@ function parseClientData(bytes, opts) {
1994
1271
  opts = opts || {};
1995
1272
  if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
1996
1273
  guard.identifier.assertKnownKeys(opts, _CLIENT_DATA_OPTS, _err, "webauthn/bad-input", "opts has an unknown key ");
1997
- // The RAW bytes, not a parsed object: this reader decides on the exact octets the authenticator
1998
- // signed over, and re-serializing a parsed object would decide on different ones.
1999
1274
  bytes = _bytesArg(bytes, "clientDataJSON");
2000
1275
  var doc = guard.json.parse(Buffer.from(bytes), _err, {
2001
1276
  maxBytes: constants.LIMITS.JSON_MAX_BYTES, maxDepth: constants.LIMITS.JSON_MAX_DEPTH,
@@ -2015,8 +1290,6 @@ function parseClientData(bytes, opts) {
2015
1290
  if (typeof doc.challenge !== "string" || !doc.challenge.length) {
2016
1291
  throw _err("webauthn/bad-client-data", "clientDataJSON carries no challenge (WebAuthn sec. 5.8.1)");
2017
1292
  }
2018
- // base64url, and STRICTLY: the challenge is a comparison key, so an encoding the
2019
- // decoder had to guess at would let two spellings of one value both "match".
2020
1293
  var challenge;
2021
1294
  try {
2022
1295
  challenge = guard.encoding.base64url(doc.challenge, constants.LIMITS.JSON_MAX_BYTES, _err,
@@ -2028,10 +1301,6 @@ function parseClientData(bytes, opts) {
2028
1301
  if (doc.crossOrigin !== undefined && typeof doc.crossOrigin !== "boolean") {
2029
1302
  throw _err("webauthn/bad-client-data", "clientDataJSON crossOrigin must be a boolean when present (WebAuthn sec. 5.8.1)");
2030
1303
  }
2031
- // A present-but-malformed topOrigin is refused rather than reported as absent. It
2032
- // is the top-level origin of a cross-origin ceremony -- something a caller makes a
2033
- // policy decision on -- so "the sender wrote something that is not an origin" and
2034
- // "the sender said nothing" must not arrive as the same `null`.
2035
1304
  if (doc.topOrigin !== undefined && (typeof doc.topOrigin !== "string" || !doc.topOrigin.length)) {
2036
1305
  throw _err("webauthn/bad-client-data", "clientDataJSON topOrigin must be a non-empty string when present (WebAuthn sec. 5.8.1)");
2037
1306
  }
@@ -2060,9 +1329,6 @@ function parseClientData(bytes, opts) {
2060
1329
  if (!allowed.length || !allowed.every(function (o) { return typeof o === "string" && o.length; })) {
2061
1330
  throw _err("webauthn/bad-input", "opts.expectedOrigin must be a non-empty origin string, or an array of them");
2062
1331
  }
2063
- // Compared WHOLE and case-sensitively as the serialization it is. A prefix or
2064
- // suffix test is how "https://example.com.attacker.tld" passes for
2065
- // "https://example.com".
2066
1332
  if (allowed.indexOf(doc.origin) === -1) {
2067
1333
  throw _err("webauthn/client-data-mismatch",
2068
1334
  "the clientDataJSON origin " + JSON.stringify(doc.origin) + " is not one this relying party accepts " +
@@ -2070,23 +1336,8 @@ function parseClientData(bytes, opts) {
2070
1336
  }
2071
1337
  checked.origin = true;
2072
1338
  }
2073
- // The top-level origin of a cross-origin ceremony. It is surfaced either way, but a value a
2074
- // relying party only READS is a value nobody compares: `origin` in a cross-origin ceremony is the
2075
- // iframe's, and the top-level page that framed it is the one the security decision is actually
2076
- // about. Given what it accepts, this compares it the same way as `origin` -- whole, case
2077
- // sensitive, against a list -- so the two cannot drift into different matching rules.
2078
- //
2079
- // The three cases are kept apart rather than collapsed. An expectation of `null` means "this
2080
- // ceremony must NOT be framed", which is a real policy and cannot be expressed by an origin list;
2081
- // a response carrying no topOrigin against a list of acceptable ones is not framed at all, so the
2082
- // list has nothing to accept and the ceremony is refused rather than passed.
2083
1339
  if (opts.expectedTopOrigin !== undefined) {
2084
1340
  var wantTop = opts.expectedTopOrigin;
2085
- // Whether the ceremony was framed is stated by TWO fields, and the answer is only usable when
2086
- // they agree. `crossOrigin` says a framing happened; `topOrigin` names the page that did it.
2087
- // Reading one and not the other lets a response that declares itself cross-origin, but omits
2088
- // the origin, satisfy a policy of "must not be framed" -- it answers the caller's question with
2089
- // the field it left out rather than the one it filled in.
2090
1341
  var framed = doc.crossOrigin === true;
2091
1342
  if (wantTop === null) {
2092
1343
  if (framed || doc.topOrigin !== undefined) {
@@ -2100,10 +1351,6 @@ function parseClientData(bytes, opts) {
2100
1351
  if (!allowedTop.length || !allowedTop.every(function (o) { return typeof o === "string" && o.length; })) {
2101
1352
  throw _err("webauthn/bad-input", "opts.expectedTopOrigin must be null, a non-empty origin string, or an array of them");
2102
1353
  }
2103
- // Allow-listing the pages that may frame this ceremony is a statement about a framed one. A
2104
- // response that does not say it was framed makes no such claim to match against, so the list
2105
- // has nothing to accept -- and treating its absent topOrigin as a pass would report a check
2106
- // that decided nothing.
2107
1354
  if (!framed || allowedTop.indexOf(doc.topOrigin) === -1) {
2108
1355
  throw _err("webauthn/client-data-mismatch",
2109
1356
  "the clientDataJSON topOrigin " + JSON.stringify(doc.topOrigin === undefined ? null : doc.topOrigin) +
@@ -2123,7 +1370,6 @@ var _CLIENT_DATA_OPTS = Object.assign(Object.create(null), {
2123
1370
  expectedType: 1, expectedChallenge: 1, expectedOrigin: 1, expectedTopOrigin: 1,
2124
1371
  });
2125
1372
 
2126
- // ---- public: parseAuthenticatorData / verifyAssertion ------------------------
2127
1373
 
2128
1374
  /**
2129
1375
  * @primitive pki.webauthn.parseAuthenticatorData
@@ -2154,8 +1400,6 @@ function parseAuthenticatorData(bytes) {
2154
1400
  return _parseAuthData(_bytesArg(bytes, "authenticatorData"), _err);
2155
1401
  }
2156
1402
 
2157
- // The options pki.webauthn.verifyAssertion recognizes, null-prototype for the same
2158
- // reason _VERIFY_OPTS is.
2159
1403
  var _ASSERT_OPTS = Object.assign(Object.create(null), {
2160
1404
  authenticatorData: 1, clientDataHash: 1, clientDataJSON: 1, signature: 1,
2161
1405
  credentialPublicKey: 1, previousSignCount: 1,
@@ -2218,26 +1462,14 @@ var _ASSERT_OPTS = Object.assign(Object.create(null), {
2218
1462
  * res.bindingChecked.rpId; // true -- the rpIdHash matched example.com
2219
1463
  * // the challenge and origin in clientDataJSON are still yours to compare
2220
1464
  */
2221
- // A private copy of the assertion inputs. Only what the caller can still write to
2222
- // is copied: the byte fields, the descriptor itself, and the option arrays. The
2223
- // COSE key is a decoded object the caller supplies from its own storage; it is
2224
- // shallow-copied so a swapped field cannot reach the verification, which is the
2225
- // mutation this can defend against without deep-copying an arbitrary object.
2226
1465
  function _snapshotAssertion(input) {
2227
1466
  if (!_isPlainObject(input)) throw _err("webauthn/bad-input", "pki.webauthn.verifyAssertion takes an options object");
2228
1467
  guard.identifier.assertKnownKeys(input, _ASSERT_OPTS, _err, "webauthn/bad-input", "verifyAssertion input has an unknown key ");
2229
1468
  var out = {}, k;
2230
1469
  for (k in input) { if (_hasOwn(input, k)) out[k] = input[k]; }
2231
- // The SAME set of byte forms the arguments accept. A snapshot list narrower than the accept list
2232
- // leaves whatever falls in the gap aliased across the yield -- which is the one input a widened
2233
- // accept list newly admits, so the two must be written from one predicate.
2234
1470
  ["authenticatorData", "clientDataJSON", "clientDataHash", "signature", "expectedChallenge"].forEach(function (f) {
2235
1471
  if (_isBufferSource(out[f])) out[f] = guard.bytes.snapshotSource(out[f], WebauthnError, "webauthn/bad-input", f);
2236
1472
  });
2237
- // The BYTES form first. A Buffer satisfies the plain-object test below, so leaving it to that
2238
- // branch copies its numeric indices into a `{0:.., 1:..}` object that is no longer a key at all
2239
- // -- the stored credential silently becoming something the SPKI builder cannot read. It is
2240
- // snapshotted here for the same reason the other byte inputs are: it is read after a yield.
2241
1473
  if (_isBufferSource(out.credentialPublicKey)) {
2242
1474
  out.credentialPublicKey = guard.bytes.snapshotSource(out.credentialPublicKey, WebauthnError,
2243
1475
  "webauthn/bad-input", "credentialPublicKey");
@@ -2245,10 +1477,6 @@ function _snapshotAssertion(input) {
2245
1477
  var key = {}, kk;
2246
1478
  for (kk in out.credentialPublicKey) {
2247
1479
  if (!_hasOwn(out.credentialPublicKey, kk)) continue;
2248
- // The read itself can fault: a stored key is a caller-supplied object, so a field may be an
2249
- // accessor, and one that THROWS would escape as a raw error from the very copy whose job is
2250
- // to make the descriptor stop being the caller's. The copy is the boundary, so the fault is
2251
- // named here rather than surfacing from wherever the field was later used.
2252
1480
  var v;
2253
1481
  try { v = out.credentialPublicKey[kk]; }
2254
1482
  catch (e) { throw _err("webauthn/bad-cose-key", "credentialPublicKey." + kk + " could not be read", e); }
@@ -2263,21 +1491,12 @@ function _snapshotAssertion(input) {
2263
1491
  }
2264
1492
 
2265
1493
  function verifyAssertion(input) {
2266
- // Every byte this verdict depends on is COPIED here, synchronously, before
2267
- // anything defers. The descriptor is the caller's object and the buffers in it
2268
- // are the caller's memory: a caller that reuses or zeroizes them on the line
2269
- // after this call has already changed them before the deferred body would run,
2270
- // and an assertion that did not verify could be replaced by one that does. The
2271
- // same rule pki.cmc.verify applies to a response, for the same reason.
2272
1494
  var frozen;
2273
1495
  try { frozen = _snapshotAssertion(input); } catch (e) { return Promise.reject(e); }
2274
1496
  return Promise.resolve().then(function () {
2275
1497
  input = frozen;
2276
1498
  var authData = parseAuthenticatorData(input.authenticatorData);
2277
1499
  input.signature = _bytesArg(input.signature, "signature");
2278
- // The two forms of the same input, and NEITHER is inferred from the other's
2279
- // absence: supplying both invites them to disagree, and picking one would make
2280
- // the signature cover something the caller did not mean.
2281
1500
  var haveJson = input.clientDataJSON !== undefined, haveHash = input.clientDataHash !== undefined;
2282
1501
  if (haveJson === haveHash) {
2283
1502
  throw _err("webauthn/bad-input", "verifyAssertion takes exactly one of clientDataJSON or clientDataHash");
@@ -2285,11 +1504,6 @@ function verifyAssertion(input) {
2285
1504
  var clientDataHash, clientData = null;
2286
1505
  if (haveJson) {
2287
1506
  input.clientDataJSON = _bytesArg(input.clientDataJSON, "clientDataJSON");
2288
- // Given the JSON, this verb reads it -- the ceremony TYPE is checked
2289
- // unconditionally, because which ceremony a response belongs to is fixed by
2290
- // the spec rather than chosen by the caller, and a registration response
2291
- // replayed as a login is exactly what that check stops. The challenge and
2292
- // origin are checked when the caller supplies what it issued.
2293
1507
  clientData = parseClientData(input.clientDataJSON, {
2294
1508
  expectedType: "webauthn.get",
2295
1509
  expectedChallenge: input.expectedChallenge,
@@ -2298,9 +1512,6 @@ function verifyAssertion(input) {
2298
1512
  });
2299
1513
  clientDataHash = _sha("sha256", Buffer.from(input.clientDataJSON));
2300
1514
  } else {
2301
- // Every expectation this verb forwards to the clientData reader is unanswerable without the
2302
- // JSON, so each is refused here rather than silently going uncompared. The list is the
2303
- // forwarded set above; a new expectation added there is added here too.
2304
1515
  if (input.expectedChallenge !== undefined || input.expectedOrigin !== undefined ||
2305
1516
  input.expectedTopOrigin !== undefined) {
2306
1517
  throw _err("webauthn/bad-input",
@@ -2310,34 +1521,19 @@ function verifyAssertion(input) {
2310
1521
  clientDataHash = _bytesArg(input.clientDataHash, "clientDataHash");
2311
1522
  if (clientDataHash.length !== 32) throw _err("webauthn/bad-input", "clientDataHash must be a 32-byte SHA-256 digest");
2312
1523
  }
2313
- // Either form a relying party can be holding. `verify` hands back the parsed object, but the
2314
- // durable form is BYTES: the object carries Buffers, so a JSON round trip through a datastore
2315
- // returns {"type":"Buffer","data":[...]} rather than what went in, and every existing credential
2316
- // store already holds the COSE bytes. Accepting only the object made a caller fabricate an
2317
- // authenticatorData that never existed just to reach their own key.
2318
1524
  var coseKey = input.credentialPublicKey;
2319
1525
  if (guard.bytes.isByteSource(coseKey)) {
2320
1526
  coseKey = parseCoseKey(coseKey);
2321
1527
  } else if (_isPlainObject(coseKey)) {
2322
- // The OBJECT form is held to the same rules about the KEY as the bytes form. The bytes went
2323
- // through the curve/length, RSA modulus-floor and exponent checks; the object went through
2324
- // none, so one stored credential was refused in one form and imported for signature
2325
- // verification in the other. Which form a relying party stores is a question about what their
2326
- // datastore round-trips, not about how carefully their credential is checked.
2327
1528
  coseKey = validator.cose.assertKeyMaterial(coseKey, WebauthnError, "webauthn/bad-cose-key", "webauthn/unsupported-algorithm");
2328
1529
  } else {
2329
1530
  throw _err("webauthn/bad-input", "credentialPublicKey must be the stored COSE key -- the object pki.webauthn.verify returned, or its COSE bytes");
2330
1531
  }
2331
1532
  var bindingChecked = _applyBindings(authData, coseKey, input);
2332
- // The counter's SHAPE is a config-time question and is answered here; whether it
2333
- // advanced is a question about the message, and that is asked only after the
2334
- // signature holds -- see below.
2335
1533
  var prev = input.previousSignCount;
2336
1534
  if (prev !== undefined && (typeof prev !== "number" || !Number.isSafeInteger(prev) || prev < 0 || prev > 0xFFFFFFFF)) {
2337
1535
  throw _err("webauthn/bad-input", "previousSignCount must be an integer in 0..4294967295");
2338
1536
  }
2339
- // The signed message is the CONCATENATION, in that order, of the
2340
- // authenticatorData exactly as it arrived and the clientDataJSON digest.
2341
1537
  var message = Buffer.concat([Buffer.from(input.authenticatorData), clientDataHash]);
2342
1538
  var spki = _coseKeyToSpki(coseKey);
2343
1539
  return _verifySig(coseKey.alg, Buffer.from(input.signature), spki, message, _err).then(function (ok) {
@@ -2345,20 +1541,6 @@ function verifyAssertion(input) {
2345
1541
  throw _err("webauthn/bad-signature",
2346
1542
  "the assertion signature does not verify under the stored credential public key (WebAuthn sec. 7.2 step 20)");
2347
1543
  }
2348
- // sec. 7.2 step 21, and only NOW. A counter that fails to advance means two
2349
- // authenticators hold one credential -- an alarm a relying party acts on, by
2350
- // locking the account or revoking the credential. Judging it before the
2351
- // signature would let anyone raise that alarm with arbitrary bytes, so the
2352
- // counter is read only out of an assertion that proved to be authentic. The
2353
- // 0/0 case is an authenticator that implements no counter, which is permitted.
2354
- // THREE outcomes, kept apart, because this field's only purpose is to tell a relying party
2355
- // whether it has cloned-authenticator detection on this credential:
2356
- // false -- not requested; no previousSignCount was supplied
2357
- // "not-supported" -- requested, but the authenticator implements no counter (the 0/0 case
2358
- // sec. 7.2 permits), so the comparison was deliberately skipped
2359
- // true -- requested and performed
2360
- // Reporting `true` for the waived case claimed a detection that cannot happen, and a bare
2361
- // `false` there would be indistinguishable from never having asked.
2362
1544
  var signCountChecked = false;
2363
1545
  if (prev !== undefined) {
2364
1546
  if (prev === 0 && authData.signCount === 0) {
@@ -2373,16 +1555,13 @@ function verifyAssertion(input) {
2373
1555
  }
2374
1556
  }
2375
1557
  return {
2376
- valid: true, // canonical toolkit-wide verdict alias, beside the verb's own `signatureVerified`
1558
+ valid: true,
2377
1559
  signatureVerified: true,
2378
1560
  signCount: authData.signCount,
2379
1561
  signCountChecked: signCountChecked,
2380
1562
  flags: authData.flags,
2381
1563
  rpIdHash: authData.rpIdHash,
2382
1564
  extensions: authData.extensions,
2383
- // The decoded clientData when this call was given the JSON, so a caller that
2384
- // still owes itself a comparison has the values without re-parsing; null
2385
- // when only the digest was supplied and there was nothing to read.
2386
1565
  clientData: clientData,
2387
1566
  bindingChecked: bindingChecked,
2388
1567
  };