@blamejs/pki 0.4.10 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,34 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## v0.4.10 — 2026-08-09
7
+ ## v0.4.11 — 2026-08-09
8
+
9
+ A WebAuthn attestation can now be bound to the roots the authenticator's own model actually registered, by reading a FIDO Metadata Service BLOB that is verified and chained to a root you supply before any of its contents are parsed.
10
+
11
+ ### Added
12
+
13
+ - pki.webauthn.verifyMetadataBlob reads a FIDO Metadata Service (MDS v3) BLOB and returns its entries indexed by aaguid. The BLOB is a JWS: its signature is checked under the certificate in its own header, that chain is validated to one of the roots the caller pins, and only then is the payload parsed -- the ordering is the point, because a reader that parses first hands an attacker every structure behind the signature.
14
+ - The catalogue's freshness is enforced rather than reported, and enforced again wherever it is used. A BLOB whose sequence number does not exceed the one the caller already holds is refused as a rollback, and requireRollbackCheck makes supplying that number mandatory so the check cannot be skipped by forgetting the option. A BLOB past its nextUpdate is refused as stale -- and because a verified result is an ordinary object a relying party may cache, its expiry is re-checked each time it is passed to verify, so a catalogue fetched while current cannot keep authorizing an authenticator whose status reports have since revoked it. Both checks fail closed, and the caller's own allowStale decision rides on the result rather than needing to be repeated.
15
+ - Passing the verified result to pki.webauthn.verify as opts.metadata binds the attestation to its own model: the authenticator's registered attestation roots are resolved from its identifier, and its trust path must fully validate to one of them -- signature chaining, validity, and constraints, the same path validation any certificate chain gets, not a name comparison against the top of the path. An authenticator whose model the catalogue does not list, whose entry registers no attestation root, or whose status reports disqualify it, is refused. Which reports disqualify is selectable -- any report ever filed, only the most recent one so that a later remediation clears an earlier revocation, or a predicate of the caller's own -- and an unrecognised status is ignored by default, as the specification requires, or treated as disqualifying on request.
16
+ - Authenticators that carry no aaguid are covered too. A U2F authenticator declares no model identity, and the catalogue keys it by the key identifiers of its attestation certificates instead; both key spaces are indexed and looked up, so a U2F registration binds to its registered roots rather than being refused as unlisted. The identifier is read from the entry and from its metadata statement, since live entries populate both, and is computed as RFC 5280 sec. 4.2.1.2 method 1 defines it. A compound attestation is covered as well: its elements carry independent trust paths, and every one of them must reach a registered root.
17
+ - Which identifier is allowed to select an entry depends on what the attestation signature covers. The fido-u2f signature is computed over named fields and does not include the aaguid, so for that format the certificate decides and the declared aaguid is ignored -- otherwise setting it to a listed model that shares the vendor's registered root would resolve to that model's entry and skip the real one's status reports. For relying parties: res.aaguid reports what the authenticator presented and is not signature-bound for that format; res.metadata.aaguid names the entry that actually matched.
18
+ - Only a catalogue this library verified can decide anything. A metadata result is recognised by provenance rather than by its shape, so an object restored from a cache -- which has been through none of the signature and chain checks, and whose contents an attacker able to write that cache would choose -- is refused rather than accepted as a catalogue. Re-verify the BLOB instead, which the freshness rule asks for anyway. The verified result is also frozen, so the catalogue that decides a later verification is the one the signature covered rather than a copy something has since edited.
19
+ - A stored attestation is anchored at the instant its own format judged it. An android-safetynet response carries its signing time and its service chain has usually expired since, so the metadata anchor check reuses that instant rather than the current clock -- otherwise it would refuse the very registration the format verifier had just accepted. An explicit opts.time still takes precedence.
20
+ - Status reports are read against the instant being judged: a report dated in the future has not taken effect, so it cannot displace a revocation that is in force now, and a deliberately historical verification does not see reports filed after the time it asks about.
21
+ - A status report that names a single certificate is judged against the certificate actually presented. A whole batch of authenticators is commonly listed under one entry, so a key-compromise report naming one attestation certificate denies that one rather than every device the entry covers; a report that names nothing, or names something that does not decode, still applies to the entry as a whole. Trust anchors are recognised by name and public key rather than by self-issuedness, so a chain terminating in a cross-signed form of a root you supplied still anchors to it.
22
+ - The BLOB's own signing certificate must be permitted to sign. A certificate that carries a key-usage extension omitting digitalSignature is refused before its key is used to check the BLOB signature, so a certificate restricted to some other purpose cannot confer metadata-signing authority just because it chains to the root you supplied.
23
+ - pki.webauthn.metadataFor looks an entry up against a verified result only, never raw bytes, so a lookup cannot be answered out of a catalogue nobody verified. It takes either identifier -- an aaguid or an attestation-certificate key identifier -- dispatching on the form, which are disjoint by shape. pki.webauthn.metadataAnchors decodes an entry's registered attestation roots, per entry rather than for the whole catalogue: a handful of certificates in the live metadata do not parse under a strict decoder, and decoding everything up front would let one vendor's malformed root refuse the entire catalogue for every other authenticator in it.
24
+
25
+ ### Changed
26
+
27
+ - pki.webauthn.verify now rejects an unrecognised option key instead of ignoring it, and validates opts.time at the boundary. Every option it takes either gates the verdict or supplies the trust material a gate needs, so a misspelled key was not harmless: asking for metadata enforcement and mistyping the key left the gate switched off and returned a pass the caller believed had been checked against the catalogue. An invalid time is now a configuration fault reported as such, rather than surfacing later as an authenticator trust failure.
28
+ - Configuration objects across certificate, CRL, attribute-certificate, CSR, and CMP issuance now reject an unrecognised option key through one shared check instead of a dozen separate ones. Two cases that a hand-written check gets wrong are fixed everywhere at once: a key that every JavaScript object inherits, such as constructor or toString, is no longer accepted as a recognised option name, and an option object built by parsing JSON that carries its own __proto__ key is now inspected rather than skipped. The wording of every rejection is unchanged. This matters because the failure is silent in the quietest possible way -- a misspelled option key leaves the default in force, so a caller who asked for a stricter check gets the looser behaviour and no error anywhere.
29
+
30
+ ### Fixed
31
+
32
+ - An invalid opts.time passed to pki.webauthn.verify for an android-safetynet attestation raised an untyped internal error instead of the typed webauthn/bad-input verdict. Every failure from the verifier is a typed error again, so a caller catching pki.errors.PkiError no longer has a hole on that path.
33
+
34
+ ## v0.4.10 — 2026-08-08
8
35
 
9
36
  A TPM attestation now reports the credential key's own object attributes and access policy, so a relying party can require the properties it cares about -- a key bound to one TPM, generated by that TPM, not duplicable -- instead of taking the attestation on trust.
10
37
 
package/README.md CHANGED
@@ -244,7 +244,7 @@ is callable today; nothing below is a stub.
244
244
  | `pki.hpke` | Hybrid Public Key Encryption (RFC 9180) — the encrypt-to-a-public-key primitive behind TLS ECH, MLS, and OHTTP. `setupS`/`setupR` establish a sender/recipient context (KEM encapsulation + HKDF key schedule); the context's `seal`/`open` AEAD-encrypt with a sequence-counter nonce and `export` derives further secrets; `seal`/`open` are single-shot wrappers. DHKEM (P-256, P-521, X25519, X448) × HKDF-SHA256/SHA512 × AES-GCM/ChaCha20Poly1305/export-only × all four modes, proven against the RFC 9180 Appendix A vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered but Appendix A ships no vector for them, so they fail closed until an authoritative KAT exists. Pure composition over `node:crypto`; ML-KEM / X-Wing are a registry data-row extension pending stable drafts — `suites`, `setupS`, `setupR`, `seal`, `open` |
245
245
  | `pki.sigstore` | Offline verifier for a Sigstore bundle — the exact artifact `npm publish --provenance` produces and the registry serves. `verifyBundle` composes five fail-closed legs against caller-pinned trust (Fulcio CA roots + Rekor log keys, never trusted from the bundle): the DSSE signature over its PAE preimage under the Fulcio leaf key; the ephemeral Fulcio certificate chain, validated as of the Rekor log time; the RFC 9162 inclusion proof folded to a Rekor-signed tree root; the log entry bound to this exact signature; and the in-toto SLSA subject digest the caller confirms against the published artifact. Zero runtime deps — reuses the X.509 parser, RFC 5280 path validator, and Merkle verifier; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON reader. `pae`, `parseBundle`, `verifyBundle` |
246
246
  | `pki.inspect` | Human-readable inspection — the pure-JS equivalent of `openssl x509/crl/req/cms -text`. `certificate(pem \| der \| parsed)` renders a familiar OpenSSL-style report: version, serial, signature algorithm, issuer/subject distinguished names, validity, public-key details (curve or modulus size + the raw point/modulus), every decoded extension with its critical flag, and the signature. `crl` / `csr` / `cms` render the non-certificate formats the same way — a CRL like `openssl crl -text` (issuer, Last/Next Update, CRL extensions, each revoked entry with its serial, revocation date, and named reason), a CSR like `openssl req -text` (subject, key, requested extensions and attributes), and a CMS message like `openssl cms -cmsout -print` (a SignedData's content type, digest algorithms, embedded certificates, and each SignerInfo with its signer identifier, algorithms, attributes, and signature; a non-SignedData ContentInfo gets a stable summary) — and `any(input)` detects the format and routes to the right report. Built over the strict parsers and the two-way OID registry, reusing one set of field renderers, so it names extension/algorithm OIDs an OpenSSL build shows only as raw bytes and never drifts. No OpenSSL dependency; the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version; a malformed part falls back to a hex dump rather than throwing — `certificate`, `crl`, `csr`, `cms`, `any` |
247
- | `pki.webauthn` | WebAuthn / passkey attestation verification — offline trust evaluation of a W3C WebAuthn (Level 3) attestation. `parseAttestationObject(bytes)` decodes the CBOR attestation object + authenticatorData + COSE credential key over the strict `pki.cbor` codec; `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for **packed / tpm / android-key / apple / fido-u2f / none** — the x5c leaf key, the apple nonce, the tpm `certInfo` Name/`extraData` over the `pubArea`, the android `KeyDescription`, the fido-u2f `verificationData` — binding the credential public key to each attestation (via the signed authenticatorData for packed/fido-u2f, or a cert/`pubArea`-key equality check for android-key/apple/tpm) and enforcing each leaf's certificate requirements. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256, EdDSA (Ed25519), and the RFC 9864 fully-specified identifiers **ESP256/384/512, Ed25519, and Ed448** — validating the public-key point on its curve, rejecting the compressed EC point form, and enforcing a minimally-encoded DER ECDSA signature. A verifier, not a ceremony client; fail-closed with typed `webauthn/*` errors. Chaining the returned trust path to a pinned root (and aaguid→root via FIDO MDS) is the caller's step through `pki.path.validate` — `parseAttestationObject`, `verify` |
247
+ | `pki.webauthn` | WebAuthn / passkey attestation verification — offline trust evaluation of a W3C WebAuthn (Level 3) attestation. `parseAttestationObject(bytes)` decodes the CBOR attestation object + authenticatorData + COSE credential key over the strict `pki.cbor` codec; `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for **packed / tpm / android-key / apple / fido-u2f / none** — the x5c leaf key, the apple nonce, the tpm `certInfo` Name/`extraData` over the `pubArea`, the android `KeyDescription`, the fido-u2f `verificationData` — binding the credential public key to each attestation (via the signed authenticatorData for packed/fido-u2f, or a cert/`pubArea`-key equality check for android-key/apple/tpm) and enforcing each leaf's certificate requirements. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256, EdDSA (Ed25519), and the RFC 9864 fully-specified identifiers **ESP256/384/512, Ed25519, and Ed448** — validating the public-key point on its curve, rejecting the compressed EC point form, and enforcing a minimally-encoded DER ECDSA signature. A verifier, not a ceremony client; fail-closed with typed `webauthn/*` errors. `verifyMetadataBlob(blob, opts)` reads a **FIDO Metadata Service (MDS v3)** BLOB — the signed catalogue of registered authenticator models verifying its JWS and chaining its signer to an operator-supplied FIDO root **before** the payload is parsed, with sequence-number rollback and `nextUpdate` freshness checks; passing the result as `opts.metadata` to `verify` resolves the authenticator's registered attestation roots from its identifier and requires the trust path to fully validate to one of them, refusing an unlisted or revoked model. Both of the catalogue's key spaces are covered an aaguid, and the attestation-certificate key identifiers a U2F authenticator is listed under instead. No FIDO root is bundled and there is no trust-on-first-use; retrieving the BLOB is out of scope — `parseAttestationObject`, `verify`, `verifyMetadataBlob`, `metadataFor`, `metadataAnchors` |
248
248
  | `pki.lint` | Certificate linting — the zlint / pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded, advisory findings — each with a stable id, a severity (`fatal` > `error` > `warn` > `notice`), a source, a spec-clause citation, and a message — against the RFC 5280 profile plus a representative CA/Browser Forum TLS BR subset (serial sign/size, validity ordering + the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality — basicConstraints/nameConstraints/policyConstraints/inhibitAnyPolicy must be critical and keyUsage should be, nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI/AKI presence including the end-entity subjectKeyIdentifier, SAN required + CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys). Unlike every other entry the DATA path never throws: hostile bytes return a `fatal` `lint/unparseable` finding (with the strict parser's code) so a whole directory lints without a try/catch; only config-time misuse throws a typed `LintError`. `certificate`, `rules`, `profiles` |
249
249
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
250
250
  | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `CmpError` / `PathError` / `CtError` / `JoseError` / `AcmeError` / `WebauthnError` / `LintError`, each carrying a stable `code` in `domain/reason` form |
@@ -136,7 +136,7 @@ function _objectDigestInfoContent(odi) {
136
136
  // objectDigestInfo [2] IMPLICIT ObjectDigestInfo } -- exactly one form (the profile binds a real holder).
137
137
  function _encodeHolder(holder) {
138
138
  if (!holder || typeof holder !== "object" || Buffer.isBuffer(holder)) throw _err("attrcert/bad-input", "holder must be an object with exactly one form");
139
- Object.keys(holder).forEach(function (k) { if (!KNOWN_HOLDER_KEYS[k]) throw _err("attrcert/bad-input", "unknown holder form " + JSON.stringify(k)); });
139
+ guard.identifier.assertKnownKeys(holder, KNOWN_HOLDER_KEYS, _err, "attrcert/bad-input", "unknown holder form ");
140
140
  var forms = Object.keys(holder).filter(function (k) { return holder[k] != null; });
141
141
  if (forms.length !== 1) throw _err("attrcert/bad-input", "holder must carry exactly one form (entityName, baseCertificateID, fromCertificate, or objectDigestInfo), got " + forms.length);
142
142
  if (holder.entityName != null) {
@@ -348,8 +348,10 @@ function _buildAttributes(attrSpec) {
348
348
  return b.sequence(attrs);
349
349
  }
350
350
  if (!attrSpec || typeof attrSpec !== "object") throw _err("attrcert/bad-input", "attributes must be an object or an array of pre-encoded Attribute DER");
351
+ guard.identifier.assertKnownKeys(attrSpec, ATTR_VALUE_ENCODER, _err, "attrcert/bad-input", function (k) {
352
+ return "unknown attribute " + JSON.stringify(k) + "; pass a pre-encoded Attribute DER via the array form for a custom attribute";
353
+ });
351
354
  Object.keys(attrSpec).forEach(function (k) {
352
- if (!ATTR_VALUE_ENCODER[k]) throw _err("attrcert/bad-input", "unknown attribute " + JSON.stringify(k) + "; pass a pre-encoded Attribute DER via the array form for a custom attribute");
353
355
  add(O(ATTR_OID_NAME[k]), ATTR_VALUE_ENCODER[k](attrSpec[k]));
354
356
  });
355
357
  if (!attrs.length) throw _err("attrcert/bad-attributes", "attributes must carry at least one Attribute (RFC 5755 sec. 4.2.7)");
@@ -389,8 +391,8 @@ function _buildExtensions(extSpec, aaSpki) {
389
391
  return b.sequence(exts);
390
392
  }
391
393
  if (typeof extSpec !== "object") throw _err("attrcert/bad-input", "extensions must be an object or an array of pre-encoded Extension DER");
392
- Object.keys(extSpec).forEach(function (k) {
393
- if (!EXT_META[k]) throw _err("attrcert/bad-input", "unknown extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension");
394
+ guard.identifier.assertKnownKeys(extSpec, EXT_META, _err, "attrcert/bad-input", function (k) {
395
+ return "unknown extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension";
394
396
  });
395
397
  var out = [], seen = {};
396
398
  Object.keys(extSpec).forEach(function (k) {
@@ -450,7 +452,7 @@ function sign(spec, issuer, opts) {
450
452
  function _sign(spec, issuer, opts) {
451
453
  opts = opts || {};
452
454
  if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("attrcert/bad-input", "the attribute-certificate spec must be an object");
453
- Object.keys(spec).forEach(function (k) { if (!KNOWN_SPEC_KEYS[k]) throw _err("attrcert/bad-input", "unknown spec field " + JSON.stringify(k)); });
455
+ guard.identifier.assertKnownKeys(spec, KNOWN_SPEC_KEYS, _err, "attrcert/bad-input", "unknown spec field ");
454
456
  issuer = issuer || {};
455
457
  if (issuer.key == null) throw _err("attrcert/bad-input", "a signing key (issuer.key, the AA's PKCS#8 private key) is required");
456
458
 
package/lib/cmp-build.js CHANGED
@@ -75,6 +75,7 @@ var BODY_TAG = {
75
75
  var CRMF_BODY = { ir: 1, cr: 1, kur: 1 }; // arms whose content is a CertReqMessages via pki.crmf.build
76
76
  var CERT_REP_ARM = { ip: 1, cp: 1, kup: 1, ccp: 1 }; // arms carrying a CertRepMessage
77
77
  var KNOWN_OPTS_KEYS = { key: 1, cert: 1, mac: 1, extraCerts: 1, pem: 1, pss: 1, digestAlgorithm: 1 };
78
+ var KNOWN_MESSAGE_KEYS = { header: 1, body: 1 };
78
79
  var KNOWN_MAC_KEYS = { secret: 1, salt: 1, iterationCount: 1, prf: 1, keyLength: 1, algorithm: 1 };
79
80
 
80
81
  var PBMAC1_DEFAULT_ITER = 100000;
@@ -161,7 +162,7 @@ function _algIdNoParams(name) { return b.sequence([b.oid(O(name))]); }
161
162
  // the exact headerTLV, built ONCE and reused in both the envelope and the ProtectedPart (RFC 9810 5.1.3).
162
163
  function _encodeHeader(headerSpec, protectionAlgDer, pvno) {
163
164
  if (!headerSpec || typeof headerSpec !== "object" || Buffer.isBuffer(headerSpec)) throw _err("cmp/bad-input", "message.header must be an object");
164
- Object.keys(headerSpec).forEach(function (k) { if (!KNOWN_HEADER_KEYS[k]) throw _err("cmp/bad-input", "unknown header field " + JSON.stringify(k)); });
165
+ guard.identifier.assertKnownKeys(headerSpec, KNOWN_HEADER_KEYS, _err, "cmp/bad-input", "unknown header field ");
165
166
  if (headerSpec.sender == null) throw _err("cmp/bad-input", "message.header.sender is required (GeneralName)");
166
167
  if (headerSpec.recipient == null) throw _err("cmp/bad-input", "message.header.recipient is required (GeneralName)");
167
168
 
@@ -510,7 +511,7 @@ function _resolveProtection(opts) {
510
511
  // PBMAC1
511
512
  var m = opts.mac;
512
513
  if (!m || typeof m !== "object" || Buffer.isBuffer(m)) throw _err("cmp/bad-input", "opts.mac must be an object { secret, salt?, iterationCount?, prf?, keyLength? }");
513
- Object.keys(m).forEach(function (k) { if (!KNOWN_MAC_KEYS[k]) throw _err("cmp/bad-input", "unknown opts.mac field " + JSON.stringify(k)); });
514
+ guard.identifier.assertKnownKeys(m, KNOWN_MAC_KEYS, _err, "cmp/bad-input", "unknown opts.mac field ");
514
515
  if (m.algorithm != null && m.algorithm !== "pbmac1") throw _err("cmp/unsupported-algorithm", "opts.mac.algorithm " + JSON.stringify(m.algorithm) + " is not supported (v1 ships pbmac1; passwordBasedMac is deferred)");
515
516
  var secret = m.secret;
516
517
  if (typeof secret !== "string" || !secret) {
@@ -560,8 +561,10 @@ function build(message, opts) {
560
561
  function _build(message, opts) {
561
562
  opts = opts || {};
562
563
  if (!message || typeof message !== "object" || Buffer.isBuffer(message)) throw _err("cmp/bad-input", "the PKIMessage spec must be an object { header, body }");
563
- Object.keys(message).forEach(function (k) { if (k !== "header" && k !== "body") throw _err("cmp/bad-input", "unknown message field " + JSON.stringify(k) + " (a message carries only header + body)"); });
564
- Object.keys(opts).forEach(function (k) { if (!KNOWN_OPTS_KEYS[k]) throw _err("cmp/bad-input", "unknown opts field " + JSON.stringify(k)); });
564
+ guard.identifier.assertKnownKeys(message, KNOWN_MESSAGE_KEYS, _err, "cmp/bad-input", function (k) {
565
+ return "unknown message field " + JSON.stringify(k) + " (a message carries only header + body)";
566
+ });
567
+ guard.identifier.assertKnownKeys(opts, KNOWN_OPTS_KEYS, _err, "cmp/bad-input", "unknown opts field ");
565
568
  if (message.header == null) throw _err("cmp/bad-input", "message.header is required");
566
569
  if (message.body == null) throw _err("cmp/bad-input", "message.body is required");
567
570
 
@@ -734,7 +737,7 @@ function transfer(url, message, opts) {
734
737
 
735
738
  function _transfer(url, message, opts) {
736
739
  opts = opts || {};
737
- Object.keys(opts).forEach(function (k) { if (!KNOWN_TRANSFER_OPTS[k]) throw _err("cmp/bad-input", "unknown opts field " + JSON.stringify(k)); });
740
+ guard.identifier.assertKnownKeys(opts, KNOWN_TRANSFER_OPTS, _err, "cmp/bad-input", "unknown opts field ");
738
741
  var der = _cmpMessageDer(message); // config gate: before any transport call
739
742
  var parsedUrl;
740
743
  try { parsedUrl = new URL(String(url)); } // parse only -- NO client scheme gate (the transport owns socket security)
@@ -793,7 +796,9 @@ function wellKnownUrl(base, opts) {
793
796
  opts = opts || {};
794
797
  // Reject an unknown option key (a typo like { lable } / { operaton }), as build/transfer do -- a silently
795
798
  // ignored option would build the DEFAULT endpoint and send the request to a different CA/profile.
796
- Object.keys(opts).forEach(function (k) { if (!KNOWN_WELLKNOWN_OPTS[k]) throw _err("cmp/bad-input", "unknown wellKnownUrl option " + JSON.stringify(k) + " (expected label / operation)"); });
799
+ guard.identifier.assertKnownKeys(opts, KNOWN_WELLKNOWN_OPTS, _err, "cmp/bad-input", function (k) {
800
+ return "unknown wellKnownUrl option " + JSON.stringify(k) + " (expected label / operation)";
801
+ });
797
802
  var u;
798
803
  try { u = new URL(String(base)); }
799
804
  catch (e) { throw _err("cmp/bad-url", "the CMP base URL did not parse: " + String(base), e); }
@@ -335,7 +335,7 @@ function _certConfHash(certDer) {
335
335
  function session(opts) {
336
336
  if (opts == null) opts = {};
337
337
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cmp/bad-input", "opts must be an object");
338
- Object.keys(opts).forEach(function (k) { if (!KNOWN_SESSION_OPTS[k]) throw _err("cmp/bad-input", "unknown session opts field " + JSON.stringify(k)); });
338
+ guard.identifier.assertKnownKeys(opts, KNOWN_SESSION_OPTS, _err, "cmp/bad-input", "unknown session opts field ");
339
339
  // Work on a SHALLOW COPY so a later normalization (e.g. an empty MAC trustAnchors list) never mutates the
340
340
  // caller's object -- which may be frozen or reused across sessions -- and normalizing a frozen input does not
341
341
  // throw a raw TypeError. Only top-level fields are reassigned; nested values are read, never mutated.
package/lib/cmp-verify.js CHANGED
@@ -616,7 +616,7 @@ function verify(message, opts) {
616
616
  async function _verify(message, opts) {
617
617
  if (opts == null) opts = {}; // ONLY null / undefined -> the default empty opts (a falsy false/0/"" is a bad config, not a default)
618
618
  if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cmp/bad-input", "opts must be an object");
619
- Object.keys(opts).forEach(function (k) { if (!KNOWN_VERIFY_OPTS[k]) throw _err("cmp/bad-input", "unknown opts field " + JSON.stringify(k)); });
619
+ guard.identifier.assertKnownKeys(opts, KNOWN_VERIFY_OPTS, _err, "cmp/bad-input", "unknown opts field ");
620
620
  // The opt-in echo values are byte buffers: a non-buffer (e.g. a string from JSON config) is a DEPLOYMENT
621
621
  // error that throws cmp/bad-input, never a routine transaction/nonce mismatch verdict that would misreport
622
622
  // a caller's typing mistake as a peer authentication failure.
package/lib/constants.js CHANGED
@@ -297,6 +297,29 @@ var LIMITS = {
297
297
  // terminates but exhausts memory. This matches PATH_MAX_CERTS: a chain longer than the path
298
298
  // validator will ever accept has nothing to offer, so refusing it at the decoder is free.
299
299
  TLS_CERT_MAX_ENTRIES: 100,
300
+ // A FIDO Metadata Service BLOB is a JWS whose payload carries every registered authenticator's
301
+ // metadata; the live one runs about 10 MB across ~500 entries. The byte cap bounds the decode
302
+ // before the payload is materialized, and the entry and anchor caps bound the per-entry work
303
+ // (each anchor is a DER certificate parse), since a byte ceiling alone does not bound how many
304
+ // items are declared inside it (CWE-770).
305
+ MDS_BLOB_MAX_BYTES: BYTES.mib(32),
306
+ // The BLOB's JWS protected header is read BEFORE the signature is checked, so it carries its own,
307
+ // far smaller ceiling: a header is a few hundred bytes plus the x5c chain, and the envelope cap
308
+ // would otherwise let unauthenticated input buy heap ahead of every authentication step.
309
+ MDS_BLOB_HEADER_MAX_BYTES: BYTES.kib(256),
310
+ // The JWS signature, likewise read before anything is authenticated. Every algorithm this reader
311
+ // supports has a tightly bounded signature: 64/96/132 bytes for ECDSA P-256/384/521, and the
312
+ // modulus size for RSA. 2 KiB covers an RSA-16384 key with room to spare, and refuses a segment
313
+ // whose only purpose is to make the verifier allocate.
314
+ MDS_BLOB_SIG_MAX_BYTES: BYTES.kib(2),
315
+ MDS_MAX_ENTRIES: 4096,
316
+ // A model's certification history: bounded like the other repeated per-entry structures, because
317
+ // the status gate walks the whole array on every verification.
318
+ MDS_MAX_STATUS_REPORTS_PER_ENTRY: 256,
319
+ MDS_MAX_ANCHORS_PER_ENTRY: 16,
320
+ // A U2F authenticator carries no AAGUID, so its entry is keyed by the key identifiers of its
321
+ // attestation certificates instead -- a batch-attested product line legitimately registers dozens.
322
+ MDS_MAX_KEY_IDS_PER_ENTRY: 64,
300
323
  // The most nested statements a WebAuthn compound attestation (sec. 8.9) may carry. The syntax
301
324
  // says "2*" -- unbounded -- so this is a resource bound this toolkit chooses, NOT a spec MUST.
302
325
  // The CBOR parse caps bound the decode; they do not bound the crypto, and each element costs a
package/lib/crl-sign.js CHANGED
@@ -160,7 +160,9 @@ function _akiKeyId(val, ctx) {
160
160
  // scope boolean TRUE; onlyContainsAttributeCerts MUST be FALSE; DEFAULT-FALSE booleans omitted.
161
161
  function _idpValue(idp) {
162
162
  if (!idp || typeof idp !== "object" || Array.isArray(idp) || Buffer.isBuffer(idp)) throw _err("crl/bad-idp", "issuingDistributionPoint must be an object");
163
- Object.keys(idp).forEach(function (k) { if (!KNOWN_IDP_KEYS[k]) throw _err("crl/bad-idp", "unknown issuingDistributionPoint field " + JSON.stringify(k) + " (pass a pre-encoded Extension DER via the extensions array for an exotic field like onlySomeReasons)"); });
163
+ guard.identifier.assertKnownKeys(idp, KNOWN_IDP_KEYS, _err, "crl/bad-idp", function (k) {
164
+ return "unknown issuingDistributionPoint field " + JSON.stringify(k) + " (pass a pre-encoded Extension DER via the extensions array for an exotic field like onlySomeReasons)";
165
+ });
164
166
  if (idp.onlyContainsAttributeCerts === true) throw _err("crl/bad-idp", "onlyContainsAttributeCerts=TRUE is not permitted for a conforming CRL issuer (RFC 5280 sec. 5.2.5)");
165
167
  var children = [];
166
168
  if (idp.fullName != null) {
@@ -273,7 +275,9 @@ function _buildCrlExtensions(spec, ctx) {
273
275
  return { exts: out, isDelta: isDelta };
274
276
  }
275
277
  if (typeof ext !== "object") throw _err("crl/bad-input", "extensions must be an object or an array of pre-encoded Extension DER");
276
- Object.keys(ext).forEach(function (k) { if (!KNOWN_CRL_EXT_KEYS[k]) throw _err("crl/bad-input", "unknown CRL extension " + JSON.stringify(k) + " in the extensions spec; pass a pre-encoded Extension DER via the array form"); });
278
+ guard.identifier.assertKnownKeys(ext, KNOWN_CRL_EXT_KEYS, _err, "crl/bad-input", function (k) {
279
+ return "unknown CRL extension " + JSON.stringify(k) + " in the extensions spec; pass a pre-encoded Extension DER via the array form";
280
+ });
277
281
  isDelta = ext.deltaCRLIndicator != null;
278
282
  if (ext.authorityKeyIdentifier != null) push("authorityKeyIdentifier", false, _extAki(_akiKeyId(ext.authorityKeyIdentifier, ctx)));
279
283
  if (ext.issuingDistributionPoint != null) push("issuingDistributionPoint", true, _idpValue(ext.issuingDistributionPoint));
package/lib/crmf-sign.js CHANGED
@@ -31,6 +31,7 @@ var signScheme = require("./sign-scheme");
31
31
  var pkix = require("./schema-pkix");
32
32
  var pkiBuild = require("./pki-build");
33
33
  var frameworkError = require("./framework-error");
34
+ var guard = require("./guard-all");
34
35
 
35
36
  var CrmfError = frameworkError.CrmfError;
36
37
  var b = asn1.build;
@@ -46,6 +47,7 @@ var _b = pkiBuild.makeBuilder({
46
47
  });
47
48
 
48
49
  var KNOWN_SPEC_KEYS = { certReqId: 1, certTemplate: 1, controls: 1, regInfo: 1, pop: 1 };
50
+ var KNOWN_BATCH_KEYS = { messages: 1 };
49
51
  var KNOWN_TEMPLATE_KEYS = { version: 1, subject: 1, publicKey: 1, validity: 1, extensions: 1, issuer: 1 };
50
52
  var REVOCATION_TEMPLATE_KEYS = { version: 1, subject: 1, publicKey: 1, validity: 1, extensions: 1, issuer: 1, serialNumber: 1 };
51
53
  // The controls (RFC 4211 sec. 6) and regInfo (sec. 7) are DISJOINT AttributeTypeAndValue namespaces --
@@ -83,7 +85,7 @@ function _encodeCertTemplate(tpl, opts) {
83
85
  // A REQUEST template omits serialNumber (CA-assigned, RFC 4211 sec. 5); a REVOCATION template (CMP rr,
84
86
  // RFC 9810 sec. 5.3.9) carries serialNumber [1] to name the certificate to revoke -- allowed only then.
85
87
  var allowed = (opts && opts.revocation) ? REVOCATION_TEMPLATE_KEYS : KNOWN_TEMPLATE_KEYS;
86
- Object.keys(tpl).forEach(function (k) { if (!allowed[k]) throw _err("crmf/bad-input", "unknown certTemplate field " + JSON.stringify(k)); });
88
+ guard.identifier.assertKnownKeys(tpl, allowed, _err, "crmf/bad-input", "unknown certTemplate field ");
87
89
  var fields = [];
88
90
  if (tpl.version != null) {
89
91
  if (tpl.version !== 2) throw _err("crmf/bad-version", "certTemplate version MUST be 2 (v3) if supplied (RFC 4211 sec. 5)");
@@ -127,9 +129,11 @@ function _buildAttrTypeAndValues(spec, code, label, valueMap) {
127
129
  }
128
130
  if (!spec || typeof spec !== "object") throw _err("crmf/bad-input", label + " must be an object or an array of pre-encoded AttributeTypeAndValue DER");
129
131
  var out = [], seen = {};
132
+ guard.identifier.assertKnownKeys(spec, valueMap, _err, "crmf/bad-input", function (k) {
133
+ return "unknown " + label + " " + JSON.stringify(k) + "; pass a pre-encoded AttributeTypeAndValue DER via the array form for a " + label + " entry outside " + Object.keys(valueMap).join("/");
134
+ });
130
135
  Object.keys(spec).forEach(function (k) {
131
136
  var enc = valueMap[k];
132
- if (!enc) throw _err("crmf/bad-input", "unknown " + label + " " + JSON.stringify(k) + "; pass a pre-encoded AttributeTypeAndValue DER via the array form for a " + label + " entry outside " + Object.keys(valueMap).join("/"));
133
137
  var typeOid = O(k);
134
138
  if (seen[typeOid]) throw _err(code, "duplicate " + label + " type " + k);
135
139
  seen[typeOid] = true;
@@ -236,7 +240,7 @@ function build(spec, key, opts) {
236
240
 
237
241
  function _buildCertReqMsg(spec, key, opts) {
238
242
  if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("crmf/bad-input", "each certificate-request-message spec must be an object");
239
- Object.keys(spec).forEach(function (k) { if (!KNOWN_SPEC_KEYS[k]) throw _err("crmf/bad-input", "unknown spec field " + JSON.stringify(k)); });
243
+ guard.identifier.assertKnownKeys(spec, KNOWN_SPEC_KEYS, _err, "crmf/bad-input", "unknown spec field ");
240
244
  if (spec.certTemplate == null) throw _err("crmf/bad-input", "spec.certTemplate is required");
241
245
 
242
246
  var signingKey = (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type == null && "key" in key) ? key.key : key;
@@ -261,7 +265,9 @@ function _build(spec, key, opts) {
261
265
  // Batch form: the envelope carries ONLY `messages` -- reject a stray field so a request spec written at
262
266
  // the wrong nesting level (e.g. certTemplate alongside messages) is not silently dropped.
263
267
  if (!Array.isArray(spec.messages)) throw _err("crmf/bad-input", "spec.messages must be an array of certificate-request-message specs");
264
- Object.keys(spec).forEach(function (k) { if (k !== "messages") throw _err("crmf/bad-input", "unknown batch-envelope field " + JSON.stringify(k) + " -- a batch spec carries only 'messages'"); });
268
+ guard.identifier.assertKnownKeys(spec, KNOWN_BATCH_KEYS, _err, "crmf/bad-input", function (k) {
269
+ return "unknown batch-envelope field " + JSON.stringify(k) + " -- a batch spec carries only 'messages'";
270
+ });
265
271
  specs = spec.messages;
266
272
  } else {
267
273
  specs = [spec];
package/lib/csr-sign.js CHANGED
@@ -28,8 +28,10 @@ var signScheme = require("./sign-scheme");
28
28
  var pkix = require("./schema-pkix");
29
29
  var pkiBuild = require("./pki-build");
30
30
  var frameworkError = require("./framework-error");
31
+ var guard = require("./guard-all");
31
32
 
32
33
  var CsrError = frameworkError.CsrError;
34
+ var KNOWN_SPEC_KEYS = { subject: 1, subjectPublicKey: 1, extensionRequest: 1, challengePassword: 1, attributes: 1 };
33
35
  var b = asn1.build;
34
36
  function _err(code, message, cause) { return new CsrError(code, message, cause); }
35
37
  function _signE(kind, message, cause) { return new CsrError("csr/" + kind, message, cause); }
@@ -102,9 +104,7 @@ function _sign(spec, key, opts) {
102
104
  if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("csr/bad-input", "the certification-request spec must be an object");
103
105
  // Reject a typo'd spec field at config-time rather than silently dropping it (a misspelled `subjcet`
104
106
  // would otherwise yield an empty-subject request).
105
- Object.keys(spec).forEach(function (k) {
106
- if (k !== "subject" && k !== "subjectPublicKey" && k !== "extensionRequest" && k !== "challengePassword" && k !== "attributes") throw _err("csr/bad-input", "unknown spec field " + JSON.stringify(k));
107
- });
107
+ guard.identifier.assertKnownKeys(spec, KNOWN_SPEC_KEYS, _err, "csr/bad-input", "unknown spec field ");
108
108
  // The signing key is the second argument (a PKCS#8 key / CryptoKey / composite key pair), or { key }.
109
109
  var signingKey = (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type == null && "key" in key) ? key.key : key;
110
110
  if (signingKey == null) throw _err("csr/bad-input", "a signing key (the subject's private key) is required");
package/lib/ct.js CHANGED
@@ -970,7 +970,7 @@ function _fetchBody(transport, url, req, label) {
970
970
  */
971
971
  async function fetchLogList(opts) {
972
972
  opts = opts || {};
973
- Object.keys(opts).forEach(function (k) { if (!KNOWN_FETCH_OPTS[k]) throw _ctErr("ct/bad-input", "unknown opts field " + JSON.stringify(k)); });
973
+ guard.identifier.assertKnownKeys(opts, KNOWN_FETCH_OPTS, _ctErr, "ct/bad-input", "unknown opts field ");
974
974
  if (opts.signerKey == null) throw _ctErr("ct/bad-input", "opts.signerKey is required -- the caller-pinned CT log-list distributor SPKI (there is no baked-in key)");
975
975
  if (opts.url == null) throw _ctErr("ct/bad-input", "opts.url is required -- the log_list.json URL (there is no baked-in vendor URL)");
976
976
  // Parse + https-gate both URLs BEFORE any transport call -- the gate runs across the injectable seam so an
@@ -53,4 +53,34 @@ function assertCanonicalOid(str, E, code, label, boundsCode) {
53
53
  return str;
54
54
  }
55
55
 
56
- module.exports = { assertCanonicalOid: assertCanonicalOid };
56
+ // assertKnownKeys(obj, known, E, code, label) -- every own key of `obj` must appear in `known`.
57
+ //
58
+ // The shape this replaces was hand-written in a dozen callers, and it is worth one home because
59
+ // getting it wrong fails OPEN in the quietest possible way: an options object whose key is
60
+ // misspelled silently carries the default, so a caller who asked for a stricter check gets the
61
+ // looser behaviour and no error. Two details are easy to lose in a re-inline and are fixed here.
62
+ // `known` is consulted with hasOwnProperty rather than a truthiness test, so an inherited Object
63
+ // member ("constructor", "toString") cannot read as a recognised key; and the walk is over own
64
+ // enumerable keys, so a `__proto__` arriving from JSON is inspected rather than skipped.
65
+ //
66
+ // @enforced-by behavioral -- an options-shape walk has no rename-proof code shape; the RED vectors
67
+ // (an unknown key, an inherited name, a JSON-borne __proto__) are the guard.
68
+ //
69
+ // `message` is the caller's OWN wording, so routing an existing check through this guard changes
70
+ // what is checked, never what the operator reads: a string is the prefix the quoted key is appended
71
+ // to, and a function (key) -> string builds the whole sentence, for the many callers that name the
72
+ // key mid-sentence and follow it with the hint that says what to pass instead.
73
+ //
74
+ // @enforced-by guard-shape-reinlined
75
+ // The shape requires the THROW: an identical walk whose body FILTERS on the same table (copying the
76
+ // recognised keys onward rather than rejecting the unrecognised ones) is a different operation with
77
+ // no fail-open risk, and must not be dragged through a guard that only knows how to reject.
78
+ // @guard-shape Object\.keys\(\w+(?:\.\w+)*\)\.forEach\(function \((\w+)\) \{\s*if \(![\w.]+\[\1\]\) (?:\{\s*)?throw
79
+ function assertKnownKeys(obj, known, E, code, message) {
80
+ var describe = typeof message === "function" ? message : function (k) { return message + JSON.stringify(k); };
81
+ Object.keys(obj).forEach(function (k) {
82
+ if (!Object.prototype.hasOwnProperty.call(known, k)) throw E(code, describe(k));
83
+ });
84
+ }
85
+
86
+ module.exports = { assertCanonicalOid: assertCanonicalOid, assertKnownKeys: assertKnownKeys };
@@ -1137,7 +1137,9 @@ function validateCriticalExtensionStructure(cert) {
1137
1137
  * var cert = pki.schema.x509.parse(der);
1138
1138
  * var res = await pki.path.validate([cert], {
1139
1139
  * time: new Date("2020-01-01T00:00:00Z"),
1140
- * trustAnchor: { name: cert.issuer, publicKey: cert.subjectPublicKeyInfo.bytes, algorithm: cert.signatureAlgorithm.oid },
1140
+ * // the anchor's own key algorithm, not the algorithm its issuer signed it with
1141
+ * trustAnchor: { name: cert.issuer, publicKey: cert.subjectPublicKeyInfo.bytes,
1142
+ * algorithm: cert.subjectPublicKeyInfo.algorithm.oid },
1141
1143
  * });
1142
1144
  * res.valid; // boolean; res.results[0].checks carries the per-check codes
1143
1145
  */
package/lib/pki-build.js CHANGED
@@ -168,9 +168,7 @@ function makeBuilder(ctx) {
168
168
  if (bc.cA != null && typeof bc.cA !== "boolean") throw E("bad-input", "basicConstraints cA must be a boolean");
169
169
  if (bc.critical != null && typeof bc.critical !== "boolean") throw E("bad-input", "basicConstraints critical must be a boolean");
170
170
  if (bc.pathLen != null) pathLen(bc.pathLen);
171
- Object.keys(bc).forEach(function (k) {
172
- if (k !== "cA" && k !== "pathLen" && k !== "critical") throw E("bad-input", "unknown basicConstraints field " + JSON.stringify(k));
173
- });
171
+ guard.identifier.assertKnownKeys(bc, BC_KEYS, E, "bad-input", "unknown basicConstraints field ");
174
172
  }
175
173
  function extBasicConstraints(spec) {
176
174
  var children = [];
@@ -270,6 +268,7 @@ function makeBuilder(ctx) {
270
268
  // request that carries requested extensions (a PKCS#10 extensionRequest attribute, a CRMF CertTemplate
271
269
  // extensions [9]); a request REQUESTS extensions, so there are no CA cross-field gates. `spki` feeds the
272
270
  // subjectKeyIdentifier auto-derive.
271
+ var BC_KEYS = { cA: 1, pathLen: 1, critical: 1 };
273
272
  var REQ_EXT_KEYS = {
274
273
  subjectAltName: 1, keyUsage: 1, keyUsageCritical: 1, extendedKeyUsage: 1, extendedKeyUsageCritical: 1,
275
274
  basicConstraints: 1, certificatePolicies: 1, certificatePoliciesCritical: 1, subjectKeyIdentifier: 1,
@@ -295,8 +294,8 @@ function makeBuilder(ctx) {
295
294
  }));
296
295
  }
297
296
  if (!extSpec || typeof extSpec !== "object") throw E("bad-input", "requested extensions must be an object or an array of pre-encoded Extension DER");
298
- Object.keys(extSpec).forEach(function (k) {
299
- if (!REQ_EXT_KEYS[k]) throw E("bad-input", "unknown requested extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension");
297
+ guard.identifier.assertKnownKeys(extSpec, REQ_EXT_KEYS, E, "bad-input", function (k) {
298
+ return "unknown requested extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension";
300
299
  });
301
300
  var out = [];
302
301
  if (extSpec.subjectKeyIdentifier != null) {
@@ -168,6 +168,7 @@ var TPM_POLICY_PROFILES = Object.assign(Object.create(null), {
168
168
  "hardware-bound": { fixedTPM: true, fixedParent: true, sensitiveDataOrigin: true, sign: true, restricted: false, x509sign: false },
169
169
  });
170
170
  var _TPM_POLICY_KEYS = Object.assign(Object.create(null), { profile: 1, objectAttributes: 1, reservedBitsClear: 1, consistency: 1, authPolicy: 1 });
171
+ var _AUTH_POLICY_KEYS = Object.assign(Object.create(null), { present: 1, allow: 1 });
171
172
 
172
173
  // Config-time validation of opts.tpmPolicy: a typo must never silently disable a check, so an
173
174
  // unknown key or attribute name throws at the boundary rather than being ignored.
@@ -177,9 +178,7 @@ var _TPM_POLICY_KEYS = Object.assign(Object.create(null), { profile: 1, objectAt
177
178
  function normalizeObjectAttributePolicy(policy, E, code) {
178
179
  if (policy === undefined) return null;
179
180
  if (!policy || typeof policy !== "object" || Array.isArray(policy)) throw new E(code, "opts.tpmPolicy must be an object");
180
- Object.keys(policy).forEach(function (k) {
181
- if (!_TPM_POLICY_KEYS[k]) throw new E(code, "opts.tpmPolicy has an unknown key " + JSON.stringify(k));
182
- });
181
+ guard.identifier.assertKnownKeys(policy, _TPM_POLICY_KEYS, function (c, m) { return new E(c, m); }, code, "opts.tpmPolicy has an unknown key ");
183
182
  var want = {};
184
183
  if (policy.profile !== undefined) {
185
184
  var preset = TPM_POLICY_PROFILES[policy.profile];
@@ -212,9 +211,7 @@ function normalizeObjectAttributePolicy(policy, E, code) {
212
211
  // The nested keys are enumerated for the same reason the top-level ones are: a misspelled
213
212
  // `alow` would leave the allow-list unset, and the assertion would then impose no digest
214
213
  // restriction at all -- the caller's policy silently doing nothing.
215
- Object.keys(ap).forEach(function (k) {
216
- if (k !== "present" && k !== "allow") throw new E(code, "opts.tpmPolicy.authPolicy has an unknown key " + JSON.stringify(k));
217
- });
214
+ guard.identifier.assertKnownKeys(ap, _AUTH_POLICY_KEYS, function (c, m) { return new E(c, m); }, code, "opts.tpmPolicy.authPolicy has an unknown key ");
218
215
  if (ap.present !== undefined && typeof ap.present !== "boolean") throw new E(code, "opts.tpmPolicy.authPolicy.present must be a boolean");
219
216
  if (ap.allow !== undefined) {
220
217
  if (!Array.isArray(ap.allow)) throw new E(code, "opts.tpmPolicy.authPolicy.allow must be an array");