@blamejs/pki 0.1.8 → 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,17 @@ 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.8 — 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
8
18
 
9
19
  A PKCS#10 certification-request parser joins the pki.schema family.
10
20
 
package/README.md CHANGED
@@ -194,9 +194,10 @@ is callable today; nothing below is a stub.
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
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` |
197
198
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
198
199
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
199
- | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError`, 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 |
200
201
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
201
202
 
202
203
  ### CLI
@@ -212,7 +213,7 @@ pki parse cert.pem # structured JSON summary of a certific
212
213
 
213
214
  Certificate build/sign/verify, certification-path
214
215
  validation, CMS (SignedData / EnvelopedData / EncryptedData / AuthenticatedData),
215
- 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
216
217
  post-quantum certificate and CMS surface (ML-DSA / ML-KEM / SLH-DSA and hybrid
217
218
  composites) are on the roadmap and ride this same core. See
218
219
  [ROADMAP.md](ROADMAP.md) for the full plan and current status of each area, and
package/lib/asn1-der.js CHANGED
@@ -318,9 +318,11 @@ function readEnumerated(node) {
318
318
  return _readIntegerLikeContent(node, "ENUMERATED", "readEnumerated");
319
319
  }
320
320
 
321
- function readBitString(node) {
322
- _expectUniversal(node, TAGS.BIT_STRING, "readBitString");
323
- _expectPrimitive(node, "readBitString");
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);
324
326
  var c = node.content;
325
327
  if (c.length === 0) throw new Asn1Error("asn1/bad-bit-string", "BIT STRING must have >= 1 content octet");
326
328
  var unusedBits = c[0];
@@ -335,6 +337,23 @@ function readBitString(node) {
335
337
  return { unusedBits: unusedBits, bytes: c.subarray(1) };
336
338
  }
337
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
+
338
357
  function readOctetString(node) {
339
358
  _expectUniversal(node, TAGS.OCTET_STRING, "readOctetString");
340
359
  _expectPrimitive(node, "readOctetString");
@@ -773,6 +792,7 @@ module.exports = {
773
792
  integer: readInteger,
774
793
  enumerated: readEnumerated,
775
794
  bitString: readBitString,
795
+ bitStringImplicit: readBitStringImplicit,
776
796
  octetString: readOctetString,
777
797
  nullValue: readNull,
778
798
  oid: readOid,
@@ -133,6 +133,10 @@ var SchemaError = defineClass("SchemaError", { withCause: true });
133
133
  // (RFC 2986).
134
134
  var CsrError = defineClass("CsrError", { withCause: true });
135
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
+
136
140
  module.exports = {
137
141
  PkiError: PkiError,
138
142
  defineClass: defineClass,
@@ -144,4 +148,5 @@ module.exports = {
144
148
  CrlError: CrlError,
145
149
  SchemaError: SchemaError,
146
150
  CsrError: CsrError,
151
+ Pkcs8Error: Pkcs8Error,
147
152
  };
package/lib/schema-all.js CHANGED
@@ -35,6 +35,7 @@ var pkix = require("./schema-pkix");
35
35
  var x509 = require("./schema-x509");
36
36
  var crl = require("./schema-crl");
37
37
  var csr = require("./schema-csr");
38
+ var pkcs8 = require("./schema-pkcs8");
38
39
  var frameworkError = require("./framework-error");
39
40
 
40
41
  var SchemaError = frameworkError.SchemaError;
@@ -51,6 +52,19 @@ var ENTRY = { pemLabel: null, PemError: PemError, ErrorClass: SchemaError, prefi
51
52
  // logic. Order matters: the most specific detector wins, so a new member is
52
53
  // inserted ahead of any more-permissive one.
53
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
+ },
54
68
  {
55
69
  // CertificationRequest ::= SEQUENCE { certificationRequestInfo,
56
70
  // signatureAlgorithm, signature } — the same outer 3-element shape,
@@ -93,7 +107,7 @@ var FORMATS = [
93
107
  * The names of every registered format, in detection order.
94
108
  *
95
109
  * @example
96
- * pki.schema.all(); // → ["csr", "crl", "x509"]
110
+ * pki.schema.all(); // → ["pkcs8", "csr", "crl", "x509"]
97
111
  */
98
112
  function all() { return FORMATS.map(function (f) { return f.name; }); }
99
113
 
@@ -134,6 +148,7 @@ module.exports = {
134
148
  x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode },
135
149
  crl: { parse: crl.parse, pemDecode: crl.pemDecode },
136
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 },
137
152
  all: all,
138
153
  parse: parse,
139
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
package/lib/schema-csr.js CHANGED
@@ -40,7 +40,7 @@ var PemError = frameworkError.PemError;
40
40
 
41
41
  // The csr error namespace the schema engine walks under. Shared PKIX sub-schemas
42
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 };
43
+ var NS = pkix.makeNS("csr", CsrError, oid);
44
44
 
45
45
  var NAME = pkix.name(NS);
46
46
  var SPKI = pkix.spki(NS);
@@ -50,30 +50,12 @@ var SPKI = pkix.spki(NS);
50
50
  // readers, which reject 0 as the DER-forbidden default. Surface v1 as 1. A
51
51
  // cert-shaped [0] EXPLICIT wrapper at this position makes asn1.read.integer throw
52
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
- });
53
+ var CSR_VERSION = pkix.versionReader(NS, { "0": 1 });
58
54
 
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
- });
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);
77
59
 
78
60
  // CertificationRequestInfo ::= SEQUENCE { version, subject Name,
79
61
  // subjectPKInfo SubjectPublicKeyInfo, attributes [0] IMPLICIT SET OF Attribute }.
@@ -97,6 +97,9 @@ function integerLeaf() { return { kind: "leaf", read: asn1.read.integer }; }
97
97
  function boolean() { return { kind: "leaf", read: asn1.read.boolean }; }
98
98
  function octetString() { return { kind: "leaf", read: asn1.read.octetString }; }
99
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 }; } }; }
100
103
  function any() { return { kind: "any" }; }
101
104
  function decode(fn) { return { kind: "decode", fn: fn }; }
102
105
 
@@ -368,7 +371,7 @@ module.exports = {
368
371
  seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, implicitSetOf: implicitSetOf, choice: choice,
369
372
  // leaves
370
373
  oidLeaf: oidLeaf, integerLeaf: integerLeaf, boolean: boolean, octetString: octetString,
371
- bitString: bitString, any: any, decode: decode, time: time,
374
+ bitString: bitString, implicitBitString: implicitBitString, any: any, decode: decode, time: time,
372
375
  // engine
373
376
  walk: walk,
374
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()),
@@ -242,6 +266,28 @@ function spki(ns) {
242
266
  });
243
267
  }
244
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
+
245
291
  // Certificate, CertificateList, and CertificationRequest share one outer shape:
246
292
  // SEQUENCE { toBeSigned SEQUENCE, signatureAlgorithm, signatureValue }. This
247
293
  // returns the first element (the to-be-signed info) when `root` is that
@@ -304,6 +350,8 @@ module.exports = {
304
350
  coerceToDer: coerceToDer,
305
351
  decodeRoot: decodeRoot,
306
352
  runParse: runParse,
353
+ makeNS: makeNS,
354
+ versionReader: versionReader,
307
355
  DN_SHORT: DN_SHORT,
308
356
  algorithmIdentifier: algorithmIdentifier,
309
357
  spki: spki,
@@ -314,6 +362,7 @@ module.exports = {
314
362
  attributeTypeAndValue: attributeTypeAndValue,
315
363
  relativeDistinguishedName: relativeDistinguishedName,
316
364
  name: name,
365
+ attribute: attribute,
317
366
  extension: extension,
318
367
  extensions: extensions,
319
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);
@@ -103,15 +103,9 @@ var EXTENSIONS = pkix.extensions(NS);
103
103
  // Version ::= INTEGER { v1(0), v2(1), v3(2) }, [0] EXPLICIT DEFAULT v1. Read as a
104
104
  // BigInt so an out-of-range value can't coerce to a float; reject an explicitly-
105
105
  // encoded v1 (DER forbids encoding the DEFAULT).
106
- function readVersion(ns) {
107
- return schema.decode(function (n) {
108
- var v = asn1.read.integer(n);
109
- if (v === 0n) throw ns.E(ns.prefix + "/bad-version", "DER forbids explicitly encoding the default version v1");
110
- if (v === 1n) return 2;
111
- if (v === 2n) return 3;
112
- throw ns.E(ns.prefix + "/bad-version", "unsupported certificate version " + v.toString());
113
- });
114
- }
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 });
115
109
 
116
110
  // TBSCertificate ::= SEQUENCE { version [0] EXPLICIT DEFAULT v1, serialNumber
117
111
  // INTEGER, signature AlgorithmIdentifier, issuer Name, validity Validity,
@@ -120,7 +114,7 @@ function readVersion(ns) {
120
114
  // EXPLICIT OPTIONAL }. The trailing fields are at-most-once, in increasing tag
121
115
  // order (the engine enforces it); only extensions are surfaced.
122
116
  var CERTIFICATE_TBS = schema.seq([
123
- 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 }),
124
118
  schema.field("serialNumber", schema.integerLeaf()),
125
119
  schema.field("signature", ALGORITHM_IDENTIFIER),
126
120
  schema.field("issuer", NAME),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.8",
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:64eeff87-502c-43f1-a398-5fbe7fc7a614",
5
+ "serialNumber": "urn:uuid:3cf64a06-eef0-417c-a199-787130b9149a",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-05T06:26:48.420Z",
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.8",
22
+ "bom-ref": "@blamejs/pki@0.1.9",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.8",
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.8",
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.8",
57
+ "ref": "@blamejs/pki@0.1.9",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]