@blamejs/pki 0.1.7 → 0.1.9

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,40 @@ 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.9 — 2026-07-05
8
+
9
+ A PKCS#8 private-key parser joins the pki.schema family.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.pkcs8.parse — a PKCS#8 PrivateKeyInfo / OneAsymmetricKey parser per RFC 5208 §5 and RFC 5958 §2. It turns a DER Buffer or a 'PRIVATE KEY' PEM string into { version, privateKeyAlgorithm, privateKey, attributes, publicKey }, where privateKey is the raw OCTET STRING content (the inner RSA/EC/curve key, decoded by the caller via privateKeyAlgorithm.oid) and publicKey is present only for a v2 key. The version must be v1 (0) or v2 (1), and a [1] public key is permitted only in a v2 key (both directions enforced). A malformed key throws a typed Pkcs8Error (pkcs8/*); a leaf-level codec fault surfaces as asn1/*. pki.schema.pkcs8.pemDecode / pemEncode handle the PEM envelope.
14
+ - pki.schema.pkcs8.parseEncrypted — recognizes an EncryptedPrivateKeyInfo ('ENCRYPTED PRIVATE KEY') and surfaces its encryptionAlgorithm and raw encryptedData. Decryption (PBES2/PBKDF2 + a passphrase) is a separate concern and is not performed here. This is an explicit call — an EncryptedPrivateKeyInfo shares its SEQUENCE{SEQUENCE, OCTET STRING} shape with a PKCS#1 DigestInfo, so pki.schema.parse does not auto-route it (structure alone cannot classify it without a validated encryption-algorithm discriminator).
15
+ - pki.asn1.read.enumerated's sibling pki.asn1.read.bitStringImplicit and the pki.schema.engine.implicitBitString(tag) leaf — read a context-tagged IMPLICIT BIT STRING (the shape a PKCS#8 OneAsymmetricKey public key [1] takes).
16
+
17
+ ## v0.1.8 — 2026-07-04
18
+
19
+ A PKCS#10 certification-request parser joins the pki.schema family.
20
+
21
+ ### Added
22
+
23
+ - 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.
24
+ - 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.
25
+
26
+ ### Changed
27
+
28
+ - 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.
29
+
30
+ ### Security
31
+
32
+ - 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.
33
+ - 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.
34
+ - 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.
35
+
36
+ ### Migration
37
+
38
+ - Replace C.TIME.ms(n) with C.TIME.milliseconds(n). The other C.TIME and C.BYTES helpers are unchanged.
39
+
40
+ ## v0.1.7 — 2026-07-04
8
41
 
9
42
  A unified pki.schema family: the structure-schema engine, the X.509 parser, a new CRL parser, and a detect-and-route orchestrator.
10
43
 
package/README.md CHANGED
@@ -193,9 +193,11 @@ 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` |
197
+ | `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
196
198
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
197
199
  | `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 |
200
+ | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error`, each carrying a stable `code` in `domain/reason` form |
199
201
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
200
202
 
201
203
  ### CLI
@@ -209,9 +211,9 @@ pki parse cert.pem # structured JSON summary of a certific
209
211
 
210
212
  ### What's coming
211
213
 
212
- Certificate build/sign/verify, CRLs, CSRs (PKCS#10), certification-path
214
+ Certificate build/sign/verify, certification-path
213
215
  validation, CMS (SignedData / EnvelopedData / EncryptedData / AuthenticatedData),
214
- OCSP, RFC 3161 timestamping, PKCS#8 / SPKI / PBES2 / PKCS#12, and the
216
+ OCSP, RFC 3161 timestamping, PKCS#8 decryption (PBES2) / SPKI / PKCS#12, and the
215
217
  post-quantum certificate and CMS surface (ML-DSA / ML-KEM / SLH-DSA and hybrid
216
218
  composites) are on the roadmap and ride this same core. See
217
219
  [ROADMAP.md](ROADMAP.md) for the full plan and current status of each area, and
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,20 +290,39 @@ 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
- function readBitString(node) {
305
- _expectUniversal(node, TAGS.BIT_STRING, "readBitString");
306
- _expectPrimitive(node, "readBitString");
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
+
321
+ // The BIT STRING content decoder (the leading unused-bit octet + body, with the
322
+ // DER zero-pad rule). Shared by the universal reader and the IMPLICIT
323
+ // context-tagged reader — the caller asserts the tag first.
324
+ function _readBitStringContent(node, who) {
325
+ _expectPrimitive(node, who);
307
326
  var c = node.content;
308
327
  if (c.length === 0) throw new Asn1Error("asn1/bad-bit-string", "BIT STRING must have >= 1 content octet");
309
328
  var unusedBits = c[0];
@@ -318,6 +337,23 @@ function readBitString(node) {
318
337
  return { unusedBits: unusedBits, bytes: c.subarray(1) };
319
338
  }
320
339
 
340
+ function readBitString(node) {
341
+ _expectUniversal(node, TAGS.BIT_STRING, "readBitString");
342
+ return _readBitStringContent(node, "readBitString");
343
+ }
344
+
345
+ // Read a [tag] IMPLICIT BIT STRING — a context-class PRIMITIVE node whose content
346
+ // is a bit-string body (identical content rules to a universal BIT STRING). The
347
+ // IMPLICIT tag replaces the universal one, so there is no inner universal node.
348
+ // Used for the PKCS#8 OneAsymmetricKey publicKey [1] (RFC 5958 §2).
349
+ function readBitStringImplicit(node, tag) {
350
+ if (node.tagClass !== "context" || node.tagNumber !== tag) {
351
+ throw new Asn1Error("asn1/unexpected-tag", "readBitStringImplicit: expected context tag [" + tag +
352
+ "], got " + node.tagClass + "/" + node.tagNumber);
353
+ }
354
+ return _readBitStringContent(node, "readBitStringImplicit");
355
+ }
356
+
321
357
  function readOctetString(node) {
322
358
  _expectUniversal(node, TAGS.OCTET_STRING, "readOctetString");
323
359
  _expectPrimitive(node, "readOctetString");
@@ -754,7 +790,9 @@ module.exports = {
754
790
  read: {
755
791
  boolean: readBoolean,
756
792
  integer: readInteger,
793
+ enumerated: readEnumerated,
757
794
  bitString: readBitString,
795
+ bitStringImplicit: readBitStringImplicit,
758
796
  octetString: readOctetString,
759
797
  nullValue: readNull,
760
798
  oid: readOid,
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,14 @@ 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
+
136
+ // Pkcs8Error — a byte sequence that is not a well-formed PKCS#8 PrivateKeyInfo /
137
+ // OneAsymmetricKey or EncryptedPrivateKeyInfo (RFC 5208 §5, RFC 5958 §2/§3).
138
+ var Pkcs8Error = defineClass("Pkcs8Error", { withCause: true });
139
+
132
140
  module.exports = {
133
141
  PkiError: PkiError,
134
142
  defineClass: defineClass,
@@ -139,4 +147,6 @@ module.exports = {
139
147
  CertificateError: CertificateError,
140
148
  CrlError: CrlError,
141
149
  SchemaError: SchemaError,
150
+ CsrError: CsrError,
151
+ Pkcs8Error: Pkcs8Error,
142
152
  };
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,8 @@ 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");
38
+ var pkcs8 = require("./schema-pkcs8");
36
39
  var frameworkError = require("./framework-error");
37
40
 
38
41
  var SchemaError = frameworkError.SchemaError;
@@ -49,11 +52,34 @@ var ENTRY = { pemLabel: null, PemError: PemError, ErrorClass: SchemaError, prefi
49
52
  // logic. Order matters: the most specific detector wins, so a new member is
50
53
  // inserted ahead of any more-permissive one.
51
54
  var FORMATS = [
55
+ {
56
+ // PKCS#8 PrivateKeyInfo / OneAsymmetricKey — SEQUENCE whose first child is an
57
+ // INTEGER (version) and third an OCTET STRING (privateKey); disjoint from the
58
+ // signed-envelope trio. (EncryptedPrivateKeyInfo is deliberately NOT
59
+ // auto-routed: its SEQUENCE{SEQUENCE, OCTET STRING} shape is ambiguous — a
60
+ // PKCS#1 DigestInfo is identical — so structural detection cannot classify it
61
+ // without a validated encryption-algorithm discriminator, which arrives with
62
+ // the PBES layer. It is reached explicitly via pki.schema.pkcs8.parseEncrypted.)
63
+ name: "pkcs8",
64
+ module: pkcs8,
65
+ detect: pkcs8.matches,
66
+ parse: function (input) { return pkcs8.parse(input); },
67
+ },
68
+ {
69
+ // CertificationRequest ::= SEQUENCE { certificationRequestInfo,
70
+ // signatureAlgorithm, signature } — the same outer 3-element shape,
71
+ // distinguished by a CertificationRequestInfo of EXACTLY four children
72
+ // ending in the IMPLICIT [0] attributes element. Checked first because that
73
+ // detector is the most specific and mutually exclusive with the others.
74
+ name: "csr",
75
+ module: csr,
76
+ detect: csr.matches,
77
+ parse: function (input) { return csr.parse(input); },
78
+ },
52
79
  {
53
80
  // CertificateList ::= SEQUENCE { tbsCertList, signatureAlgorithm,
54
81
  // signatureValue } — the same outer shape as a certificate, distinguished
55
82
  // by its tbsCertList (a bare Time at the certificate's Validity position).
56
- // Checked first because its detector is the specific one.
57
83
  name: "crl",
58
84
  module: crl,
59
85
  detect: crl.matches,
@@ -81,7 +107,7 @@ var FORMATS = [
81
107
  * The names of every registered format, in detection order.
82
108
  *
83
109
  * @example
84
- * pki.schema.all(); // → ["crl", "x509"]
110
+ * pki.schema.all(); // → ["pkcs8", "csr", "crl", "x509"]
85
111
  */
86
112
  function all() { return FORMATS.map(function (f) { return f.name; }); }
87
113
 
@@ -121,6 +147,8 @@ module.exports = {
121
147
  engine: engine,
122
148
  x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode },
123
149
  crl: { parse: crl.parse, pemDecode: crl.pemDecode },
150
+ csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
151
+ pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
124
152
  all: all,
125
153
  parse: parse,
126
154
  };
package/lib/schema-crl.js CHANGED
@@ -46,7 +46,7 @@ var OID_INVALIDITY_DATE = oid.byName("invalidityDate");
46
46
 
47
47
  // The crl error namespace the schema engine walks under. Shared PKIX sub-schemas
48
48
  // are instantiated here under crl/* so a structural fault reports a crl code.
49
- var NS = { prefix: "crl", E: function (code, message, cause) { return new CrlError(code, message, cause); }, oid: oid };
49
+ var NS = pkix.makeNS("crl", CrlError, oid);
50
50
 
51
51
  var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
52
52
  var NAME = pkix.name(NS);
@@ -56,12 +56,7 @@ var TIME = schema.time(NS);
56
56
  // CRL Version ::= INTEGER { v1(0), v2(1) } — a BARE INTEGER (not [0] EXPLICIT).
57
57
  // A CRL is at most v2; reject an explicit v1 (DER forbids the default) and any
58
58
  // value >= 2. Do NOT reuse the certificate readVersion (it maps 2 -> v3).
59
- var CRL_VERSION = schema.decode(function (n) {
60
- var v = asn1.read.integer(n);
61
- if (v === 1n) return 2;
62
- if (v === 0n) throw NS.E("crl/bad-version", "DER forbids explicitly encoding the default version v1");
63
- throw NS.E("crl/bad-version", "unsupported CRL version " + v.toString() + " (a CRL is at most v2)");
64
- });
59
+ var CRL_VERSION = pkix.versionReader(NS, { "1": 2 });
65
60
 
66
61
  // The three cheap, high-value CRL extension values are decoded from their raw
67
62
  // extnValue octets (RFC 5280 §5.2/§5.3); GeneralNames-based extensions
@@ -74,11 +69,9 @@ function decodeExt(ext) {
74
69
  value = asn1.read.integer(asn1.decode(ext.value));
75
70
  if (value < 0n) throw new Error("cRLNumber must be non-negative (INTEGER 0..MAX)");
76
71
  } 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);
72
+ // read.enumerated asserts the ENUMERATED tag (a bare INTEGER is rejected)
73
+ // the codec owns the tag check, so this reader need not repeat it.
74
+ var reason = asn1.read.enumerated(asn1.decode(ext.value));
82
75
  if (CRL_REASONS.indexOf(reason) === -1) throw new Error("undefined CRLReason " + reason.toString());
83
76
  value = Number(reason);
84
77
  } else if (ext.oid === OID_INVALIDITY_DATE) { // invalidityDate ::= GeneralizedTime
@@ -153,17 +146,15 @@ var TBS_CERTLIST = schema.seq([
153
146
  });
154
147
 
155
148
  // 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;
149
+ // the shared SIGNED envelope. The CRL-specific invariants (outer==inner
150
+ // signatureAlgorithm agreement, non-empty issuer, v2-only extensions) live in the
151
+ // build; the envelope owns the SEQUENCE-of-3 shape and signature extraction.
152
+ var CERTIFICATE_LIST = pkix.signedEnvelope(NS, TBS_CERTLIST, {
153
+ code: "crl/not-a-crl", what: "CertificateList",
154
+ build: function (e) {
155
+ var tbs = e.tbsMatch.result;
165
156
  // 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)) {
157
+ if (!e.outerSignatureAlgorithmBytes.equals(e.tbsMatch.fields.signature.node.bytes)) {
167
158
  throw NS.E("crl/bad-signature-algorithm", "signatureAlgorithm must match tbsCertList.signature (RFC 5280 §5.1.1.2)");
168
159
  }
169
160
  // RFC 5280 §5.1.2.3 — the issuer MUST be a non-empty distinguished name.
@@ -183,9 +174,9 @@ var CERTIFICATE_LIST = schema.seq([
183
174
  nextUpdate: tbs.nextUpdate,
184
175
  revokedCertificates: tbs.revokedCertificates,
185
176
  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 },
177
+ tbsBytes: e.tbsBytes,
178
+ signatureAlgorithm: e.signatureAlgorithm,
179
+ signatureValue: e.signatureValue,
189
180
  };
190
181
  },
191
182
  });
@@ -208,9 +199,7 @@ var CERTIFICATE_LIST = schema.seq([
208
199
  * var crl = pki.schema.crl.parse(der);
209
200
  * crl.revokedCertificates[0].serialNumberHex; // → "0a3f…"
210
201
  */
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
- }
202
+ var parse = pkix.makeParser({ pemLabel: "X509 CRL", PemError: PemError, ErrorClass: CrlError, prefix: "crl", what: "CRL", topSchema: CERTIFICATE_LIST, ns: NS });
214
203
 
215
204
  /**
216
205
  * @primitive pki.schema.crl.pemDecode
@@ -236,10 +225,8 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "X509 CRL
236
225
  // version or an INTEGER serial FOLLOWED by an AlgorithmIdentifier and a Name,
237
226
  // then a Validity SEQUENCE, never a bare Time at that depth.
238
227
  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;
228
+ var tbs = pkix.signedEnvelopeTbs(root);
229
+ if (!tbs) return false;
243
230
  // A certificate's tbs leads with [0] EXPLICIT version — a CRL never does.
244
231
  if (tbs.children[0] && tbs.children[0].tagClass === "context") return false;
245
232
  // Walk to the thisUpdate / validity position: skip an optional bare INTEGER
@@ -0,0 +1,185 @@
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 = pkix.makeNS("csr", CsrError, 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 = pkix.versionReader(NS, { "0": 1 });
54
+
55
+ // Attribute ::= SEQUENCE { type OID, values SET OF ANY } — the shared pkix factory
56
+ // under the csr NS (values raw DER, SET SIZE(1..MAX), no uniqueness). The same
57
+ // factory serves the PKCS#8 private-key attributes.
58
+ var ATTRIBUTE = pkix.attribute(NS);
59
+
60
+ // CertificationRequestInfo ::= SEQUENCE { version, subject Name,
61
+ // subjectPKInfo SubjectPublicKeyInfo, attributes [0] IMPLICIT SET OF Attribute }.
62
+ // attributes is a REQUIRED field (a CRI omitting [0] is a missing-required-field
63
+ // fault, not silently accepted) modeled as an IMPLICIT [0] SET OF with min:0 (an
64
+ // empty attributes SET is legal). subject MAY be empty — no non-empty-DN guard.
65
+ var CERTIFICATION_REQUEST_INFO = schema.seq([
66
+ schema.field("version", CSR_VERSION),
67
+ schema.field("subject", NAME),
68
+ schema.field("subjectPKInfo", SPKI),
69
+ schema.field("attributes", schema.implicitSetOf(0, ATTRIBUTE, { min: 0, code: "csr/bad-attributes", what: "attributes" })),
70
+ ], {
71
+ assert: "sequence", code: "csr/bad-cri", what: "certificationRequestInfo",
72
+ build: function (m) {
73
+ return {
74
+ version: m.fields.version.value,
75
+ subject: m.fields.subject.value.result, // Name is a seqOf → field.value is the match; .result is the {rdns, dn} build
76
+ subjectPublicKeyInfo: m.fields.subjectPKInfo.value.result,
77
+ attributes: m.fields.attributes.value.items.map(function (it) { return it.value.result; }),
78
+ };
79
+ },
80
+ });
81
+
82
+ // CertificationRequest ::= SEQUENCE { certificationRequestInfo, signatureAlgorithm,
83
+ // signature BIT STRING } — the shared SIGNED envelope. DIVERGENCE from the
84
+ // cert/CRL builders: OMIT the outer-vs-inner signatureAlgorithm agreement check
85
+ // (the CRI has no inner signature AlgorithmIdentifier) and the non-empty-subject
86
+ // guard (a CSR subject MAY be empty). The omission is structural — this build
87
+ // simply never references the agreement bytes.
88
+ var CERTIFICATION_REQUEST = pkix.signedEnvelope(NS, CERTIFICATION_REQUEST_INFO, {
89
+ code: "csr/not-a-certification-request", what: "CertificationRequest",
90
+ build: function (e) {
91
+ var cri = e.tbsMatch.result;
92
+ return {
93
+ version: cri.version,
94
+ subject: cri.subject,
95
+ subjectPublicKeyInfo: cri.subjectPublicKeyInfo,
96
+ attributes: cri.attributes,
97
+ certificationRequestInfoBytes: e.tbsBytes,
98
+ tbsBytes: e.tbsBytes,
99
+ signatureAlgorithm: e.signatureAlgorithm,
100
+ signatureValue: e.signatureValue,
101
+ };
102
+ },
103
+ });
104
+
105
+ /**
106
+ * @primitive pki.schema.csr.parse
107
+ * @signature pki.schema.csr.parse(input) -> csr
108
+ * @since 0.1.8
109
+ * @status experimental
110
+ * @spec RFC 2986
111
+ * @related pki.schema.x509.parse, pki.schema.parse
112
+ *
113
+ * Parse a DER `Buffer` or a PEM (`CERTIFICATE REQUEST`) string into a structured
114
+ * PKCS#10 request: `{ version, subject, subjectPublicKeyInfo, attributes,
115
+ * certificationRequestInfoBytes, tbsBytes, signatureAlgorithm, signatureValue }`.
116
+ * Every field is validated on the way in; a malformed CertificationRequest /
117
+ * CertificationRequestInfo throws a typed `CsrError` (`csr/*`) and a leaf-level
118
+ * codec fault surfaces as `asn1/*`. Attribute values are returned as raw DER
119
+ * buffers so an unrecognized attribute type never fails the parse.
120
+ *
121
+ * @example
122
+ * var csr = pki.schema.csr.parse(der);
123
+ * csr.subject.dn; // → "CN=req.example"
124
+ * csr.attributes[0].type; // → "1.2.840.113549.1.9.14"
125
+ */
126
+ var parse = pkix.makeParser({ pemLabel: "CERTIFICATE REQUEST", PemError: PemError, ErrorClass: CsrError, prefix: "csr", what: "certification request", topSchema: CERTIFICATION_REQUEST, ns: NS });
127
+
128
+ /**
129
+ * @primitive pki.schema.csr.pemDecode
130
+ * @signature pki.schema.csr.pemDecode(text, label?) -> Buffer
131
+ * @since 0.1.8
132
+ * @status experimental
133
+ * @spec RFC 7468, RFC 2986
134
+ * @related pki.schema.csr.parse
135
+ *
136
+ * Extract the DER bytes from a PEM CSR block (default label `CERTIFICATE
137
+ * REQUEST`). Throws `PemError` on a missing / mismatched envelope or a non-base64
138
+ * body.
139
+ *
140
+ * @example
141
+ * var der = pki.schema.csr.pemDecode(pemText);
142
+ */
143
+ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CERTIFICATE REQUEST", PemError); }
144
+
145
+ /**
146
+ * @primitive pki.schema.csr.pemEncode
147
+ * @signature pki.schema.csr.pemEncode(der, label?) -> string
148
+ * @since 0.1.8
149
+ * @status experimental
150
+ * @spec RFC 7468
151
+ * @related pki.schema.csr.pemDecode
152
+ *
153
+ * Wrap DER bytes in a PEM CSR envelope (default label `CERTIFICATE REQUEST`).
154
+ *
155
+ * @example
156
+ * var pem = pki.schema.csr.pemEncode(der);
157
+ */
158
+ function pemEncode(der, label) { return pkix.pemEncode(der, label || "CERTIFICATE REQUEST", PemError); }
159
+
160
+ // A CertificationRequest shares the outer SEQUENCE-of-3 shape with a certificate
161
+ // and a CRL; a CSR is distinguished by its CertificationRequestInfo — a SEQUENCE
162
+ // of EXACTLY four children {version INTEGER, subject SEQUENCE, subjectPKInfo
163
+ // SEQUENCE, attributes [0]}. The mandatory trailing context-[0] attributes
164
+ // element is what a cert (whose tbs leads with [0] EXPLICIT version or an INTEGER
165
+ // serial and never ends the leading run this way) and a CRL (whose tbs reaches a
166
+ // bare Time at the validity position) never present, so the three detectors are
167
+ // mutually exclusive.
168
+ function matches(root) {
169
+ var TAGS = asn1.TAGS;
170
+ var cri = pkix.signedEnvelopeTbs(root);
171
+ if (!cri) return false;
172
+ var k = cri.children;
173
+ if (k.length !== 4) return false;
174
+ return k[0].tagClass === "universal" && k[0].tagNumber === TAGS.INTEGER &&
175
+ k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE &&
176
+ k[2].tagClass === "universal" && k[2].tagNumber === TAGS.SEQUENCE &&
177
+ k[3].tagClass === "context" && k[3].tagNumber === 0;
178
+ }
179
+
180
+ module.exports = {
181
+ parse: parse,
182
+ pemDecode: pemDecode,
183
+ pemEncode: pemEncode,
184
+ matches: matches,
185
+ };
@@ -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
  }
@@ -90,6 +97,9 @@ function integerLeaf() { return { kind: "leaf", read: asn1.read.integer }; }
90
97
  function boolean() { return { kind: "leaf", read: asn1.read.boolean }; }
91
98
  function octetString() { return { kind: "leaf", read: asn1.read.octetString }; }
92
99
  function bitString() { return { kind: "leaf", read: function (n) { var b = asn1.read.bitString(n); return { unusedBits: b.unusedBits, bytes: b.bytes }; } }; }
100
+ // A [tag] IMPLICIT BIT STRING leaf (context-class primitive) — the primitive-leaf
101
+ // counterpart to implicitSetOf, for e.g. the PKCS#8 publicKey [1] (RFC 5958 §2).
102
+ function implicitBitString(tag) { return { kind: "leaf", read: function (n) { var b = asn1.read.bitStringImplicit(n, tag); return { unusedBits: b.unusedBits, bytes: b.bytes }; } }; }
93
103
  function any() { return { kind: "any" }; }
94
104
  function decode(fn) { return { kind: "decode", fn: fn }; }
95
105
 
@@ -163,12 +173,20 @@ function seqOf(item, opts) {
163
173
  }
164
174
  function setOf(item, opts) {
165
175
  opts = opts || {};
166
- return { kind: "repeat", item: item, assert: opts.assert || "set", code: opts.code, what: opts.what,
176
+ return { kind: "repeat", item: item, assert: opts.assert || "set", derSetOrder: true, code: opts.code, what: opts.what,
167
177
  min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
168
178
  }
169
179
  function setOfUnique(item, keyFn, opts) {
170
180
  return setOf(item, Object.assign({ unique: keyFn }, opts || {}));
171
181
  }
182
+ // [tag] IMPLICIT SET OF item — the context tag REPLACES the universal SET tag,
183
+ // so the node is a context-class constructed [tag] whose direct children are the
184
+ // items (RFC 2986 §4.1 CSR attributes). No inner SET, no EXPLICIT unwrap.
185
+ function implicitSetOf(tag, item, opts) {
186
+ opts = opts || {};
187
+ return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, derSetOrder: true, code: opts.code, what: opts.what,
188
+ min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
189
+ }
172
190
 
173
191
  // ---- the walk engine -------------------------------------------------
174
192
 
@@ -246,6 +264,19 @@ function _walkRepeat(schema, node, ctx) {
246
264
  if (schema.min != null && kids.length < schema.min) {
247
265
  _fail(ctx, schema.code, (schema.what || "value") + " must contain at least " + schema.min + " element(s)");
248
266
  }
267
+ // DER (X.690 §11.6) — the components of a SET OF appear in ascending order, the
268
+ // encodings compared as octet strings. SEQUENCE OF is order-preserving and is
269
+ // exempt (no derSetOrder). Compare full TLV byte strings, the same canonical
270
+ // order build.set emits, so a decode round-trips a build. Equal-adjacent
271
+ // encodings are permitted here (semantic uniqueness is the `unique` option's
272
+ // job); only a descending pair is non-canonical.
273
+ if (schema.derSetOrder) {
274
+ for (var s = 1; s < kids.length; s++) {
275
+ if (Buffer.compare(kids[s - 1].bytes, kids[s].bytes) > 0) {
276
+ _fail(ctx, schema.code, (schema.what || "value") + " components must be in ascending DER order (X.690 §11.6)");
277
+ }
278
+ }
279
+ }
249
280
  var items = [];
250
281
  var seen = schema.unique ? new Set() : null;
251
282
  for (var i = 0; i < kids.length; i++) {
@@ -337,10 +368,10 @@ function _consumeTrailing(fld, kids, start, fields, ctx) {
337
368
  module.exports = {
338
369
  // structural
339
370
  seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
340
- seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, choice: choice,
371
+ seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, implicitSetOf: implicitSetOf, choice: choice,
341
372
  // leaves
342
373
  oidLeaf: oidLeaf, integerLeaf: integerLeaf, boolean: boolean, octetString: octetString,
343
- bitString: bitString, any: any, decode: decode, time: time,
374
+ bitString: bitString, implicitBitString: implicitBitString, any: any, decode: decode, time: time,
344
375
  // engine
345
376
  walk: walk,
346
377
  };
@@ -0,0 +1,211 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.schema.pkcs8
6
+ * @nav Schema
7
+ * @title PKCS#8
8
+ * @order 140
9
+ * @slug pkcs8
10
+ *
11
+ * @intro
12
+ * PKCS#8 private-key handling per RFC 5208 §5 (PrivateKeyInfo) and RFC 5958 §2
13
+ * (OneAsymmetricKey). `parse` turns a DER or PEM (`PRIVATE KEY`) key into a
14
+ * structured object: version, the private-key algorithm identifier, the raw
15
+ * private-key bytes, the optional attributes, and — for a v2 OneAsymmetricKey —
16
+ * the optional public key. It composes the same schema engine and shared PKIX
17
+ * sub-schemas (AlgorithmIdentifier, Attribute) the other parsers use.
18
+ *
19
+ * A PKCS#8 key is a container, not a signed structure: it has no signature, no
20
+ * distinguished name, and no to-be-signed region. The private-key OCTET STRING
21
+ * content is kept raw — the algorithm-specific inner key (an RSAPrivateKey, an
22
+ * ECPrivateKey, a CurvePrivateKey) is decoded by the caller using the surfaced
23
+ * algorithm OID, so an unknown or future key type never fails the parse. An
24
+ * `ENCRYPTED PRIVATE KEY` (EncryptedPrivateKeyInfo, RFC 5958 §3) is recognized
25
+ * and surfaced with its encryption algorithm and raw ciphertext; decrypting it
26
+ * needs a passphrase and is out of scope for structural parsing.
27
+ *
28
+ * @card
29
+ * Parse DER / PEM PKCS#8 private keys (RFC 5208 / 5958) into structured,
30
+ * validated fields — algorithm, raw key bytes, attributes, optional public key,
31
+ * fail-closed. Encrypted keys are recognized, not decrypted.
32
+ */
33
+
34
+ var asn1 = require("./asn1-der");
35
+ var schema = require("./schema-engine");
36
+ var pkix = require("./schema-pkix");
37
+ var oid = require("./oid");
38
+ var frameworkError = require("./framework-error");
39
+
40
+ var Pkcs8Error = frameworkError.Pkcs8Error;
41
+ var PemError = frameworkError.PemError;
42
+
43
+ // The pkcs8 error namespace the schema engine walks under.
44
+ var NS = pkix.makeNS("pkcs8", Pkcs8Error, oid);
45
+
46
+ var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
47
+ var ATTRIBUTE = pkix.attribute(NS);
48
+
49
+ // version ::= INTEGER { v1(0), v2(1) } — 0 and 1 are both LEGAL (the divergence
50
+ // from every other reader; cert/CRL reject 0, CSR accepts only 0). Surface as the
51
+ // RFC's vN number.
52
+ var PKCS8_VERSION = pkix.versionReader(NS, { "0": 1, "1": 2 });
53
+
54
+ // PrivateKeyInfo ::= SEQUENCE { version, privateKeyAlgorithm AlgorithmIdentifier,
55
+ // privateKey OCTET STRING, attributes [0] IMPLICIT SET OF Attribute OPTIONAL,
56
+ // publicKey [1] IMPLICIT BIT STRING OPTIONAL (v2 only) }. The two trailing
57
+ // context fields are modeled with `trailing` so they must appear in ascending tag
58
+ // order and an unknown trailing context tag is rejected (not bled into a field).
59
+ var PRIVATE_KEY_INFO = schema.seq([
60
+ schema.field("version", PKCS8_VERSION),
61
+ schema.field("privateKeyAlgorithm", ALGORITHM_IDENTIFIER),
62
+ schema.field("privateKey", schema.octetString()),
63
+ schema.trailing([
64
+ { tag: 0, name: "attributes", schema: schema.implicitSetOf(0, ATTRIBUTE, { min: 0, code: "pkcs8/bad-attributes", what: "attributes" }) },
65
+ { tag: 1, name: "publicKey", schema: schema.implicitBitString(1) },
66
+ ], { minTag: 0, maxTag: 1, unexpectedCode: "pkcs8/not-a-private-key-info", orderCode: "pkcs8/bad-order" }),
67
+ ], {
68
+ assert: "sequence", arity: { min: 3 }, code: "pkcs8/not-a-private-key-info", what: "PrivateKeyInfo",
69
+ build: function (m) {
70
+ var version = m.fields.version.value; // 1 (v1) | 2 (v2)
71
+ var hasPublicKey = m.fields.publicKey.present;
72
+ // RFC 5958 §2 — publicKey present <=> version is v2. Enforce both directions.
73
+ if (hasPublicKey && version !== 2) throw NS.E("pkcs8/bad-version", "a [1] publicKey is permitted only in a v2 OneAsymmetricKey");
74
+ if (!hasPublicKey && version === 2) throw NS.E("pkcs8/bad-version", "a v2 OneAsymmetricKey must carry a [1] publicKey");
75
+ return {
76
+ version: version,
77
+ privateKeyAlgorithm: m.fields.privateKeyAlgorithm.value.result,
78
+ privateKey: m.fields.privateKey.value, // raw OCTET STRING content (the inner key DER)
79
+ attributes: m.fields.attributes.present ? m.fields.attributes.value.items.map(function (it) { return it.value.result; }) : [],
80
+ publicKey: hasPublicKey ? m.fields.publicKey.value : null,
81
+ };
82
+ },
83
+ });
84
+
85
+ // EncryptedPrivateKeyInfo ::= SEQUENCE { encryptionAlgorithm AlgorithmIdentifier,
86
+ // encryptedData OCTET STRING } (RFC 5958 §3). Recognized and surfaced; the raw
87
+ // ciphertext is kept for a later decryption layer (PBES2/PBKDF2 + a passphrase).
88
+ var ENCRYPTED_PRIVATE_KEY_INFO = schema.seq([
89
+ schema.field("encryptionAlgorithm", ALGORITHM_IDENTIFIER),
90
+ schema.field("encryptedData", schema.octetString()),
91
+ ], {
92
+ assert: "sequence", arity: { exact: 2 }, code: "pkcs8/not-an-encrypted-private-key-info", what: "EncryptedPrivateKeyInfo",
93
+ build: function (m) {
94
+ return {
95
+ encryptionAlgorithm: m.fields.encryptionAlgorithm.value.result,
96
+ encryptedData: m.fields.encryptedData.value, // raw ciphertext
97
+ };
98
+ },
99
+ });
100
+
101
+ /**
102
+ * @primitive pki.schema.pkcs8.parse
103
+ * @signature pki.schema.pkcs8.parse(input) -> privateKey
104
+ * @since 0.1.9
105
+ * @status experimental
106
+ * @spec RFC 5208, RFC 5958
107
+ * @related pki.schema.parse, pki.schema.x509.parse
108
+ *
109
+ * Parse a DER `Buffer` or a PEM (`PRIVATE KEY`) string into a structured PKCS#8
110
+ * key: `{ version, privateKeyAlgorithm, privateKey, attributes, publicKey }`. The
111
+ * `privateKey` is the raw OCTET STRING content (the algorithm-specific inner key,
112
+ * decoded by the caller using `privateKeyAlgorithm.oid`); `publicKey` is `null`
113
+ * for a v1 key. A malformed PrivateKeyInfo throws a typed `Pkcs8Error` (`pkcs8/*`)
114
+ * and a leaf-level codec fault surfaces as `asn1/*`.
115
+ *
116
+ * @example
117
+ * var key = pki.schema.pkcs8.parse(der);
118
+ * key.privateKeyAlgorithm.oid; // -> "1.3.101.112" (Ed25519)
119
+ * key.privateKey; // -> Buffer (the inner key encoding)
120
+ */
121
+ var parse = pkix.makeParser({ pemLabel: "PRIVATE KEY", PemError: PemError, ErrorClass: Pkcs8Error, prefix: "pkcs8", what: "private key", topSchema: PRIVATE_KEY_INFO, ns: NS });
122
+
123
+ /**
124
+ * @primitive pki.schema.pkcs8.parseEncrypted
125
+ * @signature pki.schema.pkcs8.parseEncrypted(input) -> encrypted
126
+ * @since 0.1.9
127
+ * @status experimental
128
+ * @spec RFC 5958, RFC 5208
129
+ * @related pki.schema.pkcs8.parse
130
+ *
131
+ * Parse a DER `Buffer` or a PEM (`ENCRYPTED PRIVATE KEY`) string into an
132
+ * EncryptedPrivateKeyInfo: `{ encryptionAlgorithm, encryptedData }`. The
133
+ * ciphertext is surfaced raw; decrypting it (PBES2/PBKDF2 + a passphrase) is a
134
+ * separate concern from structural validation.
135
+ *
136
+ * @example
137
+ * var enc = pki.schema.pkcs8.parseEncrypted(der);
138
+ * enc.encryptionAlgorithm.oid; // -> "1.2.840.113549.1.5.13" (PBES2)
139
+ */
140
+ var parseEncrypted = pkix.makeParser({ pemLabel: "ENCRYPTED PRIVATE KEY", PemError: PemError, ErrorClass: Pkcs8Error, prefix: "pkcs8", what: "encrypted private key", topSchema: ENCRYPTED_PRIVATE_KEY_INFO, ns: NS });
141
+
142
+ /**
143
+ * @primitive pki.schema.pkcs8.pemDecode
144
+ * @signature pki.schema.pkcs8.pemDecode(text, label?) -> Buffer
145
+ * @since 0.1.9
146
+ * @status experimental
147
+ * @spec RFC 7468, RFC 5958
148
+ * @related pki.schema.pkcs8.parse
149
+ *
150
+ * Extract the DER bytes from a PEM private-key block (default label `PRIVATE
151
+ * KEY`). Throws `PemError` on a missing / mismatched envelope or a non-base64
152
+ * body.
153
+ *
154
+ * @example
155
+ * var der = pki.schema.pkcs8.pemDecode(pemText);
156
+ */
157
+ function pemDecode(text, label) { return pkix.pemDecode(text, label || "PRIVATE KEY", PemError); }
158
+
159
+ /**
160
+ * @primitive pki.schema.pkcs8.pemEncode
161
+ * @signature pki.schema.pkcs8.pemEncode(der, label?) -> string
162
+ * @since 0.1.9
163
+ * @status experimental
164
+ * @spec RFC 7468
165
+ * @related pki.schema.pkcs8.pemDecode
166
+ *
167
+ * Wrap DER bytes in a PEM private-key envelope (default label `PRIVATE KEY`).
168
+ *
169
+ * @example
170
+ * var pem = pki.schema.pkcs8.pemEncode(der);
171
+ */
172
+ function pemEncode(der, label) { return pkix.pemEncode(der, label || "PRIVATE KEY", PemError); }
173
+
174
+ // A PKCS#8 PrivateKeyInfo root is a SEQUENCE whose first child is a universal
175
+ // INTEGER (version) and third child a universal OCTET STRING (privateKey) — a
176
+ // shape the signed-envelope trio (whose children[0] is a SEQUENCE and children[2]
177
+ // a BIT STRING) can never present, so the detectors are mutually exclusive
178
+ // regardless of registry order. Trailing context fields are [0]/[1] in ascending
179
+ // order.
180
+ function matches(root) {
181
+ var TAGS = asn1.TAGS;
182
+ if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE) return false;
183
+ var k = root.children;
184
+ if (!k || k.length < 3 || k.length > 5) return false;
185
+ if (!(k[0].tagClass === "universal" && k[0].tagNumber === TAGS.INTEGER)) return false;
186
+ if (!(k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE)) return false;
187
+ if (!(k[2].tagClass === "universal" && k[2].tagNumber === TAGS.OCTET_STRING)) return false;
188
+ var last = -1;
189
+ for (var i = 3; i < k.length; i++) {
190
+ if (k[i].tagClass !== "context" || k[i].tagNumber <= last || k[i].tagNumber > 1) return false;
191
+ last = k[i].tagNumber;
192
+ }
193
+ return true;
194
+ }
195
+
196
+ // NOTE: there is intentionally no `matchesEncrypted` detector. An
197
+ // EncryptedPrivateKeyInfo is a SEQUENCE { SEQUENCE, OCTET STRING } — a shape it
198
+ // shares with a PKCS#1 DigestInfo and other AlgorithmIdentifier-plus-octets
199
+ // structures — so it cannot be classified from structure alone. The orchestrator
200
+ // does not auto-route it; an operator who knows a key is encrypted (e.g. from an
201
+ // 'ENCRYPTED PRIVATE KEY' PEM label) calls parseEncrypted directly. A structural
202
+ // auto-route can return once a validated encryption-algorithm discriminator (the
203
+ // PBES layer) exists.
204
+
205
+ module.exports = {
206
+ parse: parse,
207
+ parseEncrypted: parseEncrypted,
208
+ pemDecode: pemDecode,
209
+ pemEncode: pemEncode,
210
+ matches: matches,
211
+ };
@@ -105,6 +105,30 @@ var DN_SHORT = {
105
105
  };
106
106
 
107
107
  // AlgorithmIdentifier ::= SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY OPTIONAL }
108
+ // The error/OID namespace every format module walks its schema under:
109
+ // { prefix, E:(code,message,cause)=>new ErrorClass(...), oid }. Factored so a
110
+ // format declares it in one line instead of repeating the error-constructor
111
+ // closure (a caller that never passes a cause is unaffected — withCause ignores
112
+ // an undefined third arg).
113
+ function makeNS(prefix, ErrorClass, oidModule) {
114
+ return { prefix: prefix, E: function (code, message, cause) { return new ErrorClass(code, message, cause); }, oid: oidModule };
115
+ }
116
+
117
+ // A bounded universal-INTEGER version reader. `accept` maps each legal wire value
118
+ // (as a decimal string) to its surfaced version number; any other value is a
119
+ // <prefix>/bad-version fault. The one genuine per-format divergence — the cert
120
+ // rejects 0 and maps 1->2/2->3, a CRL accepts only 1->2, a CSR only 0->1, a PKCS#8
121
+ // 0->1/1->2 — is expressed purely as the accept map (RFC 5280 §4.1.2.1 / §5.1.2.1,
122
+ // RFC 2986 §4.1, RFC 5958 §2). read.integer is strict, so an ENUMERATED-tagged
123
+ // version is rejected at the leaf (asn1/*).
124
+ function versionReader(ns, accept) {
125
+ return schema.decode(function (n) {
126
+ var key = asn1.read.integer(n).toString();
127
+ if (Object.prototype.hasOwnProperty.call(accept, key)) return accept[key];
128
+ throw ns.E(ns.prefix + "/bad-version", "unsupported version " + key);
129
+ });
130
+ }
131
+
108
132
  function algorithmIdentifier(ns) {
109
133
  return schema.seq([
110
134
  schema.field("algorithm", schema.oidLeaf()),
@@ -221,18 +245,124 @@ function extensions(ns) {
221
245
  });
222
246
  }
223
247
 
248
+ // SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier,
249
+ // subjectPublicKey BIT STRING } (RFC 5280 §4.1.2.7, RFC 2986 §4.1). Asserted as a
250
+ // universal SEQUENCE — a context-tagged or SET-tagged constructed node carrying
251
+ // two well-formed children is NOT a SubjectPublicKeyInfo. Shared by the
252
+ // certificate and CSR parsers.
253
+ function spki(ns) {
254
+ return schema.seq([
255
+ schema.field("algorithm", algorithmIdentifier(ns)),
256
+ schema.field("subjectPublicKey", schema.bitString()),
257
+ ], {
258
+ assert: "sequence", arity: { exact: 2 }, code: ns.prefix + "/bad-spki", what: "SubjectPublicKeyInfo",
259
+ build: function (m) {
260
+ return {
261
+ algorithm: m.fields.algorithm.value.result,
262
+ publicKey: { unusedBits: m.fields.subjectPublicKey.value.unusedBits, bytes: m.fields.subjectPublicKey.value.bytes },
263
+ bytes: m.node.bytes,
264
+ };
265
+ },
266
+ });
267
+ }
268
+
269
+ // Attribute ::= SEQUENCE { type OBJECT IDENTIFIER, values SET OF AttributeValue }.
270
+ // AttributeValue is ANY, kept as raw DER (node.bytes) — an unrecognized attribute
271
+ // type never fails the parse. values is SET SIZE (1..MAX): an empty SET is
272
+ // rejected; there is NO SET-OF uniqueness. Shared by the CSR (requested
273
+ // attributes, RFC 2986 §4.1) and PKCS#8 (private-key attributes, RFC 5958 §2).
274
+ function attribute(ns) {
275
+ return schema.seq([
276
+ schema.field("type", schema.oidLeaf()),
277
+ schema.field("values", schema.setOf(schema.any(), { assert: "set", min: 1, code: ns.prefix + "/bad-attribute-values", what: "attribute values" })),
278
+ ], {
279
+ assert: "sequence", arity: { exact: 2 }, code: ns.prefix + "/bad-attribute", what: "Attribute",
280
+ build: function (m, ctx) {
281
+ var t = m.fields.type.value;
282
+ return {
283
+ type: t,
284
+ name: ctx.oid.name(t) || null,
285
+ values: m.fields.values.value.items.map(function (it) { return it.node.bytes; }),
286
+ };
287
+ },
288
+ });
289
+ }
290
+
291
+ // Certificate, CertificateList, and CertificationRequest share one outer shape:
292
+ // SEQUENCE { toBeSigned SEQUENCE, signatureAlgorithm, signatureValue }. This
293
+ // returns the first element (the to-be-signed info) when `root` is that
294
+ // SEQUENCE-of-exactly-3 whose first child is itself a constructed universal
295
+ // SEQUENCE, or null otherwise. Every format's `matches` detector shares this
296
+ // preamble, so the signed-envelope shape is recognized in one place and the three
297
+ // detectors cannot drift on it (the CRL detector historically omitted the
298
+ // tbs-is-universal check this recovers).
299
+ function signedEnvelopeTbs(root) {
300
+ if (!root || root.tagClass !== "universal" || root.tagNumber !== asn1.TAGS.SEQUENCE) return null;
301
+ if (!root.children || root.children.length !== 3) return null;
302
+ var tbs = root.children[0];
303
+ if (!tbs.children || tbs.tagClass !== "universal" || tbs.tagNumber !== asn1.TAGS.SEQUENCE) return null;
304
+ return tbs;
305
+ }
306
+
307
+ // Every format's `parse` is the shared runParse bound to that format's identity
308
+ // (PEM label, error class, error-code prefix, top-level schema). This returns the
309
+ // bound parser so a format declares its configuration once and never re-writes the
310
+ // coerce -> decode -> walk wrapper. `opts`: { pemLabel, PemError, ErrorClass,
311
+ // prefix, what, topSchema, ns }.
312
+ function makeParser(opts) {
313
+ return function (input) { return runParse(input, opts); };
314
+ }
315
+
316
+ // The X.509 SIGNED{ToBeSigned} macro (RFC 5280 §4.1.1.3): the outer
317
+ // SEQUENCE { toBeSigned, signatureAlgorithm AlgorithmIdentifier,
318
+ // signatureValue BIT STRING } shared by Certificate, CertificateList and
319
+ // CertificationRequest. `tbsSchema` parses the first element; the SEQUENCE-of-3
320
+ // shape, the arity, the signature extraction and the raw tbs / outer-signature
321
+ // bytes (for the cert/CRL outer==inner agreement check) are owned here once, and
322
+ // each format's `opts.build(envelope, ctx)` shapes its own object from the
323
+ // envelope. A CSR's build simply omits the agreement check — its CRI has no inner
324
+ // signature AlgorithmIdentifier — so the omission is structural, not a copy that
325
+ // forgot a guard.
326
+ function signedEnvelope(ns, tbsSchema, opts) {
327
+ return schema.seq([
328
+ schema.field("toBeSigned", tbsSchema),
329
+ schema.field("signatureAlgorithm", algorithmIdentifier(ns)),
330
+ schema.field("signatureValue", schema.bitString()),
331
+ ], {
332
+ assert: "sequence", arity: { exact: 3 }, code: opts.code, what: opts.what,
333
+ build: function (m, ctx) {
334
+ var tbsMatch = m.fields.toBeSigned.value;
335
+ var sigBits = m.fields.signatureValue.value;
336
+ return opts.build({
337
+ tbsMatch: tbsMatch, // raw seq-match: .fields.* / .result / .node
338
+ tbsBytes: tbsMatch.node.bytes, // the exact signed region
339
+ outerSignatureAlgorithmBytes: m.fields.signatureAlgorithm.node.bytes,
340
+ signatureAlgorithm: m.fields.signatureAlgorithm.value.result,
341
+ signatureValue: { unusedBits: sigBits.unusedBits, bytes: sigBits.bytes },
342
+ }, ctx);
343
+ },
344
+ });
345
+ }
346
+
224
347
  module.exports = {
225
348
  pemDecode: pemDecode,
226
349
  pemEncode: pemEncode,
227
350
  coerceToDer: coerceToDer,
228
351
  decodeRoot: decodeRoot,
229
352
  runParse: runParse,
353
+ makeNS: makeNS,
354
+ versionReader: versionReader,
230
355
  DN_SHORT: DN_SHORT,
231
356
  algorithmIdentifier: algorithmIdentifier,
357
+ spki: spki,
358
+ makeParser: makeParser,
359
+ signedEnvelopeTbs: signedEnvelopeTbs,
360
+ signedEnvelope: signedEnvelope,
232
361
  attrValueToString: attrValueToString,
233
362
  attributeTypeAndValue: attributeTypeAndValue,
234
363
  relativeDistinguishedName: relativeDistinguishedName,
235
364
  name: name,
365
+ attribute: attribute,
236
366
  extension: extension,
237
367
  extensions: extensions,
238
368
  };
@@ -80,7 +80,7 @@ function pemEncode(der, label) { return pkix.pemEncode(der, label, PemError); }
80
80
  // sub-schemas (AlgorithmIdentifier, Name, Extension) live in lib/pkix-schema.js
81
81
  // as ns-parameterized factories so crl.js / cms.js reuse them while emitting
82
82
  // their own crl/*, cms/* codes; here they are instantiated under this NS.
83
- var NS = { prefix: "x509", E: function (code, message) { return new CertificateError(code, message); }, oid: oid };
83
+ var NS = pkix.makeNS("x509", CertificateError, oid);
84
84
 
85
85
  var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
86
86
  var NAME = pkix.name(NS);
@@ -95,35 +95,17 @@ 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
 
115
103
  // Version ::= INTEGER { v1(0), v2(1), v3(2) }, [0] EXPLICIT DEFAULT v1. Read as a
116
104
  // BigInt so an out-of-range value can't coerce to a float; reject an explicitly-
117
105
  // encoded v1 (DER forbids encoding the DEFAULT).
118
- function readVersion(ns) {
119
- return schema.decode(function (n) {
120
- var v = asn1.read.integer(n);
121
- if (v === 0n) throw ns.E(ns.prefix + "/bad-version", "DER forbids explicitly encoding the default version v1");
122
- if (v === 1n) return 2;
123
- if (v === 2n) return 3;
124
- throw ns.E(ns.prefix + "/bad-version", "unsupported certificate version " + v.toString());
125
- });
126
- }
106
+ // version ::= INTEGER { v1(0), v2(1), v3(2) } inside a [0] EXPLICIT DEFAULT v1 —
107
+ // DER forbids encoding the default, so 0 is rejected; 1->v2, 2->v3.
108
+ var CERTIFICATE_VERSION = pkix.versionReader(NS, { "1": 2, "2": 3 });
127
109
 
128
110
  // TBSCertificate ::= SEQUENCE { version [0] EXPLICIT DEFAULT v1, serialNumber
129
111
  // INTEGER, signature AlgorithmIdentifier, issuer Name, validity Validity,
@@ -132,7 +114,7 @@ function readVersion(ns) {
132
114
  // EXPLICIT OPTIONAL }. The trailing fields are at-most-once, in increasing tag
133
115
  // order (the engine enforces it); only extensions are surfaced.
134
116
  var CERTIFICATE_TBS = schema.seq([
135
- schema.optional("version", readVersion(NS), { tag: 0, explicit: true, emptyCode: "x509/bad-version", default: 1 }),
117
+ schema.optional("version", CERTIFICATE_VERSION, { tag: 0, explicit: true, emptyCode: "x509/bad-version", default: 1 }),
136
118
  schema.field("serialNumber", schema.integerLeaf()),
137
119
  schema.field("signature", ALGORITHM_IDENTIFIER),
138
120
  schema.field("issuer", NAME),
@@ -151,17 +133,17 @@ var CERTIFICATE_TBS = schema.seq([
151
133
  // agreement, §4.1.2.4 non-empty issuer, §4.1.2.9 extensions-only-in-v3) after the
152
134
  // structural walk, then assembles the public parse result. Raw-byte accessors
153
135
  // (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)
136
+ // Certificate ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }
137
+ // — the shared SIGNED envelope. The certificate-specific invariants (outer==inner
138
+ // signatureAlgorithm agreement, non-empty issuer, v3-only extensions) live in the
139
+ // build; the envelope owns the SEQUENCE-of-3 shape and signature extraction.
140
+ var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
141
+ code: "x509/not-a-certificate", what: "Certificate",
142
+ build: function (e, ctx) {
143
+ var tbs = e.tbsMatch; // CERTIFICATE_TBS seq-match (no build)
162
144
 
163
145
  // 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)) {
146
+ if (!e.outerSignatureAlgorithmBytes.equals(tbs.fields.signature.node.bytes)) {
165
147
  throw ctx.E("x509/bad-signature-algorithm", "signatureAlgorithm must match tbsCertificate.signature (RFC 5280 §4.1.1.2)");
166
148
  }
167
149
 
@@ -180,20 +162,19 @@ var CERTIFICATE = schema.seq([
180
162
  }
181
163
 
182
164
  var serialNode = tbs.fields.serialNumber.node;
183
- var sigBits = m.fields.signatureValue.value; // { unusedBits, bytes }
184
165
  return {
185
166
  version: version,
186
167
  serialNumber: tbs.fields.serialNumber.value,
187
168
  serialNumberHex: serialNode.content.toString("hex"),
188
- signatureAlgorithm: m.fields.signatureAlgorithm.value.result,
169
+ signatureAlgorithm: e.signatureAlgorithm,
189
170
  tbsSignatureAlgorithm: tbs.fields.signature.value.result,
190
171
  issuer: issuer,
191
172
  subject: tbs.fields.subject.value.result,
192
173
  validity: tbs.fields.validity.value.result,
193
174
  subjectPublicKeyInfo: tbs.fields.subjectPublicKeyInfo.value.result,
194
175
  extensions: hasExtensions ? extField.value.result : [],
195
- tbsBytes: tbs.node.bytes,
196
- signatureValue: { unusedBits: sigBits.unusedBits, bytes: sigBits.bytes },
176
+ tbsBytes: e.tbsBytes,
177
+ signatureValue: e.signatureValue,
197
178
  };
198
179
  },
199
180
  });
@@ -226,9 +207,7 @@ var CERTIFICATE = schema.seq([
226
207
  * cert.validity.notAfter; // Date
227
208
  * cert.signatureAlgorithm.name; // "sha256WithRSAEncryption"
228
209
  */
229
- function parse(input) {
230
- return pkix.runParse(input, { pemLabel: "CERTIFICATE", PemError: PemError, ErrorClass: CertificateError, prefix: "x509", what: "certificate", topSchema: CERTIFICATE, ns: NS });
231
- }
210
+ var parse = pkix.makeParser({ pemLabel: "CERTIFICATE", PemError: PemError, ErrorClass: CertificateError, prefix: "x509", what: "certificate", topSchema: CERTIFICATE, ns: NS });
232
211
 
233
212
  // matches(root): does the decoded DER look like a Certificate? A CSR and a CRL
234
213
  // share the outer SEQUENCE-of-3 envelope, so the discriminator is inside the
@@ -238,10 +217,8 @@ function parse(input) {
238
217
  // and a CRL leads with a bare Time; neither matches. Used by the orchestrator.
239
218
  function matches(root) {
240
219
  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;
220
+ var tbs = pkix.signedEnvelopeTbs(root);
221
+ if (!tbs) return false;
245
222
  var kids = tbs.children;
246
223
  var i = (kids[0] && kids[0].tagClass === "context" && kids[0].tagNumber === 0) ? 1 : 0; // optional [0] version
247
224
  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.9",
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:3cf64a06-eef0-417c-a199-787130b9149a",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-05T03:52:05.348Z",
8
+ "timestamp": "2026-07-05T08:37:25.927Z",
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.9",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.7",
25
+ "version": "0.1.9",
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.9",
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.9",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]