@blamejs/pki 0.1.32 → 0.2.1
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 +38 -0
- package/README.md +3 -1
- package/index.js +13 -0
- package/lib/asn1-der.js +31 -22
- package/lib/constants.js +33 -0
- package/lib/ct.js +13 -12
- package/lib/est.js +10 -10
- package/lib/framework-error.js +13 -0
- package/lib/guard-all.js +15 -0
- package/lib/guard-crypto.js +31 -1
- package/lib/guard-encoding.js +90 -0
- package/lib/guard-identifier.js +56 -0
- package/lib/guard-json.js +149 -0
- package/lib/guard-limits.js +58 -8
- package/lib/guard-range.js +40 -7
- package/lib/guard-text.js +7 -0
- package/lib/jose.js +17 -128
- package/lib/merkle.js +7 -16
- package/lib/oid.js +33 -10
- package/lib/path-validate.js +173 -16
- package/lib/schema-attrcert.js +5 -0
- package/lib/schema-cms.js +22 -1
- package/lib/schema-crmf.js +3 -5
- package/lib/schema-ocsp.js +5 -1
- package/lib/schema-pkix.js +92 -12
- package/lib/shbs.js +348 -0
- package/lib/trust.js +707 -0
- package/lib/webcrypto.js +18 -7
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/oid.js
CHANGED
|
@@ -39,6 +39,10 @@ var guard = require("./guard-all");
|
|
|
39
39
|
|
|
40
40
|
var OidError = frameworkError.OidError;
|
|
41
41
|
|
|
42
|
+
// (code, message) -> OidError, the factory shape the composed guards throw
|
|
43
|
+
// through so a malformed identifier keeps the oid/* typed verdict.
|
|
44
|
+
function _oidError(c, m) { return new OidError(c, m); }
|
|
45
|
+
|
|
42
46
|
// FAMILIES -- OIDs grouped by their shared base arc (the "similar starting
|
|
43
47
|
// variable" that defines a class). A member is `name: leaf`, where leaf is a
|
|
44
48
|
// trailing arc (number) or a short arc array for a multi-level leaf; the full
|
|
@@ -197,7 +201,14 @@ var FAMILIES = {
|
|
|
197
201
|
// CEK-HKDF content-encryption wrapper (RFC 9709) a KEMRecipientInfo names, plus
|
|
198
202
|
// the RSA-KEM SPKI algorithm (RFC 9690). Parameters are absent for the HKDFs.
|
|
199
203
|
smimeAlg: { base: [1, 2, 840, 113549, 1, 9, 16, 3], of: {
|
|
200
|
-
"id-rsa-kem": 14,
|
|
204
|
+
"id-rsa-kem": 14, "id-alg-hss-lms-hashsig": 17,
|
|
205
|
+
hkdfWithSha256: 28, hkdfWithSha384: 29, hkdfWithSha512: 30, cekHkdfSha256: 31 } },
|
|
206
|
+
|
|
207
|
+
// PKIX algorithms arc -- the stateful hash-based signature algorithm
|
|
208
|
+
// identifiers (RFC 9802 sec. 4). HSS/LMS additionally has the SMIME
|
|
209
|
+
// id-alg-hss-lms-hashsig OID above (RFC 9708 / RFC 9802 share it).
|
|
210
|
+
pkixAlg: { base: [1, 3, 6, 1, 5, 5, 7, 6], of: {
|
|
211
|
+
"id-alg-xmss-hashsig": 34, "id-alg-xmssmt-hashsig": 35 } },
|
|
201
212
|
|
|
202
213
|
// RSA-KEM key-transport algorithm (RFC 9690, obsoletes RFC 5990) on the ISO
|
|
203
214
|
// 18033-2 arc -- the kem OID an RSA KEMRecipientInfo carries (distinct from the
|
|
@@ -302,11 +313,18 @@ Object.keys(FAMILIES).forEach(function (fam) {
|
|
|
302
313
|
});
|
|
303
314
|
|
|
304
315
|
function _assertDotted(dotted, who) {
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
316
|
+
// SYNTAX only (canonical dotted form, no leading-zero arc that would round-trip
|
|
317
|
+
// to a DIFFERENT OID). For the LOOKUP entry points (name / has): a well-formed
|
|
318
|
+
// but non-encodable OID is simply not registered (a miss), not an error, so the
|
|
319
|
+
// X.660 arc bounds are waived (boundsCode null).
|
|
320
|
+
guard.identifier.assertCanonicalOid(dotted, _oidError, "oid/bad-input", who, null);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function _assertEncodable(dotted, who) {
|
|
324
|
+
// SYNTAX and the X.660 arc bounds -- for the paths that assert / convert a real
|
|
325
|
+
// encodable OID (toArcs / toDER / register), where an out-of-bounds arc is a
|
|
326
|
+
// hard reject (oid/bad-arc), so the string form agrees with the DER form.
|
|
327
|
+
guard.identifier.assertCanonicalOid(dotted, _oidError, "oid/bad-input", who, "oid/bad-arc");
|
|
310
328
|
}
|
|
311
329
|
|
|
312
330
|
// X.660 encodability: the root arc is 0..2 and, under roots 0 and 1, the
|
|
@@ -370,9 +388,11 @@ function has(dotted) {
|
|
|
370
388
|
* pki.oid.register("1.3.6.1.4.1.99999.1", "acmeWidgetPolicy");
|
|
371
389
|
*/
|
|
372
390
|
function register(dotted, n) {
|
|
373
|
-
_assertDotted
|
|
391
|
+
// _assertDotted now enforces the X.660 arc bounds on the string form too, so
|
|
392
|
+
// the separate arc-based check register used to run is redundant here (it is
|
|
393
|
+
// kept for registerFamily, which validates arcs it assembles, not a string).
|
|
394
|
+
_assertEncodable(dotted, "register");
|
|
374
395
|
if (typeof n !== "string" || n.length === 0) throw new OidError("oid/bad-input", "register: name must be a non-empty string");
|
|
375
|
-
_assertEncodableArcs(dotted.split(".").map(BigInt), "register");
|
|
376
396
|
_index(dotted, n);
|
|
377
397
|
}
|
|
378
398
|
|
|
@@ -432,7 +452,7 @@ function all() {
|
|
|
432
452
|
// Number where safe and BigInt where an arc exceeds 2^53 (rare, but a
|
|
433
453
|
// UUID-based OID arc can), so the round-trip never loses precision.
|
|
434
454
|
function toArcs(dotted) {
|
|
435
|
-
|
|
455
|
+
_assertEncodable(dotted, "toArcs");
|
|
436
456
|
return dotted.split(".").map(function (p) {
|
|
437
457
|
var b = BigInt(p);
|
|
438
458
|
return b <= 9007199254740991n ? Number(b) : b;
|
|
@@ -453,7 +473,7 @@ function fromArcs(arcs) {
|
|
|
453
473
|
|
|
454
474
|
// DER convenience -- thin pass-throughs to the codec so callers reach for
|
|
455
475
|
// one namespace when they have a dotted string in hand.
|
|
456
|
-
function toDER(dotted) {
|
|
476
|
+
function toDER(dotted) { _assertEncodable(dotted, "toDER"); return asn1.build.oid(dotted); }
|
|
457
477
|
function fromDER(input) {
|
|
458
478
|
var buf = guard.bytes.view(input, OidError, "oid/bad-input", "fromDER");
|
|
459
479
|
var node = asn1.decode(buf);
|
|
@@ -488,6 +508,9 @@ var _PARAMS_ABSENT = new Set();
|
|
|
488
508
|
// HKDF key-derivation identifiers (RFC 8619 sec. 2: when any of these appear
|
|
489
509
|
// within AlgorithmIdentifier, the parameters component SHALL be absent).
|
|
490
510
|
"hkdfWithSha256", "hkdfWithSha384", "hkdfWithSha512",
|
|
511
|
+
// Stateful hash-based signatures (RFC 9802 sec. 4 / RFC 9708): the parameters
|
|
512
|
+
// field MUST be absent for HSS/LMS, XMSS, and XMSS^MT public keys and signatures.
|
|
513
|
+
"id-alg-hss-lms-hashsig", "id-alg-xmss-hashsig", "id-alg-xmssmt-hashsig",
|
|
491
514
|
].forEach(function (nm) {
|
|
492
515
|
var d = byName(nm);
|
|
493
516
|
// A seed-list typo must fail at module load -- admitting undefined would
|
package/lib/path-validate.js
CHANGED
|
@@ -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
|
|
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
|
|
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 (
|
|
1068
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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" }`.
|
|
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
|
|
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
|
|
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
|
|
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;
|
package/lib/schema-attrcert.js
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|
package/lib/schema-crmf.js
CHANGED
|
@@ -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.
|
|
259
|
-
//
|
|
260
|
-
|
|
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
|
});
|
package/lib/schema-ocsp.js
CHANGED
|
@@ -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
|
-
|
|
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 /
|