@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/CHANGELOG.md +48 -1
- package/README.md +3 -3
- package/lib/attrcert-sign.js +10 -6
- package/lib/cmp-build.js +11 -6
- package/lib/cmp-session.js +1 -1
- package/lib/cmp-verify.js +1 -1
- package/lib/cms-decrypt.js +27 -1
- package/lib/constants.js +23 -0
- package/lib/crl-sign.js +8 -3
- package/lib/crmf-sign.js +10 -4
- package/lib/csr-sign.js +6 -4
- package/lib/ct.js +1 -1
- package/lib/guard-identifier.js +31 -1
- package/lib/inspect.js +38 -4
- package/lib/lint.js +111 -0
- package/lib/path-validate.js +3 -1
- package/lib/pbes2.js +10 -3
- package/lib/pki-build.js +4 -5
- package/lib/schema-c509.js +5 -1
- package/lib/schema-pkix.js +105 -0
- package/lib/validator-tpm.js +3 -6
- package/lib/webauthn-mds.js +753 -0
- package/lib/webauthn.js +287 -9
- package/lib/x509-sign.js +5 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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; }
|
package/lib/path-validate.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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) {
|
package/lib/schema-c509.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/lib/schema-pkix.js
CHANGED
|
@@ -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,
|
package/lib/validator-tpm.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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");
|