@blamejs/pki 0.2.32 → 0.2.33

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 CHANGED
@@ -4,6 +4,14 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.2.33 — 2026-07-16
8
+
9
+ Attribute certificates now decode their RFC 5755 attribute values and attribute-certificate extensions, not just the certificate structure.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.attrcert.parse decodes the RFC 5755 section 4.4 attribute values -- role (RoleSyntax), clearance (Clearance, including the classification bit list and security categories), authenticationInfo and accessIdentity (SvceAuthInfo), and group and chargingIdentity (IetfAttrSyntax) -- and the section 4.3 attribute-certificate extensions -- auditIdentity, targetInformation and proxyInfo (Targets, with targetCert reusing the IssuerSerial decoder), noRevAvail, and aaControls. Each is surfaced additively (a decoded field alongside the raw value) through the parse consumer path; an unrecognized attribute type or extension id is preserved opaque, and a malformed recognized value fails closed with a typed error. Every GeneralName inside these structures is validated through the shared name decoder.
14
+
7
15
  ## v0.2.32 — 2026-07-16
8
16
 
9
17
  X.509 certificates now decode the Microsoft Active Directory Certificate Services enrollment extensions, and pki.lint's critical-extension check is aligned with certification-path validation.
package/README.md CHANGED
@@ -211,7 +211,7 @@ is callable today; nothing below is a stub.
211
211
  | `pki.schema.cms` | Parse DER / PEM CMS per RFC 5652 / 5083 / 9629 — SignedData (§5, signer infos + raw signed-attribute bytes for external verification), EnvelopedData (§6, all five RecipientInfo kinds incl. RFC 5753 key-agreement and RFC 9629 KEM recipients with ML-KEM validation), EncryptedData (§8), AuthenticatedData (§9, MAC surface + raw `authAttrsBytes`), and AuthEnvelopedData (RFC 5083, with RFC 5084 AES-GCM/CCM parameter validation); §11 attribute placement enforced, countersignatures validated recursively, certificates / CRLs validated against the closed CHOICE sets and kept raw, every result tagged `contentTypeName`, fail-closed — `parse`, `pemDecode`, `pemEncode` |
212
212
  | `pki.schema.ocsp` | Parse DER / PEM OCSP requests and responses per RFC 6960 — per-certificate status (good / revoked / unknown), responder identity, raw tbs bytes for external verification, certificates kept raw, fail-closed; non-basic response types recognized (not decoded) — `parseRequest`, `parseResponse`, `pemDecode`, `pemEncode` |
213
213
  | `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp requests, responses, and tokens — the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseRequest`, `parseResponse`, `parseTstInfo`, `parseToken`, `pemDecode`, `pemEncode` |
214
- | `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes (role / group / clearance) and extensions, with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode`, `pemEncode` |
214
+ | `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes and extensions **decoded to structured values** (role, clearance, service/access identity, group, charging identity; audit identity, target/proxy information, no-rev-avail, AA controls — unknown types preserved opaque), with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode`, `pemEncode` |
215
215
  | `pki.schema.crmf` | Parse DER / PEM RFC 4211 certificate request messages (CertReqMessages — the CMP / EST enrollment body) — the requested-certificate template (subject, public key, validity, extensions), proof-of-possession, and registration controls, with the raw `CertRequest` region a POP verifier hashes; names dual-accepted (IMPLICIT and EXPLICIT), fail-closed — `parse`, `pemDecode`, `pemEncode` |
216
216
  | `pki.schema.pkcs12` | Parse DER / BER / PEM RFC 7292 PKCS#12 (PFX) stores — key bags via the PKCS#8 parser, shrouded keys (algorithm surfaced, ciphertext opaque), cert / CRL / secret bags raw and byte-exact, encrypted and enveloped safes structurally via CMS, `friendlyName` / `localKeyId` decoded, and the exact MAC byte range (`macedBytes`) plus RFC 9579 PBMAC1 recognition for external verification; BER accepted exactly where §4.1 requires it, fail-closed — `parse`, `pemDecode`, `pemEncode` |
217
217
  | `pki.schema.cmp` | Parse DER / PEM RFC 9810 Certificate Management Protocol messages (PKIMessage) — the header (version, sender / recipient incl. the anonymous NULL-DN, nonces, transaction id, general info), the 27-arm body (certificate requests via the CRMF parser, an encrypted certificate's EnvelopedData via CMS, response / revocation / confirmation / error / support / polling arms structural, the rest raw), and the exact `headerBytes` / `bodyBytes` slices an external verifier reconstructs the protected part from; the CMP-before-OCSP dispatch order is enforced, fail-closed — `parse`, `pemDecode`, `pemEncode` |
@@ -281,7 +281,24 @@ var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
281
281
  // kept stable for consumers keying off `form`.
282
282
  var issuer = { form: "v2Form", v2Form: acinfo.fields.issuer.value.result, v1Form: null };
283
283
 
284
+ // RFC 5755 sec. 4.4 -- decode each attribute's value(s) additively: a recognized attribute type
285
+ // gains `decoded` (parallel to `values`); an unrecognized type falls back to { opaque, bytes } so
286
+ // the parse never fails on an unknown attribute, and a recognized-but-malformed value fails closed.
287
+ var attributes = acinfo.fields.attributes.value.result;
288
+ attributes.forEach(function (a) {
289
+ var dec = _ATTR_VALUE_DECODERS[a.type];
290
+ a.decoded = a.values.map(function (v) { return dec ? dec(v) : { opaque: true, bytes: v }; });
291
+ });
292
+
293
+ // RFC 5755 sec. 4.3 -- decode each AC extension additively (extensions[i].decoded), same opaque
294
+ // fallback + fail-closed-on-malformed contract as the attributes above.
284
295
  var extField = acinfo.fields.extensions;
296
+ var extensions = (extField && extField.present) ? extField.value.result : [];
297
+ extensions.forEach(function (x) {
298
+ var dec = _AC_EXT_DECODERS[x.oid];
299
+ x.decoded = dec ? dec(x.value) : { opaque: true, bytes: x.value };
300
+ });
301
+
285
302
  return {
286
303
  version: acinfo.fields.version.value, // 2
287
304
  holder: acinfo.fields.holder.value.result,
@@ -291,9 +308,9 @@ var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
291
308
  serialNumber: acinfo.fields.serialNumber.value,
292
309
  serialNumberHex: sc.toString("hex"),
293
310
  validity: acinfo.fields.attrCertValidityPeriod.value.result,
294
- attributes: acinfo.fields.attributes.value.result,
311
+ attributes: attributes,
295
312
  issuerUniqueID: acinfo.fields.issuerUniqueID.present ? acinfo.fields.issuerUniqueID.value : null,
296
- extensions: (extField && extField.present) ? extField.value.result : [],
313
+ extensions: extensions,
297
314
  tbsBytes: e.tbsBytes,
298
315
  signatureValue: e.signatureValue,
299
316
  };
@@ -417,6 +434,238 @@ function matchesV1(root) {
417
434
  return schema.isUniversal(k[1], TAGS.SEQUENCE);
418
435
  }
419
436
 
437
+ // ---- RFC 5755 sec. 4.4 attribute-value decoders ----------------------
438
+ // A per-attribute-type decoder table (mirroring pkix.certExtensionDecoders): each takes the raw
439
+ // AttributeValue TLV Buffer and returns a structured value, throwing a typed attrcert/bad-* error on
440
+ // any non-conformant shape. Composes pkix's shared imperative helpers + generalName/generalNames.
441
+ // A single GeneralName decodes as the walk OBJECT itself (gn.tagNumber / gn.value); generalNames
442
+ // (plural) returns { names:[...] } on .result.
443
+ var _AV = pkix._decodeHelpers(NS);
444
+ var _decodeTop = _AV.decodeTop, _seqChildren = _AV.seqChildren, _readInt = _AV.readInt, _Oav = _AV.O;
445
+ var _GN = pkix.generalName(NS, { decodeValue: true, code: "attrcert/bad-general-name" });
446
+ var _GNS_0 = pkix.generalNames(NS, { implicitTag: 0, decodeValue: true, code: "attrcert/bad-general-names" });
447
+ function _isCtx(node, n) { return node && node.tagClass === "context" && node.tagNumber === n; }
448
+ function _isU(node, tag) { return node && node.tagClass === "universal" && node.tagNumber === tag; }
449
+ function _oneGN(node, code) {
450
+ if (!_isCtx(node, 0) && !_isCtx(node, 1) && !_isCtx(node, 2) && !_isCtx(node, 3) &&
451
+ !_isCtx(node, 4) && !_isCtx(node, 5) && !_isCtx(node, 6) && !_isCtx(node, 7) && !_isCtx(node, 8)) {
452
+ throw NS.E(code, "a GeneralName must be a context-tagged [0]..[8] CHOICE alternative");
453
+ }
454
+ var gn = schema.walk(_GN, node, NS);
455
+ return { tagNumber: gn.tagNumber, value: gn.value };
456
+ }
457
+
458
+ // SvceAuthInfo ::= SEQUENCE { service GeneralName, ident GeneralName, authInfo OCTET STRING OPTIONAL }
459
+ // -- authenticationInfo (sec. 4.4.1) allows authInfo; accessIdentity (sec. 4.4.2) MUST omit it.
460
+ function _svceAuthInfo(authInfoAllowed, code) {
461
+ return function (buf) {
462
+ var kids = _seqChildren(buf, code, "SvceAuthInfo");
463
+ if (kids.length < 2 || kids.length > 3) throw NS.E(code, "SvceAuthInfo must be a SEQUENCE { service, ident, authInfo? }");
464
+ var service = _oneGN(kids[0], code);
465
+ var ident = _oneGN(kids[1], code);
466
+ var authInfo = null;
467
+ if (kids.length === 3) {
468
+ if (!authInfoAllowed) throw NS.E(code, "this attribute must not carry authInfo (RFC 5755 sec. 4.4.2)");
469
+ if (!_isU(kids[2], TAGS.OCTET_STRING)) throw NS.E(code, "SvceAuthInfo authInfo must be an OCTET STRING");
470
+ authInfo = Buffer.concat([asn1.read.octetString(kids[2])]);
471
+ }
472
+ return { service: service, ident: ident, authInfo: authInfo };
473
+ };
474
+ }
475
+
476
+ // IetfAttrSyntax ::= SEQUENCE { policyAuthority [0] IMPLICIT GeneralNames OPTIONAL,
477
+ // values SEQUENCE OF CHOICE { octets OCTET STRING, oid OBJECT IDENTIFIER, string UTF8String } }
478
+ function _ietfAttrSyntax(buf) {
479
+ var C = "attrcert/bad-ietf-attr";
480
+ var kids = _seqChildren(buf, C, "IetfAttrSyntax");
481
+ if (!kids.length || kids.length > 2) throw NS.E(C, "IetfAttrSyntax must be a SEQUENCE { policyAuthority [0]?, values }");
482
+ var policyAuthority = null, i = 0;
483
+ if (_isCtx(kids[0], 0)) { policyAuthority = schema.walk(_GNS_0, kids[0], NS).result; i = 1; }
484
+ var valuesNode = kids[i];
485
+ if (!valuesNode || !_isU(valuesNode, TAGS.SEQUENCE) || !valuesNode.children || !valuesNode.children.length) {
486
+ throw NS.E(C, "IetfAttrSyntax values must be a non-empty SEQUENCE OF value");
487
+ }
488
+ if (i + 1 !== kids.length) throw NS.E(C, "IetfAttrSyntax has unexpected trailing fields (a [0] must precede values)");
489
+ var kinds = {};
490
+ var values = valuesNode.children.map(function (v) {
491
+ if (_isU(v, TAGS.OCTET_STRING)) { kinds.octets = true; return { kind: "octets", value: Buffer.concat([asn1.read.octetString(v)]) }; }
492
+ if (_isU(v, TAGS.OBJECT_IDENTIFIER)) { kinds.oid = true; return { kind: "oid", value: asn1.read.oid(v) }; }
493
+ if (_isU(v, TAGS.UTF8_STRING)) { kinds.string = true; return { kind: "string", value: asn1.read.string(v) }; }
494
+ throw NS.E(C, "IetfAttrSyntax value must be an OCTET STRING, OBJECT IDENTIFIER, or UTF8String");
495
+ });
496
+ return { policyAuthority: policyAuthority, values: values, homogeneous: Object.keys(kinds).length === 1 };
497
+ }
498
+
499
+ // RoleSyntax ::= SEQUENCE { roleAuthority [0] IMPLICIT GeneralNames OPTIONAL, roleName [1] EXPLICIT GeneralName }
500
+ function _role(buf) {
501
+ var C = "attrcert/bad-role";
502
+ var kids = _seqChildren(buf, C, "RoleSyntax");
503
+ if (!kids.length || kids.length > 2) throw NS.E(C, "RoleSyntax must be a SEQUENCE { roleAuthority [0]?, roleName [1] }");
504
+ var roleAuthority = null, i = 0;
505
+ if (_isCtx(kids[0], 0)) { roleAuthority = schema.walk(_GNS_0, kids[0], NS).result; i = 1; }
506
+ var rn = kids[i];
507
+ if (!_isCtx(rn, 1) || !rn.children || rn.children.length !== 1) throw NS.E(C, "RoleSyntax roleName must be an EXPLICIT [1] wrapping exactly one GeneralName");
508
+ if (i + 1 !== kids.length) throw NS.E(C, "RoleSyntax has unexpected trailing fields");
509
+ return { roleAuthority: roleAuthority, roleName: _oneGN(rn.children[0], C) };
510
+ }
511
+
512
+ // Clearance ::= SEQUENCE { policyId OBJECT IDENTIFIER, classList ClassList DEFAULT {unclassified},
513
+ // securityCategories SET OF SecurityCategory OPTIONAL }.
514
+ // ClassList ::= BIT STRING { unmarked(0), unclassified(1), restricted(2), confidential(3), secret(4), topSecret(5) }.
515
+ var _CLASSLIST_NAMES = ["unmarked", "unclassified", "restricted", "confidential", "secret", "topSecret"];
516
+ function _decodeClassList(node, C) {
517
+ var bs;
518
+ try { bs = asn1.read.bitString(node); } catch (e) { throw NS.E(C, "Clearance classList must be a BIT STRING", e); }
519
+ schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (m) { throw NS.E(C, m); });
520
+ var names = [], flags = {}, reserved = [];
521
+ var total = bs.bytes.length * 8 - bs.unusedBits;
522
+ for (var bit = 0; bit < total; bit++) {
523
+ if ((bs.bytes[bit >> 3] & (0x80 >> (bit & 7))) === 0) continue;
524
+ if (bit < _CLASSLIST_NAMES.length) { names.push(_CLASSLIST_NAMES[bit]); flags[_CLASSLIST_NAMES[bit]] = true; }
525
+ else reserved.push(bit);
526
+ }
527
+ return { names: names, flags: flags, reservedBits: reserved };
528
+ }
529
+ // SecurityCategory ::= SEQUENCE { type [0] IMPLICIT OBJECT IDENTIFIER, value [1] EXPLICIT ANY }.
530
+ function _securityCategory(node, C) {
531
+ if (!_isU(node, TAGS.SEQUENCE) || !node.children || node.children.length !== 2) throw NS.E(C, "a SecurityCategory must be a SEQUENCE of a type and a value");
532
+ var typeNode = node.children[0], valueNode = node.children[1];
533
+ if (!_isCtx(typeNode, 0) || typeNode.children) throw NS.E(C, "SecurityCategory type must be a primitive [0] IMPLICIT OBJECT IDENTIFIER");
534
+ var type; try { type = asn1.decodeOidContent(typeNode.content); } catch (e) { throw NS.E(C, "SecurityCategory type is not a valid OBJECT IDENTIFIER", e); }
535
+ if (!_isCtx(valueNode, 1) || !valueNode.children || valueNode.children.length !== 1) throw NS.E(C, "SecurityCategory value must be an EXPLICIT [1] wrapping one element");
536
+ return { type: type, valueBytes: Buffer.concat([valueNode.children[0].bytes]) };
537
+ }
538
+ function _clearance(buf) {
539
+ var C = "attrcert/bad-clearance";
540
+ var kids = _seqChildren(buf, C, "Clearance");
541
+ if (!kids.length || kids.length > 3) throw NS.E(C, "Clearance must be a SEQUENCE { policyId, classList?, securityCategories? }");
542
+ if (!_isU(kids[0], TAGS.OBJECT_IDENTIFIER)) throw NS.E(C, "Clearance policyId must be an OBJECT IDENTIFIER");
543
+ var policyId; try { policyId = asn1.read.oid(kids[0]); } catch (e) { throw NS.E(C, "Clearance policyId must be an OBJECT IDENTIFIER", e); }
544
+ var classList = { names: ["unclassified"], flags: { unclassified: true }, reservedBits: [] }, i = 1; // DEFAULT {unclassified}
545
+ if (kids[i] && _isU(kids[i], TAGS.BIT_STRING)) {
546
+ classList = _decodeClassList(kids[i], C);
547
+ if (classList.names.length === 1 && classList.flags.unclassified && !classList.reservedBits.length) {
548
+ throw NS.E(C, "Clearance classList equal to the DEFAULT {unclassified} must be omitted (non-canonical DER)");
549
+ }
550
+ i++;
551
+ }
552
+ var securityCategories = null;
553
+ if (kids[i]) {
554
+ var setNode = kids[i];
555
+ if (!_isU(setNode, TAGS.SET) || !setNode.children || !setNode.children.length) throw NS.E(C, "Clearance securityCategories must be a non-empty SET OF SecurityCategory");
556
+ securityCategories = setNode.children.map(function (categoryNode) { return _securityCategory(categoryNode, C); });
557
+ i++;
558
+ }
559
+ if (i !== kids.length) throw NS.E(C, "Clearance has fields out of order or unexpected trailing fields");
560
+ return { policyId: policyId, classList: classList, securityCategories: securityCategories };
561
+ }
562
+
563
+ var _ATTR_VALUE_DECODERS = {};
564
+ _ATTR_VALUE_DECODERS[_Oav("role")] = _role;
565
+ _ATTR_VALUE_DECODERS[_Oav("clearance")] = _clearance;
566
+ // RFC 5755 sec. 4.4.6 requires decoding the legacy RFC 3281 id-at-clearance too (the X.501
567
+ // selected-attribute-types arc, an alias of "clearance" -- built from arcs, never a dotted literal).
568
+ _ATTR_VALUE_DECODERS[oid.fromArcs([2, 5, 1, 5, 55])] = _clearance;
569
+ _ATTR_VALUE_DECODERS[_Oav("authenticationInfo")] = _svceAuthInfo(true, "attrcert/bad-svce-auth-info");
570
+ _ATTR_VALUE_DECODERS[_Oav("accessIdentity")] = _svceAuthInfo(false, "attrcert/bad-access-identity");
571
+ _ATTR_VALUE_DECODERS[_Oav("chargingIdentity")] = _ietfAttrSyntax;
572
+ _ATTR_VALUE_DECODERS[_Oav("group")] = _ietfAttrSyntax;
573
+
574
+ // ---- RFC 5755 sec. 4.3 AC-extension decoders -------------------------
575
+ // A per-extension-OID decoder table for the AC-specific extensions (kept in the AC domain, not the
576
+ // shared cert-extension table, because Targets/TargetCert reuse the local IssuerSerial). Each takes
577
+ // the raw extnValue content Buffer and returns a structured value, fail-closed with a typed error.
578
+
579
+ // AuditIdentity ::= OCTET STRING (sec. 4.3.1) -- an opaque audit tag, unconstrained (no SIZE), so an
580
+ // empty OCTET STRING (04 00) is valid; only the OCTET STRING type is enforced.
581
+ function _acAuditIdentity(buf) {
582
+ var C = "attrcert/bad-audit-identity";
583
+ var n = _decodeTop(buf, C, "AuditIdentity");
584
+ if (!_isU(n, TAGS.OCTET_STRING)) throw NS.E(C, "AuditIdentity must be an OCTET STRING");
585
+ return Buffer.concat([asn1.read.octetString(n)]);
586
+ }
587
+
588
+ // NoRevAvail syntax is NULL (sec. 4.3.6: '0500'H is the DER encoding) -- the extnValue OCTET STRING
589
+ // contains a DER NULL. An empty or any-other-shape value is malformed and fails closed.
590
+ function _noRevAvail(buf) {
591
+ var C = "attrcert/bad-no-rev-avail";
592
+ var n = _decodeTop(buf, C, "NoRevAvail");
593
+ try { asn1.read.nullValue(n); } catch (e) { throw NS.E(C, "NoRevAvail must be a DER NULL (RFC 5755 sec. 4.3.6)", e); }
594
+ return { noRevAvail: true };
595
+ }
596
+
597
+ // Target ::= CHOICE { targetName [0] GeneralName, targetGroup [1] GeneralName, targetCert [2] TargetCert }.
598
+ // [0]/[1] wrap a GeneralName CHOICE (EXPLICIT); [2] is an IMPLICIT TargetCert SEQUENCE.
599
+ function _acTargetCert(node, C) {
600
+ if (!node.children || !node.children.length) throw NS.E(C, "a targetCert [2] must be a non-empty TargetCert SEQUENCE");
601
+ var out = { kind: "targetCert", targetCertificate: null, targetName: null, certDigestInfo: null }, i = 0;
602
+ out.targetCertificate = schema.walk(ISSUER_SERIAL, node.children[i++], NS).result;
603
+ if (node.children[i] && node.children[i].tagClass === "context") out.targetName = _oneGN(node.children[i++], C);
604
+ if (node.children[i] && _isU(node.children[i], TAGS.SEQUENCE)) out.certDigestInfo = schema.walk(OBJECT_DIGEST_INFO, node.children[i++], NS).result;
605
+ if (i !== node.children.length) throw NS.E(C, "a targetCert has unexpected trailing fields");
606
+ return out;
607
+ }
608
+ function _acTarget(node, C, allowTargetCert) {
609
+ if (_isCtx(node, 0) || _isCtx(node, 1)) {
610
+ if (!node.children || node.children.length !== 1) throw NS.E(C, "a Target name must be an EXPLICIT [0]/[1] wrapping one GeneralName");
611
+ return { kind: node.tagNumber === 0 ? "targetName" : "targetGroup", name: _oneGN(node.children[0], C) };
612
+ }
613
+ if (_isCtx(node, 2)) {
614
+ if (!allowTargetCert) throw NS.E(C, "the targetCert [2] CHOICE MUST NOT be used in proxying information (RFC 5755 sec. 7.4)");
615
+ return _acTargetCert(node, C);
616
+ }
617
+ throw NS.E(C, "a Target must be [0] targetName, [1] targetGroup, or [2] targetCert");
618
+ }
619
+ // Targets ::= SEQUENCE OF Target -- decode a Targets SEQUENCE node into an array of Target.
620
+ function _acDecodeTargets(node, C, allowTargetCert) {
621
+ if (!_isU(node, TAGS.SEQUENCE) || !node.children || !node.children.length) throw NS.E(C, "Targets must be a non-empty SEQUENCE OF Target");
622
+ return node.children.map(function (t) { return _acTarget(t, C, allowTargetCert); });
623
+ }
624
+ // Both targetInformation (sec. 4.3.2) and ProxyInfo (sec. 7.4) are SEQUENCE OF Targets -- an outer
625
+ // SEQUENCE whose every element is itself a Targets (SEQUENCE OF Target). A conforming issuer emits one
626
+ // Targets, but users MUST accept several. proxying additionally forbids the targetCert [2] alternative.
627
+ function _seqOfTargets(buf, C, allowTargetCert) {
628
+ var kids = _seqChildren(buf, C, "SEQUENCE OF Targets");
629
+ if (!kids.length) throw NS.E(C, "a SEQUENCE OF Targets must be non-empty");
630
+ return kids.map(function (targetsNode) { return _acDecodeTargets(targetsNode, C, allowTargetCert); });
631
+ }
632
+ function _targetInformation(buf) { return _seqOfTargets(buf, "attrcert/bad-targets", true); }
633
+ function _acProxying(buf) { return _seqOfTargets(buf, "attrcert/bad-proxy-info", false); }
634
+
635
+ // AAControls ::= SEQUENCE { pathLenConstraint INTEGER (0..MAX) OPTIONAL, permittedAttrs [0] AttrSpec
636
+ // OPTIONAL, excludedAttrs [1] AttrSpec OPTIONAL, permitUnSpecified BOOLEAN DEFAULT TRUE }.
637
+ // AttrSpec ::= SEQUENCE OF OBJECT IDENTIFIER (the [0]/[1] IMPLICIT tag replaces the SEQUENCE tag).
638
+ function _acAttrSpec(node, C) {
639
+ if (!node.children) throw NS.E(C, "an AttrSpec must be a SEQUENCE OF OBJECT IDENTIFIER");
640
+ return node.children.map(function (o) {
641
+ if (!_isU(o, TAGS.OBJECT_IDENTIFIER)) throw NS.E(C, "an AttrSpec element must be an OBJECT IDENTIFIER");
642
+ try { return asn1.read.oid(o); } catch (e) { throw NS.E(C, "an AttrSpec element must be an OBJECT IDENTIFIER", e); }
643
+ });
644
+ }
645
+ function _aaControls(buf) {
646
+ var C = "attrcert/bad-aa-controls";
647
+ var kids = _seqChildren(buf, C, "AAControls");
648
+ if (kids.length > 4) throw NS.E(C, "AAControls has too many fields");
649
+ var out = { pathLenConstraint: null, permittedAttrs: null, excludedAttrs: null, permitUnSpecified: true }, i = 0;
650
+ if (kids[i] && _isU(kids[i], TAGS.INTEGER)) { out.pathLenConstraint = guard.range.uint31(_readInt(kids[i], C, "pathLenConstraint"), NS.E, C, "AAControls pathLenConstraint"); i++; }
651
+ if (kids[i] && _isCtx(kids[i], 0)) { out.permittedAttrs = _acAttrSpec(kids[i], C); i++; }
652
+ if (kids[i] && _isCtx(kids[i], 1)) { out.excludedAttrs = _acAttrSpec(kids[i], C); i++; }
653
+ if (kids[i] && _isU(kids[i], TAGS.BOOLEAN)) {
654
+ var v = asn1.read.boolean(kids[i]);
655
+ if (v === true) throw NS.E(C, "AAControls permitUnSpecified TRUE equals the DEFAULT and must be omitted (non-canonical DER)");
656
+ out.permitUnSpecified = v; i++;
657
+ }
658
+ if (i !== kids.length) throw NS.E(C, "AAControls has fields out of order or unexpected trailing fields");
659
+ return out;
660
+ }
661
+
662
+ var _AC_EXT_DECODERS = {};
663
+ _AC_EXT_DECODERS[_Oav("acAuditIdentity")] = _acAuditIdentity;
664
+ _AC_EXT_DECODERS[_Oav("targetInformation")] = _targetInformation;
665
+ _AC_EXT_DECODERS[_Oav("noRevAvail")] = _noRevAvail;
666
+ _AC_EXT_DECODERS[_Oav("aaControls")] = _aaControls;
667
+ _AC_EXT_DECODERS[_Oav("acProxying")] = _acProxying;
668
+
420
669
  module.exports = {
421
670
  parse: parse,
422
671
  parseV1: parseV1,
@@ -598,13 +598,14 @@ function assertPolicyQualifiers(qNode, fail) {
598
598
  });
599
599
  }
600
600
 
601
- function certExtensionDecoders(ns) {
602
- var GN_SUBTREE = generalName(ns, { decodeValue: true, subtreeBase: true, code: ns.prefix + "/bad-name-constraints" });
603
-
601
+ // Shared imperative decode helpers for the certExtensionDecoders + attrValueDecoders factories. Both
602
+ // take the caller's ns, so the helpers close over ns.E / ns.oid; extracted once so the two factories
603
+ // share one copy (a per-OID value-decoder body is the identical idiom to a per-OID extension body).
604
+ function _decodeHelpers(ns) {
604
605
  function decodeTop(buf, code, what) {
605
606
  var n;
606
607
  try { n = asn1.decode(buf); }
607
- catch (e) { throw ns.E(code, "malformed " + what + " extension value: " + ((e && e.message) || String(e)), e); }
608
+ catch (e) { throw ns.E(code, "malformed " + what + " value: " + ((e && e.message) || String(e)), e); }
608
609
  return n;
609
610
  }
610
611
  function seqChildren(buf, code, what) {
@@ -618,6 +619,19 @@ function certExtensionDecoders(ns) {
618
619
  try { return asn1.read.integer(node); }
619
620
  catch (e) { throw ns.E(code, what + " must be an INTEGER", e); }
620
621
  }
622
+ // Resolve a registered OID name -> dotted string, throwing at module load on a typo (a decoder keyed
623
+ // by an unresolved name would silently never match, treating a real extension/attribute as opaque).
624
+ function O(nm) {
625
+ var d = ns.oid.byName(nm);
626
+ if (typeof d !== "string") throw new TypeError("pki.schema.pkix: " + JSON.stringify(nm) + " is not a registered OID name");
627
+ return d;
628
+ }
629
+ return { decodeTop: decodeTop, seqChildren: seqChildren, readInt: readInt, O: O };
630
+ }
631
+
632
+ function certExtensionDecoders(ns) {
633
+ var GN_SUBTREE = generalName(ns, { decodeValue: true, subtreeBase: true, code: ns.prefix + "/bad-name-constraints" });
634
+ var _h = _decodeHelpers(ns), decodeTop = _h.decodeTop, seqChildren = _h.seqChildren, readInt = _h.readInt, O = _h.O;
621
635
 
622
636
  // basicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLen INTEGER (0..MAX) OPTIONAL }
623
637
  function basicConstraints(buf) {
@@ -910,15 +924,6 @@ function certExtensionDecoders(ns) {
910
924
  });
911
925
  }
912
926
 
913
- // byName returns undefined for an unregistered name; keying a decoder row
914
- // under "undefined" would silently drop that extension from dispatch (the
915
- // consumer would treat the extension as structurally unvalidated / absent),
916
- // so a typo'd key-list entry must fail here, at module load.
917
- function O(nm) {
918
- var d = ns.oid.byName(nm);
919
- if (typeof d !== "string") throw new TypeError("certExtensionDecoders: " + JSON.stringify(nm) + " is not a registered OID name");
920
- return d;
921
- }
922
927
  // qcStatements ::= SEQUENCE OF QCStatement (RFC 3739 sec. 3.2.6). QCStatement ::= SEQUENCE {
923
928
  // statementId OBJECT IDENTIFIER, statementInfo ANY DEFINED BY statementId OPTIONAL }. Known statementIds
924
929
  // (RFC 3739 id-qcs + the ETSI EN 319 412-5 esi4 catalog) decode their statementInfo; an unknown statementId
@@ -1319,6 +1324,7 @@ module.exports = {
1319
1324
  rawNonEmptySequence: rawNonEmptySequence,
1320
1325
  CRL_REASON_NAMES: CRL_REASON_NAMES,
1321
1326
  certExtensionDecoders: certExtensionDecoders,
1327
+ _decodeHelpers: _decodeHelpers,
1322
1328
  attribute: attribute,
1323
1329
  extension: extension,
1324
1330
  extensions: extensions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.2.32",
3
+ "version": "0.2.33",
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:c1caa29b-a81a-443d-9296-76988e159993",
5
+ "serialNumber": "urn:uuid:81b0b6a6-dee2-4e3f-b995-abbe40a0f560",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-16T23:57:14.646Z",
8
+ "timestamp": "2026-07-17T00:29:40.078Z",
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.2.32",
22
+ "bom-ref": "@blamejs/pki@0.2.33",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.2.32",
25
+ "version": "0.2.33",
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.2.32",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.2.33",
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.2.32",
57
+ "ref": "@blamejs/pki@0.2.33",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]