@blamejs/pki 0.6.34 → 0.6.35

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,20 @@ 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.6.35 — 2026-09-04
8
+
9
+ Three redundant C509 spellings are refused, so a certificate has one encoding and an issuer signature cannot cover two different C509 byte strings.
10
+
11
+ ### Fixed
12
+
13
+ - pki.schema.c509.encode refuses a serialNumberHex that is not an even-length hexadecimal string instead of decoding a prefix of it. An odd-length or partly non-hexadecimal value previously became a different, valid serial number without any error, which the encoder's own re-parse could not catch. The check applies whether the result is re-emitted from its preserved field bytes or built field by field, so a caller is told the value is malformed rather than having it quietly ignored.
14
+
15
+ ### Security
16
+
17
+ - pki.schema.c509.parse refuses an unsigned bignum whose first octet is 0x00, including a value that is only that octet, so every number has exactly one C509 encoding (draft sec. 3.1.2). A zero serial previously had two spellings that reconstructed byte-identical DER, letting one issuer signature cover two distinct C509 certificates. The rule covers the certificate serial number, the authorityKeyIdentifier serial, and the RSA modulus and exponent.
18
+ - pki.schema.c509.parse refuses an RSA subjectPublicKey written as [modulus, 65537] instead of the bare modulus. Draft sec. 3.1.9 omits the array and the exponent when the exponent is 65537, so the array spelling of such a key is a second encoding of one certificate, reconstructing byte-identical DER. An RSA key with any other exponent still uses the array form.
19
+ - pki.schema.c509.parse refuses a notAfter written as the epoch 253402300799 rather than the CBOR simple value null. Draft sec. 3.1.5 encodes the no-expiration time 99991231235959Z as null, so the explicit epoch is a second spelling that reconstructs the same GeneralizedTime and therefore the same DER. A notBefore is unaffected, and pki.schema.c509.encode refuses that Date on notAfter instead of emitting a form the decoder rejects.
20
+
7
21
  ## v0.6.34 — 2026-09-04
8
22
 
9
23
  An injected transport that does not return a promise is reported as a typed option error naming the option, in every client that accepts one.
@@ -33,6 +33,7 @@ var C = constants;
33
33
  var C509Error = frameworkError.C509Error;
34
34
  function _err(code, message, cause) { return new C509Error(code, message, cause); }
35
35
  var MAX_EPOCH_SECONDS = 8640000000000n;
36
+ var NO_EXPIRY_SECONDS = 253402300799n;
36
37
 
37
38
  function _name(n) { var d = oid.byName(n); if (!d) { throw new Error("schema-c509: unregistered OID name " + JSON.stringify(n)); } return n; }
38
39
 
@@ -142,14 +143,29 @@ function _biguint(node, code, label) {
142
143
  if (!node || node.majorType !== 2) throw _err(code, label + " must be an unwrapped CBOR byte string (~biguint)");
143
144
  var b = node.content;
144
145
  if (b.length > C.LIMITS.CBOR_MAX_BIGUINT_BYTES) throw _err(code, label + " exceeds the ~biguint byte cap");
145
- if (b.length > 1 && b[0] === 0x00) throw _err("c509/non-minimal-serial", label + " has a redundant leading 0x00 (~biguint omits the sign octet)");
146
+ if (b.length > 0 && b[0] === 0x00) throw _err("c509/non-minimal-serial", label + " has a redundant leading 0x00 (~biguint omits the sign octet)");
146
147
  return b.length ? BigInt("0x" + b.toString("hex")) : 0n;
147
148
  }
148
149
 
150
+ function _hexBytes(s, code, label) {
151
+ if (typeof s !== "string") throw _err(code, label + " must be a hexadecimal string");
152
+ if ((s.length % 2) !== 0) throw _err(code, label + " must have an even number of hexadecimal digits");
153
+ for (var i = 0; i < s.length; i++) {
154
+ var c = s.charCodeAt(i);
155
+ if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70))) {
156
+ throw _err(code, label + " must be hexadecimal");
157
+ }
158
+ }
159
+ return Buffer.from(s, "hex");
160
+ }
161
+
149
162
  function _time(node, allowNull, label) {
150
163
  if (allowNull && node.majorType === 7 && node.ai === 22) return null;
151
164
  if (node.majorType !== 0) throw _err("c509/bad-validity", label + " must be an unwrapped CBOR epoch integer (~time)");
152
165
  var secs = node.argument;
166
+ if (allowNull && secs === NO_EXPIRY_SECONDS) {
167
+ throw _err("c509/bad-validity", label + " of 99991231235959Z is encoded as the CBOR simple value null (draft sec. 3.1.5), not as the epoch " + NO_EXPIRY_SECONDS);
168
+ }
153
169
  if (secs > MAX_EPOCH_SECONDS) throw _err("c509/bad-validity", label + " is outside the representable Date range");
154
170
  return new Date(C.TIME.seconds(Number(secs)));
155
171
  }
@@ -1521,6 +1537,7 @@ function parse(input) {
1521
1537
  if (f[8].majorType === 2) rsaKey = { modulus: _biguint(f[8], "c509/bad-spki", "RSA modulus"), exponent: 65537n };
1522
1538
  else if (f[8].majorType === 4 && f[8].children && f[8].children.length === 2) {
1523
1539
  rsaKey = { modulus: _biguint(f[8].children[0], "c509/bad-spki", "RSA modulus"), exponent: _biguint(f[8].children[1], "c509/bad-spki", "RSA exponent") };
1540
+ if (rsaKey.exponent === 65537n) throw _err("c509/bad-spki", "an RSA subjectPublicKey with exponent 65537 omits the array and the exponent (sec. 3.1.9)");
1524
1541
  } else throw _err("c509/bad-spki", "an RSA subjectPublicKey must be a ~biguint modulus or [modulus, exponent]");
1525
1542
  if (rsaKey.modulus < 1n || rsaKey.exponent < 1n) throw _err("c509/bad-spki", "an RSA modulus and public exponent must be positive");
1526
1543
  } else {
@@ -1650,6 +1667,7 @@ function _validityUint(date, label) {
1650
1667
  function _encodeC509Array(r) {
1651
1668
  if (Buffer.isBuffer(r._fieldBytes)) {
1652
1669
  if (!Buffer.isBuffer(r.signatureValue)) throw _err("c509/bad-input", "a re-emitted certificate must carry a Buffer signatureValue");
1670
+ if (r.serialNumberHex != null) _hexBytes(r.serialNumberHex, "c509/bad-serial", "serialNumberHex");
1653
1671
  var out = Buffer.concat([Buffer.from([0x8b]), r._fieldBytes, cbor.build.byteString(r.signatureValue)]);
1654
1672
  parse(out);
1655
1673
  return out;
@@ -1658,7 +1676,7 @@ function _encodeC509Array(r) {
1658
1676
  var pkKey = r.subjectPublicKeyAlgorithm.curve ? r.subjectPublicKeyAlgorithm.name + "|" + r.subjectPublicKeyAlgorithm.curve : r.subjectPublicKeyAlgorithm.name;
1659
1677
  var arr = cbor.build.array([
1660
1678
  cbor.build.int(BigInt(r.certificateType)),
1661
- cbor.build.byteString(r.serialNumberHex != null ? Buffer.from(r.serialNumberHex, "hex") : _minBytes(r.serialNumber)),
1679
+ cbor.build.byteString(r.serialNumberHex != null ? _hexBytes(r.serialNumberHex, "c509/bad-serial", "serialNumberHex") : _minBytes(r.serialNumber)),
1662
1680
  _encAlgorithm(r.signatureAlgorithm, SIG_ALG_TO_INT, r.signatureAlgorithm.name),
1663
1681
  _encName(r.issuer, false),
1664
1682
  _validityUint(r.validity.notBefore, "validityNotBefore"),
@@ -1753,7 +1771,7 @@ function _keyUsageBitsFromDer(extnValue) {
1753
1771
  }
1754
1772
  return _namedBitsFromContent(bs.bytes, bs.unusedBits);
1755
1773
  }
1756
- var _NO_EXPIRY = Date.UTC(9999, 11, 31, 23, 59, 59);
1774
+ var _NO_EXPIRY = C.TIME.seconds(Number(NO_EXPIRY_SECONDS));
1757
1775
  var NODE_TO_WEBCRYPTO = Object.assign(Object.create(null), { "prime256v1": "P-256", "secp384r1": "P-384", "secp521r1": "P-521" });
1758
1776
  var WEBCRYPTO_FIELD_BYTES = Object.assign(Object.create(null), { "P-256": 32, "P-384": 48, "P-521": 66 });
1759
1777
  var SIG_ALG_TO_CURVE = Object.assign(Object.create(null), { "ecdsaWithSHA256": "P-256", "ecdsaWithSHA384": "P-384", "ecdsaWithSHA512": "P-521" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.6.34",
3
+ "version": "0.6.35",
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:c595f123-cf34-46db-924e-ffa5c0c5018e",
5
+ "serialNumber": "urn:uuid:3601ee14-7f92-4a66-bdb2-bab4e6360e45",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-04T22:46:35.510Z",
8
+ "timestamp": "2026-09-04T23:41:31.757Z",
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.6.34",
22
+ "bom-ref": "@blamejs/pki@0.6.35",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.6.34",
25
+ "version": "0.6.35",
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.6.34",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.6.35",
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.6.34",
57
+ "ref": "@blamejs/pki@0.6.35",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]