@blamejs/pki 0.1.15 → 0.1.16
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 +12 -0
- package/README.md +2 -1
- package/index.js +4 -0
- package/lib/constants.js +4 -0
- package/lib/framework-error.js +8 -0
- package/lib/oid.js +8 -3
- package/lib/path-validate.js +1393 -0
- package/lib/schema-pkix.js +316 -11
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/schema-pkix.js
CHANGED
|
@@ -224,11 +224,22 @@ function name(ns) {
|
|
|
224
224
|
// OCTET STRING; registeredID [8] is a primitive OBJECT IDENTIFIER — then surfaces the
|
|
225
225
|
// value RAW ({ bytes, tagClass, tagNumber }). `opts.code` is the caller's error code
|
|
226
226
|
// (e.g. ocsp/bad-requestor-name, tsp/bad-tsa). Shared so the two parsers cannot drift.
|
|
227
|
+
// opts.decodeValue (default false): in addition to the raw { bytes, tagClass,
|
|
228
|
+
// tagNumber }, surface the DECODED `value` per arm — IA5 text (string), an
|
|
229
|
+
// iPAddress Buffer, a directoryName { rdns, dn }, a registeredID OID string,
|
|
230
|
+
// or an otherName { typeId, valueBytes }. The path validator's name-constraint
|
|
231
|
+
// matcher needs the decoded value; the tsp/ocsp/attrcert consumers pass no
|
|
232
|
+
// flag and get the byte-identical raw-only shape.
|
|
233
|
+
// opts.subtreeBase (default false): the GeneralSubtree.base form (RFC 5280
|
|
234
|
+
// §4.2.1.10) — an iPAddress base is an address+mask (8 octets IPv4 / 32 IPv6),
|
|
235
|
+
// NOT the 4/16-octet SAN address form. Only the iPAddress size rule changes.
|
|
227
236
|
var GN_CONSTRUCTED = { 0: 1, 3: 1, 4: 1, 5: 1 };
|
|
228
237
|
var GN_IA5 = { 1: 1, 2: 1, 6: 1 };
|
|
229
238
|
function generalName(ns, opts) {
|
|
230
239
|
opts = opts || {};
|
|
231
240
|
var code = opts.code || (ns.prefix + "/bad-general-name");
|
|
241
|
+
var decodeValue = opts.decodeValue === true;
|
|
242
|
+
var subtreeBase = opts.subtreeBase === true;
|
|
232
243
|
var NAME = name(ns);
|
|
233
244
|
return schema.decode(function (n, ctx) {
|
|
234
245
|
if (n.tagClass !== "context" || n.tagNumber < 0 || n.tagNumber > 8) {
|
|
@@ -236,45 +247,66 @@ function generalName(ns, opts) {
|
|
|
236
247
|
}
|
|
237
248
|
var t = n.tagNumber;
|
|
238
249
|
var constructed = !!n.children;
|
|
250
|
+
var value;
|
|
239
251
|
if (GN_CONSTRUCTED[t]) {
|
|
240
252
|
if (!constructed || n.children.length < 1) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty constructed value (RFC 5280 §4.2.1.6)");
|
|
241
253
|
if (t === 0) {
|
|
242
254
|
// otherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY }.
|
|
243
255
|
if (n.children.length !== 2) throw ctx.E(code, "GeneralName otherName [0] must be a SEQUENCE { type-id, value [0] }");
|
|
244
|
-
|
|
256
|
+
var typeId;
|
|
257
|
+
try { typeId = asn1.read.oid(n.children[0]); }
|
|
245
258
|
catch (e) { throw ctx.E(code, "GeneralName otherName [0] must lead with a type-id OBJECT IDENTIFIER", e); }
|
|
246
|
-
var
|
|
247
|
-
if (!(
|
|
259
|
+
var ov = n.children[1];
|
|
260
|
+
if (!(ov.tagClass === "context" && ov.tagNumber === 0 && ov.children && ov.children.length === 1)) {
|
|
248
261
|
throw ctx.E(code, "GeneralName otherName [0] value must be a [0] EXPLICIT wrapper carrying exactly one value");
|
|
249
262
|
}
|
|
263
|
+
if (decodeValue) value = { typeId: typeId, valueBytes: ov.children[0].bytes };
|
|
250
264
|
} else if (t === 4) {
|
|
251
265
|
// directoryName [4] EXPLICIT Name — validate the wrapped RDNSequence.
|
|
252
266
|
if (n.children.length !== 1) throw ctx.E(code, "GeneralName directoryName [4] must wrap exactly one Name");
|
|
253
|
-
schema.walk(NAME, n.children[0], ctx);
|
|
267
|
+
var dnMatch = schema.walk(NAME, n.children[0], ctx);
|
|
268
|
+
if (decodeValue) value = dnMatch.result;
|
|
254
269
|
}
|
|
255
270
|
} else {
|
|
256
271
|
if (constructed) throw ctx.E(code, "GeneralName [" + t + "] must be primitive (X.690 §10.2)");
|
|
257
272
|
if (GN_IA5[t]) {
|
|
258
273
|
if (n.content.length === 0) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty IA5String");
|
|
259
274
|
for (var i = 0; i < n.content.length; i++) {
|
|
260
|
-
|
|
275
|
+
// 7-bit IA5, and no C0/DEL control byte — an embedded NUL/control in a
|
|
276
|
+
// dNSName/rfc822Name/URI enables a name-truncation/confusion bypass
|
|
277
|
+
// (CVE-2009-2408 class) downstream, so reject it at decode.
|
|
278
|
+
if (n.content[i] < 0x20 || n.content[i] > 0x7e) throw ctx.E(code, "GeneralName [" + t + "] must be a printable IA5String (no control bytes)");
|
|
261
279
|
}
|
|
280
|
+
if (decodeValue) value = n.content.toString("latin1");
|
|
262
281
|
} else if (t === 7) {
|
|
263
|
-
if (
|
|
282
|
+
if (subtreeBase) {
|
|
283
|
+
if (n.content.length !== 8 && n.content.length !== 32) throw ctx.E(code, "GeneralName iPAddress [7] subtree base must be an 8- or 32-octet address+mask (RFC 5280 §4.2.1.10)");
|
|
284
|
+
} else if (n.content.length !== 4 && n.content.length !== 16) {
|
|
285
|
+
throw ctx.E(code, "GeneralName iPAddress [7] must be a 4- or 16-octet address");
|
|
286
|
+
}
|
|
287
|
+
// n.content is proven non-null here (the primitive-form guard above
|
|
288
|
+
// threw on a constructed node); copy the octets out.
|
|
289
|
+
if (decodeValue) value = Buffer.concat([n.content]);
|
|
264
290
|
} else if (t === 8) {
|
|
265
|
-
|
|
291
|
+
var regId;
|
|
292
|
+
try { regId = asn1.decodeOidContent(n.content); }
|
|
266
293
|
catch (e) { throw ctx.E(code, "GeneralName registeredID [8] must be a valid OBJECT IDENTIFIER", e); }
|
|
294
|
+
if (decodeValue) value = regId;
|
|
267
295
|
}
|
|
268
296
|
}
|
|
269
|
-
|
|
297
|
+
var out = { bytes: n.bytes, tagClass: n.tagClass, tagNumber: n.tagNumber };
|
|
298
|
+
if (decodeValue) out.value = value === undefined ? null : value;
|
|
299
|
+
return out;
|
|
270
300
|
});
|
|
271
301
|
}
|
|
272
302
|
|
|
273
303
|
// GeneralNames ::= SEQUENCE OF GeneralName (RFC 5280 §4.2.1.6), SIZE (1..MAX). Every
|
|
274
304
|
// element is validated by generalName (its CHOICE alternative's form + content) and
|
|
275
305
|
// surfaced raw, so a caller carrying a GeneralNames field cannot accept a malformed
|
|
276
|
-
// element by treating the whole sequence as opaque bytes. Returns { names
|
|
277
|
-
//
|
|
306
|
+
// element by treating the whole sequence as opaque bytes. Returns { names, bytes }
|
|
307
|
+
// where each `names[i]` is the generalName leaf's return — { bytes, tagClass,
|
|
308
|
+
// tagNumber } and, when opts.decodeValue is set, the decoded `value` — and `bytes`
|
|
309
|
+
// is the raw outer DER.
|
|
278
310
|
// `opts.implicitTag` handles a [tag] IMPLICIT GeneralNames — the context tag REPLACES
|
|
279
311
|
// the universal SEQUENCE tag (RFC 5755 Holder.entityName [1]); otherwise it is a bare
|
|
280
312
|
// universal SEQUENCE OF. `opts.code` is the caller's error code. Shared so the x509 /
|
|
@@ -282,7 +314,9 @@ function generalName(ns, opts) {
|
|
|
282
314
|
function generalNames(ns, opts) {
|
|
283
315
|
opts = opts || {};
|
|
284
316
|
var code = opts.code || (ns.prefix + "/bad-general-names");
|
|
285
|
-
|
|
317
|
+
// decodeValue / subtreeBase thread through to each element (the path
|
|
318
|
+
// validator's constraint matcher passes decodeValue:true).
|
|
319
|
+
var gn = generalName(ns, { code: code, decodeValue: opts.decodeValue === true, subtreeBase: opts.subtreeBase === true });
|
|
286
320
|
function build(m) { return { names: m.items.map(function (it) { return it.value; }), bytes: m.node.bytes }; }
|
|
287
321
|
if (opts.implicitTag != null) {
|
|
288
322
|
return schema.implicitSeqOf(opts.implicitTag, gn, { min: 1, code: code, what: opts.what || "GeneralNames", build: build });
|
|
@@ -290,6 +324,276 @@ function generalNames(ns, opts) {
|
|
|
290
324
|
return schema.seqOf(gn, { assert: "sequence", min: 1, code: code, what: opts.what || "GeneralNames", build: build });
|
|
291
325
|
}
|
|
292
326
|
|
|
327
|
+
// certExtensionDecoders(ns) — the ns-parameterized RFC 5280 §4.2.1 extension
|
|
328
|
+
// VALUE decoders. `x509.parse` surfaces each extension as { oid, name, critical,
|
|
329
|
+
// value } with `value` the raw inner OCTET-STRING content (a Buffer); the path
|
|
330
|
+
// validator and the future complete-extension-set surface need the STRUCTURE
|
|
331
|
+
// inside. Each decoder takes that raw Buffer and returns the decoded value, or
|
|
332
|
+
// throws a typed `<prefix>/bad-*` code, mirroring the CRL's fail-closed
|
|
333
|
+
// `decodeExt` pattern (asn1.decode → read.* / schema.walk, wrapped). Returns
|
|
334
|
+
// { byOid } keyed by dotted OID so a consumer dispatches by ext.oid.
|
|
335
|
+
var _T = asn1.TAGS;
|
|
336
|
+
function certExtensionDecoders(ns) {
|
|
337
|
+
var GN_SUBTREE = generalName(ns, { decodeValue: true, subtreeBase: true, code: ns.prefix + "/bad-name-constraints" });
|
|
338
|
+
|
|
339
|
+
function decodeTop(buf, code, what) {
|
|
340
|
+
var n;
|
|
341
|
+
try { n = asn1.decode(buf); }
|
|
342
|
+
catch (e) { throw ns.E(code, "malformed " + what + " extension value: " + ((e && e.message) || String(e)), e); }
|
|
343
|
+
return n;
|
|
344
|
+
}
|
|
345
|
+
function seqChildren(buf, code, what) {
|
|
346
|
+
var n = decodeTop(buf, code, what);
|
|
347
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.SEQUENCE || !n.children) {
|
|
348
|
+
throw ns.E(code, what + " must be a SEQUENCE (RFC 5280 §4.2.1)");
|
|
349
|
+
}
|
|
350
|
+
return n.children;
|
|
351
|
+
}
|
|
352
|
+
function readInt(node, code, what) {
|
|
353
|
+
try { return asn1.read.integer(node); }
|
|
354
|
+
catch (e) { throw ns.E(code, what + " must be an INTEGER", e); }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// basicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLen INTEGER (0..MAX) OPTIONAL }
|
|
358
|
+
function basicConstraints(buf) {
|
|
359
|
+
var C = ns.prefix + "/bad-basic-constraints";
|
|
360
|
+
var kids = seqChildren(buf, C, "BasicConstraints");
|
|
361
|
+
var i = 0, cA = false, pathLen = null;
|
|
362
|
+
if (kids[i] && kids[i].tagClass === "universal" && kids[i].tagNumber === _T.BOOLEAN) {
|
|
363
|
+
var v;
|
|
364
|
+
try { v = asn1.read.boolean(kids[i]); } catch (e) { throw ns.E(C, "BasicConstraints cA must be a BOOLEAN", e); }
|
|
365
|
+
if (v !== true) throw ns.E(C, "BasicConstraints cA DEFAULT FALSE must be omitted, not an explicit FALSE (X.690 §11.5)");
|
|
366
|
+
cA = true; i++;
|
|
367
|
+
}
|
|
368
|
+
if (kids[i] && kids[i].tagClass === "universal" && kids[i].tagNumber === _T.INTEGER) {
|
|
369
|
+
if (!cA) throw ns.E(C, "BasicConstraints pathLenConstraint is only permitted when cA is TRUE (RFC 5280 §4.2.1.9)");
|
|
370
|
+
var pl = readInt(kids[i], C, "pathLenConstraint");
|
|
371
|
+
if (pl < 0n) throw ns.E(C, "BasicConstraints pathLenConstraint must be non-negative");
|
|
372
|
+
pathLen = Number(pl); i++;
|
|
373
|
+
}
|
|
374
|
+
if (i !== kids.length) throw ns.E(C, "BasicConstraints has unexpected trailing fields");
|
|
375
|
+
return { cA: cA, pathLenConstraint: pathLen };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// keyUsage ::= BIT STRING (named bits). At least one bit MUST be set.
|
|
379
|
+
var KU_BITS = ["digitalSignature", "nonRepudiation", "keyEncipherment", "dataEncipherment",
|
|
380
|
+
"keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly"];
|
|
381
|
+
function keyUsage(buf) {
|
|
382
|
+
var C = ns.prefix + "/bad-key-usage";
|
|
383
|
+
var n = decodeTop(buf, C, "KeyUsage");
|
|
384
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.BIT_STRING) throw ns.E(C, "KeyUsage must be a BIT STRING (RFC 5280 §4.2.1.3)");
|
|
385
|
+
var bs;
|
|
386
|
+
try { bs = asn1.read.bitString(n); } catch (e) { throw ns.E(C, "KeyUsage must be a well-formed BIT STRING", e); }
|
|
387
|
+
// At least one bit MUST be set (RFC 5280 §4.2.1.3) — an all-zero value (a
|
|
388
|
+
// non-empty byte run of only zero bits, e.g. 03 02 07 00) is malformed too.
|
|
389
|
+
var anyBit = false;
|
|
390
|
+
for (var z = 0; z < bs.bytes.length; z++) { if (bs.bytes[z] !== 0) { anyBit = true; break; } }
|
|
391
|
+
if (!anyBit) throw ns.E(C, "KeyUsage must assert at least one bit (RFC 5280 §4.2.1.3)");
|
|
392
|
+
// KeyUsage is a NamedBitList: DER (X.690 §11.2.2) strips all trailing zero
|
|
393
|
+
// bits, so the last content octet must be non-zero and the lowest USED bit
|
|
394
|
+
// (at position unusedBits) must be set — one canonical encoding per value.
|
|
395
|
+
var last = bs.bytes[bs.bytes.length - 1];
|
|
396
|
+
if (last === 0 || ((last >> bs.unusedBits) & 1) !== 1) {
|
|
397
|
+
throw ns.E(C, "KeyUsage must use the minimal DER NamedBitList encoding (no trailing zero bits, X.690 §11.2.2)");
|
|
398
|
+
}
|
|
399
|
+
var out = {};
|
|
400
|
+
KU_BITS.forEach(function (nm, bit) {
|
|
401
|
+
var byte = bit >> 3, mask = 0x80 >> (bit & 7);
|
|
402
|
+
out[nm] = byte < bs.bytes.length && (bs.bytes[byte] & mask) !== 0;
|
|
403
|
+
});
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// GeneralSubtree ::= SEQUENCE { base GeneralName, minimum [0] DEFAULT 0, maximum [1] OPTIONAL }
|
|
408
|
+
function subtreeList(node, C) {
|
|
409
|
+
// node is the [0]/[1] IMPLICIT SEQUENCE OF GeneralSubtree (context-constructed).
|
|
410
|
+
// GeneralSubtrees is SIZE(1..MAX) — an explicitly present but empty subtree
|
|
411
|
+
// list (a0 00) absorbs no constraint and is malformed, not a no-op.
|
|
412
|
+
if (!node.children || node.children.length < 1) throw ns.E(C, "NameConstraints permittedSubtrees/excludedSubtrees must be a non-empty GeneralSubtrees (SIZE 1..MAX, RFC 5280 §4.2.1.10)");
|
|
413
|
+
return node.children.map(function (st) {
|
|
414
|
+
if (st.tagClass !== "universal" || st.tagNumber !== _T.SEQUENCE || !st.children || st.children.length < 1) {
|
|
415
|
+
throw ns.E(C, "GeneralSubtree must be a SEQUENCE { base, minimum?, maximum? }");
|
|
416
|
+
}
|
|
417
|
+
var base = schema.walk(GN_SUBTREE, st.children[0], ns);
|
|
418
|
+
for (var j = 1; j < st.children.length; j++) {
|
|
419
|
+
var f = st.children[j];
|
|
420
|
+
if (f.tagClass !== "context" || (f.tagNumber !== 0 && f.tagNumber !== 1)) throw ns.E(C, "GeneralSubtree has an unexpected field");
|
|
421
|
+
if (f.tagNumber === 0) throw ns.E(C, "GeneralSubtree minimum DEFAULT 0 must be omitted (RFC 5280 §4.2.1.10 requires minimum = 0)");
|
|
422
|
+
if (f.tagNumber === 1) throw ns.E(C, "GeneralSubtree maximum is not permitted in the RFC 5280 profile (§4.2.1.10)");
|
|
423
|
+
}
|
|
424
|
+
return { base: base };
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
function nameConstraints(buf) {
|
|
428
|
+
var C = ns.prefix + "/bad-name-constraints";
|
|
429
|
+
var kids = seqChildren(buf, C, "NameConstraints");
|
|
430
|
+
var permitted = [], excluded = [], sawP = false, sawE = false, ncLastTag = -1;
|
|
431
|
+
kids.forEach(function (k) {
|
|
432
|
+
if (k.tagClass !== "context" || !k.children) throw ns.E(C, "NameConstraints fields are [0] permittedSubtrees / [1] excludedSubtrees");
|
|
433
|
+
if (k.tagNumber <= ncLastTag) throw ns.E(C, "NameConstraints fields must be unique and in ascending order (DER)");
|
|
434
|
+
ncLastTag = k.tagNumber;
|
|
435
|
+
if (k.tagNumber === 0) { if (sawP) throw ns.E(C, "duplicate permittedSubtrees"); sawP = true; permitted = subtreeList(k, C); }
|
|
436
|
+
else if (k.tagNumber === 1) { if (sawE) throw ns.E(C, "duplicate excludedSubtrees"); sawE = true; excluded = subtreeList(k, C); }
|
|
437
|
+
else throw ns.E(C, "NameConstraints has an unexpected field [" + k.tagNumber + "]");
|
|
438
|
+
});
|
|
439
|
+
if (!sawP && !sawE) throw ns.E(C, "NameConstraints must contain permittedSubtrees or excludedSubtrees (RFC 5280 §4.2.1.10)");
|
|
440
|
+
return { permittedSubtrees: permitted, excludedSubtrees: excluded };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// certificatePolicies ::= SEQUENCE SIZE(1..MAX) OF PolicyInformation
|
|
444
|
+
function certificatePolicies(buf) {
|
|
445
|
+
var C = ns.prefix + "/bad-policy";
|
|
446
|
+
var kids = seqChildren(buf, C, "CertificatePolicies");
|
|
447
|
+
if (kids.length < 1) throw ns.E(C, "CertificatePolicies must contain at least one PolicyInformation (RFC 5280 §4.2.1.4)");
|
|
448
|
+
var seen = {};
|
|
449
|
+
return kids.map(function (pi) {
|
|
450
|
+
// PolicyInformation ::= SEQUENCE { policyIdentifier, policyQualifiers
|
|
451
|
+
// SEQUENCE SIZE(1..MAX) OPTIONAL } — exactly one or two fields; a second
|
|
452
|
+
// field, if present, MUST be a SEQUENCE. Extra/mis-typed fields are malformed.
|
|
453
|
+
if (pi.tagClass !== "universal" || pi.tagNumber !== _T.SEQUENCE || !pi.children || pi.children.length < 1 || pi.children.length > 2) {
|
|
454
|
+
throw ns.E(C, "PolicyInformation must be a SEQUENCE { policyIdentifier, policyQualifiers? }");
|
|
455
|
+
}
|
|
456
|
+
var pid;
|
|
457
|
+
try { pid = asn1.read.oid(pi.children[0]); } catch (e) { throw ns.E(C, "PolicyInformation policyIdentifier must be an OBJECT IDENTIFIER", e); }
|
|
458
|
+
if (seen[pid]) throw ns.E(C, "duplicate policy OID " + pid + " (RFC 5280 §4.2.1.4)");
|
|
459
|
+
seen[pid] = true;
|
|
460
|
+
var qualifiers = null;
|
|
461
|
+
if (pi.children.length > 1) {
|
|
462
|
+
var q = pi.children[1];
|
|
463
|
+
if (q.tagClass !== "universal" || q.tagNumber !== _T.SEQUENCE || !q.children || !q.children.length) {
|
|
464
|
+
throw ns.E(C, "PolicyInformation policyQualifiers must be a non-empty SEQUENCE (RFC 5280 §4.2.1.4)");
|
|
465
|
+
}
|
|
466
|
+
// Each element is a PolicyQualifierInfo ::= SEQUENCE { policyQualifierId
|
|
467
|
+
// OID, qualifier ANY DEFINED BY policyQualifierId } — EXACTLY two members
|
|
468
|
+
// (qualifier is not OPTIONAL). A SEQUENCE missing the qualifier or
|
|
469
|
+
// carrying trailing extra fields is malformed; the qualifier body itself
|
|
470
|
+
// stays opaque.
|
|
471
|
+
q.children.forEach(function (pq) {
|
|
472
|
+
if (pq.tagClass !== "universal" || pq.tagNumber !== _T.SEQUENCE || !pq.children || pq.children.length !== 2) {
|
|
473
|
+
throw ns.E(C, "policyQualifiers element must be a PolicyQualifierInfo SEQUENCE { policyQualifierId, qualifier } (RFC 5280 §4.2.1.4)");
|
|
474
|
+
}
|
|
475
|
+
try { asn1.read.oid(pq.children[0]); } catch (e) { throw ns.E(C, "PolicyQualifierInfo must lead with a policyQualifierId OID", e); }
|
|
476
|
+
});
|
|
477
|
+
qualifiers = q.bytes;
|
|
478
|
+
}
|
|
479
|
+
return { policyIdentifier: pid, qualifiersBytes: qualifiers };
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// policyMappings ::= SEQUENCE SIZE(1..MAX) OF SEQUENCE { issuerDomainPolicy, subjectDomainPolicy }
|
|
484
|
+
function policyMappings(buf) {
|
|
485
|
+
var C = ns.prefix + "/bad-policy";
|
|
486
|
+
var kids = seqChildren(buf, C, "PolicyMappings");
|
|
487
|
+
if (kids.length < 1) throw ns.E(C, "PolicyMappings must contain at least one mapping (RFC 5280 §4.2.1.5)");
|
|
488
|
+
return kids.map(function (mp) {
|
|
489
|
+
if (mp.tagClass !== "universal" || mp.tagNumber !== _T.SEQUENCE || !mp.children || mp.children.length !== 2) {
|
|
490
|
+
throw ns.E(C, "policy mapping must be a SEQUENCE { issuerDomainPolicy, subjectDomainPolicy }");
|
|
491
|
+
}
|
|
492
|
+
var idp, sdp;
|
|
493
|
+
try { idp = asn1.read.oid(mp.children[0]); sdp = asn1.read.oid(mp.children[1]); }
|
|
494
|
+
catch (e) { throw ns.E(C, "policy mapping members must be OBJECT IDENTIFIERs", e); }
|
|
495
|
+
return { issuerDomainPolicy: idp, subjectDomainPolicy: sdp };
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// policyConstraints ::= SEQUENCE { requireExplicitPolicy [0] INTEGER OPT, inhibitPolicyMapping [1] INTEGER OPT }
|
|
500
|
+
function policyConstraints(buf) {
|
|
501
|
+
var C = ns.prefix + "/bad-policy";
|
|
502
|
+
var kids = seqChildren(buf, C, "PolicyConstraints");
|
|
503
|
+
if (kids.length < 1) throw ns.E(C, "PolicyConstraints must contain at least one field (RFC 5280 §4.2.1.11)");
|
|
504
|
+
var rep = null, ipm = null, pcLastTag = -1;
|
|
505
|
+
kids.forEach(function (k) {
|
|
506
|
+
if (k.tagClass !== "context") throw ns.E(C, "PolicyConstraints fields are context-tagged [0]/[1]");
|
|
507
|
+
if (k.tagNumber <= pcLastTag) throw ns.E(C, "PolicyConstraints fields must be unique and in ascending order (DER)");
|
|
508
|
+
pcLastTag = k.tagNumber;
|
|
509
|
+
var v;
|
|
510
|
+
try { v = asn1.read.integerImplicit(k, k.tagNumber); } catch (e) { throw ns.E(C, "PolicyConstraints field must be an INTEGER", e); }
|
|
511
|
+
if (v < 0n) throw ns.E(C, "PolicyConstraints skip count must be non-negative");
|
|
512
|
+
if (k.tagNumber === 0) rep = Number(v);
|
|
513
|
+
else if (k.tagNumber === 1) ipm = Number(v);
|
|
514
|
+
else throw ns.E(C, "PolicyConstraints has an unexpected field [" + k.tagNumber + "]");
|
|
515
|
+
});
|
|
516
|
+
return { requireExplicitPolicy: rep, inhibitPolicyMapping: ipm };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// inhibitAnyPolicy ::= INTEGER (0..MAX)
|
|
520
|
+
function inhibitAnyPolicy(buf) {
|
|
521
|
+
var C = ns.prefix + "/bad-policy";
|
|
522
|
+
var n = decodeTop(buf, C, "InhibitAnyPolicy");
|
|
523
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.INTEGER) throw ns.E(C, "InhibitAnyPolicy must be an INTEGER (RFC 5280 §4.2.1.14)");
|
|
524
|
+
var v = readInt(n, C, "InhibitAnyPolicy");
|
|
525
|
+
if (v < 0n) throw ns.E(C, "InhibitAnyPolicy skip count must be non-negative");
|
|
526
|
+
return Number(v);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// subjectAltName / issuerAltName ::= GeneralNames — decoded values surfaced.
|
|
530
|
+
function altName(buf) {
|
|
531
|
+
var C = ns.prefix + "/bad-extension-value";
|
|
532
|
+
var n = decodeTop(buf, C, "GeneralNames");
|
|
533
|
+
return schema.walk(generalNames(ns, { decodeValue: true, code: C }), n, ns).result;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// extKeyUsage ::= SEQUENCE SIZE(1..MAX) OF KeyPurposeId (OID)
|
|
537
|
+
function extKeyUsage(buf) {
|
|
538
|
+
var C = ns.prefix + "/bad-extension-value";
|
|
539
|
+
var kids = seqChildren(buf, C, "ExtKeyUsage");
|
|
540
|
+
if (kids.length < 1) throw ns.E(C, "ExtKeyUsage must contain at least one KeyPurposeId (RFC 5280 §4.2.1.12)");
|
|
541
|
+
return kids.map(function (k) {
|
|
542
|
+
try { return asn1.read.oid(k); } catch (e) { throw ns.E(C, "ExtKeyUsage KeyPurposeId must be an OBJECT IDENTIFIER", e); }
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// authorityKeyIdentifier ::= SEQUENCE { keyIdentifier [0] OPT, authorityCertIssuer [1] OPT, authorityCertSerialNumber [2] OPT }
|
|
547
|
+
function authorityKeyIdentifier(buf) {
|
|
548
|
+
var C = ns.prefix + "/bad-extension-value";
|
|
549
|
+
var kids = seqChildren(buf, C, "AuthorityKeyIdentifier");
|
|
550
|
+
var out = { keyIdentifier: null, authorityCertIssuer: null, authorityCertSerialNumber: null };
|
|
551
|
+
var lastTag = -1;
|
|
552
|
+
kids.forEach(function (k) {
|
|
553
|
+
if (k.tagClass !== "context") throw ns.E(C, "AuthorityKeyIdentifier fields are context-tagged");
|
|
554
|
+
// DER: the OPTIONAL fields appear at most once and in ascending tag order.
|
|
555
|
+
if (k.tagNumber <= lastTag) throw ns.E(C, "AuthorityKeyIdentifier fields must be unique and in ascending order (DER)");
|
|
556
|
+
lastTag = k.tagNumber;
|
|
557
|
+
// keyIdentifier [0] IMPLICIT OCTET STRING and authorityCertSerialNumber
|
|
558
|
+
// [2] IMPLICIT INTEGER are primitive leaves; the readers enforce the
|
|
559
|
+
// primitive form fail-closed (a constructed context node is rejected,
|
|
560
|
+
// never dereferenced for its absent content).
|
|
561
|
+
if (k.tagNumber === 0) { try { out.keyIdentifier = Buffer.from(asn1.read.octetStringImplicit(k, 0)); } catch (e) { throw ns.E(C, "AuthorityKeyIdentifier keyIdentifier [0] must be an IMPLICIT OCTET STRING", e); } }
|
|
562
|
+
else if (k.tagNumber === 1) out.authorityCertIssuer = schema.walk(generalNames(ns, { implicitTag: 1, decodeValue: true, code: C }), k, ns).result;
|
|
563
|
+
else if (k.tagNumber === 2) { try { out.authorityCertSerialNumber = asn1.read.integerImplicit(k, 2); } catch (e) { throw ns.E(C, "authorityCertSerialNumber must be an INTEGER", e); } }
|
|
564
|
+
else throw ns.E(C, "AuthorityKeyIdentifier has an unexpected field [" + k.tagNumber + "]");
|
|
565
|
+
});
|
|
566
|
+
if ((out.authorityCertIssuer === null) !== (out.authorityCertSerialNumber === null)) {
|
|
567
|
+
throw ns.E(C, "AuthorityKeyIdentifier authorityCertIssuer and authorityCertSerialNumber must both be present or both absent (RFC 5280 §4.2.1.1)");
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// subjectKeyIdentifier ::= OCTET STRING
|
|
573
|
+
function subjectKeyIdentifier(buf) {
|
|
574
|
+
var C = ns.prefix + "/bad-extension-value";
|
|
575
|
+
var n = decodeTop(buf, C, "SubjectKeyIdentifier");
|
|
576
|
+
try { return Buffer.concat([asn1.read.octetString(n)]); }
|
|
577
|
+
catch (e) { throw ns.E(C, "SubjectKeyIdentifier must be an OCTET STRING (RFC 5280 §4.2.1.2)", e); }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function O(nm) { return ns.oid.byName(nm); }
|
|
581
|
+
var byOid = {};
|
|
582
|
+
byOid[O("basicConstraints")] = basicConstraints;
|
|
583
|
+
byOid[O("keyUsage")] = keyUsage;
|
|
584
|
+
byOid[O("nameConstraints")] = nameConstraints;
|
|
585
|
+
byOid[O("certificatePolicies")] = certificatePolicies;
|
|
586
|
+
byOid[O("policyMappings")] = policyMappings;
|
|
587
|
+
byOid[O("policyConstraints")] = policyConstraints;
|
|
588
|
+
byOid[O("inhibitAnyPolicy")] = inhibitAnyPolicy;
|
|
589
|
+
byOid[O("subjectAltName")] = altName;
|
|
590
|
+
byOid[O("issuerAltName")] = altName;
|
|
591
|
+
byOid[O("extKeyUsage")] = extKeyUsage;
|
|
592
|
+
byOid[O("authorityKeyIdentifier")] = authorityKeyIdentifier;
|
|
593
|
+
byOid[O("subjectKeyIdentifier")] = subjectKeyIdentifier;
|
|
594
|
+
return { byOid: byOid };
|
|
595
|
+
}
|
|
596
|
+
|
|
293
597
|
// Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE,
|
|
294
598
|
// extnValue OCTET STRING }. `critical` is a universal BOOLEAN present-by-count
|
|
295
599
|
// (not context-tagged), so the per-extension decode handles the 2-vs-3-child
|
|
@@ -446,6 +750,7 @@ module.exports = {
|
|
|
446
750
|
name: name,
|
|
447
751
|
generalName: generalName,
|
|
448
752
|
generalNames: generalNames,
|
|
753
|
+
certExtensionDecoders: certExtensionDecoders,
|
|
449
754
|
attribute: attribute,
|
|
450
755
|
extension: extension,
|
|
451
756
|
extensions: extensions,
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:69692b51-d096-4124-9a0c-886b9a5ad78e",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-06T19:48:47.311Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.1.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.1.16",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
25
|
+
"version": "0.1.16",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.1.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.1.16",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.1.
|
|
57
|
+
"ref": "@blamejs/pki@0.1.16",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|