@blamejs/pki 0.1.32 → 0.2.0

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.
@@ -70,6 +70,7 @@ var OID = {
70
70
  emailAddress: oid.byName("emailAddress"),
71
71
  extKeyUsage: oid.byName("extKeyUsage"),
72
72
  anyExtendedKeyUsage: oid.byName("anyExtendedKeyUsage"),
73
+ cRLDistributionPoints: oid.byName("cRLDistributionPoints"),
73
74
  };
74
75
 
75
76
  // The set of extension OIDs the validator PROCESSES -- an unrecognized critical
@@ -80,9 +81,15 @@ var OID = {
80
81
  // is the caller's opt-in via opts.requiredEku (RFC 5280 6.1 defines no EKU
81
82
  // processing step -- the required purpose is application context).
82
83
  var PROCESSED_EXTENSIONS = {};
84
+ // cRLDistributionPoints is processed: the CRL checker consults it for the
85
+ // sec. 6.3.3 shard correspondence, and a critical instance (sec. 4.2.1.13 is a
86
+ // SHOULD-non-critical) is structurally validated by
87
+ // validateCriticalExtensionStructure via the registered decoder. freshestCRL
88
+ // stays OUT: sec. 4.2.1.15 requires it non-critical and the validator does not
89
+ // consult it (no delta merge), so a critical instance fails unrecognized.
83
90
  [OID.basicConstraints, OID.keyUsage, OID.nameConstraints, OID.certificatePolicies,
84
91
  OID.policyMappings, OID.policyConstraints, OID.inhibitAnyPolicy, OID.subjectAltName,
85
- OID.extKeyUsage].
92
+ OID.extKeyUsage, OID.cRLDistributionPoints].
86
93
  forEach(function (o) { PROCESSED_EXTENSIONS[o] = true; });
87
94
 
88
95
  // ---- signature verify bridge (NEW 6) ---------------------------------------
@@ -318,7 +325,7 @@ function builtinVerify(state, cert) {
318
325
  } catch (e) { return Promise.resolve({ ok: false, code: pathCode(e, "path/unsupported-algorithm"), error: e }); }
319
326
  // The signature is an octet-aligned BIT STRING (no unused bits) for every
320
327
  // supported algorithm; a non-zero unused-bit count is malformed.
321
- if (cert.signatureValue.unusedBits !== 0) return Promise.resolve({ ok: false, code: "path/bad-signature" });
328
+ if (!guard.crypto.isOctetAligned(cert.signatureValue)) return Promise.resolve({ ok: false, code: "path/bad-signature" });
322
329
  var key;
323
330
  return subtle.importKey("spki", state.workingPublicKey, d.imp, false, ["verify"]).then(function (k) {
324
331
  key = k;
@@ -1050,8 +1057,7 @@ async function validate(path, opts) {
1050
1057
  // Bound the per-cert asymmetric-verify work BEFORE parsing an untrusted bundle
1051
1058
  // (the policy-tree cap guards CVE-2023-0464-style blow-up; this guards linear
1052
1059
  // crypto amplification from an oversized path). Entry-point tier: throw.
1053
- var maxCerts = opts.maxPathCerts !== undefined ? opts.maxPathCerts : constants.LIMITS.PATH_MAX_CERTS;
1054
- if (typeof maxCerts !== "number" || !isFinite(maxCerts) || maxCerts < 1) throw E("path/bad-input", "validate: opts.maxPathCerts must be a positive number");
1060
+ var maxCerts = guard.limits.cap(opts.maxPathCerts, "validate: opts.maxPathCerts", constants.LIMITS.PATH_MAX_CERTS, { E: E, code: "path/bad-input", min: 1 });
1055
1061
  if (path.length > maxCerts) throw E("path/bad-input", "validate: the certification path has " + path.length + " certificates, exceeding the maxPathCerts limit (" + maxCerts + ")");
1056
1062
  var certs = path.map(function (c) { return (c && c.tbsBytes) ? c : x509.parse(c); });
1057
1063
  var n = certs.length;
@@ -1064,9 +1070,9 @@ async function validate(path, opts) {
1064
1070
  }
1065
1071
  // Entry-point tier for the remaining 6.1.1 user-initial inputs: a bad value
1066
1072
  // throws here rather than silently disabling the behavior it configures.
1067
- if (opts.maxPolicyNodes !== undefined && (typeof opts.maxPolicyNodes !== "number" || !isFinite(opts.maxPolicyNodes) || opts.maxPolicyNodes < 1)) {
1068
- throw E("path/bad-input", "validate: opts.maxPolicyNodes must be a positive number");
1069
- }
1073
+ // Validate-if-present (the default is applied where the policy state is built);
1074
+ // the shared integer cap rejects a fractional / negative / non-numeric budget.
1075
+ guard.limits.cap(opts.maxPolicyNodes, "validate: opts.maxPolicyNodes", undefined, { E: E, code: "path/bad-input", min: 1 });
1070
1076
  // user-initial-policy-set is a non-empty SET of policy OID strings (6.1.1(c)).
1071
1077
  // Membership is tested with indexOf, so a raw string here would be consulted
1072
1078
  // as a SUBSTRING match -- validated to an array of strings instead.
@@ -1074,6 +1080,11 @@ async function validate(path, opts) {
1074
1080
  var uipsOk = Array.isArray(opts.userInitialPolicySet) && opts.userInitialPolicySet.length > 0 &&
1075
1081
  opts.userInitialPolicySet.every(function (p) { return typeof p === "string" && p.length > 0; });
1076
1082
  if (!uipsOk) throw E("path/bad-input", "validate: opts.userInitialPolicySet must be a non-empty array of policy OID strings");
1083
+ // Each entry is compared (indexOf) against canonical decoder output, so a
1084
+ // non-canonical key would silently never match -- fail the typo closed here.
1085
+ opts.userInitialPolicySet.forEach(function (p) {
1086
+ guard.identifier.assertCanonicalOid(p, E, "path/bad-input", "validate: opts.userInitialPolicySet entry " + JSON.stringify(p));
1087
+ });
1077
1088
  }
1078
1089
  var seeds = {
1079
1090
  permitted: checkedSubtreeSeeds(opts.initialPermittedSubtrees, "initialPermittedSubtrees"),
@@ -1089,12 +1100,33 @@ async function validate(path, opts) {
1089
1100
  }
1090
1101
  requiredEku = opts.requiredEku.map(function (p) {
1091
1102
  if (typeof p !== "string" || p.length === 0) throw E("path/bad-input", "validate: opts.requiredEku entries must be non-empty strings");
1092
- if (/^\d+(\.\d+)+$/.test(p)) return p;
1103
+ // A dotted-form attempt (leads with a digit) must be a canonical OID -- a
1104
+ // loose regex accepted a leading-zero / out-of-bounds key that would never
1105
+ // match the canonical EKU the target advertises; anything else is a name.
1106
+ if (/^[0-9]/.test(p)) return guard.identifier.assertCanonicalOid(p, E, "path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p));
1093
1107
  var dotted = oid.byName(p);
1094
1108
  if (typeof dotted !== "string") throw E("path/bad-input", "validate: opts.requiredEku entry " + JSON.stringify(p) + " is not a registered OID name");
1095
1109
  return dotted;
1096
1110
  });
1097
1111
  }
1112
+ // opts.checkPurpose -- the single key purpose the ANCHOR's NSS trust metadata
1113
+ // (distrustAfter / purposes) is consulted for. Independent of requiredEku
1114
+ // (which gates the leaf's own EKU extension): this selects the per-purpose
1115
+ // key in the trust-anchor constraint contract. A purpose OID name (or a
1116
+ // canonical dotted OID normalized to its name); a bad value throws here.
1117
+ var checkPurpose = null;
1118
+ if (opts.checkPurpose !== undefined) {
1119
+ if (typeof opts.checkPurpose !== "string" || opts.checkPurpose.length === 0) {
1120
+ throw E("path/bad-input", "validate: opts.checkPurpose must be a key-purpose OID name or dotted OID string");
1121
+ }
1122
+ if (/^[0-9]/.test(opts.checkPurpose)) {
1123
+ var cpDotted = guard.identifier.assertCanonicalOid(opts.checkPurpose, E, "path/bad-input", "validate: opts.checkPurpose");
1124
+ checkPurpose = oid.name(cpDotted) || cpDotted; // normalize a dotted purpose OID to its name for the anchor map
1125
+ } else {
1126
+ if (typeof oid.byName(opts.checkPurpose) !== "string") throw E("path/bad-input", "validate: opts.checkPurpose " + JSON.stringify(opts.checkPurpose) + " is not a registered OID name");
1127
+ checkPurpose = opts.checkPurpose;
1128
+ }
1129
+ }
1098
1130
 
1099
1131
  var state = initialize(certs, opts, seeds);
1100
1132
  state._n = n;
@@ -1233,6 +1265,22 @@ async function validate(path, opts) {
1233
1265
  // default). An ABSENT extension leaves the key unrestricted, so it
1234
1266
  // satisfies any required purpose.
1235
1267
  if (requiredEku && ekuPurposeFails(cert, requiredEku, checks)) failed = true;
1268
+ // Trust-anchor constraint contract (NSS / CCADB metadata; gated so a bare
1269
+ // anchor or an absent checkPurpose is byte-identical to today). The anchor's
1270
+ // per-purpose distrust-after date and delegator purposes apply to the
1271
+ // end-entity leaf it ultimately certifies.
1272
+ var ta = opts.trustAnchor;
1273
+ if (checkPurpose && ta.distrustAfter && ta.distrustAfter[checkPurpose] instanceof Date &&
1274
+ cert.validity.notBefore > ta.distrustAfter[checkPurpose]) {
1275
+ // STRICTLY > : a leaf whose notBefore == the distrust date stays trusted
1276
+ // (Mozilla certverifier isDistrustedCertificateChain: endEntityNotBefore
1277
+ // <= distrustAfterTime -> not distrusted; the end-of-day ...235959Z
1278
+ // convention keeps the whole boundary day trusted).
1279
+ checks.push({ name: "distrustAfter", ok: false, code: "path/distrusted-after" }); failed = true;
1280
+ }
1281
+ if (checkPurpose && ta.purposes && ta.purposes[checkPurpose] !== true) {
1282
+ checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
1283
+ }
1236
1284
  updateWorkingKey(state, cert); // 6.1.5(c),(d) -- key AND algorithm AND parameters
1237
1285
  }
1238
1286
 
@@ -1330,11 +1378,24 @@ var IDP_SCHEMA = schema.seq([
1330
1378
  function decodeIdp(ext) {
1331
1379
  // Surface the scope flags the checker gates on. ANY structural or value fault
1332
1380
  // -- non-SEQUENCE, unknown/duplicate/out-of-order field tag, a non-DER BOOLEAN,
1333
- // an encoded-FALSE default -- leaves the CRL's scope unknown: the CRL is
1334
- // unusable, never assumed unrestricted.
1335
- var out = { hasDistributionPoint: false, onlyUser: false, onlyCa: false, onlySomeReasons: null, indirect: false, onlyAttr: false, malformed: false };
1381
+ // an encoded-FALSE default, a malformed DistributionPointName -- leaves the
1382
+ // CRL's scope unknown: the CRL is unusable, never assumed unrestricted.
1383
+ var out = { hasDistributionPoint: false, distributionPoint: null, onlyUser: false, onlyCa: false, onlySomeReasons: null, indirect: false, onlyAttr: false, malformed: false };
1336
1384
  var m;
1337
- try { m = schema.walk(IDP_SCHEMA, asn1.decode(ext.value), NS); }
1385
+ try {
1386
+ m = schema.walk(IDP_SCHEMA, asn1.decode(ext.value), NS);
1387
+ if (m.fields.distributionPoint.present) {
1388
+ // distributionPoint [0] EXPLICIT-wraps the DistributionPointName CHOICE
1389
+ // (a context tag on a CHOICE-typed field is always EXPLICIT). The decoded
1390
+ // name feeds the sec. 6.3.3(b)(2)(i) correspondence against the
1391
+ // certificate's own DistributionPoints in the checker below.
1392
+ var dpnWrap = m.fields.distributionPoint.node;
1393
+ if (!dpnWrap.children || dpnWrap.children.length !== 1) {
1394
+ throw E("path/bad-idp", "IssuingDistributionPoint distributionPoint [0] must wrap exactly one DistributionPointName");
1395
+ }
1396
+ out.distributionPoint = pkix.distributionPointName(NS, dpnWrap.children[0], "path/bad-idp");
1397
+ }
1398
+ }
1338
1399
  catch (_e) { out.malformed = true; return out; }
1339
1400
  function flag(f) {
1340
1401
  if (!f.present) return false;
@@ -1353,6 +1414,58 @@ function decodeIdp(ext) {
1353
1414
  return out;
1354
1415
  }
1355
1416
 
1417
+ // RFC 5280 sec. 6.3.3(b)(2)(i): find the certificate DistributionPoint that
1418
+ // CORRESPONDS to a shard CRL's IDP distribution point name -- at least one
1419
+ // name in common between the two DistributionPointNames, compared by
1420
+ // BYTE-IDENTICAL DER encoding (Buffer.equals on the raw GeneralName / RDN
1421
+ // TLVs). sec. 5.2.5 pins the comparison key: "The identical encoding MUST be
1422
+ // used in the distributionPoint fields of the certificate and the CRL" -- so
1423
+ // a canonicalized or semantic comparison (which would equate two
1424
+ // differently-encoded names) is forbidden, and whole-set equality (which
1425
+ // would falsely reject a legitimate multi-URI DP sharing one name) is
1426
+ // over-strict. Mixed forms -- fullName on one side, nameRelativeToCRLIssuer
1427
+ // on the other -- never correspond here: resolving an RDN fragment against
1428
+ // the issuer DN is not attempted (fail closed). Per sec. 6.3.3(b)(1), a DP
1429
+ // naming a cRLIssuer participates only when that cRLIssuer is the certificate
1430
+ // issuer itself: the checker only consults CRLs issued BY the certificate
1431
+ // issuer and rejects indirect CRLs, so a DP delegated to another CRL issuer
1432
+ // is out of play. Returns the matched DP (its `reasons` feeds the coarse
1433
+ // reason-mask rule) or null.
1434
+ function correspondingCertDp(idpDpn, certDPs, issuerRdns) {
1435
+ if (!idpDpn || !certDPs) return null;
1436
+ for (var i = 0; i < certDPs.length; i++) {
1437
+ var dp = certDPs[i];
1438
+ if (!dp.distributionPoint) continue;
1439
+ if (dp.cRLIssuer && !crlIssuerNamesIssuer(dp.cRLIssuer, issuerRdns)) continue;
1440
+ var cdpn = dp.distributionPoint;
1441
+ if (idpDpn.kind === "fullName" && cdpn.kind === "fullName") {
1442
+ for (var a = 0; a < idpDpn.names.length; a++) {
1443
+ for (var c = 0; c < cdpn.names.length; c++) {
1444
+ if (idpDpn.names[a].equals(cdpn.names[c])) return dp;
1445
+ }
1446
+ }
1447
+ } else if (idpDpn.kind === "rdn" && cdpn.kind === "rdn") {
1448
+ if (idpDpn.bytes.equals(cdpn.bytes)) return dp;
1449
+ }
1450
+ }
1451
+ return null;
1452
+ }
1453
+
1454
+ // Does a DistributionPoint's cRLIssuer name the certificate issuer? True iff
1455
+ // one of its GeneralNames is a directoryName equal to the issuer DN under the
1456
+ // RFC 5280 sec. 7.1 comparison (the shared name guard, via this file's dnEqual
1457
+ // wrapper). Any fault -- a DN the comparison rejects for an embedded control
1458
+ // byte -- resolves false: the DP stays out of the correspondence (fail closed).
1459
+ function crlIssuerNamesIssuer(cRLIssuer, issuerRdns) {
1460
+ for (var i = 0; i < cRLIssuer.names.length; i++) {
1461
+ var n = cRLIssuer.names[i];
1462
+ if (n.tagNumber !== 4 || !n.value || !n.value.rdns) continue;
1463
+ try { if (dnEqual(n.value.rdns, issuerRdns)) return true; }
1464
+ catch (_e) { return false; }
1465
+ }
1466
+ return false;
1467
+ }
1468
+
1356
1469
  /**
1357
1470
  * @primitive pki.path.crlChecker
1358
1471
  * @signature pki.path.crlChecker(crls) -> RevocationChecker
@@ -1367,7 +1480,12 @@ function decodeIdp(ext) {
1367
1480
  * verifies the CRL signature over its `tbsBytes`, honors the issuing
1368
1481
  * distribution point scope and reason coverage, checks currency
1369
1482
  * (`thisUpdate`/`nextUpdate`), and reports `{ status: "good"|"revoked"|
1370
- * "unknown" }`. An out-of-scope, stale, unauthorized, or unverifiable CRL
1483
+ * "unknown" }`. A partitioned/sharded CRL (a critical IDP naming a
1484
+ * distribution point) establishes "good" when it corresponds to one of the
1485
+ * certificate's own cRLDistributionPoints -- at least one identically-encoded
1486
+ * name in common (RFC 5280 sec. 6.3.3) -- and neither side restricts reason
1487
+ * codes; a non-corresponding or reason-restricted shard is consulted for
1488
+ * revocation only. An out-of-scope, stale, unauthorized, or unverifiable CRL
1371
1489
  * yields `unknown`, which the validator fails closed unless `softFail` is set.
1372
1490
  *
1373
1491
  * @example
@@ -1395,6 +1513,18 @@ function crlChecker(crls) {
1395
1513
  } catch (e) {
1396
1514
  certScopeFault = pathCode(e, "path/bad-extension-value");
1397
1515
  }
1516
+ // The certificate's own cRLDistributionPoints, decoded once per check:
1517
+ // the sec. 6.3.3(b)(2)(i) correspondence gate below needs the DP names.
1518
+ // A decode fault leaves certDPs null -- every DP-scoped shard then stays
1519
+ // revocation-only (no correspondence can be shown; fail closed) while
1520
+ // the full-CRL path is unaffected (mirrors the basicConstraints
1521
+ // handling above); the check never crashes on a malformed extension.
1522
+ var certDPs = null;
1523
+ var certCdpExt = findExt(cert, OID.cRLDistributionPoints);
1524
+ if (certCdpExt) {
1525
+ try { certDPs = EXT_DECODERS[OID.cRLDistributionPoints](certCdpExt.value); }
1526
+ catch (_e) { certDPs = null; }
1527
+ }
1398
1528
  // RFC 5280 sec. 6.3.3(f): IF a keyUsage extension is present in the CRL issuer's
1399
1529
  // certificate, the cRLSign bit must be VERIFIED set. An issuer that OMITS
1400
1530
  // keyUsage is unconstrained (the same rule the sec. 6.1.4(n) keyCertSign gate
@@ -1489,7 +1619,34 @@ function crlChecker(crls) {
1489
1619
  if (idp.onlyAttr) continue; // scoped to attribute certs, not this public-key cert
1490
1620
  if (idp.onlyCa && certIsCa !== true) continue; // out of scope (or CA-ness undeterminable)
1491
1621
  if (idp.onlyUser && certIsCa !== false) continue;
1492
- if (idp.hasDistributionPoint || idp.onlySomeReasons) scopeRevocationOnly = true;
1622
+ if (idp.hasDistributionPoint) {
1623
+ // RFC 5280 sec. 6.3.3(b)(2)(i): a partition shard speaks for this
1624
+ // certificate only when the IDP's distribution point shares at
1625
+ // least one IDENTICALLY-ENCODED name with one of the certificate's
1626
+ // own DistributionPoints (sec. 5.2.5: "The identical encoding MUST
1627
+ // be used in the distributionPoint fields of the certificate and
1628
+ // the CRL"). The IDP must also be CRITICAL to be relied on for
1629
+ // scope: sec. 5.2.5 defines the IDP as "a critical CRL extension"
1630
+ // (descriptive phrasing, not an imperative MUST), and a partition
1631
+ // scope a non-supporting relying party would ignore is not a scope
1632
+ // to build "good" on -- a deliberate fail-closed decision. A
1633
+ // non-corresponding shard cannot establish "good" but is still
1634
+ // consulted for revocation below: serials are unique per issuer,
1635
+ // so a listed serial is a genuine revocation (fail closed toward
1636
+ // revoked).
1637
+ var matchedDp = idpExt.critical === true
1638
+ ? correspondingCertDp(idp.distributionPoint, certDPs, cert.issuer.rdns)
1639
+ : null;
1640
+ if (!matchedDp) scopeRevocationOnly = true;
1641
+ // sec. 6.3.3(d)(3): a matched DP carrying `reasons` bounds the
1642
+ // interim reason mask below all-reasons -- under the coarse rule
1643
+ // ("good" only at the (d)(4) all-reasons case) that shard is
1644
+ // revocation-only.
1645
+ else if (matchedDp.reasons) scopeRevocationOnly = true;
1646
+ }
1647
+ // sec. 6.3.3(d)(1)/(d)(2): any onlySomeReasons restriction keeps the
1648
+ // interim reason mask below all-reasons -- revocation-only (coarse).
1649
+ if (idp.onlySomeReasons) scopeRevocationOnly = true;
1493
1650
  }
1494
1651
  if (theCrl.thisUpdate > time) continue; // not yet valid
1495
1652
  // A CRL with no nextUpdate has no bounded validity -- its currency
@@ -1582,7 +1739,7 @@ function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
1582
1739
  }
1583
1740
 
1584
1741
  function verifyCrlSignature(theCrl, issuer) {
1585
- if (theCrl.signatureValue.unusedBits !== 0) return Promise.resolve(false); // non-octet-aligned signature
1742
+ if (!guard.crypto.isOctetAligned(theCrl.signatureValue)) return Promise.resolve(false); // non-octet-aligned signature
1586
1743
  return _verifyWithSpki(theCrl.signatureAlgorithm, theCrl.signatureValue.bytes, issuer.workingPublicKey, theCrl.tbsBytes);
1587
1744
  }
1588
1745
 
@@ -1703,7 +1860,7 @@ async function ocspAuthorizeResponder(basicResponse, cert, issuer, issuerKeyBits
1703
1860
  try { issuedByCa = dnEqual(rc.issuer.rdns, cert.issuer.rdns); }
1704
1861
  catch (_e) { continue; }
1705
1862
  if (!issuedByCa) continue;
1706
- if (rc.signatureValue.unusedBits !== 0) continue;
1863
+ if (!guard.crypto.isOctetAligned(rc.signatureValue)) continue;
1707
1864
  if (!(await _verifyWithSpki(rc.signatureAlgorithm, rc.signatureValue.bytes, issuer.workingPublicKey, rc.tbsBytes))) continue;
1708
1865
  // The delegate certificate MUST itself be valid at the validation instant.
1709
1866
  if (time < rc.validity.notBefore || time > rc.validity.notAfter) continue;
@@ -37,6 +37,7 @@ var asn1 = require("./asn1-der");
37
37
  var schema = require("./schema-engine");
38
38
  var pkix = require("./schema-pkix");
39
39
  var oid = require("./oid");
40
+ var guard = require("./guard-all");
40
41
  var frameworkError = require("./framework-error");
41
42
 
42
43
  var AttrCertError = frameworkError.AttrCertError;
@@ -119,6 +120,10 @@ var OBJECT_DIGEST_INFO = schema.seq([
119
120
  if (m.fields.otherObjectTypeID.present) {
120
121
  throw ctx.E("attrcert/bad-object-digest-info", "otherObjectTypeID is only valid with otherObjectTypes(2), which RFC 5755 sec. 7.3 forbids");
121
122
  }
123
+ // The objectDigest is a whole-octet digest over the identified object; a
124
+ // non-octet-aligned BIT STRING is malformed (RFC 5755 sec. 4.1) and has no
125
+ // in-tree verify layer to catch it later, so reject it at parse.
126
+ guard.crypto.assertOctetAligned(m.fields.objectDigest.value, ctx.E, "attrcert/bad-object-digest-info", "objectDigest");
122
127
  return {
123
128
  digestedObjectType: t,
124
129
  otherObjectTypeID: null,
package/lib/schema-cms.js CHANGED
@@ -214,7 +214,13 @@ function _assertContentTypeMatchesAttrs(attrs, eContentType) {
214
214
  for (var i = 0; i < attrs.length; i++) {
215
215
  if (attrs[i].type !== OID_CONTENT_TYPE) continue;
216
216
  if (attrs[i].values.length !== 1) throw NS.E("cms/bad-content-type-attr", "the content-type attribute must be single-valued (RFC 5652 sec. 11.1)");
217
- var ctv = asn1.read.oid(asn1.decode(attrs[i].values[0]));
217
+ // ContentType ::= OBJECT IDENTIFIER -- validate the value's full syntax (tag AND
218
+ // minimal base-128 OID content) with the CMS typed verdict, so a malformed value
219
+ // on a path that does not run _checkContentBindingAttrs (AuthEnvelopedData,
220
+ // RFC 5083 sec. 2.1) surfaces cms/bad-content-type-attr, not the raw asn1/* error.
221
+ var ctv;
222
+ try { ctv = asn1.read.oid(asn1.decode(attrs[i].values[0])); }
223
+ catch (e) { throw NS.E("cms/bad-content-type-attr", "the content-type attribute value must be a valid OBJECT IDENTIFIER", e); }
218
224
  if (ctv !== eContentType) throw NS.E("cms/content-type-mismatch", "the content-type attribute (" + ctv + ") must equal the eContentType (" + eContentType + ") (RFC 5652 sec. 5.3)");
219
225
  }
220
226
  }
@@ -1155,6 +1161,20 @@ function walkEnvelopedData(node) { return schema.walk(ENVELOPED_DATA, node, NS).
1155
1161
  function walkSignedData(node) { return schema.walk(SIGNED_DATA, node, NS).result; }
1156
1162
  function walkEncryptedData(node) { return schema.walk(ENCRYPTED_DATA, node, NS).result; }
1157
1163
 
1164
+ // assertAttachedCiphertext(eci, E, code, label): reject an EnvelopedData /
1165
+ // EncryptedData encryptedContentInfo whose encryptedContent (the payload itself)
1166
+ // is absent OR zero-length. RFC 5652 sec. 6.1 / RFC 5083 permit DETACHED content,
1167
+ // so this is NOT enforced in the base walker -- it is a per-PROFILE opt-in for a
1168
+ // consumer that requires the ciphertext to be present (an EST server-generated
1169
+ // key, a PKCS#12 shrouded bag): a valid envelope that delivers NOTHING is
1170
+ // verification-of-nothing (CWE-20). E is the caller's (code, message) factory.
1171
+ function assertAttachedCiphertext(eci, E, code, label) {
1172
+ if (!eci || eci.encryptedContent === null || eci.encryptedContent.length === 0) {
1173
+ throw E(code, (label || "encrypted content") + " must carry a non-empty attached ciphertext (RFC 5652 sec. 6.1), not be detached or empty");
1174
+ }
1175
+ return eci;
1176
+ }
1177
+
1158
1178
  module.exports = {
1159
1179
  parse: parse,
1160
1180
  pemDecode: pemDecode,
@@ -1163,4 +1183,5 @@ module.exports = {
1163
1183
  walkEnvelopedData: walkEnvelopedData,
1164
1184
  walkSignedData: walkSignedData,
1165
1185
  walkEncryptedData: walkEncryptedData,
1186
+ assertAttachedCiphertext: assertAttachedCiphertext,
1166
1187
  };
@@ -255,11 +255,9 @@ function popoPrivKey(type) {
255
255
  }
256
256
  // The encrypted key material MUST be present -- CMS allows a detached
257
257
  // EnvelopedData (encryptedContent OPTIONAL), but a POP with no key to verify
258
- // or archive is meaningless. A zero-length attached ciphertext is the same
259
- // fault as a detached one (the rule the CMP encryptedKey arm also holds).
260
- if (env.encryptedContentInfo.encryptedContent == null || env.encryptedContentInfo.encryptedContent.length === 0) {
261
- throw ctx.E("crmf/bad-popo", "encryptedKey [4] EnvelopedData MUST carry a non-empty encrypted key in encryptedContent (RFC 4211 sec. 4.2)");
262
- }
258
+ // or archive is meaningless. The shared CMS assert rejects a null OR
259
+ // zero-length attached ciphertext (the rule the EST / PKCS#12 siblings hold).
260
+ cms.assertAttachedCiphertext(env.encryptedContentInfo, ctx.E, "crmf/bad-popo", "encryptedKey [4] EnvelopedData");
263
261
  }
264
262
  return { type: type, method: POPOPRIVKEY_METHODS[inner.tagNumber], bytes: n.bytes };
265
263
  });
@@ -37,6 +37,7 @@ var asn1 = require("./asn1-der");
37
37
  var schema = require("./schema-engine");
38
38
  var pkix = require("./schema-pkix");
39
39
  var oid = require("./oid");
40
+ var constants = require("./constants");
40
41
  var frameworkError = require("./framework-error");
41
42
 
42
43
  var OcspError = frameworkError.OcspError;
@@ -119,7 +120,10 @@ function _rawSignature(field) {
119
120
 
120
121
  // certs [0] EXPLICIT SEQUENCE OF Certificate -- each element raw. Shared by the
121
122
  // request Signature and the BasicOCSPResponse.
122
- var CERTS = schema.seqOf(certificateBytes(), { assert: "sequence", code: "ocsp/bad-certs", what: "certs" });
123
+ // The certs list is capped: the delegated-responder authorization loop verifies
124
+ // each candidate BEFORE the response signature is checked, so an unbounded list in
125
+ // a relayed / stapled response would drive unbounded pre-auth signature verifies.
126
+ var CERTS = schema.seqOf(certificateBytes(), { assert: "sequence", code: "ocsp/bad-certs", what: "certs", max: constants.LIMITS.OCSP_MAX_CERTS, maxCode: "ocsp/too-many-certs" });
123
127
 
124
128
  // Validate the OCSP protocol extension values in a decoded extension list --
125
129
  // applied to every extension surface (request / single-request / response /
@@ -47,11 +47,8 @@ function pemDecode(text, label, PemError) {
47
47
  if (!m) throw new PemError("pem/no-block", "no PEM block found");
48
48
  if (label && m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + JSON.stringify(label) + " block, got " + JSON.stringify(m[1]));
49
49
  var b64 = m[2].replace(/[\r\n\t ]+/g, "");
50
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64)) throw new PemError("pem/bad-base64", "PEM body is not valid base64");
51
- if (b64.length % 4 !== 0) throw new PemError("pem/bad-base64", "PEM base64 body must be whole 4-character groups (RFC 4648 sec. 3.5)");
52
- var der = Buffer.from(b64, "base64");
53
- if (der.toString("base64") !== b64) throw new PemError("pem/bad-base64", "PEM base64 body is not canonical (RFC 4648 sec. 3.5)");
54
- return der;
50
+ // Strict canonical base64 (RFC 4648 sec. 3.5) via the shared encoding guard.
51
+ return guard.encoding.base64(b64, null, function (c, msg) { return new PemError(c, msg); }, "pem/bad-base64", "PEM base64 body");
55
52
  }
56
53
 
57
54
  // pemDecodeAll(text, label, PemError): decode EVERY PEM block under the STRICT
@@ -72,10 +69,7 @@ function pemDecodeAll(text, label, PemError) {
72
69
  if (/\S/.test(text.slice(lastEnd, m.index))) throw new PemError("pem/explanatory-text", "explanatory text is not permitted around PEM blocks (RFC 8555 sec. 9.1)");
73
70
  if (m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + JSON.stringify(label) + " block, got " + JSON.stringify(m[1]));
74
71
  var b64 = m[2].replace(/[\r\n\t ]+/g, "");
75
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64) || b64.length % 4 !== 0) throw new PemError("pem/bad-base64", "PEM body is not canonical base64 (RFC 4648 sec. 3.5)");
76
- var der = Buffer.from(b64, "base64");
77
- if (der.toString("base64") !== b64) throw new PemError("pem/bad-base64", "PEM base64 body is not canonical (RFC 4648 sec. 3.5)");
78
- blocks.push(der);
72
+ blocks.push(guard.encoding.base64(b64, null, function (c, msg) { return new PemError(c, msg); }, "pem/bad-base64", "PEM base64 body"));
79
73
  lastEnd = re.lastIndex;
80
74
  }
81
75
  if (blocks.length === 0) throw new PemError("pem/no-block", "no PEM block found");
@@ -92,7 +86,10 @@ function pemEncode(der, label, PemError) {
92
86
  if (typeof label !== "string" || !/^[A-Z0-9]+( [A-Z0-9]+)*$/.test(label)) {
93
87
  throw new PemError("pem/bad-label", "pemEncode requires an uppercase A-Z0-9 label with single spaces");
94
88
  }
95
- var buf = Buffer.isBuffer(der) ? der : Buffer.from(der);
89
+ // Re-view through the byte guard: a string input would silently utf8-armor into
90
+ // a bogus PEM, and a detached-backed Buffer would armor an empty body -- both
91
+ // fail closed (a Uint8Array / Buffer of real DER re-views cleanly).
92
+ var buf = guard.bytes.view(der, PemError, "pem/bad-input", "pemEncode DER input");
96
93
  var b64 = buf.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
97
94
  return "-----BEGIN " + label + "-----\n" + b64 + "\n-----END " + label + "-----\n";
98
95
  }
@@ -270,10 +267,12 @@ function attrValueToString(ns) {
270
267
  if (typeof value === "string" && value.charAt(0) === "\\") return asn1.build.utf8(value.slice(1));
271
268
  if (typeof value === "string" && value.charAt(0) === "#") {
272
269
  var hex = value.slice(1);
273
- if (!/^(?:[0-9A-Fa-f]{2})+$/.test(hex)) {
270
+ if (hex.length === 0) {
274
271
  throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must be a non-empty even run of hex digits (RFC 4514 sec. 2.4)");
275
272
  }
276
- var raw = Buffer.from(hex, "hex");
273
+ // Strict canonical hex via the shared encoding guard (even-length, canonical,
274
+ // no silent truncation at the first non-hex character).
275
+ var raw = guard.encoding.hex(hex, null, ns.E, ns.prefix + "/bad-atv", "a #hex attribute value");
277
276
  try { asn1.decode(raw); }
278
277
  catch (e) { throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must encode exactly one DER TLV", e); }
279
278
  return raw;
@@ -550,6 +549,36 @@ function generalNames(ns, opts) {
550
549
  return schema.seqOf(gn, { assert: "sequence", min: 1, code: code, what: opts.what || "GeneralNames", build: build });
551
550
  }
552
551
 
552
+ // DistributionPointName ::= CHOICE { fullName [0] IMPLICIT GeneralNames,
553
+ // nameRelativeToCRLIssuer [1] IMPLICIT RelativeDistinguishedName } (RFC 5280
554
+ // sec. 4.2.1.13). The caller hands the CHOICE node itself -- the alternative
555
+ // INSIDE the [0]-tagged distributionPoint field wrapper (a context tag on a
556
+ // CHOICE-typed field is always EXPLICIT, so the wire nests [0]{ [0]|[1] ... }).
557
+ // fullName surfaces each element's RAW GeneralName DER as { kind: "fullName",
558
+ // names: [Buffer, ...] } -- the byte-exact comparison key the sec. 5.2.5
559
+ // "identical encoding MUST be used" correspondence rule requires;
560
+ // nameRelativeToCRLIssuer surfaces its full [1]-tagged TLV as { kind: "rdn",
561
+ // bytes: Buffer } (byte-identical [1] TLVs are byte-identical RDN fragments).
562
+ // An empty fullName (GeneralNames is SIZE 1..MAX), a malformed element, or any
563
+ // other alternative throws the caller's `code`. Shared by the
564
+ // cRLDistributionPoints / freshestCRL extension decoders and the CRL checker's
565
+ // IssuingDistributionPoint decode, so the two sides of the sec. 6.3.3(b)(2)(i)
566
+ // correspondence comparison cannot drift.
567
+ function distributionPointName(ns, node, code) {
568
+ if (node && node.tagClass === "context" && node.tagNumber === 0) {
569
+ var gns = schema.walk(generalNames(ns, { implicitTag: 0, code: code, what: "DistributionPointName fullName" }), node, ns).result;
570
+ return { kind: "fullName", names: gns.names.map(function (n) { return n.bytes; }) };
571
+ }
572
+ if (node && node.tagClass === "context" && node.tagNumber === 1) {
573
+ // [1] IMPLICIT RelativeDistinguishedName -- the context tag replaces the
574
+ // SET tag, so the node's direct children are the AttributeTypeAndValues.
575
+ schema.walk(schema.implicitSetOf(1, attributeTypeAndValue(ns), {
576
+ min: 1, code: code, what: "DistributionPointName nameRelativeToCRLIssuer" }), node, ns);
577
+ return { kind: "rdn", bytes: node.bytes };
578
+ }
579
+ throw ns.E(code, "DistributionPointName must be fullName [0] or nameRelativeToCRLIssuer [1] (RFC 5280 sec. 4.2.1.13)");
580
+ }
581
+
553
582
  // certExtensionDecoders(ns) -- the ns-parameterized RFC 5280 sec. 4.2.1 extension
554
583
  // VALUE decoders. `x509.parse` surfaces each extension as { oid, name, critical,
555
584
  // value } with `value` the raw inner OCTET-STRING content (a Buffer); the path
@@ -837,6 +866,54 @@ function certExtensionDecoders(ns) {
837
866
  return { poison: true };
838
867
  }
839
868
 
869
+ // cRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
870
+ // (RFC 5280 sec. 4.2.1.13); DistributionPoint ::= SEQUENCE {
871
+ // distributionPoint [0] DistributionPointName OPTIONAL, reasons [1]
872
+ // ReasonFlags OPTIONAL, cRLIssuer [2] GeneralNames OPTIONAL }. "a
873
+ // DistributionPoint MUST NOT consist of only the reasons field; either
874
+ // distributionPoint or cRLIssuer MUST be present" -- a reasons-only (or
875
+ // empty) DistributionPoint is malformed. The DistributionPointName is
876
+ // surfaced through the shared helper (raw name encodings -- the sec. 5.2.5
877
+ // correspondence key); `reasons` stays a raw BIT STRING ({ unusedBits,
878
+ // bytes }); cRLIssuer is a validated GeneralNames with decoded values.
879
+ // FreshestCRL ::= CRLDistributionPoints (sec. 4.2.1.15), so the SAME decoder
880
+ // serves both OIDs -- one codec, both registry rows.
881
+ function crlDistributionPoints(buf) {
882
+ var C = ns.prefix + "/bad-crl-distribution-points";
883
+ var kids = seqChildren(buf, C, "CRLDistributionPoints");
884
+ if (kids.length < 1) throw ns.E(C, "CRLDistributionPoints must contain at least one DistributionPoint (RFC 5280 sec. 4.2.1.13)");
885
+ return kids.map(function (dp) {
886
+ if (dp.tagClass !== "universal" || dp.tagNumber !== _T.SEQUENCE || !dp.children) {
887
+ throw ns.E(C, "DistributionPoint must be a SEQUENCE { distributionPoint?, reasons?, cRLIssuer? }");
888
+ }
889
+ var out = { distributionPoint: null, reasons: null, cRLIssuer: null };
890
+ var lastTag = -1;
891
+ dp.children.forEach(function (f) {
892
+ if (f.tagClass !== "context") throw ns.E(C, "DistributionPoint fields are context-tagged [0]/[1]/[2]");
893
+ if (f.tagNumber <= lastTag) throw ns.E(C, "DistributionPoint fields must be unique and in ascending order (DER)");
894
+ lastTag = f.tagNumber;
895
+ if (f.tagNumber === 0) {
896
+ // distributionPoint [0] EXPLICIT-wraps the DistributionPointName CHOICE.
897
+ if (!f.children || f.children.length !== 1) throw ns.E(C, "DistributionPoint distributionPoint [0] must wrap exactly one DistributionPointName");
898
+ out.distributionPoint = distributionPointName(ns, f.children[0], C);
899
+ } else if (f.tagNumber === 1) {
900
+ var bs;
901
+ try { bs = asn1.read.bitStringImplicit(f, 1); }
902
+ catch (e) { throw ns.E(C, "DistributionPoint reasons [1] must be an IMPLICIT ReasonFlags BIT STRING", e); }
903
+ out.reasons = { unusedBits: bs.unusedBits, bytes: bs.bytes };
904
+ } else if (f.tagNumber === 2) {
905
+ out.cRLIssuer = schema.walk(generalNames(ns, { implicitTag: 2, decodeValue: true, code: C, what: "DistributionPoint cRLIssuer" }), f, ns).result;
906
+ } else {
907
+ throw ns.E(C, "DistributionPoint has an unexpected field [" + f.tagNumber + "]");
908
+ }
909
+ });
910
+ if (out.distributionPoint === null && out.cRLIssuer === null) {
911
+ throw ns.E(C, "a DistributionPoint must include distributionPoint or cRLIssuer -- it MUST NOT consist of only the reasons field (RFC 5280 sec. 4.2.1.13)");
912
+ }
913
+ return out;
914
+ });
915
+ }
916
+
840
917
  // byName returns undefined for an unregistered name; keying a decoder row
841
918
  // under "undefined" would silently drop that extension from dispatch (the
842
919
  // consumer would treat the extension as structurally unvalidated / absent),
@@ -861,6 +938,8 @@ function certExtensionDecoders(ns) {
861
938
  byOid[O("subjectKeyIdentifier")] = subjectKeyIdentifier;
862
939
  byOid[O("signedCertificateTimestampList")] = sctList;
863
940
  byOid[O("precertificatePoison")] = precertPoison;
941
+ byOid[O("cRLDistributionPoints")] = crlDistributionPoints;
942
+ byOid[O("freshestCRL")] = crlDistributionPoints;
864
943
  return { byOid: byOid };
865
944
  }
866
945
 
@@ -1069,6 +1148,7 @@ module.exports = {
1069
1148
  name: name,
1070
1149
  generalName: generalName,
1071
1150
  generalNames: generalNames,
1151
+ distributionPointName: distributionPointName,
1072
1152
  generalizedTime: generalizedTime,
1073
1153
  utf8Text: utf8Text,
1074
1154
  rawNonEmptySequence: rawNonEmptySequence,