@blamejs/pki 0.3.32 → 0.4.0

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,36 @@ 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.4.0 — 2026-08-06
8
+
9
+ CRL issuance and verification, PKCS#12 build and open, attribute-certificate issuance, and the key-material lifecycle graduate to stable, and pki.schema.c509 adds the compact policyMappings and policyConstraints value forms -- a C509 certificate's policy mappings and policy constraints now ride their specific draft-20 CBOR shape and interoperate with a conformant C509 implementation rather than only this decoder.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.c509 encodes and decodes the compact value forms for the policyMappings and policyConstraints extensions (draft-ietf-cose-cbor-encoded-cert-20 sec. 3.3): a policy mapping's issuerDomainPolicy and subjectDomainPolicy each ride the sec. 8.9 registry-integer / unwrapped-OID policy space the certificatePolicies extension uses (a policy mapping to or from the special anyPolicy is preserved, matching what a certificate's own decoder accepts), and a policy constraints value rides the fixed two-element [requireExplicitPolicy, inhibitPolicyMapping] array, each field a non-negative skip count or absent. Both directions invert to the DER extnValue byte-for-byte, so a certificate carrying either extension is the specific compact shape a conformant C509 implementation reads rather than an opaque DER byte string. The encoder is guarded: a value the compact form cannot hold falls the whole extension back to the unwrapped-OID byte-string form instead of encoding lossily; a malformed compact value -- an empty or both-absent policy constraints, an odd-length or empty policy-mappings array, a policy mapping member that is not a two-policy pair, or an unregistered policy integer -- fails closed with a typed C509Error.
14
+
15
+ ### Changed
16
+
17
+ - pki.crl.sign / verify / isRevoked (RFC 5280 sec. 5), pki.pkcs12.build / open / verifyMac (RFC 7292 / RFC 9579), pki.attrcert.sign (RFC 5755), and the key-material lifecycle pki.key.encrypt / decrypt / export / import / generate / publicFromPrivate (PKCS#8 / RFC 5958, RFC 8018) graduate from experimental to stable. Their governing standards are settled and each is proven against an independent implementation in the integration harness (OpenSSL), or for the attribute-certificate format through the toolkit's own conformance-vector round-trip plus coverage-guided fuzzing. They are now covered by the stability contract: a breaking change to any of them ships only after a prior deprecation cycle, never silently in a minor (the published LTS support window itself takes effect at v1.0).
18
+ - A C509 certificate carrying policyMappings or policyConstraints 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
+
20
+ ### Fixed
21
+
22
+ - The C509 encoder now bounds the basicConstraints path length and the inhibitAnyPolicy and policyConstraints skip counts to the same non-negative 31-bit range the toolkit's own certificate decoders enforce. A native C509 carrying one of these counts past that range now fails closed with a typed C509Error rather than reconstructing a DER an X.509 decoder -- this toolkit's included -- would then reject.
23
+
24
+ ## v0.3.33 — 2026-08-05
25
+
26
+ 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.
27
+
28
+ ### Added
29
+
30
+ - 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.
31
+ - 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.
32
+
33
+ ### Changed
34
+
35
+ - 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.
36
+
7
37
  ## v0.3.32 — 2026-08-05
8
38
 
9
39
  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, certificate policies (registry-integer or OID policy identifiers with their CPS-URI and UserNotice qualifiers), and policy mappings and policy constraints; 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` |
@@ -408,7 +408,7 @@ function _buildExtensions(extSpec, aaSpki) {
408
408
  * @primitive pki.attrcert.sign
409
409
  * @signature pki.attrcert.sign(spec, issuer, opts?) -> Promise<Buffer|string>
410
410
  * @since 0.3.2
411
- * @status experimental
411
+ * @status stable
412
412
  * @spec RFC 5755
413
413
  * @defends forged-attribute-certificate (CWE-347)
414
414
  * @related pki.schema.attrcert.parse, pki.x509.sign
package/lib/crl-sign.js CHANGED
@@ -454,7 +454,7 @@ function _sign(spec, issuer, opts) {
454
454
  * @primitive pki.crl.sign
455
455
  * @signature pki.crl.sign(spec, issuer, opts?) -> Promise<Buffer|string>
456
456
  * @since 0.3.9
457
- * @status experimental
457
+ * @status stable
458
458
  * @spec RFC 5280 sec. 5, RFC 9882, RFC 9814
459
459
  * @defends crl-forgery (CWE-347)
460
460
  * @related pki.schema.crl.parse, pki.crl.verify, pki.path.crlChecker
@@ -510,7 +510,7 @@ function _resolveIssuerSpki(issuer) {
510
510
  * @primitive pki.crl.verify
511
511
  * @signature pki.crl.verify(crl, issuer) -> Promise<boolean>
512
512
  * @since 0.3.9
513
- * @status experimental
513
+ * @status stable
514
514
  * @spec RFC 5280 sec. 5.1.1.3, RFC 9814
515
515
  * @defends crl-signature-bypass (CWE-347)
516
516
  * @related pki.crl.sign, pki.path.crlChecker, pki.schema.crl.parse
@@ -549,7 +549,7 @@ function _serialHexOf(serial) {
549
549
  * @primitive pki.crl.isRevoked
550
550
  * @signature pki.crl.isRevoked(crl, serialNumber) -> entry | null
551
551
  * @since 0.3.9
552
- * @status experimental
552
+ * @status stable
553
553
  * @spec RFC 5280 sec. 5.1.2.6
554
554
  * @related pki.crl.verify, pki.schema.crl.parse
555
555
  *
package/lib/key.js CHANGED
@@ -81,7 +81,7 @@ async function _toPrivateKeyDer(input) {
81
81
  * @primitive pki.key.encrypt
82
82
  * @signature pki.key.encrypt(privateKey, password, opts?) -> Promise<Buffer|string>
83
83
  * @since 0.3.10
84
- * @status experimental
84
+ * @status stable
85
85
  * @spec RFC 5958 sec. 3, RFC 8018
86
86
  * @related pki.key.decrypt, pki.schema.pkcs8.parseEncrypted, pki.cms.encrypt
87
87
  *
@@ -132,7 +132,7 @@ async function encrypt(privateKey, password, opts) {
132
132
  * @primitive pki.key.decrypt
133
133
  * @signature pki.key.decrypt(encrypted, password, opts?) -> Promise<Buffer|string>
134
134
  * @since 0.3.10
135
- * @status experimental
135
+ * @status stable
136
136
  * @spec RFC 5958 sec. 3, RFC 8018 sec. 6.2, RFC 8018 sec. 8
137
137
  * @defends pbes2-padding-oracle (CWE-208), pbkdf2-work-dos (CWE-400)
138
138
  * @related pki.key.encrypt, pki.schema.pkcs8.parse
@@ -187,7 +187,7 @@ function _decryptPbes2(encAlg, ciphertext, password, opts) {
187
187
  * @primitive pki.key.export
188
188
  * @signature pki.key.export(key, opts?) -> Promise<Buffer|string>
189
189
  * @since 0.3.10
190
- * @status experimental
190
+ * @status stable
191
191
  * @spec RFC 5958, RFC 5280 sec. 4.1.2.7, RFC 8410 sec. 3
192
192
  * @related pki.key.import, pki.key.publicFromPrivate, pki.schema.pkcs8.parse
193
193
  *
@@ -222,7 +222,7 @@ async function export_(key, opts) {
222
222
  * @primitive pki.key.import
223
223
  * @signature pki.key.import(input, opts?) -> Promise<CryptoKey>
224
224
  * @since 0.3.10
225
- * @status experimental
225
+ * @status stable
226
226
  * @spec RFC 5958, RFC 5280 sec. 4.1.2.7, RFC 8018
227
227
  * @related pki.key.export, pki.key.decrypt
228
228
  *
@@ -265,7 +265,7 @@ async function import_(input, opts) {
265
265
  * @primitive pki.key.generate
266
266
  * @signature pki.key.generate(algorithm, opts?) -> Promise<{ privateKey, publicKey }>
267
267
  * @since 0.3.10
268
- * @status experimental
268
+ * @status stable
269
269
  * @spec W3C WebCrypto, FIPS 203, FIPS 204
270
270
  * @related pki.key.export, pki.key.publicFromPrivate
271
271
  *
@@ -296,7 +296,7 @@ async function generate(algorithm, opts) {
296
296
  * @primitive pki.key.publicFromPrivate
297
297
  * @signature pki.key.publicFromPrivate(privateKey, opts?) -> Promise<Buffer|string>
298
298
  * @since 0.3.10
299
- * @status experimental
299
+ * @status stable
300
300
  * @spec RFC 5280 sec. 4.1.2.7, RFC 8410 sec. 3
301
301
  * @related pki.key.export, pki.key.import
302
302
  *
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
@@ -403,7 +403,7 @@ function _normalizeSpec(spec, opts) {
403
403
  * @primitive pki.pkcs12.build
404
404
  * @signature pki.pkcs12.build(spec, opts?) -> Promise<Buffer|string>
405
405
  * @since 0.3.11
406
- * @status experimental
406
+ * @status stable
407
407
  * @spec RFC 7292, RFC 9579, RFC 8018
408
408
  * @related pki.schema.pkcs12.parse, pki.pkcs12.verifyMac
409
409
  *
@@ -468,7 +468,7 @@ async function build(spec, opts) {
468
468
  * @primitive pki.pkcs12.verifyMac
469
469
  * @signature pki.pkcs12.verifyMac(pfx, password, opts?) -> Promise<boolean>
470
470
  * @since 0.3.11
471
- * @status experimental
471
+ * @status stable
472
472
  * @spec RFC 7292 sec. 5.1, RFC 9579
473
473
  * @defends pkcs12-mac-forgery (CWE-347)
474
474
  * @related pki.pkcs12.build, pki.schema.pkcs12.parse
@@ -533,7 +533,7 @@ function _capWork(iterations, salt, opts, keyLength, hardCap) {
533
533
  * @primitive pki.pkcs12.open
534
534
  * @signature pki.pkcs12.open(pfx, password, opts?) -> Promise<OpenResult>
535
535
  * @since 0.3.12
536
- * @status experimental
536
+ * @status stable
537
537
  * @spec RFC 7292 sec. 5.1, RFC 9579, RFC 8018
538
538
  * @defends pkcs12-unauthenticated-decrypt (CWE-347), pbes2-padding-oracle (CWE-208)
539
539
  * @related pki.pkcs12.build, pki.pkcs12.verifyMac, pki.schema.pkcs12.parse
@@ -83,11 +83,14 @@ 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"),
89
90
  25: _name("issuerAltName"),
90
91
  26: _name("nameConstraints"),
92
+ 27: _name("policyMappings"),
93
+ 28: _name("policyConstraints"),
91
94
  29: _name("freshestCRL"),
92
95
  30: _name("inhibitAnyPolicy"),
93
96
  31: _name("subjectInfoAccess"),
@@ -100,7 +103,8 @@ var EXT_COMPACT = {
100
103
  subjectKeyIdentifier: 1, keyUsage: 1, basicConstraints: 1, authorityKeyIdentifier: 1,
101
104
  extKeyUsage: 1, inhibitAnyPolicy: 1, ocspNoCheck: 1, tlsFeature: 1,
102
105
  subjectAltName: 1, issuerAltName: 1, nameConstraints: 1, cRLDistributionPoints: 1,
103
- freshestCRL: 1, authorityInfoAccess: 1, subjectInfoAccess: 1,
106
+ freshestCRL: 1, authorityInfoAccess: 1, subjectInfoAccess: 1, certificatePolicies: 1,
107
+ policyMappings: 1, policyConstraints: 1,
104
108
  };
105
109
  // sec. 8.12 Extended Key Usages registry (C509 int -> registered id-kp purpose name). A KeyPurposeId
106
110
  // outside this set encodes as an unwrapped ~oid; a C509 int outside it fails closed on decode.
@@ -141,6 +145,28 @@ var IA_BY_INT = {
141
145
  var IA_TO_INT = {};
142
146
  Object.keys(IA_BY_INT).forEach(function (k) { IA_TO_INT[oid.byName(IA_BY_INT[k])] = Number(k); });
143
147
 
148
+ // sec. 8.9 C509 Certificate Policies registry (int -> registered policy OID name; the CA/Browser Forum
149
+ // levels, the RFC 3779 id-cp-ipAddr policies, and the GSMA SGP.22 id-rspRole roles). A policy OID outside
150
+ // this set encodes as ~oid; a C509 policy int outside it fails closed. CP_TO_INT is the derived inverse.
151
+ var CP_BY_INT = {
152
+ 0: _name("anyPolicy"), 1: _name("domain-validated"), 2: _name("organization-validated"),
153
+ 3: _name("individual-validated"), 4: _name("ev-guidelines"),
154
+ 7: _name("id-cp-ipAddr-asNumber"), 8: _name("id-cp-ipAddr-asNumber-v2"),
155
+ 24: _name("id-rspRole-ci"), 25: _name("id-rspRole-euicc-v2"), 26: _name("id-rspRole-euicc"),
156
+ 27: _name("id-rspRole-eum-v2"), 28: _name("id-rspRole-eum"),
157
+ 29: _name("id-rspRole-dp-tls-v2"), 30: _name("id-rspRole-dp-tls"),
158
+ 31: _name("id-rspRole-dp-auth-v2"), 32: _name("id-rspRole-dp-auth"),
159
+ 33: _name("id-rspRole-dp-pb-v2"), 34: _name("id-rspRole-dp-pb"),
160
+ 35: _name("id-rspRole-ds-tls-v2"), 36: _name("id-rspRole-ds-tls"),
161
+ 37: _name("id-rspRole-ds-auth-v2"), 38: _name("id-rspRole-ds-auth"),
162
+ };
163
+ var CP_TO_INT = {};
164
+ Object.keys(CP_BY_INT).forEach(function (k) { CP_TO_INT[oid.byName(CP_BY_INT[k])] = Number(k); });
165
+ // sec. 8.10 C509 Policies Qualifiers registry (int -> id-qt qualifier OID name; the derived inverse).
166
+ var PQ_BY_INT = { 1: _name("cps"), 2: _name("unotice") };
167
+ var PQ_TO_INT = {};
168
+ Object.keys(PQ_BY_INT).forEach(function (k) { PQ_TO_INT[oid.byName(PQ_BY_INT[k])] = Number(k); });
169
+
144
170
  // ---- field readers (the unwrapped ~biguint / ~time / ~oid contracts; draft-20 sec. 3.1) ----
145
171
 
146
172
  // ~biguint (sec. 3.1.2): a BARE byte string (major type 2), big-endian magnitude, the non-negative
@@ -266,6 +292,14 @@ function _cborIntVal(node, label) {
266
292
  if (node.majorType !== 0 && node.majorType !== 1) throw _err("c509/bad-extensions", "a " + label + " value must be a CBOR integer");
267
293
  return cbor.read.int(node);
268
294
  }
295
+ // A non-negative reconstruct-side count (a policyConstraints / inhibitAnyPolicy SkipCerts, a basicConstraints
296
+ // pathLenConstraint) narrowed to [0, 2^31-1] via the SAME guard.range.uint31 the toolkit's own DER decoders
297
+ // (schema-pkix) bound these INTEGER (0..MAX) counts through. A native CBOR value past 2^31-1 fails closed here
298
+ // rather than reconstructing a DER the toolkit's own decoder would then reject -- the reconstruct path must
299
+ // produce nothing that decoder rejects. Returns a Number; b.integer narrows it to the minimal DER INTEGER.
300
+ function _boundCount(value, label) {
301
+ return guard.range.uint31(value, _err, "c509/bad-extensions", label + " (0..2^31-1)");
302
+ }
269
303
  // A C509 KeyPurposeId (int registry alias OR unwrapped ~oid) -> the dotted extended-key-usage OID.
270
304
  function _ekuPurposeOid(node) {
271
305
  if (node.majorType === 0 || node.majorType === 1) {
@@ -531,6 +565,66 @@ function _serialIntContent(node) {
531
565
  return (mag[0] & 0x80) ? Buffer.concat([Buffer.from([0x00]), mag]) : mag;
532
566
  }
533
567
 
568
+ // ---- certificatePolicies (draft-20 sec. 3.3 / sec. 8.9 / sec. 8.10; RFC 5280 sec. 4.2.1.4) ----
569
+ // A CBOR text -> a UNIVERSAL IA5String TLV, running the same 7-bit + CVE-2009-2408 control-byte guard the
570
+ // general-name IA5 arms use (never the bare b.ia5, which permits control bytes). Non-empty (the general-name
571
+ // IA5 arms reject empty too; a CPSuri parity tightening -- RFC 5280 sets no SIZE floor on CPSuri).
572
+ function _ia5Universal(text, label) {
573
+ if (text.length === 0) throw _err("c509/bad-extensions", label + " must be non-empty");
574
+ 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"); }
575
+ guard.name.assertPrintableIa5(Buffer.from(text, "latin1"), _err, "c509/bad-extensions", label);
576
+ return b.ia5(text);
577
+ }
578
+ // A CBOR policyIdentifier (a sec. 8.9 registry int, or an unwrapped ~oid) -> the dotted policy OID.
579
+ function _policyIdToDerOid(node) {
580
+ if (node.majorType === 0 || node.majorType === 1) {
581
+ var i = Number(cbor.read.int(node)), nm = CP_BY_INT[i];
582
+ if (nm === undefined) throw _err("c509/bad-extensions", "a certificatePolicies policy int " + i + " has no C509 sec. 8.9 registry row");
583
+ return oid.byName(nm);
584
+ }
585
+ return _oidName(node, "c509/bad-extensions", "a certificatePolicies policyIdentifier").oid;
586
+ }
587
+ // The inverse: a dotted policy OID -> CBOR (a sec. 8.9 int if registered, else an unwrapped ~oid).
588
+ function _policyIdFromDer(dotted) {
589
+ var i = CP_TO_INT[dotted];
590
+ return i !== undefined ? cbor.build.int(BigInt(i)) : _oidCbor(dotted);
591
+ }
592
+ // (qualifierId CBOR, qualifier-text CBOR) -> one DER PolicyQualifierInfo SEQUENCE { policyQualifierId OID,
593
+ // qualifier ANY DEFINED BY }. The qualifierId resolves to a sec. 8.10 int ONLY: a ~oid / unregistered
594
+ // qualifierId has no defined qualifier:text semantics, so it is not compact-representable (fail closed, never
595
+ // guess the ANY type). id-qt-cps -> CPSuri IA5String; id-qt-unotice -> UserNotice { explicitText utf8String }.
596
+ function _qualifierToDer(qidNode, qtextNode) {
597
+ 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)");
598
+ var qi = Number(cbor.read.int(qidNode)), nm = PQ_BY_INT[qi];
599
+ if (nm === undefined) throw _err("c509/bad-extensions", "a policyQualifierId int " + qi + " has no C509 sec. 8.10 registry row");
600
+ if (qtextNode.majorType !== 3) throw _err("c509/bad-extensions", "a policyQualifier value must be a CBOR text string");
601
+ var text = cbor.read.textString(qtextNode);
602
+ if (qi === 1) return b.sequence([b.oid(oid.byName("cps")), _ia5Universal(text, "a CPSuri")]); // CPSuri ::= IA5String
603
+ // id-qt-unotice: UserNotice ::= SEQUENCE { explicitText utf8String } -- noticeRef omitted, DisplayText SIZE 1..200 floor.
604
+ if (text.length === 0) throw _err("c509/bad-extensions", "a UserNotice explicitText must be non-empty (DisplayText SIZE 1..200)");
605
+ return b.sequence([b.oid(oid.byName("unotice")), b.sequence([b.utf8(text)])]);
606
+ }
607
+ // One DER PolicyQualifierInfo node -> [ qidCbor, qtextCbor ], or null when not compact-representable: an
608
+ // unregistered qualifierId, a cps qualifier whose value is not a plain IA5String, or a UserNotice that is
609
+ // anything but SEQUENCE { explicitText utf8String } (a noticeRef, a non-UTF8String DisplayText arm, or more
610
+ // than the one explicitText child) -> whole-extension ~oid fallback.
611
+ function _qualifierFromDer(pq) {
612
+ var qid = asn1.read.oid(pq.children[0]), qi = PQ_TO_INT[qid];
613
+ if (qi === undefined) return null;
614
+ var q = pq.children[1];
615
+ if (qi === 1) { // CPSuri ::= IA5String
616
+ if (q.tagClass !== "universal" || q.tagNumber !== asn1.TAGS.IA5_STRING) return null;
617
+ var uri; try { uri = asn1.read.string(q); } catch (_e) { return null; }
618
+ return [cbor.build.int(1n), cbor.build.textString(uri)];
619
+ }
620
+ // id-qt-unotice: SEQUENCE { explicitText utf8String } ONLY (noticeRef absent).
621
+ if (q.tagClass !== "universal" || q.tagNumber !== asn1.TAGS.SEQUENCE || !q.children || q.children.length !== 1) return null;
622
+ var dt = q.children[0];
623
+ if (dt.tagClass !== "universal" || dt.tagNumber !== asn1.TAGS.UTF8_STRING) return null;
624
+ var txt; try { txt = asn1.read.string(dt); } catch (_e2) { return null; }
625
+ return [cbor.build.int(2n), cbor.build.textString(txt)];
626
+ }
627
+
534
628
  // One CBOR DistributionPointName [ fullName, reasons, cRLIssuer ] -> one DER DistributionPoint SEQUENCE.
535
629
  // distributionPoint [0] is EXPLICIT (it wraps the DistributionPointName CHOICE); fullName [0], reasons [1],
536
630
  // cRLIssuer [2] are IMPLICIT (RFC 5280 sec. 4.2.1.13) -- mixing these is the classic byte-exactness trap.
@@ -603,7 +697,7 @@ function _extValueToDer(name, node) {
603
697
  var iv = _cborIntVal(node, "basicConstraints");
604
698
  if (iv === -2n) return b.sequence([]); // cA false (omitted)
605
699
  if (iv === -1n) return b.sequence([b.boolean(true)]); // cA true, no pathLen
606
- if (iv >= 0n) return b.sequence([b.boolean(true), b.integer(iv)]); // cA true, pathLen
700
+ if (iv >= 0n) return b.sequence([b.boolean(true), b.integer(_boundCount(iv, "a basicConstraints pathLenConstraint"))]); // cA true, pathLen (0..2^31-1, as the DER decoder bounds it)
607
701
  throw _err("c509/bad-extensions", "a basicConstraints int " + iv + " is outside the -2/-1/pathLen range");
608
702
  }
609
703
  case "authorityKeyIdentifier": // keyId-only bytes, or [ keyId, authorityCertIssuer, serial ] (sec. 3.3)
@@ -627,8 +721,8 @@ function _extValueToDer(name, node) {
627
721
  }
628
722
  return b.sequence(items.map(function (it) { return b.oid(_ekuPurposeOid(it)); }));
629
723
  }
630
- case "inhibitAnyPolicy": // uint -> INTEGER SkipCerts
631
- return b.integer(_cborUint(node, "inhibitAnyPolicy"));
724
+ case "inhibitAnyPolicy": // uint -> INTEGER SkipCerts (0..2^31-1, as the DER decoder bounds it)
725
+ return b.integer(_boundCount(_cborUint(node, "inhibitAnyPolicy"), "an inhibitAnyPolicy SkipCerts"));
632
726
  case "ocspNoCheck": // null -> NULL
633
727
  if (node.majorType !== 7 || !(Buffer.isBuffer(node.bytes) && node.bytes.length === 1 && node.bytes[0] === 0xf6)) throw _err("c509/bad-extensions", "an ocspNoCheck value must be the CBOR simple value null");
634
728
  return b.nullValue();
@@ -662,6 +756,52 @@ function _extValueToDer(name, node) {
662
756
  if (node.majorType === 3) return b.sequence([b.sequence([b.explicit(0, b.contextConstructed(0, b.contextPrimitive(6, _ia5Bytes(node, 6))))])]);
663
757
  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
758
  return b.sequence(node.children.map(function (dp) { return _dpToDer(dp); }));
759
+ case "certificatePolicies": { // [ pid, [ *(qid, qtext) ], ... ] -> SEQUENCE OF PolicyInformation
760
+ if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a certificatePolicies value must be a CBOR array");
761
+ var cpKids = node.children;
762
+ 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)");
763
+ var polInfos = [], seenPolicy = {};
764
+ for (var cpi = 0; cpi + 1 < cpKids.length; cpi += 2) {
765
+ var quals = cpKids[cpi + 1];
766
+ if (quals.majorType !== 4 || !quals.children) throw _err("c509/bad-extensions", "a certificatePolicies qualifiers slot must be a CBOR array");
767
+ // A policy OID MUST NOT appear more than once (RFC 5280 sec. 4.2.1.4); the toolkit's own DER decoder
768
+ // rejects a duplicate (schema-pkix certExtensionDecoders), so reconstruct fails closed rather than
769
+ // producing a certificatePolicies extension that decoder would reject.
770
+ var policyOid = _policyIdToDerOid(cpKids[cpi]);
771
+ 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)");
772
+ seenPolicy[policyOid] = true;
773
+ var polFields = [b.oid(policyOid)];
774
+ if (quals.children.length) {
775
+ if (quals.children.length % 2 !== 0) throw _err("c509/bad-extensions", "a policyQualifiers array must be (policyQualifierId, qualifier) pairs");
776
+ var pqDers = [];
777
+ for (var qk = 0; qk + 1 < quals.children.length; qk += 2) pqDers.push(_qualifierToDer(quals.children[qk], quals.children[qk + 1]));
778
+ polFields.push(b.sequence(pqDers)); // policyQualifiers SEQUENCE OF PolicyQualifierInfo
779
+ }
780
+ polInfos.push(b.sequence(polFields));
781
+ }
782
+ return b.sequence(polInfos);
783
+ }
784
+ case "policyMappings": { // [ idp, sdp, idp, sdp, ... ] -> SEQUENCE OF SEQUENCE { OID, OID } (sec. 3.3)
785
+ if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a policyMappings value must be a CBOR array");
786
+ var pmKids = node.children;
787
+ if (pmKids.length === 0 || pmKids.length % 2 !== 0) throw _err("c509/bad-extensions", "a policyMappings array must be non-empty (issuerDomainPolicy, subjectDomainPolicy) pairs (sec. 3.3)");
788
+ var maps = [];
789
+ // each member is a CertPolicyId -- the same sec. 8.9 int/~oid policy space as certificatePolicies (an int
790
+ // outside sec. 8.9 fails closed in _policyIdToDerOid). anyPolicy is accepted: RFC 5280 sec. 4.2.1.5's
791
+ // "MUST NOT map to/from anyPolicy" is a generation rule the toolkit's own DER decoder does not reject.
792
+ for (var mi = 0; mi + 1 < pmKids.length; mi += 2) maps.push(b.sequence([b.oid(_policyIdToDerOid(pmKids[mi])), b.oid(_policyIdToDerOid(pmKids[mi + 1]))]));
793
+ return b.sequence(maps);
794
+ }
795
+ case "policyConstraints": { // [ requireExplicitPolicy: uint/null, inhibitPolicyMapping: uint/null ] -> SEQUENCE { [0]?, [1]? }
796
+ if (node.majorType !== 4 || !node.children || node.children.length !== 2) throw _err("c509/bad-extensions", "a policyConstraints value must be a 2-element CBOR array [ requireExplicitPolicy, inhibitPolicyMapping ] (sec. 3.3)");
797
+ var pcFields = [], repN = node.children[0], ipmN = node.children[1];
798
+ if (!_isCborNull(repN)) pcFields.push(b.implicit(0, b.integer(_boundCount(_cborUint(repN, "a policyConstraints requireExplicitPolicy"), "a policyConstraints requireExplicitPolicy")))); // requireExplicitPolicy [0] IMPLICIT SkipCerts
799
+ if (!_isCborNull(ipmN)) pcFields.push(b.implicit(1, b.integer(_boundCount(_cborUint(ipmN, "a policyConstraints inhibitPolicyMapping"), "a policyConstraints inhibitPolicyMapping")))); // inhibitPolicyMapping [1] IMPLICIT SkipCerts
800
+ // both-null is the empty-SEQUENCE case RFC 5280 sec. 4.2.1.11 forbids ("either ... MUST be present"); the
801
+ // toolkit's own DER decoder rejects the empty PolicyConstraints, so the reconstruct fails closed too.
802
+ if (pcFields.length === 0) throw _err("c509/bad-extensions", "policyConstraints must contain requireExplicitPolicy or inhibitPolicyMapping (RFC 5280 sec. 4.2.1.11)");
803
+ return b.sequence(pcFields); // positional slot -> tag makes [0] < [1] unique + ascending by construction
804
+ }
665
805
  default:
666
806
  throw _err("c509/bad-extensions", "extension " + name + " has no compact value decoder");
667
807
  }
@@ -775,6 +915,55 @@ function _extValueFromDer(name, der) {
775
915
  if (dpResults.length === 1 && dpResults[0].oneUri != null && dpResults[0].noReasons && dpResults[0].noIssuer) return cbor.build.textString(dpResults[0].oneUri);
776
916
  return cbor.build.array(dpResults.map(function (r) { return r.triple; }));
777
917
  }
918
+ case "certificatePolicies": { // SEQUENCE OF PolicyInformation -> [ pid, [ *(qid, qtext) ], ... ]
919
+ if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length < 1) return null;
920
+ var cpOut = [];
921
+ for (var pli = 0; pli < node.children.length; pli++) {
922
+ var pol = node.children[pli];
923
+ if (pol.tagClass !== "universal" || pol.tagNumber !== asn1.TAGS.SEQUENCE || !pol.children || pol.children.length < 1 || pol.children.length > 2) return null;
924
+ cpOut.push(_policyIdFromDer(asn1.read.oid(pol.children[0])));
925
+ var qArr = [];
926
+ if (pol.children.length === 2) {
927
+ var qseq = pol.children[1];
928
+ if (qseq.tagClass !== "universal" || qseq.tagNumber !== asn1.TAGS.SEQUENCE || !qseq.children || !qseq.children.length) return null; // policyQualifiers SIZE 1..MAX
929
+ for (var qj = 0; qj < qseq.children.length; qj++) {
930
+ var pq = qseq.children[qj];
931
+ if (pq.tagClass !== "universal" || pq.tagNumber !== asn1.TAGS.SEQUENCE || !pq.children || pq.children.length !== 2) return null;
932
+ var qpair = _qualifierFromDer(pq);
933
+ if (qpair == null) return null; // any non-cps/unotice qualifier -> whole-ext fallback
934
+ qArr.push(qpair[0]); qArr.push(qpair[1]);
935
+ }
936
+ }
937
+ cpOut.push(cbor.build.array(qArr)); // empty [] when policyQualifiers absent
938
+ }
939
+ return cbor.build.array(cpOut);
940
+ }
941
+ case "policyMappings": { // SEQUENCE OF SEQUENCE { OID, OID } -> [ idp, sdp, ... ]
942
+ if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length < 1) return null;
943
+ var pmOut = [];
944
+ for (var pm = 0; pm < node.children.length; pm++) {
945
+ var mp = node.children[pm];
946
+ if (mp.tagClass !== "universal" || mp.tagNumber !== asn1.TAGS.SEQUENCE || !mp.children || mp.children.length !== 2) return null;
947
+ pmOut.push(_policyIdFromDer(asn1.read.oid(mp.children[0])));
948
+ pmOut.push(_policyIdFromDer(asn1.read.oid(mp.children[1])));
949
+ }
950
+ return cbor.build.array(pmOut);
951
+ }
952
+ case "policyConstraints": { // SEQUENCE { [0] rep?, [1] ipm? } -> [ rep/null, ipm/null ]
953
+ if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length < 1 || node.children.length > 2) return null;
954
+ var repC = cbor.build.nullValue(), ipmC = cbor.build.nullValue(), pcLast = -1;
955
+ for (var pci = 0; pci < node.children.length; pci++) {
956
+ var f = node.children[pci];
957
+ if (f.tagClass !== "context" || f.tagNumber <= pcLast) return null; // DER: unique + ascending [0] < [1]
958
+ pcLast = f.tagNumber;
959
+ var sv = asn1.read.integerImplicit(f, f.tagNumber);
960
+ if (sv < 0n) return null; // SkipCerts (0..MAX) -- a negative is non-compact
961
+ if (f.tagNumber === 0) repC = cbor.build.uint(sv);
962
+ else if (f.tagNumber === 1) ipmC = cbor.build.uint(sv);
963
+ else return null;
964
+ }
965
+ return cbor.build.array([repC, ipmC]);
966
+ }
778
967
  default:
779
968
  return null;
780
969
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.3.32",
3
+ "version": "0.4.0",
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:40b5fc50-7b82-41d2-9e80-f01b6be97878",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-05T23:30:49.498Z",
8
+ "timestamp": "2026-08-06T23:41:09.454Z",
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.4.0",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.3.32",
25
+ "version": "0.4.0",
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.4.0",
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.4.0",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]