@blamejs/pki 0.8.9 → 0.8.11

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.
@@ -35,6 +35,7 @@ var schema = require("./schema-engine");
35
35
  var oid = require("./oid");
36
36
  var frameworkError = require("./framework-error");
37
37
  var pkix = require("./schema-pkix");
38
+ var intrinsic = require("./guard-intrinsic");
38
39
 
39
40
  var CertificateError = frameworkError.CertificateError;
40
41
  var PemError = frameworkError.PemError;
@@ -124,7 +125,7 @@ var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
124
125
  build: function (e, ctx) {
125
126
  var tbs = e.tbsMatch;
126
127
 
127
- if (!e.outerSignatureAlgorithmBytes.equals(tbs.fields.signature.node.bytes)) {
128
+ if (!intrinsic.bufferEquals(e.outerSignatureAlgorithmBytes, tbs.fields.signature.node.bytes)) {
128
129
  throw ctx.E("x509/bad-signature-algorithm", "signatureAlgorithm must match tbsCertificate.signature (RFC 5280 sec. 4.1.1.2)");
129
130
  }
130
131
 
@@ -148,7 +149,7 @@ var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
148
149
  return {
149
150
  version: version,
150
151
  serialNumber: tbs.fields.serialNumber.value,
151
- serialNumberHex: serialNode.content.toString("hex"),
152
+ serialNumberHex: intrinsic.bufToString(serialNode.content, "hex"),
152
153
  signatureAlgorithm: e.signatureAlgorithm,
153
154
  tbsSignatureAlgorithm: tbs.fields.signature.value.result,
154
155
  issuer: issuer,
@@ -213,14 +214,107 @@ function matches(root) {
213
214
  var validity = kids[i + 3];
214
215
  return schema.isUniversal(validity, TAGS.SEQUENCE) &&
215
216
  !!validity.children && validity.children.length === 2 &&
216
- validity.children.every(function (t) {
217
+ intrinsic.every(validity.children, function (t) {
217
218
  return schema.isUniversalOneOf(t, [TAGS.UTC_TIME, TAGS.GENERALIZED_TIME]);
218
219
  });
219
220
  }
220
221
 
222
+ /** @internal The certificate scope, as the shared door reads it: RFC 5280 sec. 4.2's extension
223
+ * table and the criticality that profile fixes. One scope, so the single-extension verb takes no
224
+ * `opts.scope`. */
225
+ var CERT_SCOPES = intrinsic.assign(intrinsic.create(null), {
226
+ certificate: {
227
+ decoders: function (ctx) { return pkix.certExtensionDecoders(ctx).byOid; },
228
+ criticality: pkix.certFixedCriticality(oid),
229
+ },
230
+ });
231
+ var DOOR = pkix.extensionDoor({
232
+ ns: NS, ErrorClass: CertificateError, key: "x509", kind: "certificate", what: "the certificate",
233
+ parse: parse, scopes: CERT_SCOPES, defaultScope: "certificate",
234
+ segments: function (cert) { return [{ list: cert.extensions || [], scope: "certificate" }]; },
235
+ });
236
+
237
+ /**
238
+ * @primitive pki.schema.x509.decodeExtensions
239
+ * @signature pki.schema.x509.decodeExtensions(certOrBytes, opts?) -> rows
240
+ * @since 0.8.11
241
+ * @status stable
242
+ * @spec RFC 5280 sec. 4.2
243
+ * @related pki.schema.x509.parse, pki.lint.certificate, pki.inspect.certificate
244
+ *
245
+ * Read a certificate's extensions as decoded records. `parse` hands back each extension's raw
246
+ * `extnValue` octets, because what an extension means is a question the reader asks rather than
247
+ * one the parse answers; this is the verb that asks it. The argument is the object `parse`
248
+ * returned or the bytes `parse` takes, and a parse result is read rather than re-derived.
249
+ *
250
+ * Each row is `{ oid, name, critical, value, scope, index, state, decoded, code, profile }`, with
251
+ * every field always present. `state` is the discriminator: `"decoded"` when the value read as the
252
+ * structure its identifier names, `"unrecognized"` when this toolkit registers no decoder for that
253
+ * OID, and `"undecodable"` when it does and the value did not read. Read `state` rather than
254
+ * testing `decoded`, because a valid `ocspNoCheck` decodes to `null`.
255
+ *
256
+ * `value` carries the raw octets in every arm, byte-identical to the certificate's, for a caller
257
+ * recomputing a hash or comparing an encoding. `code` is the typed error code in the undecodable
258
+ * arm and `null` elsewhere. `profile` reports the criticality the RFC fixes for that OID, as
259
+ * `{ criticalExpected, citation, deviates, violations }`, and is `null` for an extension whose
260
+ * criticality the issuer chooses.
261
+ *
262
+ * Nothing here is a verdict. An unrecognized critical extension is reported rather than refused,
263
+ * whatever `opts.strict` says, because RFC 5280 sec. 4.2's rule about one is the caller's to
264
+ * apply and refusing it would make a certificate carrying a private critical extension
265
+ * unreadable. The two facts that rule needs, `critical` and `state`, are on the row.
266
+ *
267
+ * @opts
268
+ * - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
269
+ * undecodable value. It does not turn an unrecognized OID into a throw.
270
+ * - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call. Each may only
271
+ * tighten the built-in ceiling, and each bounds the decoding this call performs.
272
+ * @example
273
+ * var pair = await pki.key.generate("Ed25519");
274
+ * var certDer = await pki.x509.sign({ subject: "leaf.example", subjectPublicKey: await pki.key.export(pair.publicKey),
275
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2027-01-01T00:00:00Z"),
276
+ * extensions: { basicConstraints: { cA: false }, subjectAltName: [{ dNSName: "leaf.example" }] } },
277
+ * { key: await pki.key.export(pair.privateKey) });
278
+ * var rows = pki.schema.x509.decodeExtensions(certDer);
279
+ * rows.filter(function (r) { return r.critical && r.state !== "decoded"; }).length; // -> 0
280
+ * // The filter above is the RFC 5280 sec. 4.2 rule: a relying party rejects a certificate whose
281
+ * // critical extensions it cannot read.
282
+ */
283
+ var decodeExtensions = DOOR.decodeExtensions;
284
+
285
+ /**
286
+ * @primitive pki.schema.x509.decodeExtension
287
+ * @signature pki.schema.x509.decodeExtension(ext, opts?) -> row
288
+ * @since 0.8.11
289
+ * @status stable
290
+ * @spec RFC 5280 sec. 4.2
291
+ * @related pki.schema.x509.decodeExtensions
292
+ *
293
+ * Read one certificate extension record as a decoded row. The argument is one element of a
294
+ * `pki.schema.x509.parse` result's `extensions`, as `{ oid, name, critical, value }`, and the row
295
+ * is the one `decodeExtensions` would have produced for it, with `index` 0.
296
+ *
297
+ * @opts
298
+ * - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
299
+ * undecodable value.
300
+ * - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call. Each may only
301
+ * tighten the built-in ceiling.
302
+ * @example
303
+ * var pair = await pki.key.generate("Ed25519");
304
+ * var certDer = await pki.x509.sign({ subject: "leaf.example", subjectPublicKey: await pki.key.export(pair.publicKey),
305
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2027-01-01T00:00:00Z"),
306
+ * extensions: { basicConstraints: { cA: false } } },
307
+ * { key: await pki.key.export(pair.privateKey) });
308
+ * var parsed = pki.schema.x509.parse(certDer);
309
+ * pki.schema.x509.decodeExtension(parsed.extensions[0]).state; // -> "decoded"
310
+ */
311
+ var decodeExtension = DOOR.decodeExtension;
312
+
221
313
  module.exports = {
222
314
  parse: parse,
223
315
  pemDecode: pemDecode,
224
316
  pemEncode: pemEncode,
225
317
  matches: matches,
318
+ decodeExtensions: decodeExtensions,
319
+ decodeExtension: decodeExtension,
226
320
  };
package/lib/sigstore.js CHANGED
@@ -1055,6 +1055,13 @@ async function _verifyChain(leaf, chainDers, fulcioRoots, timeMs) {
1055
1055
 
1056
1056
 
1057
1057
  var SAN_OID = oid.byName("subjectAltName");
1058
+ /** @internal The shared table's entry, so the SAN this verb reads is read under the rules every
1059
+ * other consumer of a GeneralNames reads it under. It is built on the certificate namespace rather
1060
+ * than this module's, so a malformed name raises a CertificateError that `_identity` re-types to
1061
+ * `sigstore/bad-certificate` with that fault as its cause. That is the code this module already
1062
+ * reports for a certificate it cannot read, so the fix adds no new code to the public surface. */
1063
+ var _sanDecoder = pkix.certExtensionDecoders(
1064
+ pkix.makeNS("x509", frameworkError.CertificateError, oid)).byOid[SAN_OID];
1058
1065
  var FULCIO_PREFIX = _join(_arraySlice(_split(oid.byName("otherName"), "."), 0, -1), ".") + ".";
1059
1066
  function _identity(leaf) {
1060
1067
  var out = { san: null, extensions: _create(null) };
@@ -1072,29 +1079,36 @@ function _identity(leaf) {
1072
1079
  return out;
1073
1080
  }
1074
1081
  var GN_TYPE = _assign(_create(null), { 1: "rfc822Name", 2: "dNSName", 6: "uri" });
1082
+ /** @internal The GeneralNames come from the shared extension decoder, which is where the RFC 5280
1083
+ * sec. 4.2.1.6 rules live: at least one name, a non-empty IA5String for the text forms, and no
1084
+ * control byte inside one (CVE-2009-2408). Reading the SEQUENCE here instead left this verb
1085
+ * accepting a dNSName carrying a NUL or a CR and handing it on as the signer's identity.
1086
+ *
1087
+ * The one-identity rule counts every name the SAN carries, not the subset this verb turns into an
1088
+ * identity string. Counting only the recognized forms let a certificate pair a URI with an
1089
+ * iPAddress and pass as single-identity, with the second binding invisible to the caller's policy
1090
+ * -- which is the thing the rule exists to stop. A SAN of one name this verb has no string form
1091
+ * for still yields null, so a policy gates on the null rather than on a name nothing read. */
1075
1092
  function _sanValue(ext) {
1076
- var seq = asn1.decode(ext.value);
1077
- var names = [];
1078
- for (var i = 0; i < (seq.children || []).length; i++) {
1079
- var n = seq.children[i];
1080
- if (n.tagClass !== "context") continue;
1081
- var val = null;
1082
- if (!n.constructed && GN_TYPE[n.tagNumber]) {
1083
- val = { type: GN_TYPE[n.tagNumber], value: _bufToString(asn1.read.octetStringImplicit(n, n.tagNumber), "utf8") };
1084
- } else if (n.constructed && n.tagNumber === 0 && (n.children || []).length >= 2) {
1085
- var inner = (n.children[1].children || [])[0] || n.children[1];
1086
- var v;
1087
- try {
1088
- v = asn1.read.string(inner);
1089
- } catch (_e) {
1090
- v = null;
1091
- }
1092
- val = { type: "otherName", oid: asn1.read.oid(n.children[0]), value: v };
1093
- }
1094
- if (val) guard.list.append(names, val);
1093
+ /** @internal The shared decoder is the gate: it applies the sec. 4.2.1.6 rules and throws on a
1094
+ * name that breaks one. What it returns is a GeneralNames view rather than the codec nodes the
1095
+ * mapping below reads, so the count comes from it and the one name is then taken from the raw
1096
+ * decode, which carries the node shape `octetStringImplicit` and the otherName walk need. */
1097
+ var validated = _sanDecoder(ext.value).names || [];
1098
+ if (validated.length > 1) throw _err("sigstore/bad-certificate", "the Fulcio certificate carries multiple SAN identities");
1099
+ var n = (asn1.decode(ext.value).children || [])[0];
1100
+ if (!n || n.tagClass !== "context") return null;
1101
+ if (!n.constructed && GN_TYPE[n.tagNumber]) {
1102
+ return { type: GN_TYPE[n.tagNumber], value: _bufToString(asn1.read.octetStringImplicit(n, n.tagNumber), "utf8") };
1103
+ }
1104
+ if (n.constructed && n.tagNumber === 0 && (n.children || []).length >= 2) {
1105
+ var inner = (n.children[1].children || [])[0] || n.children[1];
1106
+ var v;
1107
+ try { v = asn1.read.string(inner); }
1108
+ catch (_e) { v = null; } // allow:swallow-unverified an otherName body with no string form has no identity string, which the caller's policy then gates on
1109
+ return { type: "otherName", oid: asn1.read.oid(n.children[0]), value: v };
1095
1110
  }
1096
- if (names.length > 1) throw _err("sigstore/bad-certificate", "the Fulcio certificate carries multiple SAN identities");
1097
- return names[0] || null;
1111
+ return null;
1098
1112
  }
1099
1113
  function _fulcioExtValue(ext, leafArc) {
1100
1114
  if (!_isBuffer(ext.value)) throw _err("sigstore/bad-certificate", "Fulcio extension " + ext.oid + " has no value");
package/lib/tsp-sign.js CHANGED
@@ -34,6 +34,9 @@ var pkiBuild = require("./pki-build");
34
34
  var constants = require("./constants");
35
35
  var TspError = frameworkError.TspError;
36
36
  var _NS = pkix.makeNS("tsp", TspError, oid);
37
+ /** @internal The shared certificate-extension decoders, so an extension this module reads is read
38
+ * under the rules every other consumer reads it under rather than under a second set written here. */
39
+ var _EXT_DECODERS = pkix.certExtensionDecoders(_NS).byOid;
37
40
  var b = asn1.build;
38
41
  function _err(code, message, cause) { return new TspError(code, message, cause); }
39
42
 
@@ -487,12 +490,13 @@ function _checkTsaCertUsage(tsaCertDer) {
487
490
  var exts = (cert.extensions || []).filter(function (e) { return e.oid === O("extKeyUsage"); });
488
491
  if (exts.length !== 1) return "tsp/bad-eku";
489
492
  if (!exts[0].critical) return "tsp/eku-not-critical";
490
- var purposes, ekuNode;
491
- try { ekuNode = asn1.decode(exts[0].value); }
492
- catch (_e) { return "tsp/bad-eku"; }
493
- if (ekuNode.tagClass !== "universal" || ekuNode.tagNumber !== asn1.TAGS.SEQUENCE || !ekuNode.children) return "tsp/bad-eku";
494
- try { purposes = ekuNode.children.map(function (c) { return asn1.read.oid(c); }); }
495
- catch (_e) { return "tsp/bad-eku"; }
493
+ /** @internal Read through the shared decoder, which is where the sec. 4.2.1.12 rules live: an
494
+ * ExtKeyUsage carries at least one KeyPurposeId. Reading the SEQUENCE here instead called an
495
+ * empty one well-formed and then reported it as the wrong PURPOSE, so the same class of fault
496
+ * answered with a different code here than at every other consumer of this extension. */
497
+ var purposes;
498
+ try { purposes = _EXT_DECODERS[O("extKeyUsage")](exts[0].value); }
499
+ catch (_e) { return "tsp/bad-eku"; } // allow:swallow-unverified this verb answers with a code rather than throwing, and every malformed shape is the one code
496
500
  if (purposes.length !== 1 || purposes[0] !== O("timeStamping")) return "tsp/eku-not-exclusive";
497
501
  var ku;
498
502
  try { ku = pkix.keyUsageOf(_NS, cert, _err, "tsp/bad-key-usage", "TSA certificate"); }
@@ -508,13 +512,16 @@ function _tsaMatchesCert(tsaBytes, signerCert) {
508
512
  if (gn.tagNumber === 4 && gn.value && guard.name.dnEqual(gn.value.rdns, signerCert.subject.rdns, _err, "tsp/tsa-mismatch", "the TSA subject")) return true;
509
513
  var sanExt = (signerCert.extensions || []).filter(function (e) { return e.oid === O("subjectAltName"); })[0];
510
514
  if (sanExt) {
511
- var san;
512
- try { san = asn1.decode(sanExt.value); }
513
- catch (_e) { return false; }
514
- if (san.tagClass === "universal" && san.tagNumber === asn1.TAGS.SEQUENCE && san.children) {
515
- for (var i = 0; i < san.children.length; i++) {
516
- if (san.children[i].bytes.equals(tsaBytes)) return true;
517
- }
515
+ /** @internal Gated by the shared decoder before the comparison, so a SAN this toolkit refuses
516
+ * everywhere else cannot be a source of identity here. The match itself stays a comparison of
517
+ * the encoded GeneralName bytes, which is what "the token names this TSA" means: the name in
518
+ * the token and the name in the certificate have to be the same name, not merely names that
519
+ * decode alike. */
520
+ var names;
521
+ try { names = _EXT_DECODERS[O("subjectAltName")](sanExt.value).names || []; }
522
+ catch (_e) { return false; } // allow:swallow-unverified a SAN that does not decode names no TSA, which is what this verb answers
523
+ for (var i = 0; i < names.length; i++) {
524
+ if (names[i].bytes && names[i].bytes.equals(tsaBytes)) return true;
518
525
  }
519
526
  }
520
527
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.9",
3
+ "version": "0.8.11",
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:1d0c3725-eae5-4743-8430-5684e3e52ec7",
5
+ "serialNumber": "urn:uuid:41170205-6ef3-4f77-9577-7ba33e5632bc",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-23T19:11:33.586Z",
8
+ "timestamp": "2026-09-23T22:37:26.125Z",
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.8.9",
22
+ "bom-ref": "@blamejs/pki@0.8.11",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.9",
25
+ "version": "0.8.11",
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.8.9",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.11",
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.8.9",
57
+ "ref": "@blamejs/pki@0.8.11",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]