@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.
@@ -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
- function name(ns) {
197
- return schema.seqOf(relativeDistinguishedName(ns), {
198
- assert: "sequence", code: ns.prefix + "/bad-name", what: "Name",
199
- build: function (m) {
200
- var rdns = [], parts = [];
201
- m.items.forEach(function (rdnItem) {
202
- var atvs = [], atvParts = [];
203
- rdnItem.value.items.forEach(function (atvItem) {
204
- var a = atvItem.value.result; // atvItem.value = the atv seq-match; .result = its build result
205
- atvs.push(a);
206
- var label = (a.name && DN_SHORT[a.name]) || a.name || a.type;
207
- atvParts.push(label + "=" + _escapeDnValue(a.value));
208
- });
209
- rdns.push(atvs);
210
- parts.push(atvParts.join("+"));
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
- return { rdns: rdns, dn: parts.join(", ") };
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
- try { asn1.read.oid(n.children[0]); }
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 v = n.children[1];
247
- if (!(v.tagClass === "context" && v.tagNumber === 0 && v.children && v.children.length === 1)) {
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
- if (n.content[i] > 0x7f) throw ctx.E(code, "GeneralName [" + t + "] must be an IA5String (7-bit)");
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 (n.content.length !== 4 && n.content.length !== 16) throw ctx.E(code, "GeneralName iPAddress [7] must be a 4- or 16-octet address");
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
- try { asn1.decodeOidContent(n.content); }
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
- return { bytes: n.bytes, tagClass: n.tagClass, tagNumber: n.tagNumber };
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: [{ bytes,
277
- // tagClass, tagNumber }], bytes } (the decoded element list + the raw outer DER).
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
- var gn = generalName(ns, { code: code });
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
- function extensions(ns) {
323
- return schema.seqOf(extension(ns), {
324
- assert: "sequence", min: 1, code: ns.prefix + "/bad-extensions", what: "Extensions",
325
- unique: function (item) { return item.value.oid; }, dupCode: ns.prefix + "/duplicate-extension",
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
- function spki(ns) {
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: "sequence", arity: { exact: 2 }, code: ns.prefix + "/bad-spki", what: "SubjectPublicKeyInfo",
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: m.node.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:d18de1ff-7ebd-406f-91cb-801da4286f4f",
5
+ "serialNumber": "urn:uuid:b6db5d53-2a3e-46f9-9187-9c2f6fd9249b",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-06T08:32:53.685Z",
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.15",
22
+ "bom-ref": "@blamejs/pki@0.1.17",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.15",
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.15",
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.15",
57
+ "ref": "@blamejs/pki@0.1.17",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]