@blamejs/pki 0.1.30 → 0.1.31

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,15 @@ 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.1.31 — 2026-07-11
8
+
9
+ The DER format cohort and the JOSE surface graduate to stable.
10
+
11
+ ### Changed
12
+
13
+ - pki.schema.pkcs12 / attrcert / crmf / cmp / csrattrs (parse, pemDecode, pemEncode), pki.schema.all and pki.schema.parse, and the pki.jose signing / verification / thumbprint / base64url / JSON surface graduate from experimental to stable.
14
+ - The LTS-CALENDAR graduation criterion now states that a settled, well-tested format no mainstream tool implements graduates on the toolkit's own conformance-vector round-trip plus coverage-guided fuzzing, rather than on a harness oracle that does not exist.
15
+
7
16
  ## v0.1.30 — 2026-07-11
8
17
 
9
18
  Fail-closed hardening of the byte-input and text-decode boundaries.
package/lib/guard-all.js CHANGED
@@ -18,6 +18,9 @@
18
18
  // (recursion / allocation DoS defence)
19
19
  // guard.crypto.constantTimeEqual -- length-checked constant-time compare
20
20
  // (timing side-channel defence)
21
+ // guard.range.int / .uint31 / .positiveInt31
22
+ // -- bound a decoded integer before narrowing
23
+ // to Number (silent-narrowing defence)
21
24
  //
22
25
  // Each shape is enforced by a codebase-patterns detector: the characteristic
23
26
  // token of a guard (the Buffer.from(x.buffer, byteOffset) re-view, the
@@ -29,10 +32,14 @@ var bytes = require("./guard-bytes");
29
32
  var text = require("./guard-text");
30
33
  var limits = require("./guard-limits");
31
34
  var crypto = require("./guard-crypto");
35
+ var range = require("./guard-range");
36
+ var name = require("./guard-name");
32
37
 
33
38
  module.exports = {
34
39
  bytes: bytes,
35
40
  text: text,
36
41
  limits: limits,
37
42
  crypto: crypto,
43
+ range: range,
44
+ name: name,
38
45
  };
@@ -27,6 +27,8 @@
27
27
  // Accepts a Buffer / Uint8Array -- the DER / CBOR / CT / Merkle input contract.
28
28
  // ErrorClass MUST be a withCause PkiError subclass (the raw detach failure is
29
29
  // threaded as the cause).
30
+ // @enforced-by guard-shape-reinlined
31
+ // @guard-shape Buffer\.from\(\s*([A-Za-z_$][\w$]*)\.buffer\s*,\s*\1\.byteOffset
30
32
  function view(input, ErrorClass, code, label) {
31
33
  if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
32
34
  try {
@@ -42,6 +44,7 @@ function view(input, ErrorClass, code, label) {
42
44
  // Accepts the full W3C BufferSource (Buffer / TypedArray view / raw ArrayBuffer)
43
45
  // -- the WebCrypto input contract. A raw ArrayBuffer is copied via Buffer.from,
44
46
  // which also throws on a detached backing store.
47
+ // @enforced-by guard-shape-reinlined (the re-view shape is declared on view above)
45
48
  function source(input, ErrorClass, code, label) {
46
49
  var isAb = input instanceof ArrayBuffer;
47
50
  if (isAb || ArrayBuffer.isView(input)) {
@@ -25,6 +25,8 @@ var nodeCrypto = require("node:crypto");
25
25
 
26
26
  // constantTimeEqual(a, b) -> boolean. Length-checked, then constant-time over
27
27
  // equal lengths. a and b are Buffers.
28
+ // @enforced-by guard-shape-reinlined
29
+ // @guard-shape \.timingSafeEqual\s*\(
28
30
  function constantTimeEqual(a, b) {
29
31
  return a.length === b.length && nodeCrypto.timingSafeEqual(a, b);
30
32
  }
@@ -27,6 +27,8 @@ var constants = require("./constants");
27
27
  // cap(value, key, dflt) -> a validated non-negative integer, or dflt when value
28
28
  // is undefined. A non-integer / non-finite / negative value is a config-time
29
29
  // TypeError (Number.isInteger already rejects non-numbers, NaN, and Infinity).
30
+ // @enforced-by behavioral -- a config-time cap validator has no rename-proof code
31
+ // shape; the decode-rejects-a-bad-cap RED vectors (asn1/cbor) are the guard.
30
32
  function cap(value, key, dflt) {
31
33
  if (value === undefined) return dflt;
32
34
  if (!Number.isInteger(value) || value < 0) {
@@ -38,6 +40,11 @@ function cap(value, key, dflt) {
38
40
  // depthCap(value, key, dflt) -> a validated recursion-depth cap: cap() plus the
39
41
  // stack-safe ceiling, so a raised maxDepth cannot drive recursive descent into a
40
42
  // raw RangeError past the native frame limit.
43
+ // @enforced-by guard-shape-reinlined
44
+ // @guard-scope file
45
+ // @guard-shape \bopts\.maxDepth\b
46
+ // @guard-shape \bdepth\s*\+\s*1\b
47
+ // @guard-via \.depthCap\s*\(
41
48
  function depthCap(value, key, dflt) {
42
49
  var n = cap(value, key, dflt);
43
50
  if (n > constants.LIMITS.MAX_DECODE_DEPTH_CEILING) {
@@ -0,0 +1,53 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the
6
+ // consumers whose name-string integrity composes this guard (pki.path.validate
7
+ // DN chaining, pki.schema.x509 GeneralName / SAN decode).
8
+ //
9
+ // guard-name -- fail-closed rejection of an embedded control byte in a
10
+ // distinguished-name / SAN string. Two policies over one concern.
11
+ //
12
+ // Defends the name-truncation / display-confusion class (CVE-2009-2408): a NUL
13
+ // or control byte embedded in a decoded name lets an attacker make two different
14
+ // names compare equal (or a UI truncate at the NUL), so a cert issued for
15
+ // "good.example.com\0.evil.com" is treated as "good.example.com". CWE-158
16
+ // (improper neutralization of null byte) / CWE-20. The reject is at decode, so a
17
+ // truncation name never reaches a comparison or a display.
18
+
19
+ // assertNoControlBytes(str, E, code, label) -> str | throws E(code, ...)
20
+ // DirectoryString policy (a DN attribute value): reject NUL and C0 control bytes
21
+ // in a DECODED name string. TAB (0x09) is exempt; printable non-ASCII (a
22
+ // UTF8String CN carries accented / CJK characters) is allowed. `str` is assumed a
23
+ // string (the caller guards typeof). E is the (code, message) typed-error factory.
24
+ // @enforced-by behavioral -- the control-byte reject has no rename-proof code
25
+ // shape distinct from the ASN.1 charset readers; the CVE-2009-2408 RED vectors
26
+ // (a DN string with an embedded NUL / control byte rejects) are the guard.
27
+ function assertNoControlBytes(str, E, code, label) {
28
+ for (var i = 0; i < str.length; i++) {
29
+ var c = str.charCodeAt(i);
30
+ if (c === 0 || (c < 0x20 && c !== 0x09)) {
31
+ throw E(code, label + " contains an embedded control byte (CVE-2009-2408)");
32
+ }
33
+ }
34
+ return str;
35
+ }
36
+
37
+ // assertPrintableIa5(buf, E, code, label) -> buf | throws E(code, ...)
38
+ // IA5String policy (a dNSName / rfc822Name / URI GeneralName): every byte must be
39
+ // printable 7-bit ASCII [0x20, 0x7e] -- an embedded NUL / control byte enables the
40
+ // same name-truncation bypass downstream. `buf` is the raw GeneralName content.
41
+ // @enforced-by behavioral -- the printable-IA5 byte-range reject has no rename-proof
42
+ // code shape distinct from the ASN.1 IA5 reader; the CVE-2009-2408 RED vectors
43
+ // (a SAN with a control byte rejects) are the guard.
44
+ function assertPrintableIa5(buf, E, code, label) {
45
+ for (var i = 0; i < buf.length; i++) {
46
+ if (buf[i] < 0x20 || buf[i] > 0x7e) {
47
+ throw E(code, label + " must be a printable IA5String (no control bytes)");
48
+ }
49
+ }
50
+ return buf;
51
+ }
52
+
53
+ module.exports = { assertNoControlBytes: assertNoControlBytes, assertPrintableIa5: assertPrintableIa5 };
@@ -0,0 +1,69 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the
6
+ // parsers whose decoded-integer bounds compose this guard (pki.schema.x509 /
7
+ // crl / pkcs12 / cmp, pki.path.validate).
8
+ //
9
+ // guard-range -- fail-closed bound of an untrusted DECODED integer before it is
10
+ // narrowed to a JS Number. Parse-time (Tier-2): the value is malformed content,
11
+ // so it throws the caller's typed PkiError -- distinct from guard-limits, which
12
+ // validates an operator-supplied option at config time (Tier-1, TypeError).
13
+ //
14
+ // Defends the silent-narrowing class (a decoded ASN.1 INTEGER past 2^53 rounds
15
+ // when coerced to a Number, so a caller acting on the result -- a pathLen /
16
+ // skipCerts / saltLength / iteration counter -- acts on the WRONG number;
17
+ // CWE-190 integer overflow / CWE-681 incorrect conversion). The bound and the
18
+ // narrow are ATOMIC here: a caller cannot obtain the Number without the range
19
+ // having been enforced, so the value can never round silently and be acted on.
20
+
21
+ // 2^31 - 1 -- the shared skip-cert / path-length / salt / iteration ceiling.
22
+ // This literal lives ONLY here (a re-inline anywhere else is flagged: the
23
+ // guard-shape walk anchors on the literal). 2^53 - 1 is the safe-narrow ceiling.
24
+ var UINT31_MAX = 2147483647n;
25
+ var SAFE_MAX = 9007199254740991n;
26
+ var SAFE_MIN = -9007199254740991n;
27
+
28
+ // int(value, min, max, E, code, label) -> a Number in [min, max].
29
+ // value : the BigInt result of an ASN.1 INTEGER / ENUMERATED read.
30
+ // min, max : inclusive BigInt bounds.
31
+ // E : the (code, message, cause) typed-error factory in scope at the call
32
+ // site (ns.E / ctx.E / the module-local E) -- guard-range injects the
33
+ // caller's typed error through it, the tier-appropriate currency (a
34
+ // parse boundary carries a factory, not a bare ErrorClass).
35
+ // code : the frozen domain/reason code this field rejects under.
36
+ // label : field phrase (+ optional RFC cite) for the message.
37
+ // @enforced-by guard-shape-reinlined
38
+ // @guard-shape 2147483647n
39
+ function int(value, min, max, E, code, label) {
40
+ // Config-time authoring guard (Tier-1): narrowing to Number is lossless only
41
+ // when the WHOLE range sits inside the safe-integer band [-(2^53-1), 2^53-1].
42
+ // Both ends bind -- a min below the floor rounds a decoded value near it just
43
+ // as a max above the ceiling does. A wider range needs a BigInt-preserving
44
+ // guard, not this one (this is why the uint64 Merkle-coordinate domain must
45
+ // NOT route here).
46
+ if (max > SAFE_MAX) {
47
+ throw new TypeError("guard.range.int: max " + max + " exceeds the safe-integer ceiling; use a BigInt-preserving guard");
48
+ }
49
+ if (min < SAFE_MIN) {
50
+ throw new TypeError("guard.range.int: min " + min + " is below the safe-integer floor; use a BigInt-preserving guard");
51
+ }
52
+ // Parse-time reject (Tier-2): malformed / out-of-range decoded content.
53
+ if (typeof value !== "bigint" || value < min || value > max) {
54
+ throw E(code, label + " must be an integer within " + min + ".." + max);
55
+ }
56
+ return Number(value);
57
+ }
58
+
59
+ // uint31(value, E, code, label) -> Number in [0, 2^31-1]. The DER-INTEGER
60
+ // non-negative skip/path/salt shape (RFC 5280 / 4211 / 9810).
61
+ // @enforced-by guard-shape-reinlined (shares the 2147483647n shape declared on int)
62
+ function uint31(value, E, code, label) { return int(value, 0n, UINT31_MAX, E, code, label); }
63
+
64
+ // positiveInt31(value, E, code, label) -> Number in [1, 2^31-1]. The PKCS#12
65
+ // PBKDF2 / MAC iteration-count shape (a count of at least one).
66
+ // @enforced-by guard-shape-reinlined (shares the 2147483647n shape declared on int)
67
+ function positiveInt31(value, E, code, label) { return int(value, 1n, UINT31_MAX, E, code, label); }
68
+
69
+ module.exports = { int: int, uint31: uint31, positiveInt31: positiveInt31 };
package/lib/guard-text.js CHANGED
@@ -35,6 +35,9 @@ var LATIN1 = "latin1";
35
35
  // (required only when fatal is set),
36
36
  // badInput: code for a non-Buffer/non-string reject,
37
37
  // label: human phrase for the message }
38
+ // @enforced-by behavioral -- cap-before-copy has no rename-proof code shape; the
39
+ // over-cap RED vectors + the detached re-view through guard.bytes.view (itself
40
+ // enforced) are the guard.
38
41
  function decode(input, maxBytes, ErrorClass, spec) {
39
42
  var charset = spec.charset || LATIN1;
40
43
  if (Buffer.isBuffer(input)) {
package/lib/jose.js CHANGED
@@ -53,7 +53,7 @@ var B64U_ALPHABET = /^[A-Za-z0-9_-]*$/;
53
53
  * @primitive pki.jose.base64url.encode
54
54
  * @signature pki.jose.base64url.encode(bytes) -> string
55
55
  * @since 0.1.25
56
- * @status experimental
56
+ * @status stable
57
57
  * @spec RFC 7515, RFC 4648
58
58
  * @related pki.jose.base64url.decode
59
59
  *
@@ -72,7 +72,7 @@ function b64uEncode(bytes) {
72
72
  * @primitive pki.jose.base64url.decode
73
73
  * @signature pki.jose.base64url.decode(text) -> Buffer
74
74
  * @since 0.1.25
75
- * @status experimental
75
+ * @status stable
76
76
  * @spec RFC 7515, RFC 4648, RFC 8555
77
77
  * @related pki.jose.base64url.encode
78
78
  *
@@ -214,7 +214,7 @@ function _jsonReader(str) {
214
214
  * @primitive pki.jose.parseJson
215
215
  * @signature pki.jose.parseJson(input) -> value
216
216
  * @since 0.1.25
217
- * @status experimental
217
+ * @status stable
218
218
  * @spec RFC 7515, RFC 8259
219
219
  * @related pki.jose.base64url.decode
220
220
  *
@@ -408,7 +408,7 @@ var PRIVATE_JWK_MEMBERS = ["d", "p", "q", "dp", "dq", "qi", "k", "priv"];
408
408
  * @primitive pki.jose.assertPublicJwk
409
409
  * @signature pki.jose.assertPublicJwk(jwk) -> jwk
410
410
  * @since 0.1.25
411
- * @status experimental
411
+ * @status stable
412
412
  * @spec RFC 7517, RFC 7518
413
413
  * @related pki.jose.sign, pki.jose.thumbprint
414
414
  *
@@ -435,7 +435,7 @@ function assertPublicJwk(jwk) {
435
435
  * @primitive pki.jose.verify
436
436
  * @signature pki.jose.verify(jws, opts) -> Promise<{ header, payload }>
437
437
  * @since 0.1.25
438
- * @status experimental
438
+ * @status stable
439
439
  * @spec RFC 7515, RFC 7518, RFC 8555
440
440
  * @related pki.jose.sign, pki.jose.parseJson
441
441
  *
@@ -488,7 +488,7 @@ async function verify(jws, opts) {
488
488
  * @primitive pki.jose.sign
489
489
  * @signature pki.jose.sign(opts) -> Promise<{ protected, payload, signature }>
490
490
  * @since 0.1.25
491
- * @status experimental
491
+ * @status stable
492
492
  * @spec RFC 7515, RFC 7518, RFC 8555
493
493
  * @related pki.jose.verify
494
494
  *
@@ -574,7 +574,7 @@ var THUMBPRINT_MEMBERS = {
574
574
  * @primitive pki.jose.thumbprint
575
575
  * @signature pki.jose.thumbprint(jwk) -> Promise<string>
576
576
  * @since 0.1.25
577
- * @status experimental
577
+ * @status stable
578
578
  * @spec RFC 7638, RFC 8037, RFC 9964
579
579
  * @related pki.jose.verify
580
580
  *
@@ -37,6 +37,7 @@ var asn1 = require("./asn1-der");
37
37
  var schema = require("./schema-engine");
38
38
  var x509 = require("./schema-x509");
39
39
  var crl = require("./schema-crl");
40
+ var guard = require("./guard-all");
40
41
  var constants = require("./constants");
41
42
 
42
43
  var PathError = errors.PathError;
@@ -226,14 +227,11 @@ function resolveRsaPss(paramsBytes) {
226
227
  // verifier binds to the salt length the certificate states, so a value
227
228
  // that would round is not verifiable material (no real salt exceeds the
228
229
  // modulus size, let alone this).
229
- if (sl < 0n || sl > 2147483647n) throw E("path/unsupported-algorithm", "RSASSA-PSS saltLength must be a non-negative integer within the salt-length range");
230
- saltLength = Number(sl);
230
+ saltLength = guard.range.uint31(sl, E, "path/unsupported-algorithm", "RSASSA-PSS saltLength");
231
231
  } else if (f.tagNumber === 3) {
232
232
  // Compared for equality with 1 below -- bound before conversion so an
233
233
  // oversized value cannot round on its way to the comparison.
234
- var tf = asn1.read.integer(f.children[0]);
235
- if (tf < 0n || tf > 2147483647n) throw E("path/unsupported-algorithm", "unsupported RSASSA-PSS trailerField " + tf.toString());
236
- trailer = Number(tf);
234
+ trailer = guard.range.uint31(asn1.read.integer(f.children[0]), E, "path/unsupported-algorithm", "RSASSA-PSS trailerField");
237
235
  }
238
236
  });
239
237
  if (hash === null) throw E("path/unsupported-algorithm", "RSASSA-PSS hashAlgorithm must be stated explicitly (the SHA-1 default is rejected)");
@@ -347,10 +345,7 @@ function normalizeAttrValue(v) {
347
345
  // Reject embedded NUL / control bytes so a truncation attack (CVE-2009-2408)
348
346
  // cannot make two different names compare equal.
349
347
  if (typeof v !== "string") return v;
350
- for (var i = 0; i < v.length; i++) {
351
- var c = v.charCodeAt(i);
352
- if (c === 0 || (c < 0x20 && c !== 0x09)) throw E("path/name-chaining", "distinguished name contains an embedded control byte (CVE-2009-2408)");
353
- }
348
+ guard.name.assertNoControlBytes(v, E, "path/name-chaining", "distinguished name");
354
349
  return v.trim().replace(/\s+/g, " ").toLowerCase();
355
350
  }
356
351
 
package/lib/schema-all.js CHANGED
@@ -249,7 +249,7 @@ var FORMATS = [
249
249
  * @primitive pki.schema.all
250
250
  * @signature pki.schema.all() -> string[]
251
251
  * @since 0.1.7
252
- * @status experimental
252
+ * @status stable
253
253
  * @spec RFC 5280
254
254
  * @related pki.schema.parse
255
255
  *
@@ -264,7 +264,7 @@ function all() { return FORMATS.map(function (f) { return f.name; }); }
264
264
  * @primitive pki.schema.parse
265
265
  * @signature pki.schema.parse(input) -> parsed
266
266
  * @since 0.1.7
267
- * @status experimental
267
+ * @status stable
268
268
  * @spec RFC 5280
269
269
  * @related pki.schema.x509, pki.schema.all
270
270
  *
@@ -299,7 +299,7 @@ var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
299
299
  * @primitive pki.schema.attrcert.parse
300
300
  * @signature pki.schema.attrcert.parse(input) -> attributeCertificate
301
301
  * @since 0.1.14
302
- * @status experimental
302
+ * @status stable
303
303
  * @spec RFC 5755, X.509
304
304
  * @related pki.schema.parse, pki.schema.x509.parse
305
305
  *
@@ -348,7 +348,7 @@ function parseV1(input) {
348
348
  * @primitive pki.schema.attrcert.pemDecode
349
349
  * @signature pki.schema.attrcert.pemDecode(text, label?) -> Buffer
350
350
  * @since 0.1.14
351
- * @status experimental
351
+ * @status stable
352
352
  * @spec RFC 7468, RFC 5755
353
353
  * @related pki.schema.attrcert.parse
354
354
  *
@@ -367,7 +367,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label === null ? n
367
367
  * @primitive pki.schema.attrcert.pemEncode
368
368
  * @signature pki.schema.attrcert.pemEncode(der, label?) -> string
369
369
  * @since 0.1.23
370
- * @status experimental
370
+ * @status stable
371
371
  * @spec RFC 7468, RFC 5755
372
372
  * @related pki.schema.attrcert.pemDecode
373
373
  *
package/lib/schema-cmp.js CHANGED
@@ -42,6 +42,7 @@ var pkix = require("./schema-pkix.js");
42
42
  var cms = require("./schema-cms.js");
43
43
  var crmf = require("./schema-crmf.js");
44
44
  var frameworkError = require("./framework-error.js");
45
+ var guard = require("./guard-all.js");
45
46
 
46
47
  var CmpError = frameworkError.CmpError;
47
48
  var PemError = frameworkError.PemError;
@@ -99,9 +100,7 @@ var PKI_FAILURE_INFO = schema.decode(function (n, ctx) {
99
100
  // PollRep checkAfter is a delay in seconds -- negative poisons scheduling, and
100
101
  // the value surfaces as an exact number or not at all.
101
102
  var CHECK_AFTER = schema.decode(function (n, ctx) {
102
- var v = asn1.read.integer(n);
103
- if (v < 0n || v > 2147483647n) throw ctx.E("cmp/bad-poll-rep", "checkAfter must be a non-negative delay in seconds (RFC 9810 sec. 5.3.22)");
104
- return Number(v);
103
+ return guard.range.uint31(asn1.read.integer(n), ctx.E, "cmp/bad-poll-rep", "checkAfter delay in seconds (RFC 9810 sec. 5.3.22)");
105
104
  });
106
105
 
107
106
  // A raw certificate / CRL / publication-info element -- surfaced byte-exact for
@@ -841,7 +840,7 @@ var PKI_MESSAGE = schema.seq([
841
840
  * @primitive pki.schema.cmp.parse
842
841
  * @signature pki.schema.cmp.parse(input) -> pkiMessage
843
842
  * @since 0.1.19
844
- * @status experimental
843
+ * @status stable
845
844
  * @spec RFC 9810, RFC 9483, RFC 9481
846
845
  * @related pki.schema.parse, pki.schema.crmf.parse, pki.schema.cms.parse
847
846
  *
@@ -895,7 +894,7 @@ var parse = pkix.makeParser({
895
894
  * @primitive pki.schema.cmp.pemDecode
896
895
  * @signature pki.schema.cmp.pemDecode(text, label?) -> Buffer
897
896
  * @since 0.1.19
898
- * @status experimental
897
+ * @status stable
899
898
  * @spec RFC 7468, RFC 9810
900
899
  * @related pki.schema.cmp.parse
901
900
  *
@@ -912,7 +911,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CMP", Pe
912
911
  * @primitive pki.schema.cmp.pemEncode
913
912
  * @signature pki.schema.cmp.pemEncode(der, label?) -> string
914
913
  * @since 0.1.19
915
- * @status experimental
914
+ * @status stable
916
915
  * @spec RFC 7468
917
916
  * @related pki.schema.cmp.pemDecode
918
917
  *
@@ -455,7 +455,7 @@ var CERT_REQ_MESSAGES = schema.seqOf(CERT_REQ_MSG, {
455
455
  * @primitive pki.schema.crmf.parse
456
456
  * @signature pki.schema.crmf.parse(input) -> certReqMessages
457
457
  * @since 0.1.17
458
- * @status experimental
458
+ * @status stable
459
459
  * @spec RFC 4211
460
460
  * @related pki.schema.parse, pki.schema.csr.parse
461
461
  *
@@ -496,7 +496,7 @@ var parse = pkix.makeParser({
496
496
  * @primitive pki.schema.crmf.pemDecode
497
497
  * @signature pki.schema.crmf.pemDecode(text, label?) -> Buffer
498
498
  * @since 0.1.17
499
- * @status experimental
499
+ * @status stable
500
500
  * @spec RFC 7468, RFC 4211
501
501
  * @related pki.schema.crmf.parse
502
502
  *
@@ -513,7 +513,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || null, Pem
513
513
  * @primitive pki.schema.crmf.pemEncode
514
514
  * @signature pki.schema.crmf.pemEncode(der, label) -> string
515
515
  * @since 0.1.23
516
- * @status experimental
516
+ * @status stable
517
517
  * @spec RFC 7468, RFC 4211
518
518
  * @related pki.schema.crmf.pemDecode
519
519
  *
@@ -320,7 +320,7 @@ var CSR_ATTRS = schema.seqOf(ATTR_OR_OID, {
320
320
  * @primitive pki.schema.csrattrs.parse
321
321
  * @signature pki.schema.csrattrs.parse(der) -> { items }
322
322
  * @since 0.1.24
323
- * @status experimental
323
+ * @status stable
324
324
  * @spec RFC 8951, RFC 9908, RFC 7030
325
325
  * @related pki.schema.parse, pki.est.buildEnrollAttributes
326
326
  *
@@ -46,6 +46,7 @@ var pkix = require("./schema-pkix.js");
46
46
  var cms = require("./schema-cms.js");
47
47
  var pkcs8 = require("./schema-pkcs8.js");
48
48
  var frameworkError = require("./framework-error.js");
49
+ var guard = require("./guard-all.js");
49
50
 
50
51
  var Pkcs12Error = frameworkError.Pkcs12Error;
51
52
  var PemError = frameworkError.PemError;
@@ -102,16 +103,10 @@ var PBKDF2_PARAMS = schema.seq([
102
103
  if (!m.fields.keyLength.present) {
103
104
  throw ctx.E("pkcs12/bad-mac-data", "PBMAC1 PBKDF2-params must carry keyLength (RFC 9579 sec. 5)");
104
105
  }
105
- // Both counters surface as exact JS numbers -- a value past the bound
106
- // would round silently on conversion and hand a verifier wrong inputs.
107
- var iterations = m.fields.iterationCount.value;
108
- if (iterations < 1n || iterations > 2147483647n) {
109
- throw ctx.E("pkcs12/bad-mac-data", "PBKDF2 iterationCount must be a positive integer within the iteration-count range");
110
- }
111
- var keyLength = m.fields.keyLength.value;
112
- if (keyLength < 1n || keyLength > 2147483647n) {
113
- throw ctx.E("pkcs12/bad-mac-data", "PBKDF2 keyLength must be a positive integer within the key-length range");
114
- }
106
+ // guard.range.positiveInt31 bounds + narrows each counter atomically -- a
107
+ // value past the bound would round silently and hand a verifier wrong inputs.
108
+ var iterationCount = guard.range.positiveInt31(m.fields.iterationCount.value, ctx.E, "pkcs12/bad-mac-data", "PBKDF2 iterationCount");
109
+ var keyLength = guard.range.positiveInt31(m.fields.keyLength.value, ctx.E, "pkcs12/bad-mac-data", "PBKDF2 keyLength");
115
110
  var prf = m.fields.prf.present ? m.fields.prf.value.result : null;
116
111
  // X.690 sec. 11.5 -- a DEFAULT-valued component must be omitted from a DER
117
112
  // encoding, and the prf DEFAULT is algid-hmacWithSHA1: hmacWithSHA1 with
@@ -124,8 +119,8 @@ var PBKDF2_PARAMS = schema.seq([
124
119
  }
125
120
  return {
126
121
  salt: m.fields.salt.value,
127
- iterationCount: Number(iterations),
128
- keyLength: Number(keyLength),
122
+ iterationCount: iterationCount,
123
+ keyLength: keyLength,
129
124
  prfOid: prf ? prf.oid : OID_HMAC_SHA1,
130
125
  prfName: prf ? prf.name : "hmacWithSHA1",
131
126
  };
@@ -172,8 +167,7 @@ var MAC_DATA = schema.seq([
172
167
  if (it.present) {
173
168
  var v = it.value;
174
169
  if (v === 1n) throw ctx.E("pkcs12/bad-mac-iterations", "iterations equal to its DEFAULT 1 must be omitted (X.690 sec. 11.5)");
175
- if (v < 1n || v > 2147483647n) throw ctx.E("pkcs12/bad-mac-iterations", "iterations must be a positive integer within the iteration-count range");
176
- iterations = Number(v);
170
+ iterations = guard.range.positiveInt31(v, ctx.E, "pkcs12/bad-mac-iterations", "MacData iterations");
177
171
  }
178
172
  var di = m.fields.mac.value.result;
179
173
  var pbmac1 = null;
@@ -529,7 +523,7 @@ function _buildBag(rec, state, ctx) {
529
523
  * @primitive pki.schema.pkcs12.parse
530
524
  * @signature pki.schema.pkcs12.parse(input) -> pfx
531
525
  * @since 0.1.18
532
- * @status experimental
526
+ * @status stable
533
527
  * @spec RFC 7292, RFC 9579
534
528
  * @defends ASN.1-parser-DoS (CWE-400)
535
529
  * @related pki.schema.parse, pki.schema.pkcs8.parse, pki.schema.cms.parse
@@ -577,7 +571,7 @@ var parse = pkix.makeParser({
577
571
  * @primitive pki.schema.pkcs12.pemDecode
578
572
  * @signature pki.schema.pkcs12.pemDecode(text, label?) -> Buffer
579
573
  * @since 0.1.18
580
- * @status experimental
574
+ * @status stable
581
575
  * @spec RFC 7468, RFC 7292
582
576
  * @related pki.schema.pkcs12.parse
583
577
  *
@@ -594,7 +588,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "PKCS12",
594
588
  * @primitive pki.schema.pkcs12.pemEncode
595
589
  * @signature pki.schema.pkcs12.pemEncode(der, label?) -> string
596
590
  * @since 0.1.18
597
- * @status experimental
591
+ * @status stable
598
592
  * @spec RFC 7468
599
593
  * @related pki.schema.pkcs12.pemDecode
600
594
  *
@@ -500,12 +500,9 @@ function generalName(ns, opts) {
500
500
  if (constructed) throw ctx.E(code, "GeneralName [" + t + "] must be primitive (X.690 sec. 10.2)");
501
501
  if (GN_IA5[t]) {
502
502
  if (n.content.length === 0) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty IA5String");
503
- for (var i = 0; i < n.content.length; i++) {
504
- // 7-bit IA5, and no C0/DEL control byte -- an embedded NUL/control in a
505
- // dNSName/rfc822Name/URI enables a name-truncation/confusion bypass
506
- // (CVE-2009-2408 class) downstream, so reject it at decode.
507
- if (n.content[i] < 0x20 || n.content[i] > 0x7e) throw ctx.E(code, "GeneralName [" + t + "] must be a printable IA5String (no control bytes)");
508
- }
503
+ // 7-bit IA5, no C0/DEL control byte -- an embedded NUL/control in a
504
+ // dNSName/rfc822Name/URI enables a name-truncation bypass (CVE-2009-2408).
505
+ guard.name.assertPrintableIa5(n.content, ctx.E, code, "GeneralName [" + t + "]");
509
506
  if (decodeValue) value = n.content.toString("latin1");
510
507
  } else if (t === 7) {
511
508
  if (subtreeBase) {
@@ -619,11 +616,10 @@ function certExtensionDecoders(ns) {
619
616
  if (kids[i] && kids[i].tagClass === "universal" && kids[i].tagNumber === _T.INTEGER) {
620
617
  if (!cA) throw ns.E(C, "BasicConstraints pathLenConstraint is only permitted when cA is TRUE (RFC 5280 sec. 4.2.1.9)");
621
618
  var pl = readInt(kids[i], C, "pathLenConstraint");
622
- // Bounded before Number() narrowing -- a value past the safe-integer range
623
- // would round silently and be compared as a path-length ceiling, so it is
624
- // exact-or-rejected (the same rule the RSASSA-PSS/PKCS#12 counters follow).
625
- if (pl < 0n || pl > 2147483647n) throw ns.E(C, "BasicConstraints pathLenConstraint must be a non-negative integer within range");
626
- pathLen = Number(pl); i++;
619
+ // guard.range.uint31 bounds the decoded BigInt and narrows atomically -- a
620
+ // value past the safe-integer range would otherwise round silently and be
621
+ // compared as a path-length ceiling.
622
+ pathLen = guard.range.uint31(pl, ns.E, C, "BasicConstraints pathLenConstraint (RFC 5280 sec. 4.2.1.9)"); i++;
627
623
  }
628
624
  if (i !== kids.length) throw ns.E(C, "BasicConstraints has unexpected trailing fields");
629
625
  return { cA: cA, pathLenConstraint: pathLen };
@@ -746,11 +742,11 @@ function certExtensionDecoders(ns) {
746
742
  pcLastTag = k.tagNumber;
747
743
  var v;
748
744
  try { v = asn1.read.integerImplicit(k, k.tagNumber); } catch (e) { throw ns.E(C, "PolicyConstraints field must be an INTEGER", e); }
749
- // Bounded before Number() narrowing (a skip count past the safe-integer
750
- // range would round silently) -- exact-or-rejected.
751
- if (v < 0n || v > 2147483647n) throw ns.E(C, "PolicyConstraints skip count must be a non-negative integer within range");
752
- if (k.tagNumber === 0) rep = Number(v);
753
- else if (k.tagNumber === 1) ipm = Number(v);
745
+ // guard.range.uint31 bounds + narrows atomically (a skip count past the
746
+ // safe-integer range would round silently).
747
+ var skip = guard.range.uint31(v, ns.E, C, "PolicyConstraints skip count (RFC 5280 sec. 4.2.1.11)");
748
+ if (k.tagNumber === 0) rep = skip;
749
+ else if (k.tagNumber === 1) ipm = skip;
754
750
  else throw ns.E(C, "PolicyConstraints has an unexpected field [" + k.tagNumber + "]");
755
751
  });
756
752
  return { requireExplicitPolicy: rep, inhibitPolicyMapping: ipm };
@@ -762,10 +758,9 @@ function certExtensionDecoders(ns) {
762
758
  var n = decodeTop(buf, C, "InhibitAnyPolicy");
763
759
  if (n.tagClass !== "universal" || n.tagNumber !== _T.INTEGER) throw ns.E(C, "InhibitAnyPolicy must be an INTEGER (RFC 5280 sec. 4.2.1.14)");
764
760
  var v = readInt(n, C, "InhibitAnyPolicy");
765
- // Bounded before Number() narrowing (a skip count past the safe-integer
766
- // range would round silently) -- exact-or-rejected.
767
- if (v < 0n || v > 2147483647n) throw ns.E(C, "InhibitAnyPolicy skip count must be a non-negative integer within range");
768
- return Number(v);
761
+ // guard.range.uint31 bounds + narrows atomically (a skip count past the
762
+ // safe-integer range would round silently).
763
+ return guard.range.uint31(v, ns.E, C, "InhibitAnyPolicy skip count (RFC 5280 sec. 4.2.1.14)");
769
764
  }
770
765
 
771
766
  // subjectAltName / issuerAltName ::= GeneralNames -- decoded values surfaced.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
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:9b3c1a14-9eb9-492b-a652-b6fa68d6b4b4",
5
+ "serialNumber": "urn:uuid:2ed2aa23-5194-4472-96f3-c4013f134834",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-11T07:38:46.451Z",
8
+ "timestamp": "2026-07-11T16:09:44.713Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.1.30",
22
+ "bom-ref": "@blamejs/pki@0.1.31",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.30",
25
+ "version": "0.1.31",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.1.30",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.31",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.1.30",
57
+ "ref": "@blamejs/pki@0.1.31",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]