@blamejs/pki 0.1.15 → 0.1.17
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 +29 -0
- package/README.md +3 -1
- package/index.js +4 -0
- package/lib/asn1-der.js +13 -0
- package/lib/constants.js +4 -0
- package/lib/framework-error.js +14 -0
- package/lib/oid.js +17 -4
- package/lib/path-validate.js +1393 -0
- package/lib/schema-all.js +16 -1
- package/lib/schema-cms.js +12 -3
- package/lib/schema-crl.js +2 -2
- package/lib/schema-crmf.js +487 -0
- package/lib/schema-csr.js +3 -3
- package/lib/schema-engine.js +148 -14
- package/lib/schema-ocsp.js +3 -3
- package/lib/schema-pkcs8.js +4 -4
- package/lib/schema-pkix.js +374 -37
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/schema-pkix.js
CHANGED
|
@@ -166,6 +166,12 @@ function attrValueToString(ns) {
|
|
|
166
166
|
}
|
|
167
167
|
return "#" + node.bytes.toString("hex");
|
|
168
168
|
}
|
|
169
|
+
}, function (value) {
|
|
170
|
+
// encode: a "#hex" escape (RFC 4514 §2.4) round-trips its raw DER verbatim; any
|
|
171
|
+
// other string encodes as UTF8String (the decode does not preserve the original
|
|
172
|
+
// string type, so this is the canonical re-encoding, not a byte-exact one).
|
|
173
|
+
if (typeof value === "string" && value.charAt(0) === "#") return Buffer.from(value.slice(1), "hex");
|
|
174
|
+
return asn1.build.utf8(value);
|
|
169
175
|
});
|
|
170
176
|
}
|
|
171
177
|
|
|
@@ -193,25 +199,34 @@ function relativeDistinguishedName(ns) {
|
|
|
193
199
|
// RelativeDistinguishedName ::= SET SIZE (1..MAX) — an empty SET {} is malformed.
|
|
194
200
|
return schema.setOf(attributeTypeAndValue(ns), { assert: "set", min: 1, code: ns.prefix + "/bad-rdn", what: "RelativeDistinguishedName" });
|
|
195
201
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
202
|
+
// opts.implicitTag (optional): read the Name as a [tag] IMPLICIT RDNSequence — the
|
|
203
|
+
// context tag REPLACES the universal SEQUENCE tag, so the node is a context-class
|
|
204
|
+
// constructed [tag] whose children ARE the RDN SETs (the CRMF CertTemplate issuer
|
|
205
|
+
// [3] / subject [5] IMPLICIT arm, RFC 4211 §5). With no opts the shape is a bare
|
|
206
|
+
// universal SEQUENCE OF, byte-identical to every existing caller.
|
|
207
|
+
function name(ns, opts) {
|
|
208
|
+
opts = opts || {};
|
|
209
|
+
function nameBuild(m) {
|
|
210
|
+
var rdns = [], parts = [];
|
|
211
|
+
m.items.forEach(function (rdnItem) {
|
|
212
|
+
var atvs = [], atvParts = [];
|
|
213
|
+
rdnItem.value.items.forEach(function (atvItem) {
|
|
214
|
+
var a = atvItem.value.result; // atvItem.value = the atv seq-match; .result = its build result
|
|
215
|
+
atvs.push(a);
|
|
216
|
+
var label = (a.name && DN_SHORT[a.name]) || a.name || a.type;
|
|
217
|
+
atvParts.push(label + "=" + _escapeDnValue(a.value));
|
|
211
218
|
});
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
rdns.push(atvs);
|
|
220
|
+
parts.push(atvParts.join("+"));
|
|
221
|
+
});
|
|
222
|
+
return { rdns: rdns, dn: parts.join(", ") };
|
|
223
|
+
}
|
|
224
|
+
if (opts.implicitTag != null) {
|
|
225
|
+
return schema.implicitSeqOf(opts.implicitTag, relativeDistinguishedName(ns), {
|
|
226
|
+
code: ns.prefix + "/bad-name", what: "Name", build: nameBuild });
|
|
227
|
+
}
|
|
228
|
+
return schema.seqOf(relativeDistinguishedName(ns), {
|
|
229
|
+
assert: "sequence", code: ns.prefix + "/bad-name", what: "Name", build: nameBuild });
|
|
215
230
|
}
|
|
216
231
|
|
|
217
232
|
// GeneralName ::= CHOICE (RFC 5280 §4.2.1.6). A validate-and-surface-raw leaf for a
|
|
@@ -224,11 +239,22 @@ function name(ns) {
|
|
|
224
239
|
// OCTET STRING; registeredID [8] is a primitive OBJECT IDENTIFIER — then surfaces the
|
|
225
240
|
// value RAW ({ bytes, tagClass, tagNumber }). `opts.code` is the caller's error code
|
|
226
241
|
// (e.g. ocsp/bad-requestor-name, tsp/bad-tsa). Shared so the two parsers cannot drift.
|
|
242
|
+
// opts.decodeValue (default false): in addition to the raw { bytes, tagClass,
|
|
243
|
+
// tagNumber }, surface the DECODED `value` per arm — IA5 text (string), an
|
|
244
|
+
// iPAddress Buffer, a directoryName { rdns, dn }, a registeredID OID string,
|
|
245
|
+
// or an otherName { typeId, valueBytes }. The path validator's name-constraint
|
|
246
|
+
// matcher needs the decoded value; the tsp/ocsp/attrcert consumers pass no
|
|
247
|
+
// flag and get the byte-identical raw-only shape.
|
|
248
|
+
// opts.subtreeBase (default false): the GeneralSubtree.base form (RFC 5280
|
|
249
|
+
// §4.2.1.10) — an iPAddress base is an address+mask (8 octets IPv4 / 32 IPv6),
|
|
250
|
+
// NOT the 4/16-octet SAN address form. Only the iPAddress size rule changes.
|
|
227
251
|
var GN_CONSTRUCTED = { 0: 1, 3: 1, 4: 1, 5: 1 };
|
|
228
252
|
var GN_IA5 = { 1: 1, 2: 1, 6: 1 };
|
|
229
253
|
function generalName(ns, opts) {
|
|
230
254
|
opts = opts || {};
|
|
231
255
|
var code = opts.code || (ns.prefix + "/bad-general-name");
|
|
256
|
+
var decodeValue = opts.decodeValue === true;
|
|
257
|
+
var subtreeBase = opts.subtreeBase === true;
|
|
232
258
|
var NAME = name(ns);
|
|
233
259
|
return schema.decode(function (n, ctx) {
|
|
234
260
|
if (n.tagClass !== "context" || n.tagNumber < 0 || n.tagNumber > 8) {
|
|
@@ -236,45 +262,66 @@ function generalName(ns, opts) {
|
|
|
236
262
|
}
|
|
237
263
|
var t = n.tagNumber;
|
|
238
264
|
var constructed = !!n.children;
|
|
265
|
+
var value;
|
|
239
266
|
if (GN_CONSTRUCTED[t]) {
|
|
240
267
|
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
268
|
if (t === 0) {
|
|
242
269
|
// otherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY }.
|
|
243
270
|
if (n.children.length !== 2) throw ctx.E(code, "GeneralName otherName [0] must be a SEQUENCE { type-id, value [0] }");
|
|
244
|
-
|
|
271
|
+
var typeId;
|
|
272
|
+
try { typeId = asn1.read.oid(n.children[0]); }
|
|
245
273
|
catch (e) { throw ctx.E(code, "GeneralName otherName [0] must lead with a type-id OBJECT IDENTIFIER", e); }
|
|
246
|
-
var
|
|
247
|
-
if (!(
|
|
274
|
+
var ov = n.children[1];
|
|
275
|
+
if (!(ov.tagClass === "context" && ov.tagNumber === 0 && ov.children && ov.children.length === 1)) {
|
|
248
276
|
throw ctx.E(code, "GeneralName otherName [0] value must be a [0] EXPLICIT wrapper carrying exactly one value");
|
|
249
277
|
}
|
|
278
|
+
if (decodeValue) value = { typeId: typeId, valueBytes: ov.children[0].bytes };
|
|
250
279
|
} else if (t === 4) {
|
|
251
280
|
// directoryName [4] EXPLICIT Name — validate the wrapped RDNSequence.
|
|
252
281
|
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);
|
|
282
|
+
var dnMatch = schema.walk(NAME, n.children[0], ctx);
|
|
283
|
+
if (decodeValue) value = dnMatch.result;
|
|
254
284
|
}
|
|
255
285
|
} else {
|
|
256
286
|
if (constructed) throw ctx.E(code, "GeneralName [" + t + "] must be primitive (X.690 §10.2)");
|
|
257
287
|
if (GN_IA5[t]) {
|
|
258
288
|
if (n.content.length === 0) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty IA5String");
|
|
259
289
|
for (var i = 0; i < n.content.length; i++) {
|
|
260
|
-
|
|
290
|
+
// 7-bit IA5, and no C0/DEL control byte — an embedded NUL/control in a
|
|
291
|
+
// dNSName/rfc822Name/URI enables a name-truncation/confusion bypass
|
|
292
|
+
// (CVE-2009-2408 class) downstream, so reject it at decode.
|
|
293
|
+
if (n.content[i] < 0x20 || n.content[i] > 0x7e) throw ctx.E(code, "GeneralName [" + t + "] must be a printable IA5String (no control bytes)");
|
|
261
294
|
}
|
|
295
|
+
if (decodeValue) value = n.content.toString("latin1");
|
|
262
296
|
} else if (t === 7) {
|
|
263
|
-
if (
|
|
297
|
+
if (subtreeBase) {
|
|
298
|
+
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)");
|
|
299
|
+
} else if (n.content.length !== 4 && n.content.length !== 16) {
|
|
300
|
+
throw ctx.E(code, "GeneralName iPAddress [7] must be a 4- or 16-octet address");
|
|
301
|
+
}
|
|
302
|
+
// n.content is proven non-null here (the primitive-form guard above
|
|
303
|
+
// threw on a constructed node); copy the octets out.
|
|
304
|
+
if (decodeValue) value = Buffer.concat([n.content]);
|
|
264
305
|
} else if (t === 8) {
|
|
265
|
-
|
|
306
|
+
var regId;
|
|
307
|
+
try { regId = asn1.decodeOidContent(n.content); }
|
|
266
308
|
catch (e) { throw ctx.E(code, "GeneralName registeredID [8] must be a valid OBJECT IDENTIFIER", e); }
|
|
309
|
+
if (decodeValue) value = regId;
|
|
267
310
|
}
|
|
268
311
|
}
|
|
269
|
-
|
|
312
|
+
var out = { bytes: n.bytes, tagClass: n.tagClass, tagNumber: n.tagNumber };
|
|
313
|
+
if (decodeValue) out.value = value === undefined ? null : value;
|
|
314
|
+
return out;
|
|
270
315
|
});
|
|
271
316
|
}
|
|
272
317
|
|
|
273
318
|
// GeneralNames ::= SEQUENCE OF GeneralName (RFC 5280 §4.2.1.6), SIZE (1..MAX). Every
|
|
274
319
|
// element is validated by generalName (its CHOICE alternative's form + content) and
|
|
275
320
|
// 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
|
-
//
|
|
321
|
+
// element by treating the whole sequence as opaque bytes. Returns { names, bytes }
|
|
322
|
+
// where each `names[i]` is the generalName leaf's return — { bytes, tagClass,
|
|
323
|
+
// tagNumber } and, when opts.decodeValue is set, the decoded `value` — and `bytes`
|
|
324
|
+
// is the raw outer DER.
|
|
278
325
|
// `opts.implicitTag` handles a [tag] IMPLICIT GeneralNames — the context tag REPLACES
|
|
279
326
|
// the universal SEQUENCE tag (RFC 5755 Holder.entityName [1]); otherwise it is a bare
|
|
280
327
|
// universal SEQUENCE OF. `opts.code` is the caller's error code. Shared so the x509 /
|
|
@@ -282,7 +329,9 @@ function generalName(ns, opts) {
|
|
|
282
329
|
function generalNames(ns, opts) {
|
|
283
330
|
opts = opts || {};
|
|
284
331
|
var code = opts.code || (ns.prefix + "/bad-general-names");
|
|
285
|
-
|
|
332
|
+
// decodeValue / subtreeBase thread through to each element (the path
|
|
333
|
+
// validator's constraint matcher passes decodeValue:true).
|
|
334
|
+
var gn = generalName(ns, { code: code, decodeValue: opts.decodeValue === true, subtreeBase: opts.subtreeBase === true });
|
|
286
335
|
function build(m) { return { names: m.items.map(function (it) { return it.value; }), bytes: m.node.bytes }; }
|
|
287
336
|
if (opts.implicitTag != null) {
|
|
288
337
|
return schema.implicitSeqOf(opts.implicitTag, gn, { min: 1, code: code, what: opts.what || "GeneralNames", build: build });
|
|
@@ -290,6 +339,276 @@ function generalNames(ns, opts) {
|
|
|
290
339
|
return schema.seqOf(gn, { assert: "sequence", min: 1, code: code, what: opts.what || "GeneralNames", build: build });
|
|
291
340
|
}
|
|
292
341
|
|
|
342
|
+
// certExtensionDecoders(ns) — the ns-parameterized RFC 5280 §4.2.1 extension
|
|
343
|
+
// VALUE decoders. `x509.parse` surfaces each extension as { oid, name, critical,
|
|
344
|
+
// value } with `value` the raw inner OCTET-STRING content (a Buffer); the path
|
|
345
|
+
// validator and the future complete-extension-set surface need the STRUCTURE
|
|
346
|
+
// inside. Each decoder takes that raw Buffer and returns the decoded value, or
|
|
347
|
+
// throws a typed `<prefix>/bad-*` code, mirroring the CRL's fail-closed
|
|
348
|
+
// `decodeExt` pattern (asn1.decode → read.* / schema.walk, wrapped). Returns
|
|
349
|
+
// { byOid } keyed by dotted OID so a consumer dispatches by ext.oid.
|
|
350
|
+
var _T = asn1.TAGS;
|
|
351
|
+
function certExtensionDecoders(ns) {
|
|
352
|
+
var GN_SUBTREE = generalName(ns, { decodeValue: true, subtreeBase: true, code: ns.prefix + "/bad-name-constraints" });
|
|
353
|
+
|
|
354
|
+
function decodeTop(buf, code, what) {
|
|
355
|
+
var n;
|
|
356
|
+
try { n = asn1.decode(buf); }
|
|
357
|
+
catch (e) { throw ns.E(code, "malformed " + what + " extension value: " + ((e && e.message) || String(e)), e); }
|
|
358
|
+
return n;
|
|
359
|
+
}
|
|
360
|
+
function seqChildren(buf, code, what) {
|
|
361
|
+
var n = decodeTop(buf, code, what);
|
|
362
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.SEQUENCE || !n.children) {
|
|
363
|
+
throw ns.E(code, what + " must be a SEQUENCE (RFC 5280 §4.2.1)");
|
|
364
|
+
}
|
|
365
|
+
return n.children;
|
|
366
|
+
}
|
|
367
|
+
function readInt(node, code, what) {
|
|
368
|
+
try { return asn1.read.integer(node); }
|
|
369
|
+
catch (e) { throw ns.E(code, what + " must be an INTEGER", e); }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// basicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLen INTEGER (0..MAX) OPTIONAL }
|
|
373
|
+
function basicConstraints(buf) {
|
|
374
|
+
var C = ns.prefix + "/bad-basic-constraints";
|
|
375
|
+
var kids = seqChildren(buf, C, "BasicConstraints");
|
|
376
|
+
var i = 0, cA = false, pathLen = null;
|
|
377
|
+
if (kids[i] && kids[i].tagClass === "universal" && kids[i].tagNumber === _T.BOOLEAN) {
|
|
378
|
+
var v;
|
|
379
|
+
try { v = asn1.read.boolean(kids[i]); } catch (e) { throw ns.E(C, "BasicConstraints cA must be a BOOLEAN", e); }
|
|
380
|
+
if (v !== true) throw ns.E(C, "BasicConstraints cA DEFAULT FALSE must be omitted, not an explicit FALSE (X.690 §11.5)");
|
|
381
|
+
cA = true; i++;
|
|
382
|
+
}
|
|
383
|
+
if (kids[i] && kids[i].tagClass === "universal" && kids[i].tagNumber === _T.INTEGER) {
|
|
384
|
+
if (!cA) throw ns.E(C, "BasicConstraints pathLenConstraint is only permitted when cA is TRUE (RFC 5280 §4.2.1.9)");
|
|
385
|
+
var pl = readInt(kids[i], C, "pathLenConstraint");
|
|
386
|
+
if (pl < 0n) throw ns.E(C, "BasicConstraints pathLenConstraint must be non-negative");
|
|
387
|
+
pathLen = Number(pl); i++;
|
|
388
|
+
}
|
|
389
|
+
if (i !== kids.length) throw ns.E(C, "BasicConstraints has unexpected trailing fields");
|
|
390
|
+
return { cA: cA, pathLenConstraint: pathLen };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// keyUsage ::= BIT STRING (named bits). At least one bit MUST be set.
|
|
394
|
+
var KU_BITS = ["digitalSignature", "nonRepudiation", "keyEncipherment", "dataEncipherment",
|
|
395
|
+
"keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly"];
|
|
396
|
+
function keyUsage(buf) {
|
|
397
|
+
var C = ns.prefix + "/bad-key-usage";
|
|
398
|
+
var n = decodeTop(buf, C, "KeyUsage");
|
|
399
|
+
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)");
|
|
400
|
+
var bs;
|
|
401
|
+
try { bs = asn1.read.bitString(n); } catch (e) { throw ns.E(C, "KeyUsage must be a well-formed BIT STRING", e); }
|
|
402
|
+
// At least one bit MUST be set (RFC 5280 §4.2.1.3) — an all-zero value (a
|
|
403
|
+
// non-empty byte run of only zero bits, e.g. 03 02 07 00) is malformed too.
|
|
404
|
+
var anyBit = false;
|
|
405
|
+
for (var z = 0; z < bs.bytes.length; z++) { if (bs.bytes[z] !== 0) { anyBit = true; break; } }
|
|
406
|
+
if (!anyBit) throw ns.E(C, "KeyUsage must assert at least one bit (RFC 5280 §4.2.1.3)");
|
|
407
|
+
// KeyUsage is a NamedBitList: DER (X.690 §11.2.2) strips all trailing zero
|
|
408
|
+
// bits, so the last content octet must be non-zero and the lowest USED bit
|
|
409
|
+
// (at position unusedBits) must be set — one canonical encoding per value.
|
|
410
|
+
var last = bs.bytes[bs.bytes.length - 1];
|
|
411
|
+
if (last === 0 || ((last >> bs.unusedBits) & 1) !== 1) {
|
|
412
|
+
throw ns.E(C, "KeyUsage must use the minimal DER NamedBitList encoding (no trailing zero bits, X.690 §11.2.2)");
|
|
413
|
+
}
|
|
414
|
+
var out = {};
|
|
415
|
+
KU_BITS.forEach(function (nm, bit) {
|
|
416
|
+
var byte = bit >> 3, mask = 0x80 >> (bit & 7);
|
|
417
|
+
out[nm] = byte < bs.bytes.length && (bs.bytes[byte] & mask) !== 0;
|
|
418
|
+
});
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// GeneralSubtree ::= SEQUENCE { base GeneralName, minimum [0] DEFAULT 0, maximum [1] OPTIONAL }
|
|
423
|
+
function subtreeList(node, C) {
|
|
424
|
+
// node is the [0]/[1] IMPLICIT SEQUENCE OF GeneralSubtree (context-constructed).
|
|
425
|
+
// GeneralSubtrees is SIZE(1..MAX) — an explicitly present but empty subtree
|
|
426
|
+
// list (a0 00) absorbs no constraint and is malformed, not a no-op.
|
|
427
|
+
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)");
|
|
428
|
+
return node.children.map(function (st) {
|
|
429
|
+
if (st.tagClass !== "universal" || st.tagNumber !== _T.SEQUENCE || !st.children || st.children.length < 1) {
|
|
430
|
+
throw ns.E(C, "GeneralSubtree must be a SEQUENCE { base, minimum?, maximum? }");
|
|
431
|
+
}
|
|
432
|
+
var base = schema.walk(GN_SUBTREE, st.children[0], ns);
|
|
433
|
+
for (var j = 1; j < st.children.length; j++) {
|
|
434
|
+
var f = st.children[j];
|
|
435
|
+
if (f.tagClass !== "context" || (f.tagNumber !== 0 && f.tagNumber !== 1)) throw ns.E(C, "GeneralSubtree has an unexpected field");
|
|
436
|
+
if (f.tagNumber === 0) throw ns.E(C, "GeneralSubtree minimum DEFAULT 0 must be omitted (RFC 5280 §4.2.1.10 requires minimum = 0)");
|
|
437
|
+
if (f.tagNumber === 1) throw ns.E(C, "GeneralSubtree maximum is not permitted in the RFC 5280 profile (§4.2.1.10)");
|
|
438
|
+
}
|
|
439
|
+
return { base: base };
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function nameConstraints(buf) {
|
|
443
|
+
var C = ns.prefix + "/bad-name-constraints";
|
|
444
|
+
var kids = seqChildren(buf, C, "NameConstraints");
|
|
445
|
+
var permitted = [], excluded = [], sawP = false, sawE = false, ncLastTag = -1;
|
|
446
|
+
kids.forEach(function (k) {
|
|
447
|
+
if (k.tagClass !== "context" || !k.children) throw ns.E(C, "NameConstraints fields are [0] permittedSubtrees / [1] excludedSubtrees");
|
|
448
|
+
if (k.tagNumber <= ncLastTag) throw ns.E(C, "NameConstraints fields must be unique and in ascending order (DER)");
|
|
449
|
+
ncLastTag = k.tagNumber;
|
|
450
|
+
if (k.tagNumber === 0) { if (sawP) throw ns.E(C, "duplicate permittedSubtrees"); sawP = true; permitted = subtreeList(k, C); }
|
|
451
|
+
else if (k.tagNumber === 1) { if (sawE) throw ns.E(C, "duplicate excludedSubtrees"); sawE = true; excluded = subtreeList(k, C); }
|
|
452
|
+
else throw ns.E(C, "NameConstraints has an unexpected field [" + k.tagNumber + "]");
|
|
453
|
+
});
|
|
454
|
+
if (!sawP && !sawE) throw ns.E(C, "NameConstraints must contain permittedSubtrees or excludedSubtrees (RFC 5280 §4.2.1.10)");
|
|
455
|
+
return { permittedSubtrees: permitted, excludedSubtrees: excluded };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// certificatePolicies ::= SEQUENCE SIZE(1..MAX) OF PolicyInformation
|
|
459
|
+
function certificatePolicies(buf) {
|
|
460
|
+
var C = ns.prefix + "/bad-policy";
|
|
461
|
+
var kids = seqChildren(buf, C, "CertificatePolicies");
|
|
462
|
+
if (kids.length < 1) throw ns.E(C, "CertificatePolicies must contain at least one PolicyInformation (RFC 5280 §4.2.1.4)");
|
|
463
|
+
var seen = {};
|
|
464
|
+
return kids.map(function (pi) {
|
|
465
|
+
// PolicyInformation ::= SEQUENCE { policyIdentifier, policyQualifiers
|
|
466
|
+
// SEQUENCE SIZE(1..MAX) OPTIONAL } — exactly one or two fields; a second
|
|
467
|
+
// field, if present, MUST be a SEQUENCE. Extra/mis-typed fields are malformed.
|
|
468
|
+
if (pi.tagClass !== "universal" || pi.tagNumber !== _T.SEQUENCE || !pi.children || pi.children.length < 1 || pi.children.length > 2) {
|
|
469
|
+
throw ns.E(C, "PolicyInformation must be a SEQUENCE { policyIdentifier, policyQualifiers? }");
|
|
470
|
+
}
|
|
471
|
+
var pid;
|
|
472
|
+
try { pid = asn1.read.oid(pi.children[0]); } catch (e) { throw ns.E(C, "PolicyInformation policyIdentifier must be an OBJECT IDENTIFIER", e); }
|
|
473
|
+
if (seen[pid]) throw ns.E(C, "duplicate policy OID " + pid + " (RFC 5280 §4.2.1.4)");
|
|
474
|
+
seen[pid] = true;
|
|
475
|
+
var qualifiers = null;
|
|
476
|
+
if (pi.children.length > 1) {
|
|
477
|
+
var q = pi.children[1];
|
|
478
|
+
if (q.tagClass !== "universal" || q.tagNumber !== _T.SEQUENCE || !q.children || !q.children.length) {
|
|
479
|
+
throw ns.E(C, "PolicyInformation policyQualifiers must be a non-empty SEQUENCE (RFC 5280 §4.2.1.4)");
|
|
480
|
+
}
|
|
481
|
+
// Each element is a PolicyQualifierInfo ::= SEQUENCE { policyQualifierId
|
|
482
|
+
// OID, qualifier ANY DEFINED BY policyQualifierId } — EXACTLY two members
|
|
483
|
+
// (qualifier is not OPTIONAL). A SEQUENCE missing the qualifier or
|
|
484
|
+
// carrying trailing extra fields is malformed; the qualifier body itself
|
|
485
|
+
// stays opaque.
|
|
486
|
+
q.children.forEach(function (pq) {
|
|
487
|
+
if (pq.tagClass !== "universal" || pq.tagNumber !== _T.SEQUENCE || !pq.children || pq.children.length !== 2) {
|
|
488
|
+
throw ns.E(C, "policyQualifiers element must be a PolicyQualifierInfo SEQUENCE { policyQualifierId, qualifier } (RFC 5280 §4.2.1.4)");
|
|
489
|
+
}
|
|
490
|
+
try { asn1.read.oid(pq.children[0]); } catch (e) { throw ns.E(C, "PolicyQualifierInfo must lead with a policyQualifierId OID", e); }
|
|
491
|
+
});
|
|
492
|
+
qualifiers = q.bytes;
|
|
493
|
+
}
|
|
494
|
+
return { policyIdentifier: pid, qualifiersBytes: qualifiers };
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// policyMappings ::= SEQUENCE SIZE(1..MAX) OF SEQUENCE { issuerDomainPolicy, subjectDomainPolicy }
|
|
499
|
+
function policyMappings(buf) {
|
|
500
|
+
var C = ns.prefix + "/bad-policy";
|
|
501
|
+
var kids = seqChildren(buf, C, "PolicyMappings");
|
|
502
|
+
if (kids.length < 1) throw ns.E(C, "PolicyMappings must contain at least one mapping (RFC 5280 §4.2.1.5)");
|
|
503
|
+
return kids.map(function (mp) {
|
|
504
|
+
if (mp.tagClass !== "universal" || mp.tagNumber !== _T.SEQUENCE || !mp.children || mp.children.length !== 2) {
|
|
505
|
+
throw ns.E(C, "policy mapping must be a SEQUENCE { issuerDomainPolicy, subjectDomainPolicy }");
|
|
506
|
+
}
|
|
507
|
+
var idp, sdp;
|
|
508
|
+
try { idp = asn1.read.oid(mp.children[0]); sdp = asn1.read.oid(mp.children[1]); }
|
|
509
|
+
catch (e) { throw ns.E(C, "policy mapping members must be OBJECT IDENTIFIERs", e); }
|
|
510
|
+
return { issuerDomainPolicy: idp, subjectDomainPolicy: sdp };
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// policyConstraints ::= SEQUENCE { requireExplicitPolicy [0] INTEGER OPT, inhibitPolicyMapping [1] INTEGER OPT }
|
|
515
|
+
function policyConstraints(buf) {
|
|
516
|
+
var C = ns.prefix + "/bad-policy";
|
|
517
|
+
var kids = seqChildren(buf, C, "PolicyConstraints");
|
|
518
|
+
if (kids.length < 1) throw ns.E(C, "PolicyConstraints must contain at least one field (RFC 5280 §4.2.1.11)");
|
|
519
|
+
var rep = null, ipm = null, pcLastTag = -1;
|
|
520
|
+
kids.forEach(function (k) {
|
|
521
|
+
if (k.tagClass !== "context") throw ns.E(C, "PolicyConstraints fields are context-tagged [0]/[1]");
|
|
522
|
+
if (k.tagNumber <= pcLastTag) throw ns.E(C, "PolicyConstraints fields must be unique and in ascending order (DER)");
|
|
523
|
+
pcLastTag = k.tagNumber;
|
|
524
|
+
var v;
|
|
525
|
+
try { v = asn1.read.integerImplicit(k, k.tagNumber); } catch (e) { throw ns.E(C, "PolicyConstraints field must be an INTEGER", e); }
|
|
526
|
+
if (v < 0n) throw ns.E(C, "PolicyConstraints skip count must be non-negative");
|
|
527
|
+
if (k.tagNumber === 0) rep = Number(v);
|
|
528
|
+
else if (k.tagNumber === 1) ipm = Number(v);
|
|
529
|
+
else throw ns.E(C, "PolicyConstraints has an unexpected field [" + k.tagNumber + "]");
|
|
530
|
+
});
|
|
531
|
+
return { requireExplicitPolicy: rep, inhibitPolicyMapping: ipm };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// inhibitAnyPolicy ::= INTEGER (0..MAX)
|
|
535
|
+
function inhibitAnyPolicy(buf) {
|
|
536
|
+
var C = ns.prefix + "/bad-policy";
|
|
537
|
+
var n = decodeTop(buf, C, "InhibitAnyPolicy");
|
|
538
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.INTEGER) throw ns.E(C, "InhibitAnyPolicy must be an INTEGER (RFC 5280 §4.2.1.14)");
|
|
539
|
+
var v = readInt(n, C, "InhibitAnyPolicy");
|
|
540
|
+
if (v < 0n) throw ns.E(C, "InhibitAnyPolicy skip count must be non-negative");
|
|
541
|
+
return Number(v);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// subjectAltName / issuerAltName ::= GeneralNames — decoded values surfaced.
|
|
545
|
+
function altName(buf) {
|
|
546
|
+
var C = ns.prefix + "/bad-extension-value";
|
|
547
|
+
var n = decodeTop(buf, C, "GeneralNames");
|
|
548
|
+
return schema.walk(generalNames(ns, { decodeValue: true, code: C }), n, ns).result;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// extKeyUsage ::= SEQUENCE SIZE(1..MAX) OF KeyPurposeId (OID)
|
|
552
|
+
function extKeyUsage(buf) {
|
|
553
|
+
var C = ns.prefix + "/bad-extension-value";
|
|
554
|
+
var kids = seqChildren(buf, C, "ExtKeyUsage");
|
|
555
|
+
if (kids.length < 1) throw ns.E(C, "ExtKeyUsage must contain at least one KeyPurposeId (RFC 5280 §4.2.1.12)");
|
|
556
|
+
return kids.map(function (k) {
|
|
557
|
+
try { return asn1.read.oid(k); } catch (e) { throw ns.E(C, "ExtKeyUsage KeyPurposeId must be an OBJECT IDENTIFIER", e); }
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// authorityKeyIdentifier ::= SEQUENCE { keyIdentifier [0] OPT, authorityCertIssuer [1] OPT, authorityCertSerialNumber [2] OPT }
|
|
562
|
+
function authorityKeyIdentifier(buf) {
|
|
563
|
+
var C = ns.prefix + "/bad-extension-value";
|
|
564
|
+
var kids = seqChildren(buf, C, "AuthorityKeyIdentifier");
|
|
565
|
+
var out = { keyIdentifier: null, authorityCertIssuer: null, authorityCertSerialNumber: null };
|
|
566
|
+
var lastTag = -1;
|
|
567
|
+
kids.forEach(function (k) {
|
|
568
|
+
if (k.tagClass !== "context") throw ns.E(C, "AuthorityKeyIdentifier fields are context-tagged");
|
|
569
|
+
// DER: the OPTIONAL fields appear at most once and in ascending tag order.
|
|
570
|
+
if (k.tagNumber <= lastTag) throw ns.E(C, "AuthorityKeyIdentifier fields must be unique and in ascending order (DER)");
|
|
571
|
+
lastTag = k.tagNumber;
|
|
572
|
+
// keyIdentifier [0] IMPLICIT OCTET STRING and authorityCertSerialNumber
|
|
573
|
+
// [2] IMPLICIT INTEGER are primitive leaves; the readers enforce the
|
|
574
|
+
// primitive form fail-closed (a constructed context node is rejected,
|
|
575
|
+
// never dereferenced for its absent content).
|
|
576
|
+
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); } }
|
|
577
|
+
else if (k.tagNumber === 1) out.authorityCertIssuer = schema.walk(generalNames(ns, { implicitTag: 1, decodeValue: true, code: C }), k, ns).result;
|
|
578
|
+
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); } }
|
|
579
|
+
else throw ns.E(C, "AuthorityKeyIdentifier has an unexpected field [" + k.tagNumber + "]");
|
|
580
|
+
});
|
|
581
|
+
if ((out.authorityCertIssuer === null) !== (out.authorityCertSerialNumber === null)) {
|
|
582
|
+
throw ns.E(C, "AuthorityKeyIdentifier authorityCertIssuer and authorityCertSerialNumber must both be present or both absent (RFC 5280 §4.2.1.1)");
|
|
583
|
+
}
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// subjectKeyIdentifier ::= OCTET STRING
|
|
588
|
+
function subjectKeyIdentifier(buf) {
|
|
589
|
+
var C = ns.prefix + "/bad-extension-value";
|
|
590
|
+
var n = decodeTop(buf, C, "SubjectKeyIdentifier");
|
|
591
|
+
try { return Buffer.concat([asn1.read.octetString(n)]); }
|
|
592
|
+
catch (e) { throw ns.E(C, "SubjectKeyIdentifier must be an OCTET STRING (RFC 5280 §4.2.1.2)", e); }
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function O(nm) { return ns.oid.byName(nm); }
|
|
596
|
+
var byOid = {};
|
|
597
|
+
byOid[O("basicConstraints")] = basicConstraints;
|
|
598
|
+
byOid[O("keyUsage")] = keyUsage;
|
|
599
|
+
byOid[O("nameConstraints")] = nameConstraints;
|
|
600
|
+
byOid[O("certificatePolicies")] = certificatePolicies;
|
|
601
|
+
byOid[O("policyMappings")] = policyMappings;
|
|
602
|
+
byOid[O("policyConstraints")] = policyConstraints;
|
|
603
|
+
byOid[O("inhibitAnyPolicy")] = inhibitAnyPolicy;
|
|
604
|
+
byOid[O("subjectAltName")] = altName;
|
|
605
|
+
byOid[O("issuerAltName")] = altName;
|
|
606
|
+
byOid[O("extKeyUsage")] = extKeyUsage;
|
|
607
|
+
byOid[O("authorityKeyIdentifier")] = authorityKeyIdentifier;
|
|
608
|
+
byOid[O("subjectKeyIdentifier")] = subjectKeyIdentifier;
|
|
609
|
+
return { byOid: byOid };
|
|
610
|
+
}
|
|
611
|
+
|
|
293
612
|
// Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE,
|
|
294
613
|
// extnValue OCTET STRING }. `critical` is a universal BOOLEAN present-by-count
|
|
295
614
|
// (not context-tagged), so the per-extension decode handles the 2-vs-3-child
|
|
@@ -319,12 +638,20 @@ function extension(ns) {
|
|
|
319
638
|
return { oid: extnID, name: ns.oid.name(extnID) || null, critical: critical, value: asn1.read.octetString(valueNode) };
|
|
320
639
|
});
|
|
321
640
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
641
|
+
// opts.implicitTag (optional): read Extensions as a [tag] IMPLICIT SEQUENCE OF
|
|
642
|
+
// Extension — the context tag REPLACES the universal SEQUENCE tag, for the CRMF
|
|
643
|
+
// CertTemplate extensions [9] (RFC 4211 §5). With no opts the shape is a bare
|
|
644
|
+
// universal SEQUENCE OF, byte-identical to every existing caller.
|
|
645
|
+
function extensions(ns, opts) {
|
|
646
|
+
opts = opts || {};
|
|
647
|
+
var EXT = extension(ns);
|
|
648
|
+
var extOpts = {
|
|
649
|
+
min: 1, code: ns.prefix + "/bad-extensions", what: "Extensions",
|
|
650
|
+
unique: function (it) { return it.value.oid; }, dupCode: ns.prefix + "/duplicate-extension",
|
|
326
651
|
build: function (m) { return m.items.map(function (it) { return it.value; }); },
|
|
327
|
-
}
|
|
652
|
+
};
|
|
653
|
+
if (opts.implicitTag != null) return schema.implicitSeqOf(opts.implicitTag, EXT, extOpts);
|
|
654
|
+
return schema.seqOf(EXT, Object.assign({ assert: "sequence" }, extOpts));
|
|
328
655
|
}
|
|
329
656
|
|
|
330
657
|
// SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier,
|
|
@@ -332,17 +659,26 @@ function extensions(ns) {
|
|
|
332
659
|
// universal SEQUENCE — a context-tagged or SET-tagged constructed node carrying
|
|
333
660
|
// two well-formed children is NOT a SubjectPublicKeyInfo. Shared by the
|
|
334
661
|
// certificate and CSR parsers.
|
|
335
|
-
|
|
662
|
+
// opts.implicitTag (optional): read the SubjectPublicKeyInfo as a [tag] IMPLICIT
|
|
663
|
+
// SEQUENCE (a context-class constructed node whose children are algorithm +
|
|
664
|
+
// subjectPublicKey), for the CRMF CertTemplate publicKey [6] (RFC 4211 §5). With no
|
|
665
|
+
// opts the shape is a universal SEQUENCE, byte-identical to every existing caller.
|
|
666
|
+
function spki(ns, opts) {
|
|
667
|
+
opts = opts || {};
|
|
336
668
|
return schema.seq([
|
|
337
669
|
schema.field("algorithm", algorithmIdentifier(ns)),
|
|
338
670
|
schema.field("subjectPublicKey", schema.bitString()),
|
|
339
671
|
], {
|
|
340
|
-
assert:
|
|
672
|
+
assert: opts.implicitTag != null ? "implicit" : "sequence", implicitTag: opts.implicitTag,
|
|
673
|
+
arity: { exact: 2 }, code: ns.prefix + "/bad-spki", what: "SubjectPublicKeyInfo",
|
|
341
674
|
build: function (m) {
|
|
342
675
|
return {
|
|
343
676
|
algorithm: m.fields.algorithm.value.result,
|
|
344
677
|
publicKey: { unusedBits: m.fields.subjectPublicKey.value.unusedBits, bytes: m.fields.subjectPublicKey.value.bytes },
|
|
345
|
-
bytes
|
|
678
|
+
// `bytes` is the importable SubjectPublicKeyInfo DER. In IMPLICIT [tag]
|
|
679
|
+
// mode the wire node leads with the context tag, so recover the SEQUENCE
|
|
680
|
+
// encoding a consumer imports / hashes rather than the [tag] wire form.
|
|
681
|
+
bytes: opts.implicitTag != null ? asn1.sequenceTlv(m.node) : m.node.bytes,
|
|
346
682
|
};
|
|
347
683
|
},
|
|
348
684
|
});
|
|
@@ -446,6 +782,7 @@ module.exports = {
|
|
|
446
782
|
name: name,
|
|
447
783
|
generalName: generalName,
|
|
448
784
|
generalNames: generalNames,
|
|
785
|
+
certExtensionDecoders: certExtensionDecoders,
|
|
449
786
|
attribute: attribute,
|
|
450
787
|
extension: extension,
|
|
451
788
|
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:b6db5d53-2a3e-46f9-9187-9c2f6fd9249b",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-07T05:30:55.753Z",
|
|
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.17",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
25
|
+
"version": "0.1.17",
|
|
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.17",
|
|
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.17",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|