@blamejs/pki 0.4.10 → 0.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/lint.js CHANGED
@@ -274,6 +274,58 @@ function _ecCurveName(spki) {
274
274
  } catch (_e) { return null; } // explicit / invalid EC parameters are not an approved named curve
275
275
  }
276
276
 
277
+ // RFC 5280 marks several extensions MUST (error) or SHOULD (warn) be critical. The shape is
278
+ // uniform: applies when the extension is present, fires when its raw `critical` flag is not
279
+ // true. The rule reads `ctx.raw(name).critical` WITHOUT decoding the value -- criticality is
280
+ // a structural property of the extension, independent of its contents.
281
+ // ---- RFC 5280 4.2.1.4 userNotice DisplayText ----
282
+ // certificatePolicies surfaces each policy's qualifiers as RAW bytes (an external verifier may hash
283
+ // them), so the DisplayText values are read out of those bytes here. The UserNotice walk itself lives
284
+ // in schema-pkix (pki.inspect renders the same values), so the two consumers cannot disagree about
285
+ // which members are DisplayText or how a BMPString decodes.
286
+ var OID_UNOTICE = oid.byName("unotice");
287
+ var _T_BMP = asn1.TAGS.BMP_STRING, _T_VISIBLE = asn1.TAGS.VISIBLE_STRING, _T_UTF8 = asn1.TAGS.UTF8_STRING;
288
+ // The C0 and C1 control ranges, tested by code point rather than a regex: the characters this rule is
289
+ // ABOUT cannot appear in the source, and a regex holding them would be a control byte here.
290
+ function _hasControlChar(str) {
291
+ for (var i = 0; i < str.length; i++) {
292
+ var c = str.charCodeAt(i);
293
+ if (c <= 0x1f || (c >= 0x7f && c <= 0x9f)) return true;
294
+ }
295
+ return false;
296
+ }
297
+ // Every DisplayText in the certificate's certificatePolicies. Both positions are collected --
298
+ // explicitText AND NoticeReference.organization -- because the SIZE bound is on the DisplayText type,
299
+ // so reporting only explicitText would leave its sibling unmeasured.
300
+ // The two decodes below are fail-safe belts on the never-throw data path, not reachable paths: the
301
+ // shared decoder ran pkix.assertPolicyQualifiers before surfacing qualifiersBytes, so the bytes are
302
+ // already known to decode as a SEQUENCE whose every element is a two-member PolicyQualifierInfo led
303
+ // by a readable OID. An undecodable extension never reaches here at all -- it is the separate
304
+ // extension-undecodable finding.
305
+ function _policyDisplayTexts(ctx) {
306
+ var d = ctx.decode("certificatePolicies");
307
+ if (!d || !Array.isArray(d.value)) return [];
308
+ var out = [];
309
+ d.value.forEach(function (pi) {
310
+ if (!pi.qualifiersBytes || !pi.qualifiersBytes.length) return;
311
+ var quals;
312
+ // allow:swallow-unverified re-decoding bytes that already decoded under assertPolicyQualifiers cannot throw
313
+ try { quals = asn1.decode(pi.qualifiersBytes).children; } catch (_e) { return; }
314
+ (quals || []).forEach(function (pq) {
315
+ var qid;
316
+ // allow:swallow-unverified assertPolicyQualifiers already read this OID, so re-reading it cannot throw
317
+ try { qid = asn1.read.oid(pq.children[0]); } catch (_e2) { return; }
318
+ if (qid !== OID_UNOTICE) return;
319
+ // Entries whose contents did not decode arrive with a null `text` and keep their tag: the rules
320
+ // below that read the text skip them, while the encoding rule -- which the ASN.1 tag alone
321
+ // answers -- still sees them.
322
+ out = out.concat(pkix.userNoticeTexts(pq.children[1]));
323
+ });
324
+ });
325
+ return out;
326
+ }
327
+ function _hasPolicyDisplayText(cert, ctx) { return _policyDisplayTexts(ctx).length > 0; }
328
+
277
329
  // RFC 5280 marks several extensions MUST (error) or SHOULD (warn) be critical. The shape is
278
330
  // uniform: applies when the extension is present, fires when its raw `critical` flag is not
279
331
  // true. The rule reads `ctx.raw(name).critical` WITHOUT decoding the value -- criticality is
@@ -413,6 +465,65 @@ var RFC5280_RULES = [
413
465
  appliesTo: function (cert, ctx) { var bc = ctx.decode("basicConstraints"); return !(bc && bc.value && bc.value.cA === true); },
414
466
  check: function (cert, ctx) { return ctx.raw("subjectKeyIdentifier") ? null : true; },
415
467
  },
468
+ // 4.2.1.4 governs the userNotice qualifier's DisplayText with one MUST NOT and three SHOULD-level
469
+ // rules. None of them can live in the decoder: the section closes by directing certificate users to
470
+ // "gracefully handle explicitText with more than 200 characters", so a verifier that rejected these
471
+ // would refuse certificates that are in the wild and otherwise valid. Reporting them is exactly what
472
+ // a linter is for, so each rule carries the severity its normative word does.
473
+ {
474
+ // DisplayText is SIZE (1..200) -- both ends -- but the two ends get SEPARATE ids rather than one
475
+ // length rule, because the section treats them differently: it tells certificate users to handle a
476
+ // notice ABOVE 200 gracefully, and says nothing of the sort about an empty one. An operator acting
477
+ // on that advice suppresses the over-long finding; folding both into a single id would silently
478
+ // suppress the empty case along with it, which has no such carve-out.
479
+ id: "lint/rfc5280/explicit-text-too-long", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.4",
480
+ message: "a userNotice DisplayText should not exceed 200 characters",
481
+ appliesTo: _hasPolicyDisplayText,
482
+ check: function (cert, ctx) {
483
+ var over = _policyDisplayTexts(ctx).filter(function (d) { return d.text !== null && d.chars > pkix.DISPLAY_TEXT_MAX; });
484
+ return over.length ? { context: { count: over.length, longest: Math.max.apply(null, over.map(function (d) { return d.chars; })) } } : null;
485
+ },
486
+ },
487
+ {
488
+ id: "lint/rfc5280/explicit-text-empty", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.4",
489
+ message: "a userNotice DisplayText must not be empty (SIZE (1..200))",
490
+ appliesTo: _hasPolicyDisplayText,
491
+ check: function (cert, ctx) {
492
+ var empty = _policyDisplayTexts(ctx).filter(function (d) { return d.text !== null && d.chars < 1; });
493
+ return empty.length ? { context: { count: empty.length } } : null;
494
+ },
495
+ },
496
+ {
497
+ id: "lint/rfc5280/explicit-text-bad-encoding", severity: "error", source: "rfc5280", citation: "RFC 5280 4.2.1.4",
498
+ message: "conforming CAs must not encode explicitText as VisibleString or BMPString",
499
+ appliesTo: _hasPolicyDisplayText,
500
+ check: function (cert, ctx) {
501
+ var bad = _policyDisplayTexts(ctx).filter(function (d) { return d.field === "explicitText" && (d.tagNumber === _T_VISIBLE || d.tagNumber === _T_BMP); });
502
+ return bad.length ? { context: { count: bad.length, encoding: bad[0].tagNumber === _T_BMP ? "BMPString" : "VisibleString" } } : null;
503
+ },
504
+ },
505
+ {
506
+ id: "lint/rfc5280/explicit-text-control-chars", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.4",
507
+ message: "an explicitText should not include control characters (U+0000 to U+001F, U+007F to U+009F)",
508
+ appliesTo: _hasPolicyDisplayText,
509
+ check: function (cert, ctx) {
510
+ var bad = _policyDisplayTexts(ctx).filter(function (d) { return d.field === "explicitText" && d.text !== null && _hasControlChar(d.text); });
511
+ return bad.length ? { context: { count: bad.length } } : null;
512
+ },
513
+ },
514
+ {
515
+ id: "lint/rfc5280/explicit-text-not-nfc", severity: "notice", source: "rfc5280", citation: "RFC 5280 4.2.1.4",
516
+ message: "a UTF8String explicitText should be normalized to Unicode normalization form C (NFC)",
517
+ appliesTo: _hasPolicyDisplayText,
518
+ check: function (cert, ctx) {
519
+ // Only the utf8String arm carries the NFC recommendation; the other arms cannot express the
520
+ // combining sequences the rule is about.
521
+ var bad = _policyDisplayTexts(ctx).filter(function (d) {
522
+ return d.field === "explicitText" && d.text !== null && d.tagNumber === _T_UTF8 && d.text.normalize("NFC") !== d.text;
523
+ });
524
+ return bad.length ? { context: { count: bad.length } } : null;
525
+ },
526
+ },
416
527
  ];
417
528
 
418
529
  function _isTls(cert, ctx) { return ctx.isTlsServerCert; }
@@ -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/pbes2.js CHANGED
@@ -26,8 +26,15 @@ var PRF_NODE_BY_NAME = { hmacWithSHA1: "sha1", hmacWithSHA256: "sha256", hmacWit
26
26
  var PRF_NODE_BY_OID = {}; Object.keys(PRF_NODE_BY_NAME).forEach(function (n) { PRF_NODE_BY_OID[O(n)] = PRF_NODE_BY_NAME[n]; });
27
27
 
28
28
  // content-encryption OID -> AES key bits (CBC + GCM). The PBES2 encryptionScheme + CMS content cipher table.
29
- var CONTENT_KEYBITS = {};
30
- [["aes128-CBC", 128], ["aes192-CBC", 192], ["aes256-CBC", 256], ["aes128-GCM", 128], ["aes192-GCM", 192], ["aes256-GCM", 256]].forEach(function (r) { CONTENT_KEYBITS[O(r[0])] = r[1]; });
29
+ // CONTENT_MODE rides the SAME rows so a cipher can never be present for its key length but absent for its
30
+ // mode: key length alone does not distinguish an AEAD cipher from a plain one, and a consumer that resolves
31
+ // only the length would open content in the wrong mode whenever the two happen to share a key size.
32
+ var CONTENT_KEYBITS = {}, CONTENT_MODE = {};
33
+ [["aes128-CBC", 128, "cbc"], ["aes192-CBC", 192, "cbc"], ["aes256-CBC", 256, "cbc"],
34
+ ["aes128-GCM", 128, "gcm"], ["aes192-GCM", 192, "gcm"], ["aes256-GCM", 256, "gcm"]].forEach(function (r) {
35
+ CONTENT_KEYBITS[O(r[0])] = r[1];
36
+ CONTENT_MODE[O(r[0])] = r[2];
37
+ });
31
38
 
32
39
  // A password is an octet string (RFC 8018 sec. 2): a string is UTF-8-encoded deterministically (correct for
33
40
  // non-ASCII, and byte-identical to OpenSSL), a Buffer/Uint8Array used verbatim.
@@ -211,6 +218,6 @@ module.exports = {
211
218
  prfNodeByName: prfNodeByName, prfNodeByOid: prfNodeByOid,
212
219
  pbkdf2ParamsSeq: pbkdf2ParamsSeq, pbes2AlgId: pbes2AlgId, parsePbkdf2Params: parsePbkdf2Params,
213
220
  requireChildren: requireChildren, seqChildren: seqChildren,
214
- cbcEncrypt: cbcEncrypt, cbcDecrypt: cbcDecrypt, pbes2Encrypt: pbes2Encrypt, pbes2Decrypt: pbes2Decrypt, CONTENT_KEYBITS: CONTENT_KEYBITS,
221
+ cbcEncrypt: cbcEncrypt, cbcDecrypt: cbcDecrypt, pbes2Encrypt: pbes2Encrypt, pbes2Decrypt: pbes2Decrypt, CONTENT_KEYBITS: CONTENT_KEYBITS, CONTENT_MODE: CONTENT_MODE,
215
222
  pbmac1AlgId: pbmac1AlgId, pbmac1: pbmac1,
216
223
  };
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) {
@@ -1031,7 +1031,11 @@ function _qualifierToDer(qidNode, qtextNode) {
1031
1031
  if (qtextNode.majorType !== 3) throw _err("c509/bad-extensions", "a policyQualifier value must be a CBOR text string");
1032
1032
  var text = cbor.read.textString(qtextNode);
1033
1033
  if (qi === 1) return b.sequence([b.oid(oid.byName("cps")), _ia5Universal(text, "a CPSuri")]); // CPSuri ::= IA5String
1034
- // id-qt-unotice: UserNotice ::= SEQUENCE { explicitText utf8String } -- noticeRef omitted, DisplayText SIZE 1..200 floor.
1034
+ // id-qt-unotice: UserNotice ::= SEQUENCE { explicitText utf8String } -- noticeRef omitted. Only the SIZE
1035
+ // (1..200) FLOOR is enforced: an empty explicitText is a degenerate value no encoder can have produced,
1036
+ // while RFC 5280 sec. 4.2.1.4 directs certificate users to gracefully handle a notice ABOVE 200
1037
+ // characters, and draft-20 sec. 3.3's compact predicate is SIZE-silent -- so an over-long notice
1038
+ // transcodes rather than being refused.
1035
1039
  if (text.length === 0) throw _err("c509/bad-extensions", "a UserNotice explicitText must be non-empty (DisplayText SIZE 1..200)");
1036
1040
  return b.sequence([b.oid(oid.byName("unotice")), b.sequence([b.utf8(text)])]);
1037
1041
  }
@@ -703,6 +703,108 @@ function assertPolicyQualifiers(qNode, fail) {
703
703
  });
704
704
  }
705
705
 
706
+ // DisplayText ::= CHOICE { ia5String, visibleString, bmpString, utf8String } each SIZE (1..200)
707
+ // -- RFC 5280 sec. 4.2.1.4. The bound is deliberately NOT enforced here, and that is a normative
708
+ // requirement rather than a gap: the same section closes with "While the explicitText has a maximum
709
+ // size of 200 characters, some non-conforming CAs exceed this limit. Therefore, certificate users
710
+ // SHOULD gracefully handle explicitText with more than 200 characters." A decoder IS the certificate
711
+ // user that note addresses, so refusing an over-long notice would violate a SHOULD and reject
712
+ // certificates that are in the wild and otherwise valid. The bound belongs to the two layers that
713
+ // can act on it: an issuer, which must not MINT one, and pki.lint, which reports it as an advisory.
714
+ //
715
+ // Measured in CHARACTERS, not octets: a conforming 200-character UTF8String notice can occupy 800
716
+ // octets, so `.length` (UTF-16 units) or a byte count would misjudge a valid value. Exported so
717
+ // every layer that reports the bound counts it identically instead of keeping its own copy.
718
+ var DISPLAY_TEXT_MAX = 200;
719
+ function displayTextChars(str) { return Array.from(str).length; }
720
+
721
+ // @internal
722
+ // userNoticeTexts(qualifier) -- the DisplayText values of a decoded UserNotice qualifier, as
723
+ // [{ field, tagNumber, text, chars }] in encounter order. `field` is "explicitText" or
724
+ // "organization" (the NoticeReference member), so a caller that must treat the two alike (the
725
+ // SIZE bound applies to both) and one that must not (only explicitText carries the encoding and
726
+ // normalization rules) can each select correctly.
727
+ //
728
+ // UserNotice ::= SEQUENCE { noticeRef NoticeReference OPTIONAL, explicitText DisplayText OPTIONAL }
729
+ // NoticeReference ::= SEQUENCE { organization DisplayText, noticeNumbers SEQUENCE OF INTEGER }
730
+ // Both members are OPTIONAL and distinguished by tag, so a leading SEQUENCE is the noticeRef and any
731
+ // DisplayText member is the explicitText. Returns [] for a qualifier that is not a UserNotice --
732
+ // this reads an already-decoded node for display and reporting, and is never a validity verdict.
733
+ var _DISPLAY_TEXT_TAGS = null;
734
+ function _isDisplayTextNode(n) {
735
+ if (!_DISPLAY_TEXT_TAGS) {
736
+ _DISPLAY_TEXT_TAGS = {};
737
+ _DISPLAY_TEXT_TAGS[_T.IA5_STRING] = 1; _DISPLAY_TEXT_TAGS[_T.VISIBLE_STRING] = 1;
738
+ _DISPLAY_TEXT_TAGS[_T.BMP_STRING] = 1; _DISPLAY_TEXT_TAGS[_T.UTF8_STRING] = 1;
739
+ }
740
+ return !!n && n.tagClass === "universal" && _DISPLAY_TEXT_TAGS[n.tagNumber] === 1;
741
+ }
742
+ // Decoded through the STRICT reader, which validates each arm against its own declared string type
743
+ // (invalid UTF-8, a high bit in an IA5String, an odd-length or lone-surrogate BMPString all throw).
744
+ // Reading the content bytes directly would repair the value instead: `toString("utf8")` substitutes
745
+ // U+FFFD for invalid sequences and a hand-rolled UCS-2 loop drops a trailing odd octet, so a caller
746
+ // would render or measure text the certificate does not contain. Returns null when the value does
747
+ // not decode, so the caller can take its own fallback rather than trust a repair.
748
+ // An undecodable value yields an entry whose `text` and `chars` are null but whose `field` and
749
+ // `tagNumber` are still present. The two are separable facts: which ASN.1 string type was used is
750
+ // established by the tag alone, so a rule about the ENCODING stays answerable even when the contents
751
+ // do not decode, while every rule that reads the TEXT must skip the entry rather than analyze a
752
+ // repair. Collapsing both into "not analyzable" would let a prohibited encoding escape its finding
753
+ // by also being malformed inside.
754
+ function _dtEntry(field, node) {
755
+ var text;
756
+ // allow:swallow-unverified an undecodable DisplayText keeps its tag and drops its text; the callers branch on text === null
757
+ try { text = asn1.read.string(node); } catch (_e) { text = null; }
758
+ return { field: field, tagNumber: node.tagNumber, text: text, chars: text === null ? null : displayTextChars(text) };
759
+ }
760
+ // A NoticeReference names its notice by ORGANIZATION **and** NUMBER -- the organization alone does not
761
+ // identify which notice is meant, so a consumer that surfaced only the text would lose the lookup key.
762
+ // Returns null when the numbers are absent, wrongly shaped, or contain a member that is not a readable
763
+ // INTEGER: an incompletely decoded reference must NOT be presentable, because dropping the members that
764
+ // failed would render a partial reference indistinguishable from a whole one -- the same defect as
765
+ // omitting the numbers entirely, in a form that is harder to notice. Numbers are decimal strings.
766
+ function _noticeNumbers(node) {
767
+ if (!node || node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children) return null;
768
+ var nums = [], ok = true;
769
+ node.children.forEach(function (n) {
770
+ // allow:swallow-unverified a non-INTEGER member makes the whole reference undecodable; the caller falls back rather than rendering part of it
771
+ try { nums.push(String(asn1.read.integer(n))); } catch (_e) { ok = false; }
772
+ });
773
+ return ok ? nums : null;
774
+ }
775
+ // Every DisplayText member is returned, including one whose contents did not decode -- that entry
776
+ // carries its tag with a null `text`. A caller that RENDERS the notice must check that every entry
777
+ // decoded before showing any of it (a partial notice is indistinguishable from a complete one); a
778
+ // caller that only classifies the encoding can read the tags regardless.
779
+ // The LAYOUT is validated before any member is read: UserNotice fixes the order (noticeRef first) and
780
+ // the cardinality (each member at most once, nothing else present). Collecting every recognized member
781
+ // and ignoring the rest would accept a duplicated explicitText, a reversed order, or an extra member,
782
+ // and the caller -- which can only see whether the entries it got look complete -- would then present
783
+ // a structurally invalid notice as a whole one. A malformed layout yields NO entries, so a renderer
784
+ // falls back to hex and a reporter measures nothing.
785
+ function userNoticeTexts(qualifier) {
786
+ if (!qualifier || qualifier.tagClass !== "universal" || qualifier.tagNumber !== _T.SEQUENCE || !qualifier.children) return [];
787
+ var kids = qualifier.children;
788
+ if (kids.length > 2) return [];
789
+ var i = 0, out = [];
790
+ // noticeRef [absent | first]: a SEQUENCE of { organization DisplayText, noticeNumbers }.
791
+ if (i < kids.length && kids[i].tagClass === "universal" && kids[i].tagNumber === _T.SEQUENCE) {
792
+ var nr = kids[i];
793
+ if (!nr.children || nr.children.length !== 2 || !_isDisplayTextNode(nr.children[0])) return [];
794
+ var org = _dtEntry("organization", nr.children[0]);
795
+ org.noticeNumbers = _noticeNumbers(nr.children[1]);
796
+ out.push(org);
797
+ i++;
798
+ }
799
+ // explicitText [absent | last]: a DisplayText. Anything else remaining is not a UserNotice member.
800
+ if (i < kids.length) {
801
+ if (!_isDisplayTextNode(kids[i])) return [];
802
+ out.push(_dtEntry("explicitText", kids[i]));
803
+ i++;
804
+ }
805
+ return i === kids.length ? out : [];
806
+ }
807
+
706
808
  // Shared imperative decode helpers for the certExtensionDecoders + attrValueDecoders factories. Both
707
809
  // take the caller's ns, so the helpers close over ns.E / ns.oid; extracted once so the two factories
708
810
  // share one copy (a per-OID value-decoder body is the identical idiom to a per-OID extension body).
@@ -1468,6 +1570,9 @@ module.exports = {
1468
1570
  signedEnvelopeTbs: signedEnvelopeTbs,
1469
1571
  rootSequenceChildren: rootSequenceChildren,
1470
1572
  assertPolicyQualifiers: assertPolicyQualifiers,
1573
+ DISPLAY_TEXT_MAX: DISPLAY_TEXT_MAX,
1574
+ displayTextChars: displayTextChars,
1575
+ userNoticeTexts: userNoticeTexts,
1471
1576
  signedEnvelope: signedEnvelope,
1472
1577
  attrValueToString: attrValueToString,
1473
1578
  attributeTypeAndValue: attributeTypeAndValue,
@@ -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");