@blamejs/pki 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,31 +20,103 @@
20
20
  // FIDO Metadata Service v3.0 sec. 3.1 / sec. 3.2, RFC 7515 (JWS).
21
21
 
22
22
  var frameworkError = require("./framework-error");
23
- var asn1 = require("./asn1-der");
24
23
  var x509 = require("./schema-x509");
25
24
  var guard = require("./guard-all");
26
25
  var jose = require("./jose");
27
26
  var rfc3339 = require("./rfc3339");
28
27
  var constants = require("./constants");
29
28
  var pathValidate = require("./path-validate");
29
+ var signScheme = require("./sign-scheme");
30
+ var edwardsPoint = require("./edwards-point");
30
31
  var webcrypto = require("./webcrypto");
31
32
  var nodeCrypto = require("crypto");
32
33
 
34
+ var oid = require("./oid");
35
+ var pkix = require("./schema-pkix");
33
36
  var WebauthnError = frameworkError.WebauthnError;
37
+ var _KU_NS = pkix.makeNS("webauthn", WebauthnError, oid);
34
38
  function _err(code, message, cause) { return new WebauthnError(code, message, cause); }
35
39
  var C = constants.LIMITS;
36
40
 
37
- // The JWS algorithms a metadata BLOB may be signed with, each with the key family it requires.
41
+ // Which signature schemes each certificate key type may perform, keyed by the SPKI algorithm the
42
+ // x5c leaf carries. Taking the scheme from the token and the key from the chain without checking
43
+ // they agree is the JWS algorithm-confusion class, and the relation is not one-to-one: a
44
+ // PKCS#1-encoded RSA key can do either RSA scheme, while an id-RSASSA-PSS key is restricted BY ITS
45
+ // CERTIFICATE to RSASSA-PSS and must not verify an RS* signature (RFC 4055 sec. 1.2 / 3.1).
46
+ //
47
+ // The Edwards and ML-DSA rows exist because an X.509 SubjectPublicKeyInfo carries those keys
48
+ // perfectly well -- RFC 8410 for Ed25519 / Ed448, RFC 9881 for ML-DSA -- so a JWS algorithm over
49
+ // one is a conformant BLOB signature, not a theoretical one. `EdDSA` names a scheme without fixing
50
+ // a curve (RFC 8037), so for that scheme the certificate decides which of the two it is; every
51
+ // other row's algorithm fixes its own.
52
+ var LEAF_SCHEMES_BY_SPKI_ALG = Object.assign(Object.create(null), {
53
+ ecPublicKey: { ECDSA: 1 },
54
+ rsaEncryption: { "RSASSA-PKCS1-v1_5": 1, "RSA-PSS": 1 },
55
+ rsassaPss: { "RSA-PSS": 1 },
56
+ Ed25519: { EdDSA: 1 },
57
+ Ed448: { EdDSA: 1 },
58
+ "id-ml-dsa-44": { "ML-DSA-44": 1 },
59
+ "id-ml-dsa-65": { "ML-DSA-65": 1 },
60
+ "id-ml-dsa-87": { "ML-DSA-87": 1 },
61
+ });
62
+
63
+ // The sign-scheme resolver's faults arrive as a bare kind; they keep webauthn/* codes here so a
64
+ // BLOB whose leaf pins a hash this toolkit cannot read fails as a webauthn verdict, not a foreign one.
65
+ function _schemeE(kind, message, cause) { return new WebauthnError("webauthn/" + kind, message, cause); }
66
+
67
+ // The JWS algorithms a metadata BLOB may be signed with, each with the key family it requires and
68
+ // the WebCrypto parameters that import an x5c leaf under it and verify with it.
69
+ //
70
+ // DERIVED, not declared. A second algorithm table maintained beside pki.jose's is how PS256 came
71
+ // to verify as an ACME signature and be refused as a metadata BLOB signature: both tables were
72
+ // correct on the day they were written and only one of them was extended. Deriving means the
73
+ // question "which algorithms does this toolkit accept for a JWS?" has exactly one answer, and a
74
+ // row added there reaches here without anybody remembering to copy it.
75
+ //
76
+ // The derivation is TOTAL over the registry -- every algorithm pki.jose verifies gets a row, and an
77
+ // X.509 SubjectPublicKeyInfo can carry a key of every type in it. EC becomes ECDSA over the curve
78
+ // the alg fixes; RSA with a salt length becomes RSASSA-PSS (RFC 7518 sec. 3.5) and without one
79
+ // RSASSA-PKCS1-v1_5; OKP becomes EdDSA, whose curve the certificate supplies because the algorithm
80
+ // does not name one; AKP becomes the ML-DSA parameter set the algorithm itself fixes.
81
+ //
82
+ // A row's import and verify parameters may therefore depend on the LEAF as well as the alg, which
83
+ // is why they are resolved per-verification rather than frozen into the row: EdDSA over an Ed25519
84
+ // certificate and EdDSA over an Ed448 certificate are the same JWS algorithm and two different
85
+ // WebCrypto algorithms.
86
+ //
38
87
  // Null-prototype: `alg` is attacker-supplied, and an inherited Object member would otherwise
39
88
  // resolve to a truthy non-row and read as a recognised algorithm.
40
- var BLOB_ALGS = Object.assign(Object.create(null), {
41
- ES256: { family: "EC", hash: "SHA-256", imp: { name: "ECDSA", namedCurve: "P-256" }, ver: { name: "ECDSA", hash: "SHA-256" } },
42
- ES384: { family: "EC", hash: "SHA-384", imp: { name: "ECDSA", namedCurve: "P-384" }, ver: { name: "ECDSA", hash: "SHA-384" } },
43
- ES512: { family: "EC", hash: "SHA-512", imp: { name: "ECDSA", namedCurve: "P-521" }, ver: { name: "ECDSA", hash: "SHA-512" } },
44
- RS256: { family: "RSA", hash: "SHA-256", imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, ver: { name: "RSASSA-PKCS1-v1_5" } },
45
- RS384: { family: "RSA", hash: "SHA-384", imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" }, ver: { name: "RSASSA-PKCS1-v1_5" } },
46
- RS512: { family: "RSA", hash: "SHA-512", imp: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, ver: { name: "RSASSA-PKCS1-v1_5" } },
47
- });
89
+ function _deriveBlobAlgs() {
90
+ var out = Object.create(null);
91
+ jose.sigAlgs().forEach(function (row) {
92
+ if (row.kty === "EC") {
93
+ out[row.alg] = { scheme: "ECDSA", hash: row.hash,
94
+ imp: { name: "ECDSA", namedCurve: row.crv }, ver: { name: "ECDSA", hash: row.hash } };
95
+ } else if (row.kty === "RSA" && row.saltLength) {
96
+ out[row.alg] = { scheme: "RSA-PSS", hash: row.hash,
97
+ imp: { name: "RSA-PSS", hash: row.hash }, ver: { name: "RSA-PSS", saltLength: row.saltLength } };
98
+ } else if (row.kty === "RSA") {
99
+ out[row.alg] = { scheme: "RSASSA-PKCS1-v1_5", hash: row.hash,
100
+ imp: { name: "RSASSA-PKCS1-v1_5", hash: row.hash }, ver: { name: "RSASSA-PKCS1-v1_5" } };
101
+ } else if (row.kty === "OKP") {
102
+ // `fromLeaf` marks a row whose WebCrypto algorithm is the certificate's own key algorithm.
103
+ out[row.alg] = { scheme: "EdDSA", hash: null, fromLeaf: true };
104
+ } else if (row.kty === "AKP") {
105
+ out[row.alg] = { scheme: row.alg, hash: null, imp: { name: row.alg }, ver: { name: row.alg } };
106
+ }
107
+ });
108
+ return out;
109
+ }
110
+ var BLOB_ALGS = _deriveBlobAlgs();
111
+
112
+ // The WebCrypto import and verify parameters for one verification. Fixed by the algorithm for every
113
+ // scheme that names its own curve or parameter set; taken from the certificate for EdDSA, which
114
+ // does not. The leaf has already been checked to permit this row's scheme, so its algorithm name is
115
+ // one of the two the scheme covers.
116
+ function _blobAlgParams(algRow, leafAlgName) {
117
+ if (!algRow.fromLeaf) return algRow;
118
+ return { scheme: algRow.scheme, hash: null, imp: { name: leafAlgName }, ver: { name: leafAlgName } };
119
+ }
48
120
 
49
121
  // The status values that deny trust (MDS v3.0 sec. 3.1.4). An unknown status is IGNORED for the
50
122
  // gate and surfaced raw -- the specification requires that a verifier not fail on a status it does
@@ -78,6 +150,15 @@ function _isPlainObject(v) { return !!v && typeof v === "object" && !Array.isArr
78
150
  var _verifiedResults = new WeakSet();
79
151
  function isVerifiedResult(v) { return _isPlainObject(v) && _verifiedResults.has(v); }
80
152
 
153
+ // Which catalogue each entry came OUT of. Knowing that a supplied catalogue is verified says
154
+ // nothing about whether the entry being judged belongs to it, and a process may hold several: pair
155
+ // an entry from catalogue A with catalogue B and B's statusPolicy and freshness decide about A's
156
+ // status reports. B reading `latest-by-date` can then hand back anchors that A, reading `any`,
157
+ // records as revoked -- and a stale A can be judged current because B is. Keyed on object identity,
158
+ // for the same reason the provenance set is, and holding no strong reference to either side.
159
+ var _entryOrigin = new WeakMap();
160
+ function _isEntryOf(entry, metadata) { return _entryOrigin.get(entry) === metadata; }
161
+
81
162
  // Provenance alone is not enough: the verified catalogue is handed to the caller, and anything
82
163
  // holding a reference could rewrite `allowStale`, a status report, or an entry's registered roots
83
164
  // and the object would still pass the identity check. Freezing it means the catalogue that decides
@@ -104,17 +185,13 @@ var UNDERSTOOD_HEADER = Object.assign(Object.create(null), {
104
185
 
105
186
  // The BLOB's signing certificate must be allowed to sign. RFC 5280 sec. 4.2.1.3 numbers
106
187
  // digitalSignature bit 0 of the keyUsage BIT STRING; an absent extension places no restriction.
188
+ // Through the shared reader, which applies the NamedBitList rules a local bit test does not: DER
189
+ // drops trailing zero bits (X.690 sec. 11.2.2) and sec. 4.2.1.3 requires at least one bit set, so
190
+ // reading the bits here would let a certificate the rest of the toolkit calls malformed sign a BLOB.
107
191
  function _assertLeafSigns(leaf) {
108
- var exts = leaf.extensions || [];
109
- for (var i = 0; i < exts.length; i++) {
110
- if (exts[i].name !== "keyUsage" || exts[i].value == null) continue;
111
- var ku;
112
- try { ku = asn1.read.bitString(asn1.decode(exts[i].value)); }
113
- catch (e) { throw _err("webauthn/bad-att-cert", "the metadata BLOB x5c leaf keyUsage extension is malformed", e); }
114
- if (!ku.bytes.length || (ku.bytes[0] & 0x80) === 0) {
115
- throw _err("webauthn/bad-att-cert", "the metadata BLOB x5c leaf keyUsage does not assert digitalSignature, so it may not sign the BLOB (RFC 5280 sec. 4.2.1.3)");
116
- }
117
- return;
192
+ var ku = pkix.keyUsageOf(_KU_NS, leaf, _err, "webauthn/bad-att-cert", "metadata BLOB x5c leaf");
193
+ if (ku && ku.digitalSignature !== true) {
194
+ throw _err("webauthn/bad-att-cert", "the metadata BLOB x5c leaf keyUsage does not assert digitalSignature, so it may not sign the BLOB (RFC 5280 sec. 4.2.1.3)");
118
195
  }
119
196
  }
120
197
 
@@ -122,7 +199,7 @@ function _assertLeafSigns(leaf) {
122
199
  // public key, so that pair -- not the issuer field, and not the certificate's bytes -- is what
123
200
  // decides. The DN comparison is the canonical one, so two spellings of one name still match.
124
201
  function _isAnchorItself(cert, anchor) {
125
- return guard.name.dnEqual(cert.subject, anchor.subject) &&
202
+ return guard.name.dnEqual(cert.subject.rdns, anchor.subject.rdns, _err, "webauthn/bad-att-cert", "the anchor subject") &&
126
203
  cert.subjectPublicKeyInfo.bytes.equals(anchor.subjectPublicKeyInfo.bytes);
127
204
  }
128
205
 
@@ -135,6 +212,13 @@ function _isAnchorItself(cert, anchor) {
135
212
  // a hand-built object literal satisfies it too and then raises a raw TypeError from deep inside the
136
213
  // path validator, which is an untyped throw escaping a public verb.
137
214
  function _asCert(v, label) {
215
+ // A certificate arriving as BYTES arrives in whichever byte form the caller holds -- the same set
216
+ // every other byte argument in this namespace takes. A DataView or an ArrayBuffer over identical
217
+ // DER is the identical certificate, and refusing one of them makes the accepted set depend on how
218
+ // the caller happened to receive the file rather than on what it contains.
219
+ if (ArrayBuffer.isView(v) || v instanceof ArrayBuffer) {
220
+ v = guard.bytes.source(v, WebauthnError, "webauthn/bad-input", label);
221
+ }
138
222
  if (v && typeof v === "object" && !Buffer.isBuffer(v) && !(v instanceof Uint8Array) &&
139
223
  v.subject && v.subjectPublicKeyInfo && Buffer.isBuffer(v.subjectPublicKeyInfo.bytes) &&
140
224
  v.signatureAlgorithm && typeof v.signatureAlgorithm.oid === "string" &&
@@ -197,8 +281,12 @@ function _verifyMetadataBlob(blob, opts) {
197
281
  // length is its character count and a BLOB is ASCII base64url + dots, so it bounds the byte count.
198
282
  // A byte input is re-viewed through the shared guard, which is where the detached-backing-buffer
199
283
  // case is handled once, rather than re-derived here.
200
- var raw = (Buffer.isBuffer(blob) || blob instanceof Uint8Array)
201
- ? guard.bytes.view(blob, WebauthnError, "webauthn/bad-input", "the metadata BLOB") : null;
284
+ // Every byte form, not only the two a Buffer-shaped API thinks of. A BLOB is retrieved over the
285
+ // network, and the ordinary way to hold a fetched body is an ArrayBuffer -- so the form an
286
+ // operator most naturally arrives with was the one form this refused. `byteLength` is declared by
287
+ // an ArrayBuffer and by every view over one, so the ceiling below still bites before any copy.
288
+ var raw = (ArrayBuffer.isView(blob) || blob instanceof ArrayBuffer)
289
+ ? guard.bytes.source(blob, WebauthnError, "webauthn/bad-input", "the metadata BLOB") : null;
202
290
  // A string's `.length` counts UTF-16 code units, not the UTF-8 bytes the conversion produces, so
203
291
  // measuring it would let a string of multi-byte characters sit under the ceiling and then expand
204
292
  // several-fold past it during the copy -- the allocation the ceiling exists to prevent.
@@ -308,19 +396,46 @@ function _verifyMetadataBlob(blob, opts) {
308
396
  // -- and the path validation that follows checks the CHAIN, not the target's application-level
309
397
  // usage, so nothing else catches it. An absent keyUsage places no restriction, per that section.
310
398
  _assertLeafSigns(leaf);
311
- // The alg names a signature scheme; the leaf key must be of the family that scheme uses. Taking
312
- // the scheme from the token and the key from the chain without checking they agree is the JWS
313
- // algorithm-confusion class.
399
+ // The alg names a signature scheme; the leaf key must be permitted to perform it.
314
400
  var leafAlg = (leaf.subjectPublicKeyInfo.algorithm || {}).name;
315
- var leafFamily = leafAlg === "ecPublicKey" ? "EC" : (leafAlg === "rsaEncryption" ? "RSA" : null);
316
- if (leafFamily !== algRow.family) {
401
+ var leafSchemes = typeof leafAlg === "string" ? LEAF_SCHEMES_BY_SPKI_ALG[leafAlg] : undefined;
402
+ if (!leafSchemes || !leafSchemes[algRow.scheme]) {
317
403
  throw _err("webauthn/unsupported-algorithm", "the metadata BLOB alg " + header.alg + " does not match the x5c leaf key type " + JSON.stringify(leafAlg));
318
404
  }
405
+ // An id-RSASSA-PSS certificate MAY narrow the key further, to one hash (RFC 4055 sec. 1.2). The
406
+ // restriction is the certificate's, so it outranks the alg: a key issued for SHA-256 must not
407
+ // verify a PS512 signature just because the header asked for one.
408
+ if (leafAlg === "rsassaPss") {
409
+ var pinnedHash = signScheme.pssSpkiPinnedHash(leaf, _schemeE);
410
+ if (pinnedHash && pinnedHash !== algRow.hash) {
411
+ throw _err("webauthn/unsupported-algorithm", "the metadata BLOB alg " + header.alg + " uses " + algRow.hash + ", but the x5c leaf key is restricted to " + pinnedHash);
412
+ }
413
+ }
319
414
 
415
+ // An Edwards leaf key is validated on-curve and full-order BEFORE it is imported. The identity
416
+ // point and the other low-order points are accepted by the platform and verify a trivial
417
+ // signature over ANY message, so a leaf carrying one authenticates whatever payload it is shown.
418
+ // Chaining to the caller's FIDO root is not protection here: the point is malformed, not
419
+ // unissued, and the certificate that carries it can be perfectly well signed. This is the same
420
+ // gate every other Edwards key in the toolkit passes -- the attestation path, CMS, the path
421
+ // validator, sigstore, JOSE -- and a new verification route skipping it is how one key type comes
422
+ // to be checked everywhere except the newest door.
423
+ if (leafAlg === "Ed25519" || leafAlg === "Ed448") {
424
+ edwardsPoint.validateSpki(leaf.subjectPublicKeyInfo.bytes, leafAlg === "Ed25519" ? 6 : 7,
425
+ WebauthnError, "webauthn/bad-att-cert");
426
+ }
427
+ var params = _blobAlgParams(algRow, leafAlg);
320
428
  var signingInput = Buffer.from(segs[0] + "." + segs[1], "ascii");
321
- return webcrypto.webcrypto.subtle.importKey("spki", leaf.subjectPublicKeyInfo.bytes, algRow.imp, false, ["verify"])
322
- .then(function (key) { return webcrypto.webcrypto.subtle.verify(algRow.ver, key, sig, signingInput); },
323
- function (e) { throw _err("webauthn/unsupported-algorithm", "the metadata BLOB x5c leaf key could not be imported for " + header.alg, e); })
429
+ return webcrypto.webcrypto.subtle.importKey("spki", leaf.subjectPublicKeyInfo.bytes, params.imp, false, ["verify"])
430
+ .then(function (key) {
431
+ // The VERIFY is wrapped as well as the import. A rejection handler attached to the import
432
+ // alone leaves anything the verify itself rejects with -- a certificate whose RSASSA-PSS
433
+ // parameters demand a longer salt than the algorithm supplies imports cleanly and then fails
434
+ // inside OpenSSL -- to escape as a raw platform Error, out of a verb whose whole contract is
435
+ // that every failure is a typed webauthn/* verdict.
436
+ return webcrypto.webcrypto.subtle.verify(params.ver, key, sig, signingInput)
437
+ .catch(function (e) { throw _err("webauthn/verify-error", "the metadata BLOB signature could not be evaluated under its x5c leaf key", e); });
438
+ }, function (e) { throw _err("webauthn/unsupported-algorithm", "the metadata BLOB x5c leaf key could not be imported for " + header.alg, e); })
324
439
  .then(function (ok) {
325
440
  if (!ok) throw _err("webauthn/verify-failed", "the metadata BLOB signature does not verify under its x5c leaf key");
326
441
  return _chainToAnchor(chain, anchors, at);
@@ -336,8 +451,17 @@ function _verifyMetadataBlob(blob, opts) {
336
451
  // The x5c chain must validate to one of the caller's anchors. Every anchor is tried because an
337
452
  // operator may hold several across a rotation; the last path verdict is threaded as the cause so a
338
453
  // caller can see WHY it did not chain rather than only that it did not.
339
- function _chainToAnchor(chain, anchors, at, what) {
454
+ //
455
+ // `what` and `code` name the caller's subject and its own refusal code, so every chain in the
456
+ // namespace -- the BLOB's, an attestation's, an android-safetynet service chain -- reaches its
457
+ // anchors through THIS walk while keeping the verdict its own callers already handle. A second copy
458
+ // of the walk is not merely duplication: the anchor-stripping rule is subtle (a terminal
459
+ // certificate that IS the anchor is identified by subject name AND public key, which is what
460
+ // recognises a cross-signed root), and a copy that got it slightly differently would refuse a valid
461
+ // chain in one place and accept a self-validated one in another.
462
+ function _chainToAnchor(chain, anchors, at, what, code) {
340
463
  var subject = what || "metadata BLOB certificate chain";
464
+ var faultCode = code || "webauthn/metadata-untrusted";
341
465
  var ordered = chain.slice().reverse(); // path.validate takes anchor-adjacent first
342
466
  var lastFault = null;
343
467
  return anchors.reduce(function (p, anchor) {
@@ -375,7 +499,7 @@ function _chainToAnchor(chain, anchors, at, what) {
375
499
  });
376
500
  }, Promise.resolve(false)).then(function (trusted) {
377
501
  if (!trusted) {
378
- throw _err("webauthn/metadata-untrusted", "the " + subject + " does not validate to any of the roots it must reach", lastFault);
502
+ throw _err(faultCode, "the " + subject + " does not validate to any of the roots it must reach", lastFault);
379
503
  }
380
504
  });
381
505
  }
@@ -538,8 +662,15 @@ function _parsePayload(seg, at, opts) {
538
662
  });
539
663
  return out;
540
664
  });
665
+ // The rollback rule leaves a trace, exactly as the freshness rule does either side of it. It runs
666
+ // only when a caller supplies the sequence number it already holds, so a result that does not say
667
+ // whether it ran cannot be told apart from one where the check was skipped -- and the whole point
668
+ // of the rule is that a caller can show its catalogue never went backwards. `previousNo` is the
669
+ // baseline it was compared against, so the claim is auditable rather than merely asserted.
541
670
  var result = { no: payload.no, legalHeader: payload.legalHeader, nextUpdate: payload.nextUpdate,
542
671
  stale: stale, allowStale: opts.allowStale === true,
672
+ rollbackChecked: opts.previousNo !== undefined,
673
+ previousNo: opts.previousNo === undefined ? null : opts.previousNo,
543
674
  entries: entries, byAaguid: byAaguid, byKeyIdentifier: byKeyIdentifier,
544
675
  statusPolicy: opts.statusPolicy || "any", rejectUnknownStatus: opts.rejectUnknownStatus === true };
545
676
  // Frozen FIRST, then recorded as verified: the mark means "this exact catalogue passed the
@@ -547,6 +678,9 @@ function _parsePayload(seg, at, opts) {
547
678
  // be edited afterwards. This is the only place a catalogue can have been through those gates.
548
679
  _deepFreeze(result, 0);
549
680
  _verifiedResults.add(result);
681
+ // Recorded AFTER the freeze, alongside the provenance mark and for the same reason: an entry may
682
+ // only be judged against the catalogue it was read out of.
683
+ entries.forEach(function (e) { _entryOrigin.set(e, result); });
550
684
  return result;
551
685
  }
552
686
 
@@ -570,12 +704,55 @@ function metadataFor(metadata, identifier) {
570
704
  return null;
571
705
  }
572
706
 
707
+ // The options metadataAnchors recognises. Null-prototype for the same reason every other
708
+ // caller-keyed table in this namespace is: a supplied key must not resolve to an inherited member.
709
+ var _ANCHOR_OPTS = Object.assign(Object.create(null), { metadata: 1, time: 1, certificate: 1 });
710
+
573
711
  // The attestation root certificates an entry's authenticator chains to, decoded on demand. Decoding
574
712
  // is deliberately per entry rather than for the whole BLOB: a handful of the certificates in the
575
713
  // live metadata do not parse under a strict decoder, and decoding everything up front would let one
576
714
  // vendor's malformed root refuse the entire BLOB for every authenticator in it.
577
- function metadataAnchors(entry) {
715
+ function metadataAnchors(entry, opts) {
578
716
  if (!entry || typeof entry !== "object") throw _err("webauthn/bad-input", "metadataAnchors expects a metadata entry");
717
+ opts = opts || {};
718
+ if (typeof opts !== "object" || Array.isArray(opts)) throw _err("webauthn/bad-input", "metadataAnchors opts must be an object");
719
+ guard.identifier.assertKnownKeys(opts, _ANCHOR_OPTS, _err, "webauthn/bad-input", "metadataAnchors opts has an unknown key ");
720
+ // ONE read of the caller's object: the status gate below decides on these values, and a value
721
+ // read twice is a value that can differ between the check and the use.
722
+ opts = Object.assign({}, opts);
723
+ if (opts.time !== undefined) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
724
+ // Provenance, the same rule metadataFor applies: only a catalogue this module actually verified
725
+ // may govern which anchors are handed out. Accepting any object carrying a `statusPolicy` would
726
+ // let a caller supply a predicate that denies nothing and defeat the gate on the next line --
727
+ // and the gate's whole purpose is that a disqualified model registers no anchors.
728
+ if (opts.metadata !== undefined) {
729
+ if (!isVerifiedResult(opts.metadata)) {
730
+ throw _err("webauthn/bad-input", "metadataAnchors opts.metadata expects a verifyMetadataBlob result -- an object that merely resembles one has not been through the signature and chain checks");
731
+ }
732
+ // ...and the entry must be one of THAT catalogue's. Verified provenance says the supplied
733
+ // catalogue is real; it does not say this entry came from it, and a process holding two would
734
+ // otherwise judge one catalogue's status reports under the other's policy and freshness.
735
+ if (!_isEntryOf(entry, opts.metadata)) {
736
+ throw _err("webauthn/bad-input", "metadataAnchors was given an entry from a different catalogue than opts.metadata, so the status reports would be judged under a policy and freshness that are not theirs");
737
+ }
738
+ }
739
+ // A model whose status reports deny trust has no anchors to offer. This is the ROUTE an operator
740
+ // follows to anchor an attestation themselves -- metadataFor, then these anchors, then
741
+ // pki.path.validate -- and nothing along it consulted the status reports, so a REVOKED
742
+ // authenticator's registered roots were handed over and the path validated against them. The
743
+ // catalogue's whole purpose is to say which models are still trusted; handing out the roots of
744
+ // one it has disqualified is answering a different question than the caller asked.
745
+ //
746
+ // Judged with the same inputs the attestation path uses, so the two readings cannot diverge: the
747
+ // caller's own statusPolicy when the verified result is supplied, the instant being judged, and
748
+ // the attestation certificate actually presented (so a report naming a single certificate is
749
+ // judged against that one rather than denying every device the entry covers). With none of them,
750
+ // the strictest reading applies -- any disqualifying report, judged now.
751
+ var at = opts.time === undefined ? new Date() : opts.time;
752
+ if (opts.metadata !== undefined) assertFresh(opts.metadata, at, "metadataAnchors");
753
+ if (statusDenied(entry, opts.metadata, opts.certificate, at)) {
754
+ throw _err("webauthn/metadata-status", "the metadata entry for this authenticator carries a disqualifying status report, so it registers no anchors to trust");
755
+ }
579
756
  var st = entry.metadataStatement;
580
757
  var list = st && Array.isArray(st.attestationRootCertificates) ? st.attestationRootCertificates : [];
581
758
  if (list.length > C.MDS_MAX_ANCHORS_PER_ENTRY) {
@@ -750,4 +927,9 @@ module.exports = {
750
927
  ZERO_AAGUID: ZERO_AAGUID,
751
928
  certKeyIdentifier: certKeyIdentifier,
752
929
  DISQUALIFYING: DISQUALIFYING,
930
+ // @internal -- the derived JWS algorithm table, exposed so a conformance vector can assert the
931
+ // derivation is TOTAL over pki.jose's registry: every algorithm the toolkit verifies has a row
932
+ // here, carrying the WebCrypto parameters its key type needs, or saying that the certificate
933
+ // supplies them where the algorithm alone does not fix a curve.
934
+ BLOB_ALGS: BLOB_ALGS,
753
935
  };