@blamejs/pki 0.3.32 → 0.3.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,19 @@ 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.3.33 — 2026-08-06
8
+
9
+ pki.schema.c509 encodes and decodes the compact certificatePolicies value form -- a C509 certificate's policy identifiers ride their draft-20 registry integers (or unwrapped OIDs) and their CPS-URI and UserNotice qualifiers ride the specific compact CBOR shape, interoperating with a conformant C509 implementation rather than only this decoder.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.c509 encodes and decodes the compact value form for the certificatePolicies extension (draft-ietf-cose-cbor-encoded-cert-20 sec. 3.3): each policy identifier is a sec. 8.9 registry integer -- the CA/Browser Forum validation levels, the RFC 3779 id-cp-ipAddr resource-certificate policies, and the GSMA SGP.22 id-rspRole roles -- or an unwrapped OID for a policy outside the registry; each policy qualifier is a sec. 8.10 integer (id-qt-cps / id-qt-unotice) with its text, reconstructing the CPS pointer as a URI IA5String and the UserNotice as an explicit-text UTF8String. Both directions invert to the DER extnValue byte-for-byte, so a certificate carrying policies is the specific compact shape a conformant C509 implementation reads rather than an opaque DER byte string. The encoder is guarded: a UserNotice with a noticeRef, a non-UTF8String explicit text, or a policy-qualifier identifier outside the sec. 8.10 registry is not compact-representable and falls the whole extension back to the unwrapped-OID byte-string form instead of encoding lossily; a malformed compact value (an empty UserNotice text, a control-byte CPS URI, an unregistered policy or qualifier integer) fails closed with a typed C509Error.
14
+ - The OID registry gains the CA/Browser Forum certificate-policy identifiers (domain-, organization-, individual-validated, ev-guidelines), the RFC 3779 id-cp-ipAddr-asNumber policies, the GSMA SGP.22 id-rspRole roles, and the RFC 5280 id-qt policy qualifiers (cps, unotice), resolvable through pki.oid.byName / pki.oid.name.
15
+
16
+ ### Changed
17
+
18
+ - A C509 certificate carrying certificatePolicies now encodes to -- and decodes from -- its compact CBOR value shape rather than the unwrapped-OID byte-string form an earlier release emitted; both reconstruct the same DER, but the CBOR bytes differ, so re-encode any C509 produced by an earlier release.
19
+
7
20
  ## v0.3.32 — 2026-08-05
8
21
 
9
22
  pki.schema.c509 encodes and decodes the compact general-name extension values -- a C509 certificate's subjectAltName, issuer alternative name, name constraints, CRL distribution points, authority/subject information access, and the authority key identifier issuer form now ride their specific draft-20 CBOR shape and interoperate with a conformant C509 implementation, not only this decoder.
package/README.md CHANGED
@@ -204,7 +204,7 @@ is callable today; nothing below is a stub.
204
204
  | `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation and certificate/PKCS#8 import — the RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare-seed or an internally inconsistent key is rejected with a typed error (KEM encapsulation lands with CMS KEM-decrypt). Zero-dependency, OpenSSL-interoperable |
205
205
  | `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
206
206
  | `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields, with named + partly-decoded extensions — including the RFC 3739 / ETSI EN 319 412-5 qualified-certificate `qcStatements` (EU-qualified declaration, reliance limit, QSCD flag, certificate type, retention, PDS URLs, country of qualification; unknown statements preserved opaque) and the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), fail-closed — `parse`, `pemDecode`, `pemEncode` |
207
- | `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms — the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, and more) and the general-name-bearing extensions (subjectAltName, issuer alt name, name constraints, CRL distribution points, authority/subject information access, and the full authority key identifier) over one shared GeneralNames value codec; a certificate outside the invertible set throws a typed `C509Error` |
207
+ | `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms — the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, and more), the general-name-bearing extensions (subjectAltName, issuer alt name, name constraints, CRL distribution points, authority/subject information access, and the full authority key identifier) over one shared GeneralNames value codec, and certificate policies (registry-integer or OID policy identifiers with their CPS-URI and UserNotice qualifiers); a certificate outside the invertible set throws a typed `C509Error` |
208
208
  | `pki.schema.crl` | Parse DER / PEM X.509 CRLs per RFC 5280 §5 — revoked serials with real-`Date` revocation times, named + partly-decoded extensions, fail-closed — `parse`, `pemDecode`, `pemEncode` |
209
209
  | `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
210
210
  | `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
package/lib/oid.js CHANGED
@@ -134,6 +134,29 @@ var FAMILIES = {
134
134
  // id-on-MACAddress (I-D.ietf-lamps-macaddress-on) on the id-pkix 8 arc.
135
135
  pkixOn: { base: [1, 3, 6, 1, 5, 5, 7, 8], of: { hardwareModuleName: 4, smtpUtf8Mailbox: 9, macAddress: 12 } },
136
136
 
137
+ // CA/Browser Forum certificate policies (draft-ietf-cose-cbor-encoded-cert sec. 8.9 ints 1-4) on the
138
+ // 2.23.140.1 arc: the id-cabf validation levels under .2, ev-guidelines under .1.
139
+ cabfPolicy: { base: [2, 23, 140, 1], of: {
140
+ "ev-guidelines": 1, "domain-validated": [2, 1], "organization-validated": [2, 2], "individual-validated": [2, 3] } },
141
+
142
+ // id-cp-ipAddr-asNumber (RFC 3779 / RFC 8360) on the id-pkix 14 arc -- the RPKI resource-certificate
143
+ // policy OIDs (sec. 8.9 ints 7-8).
144
+ idCp: { base: [1, 3, 6, 1, 5, 5, 7, 14], of: { "id-cp-ipAddr-asNumber": 2, "id-cp-ipAddr-asNumber-v2": 3 } },
145
+
146
+ // GSMA SGP.22 Remote SIM Provisioning roles (sec. 8.9 ints 24-38) on the 2.23.146.1.2.1 id-rspRole arc.
147
+ // The -v2 roles are shallow leaves .{1..7}; the non-v2 roles descend into deeper sub-arcs (draft-20).
148
+ gsmaRspRole: { base: [2, 23, 146, 1, 2, 1], of: {
149
+ "id-rspRole-ci": 0, "id-rspRole-euicc-v2": 1, "id-rspRole-euicc": [0, 0, 0, 0, 0],
150
+ "id-rspRole-eum-v2": 2, "id-rspRole-eum": [0, 0, 0],
151
+ "id-rspRole-dp-tls-v2": 3, "id-rspRole-dp-tls": [0, 0, 1, 0],
152
+ "id-rspRole-dp-auth-v2": 4, "id-rspRole-dp-auth": [0, 0, 1, 1],
153
+ "id-rspRole-dp-pb-v2": 5, "id-rspRole-dp-pb": [0, 0, 1, 2],
154
+ "id-rspRole-ds-tls-v2": 6, "id-rspRole-ds-tls": [0, 0, 2, 0],
155
+ "id-rspRole-ds-auth-v2": 7, "id-rspRole-ds-auth": [0, 0, 2, 1] } },
156
+
157
+ // id-qt policy qualifiers (RFC 5280 sec. 4.2.1.4) on the id-pkix 2 arc -- the C509 sec. 8.10 registry.
158
+ pkixQt: { base: [1, 3, 6, 1, 5, 5, 7, 2], of: { cps: 1, unotice: 2 } },
159
+
137
160
  // OCSP (RFC 6960) on the id-pkix-ocsp arc (= id-ad-ocsp). id-pkix-ocsp-basic is
138
161
  // the ResponseBytes.responseType this build decodes; id-pkix-ocsp-nonce (sec. 4.4.1)
139
162
  // names the nonce extension; the remaining members name the other OCSP extensions
@@ -83,6 +83,7 @@ var EXT_BY_INT = {
83
83
  3: _name("subjectAltName"),
84
84
  4: _name("basicConstraints"),
85
85
  5: _name("cRLDistributionPoints"),
86
+ 6: _name("certificatePolicies"),
86
87
  7: _name("authorityKeyIdentifier"),
87
88
  8: _name("extKeyUsage"),
88
89
  9: _name("authorityInfoAccess"),
@@ -100,7 +101,7 @@ var EXT_COMPACT = {
100
101
  subjectKeyIdentifier: 1, keyUsage: 1, basicConstraints: 1, authorityKeyIdentifier: 1,
101
102
  extKeyUsage: 1, inhibitAnyPolicy: 1, ocspNoCheck: 1, tlsFeature: 1,
102
103
  subjectAltName: 1, issuerAltName: 1, nameConstraints: 1, cRLDistributionPoints: 1,
103
- freshestCRL: 1, authorityInfoAccess: 1, subjectInfoAccess: 1,
104
+ freshestCRL: 1, authorityInfoAccess: 1, subjectInfoAccess: 1, certificatePolicies: 1,
104
105
  };
105
106
  // sec. 8.12 Extended Key Usages registry (C509 int -> registered id-kp purpose name). A KeyPurposeId
106
107
  // outside this set encodes as an unwrapped ~oid; a C509 int outside it fails closed on decode.
@@ -141,6 +142,28 @@ var IA_BY_INT = {
141
142
  var IA_TO_INT = {};
142
143
  Object.keys(IA_BY_INT).forEach(function (k) { IA_TO_INT[oid.byName(IA_BY_INT[k])] = Number(k); });
143
144
 
145
+ // sec. 8.9 C509 Certificate Policies registry (int -> registered policy OID name; the CA/Browser Forum
146
+ // levels, the RFC 3779 id-cp-ipAddr policies, and the GSMA SGP.22 id-rspRole roles). A policy OID outside
147
+ // this set encodes as ~oid; a C509 policy int outside it fails closed. CP_TO_INT is the derived inverse.
148
+ var CP_BY_INT = {
149
+ 0: _name("anyPolicy"), 1: _name("domain-validated"), 2: _name("organization-validated"),
150
+ 3: _name("individual-validated"), 4: _name("ev-guidelines"),
151
+ 7: _name("id-cp-ipAddr-asNumber"), 8: _name("id-cp-ipAddr-asNumber-v2"),
152
+ 24: _name("id-rspRole-ci"), 25: _name("id-rspRole-euicc-v2"), 26: _name("id-rspRole-euicc"),
153
+ 27: _name("id-rspRole-eum-v2"), 28: _name("id-rspRole-eum"),
154
+ 29: _name("id-rspRole-dp-tls-v2"), 30: _name("id-rspRole-dp-tls"),
155
+ 31: _name("id-rspRole-dp-auth-v2"), 32: _name("id-rspRole-dp-auth"),
156
+ 33: _name("id-rspRole-dp-pb-v2"), 34: _name("id-rspRole-dp-pb"),
157
+ 35: _name("id-rspRole-ds-tls-v2"), 36: _name("id-rspRole-ds-tls"),
158
+ 37: _name("id-rspRole-ds-auth-v2"), 38: _name("id-rspRole-ds-auth"),
159
+ };
160
+ var CP_TO_INT = {};
161
+ Object.keys(CP_BY_INT).forEach(function (k) { CP_TO_INT[oid.byName(CP_BY_INT[k])] = Number(k); });
162
+ // sec. 8.10 C509 Policies Qualifiers registry (int -> id-qt qualifier OID name; the derived inverse).
163
+ var PQ_BY_INT = { 1: _name("cps"), 2: _name("unotice") };
164
+ var PQ_TO_INT = {};
165
+ Object.keys(PQ_BY_INT).forEach(function (k) { PQ_TO_INT[oid.byName(PQ_BY_INT[k])] = Number(k); });
166
+
144
167
  // ---- field readers (the unwrapped ~biguint / ~time / ~oid contracts; draft-20 sec. 3.1) ----
145
168
 
146
169
  // ~biguint (sec. 3.1.2): a BARE byte string (major type 2), big-endian magnitude, the non-negative
@@ -531,6 +554,66 @@ function _serialIntContent(node) {
531
554
  return (mag[0] & 0x80) ? Buffer.concat([Buffer.from([0x00]), mag]) : mag;
532
555
  }
533
556
 
557
+ // ---- certificatePolicies (draft-20 sec. 3.3 / sec. 8.9 / sec. 8.10; RFC 5280 sec. 4.2.1.4) ----
558
+ // A CBOR text -> a UNIVERSAL IA5String TLV, running the same 7-bit + CVE-2009-2408 control-byte guard the
559
+ // general-name IA5 arms use (never the bare b.ia5, which permits control bytes). Non-empty (the general-name
560
+ // IA5 arms reject empty too; a CPSuri parity tightening -- RFC 5280 sets no SIZE floor on CPSuri).
561
+ function _ia5Universal(text, label) {
562
+ if (text.length === 0) throw _err("c509/bad-extensions", label + " must be non-empty");
563
+ for (var i = 0; i < text.length; i++) { if (text.charCodeAt(i) > 0x7f) throw _err("c509/bad-extensions", label + " must be a 7-bit IA5String"); }
564
+ guard.name.assertPrintableIa5(Buffer.from(text, "latin1"), _err, "c509/bad-extensions", label);
565
+ return b.ia5(text);
566
+ }
567
+ // A CBOR policyIdentifier (a sec. 8.9 registry int, or an unwrapped ~oid) -> the dotted policy OID.
568
+ function _policyIdToDerOid(node) {
569
+ if (node.majorType === 0 || node.majorType === 1) {
570
+ var i = Number(cbor.read.int(node)), nm = CP_BY_INT[i];
571
+ if (nm === undefined) throw _err("c509/bad-extensions", "a certificatePolicies policy int " + i + " has no C509 sec. 8.9 registry row");
572
+ return oid.byName(nm);
573
+ }
574
+ return _oidName(node, "c509/bad-extensions", "a certificatePolicies policyIdentifier").oid;
575
+ }
576
+ // The inverse: a dotted policy OID -> CBOR (a sec. 8.9 int if registered, else an unwrapped ~oid).
577
+ function _policyIdFromDer(dotted) {
578
+ var i = CP_TO_INT[dotted];
579
+ return i !== undefined ? cbor.build.int(BigInt(i)) : _oidCbor(dotted);
580
+ }
581
+ // (qualifierId CBOR, qualifier-text CBOR) -> one DER PolicyQualifierInfo SEQUENCE { policyQualifierId OID,
582
+ // qualifier ANY DEFINED BY }. The qualifierId resolves to a sec. 8.10 int ONLY: a ~oid / unregistered
583
+ // qualifierId has no defined qualifier:text semantics, so it is not compact-representable (fail closed, never
584
+ // guess the ANY type). id-qt-cps -> CPSuri IA5String; id-qt-unotice -> UserNotice { explicitText utf8String }.
585
+ function _qualifierToDer(qidNode, qtextNode) {
586
+ if (qidNode.majorType !== 0 && qidNode.majorType !== 1) throw _err("c509/bad-extensions", "a policyQualifierId must be a C509 sec. 8.10 int (a ~oid qualifier is not compact-representable)");
587
+ var qi = Number(cbor.read.int(qidNode)), nm = PQ_BY_INT[qi];
588
+ if (nm === undefined) throw _err("c509/bad-extensions", "a policyQualifierId int " + qi + " has no C509 sec. 8.10 registry row");
589
+ if (qtextNode.majorType !== 3) throw _err("c509/bad-extensions", "a policyQualifier value must be a CBOR text string");
590
+ var text = cbor.read.textString(qtextNode);
591
+ if (qi === 1) return b.sequence([b.oid(oid.byName("cps")), _ia5Universal(text, "a CPSuri")]); // CPSuri ::= IA5String
592
+ // id-qt-unotice: UserNotice ::= SEQUENCE { explicitText utf8String } -- noticeRef omitted, DisplayText SIZE 1..200 floor.
593
+ if (text.length === 0) throw _err("c509/bad-extensions", "a UserNotice explicitText must be non-empty (DisplayText SIZE 1..200)");
594
+ return b.sequence([b.oid(oid.byName("unotice")), b.sequence([b.utf8(text)])]);
595
+ }
596
+ // One DER PolicyQualifierInfo node -> [ qidCbor, qtextCbor ], or null when not compact-representable: an
597
+ // unregistered qualifierId, a cps qualifier whose value is not a plain IA5String, or a UserNotice that is
598
+ // anything but SEQUENCE { explicitText utf8String } (a noticeRef, a non-UTF8String DisplayText arm, or more
599
+ // than the one explicitText child) -> whole-extension ~oid fallback.
600
+ function _qualifierFromDer(pq) {
601
+ var qid = asn1.read.oid(pq.children[0]), qi = PQ_TO_INT[qid];
602
+ if (qi === undefined) return null;
603
+ var q = pq.children[1];
604
+ if (qi === 1) { // CPSuri ::= IA5String
605
+ if (q.tagClass !== "universal" || q.tagNumber !== asn1.TAGS.IA5_STRING) return null;
606
+ var uri; try { uri = asn1.read.string(q); } catch (_e) { return null; }
607
+ return [cbor.build.int(1n), cbor.build.textString(uri)];
608
+ }
609
+ // id-qt-unotice: SEQUENCE { explicitText utf8String } ONLY (noticeRef absent).
610
+ if (q.tagClass !== "universal" || q.tagNumber !== asn1.TAGS.SEQUENCE || !q.children || q.children.length !== 1) return null;
611
+ var dt = q.children[0];
612
+ if (dt.tagClass !== "universal" || dt.tagNumber !== asn1.TAGS.UTF8_STRING) return null;
613
+ var txt; try { txt = asn1.read.string(dt); } catch (_e2) { return null; }
614
+ return [cbor.build.int(2n), cbor.build.textString(txt)];
615
+ }
616
+
534
617
  // One CBOR DistributionPointName [ fullName, reasons, cRLIssuer ] -> one DER DistributionPoint SEQUENCE.
535
618
  // distributionPoint [0] is EXPLICIT (it wraps the DistributionPointName CHOICE); fullName [0], reasons [1],
536
619
  // cRLIssuer [2] are IMPLICIT (RFC 5280 sec. 4.2.1.13) -- mixing these is the classic byte-exactness trap.
@@ -662,6 +745,31 @@ function _extValueToDer(name, node) {
662
745
  if (node.majorType === 3) return b.sequence([b.sequence([b.explicit(0, b.contextConstructed(0, b.contextPrimitive(6, _ia5Bytes(node, 6))))])]);
663
746
  if (node.majorType !== 4 || !node.children || node.children.length < 1) throw _err("c509/bad-extensions", "a " + name + " value must be a CBOR array of DistributionPoints or a bare URI text (sec. 3.3)");
664
747
  return b.sequence(node.children.map(function (dp) { return _dpToDer(dp); }));
748
+ case "certificatePolicies": { // [ pid, [ *(qid, qtext) ], ... ] -> SEQUENCE OF PolicyInformation
749
+ if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a certificatePolicies value must be a CBOR array");
750
+ var cpKids = node.children;
751
+ if (cpKids.length === 0 || cpKids.length % 2 !== 0) throw _err("c509/bad-extensions", "a certificatePolicies array must be non-empty (policyIdentifier, qualifiers) pairs (sec. 3.3)");
752
+ var polInfos = [], seenPolicy = {};
753
+ for (var cpi = 0; cpi + 1 < cpKids.length; cpi += 2) {
754
+ var quals = cpKids[cpi + 1];
755
+ if (quals.majorType !== 4 || !quals.children) throw _err("c509/bad-extensions", "a certificatePolicies qualifiers slot must be a CBOR array");
756
+ // A policy OID MUST NOT appear more than once (RFC 5280 sec. 4.2.1.4); the toolkit's own DER decoder
757
+ // rejects a duplicate (schema-pkix certExtensionDecoders), so reconstruct fails closed rather than
758
+ // producing a certificatePolicies extension that decoder would reject.
759
+ var policyOid = _policyIdToDerOid(cpKids[cpi]);
760
+ if (seenPolicy[policyOid]) throw _err("c509/bad-extensions", "a certificatePolicies policy OID must not appear more than once (RFC 5280 sec. 4.2.1.4)");
761
+ seenPolicy[policyOid] = true;
762
+ var polFields = [b.oid(policyOid)];
763
+ if (quals.children.length) {
764
+ if (quals.children.length % 2 !== 0) throw _err("c509/bad-extensions", "a policyQualifiers array must be (policyQualifierId, qualifier) pairs");
765
+ var pqDers = [];
766
+ for (var qk = 0; qk + 1 < quals.children.length; qk += 2) pqDers.push(_qualifierToDer(quals.children[qk], quals.children[qk + 1]));
767
+ polFields.push(b.sequence(pqDers)); // policyQualifiers SEQUENCE OF PolicyQualifierInfo
768
+ }
769
+ polInfos.push(b.sequence(polFields));
770
+ }
771
+ return b.sequence(polInfos);
772
+ }
665
773
  default:
666
774
  throw _err("c509/bad-extensions", "extension " + name + " has no compact value decoder");
667
775
  }
@@ -775,6 +883,29 @@ function _extValueFromDer(name, der) {
775
883
  if (dpResults.length === 1 && dpResults[0].oneUri != null && dpResults[0].noReasons && dpResults[0].noIssuer) return cbor.build.textString(dpResults[0].oneUri);
776
884
  return cbor.build.array(dpResults.map(function (r) { return r.triple; }));
777
885
  }
886
+ case "certificatePolicies": { // SEQUENCE OF PolicyInformation -> [ pid, [ *(qid, qtext) ], ... ]
887
+ if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length < 1) return null;
888
+ var cpOut = [];
889
+ for (var pli = 0; pli < node.children.length; pli++) {
890
+ var pol = node.children[pli];
891
+ if (pol.tagClass !== "universal" || pol.tagNumber !== asn1.TAGS.SEQUENCE || !pol.children || pol.children.length < 1 || pol.children.length > 2) return null;
892
+ cpOut.push(_policyIdFromDer(asn1.read.oid(pol.children[0])));
893
+ var qArr = [];
894
+ if (pol.children.length === 2) {
895
+ var qseq = pol.children[1];
896
+ if (qseq.tagClass !== "universal" || qseq.tagNumber !== asn1.TAGS.SEQUENCE || !qseq.children || !qseq.children.length) return null; // policyQualifiers SIZE 1..MAX
897
+ for (var qj = 0; qj < qseq.children.length; qj++) {
898
+ var pq = qseq.children[qj];
899
+ if (pq.tagClass !== "universal" || pq.tagNumber !== asn1.TAGS.SEQUENCE || !pq.children || pq.children.length !== 2) return null;
900
+ var qpair = _qualifierFromDer(pq);
901
+ if (qpair == null) return null; // any non-cps/unotice qualifier -> whole-ext fallback
902
+ qArr.push(qpair[0]); qArr.push(qpair[1]);
903
+ }
904
+ }
905
+ cpOut.push(cbor.build.array(qArr)); // empty [] when policyQualifiers absent
906
+ }
907
+ return cbor.build.array(cpOut);
908
+ }
778
909
  default:
779
910
  return null;
780
911
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.3.32",
3
+ "version": "0.3.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:0d0e1498-d791-4209-890f-c1d837673a21",
5
+ "serialNumber": "urn:uuid:d39fe4d9-9f98-4126-8a96-8660ce1805e0",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-05T23:30:49.498Z",
8
+ "timestamp": "2026-08-06T04:08:23.299Z",
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.3.32",
22
+ "bom-ref": "@blamejs/pki@0.3.33",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.3.32",
25
+ "version": "0.3.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.3.32",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.3.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.3.32",
57
+ "ref": "@blamejs/pki@0.3.33",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]