@blamejs/pki 0.1.7 → 0.1.8

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,7 +4,30 @@ 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.7 — 2026-07-05
7
+ ## v0.1.8 — 2026-07-05
8
+
9
+ A PKCS#10 certification-request parser joins the pki.schema family.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.csr.parse — a PKCS#10 CertificationRequest parser per RFC 2986. It turns a DER Buffer or a 'CERTIFICATE REQUEST' PEM string into a structured object: version, subject distinguished name, subjectPublicKeyInfo, the requested attributes (each with its type OID, resolved name, and raw-DER values), and the signatureAlgorithm / signatureValue over the CertificationRequestInfo — with the raw certificationRequestInfoBytes returned for signature verification. It composes the shared schema engine and PKIX sub-schemas (AlgorithmIdentifier, Name, SubjectPublicKeyInfo), so a certification request inherits the identical fail-closed structural rules and a malformed request throws a typed CsrError (csr/*); a leaf-level codec fault surfaces as asn1/*. The version must be v1 (INTEGER 0), the [0] IMPLICIT attributes element is mandatory, and each attribute's values SET must be non-empty. pki.schema.parse now detects and routes certification requests, and pki.schema.all() lists it alongside crl and x509. pki.schema.csr.pemDecode / pemEncode handle the PEM envelope.
14
+ - pki.asn1.read.enumerated — reads an ENUMERATED value from a decoded node (the same content rules as an INTEGER), the counterpart to the now-strict pki.asn1.read.integer.
15
+
16
+ ### Changed
17
+
18
+ - C.TIME.ms is renamed to C.TIME.milliseconds, so every C.TIME duration helper now reads as a full word (milliseconds, seconds, minutes, hours, days, weeks). The behaviour is unchanged — it still returns an integer millisecond count.
19
+
20
+ ### Security
21
+
22
+ - pki.asn1.read.integer now rejects an ENUMERATED-tagged node. INTEGER and ENUMERATED share DER content encoding, so an INTEGER-pinned field — a certificate or certification-request version, a serial number, or a cRLNumber — mis-encoded as ENUMERATED was previously decoded as though it were the INTEGER, a type confusion that let malformed DER parse where a conformant reader rejects it. read.integer is now strict on the tag, and ENUMERATED values are read with the new pki.asn1.read.enumerated. Certificate, CRL, and certification-request parsing reject these inputs fail-closed.
23
+ - SubjectPublicKeyInfo is now required to be a universal SEQUENCE across the certificate and certification-request parsers — a context-tagged or SET-tagged constructed node carrying a well-formed algorithm and key is no longer accepted as an SPKI.
24
+ - SET OF components are now required to be in ascending DER order (X.690 §11.6) wherever the schema declares a SET OF — a relative distinguished name, and a certification request's attributes and attribute values. A non-canonical, unsorted encoding is rejected fail-closed.
25
+
26
+ ### Migration
27
+
28
+ - Replace C.TIME.ms(n) with C.TIME.milliseconds(n). The other C.TIME and C.BYTES helpers are unchanged.
29
+
30
+ ## v0.1.7 — 2026-07-04
8
31
 
9
32
  A unified pki.schema family: the structure-schema engine, the X.509 parser, a new CRL parser, and a detect-and-route orchestrator.
10
33
 
package/README.md CHANGED
@@ -193,9 +193,10 @@ is callable today; nothing below is a stub.
193
193
  | `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 |
194
194
  | `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields — `parse`, `pemDecode`, `pemEncode` |
195
195
  | `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` |
196
+ | `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` |
196
197
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
197
198
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
198
- | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError`, each carrying a stable `code` in `domain/reason` form |
199
+ | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError`, each carrying a stable `code` in `domain/reason` form |
199
200
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
200
201
 
201
202
  ### CLI
@@ -209,7 +210,7 @@ pki parse cert.pem # structured JSON summary of a certific
209
210
 
210
211
  ### What's coming
211
212
 
212
- Certificate build/sign/verify, CRLs, CSRs (PKCS#10), certification-path
213
+ Certificate build/sign/verify, certification-path
213
214
  validation, CMS (SignedData / EnvelopedData / EncryptedData / AuthenticatedData),
214
215
  OCSP, RFC 3161 timestamping, PKCS#8 / SPKI / PBES2 / PKCS#12, and the
215
216
  post-quantum certificate and CMS surface (ML-DSA / ML-KEM / SLH-DSA and hybrid
package/lib/asn1-der.js CHANGED
@@ -273,15 +273,15 @@ function readBoolean(node) {
273
273
  throw new Asn1Error("asn1/bad-boolean", "DER BOOLEAN must be 0x00 or 0xFF, got 0x" + v.toString(16));
274
274
  }
275
275
 
276
- function readInteger(node) {
277
- if (node.tagClass === "universal" && node.tagNumber === TAGS.ENUMERATED) {
278
- // ENUMERATED shares INTEGER's content encoding.
279
- } else {
280
- _expectUniversal(node, TAGS.INTEGER, "readInteger");
281
- }
282
- _expectPrimitive(node, "readInteger");
276
+ // INTEGER and ENUMERATED share DER content encoding (a two's-complement big-endian
277
+ // value) but are DISTINCT universal types. This decodes that shared content — the
278
+ // caller asserts the tag first. `typeName` names the type in error messages; the
279
+ // stable error codes stay the integer family (a caller switching on `code` treats
280
+ // both alike since the encoding is identical).
281
+ function _readIntegerLikeContent(node, typeName, who) {
282
+ _expectPrimitive(node, who);
283
283
  var c = node.content;
284
- if (c.length === 0) throw new Asn1Error("asn1/bad-integer", "INTEGER must have at least 1 content octet");
284
+ if (c.length === 0) throw new Asn1Error("asn1/bad-integer", typeName + " must have at least 1 content octet");
285
285
  // Defense-in-depth: refuse an over-cap magnitude before touching BigInt.
286
286
  // A one-shot hex parse is linear, but the length still bounds worst-case
287
287
  // work — reject a hostile length prefix up front. The cap bounds the
@@ -290,17 +290,34 @@ function readInteger(node) {
290
290
  // modulus is 16384 magnitude bytes plus that sign pad).
291
291
  if (c.length > constants.LIMITS.DER_MAX_INTEGER_BYTES + 1) {
292
292
  throw new Asn1Error("asn1/integer-too-large",
293
- "INTEGER content " + c.length + " bytes exceeds cap " + (constants.LIMITS.DER_MAX_INTEGER_BYTES + 1));
293
+ typeName + " content " + c.length + " bytes exceeds cap " + (constants.LIMITS.DER_MAX_INTEGER_BYTES + 1));
294
294
  }
295
295
  if (c.length > 1) {
296
- if (c[0] === 0x00 && (c[1] & 0x80) === 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal positive INTEGER");
297
- if (c[0] === 0xff && (c[1] & 0x80) !== 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal negative INTEGER");
296
+ if (c[0] === 0x00 && (c[1] & 0x80) === 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal positive " + typeName);
297
+ if (c[0] === 0xff && (c[1] & 0x80) !== 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal negative " + typeName);
298
298
  }
299
299
  var neg = (c[0] & 0x80) !== 0;
300
300
  var mag = c.length ? BigInt("0x" + Buffer.from(c).toString("hex")) : 0n;
301
301
  return neg ? mag - (1n << BigInt(c.length * 8)) : mag;
302
302
  }
303
303
 
304
+ // read.integer is STRICT on the tag: an ENUMERATED node (which shares INTEGER's
305
+ // content encoding) is NOT accepted here. Coercing an ENUMERATED to an INTEGER is a
306
+ // type confusion — an INTEGER-pinned field (a certificate/CSR version, a serial
307
+ // number, a cRLNumber) encoded as ENUMERATED would slip past a lenient reader.
308
+ // Read ENUMERATED values with read.enumerated.
309
+ function readInteger(node) {
310
+ _expectUniversal(node, TAGS.INTEGER, "readInteger");
311
+ return _readIntegerLikeContent(node, "INTEGER", "readInteger");
312
+ }
313
+
314
+ // read.enumerated reads an ENUMERATED value; its content rules are identical to
315
+ // INTEGER (non-empty, minimally encoded, magnitude within the cap).
316
+ function readEnumerated(node) {
317
+ _expectUniversal(node, TAGS.ENUMERATED, "readEnumerated");
318
+ return _readIntegerLikeContent(node, "ENUMERATED", "readEnumerated");
319
+ }
320
+
304
321
  function readBitString(node) {
305
322
  _expectUniversal(node, TAGS.BIT_STRING, "readBitString");
306
323
  _expectPrimitive(node, "readBitString");
@@ -754,6 +771,7 @@ module.exports = {
754
771
  read: {
755
772
  boolean: readBoolean,
756
773
  integer: readInteger,
774
+ enumerated: readEnumerated,
757
775
  bitString: readBitString,
758
776
  octetString: readOctetString,
759
777
  nullValue: readNull,
package/lib/constants.js CHANGED
@@ -64,7 +64,7 @@ var DAYS_PER_WEEK = 7;
64
64
  * // -> 31536000000
65
65
  */
66
66
  var TIME = {
67
- ms: function (n) { return Math.round(_positive(n, "C.TIME.ms")); },
67
+ milliseconds: function (n) { return Math.round(_positive(n, "C.TIME.milliseconds")); },
68
68
  seconds: function (n) { return Math.round(_positive(n, "C.TIME.seconds") * MS_PER_SECOND); },
69
69
  minutes: function (n) { return Math.round(_positive(n, "C.TIME.minutes") * SECONDS_PER_MINUTE * MS_PER_SECOND); },
70
70
  hours: function (n) { return Math.round(_positive(n, "C.TIME.hours") * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND); },
@@ -129,6 +129,10 @@ var CrlError = defineClass("CrlError", { withCause: true });
129
129
  // no registered format / does not decode). (code, message) shape like the rest.
130
130
  var SchemaError = defineClass("SchemaError", { withCause: true });
131
131
 
132
+ // CsrError — a byte sequence that is not a well-formed PKCS#10 CertificationRequest
133
+ // (RFC 2986).
134
+ var CsrError = defineClass("CsrError", { withCause: true });
135
+
132
136
  module.exports = {
133
137
  PkiError: PkiError,
134
138
  defineClass: defineClass,
@@ -139,4 +143,5 @@ module.exports = {
139
143
  CertificateError: CertificateError,
140
144
  CrlError: CrlError,
141
145
  SchemaError: SchemaError,
146
+ CsrError: CsrError,
142
147
  };
package/lib/oid.js CHANGED
@@ -71,7 +71,7 @@ var FAMILIES = {
71
71
  sha384WithRSAEncryption: 12, sha512WithRSAEncryption: 13 } },
72
72
 
73
73
  // PKCS#9 attribute types.
74
- pkcs9: { base: [1, 2, 840, 113549, 1, 9], of: { emailAddress: 1 } },
74
+ pkcs9: { base: [1, 2, 840, 113549, 1, 9], of: { emailAddress: 1, challengePassword: 7, extensionRequest: 14 } },
75
75
 
76
76
  // ANSI X9.62 EC public key, named curve, and ECDSA signatures.
77
77
  ansiX962: { base: [1, 2, 840, 10045], of: {
package/lib/schema-all.js CHANGED
@@ -12,7 +12,8 @@
12
12
  * @intro
13
13
  * The schema family: a declarative ASN.1 structure-schema engine and the
14
14
  * per-format parsers built on it. Every format (X.509 certificates, CRLs,
15
- * and — as they land — CMS / CSR / PKCS#8) is a member that COMPOSES the
15
+ * PKCS#10 certification requests, and — as they land — CMS / PKCS#8) is a
16
+ * member that COMPOSES the
16
17
  * shared engine and the shared PKIX sub-schemas (AlgorithmIdentifier, Name,
17
18
  * Extension), so a structural rule — bounds-checked positional reads,
18
19
  * optional / tagged field ordering, SET-OF uniqueness, fail-closed typed
@@ -33,6 +34,7 @@ var engine = require("./schema-engine");
33
34
  var pkix = require("./schema-pkix");
34
35
  var x509 = require("./schema-x509");
35
36
  var crl = require("./schema-crl");
37
+ var csr = require("./schema-csr");
36
38
  var frameworkError = require("./framework-error");
37
39
 
38
40
  var SchemaError = frameworkError.SchemaError;
@@ -49,11 +51,21 @@ var ENTRY = { pemLabel: null, PemError: PemError, ErrorClass: SchemaError, prefi
49
51
  // logic. Order matters: the most specific detector wins, so a new member is
50
52
  // inserted ahead of any more-permissive one.
51
53
  var FORMATS = [
54
+ {
55
+ // CertificationRequest ::= SEQUENCE { certificationRequestInfo,
56
+ // signatureAlgorithm, signature } — the same outer 3-element shape,
57
+ // distinguished by a CertificationRequestInfo of EXACTLY four children
58
+ // ending in the IMPLICIT [0] attributes element. Checked first because that
59
+ // detector is the most specific and mutually exclusive with the others.
60
+ name: "csr",
61
+ module: csr,
62
+ detect: csr.matches,
63
+ parse: function (input) { return csr.parse(input); },
64
+ },
52
65
  {
53
66
  // CertificateList ::= SEQUENCE { tbsCertList, signatureAlgorithm,
54
67
  // signatureValue } — the same outer shape as a certificate, distinguished
55
68
  // by its tbsCertList (a bare Time at the certificate's Validity position).
56
- // Checked first because its detector is the specific one.
57
69
  name: "crl",
58
70
  module: crl,
59
71
  detect: crl.matches,
@@ -81,7 +93,7 @@ var FORMATS = [
81
93
  * The names of every registered format, in detection order.
82
94
  *
83
95
  * @example
84
- * pki.schema.all(); // → ["crl", "x509"]
96
+ * pki.schema.all(); // → ["csr", "crl", "x509"]
85
97
  */
86
98
  function all() { return FORMATS.map(function (f) { return f.name; }); }
87
99
 
@@ -121,6 +133,7 @@ module.exports = {
121
133
  engine: engine,
122
134
  x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode },
123
135
  crl: { parse: crl.parse, pemDecode: crl.pemDecode },
136
+ csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
124
137
  all: all,
125
138
  parse: parse,
126
139
  };
package/lib/schema-crl.js CHANGED
@@ -74,11 +74,9 @@ function decodeExt(ext) {
74
74
  value = asn1.read.integer(asn1.decode(ext.value));
75
75
  if (value < 0n) throw new Error("cRLNumber must be non-negative (INTEGER 0..MAX)");
76
76
  } else if (ext.oid === OID_REASON_CODE) { // reasonCode ::= ENUMERATED (CRLReason)
77
- var rn = asn1.decode(ext.value);
78
- if (rn.tagClass !== "universal" || rn.tagNumber !== TAGS.ENUMERATED) {
79
- throw new Error("reasonCode must be an ENUMERATED");
80
- }
81
- var reason = asn1.read.integer(rn);
77
+ // read.enumerated asserts the ENUMERATED tag (a bare INTEGER is rejected)
78
+ // the codec owns the tag check, so this reader need not repeat it.
79
+ var reason = asn1.read.enumerated(asn1.decode(ext.value));
82
80
  if (CRL_REASONS.indexOf(reason) === -1) throw new Error("undefined CRLReason " + reason.toString());
83
81
  value = Number(reason);
84
82
  } else if (ext.oid === OID_INVALIDITY_DATE) { // invalidityDate ::= GeneralizedTime
@@ -153,17 +151,15 @@ var TBS_CERTLIST = schema.seq([
153
151
  });
154
152
 
155
153
  // CertificateList ::= SEQUENCE { tbsCertList, signatureAlgorithm, signatureValue }
156
- var CERTIFICATE_LIST = schema.seq([
157
- schema.field("tbsCertList", TBS_CERTLIST),
158
- schema.field("signatureAlgorithm", ALGORITHM_IDENTIFIER),
159
- schema.field("signatureValue", schema.bitString()),
160
- ], {
161
- assert: "sequence", arity: { exact: 3 }, code: "crl/not-a-crl", what: "CertificateList",
162
- build: function (m) {
163
- var tbsMatch = m.fields.tbsCertList.value;
164
- var tbs = tbsMatch.result;
154
+ // the shared SIGNED envelope. The CRL-specific invariants (outer==inner
155
+ // signatureAlgorithm agreement, non-empty issuer, v2-only extensions) live in the
156
+ // build; the envelope owns the SEQUENCE-of-3 shape and signature extraction.
157
+ var CERTIFICATE_LIST = pkix.signedEnvelope(NS, TBS_CERTLIST, {
158
+ code: "crl/not-a-crl", what: "CertificateList",
159
+ build: function (e) {
160
+ var tbs = e.tbsMatch.result;
165
161
  // RFC 5280 §5.1.1.2 — the outer signatureAlgorithm MUST equal tbsCertList.signature.
166
- if (!m.fields.signatureAlgorithm.node.bytes.equals(tbsMatch.fields.signature.node.bytes)) {
162
+ if (!e.outerSignatureAlgorithmBytes.equals(e.tbsMatch.fields.signature.node.bytes)) {
167
163
  throw NS.E("crl/bad-signature-algorithm", "signatureAlgorithm must match tbsCertList.signature (RFC 5280 §5.1.1.2)");
168
164
  }
169
165
  // RFC 5280 §5.1.2.3 — the issuer MUST be a non-empty distinguished name.
@@ -183,9 +179,9 @@ var CERTIFICATE_LIST = schema.seq([
183
179
  nextUpdate: tbs.nextUpdate,
184
180
  revokedCertificates: tbs.revokedCertificates,
185
181
  crlExtensions: tbs.crlExtensions,
186
- tbsBytes: tbsMatch.node.bytes,
187
- signatureAlgorithm: m.fields.signatureAlgorithm.value.result,
188
- signatureValue: { unusedBits: m.fields.signatureValue.value.unusedBits, bytes: m.fields.signatureValue.value.bytes },
182
+ tbsBytes: e.tbsBytes,
183
+ signatureAlgorithm: e.signatureAlgorithm,
184
+ signatureValue: e.signatureValue,
189
185
  };
190
186
  },
191
187
  });
@@ -208,9 +204,7 @@ var CERTIFICATE_LIST = schema.seq([
208
204
  * var crl = pki.schema.crl.parse(der);
209
205
  * crl.revokedCertificates[0].serialNumberHex; // → "0a3f…"
210
206
  */
211
- function parse(input) {
212
- return pkix.runParse(input, { pemLabel: "X509 CRL", PemError: PemError, ErrorClass: CrlError, prefix: "crl", what: "CRL", topSchema: CERTIFICATE_LIST, ns: NS });
213
- }
207
+ var parse = pkix.makeParser({ pemLabel: "X509 CRL", PemError: PemError, ErrorClass: CrlError, prefix: "crl", what: "CRL", topSchema: CERTIFICATE_LIST, ns: NS });
214
208
 
215
209
  /**
216
210
  * @primitive pki.schema.crl.pemDecode
@@ -236,10 +230,8 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "X509 CRL
236
230
  // version or an INTEGER serial FOLLOWED by an AlgorithmIdentifier and a Name,
237
231
  // then a Validity SEQUENCE, never a bare Time at that depth.
238
232
  function matches(root) {
239
- if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE) return false;
240
- if (!root.children || root.children.length !== 3) return false;
241
- var tbs = root.children[0];
242
- if (!tbs.children || tbs.tagNumber !== TAGS.SEQUENCE) return false;
233
+ var tbs = pkix.signedEnvelopeTbs(root);
234
+ if (!tbs) return false;
243
235
  // A certificate's tbs leads with [0] EXPLICIT version — a CRL never does.
244
236
  if (tbs.children[0] && tbs.children[0].tagClass === "context") return false;
245
237
  // Walk to the thisUpdate / validity position: skip an optional bare INTEGER
@@ -0,0 +1,203 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.schema.csr
6
+ * @nav Schema
7
+ * @title CSR
8
+ * @order 130
9
+ * @slug csr
10
+ *
11
+ * @intro
12
+ * PKCS#10 certification request handling per RFC 2986. `parse` turns a DER or
13
+ * PEM CSR into a structured object: version, subject distinguished name,
14
+ * subject public-key info, the requested attributes (each with its raw values),
15
+ * and the signature over the CertificationRequestInfo. It composes the same
16
+ * schema engine and shared PKIX sub-schemas (AlgorithmIdentifier, Name,
17
+ * SubjectPublicKeyInfo) the certificate parser uses, so the request inherits the
18
+ * identical fail-closed structural rules, and the raw
19
+ * `certificationRequestInfoBytes` are returned for signature checking.
20
+ *
21
+ * A CSR is self-signed only in the sense that the requester proves possession of
22
+ * the private key; unlike a certificate or CRL there is no inner signature
23
+ * algorithm to agree with, the subject MAY be empty, and the attribute set has
24
+ * no uniqueness constraint — the parser deliberately omits those three
25
+ * certificate/CRL guards.
26
+ *
27
+ * @card
28
+ * Parse DER / PEM PKCS#10 CSRs into structured, validated fields — subject DN,
29
+ * public key, requested attributes, signature, fail-closed.
30
+ */
31
+
32
+ var asn1 = require("./asn1-der");
33
+ var schema = require("./schema-engine");
34
+ var pkix = require("./schema-pkix");
35
+ var oid = require("./oid");
36
+ var frameworkError = require("./framework-error");
37
+
38
+ var CsrError = frameworkError.CsrError;
39
+ var PemError = frameworkError.PemError;
40
+
41
+ // The csr error namespace the schema engine walks under. Shared PKIX sub-schemas
42
+ // are instantiated here under csr/* so a structural fault reports a csr code.
43
+ var NS = { prefix: "csr", E: function (code, message, cause) { return new CsrError(code, message, cause); }, oid: oid };
44
+
45
+ var NAME = pkix.name(NS);
46
+ var SPKI = pkix.spki(NS);
47
+
48
+ // CertificationRequestInfo version ::= INTEGER { v1(0) } (RFC 2986 §4.1). A BARE
49
+ // mandatory INTEGER whose ONLY legal value is 0 — the inverse of the cert/CRL
50
+ // readers, which reject 0 as the DER-forbidden default. Surface v1 as 1. A
51
+ // cert-shaped [0] EXPLICIT wrapper at this position makes asn1.read.integer throw
52
+ // asn1/* (fail-closed).
53
+ var CSR_VERSION = schema.decode(function (n) {
54
+ var v = asn1.read.integer(n);
55
+ if (v === 0n) return 1;
56
+ throw NS.E("csr/bad-version", "unsupported CSR version " + v.toString() + " (PKCS#10 requires v1)");
57
+ });
58
+
59
+ // Attribute ::= SEQUENCE { type OBJECT IDENTIFIER, values SET OF AttributeValue }.
60
+ // AttributeValue is ANY, kept as raw DER (node.bytes) — an unrecognized attribute
61
+ // type never fails the parse. values is SET SIZE (1..MAX): an empty SET is
62
+ // rejected. There is NO SET-OF uniqueness on attributes (unlike cert extensions).
63
+ var ATTRIBUTE = schema.seq([
64
+ schema.field("type", schema.oidLeaf()),
65
+ schema.field("values", schema.setOf(schema.any(), { assert: "set", min: 1, code: "csr/bad-attribute-values", what: "attribute values" })),
66
+ ], {
67
+ assert: "sequence", arity: { exact: 2 }, code: "csr/bad-attribute", what: "Attribute",
68
+ build: function (m, ctx) {
69
+ var t = m.fields.type.value;
70
+ return {
71
+ type: t,
72
+ name: ctx.oid.name(t) || null,
73
+ values: m.fields.values.value.items.map(function (it) { return it.node.bytes; }),
74
+ };
75
+ },
76
+ });
77
+
78
+ // CertificationRequestInfo ::= SEQUENCE { version, subject Name,
79
+ // subjectPKInfo SubjectPublicKeyInfo, attributes [0] IMPLICIT SET OF Attribute }.
80
+ // attributes is a REQUIRED field (a CRI omitting [0] is a missing-required-field
81
+ // fault, not silently accepted) modeled as an IMPLICIT [0] SET OF with min:0 (an
82
+ // empty attributes SET is legal). subject MAY be empty — no non-empty-DN guard.
83
+ var CERTIFICATION_REQUEST_INFO = schema.seq([
84
+ schema.field("version", CSR_VERSION),
85
+ schema.field("subject", NAME),
86
+ schema.field("subjectPKInfo", SPKI),
87
+ schema.field("attributes", schema.implicitSetOf(0, ATTRIBUTE, { min: 0, code: "csr/bad-attributes", what: "attributes" })),
88
+ ], {
89
+ assert: "sequence", code: "csr/bad-cri", what: "certificationRequestInfo",
90
+ build: function (m) {
91
+ return {
92
+ version: m.fields.version.value,
93
+ subject: m.fields.subject.value.result, // Name is a seqOf → field.value is the match; .result is the {rdns, dn} build
94
+ subjectPublicKeyInfo: m.fields.subjectPKInfo.value.result,
95
+ attributes: m.fields.attributes.value.items.map(function (it) { return it.value.result; }),
96
+ };
97
+ },
98
+ });
99
+
100
+ // CertificationRequest ::= SEQUENCE { certificationRequestInfo, signatureAlgorithm,
101
+ // signature BIT STRING } — the shared SIGNED envelope. DIVERGENCE from the
102
+ // cert/CRL builders: OMIT the outer-vs-inner signatureAlgorithm agreement check
103
+ // (the CRI has no inner signature AlgorithmIdentifier) and the non-empty-subject
104
+ // guard (a CSR subject MAY be empty). The omission is structural — this build
105
+ // simply never references the agreement bytes.
106
+ var CERTIFICATION_REQUEST = pkix.signedEnvelope(NS, CERTIFICATION_REQUEST_INFO, {
107
+ code: "csr/not-a-certification-request", what: "CertificationRequest",
108
+ build: function (e) {
109
+ var cri = e.tbsMatch.result;
110
+ return {
111
+ version: cri.version,
112
+ subject: cri.subject,
113
+ subjectPublicKeyInfo: cri.subjectPublicKeyInfo,
114
+ attributes: cri.attributes,
115
+ certificationRequestInfoBytes: e.tbsBytes,
116
+ tbsBytes: e.tbsBytes,
117
+ signatureAlgorithm: e.signatureAlgorithm,
118
+ signatureValue: e.signatureValue,
119
+ };
120
+ },
121
+ });
122
+
123
+ /**
124
+ * @primitive pki.schema.csr.parse
125
+ * @signature pki.schema.csr.parse(input) -> csr
126
+ * @since 0.1.8
127
+ * @status experimental
128
+ * @spec RFC 2986
129
+ * @related pki.schema.x509.parse, pki.schema.parse
130
+ *
131
+ * Parse a DER `Buffer` or a PEM (`CERTIFICATE REQUEST`) string into a structured
132
+ * PKCS#10 request: `{ version, subject, subjectPublicKeyInfo, attributes,
133
+ * certificationRequestInfoBytes, tbsBytes, signatureAlgorithm, signatureValue }`.
134
+ * Every field is validated on the way in; a malformed CertificationRequest /
135
+ * CertificationRequestInfo throws a typed `CsrError` (`csr/*`) and a leaf-level
136
+ * codec fault surfaces as `asn1/*`. Attribute values are returned as raw DER
137
+ * buffers so an unrecognized attribute type never fails the parse.
138
+ *
139
+ * @example
140
+ * var csr = pki.schema.csr.parse(der);
141
+ * csr.subject.dn; // → "CN=req.example"
142
+ * csr.attributes[0].type; // → "1.2.840.113549.1.9.14"
143
+ */
144
+ var parse = pkix.makeParser({ pemLabel: "CERTIFICATE REQUEST", PemError: PemError, ErrorClass: CsrError, prefix: "csr", what: "certification request", topSchema: CERTIFICATION_REQUEST, ns: NS });
145
+
146
+ /**
147
+ * @primitive pki.schema.csr.pemDecode
148
+ * @signature pki.schema.csr.pemDecode(text, label?) -> Buffer
149
+ * @since 0.1.8
150
+ * @status experimental
151
+ * @spec RFC 7468, RFC 2986
152
+ * @related pki.schema.csr.parse
153
+ *
154
+ * Extract the DER bytes from a PEM CSR block (default label `CERTIFICATE
155
+ * REQUEST`). Throws `PemError` on a missing / mismatched envelope or a non-base64
156
+ * body.
157
+ *
158
+ * @example
159
+ * var der = pki.schema.csr.pemDecode(pemText);
160
+ */
161
+ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CERTIFICATE REQUEST", PemError); }
162
+
163
+ /**
164
+ * @primitive pki.schema.csr.pemEncode
165
+ * @signature pki.schema.csr.pemEncode(der, label?) -> string
166
+ * @since 0.1.8
167
+ * @status experimental
168
+ * @spec RFC 7468
169
+ * @related pki.schema.csr.pemDecode
170
+ *
171
+ * Wrap DER bytes in a PEM CSR envelope (default label `CERTIFICATE REQUEST`).
172
+ *
173
+ * @example
174
+ * var pem = pki.schema.csr.pemEncode(der);
175
+ */
176
+ function pemEncode(der, label) { return pkix.pemEncode(der, label || "CERTIFICATE REQUEST", PemError); }
177
+
178
+ // A CertificationRequest shares the outer SEQUENCE-of-3 shape with a certificate
179
+ // and a CRL; a CSR is distinguished by its CertificationRequestInfo — a SEQUENCE
180
+ // of EXACTLY four children {version INTEGER, subject SEQUENCE, subjectPKInfo
181
+ // SEQUENCE, attributes [0]}. The mandatory trailing context-[0] attributes
182
+ // element is what a cert (whose tbs leads with [0] EXPLICIT version or an INTEGER
183
+ // serial and never ends the leading run this way) and a CRL (whose tbs reaches a
184
+ // bare Time at the validity position) never present, so the three detectors are
185
+ // mutually exclusive.
186
+ function matches(root) {
187
+ var TAGS = asn1.TAGS;
188
+ var cri = pkix.signedEnvelopeTbs(root);
189
+ if (!cri) return false;
190
+ var k = cri.children;
191
+ if (k.length !== 4) return false;
192
+ return k[0].tagClass === "universal" && k[0].tagNumber === TAGS.INTEGER &&
193
+ k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE &&
194
+ k[2].tagClass === "universal" && k[2].tagNumber === TAGS.SEQUENCE &&
195
+ k[3].tagClass === "context" && k[3].tagNumber === 0;
196
+ }
197
+
198
+ module.exports = {
199
+ parse: parse,
200
+ pemDecode: pemDecode,
201
+ pemEncode: pemEncode,
202
+ matches: matches,
203
+ };
@@ -62,6 +62,13 @@ function _assertShape(schema, node, ctx) {
62
62
  if (!node.children) {
63
63
  _fail(ctx, schema.code, (schema.what || "value") + " must be a constructed value");
64
64
  }
65
+ } else if (mode === "implicit") {
66
+ // IMPLICIT [tag] SET/SEQUENCE OF: the context tag replaces the universal
67
+ // tag, so the node is a context-class constructed [tag] and its direct
68
+ // children are the items (no inner universal SET, no EXPLICIT unwrap).
69
+ if (node.tagClass !== "context" || node.tagNumber !== schema.implicitTag || !node.children) {
70
+ _fail(ctx, schema.code, (schema.what || "value") + " must be an IMPLICIT [" + schema.implicitTag + "] SET OF");
71
+ }
65
72
  } else {
66
73
  _fail(ctx, schema.code, "unknown assert mode " + JSON.stringify(mode));
67
74
  }
@@ -163,12 +170,20 @@ function seqOf(item, opts) {
163
170
  }
164
171
  function setOf(item, opts) {
165
172
  opts = opts || {};
166
- return { kind: "repeat", item: item, assert: opts.assert || "set", code: opts.code, what: opts.what,
173
+ return { kind: "repeat", item: item, assert: opts.assert || "set", derSetOrder: true, code: opts.code, what: opts.what,
167
174
  min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
168
175
  }
169
176
  function setOfUnique(item, keyFn, opts) {
170
177
  return setOf(item, Object.assign({ unique: keyFn }, opts || {}));
171
178
  }
179
+ // [tag] IMPLICIT SET OF item — the context tag REPLACES the universal SET tag,
180
+ // so the node is a context-class constructed [tag] whose direct children are the
181
+ // items (RFC 2986 §4.1 CSR attributes). No inner SET, no EXPLICIT unwrap.
182
+ function implicitSetOf(tag, item, opts) {
183
+ opts = opts || {};
184
+ return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, derSetOrder: true, code: opts.code, what: opts.what,
185
+ min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
186
+ }
172
187
 
173
188
  // ---- the walk engine -------------------------------------------------
174
189
 
@@ -246,6 +261,19 @@ function _walkRepeat(schema, node, ctx) {
246
261
  if (schema.min != null && kids.length < schema.min) {
247
262
  _fail(ctx, schema.code, (schema.what || "value") + " must contain at least " + schema.min + " element(s)");
248
263
  }
264
+ // DER (X.690 §11.6) — the components of a SET OF appear in ascending order, the
265
+ // encodings compared as octet strings. SEQUENCE OF is order-preserving and is
266
+ // exempt (no derSetOrder). Compare full TLV byte strings, the same canonical
267
+ // order build.set emits, so a decode round-trips a build. Equal-adjacent
268
+ // encodings are permitted here (semantic uniqueness is the `unique` option's
269
+ // job); only a descending pair is non-canonical.
270
+ if (schema.derSetOrder) {
271
+ for (var s = 1; s < kids.length; s++) {
272
+ if (Buffer.compare(kids[s - 1].bytes, kids[s].bytes) > 0) {
273
+ _fail(ctx, schema.code, (schema.what || "value") + " components must be in ascending DER order (X.690 §11.6)");
274
+ }
275
+ }
276
+ }
249
277
  var items = [];
250
278
  var seen = schema.unique ? new Set() : null;
251
279
  for (var i = 0; i < kids.length; i++) {
@@ -337,7 +365,7 @@ function _consumeTrailing(fld, kids, start, fields, ctx) {
337
365
  module.exports = {
338
366
  // structural
339
367
  seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
340
- seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, choice: choice,
368
+ seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, implicitSetOf: implicitSetOf, choice: choice,
341
369
  // leaves
342
370
  oidLeaf: oidLeaf, integerLeaf: integerLeaf, boolean: boolean, octetString: octetString,
343
371
  bitString: bitString, any: any, decode: decode, time: time,
@@ -221,6 +221,83 @@ function extensions(ns) {
221
221
  });
222
222
  }
223
223
 
224
+ // SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier,
225
+ // subjectPublicKey BIT STRING } (RFC 5280 §4.1.2.7, RFC 2986 §4.1). Asserted as a
226
+ // universal SEQUENCE — a context-tagged or SET-tagged constructed node carrying
227
+ // two well-formed children is NOT a SubjectPublicKeyInfo. Shared by the
228
+ // certificate and CSR parsers.
229
+ function spki(ns) {
230
+ return schema.seq([
231
+ schema.field("algorithm", algorithmIdentifier(ns)),
232
+ schema.field("subjectPublicKey", schema.bitString()),
233
+ ], {
234
+ assert: "sequence", arity: { exact: 2 }, code: ns.prefix + "/bad-spki", what: "SubjectPublicKeyInfo",
235
+ build: function (m) {
236
+ return {
237
+ algorithm: m.fields.algorithm.value.result,
238
+ publicKey: { unusedBits: m.fields.subjectPublicKey.value.unusedBits, bytes: m.fields.subjectPublicKey.value.bytes },
239
+ bytes: m.node.bytes,
240
+ };
241
+ },
242
+ });
243
+ }
244
+
245
+ // Certificate, CertificateList, and CertificationRequest share one outer shape:
246
+ // SEQUENCE { toBeSigned SEQUENCE, signatureAlgorithm, signatureValue }. This
247
+ // returns the first element (the to-be-signed info) when `root` is that
248
+ // SEQUENCE-of-exactly-3 whose first child is itself a constructed universal
249
+ // SEQUENCE, or null otherwise. Every format's `matches` detector shares this
250
+ // preamble, so the signed-envelope shape is recognized in one place and the three
251
+ // detectors cannot drift on it (the CRL detector historically omitted the
252
+ // tbs-is-universal check this recovers).
253
+ function signedEnvelopeTbs(root) {
254
+ if (!root || root.tagClass !== "universal" || root.tagNumber !== asn1.TAGS.SEQUENCE) return null;
255
+ if (!root.children || root.children.length !== 3) return null;
256
+ var tbs = root.children[0];
257
+ if (!tbs.children || tbs.tagClass !== "universal" || tbs.tagNumber !== asn1.TAGS.SEQUENCE) return null;
258
+ return tbs;
259
+ }
260
+
261
+ // Every format's `parse` is the shared runParse bound to that format's identity
262
+ // (PEM label, error class, error-code prefix, top-level schema). This returns the
263
+ // bound parser so a format declares its configuration once and never re-writes the
264
+ // coerce -> decode -> walk wrapper. `opts`: { pemLabel, PemError, ErrorClass,
265
+ // prefix, what, topSchema, ns }.
266
+ function makeParser(opts) {
267
+ return function (input) { return runParse(input, opts); };
268
+ }
269
+
270
+ // The X.509 SIGNED{ToBeSigned} macro (RFC 5280 §4.1.1.3): the outer
271
+ // SEQUENCE { toBeSigned, signatureAlgorithm AlgorithmIdentifier,
272
+ // signatureValue BIT STRING } shared by Certificate, CertificateList and
273
+ // CertificationRequest. `tbsSchema` parses the first element; the SEQUENCE-of-3
274
+ // shape, the arity, the signature extraction and the raw tbs / outer-signature
275
+ // bytes (for the cert/CRL outer==inner agreement check) are owned here once, and
276
+ // each format's `opts.build(envelope, ctx)` shapes its own object from the
277
+ // envelope. A CSR's build simply omits the agreement check — its CRI has no inner
278
+ // signature AlgorithmIdentifier — so the omission is structural, not a copy that
279
+ // forgot a guard.
280
+ function signedEnvelope(ns, tbsSchema, opts) {
281
+ return schema.seq([
282
+ schema.field("toBeSigned", tbsSchema),
283
+ schema.field("signatureAlgorithm", algorithmIdentifier(ns)),
284
+ schema.field("signatureValue", schema.bitString()),
285
+ ], {
286
+ assert: "sequence", arity: { exact: 3 }, code: opts.code, what: opts.what,
287
+ build: function (m, ctx) {
288
+ var tbsMatch = m.fields.toBeSigned.value;
289
+ var sigBits = m.fields.signatureValue.value;
290
+ return opts.build({
291
+ tbsMatch: tbsMatch, // raw seq-match: .fields.* / .result / .node
292
+ tbsBytes: tbsMatch.node.bytes, // the exact signed region
293
+ outerSignatureAlgorithmBytes: m.fields.signatureAlgorithm.node.bytes,
294
+ signatureAlgorithm: m.fields.signatureAlgorithm.value.result,
295
+ signatureValue: { unusedBits: sigBits.unusedBits, bytes: sigBits.bytes },
296
+ }, ctx);
297
+ },
298
+ });
299
+ }
300
+
224
301
  module.exports = {
225
302
  pemDecode: pemDecode,
226
303
  pemEncode: pemEncode,
@@ -229,6 +306,10 @@ module.exports = {
229
306
  runParse: runParse,
230
307
  DN_SHORT: DN_SHORT,
231
308
  algorithmIdentifier: algorithmIdentifier,
309
+ spki: spki,
310
+ makeParser: makeParser,
311
+ signedEnvelopeTbs: signedEnvelopeTbs,
312
+ signedEnvelope: signedEnvelope,
232
313
  attrValueToString: attrValueToString,
233
314
  attributeTypeAndValue: attributeTypeAndValue,
234
315
  relativeDistinguishedName: relativeDistinguishedName,
@@ -95,20 +95,8 @@ var VALIDITY = schema.seq([
95
95
  build: function (m) { return { notBefore: m.fields.notBefore.value, notAfter: m.fields.notAfter.value }; },
96
96
  });
97
97
 
98
- // SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING }.
99
- var SPKI = schema.seq([
100
- schema.field("algorithm", ALGORITHM_IDENTIFIER),
101
- schema.field("subjectPublicKey", schema.bitString()),
102
- ], {
103
- assert: "constructed", arity: { exact: 2 }, code: "x509/bad-spki", what: "SubjectPublicKeyInfo",
104
- build: function (m) {
105
- return {
106
- algorithm: m.fields.algorithm.value.result, // algorithm field = ALGORITHM_IDENTIFIER seq-match; .value = its build
107
- publicKey: { unusedBits: m.fields.subjectPublicKey.value.unusedBits, bytes: m.fields.subjectPublicKey.value.bytes },
108
- bytes: m.node.bytes,
109
- };
110
- },
111
- });
98
+ // SubjectPublicKeyInfo the shared pkix.spki factory under the x509 NS.
99
+ var SPKI = pkix.spki(NS);
112
100
 
113
101
  var EXTENSIONS = pkix.extensions(NS);
114
102
 
@@ -151,17 +139,17 @@ var CERTIFICATE_TBS = schema.seq([
151
139
  // agreement, §4.1.2.4 non-empty issuer, §4.1.2.9 extensions-only-in-v3) after the
152
140
  // structural walk, then assembles the public parse result. Raw-byte accessors
153
141
  // (serialNumberHex, tbsBytes, sig-alg agreement) read off the match-tree nodes.
154
- var CERTIFICATE = schema.seq([
155
- schema.field("tbsCertificate", CERTIFICATE_TBS),
156
- schema.field("signatureAlgorithm", ALGORITHM_IDENTIFIER),
157
- schema.field("signatureValue", schema.bitString()),
158
- ], {
159
- assert: "sequence", arity: { exact: 3 }, code: "x509/not-a-certificate", what: "Certificate",
160
- build: function (m, ctx) {
161
- var tbs = m.fields.tbsCertificate.value; // CERTIFICATE_TBS seq-match (no build)
142
+ // Certificate ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }
143
+ // — the shared SIGNED envelope. The certificate-specific invariants (outer==inner
144
+ // signatureAlgorithm agreement, non-empty issuer, v3-only extensions) live in the
145
+ // build; the envelope owns the SEQUENCE-of-3 shape and signature extraction.
146
+ var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
147
+ code: "x509/not-a-certificate", what: "Certificate",
148
+ build: function (e, ctx) {
149
+ var tbs = e.tbsMatch; // CERTIFICATE_TBS seq-match (no build)
162
150
 
163
151
  // RFC 5280 §4.1.1.2 — outer signatureAlgorithm MUST equal tbsCertificate.signature.
164
- if (!m.fields.signatureAlgorithm.node.bytes.equals(tbs.fields.signature.node.bytes)) {
152
+ if (!e.outerSignatureAlgorithmBytes.equals(tbs.fields.signature.node.bytes)) {
165
153
  throw ctx.E("x509/bad-signature-algorithm", "signatureAlgorithm must match tbsCertificate.signature (RFC 5280 §4.1.1.2)");
166
154
  }
167
155
 
@@ -180,20 +168,19 @@ var CERTIFICATE = schema.seq([
180
168
  }
181
169
 
182
170
  var serialNode = tbs.fields.serialNumber.node;
183
- var sigBits = m.fields.signatureValue.value; // { unusedBits, bytes }
184
171
  return {
185
172
  version: version,
186
173
  serialNumber: tbs.fields.serialNumber.value,
187
174
  serialNumberHex: serialNode.content.toString("hex"),
188
- signatureAlgorithm: m.fields.signatureAlgorithm.value.result,
175
+ signatureAlgorithm: e.signatureAlgorithm,
189
176
  tbsSignatureAlgorithm: tbs.fields.signature.value.result,
190
177
  issuer: issuer,
191
178
  subject: tbs.fields.subject.value.result,
192
179
  validity: tbs.fields.validity.value.result,
193
180
  subjectPublicKeyInfo: tbs.fields.subjectPublicKeyInfo.value.result,
194
181
  extensions: hasExtensions ? extField.value.result : [],
195
- tbsBytes: tbs.node.bytes,
196
- signatureValue: { unusedBits: sigBits.unusedBits, bytes: sigBits.bytes },
182
+ tbsBytes: e.tbsBytes,
183
+ signatureValue: e.signatureValue,
197
184
  };
198
185
  },
199
186
  });
@@ -226,9 +213,7 @@ var CERTIFICATE = schema.seq([
226
213
  * cert.validity.notAfter; // Date
227
214
  * cert.signatureAlgorithm.name; // "sha256WithRSAEncryption"
228
215
  */
229
- function parse(input) {
230
- return pkix.runParse(input, { pemLabel: "CERTIFICATE", PemError: PemError, ErrorClass: CertificateError, prefix: "x509", what: "certificate", topSchema: CERTIFICATE, ns: NS });
231
- }
216
+ var parse = pkix.makeParser({ pemLabel: "CERTIFICATE", PemError: PemError, ErrorClass: CertificateError, prefix: "x509", what: "certificate", topSchema: CERTIFICATE, ns: NS });
232
217
 
233
218
  // matches(root): does the decoded DER look like a Certificate? A CSR and a CRL
234
219
  // share the outer SEQUENCE-of-3 envelope, so the discriminator is inside the
@@ -238,10 +223,8 @@ function parse(input) {
238
223
  // and a CRL leads with a bare Time; neither matches. Used by the orchestrator.
239
224
  function matches(root) {
240
225
  var TAGS = asn1.TAGS;
241
- if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE) return false;
242
- if (!root.children || root.children.length !== 3) return false;
243
- var tbs = root.children[0];
244
- if (!tbs.children || tbs.tagClass !== "universal" || tbs.tagNumber !== TAGS.SEQUENCE) return false;
226
+ var tbs = pkix.signedEnvelopeTbs(root);
227
+ if (!tbs) return false;
245
228
  var kids = tbs.children;
246
229
  var i = (kids[0] && kids[0].tagClass === "context" && kids[0].tagNumber === 0) ? 1 : 0; // optional [0] version
247
230
  var validity = kids[i + 3]; // serial(i), signature(i+1), issuer(i+2), validity(i+3)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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:a50e8ea1-a357-4e08-b473-56bcf5e2659f",
5
+ "serialNumber": "urn:uuid:64eeff87-502c-43f1-a398-5fbe7fc7a614",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-05T03:52:05.348Z",
8
+ "timestamp": "2026-07-05T06:26:48.420Z",
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.7",
22
+ "bom-ref": "@blamejs/pki@0.1.8",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.7",
25
+ "version": "0.1.8",
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.7",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.8",
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.7",
57
+ "ref": "@blamejs/pki@0.1.8",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]