@blamejs/pki 0.5.2 → 0.5.3

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