@blamejs/pki 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ 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.18 — 2026-07-08
8
+
9
+ An RFC 7292 PKCS#12 (PFX) store parser joins the pki.schema family.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.pkcs12.parse(input) — RFC 7292 PFX parsing. It decodes a DER / BER Buffer or PEM into { version, integrityMode, mac, macedBytes, authSafeSigned, safeBags, encryptedSafes }. Password-integrity stores surface { kind, hashOid, hashName, hashParameters, pbmac1, macValue, macSalt, iterations } plus macedBytes — the exact value octets the HMAC covers, excluding the octet-string header, so an external verifier hashes the correct region. The RFC 9579 PBMAC1 arm is validated, not just recognized: its parameters must be present, the key-derivation function must be PBKDF2 with a keyLength, and the decoded KDF (salt, iteration count, key length, PRF) and MAC scheme surface on pbmac1. The X.690 DEFAULT rule is enforced (an explicitly encoded iterations = 1 is non-canonical and rejects). Public-key-integrity stores surface the CMS SignedData and must carry at least one signer (the signature itself is verified externally). Each safeBag carries its type, friendlyName / localKeyId (decoded, single-value and single-instance rules enforced), and all attributes: keyBags delegate to pki.schema.pkcs8.parse, shrouded key bags to parseEncrypted (algorithm surfaced, ciphertext opaque), cert / CRL / secret values stay raw and byte-exact, and safeContentsBags recurse under a depth ceiling. Encrypted and enveloped safes are validated structurally by the CMS module with ciphertext kept raw, and must declare id-data (a SafeContents) as their encrypted content type. The version-3 rule, the contradictory MacData-alongside-SignedData combination, the closed bag-type and cert/CRL-type sets, and per-list element caps all fail closed with typed pkcs12/* codes; a MacData-less id-data store is legal syntax and parses as integrityMode "none".
14
+ - pki.schema.pkcs12.pemDecode(text, label?) / pemEncode(der, label?) — PEM handling for stores that transit text channels (default label PKCS12).
15
+ - pki.asn1.decode gains an opt-in ber option accepting exactly two shapes — an indefinite length on a constructed value and a constructed OCTET STRING, whose segments reassemble into one primitive content — for formats whose content is normatively BER. The default remains strict DER; minimal-length, minimal-integer, trailing-byte, and size / depth verdicts are unchanged in both modes, an indefinite length on a primitive value still rejects, a foreign-type segment inside a constructed string rejects, and constructed-string nesting is capped (each level re-copies its payload, so deep nesting is memory amplification, not data).
16
+ - pki.schema.engine.embeddedDer(schema, bytes, ctx, opts) — the named form of the re-decode idiom: decode a fresh DER / BER blob carried inside an already-decoded value and walk it against a schema, wrapping codec failures in the caller's typed code. A shared budget option bounds how many nested blobs one parse may unwrap, so a container that chains encodings across octet-string boundaries cannot restart the depth caps from zero. The timestamp, OCSP, and certificate-request parsers now route their embedded-structure decodes through it.
17
+ - SEQUENCE OF / SET OF schemas can declare an element-count ceiling (max), so a container listing a great many tiny elements fails typed instead of amplifying memory through per-element parse products; a single attribute's value list is now capped this way across every format.
18
+ - The OID registry gains the PKCS#12 bag types, the PKCS#12 password-based encryption identifiers, the PKCS#9 certTypes / crlTypes / friendlyName / localKeyId entries, PKCS#5 PBKDF2 / PBES2 / PBMAC1, the NIST AES content-encryption arc, and the HMAC-with-SHA identifiers, so a store's algorithms resolve to names.
19
+ - The error taxonomy gains Pkcs12Error, carrying a stable pkcs12/* code.
20
+
21
+ ### Changed
22
+
23
+ - The npm-publish workflow's vulnerability scan now scans the SBOM unconditionally and the vendored-bundle directory only when it holds a real bundle, so the scan is exact in both states instead of warning on the empty directory.
24
+
25
+ ### Fixed
26
+
27
+ - Certification-path validation bounds the RSASSA-PSS saltLength and trailerField before numeric conversion, so an oversized value rejects with path/unsupported-algorithm instead of rounding silently on its way to the verifier — the same exact-or-rejected rule the PKCS#12 MAC parameters follow.
28
+
29
+ ## v0.1.17 — 2026-07-06
30
+
31
+ An RFC 4211 certificate-request-message parser joins the pki.schema family.
32
+
33
+ ### Added
34
+
35
+ - pki.schema.crmf.parse(input) — RFC 4211 CertReqMessages parsing. It decodes a DER Buffer or PEM into { messages: [ { certReq, popo, regInfo } ] }, where each certReq is { certReqId, certReqIdHex, certTemplate, controls, certReqBytes } and certTemplate carries the requestable fields version, issuer, validity, subject, publicKey, and extensions (each null when absent). The fields RFC 4211 §5 requires a request to omit — serialNumber and signingAlg (assigned by the CA) and issuerUID and subjectUID (deprecated) — are rejected, not returned, so a requester cannot dictate a CA-assigned value. issuer and subject Names are accepted in both the EXPLICIT and the IMPLICIT wire encodings; the OptionalValidity times are EXPLICIT UTCTime or GeneralizedTime; a supplied CertTemplate version must be 2; certReqId is an unbounded signed integer. popo is null, a raVerified marker, a decoded signature proof (with its poposkInput and signature surfaced raw), or a raw key-encipherment / key-agreement arm; for a signature proof, poposkInput's presence is checked against the template per §4.1. certReqBytes is the exact CertRequest byte range a proof-of-possession verifier hashes. Malformed input fails closed with a typed crmf/* or asn1/* code.
36
+ - pki.schema.crmf.pemDecode(text, label?) — extract the DER bytes from a PEM block (the first block unless a label is given).
37
+ - The OID registry gains the RFC 4211 registration-control (id-regCtrl) and registration-info (id-regInfo) identifiers on the id-pkip arc, so a parsed control or info entry resolves to its name (oldCertID, pkiArchiveOptions, utf8Pairs, and the rest).
38
+ - The error taxonomy gains CrmfError, carrying a stable crmf/* code.
39
+ - pki.schema.engine.encode(schema, value) — the constructor direction of the schema engine. A single declarative schema now drives both decode (walk, bytes to value) and encode (canonical DER, value to bytes): every leaf carries a read and a write, and EXPLICIT wrapping and IMPLICIT context-tag retagging are applied in one place, so an encoder can no longer emit a different tag than the decoder reads. A round-trip test proves walk(decode(encode(value))) recovers the value across universal, IMPLICIT, EXPLICIT, and SET-OF-ordered shapes, and the CRMF request format is proven to round-trip end to end.
40
+
41
+ ### Changed
42
+
43
+ - pki.schema.ocsp request and response parsing (parseRequest, parseResponse, pemDecode) is now stable.
44
+ - An experimental primitive is surfaced for a graduation review once it has been experimental for a fixed number of releases; the review is recorded as a graduation to stable or a dated keep-experimental decision, so the experimental-to-stable-to-deprecated transition is driven on a schedule (see LTS-CALENDAR.md).
45
+
7
46
  ## v0.1.16 — 2026-07-06
8
47
 
9
48
  Certification path validation joins the toolkit — RFC 5280 section 6, as a pure re-entrant algorithm.
package/README.md CHANGED
@@ -199,10 +199,12 @@ is callable today; nothing below is a stub.
199
199
  | `pki.schema.ocsp` | Parse DER / PEM OCSP requests and responses per RFC 6960 — per-certificate status (good / revoked / unknown), responder identity, raw tbs bytes for external verification, certificates kept raw, fail-closed; non-basic response types recognized (not decoded) — `parseRequest`, `parseResponse`, `pemDecode` |
200
200
  | `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp responses and tokens — the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseTstInfo`, `parseToken`, `pemDecode` |
201
201
  | `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes (role / group / clearance) and extensions, with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode` |
202
- | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
202
+ | `pki.schema.crmf` | Parse DER / PEM RFC 4211 certificate request messages (CertReqMessages the CMP / EST enrollment body) — the requested-certificate template (subject, public key, validity, extensions), proof-of-possession, and registration controls, with the raw `CertRequest` region a POP verifier hashes; names dual-accepted (IMPLICIT and EXPLICIT), fail-closed — `parse`, `pemDecode` |
203
+ | `pki.schema.pkcs12` | Parse DER / BER / PEM RFC 7292 PKCS#12 (PFX) stores — key bags via the PKCS#8 parser, shrouded keys (algorithm surfaced, ciphertext opaque), cert / CRL / secret bags raw and byte-exact, encrypted and enveloped safes structurally via CMS, `friendlyName` / `localKeyId` decoded, and the exact MAC byte range (`macedBytes`) plus RFC 9579 PBMAC1 recognition for external verification; BER accepted exactly where §4.1 requires it, fail-closed — `parse`, `pemDecode`, `pemEncode` |
204
+ | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
203
205
  | `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining, validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes; `crlChecker` supplies CRL-based revocation. Pure and re-entrant, fail-closed — `validate`, `crlChecker` |
204
206
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
205
- | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `PathError`, each carrying a stable `code` in `domain/reason` form |
207
+ | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `PathError`, each carrying a stable `code` in `domain/reason` form |
206
208
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
207
209
 
208
210
  ### CLI
@@ -216,11 +218,11 @@ pki parse cert.pem # structured JSON summary of a certific
216
218
 
217
219
  ### What's coming
218
220
 
219
- Certificate build/sign/verify, certification-path
220
- validation, CMS (SignedData / EnvelopedData / EncryptedData / AuthenticatedData),
221
- OCSP, RFC 3161 timestamping, PKCS#8 decryption (PBES2) / SPKI / PKCS#12, and the
222
- post-quantum certificate and CMS surface (ML-DSA / ML-KEM / SLH-DSA and hybrid
223
- composites) are on the roadmap and ride this same core. See
221
+ Certificate build/sign/verify, CMS AuthenticatedData /
222
+ AuthEnvelopedData, PKCS#8 decryption (PBES2) / SPKI, PKCS#12 MAC verification
223
+ and bag decryption, and the post-quantum certificate and CMS surface (ML-DSA /
224
+ ML-KEM / SLH-DSA and hybrid composites) are on the roadmap and ride this same
225
+ core. See
224
226
  [ROADMAP.md](ROADMAP.md) for the full plan and current status of each area, and
225
227
  [CHANGELOG.md](CHANGELOG.md) for what has landed.
226
228
 
package/lib/asn1-der.js CHANGED
@@ -127,10 +127,19 @@ function _asBuffer(input, who) {
127
127
  * after the top-level value (unless `allowTrailing`), or exceeding the
128
128
  * size / depth caps.
129
129
  *
130
+ * `ber: true` is a scoped relaxation for formats whose content regions are
131
+ * normatively BER (RFC 7292 PKCS#12): it accepts an indefinite length on a
132
+ * constructed value and a constructed OCTET STRING, whose segments are
133
+ * reassembled into one primitive `content`. Nothing else is relaxed —
134
+ * definite lengths stay minimal, an indefinite length on a primitive value
135
+ * and a foreign-type segment still reject, and the size / depth caps hold.
136
+ *
130
137
  * @opts
131
138
  * maxBytes: number, // default: C.LIMITS.DER_MAX_BYTES (16 MiB)
132
139
  * maxDepth: number, // default: C.LIMITS.DER_MAX_DEPTH (64)
133
140
  * allowTrailing: boolean, // default: false — allow bytes after the top TLV
141
+ * ber: boolean, // default: false — accept indefinite lengths +
142
+ * // constructed OCTET STRINGs (BER content regions)
134
143
  *
135
144
  * @example
136
145
  * var node = pki.asn1.decode(der);
@@ -144,14 +153,14 @@ function decode(input, opts) {
144
153
  if (buf.length > maxBytes) {
145
154
  throw new Asn1Error("asn1/too-large", "input " + buf.length + " bytes exceeds cap " + maxBytes);
146
155
  }
147
- var r = _decodeTLV(buf, 0, buf.length, 0, maxDepth);
156
+ var r = _decodeTLV(buf, 0, buf.length, 0, maxDepth, opts.ber === true);
148
157
  if (!opts.allowTrailing && r.end !== buf.length) {
149
158
  throw new Asn1Error("asn1/trailing-bytes", (buf.length - r.end) + " trailing byte(s) after the top-level value");
150
159
  }
151
160
  return r.node;
152
161
  }
153
162
 
154
- function _decodeTLV(buf, start, limit, depth, maxDepth) {
163
+ function _decodeTLV(buf, start, limit, depth, maxDepth, ber, strDepth) {
155
164
  if (depth > maxDepth) {
156
165
  throw new Asn1Error("asn1/too-deep", "nesting exceeds depth cap " + maxDepth);
157
166
  }
@@ -195,8 +204,20 @@ function _decodeTLV(buf, start, limit, depth, maxDepth) {
195
204
  // encoded constructed is not valid DER. Without this a constructed string
196
205
  // tag decodes to a childless node that later paths (an X.509 DN attribute
197
206
  // value) would hex-render or dereference instead of failing closed.
207
+ // The ber mode licenses exactly one constructed primitive-only type — the
208
+ // OCTET STRING (the streamed content carrier RFC 7292 §4.1 permits); its
209
+ // segments are reassembled below. Every other primitive-only type stays
210
+ // primitive even in BER. Reassembly re-copies each nesting level's payload,
211
+ // so nesting is capped on the way down: real producers segment one level
212
+ // deep, and a deep chain multiplies transient memory without carrying data.
198
213
  if (tagClassBits === CLASS_UNIVERSAL && constructed && PRIMITIVE_ONLY_UNIVERSAL_TAGS[tagNumber]) {
199
- throw new Asn1Error("asn1/constructed-primitive-type", "a universal primitive-only type must be encoded primitive in DER");
214
+ if (!(ber && tagNumber === TAGS.OCTET_STRING)) {
215
+ throw new Asn1Error("asn1/constructed-primitive-type", "a universal primitive-only type must be encoded primitive in DER");
216
+ }
217
+ strDepth = (strDepth || 0) + 1;
218
+ if (strDepth > constants.LIMITS.BER_MAX_STRING_NESTING) {
219
+ throw new Asn1Error("asn1/bad-constructed-string", "constructed OCTET STRING nesting exceeds the cap " + constants.LIMITS.BER_MAX_STRING_NESTING);
220
+ }
200
221
  }
201
222
 
202
223
  if (p >= limit) throw new Asn1Error("asn1/truncated", "expected a length octet");
@@ -205,7 +226,12 @@ function _decodeTLV(buf, start, limit, depth, maxDepth) {
205
226
  if (lenByte < 0x80) {
206
227
  length = lenByte;
207
228
  } else if (lenByte === 0x80) {
208
- throw new Asn1Error("asn1/indefinite-length", "indefinite length is not valid DER");
229
+ // X.690 §8.1.3.6 indefinite length is only defined for a constructed
230
+ // encoding; the value runs to the end-of-contents octets.
231
+ if (!ber || !constructed) {
232
+ throw new Asn1Error("asn1/indefinite-length", "indefinite length is not valid DER");
233
+ }
234
+ length = -1;
209
235
  } else {
210
236
  var numLenBytes = lenByte & 0x7f;
211
237
  if (numLenBytes > 6) throw new Asn1Error("asn1/length-too-large", "length uses more than 6 octets");
@@ -218,34 +244,79 @@ function _decodeTLV(buf, start, limit, depth, maxDepth) {
218
244
  }
219
245
 
220
246
  var contentStart = p;
221
- var contentEnd = contentStart + length;
222
- if (contentEnd > limit) throw new Asn1Error("asn1/truncated", "content length overruns the buffer");
247
+ var indefinite = length === -1;
248
+ var contentEnd = indefinite ? -1 : contentStart + length;
249
+ if (!indefinite && contentEnd > limit) throw new Asn1Error("asn1/truncated", "content length overruns the buffer");
250
+
251
+ var children = null;
252
+ var content = null;
253
+ var child;
254
+ var end;
255
+ if (constructed) {
256
+ children = [];
257
+ var cp = contentStart;
258
+ if (indefinite) {
259
+ for (;;) {
260
+ if (cp + 2 > limit) throw new Asn1Error("asn1/truncated", "indefinite-length value is missing its end-of-contents octets");
261
+ if (buf[cp] === 0x00 && buf[cp + 1] === 0x00) break;
262
+ child = _decodeTLV(buf, cp, limit, depth + 1, maxDepth, ber, strDepth);
263
+ children.push(child.node);
264
+ cp = child.end;
265
+ }
266
+ contentEnd = cp;
267
+ end = cp + 2;
268
+ } else {
269
+ while (cp < contentEnd) {
270
+ child = _decodeTLV(buf, cp, contentEnd, depth + 1, maxDepth, ber, strDepth);
271
+ children.push(child.node);
272
+ cp = child.end;
273
+ }
274
+ end = contentEnd;
275
+ }
276
+ } else {
277
+ content = buf.subarray(contentStart, contentEnd);
278
+ end = contentEnd;
279
+ }
223
280
 
224
281
  var node = {
225
282
  tagClass: _className(tagClassBits),
226
283
  constructed: constructed,
227
284
  tagNumber: tagNumber,
228
- length: length,
285
+ length: contentEnd - contentStart,
229
286
  header: { start: start, end: contentStart },
230
287
  contentStart: contentStart,
231
288
  contentEnd: contentEnd,
232
- content: null,
233
- children: null,
234
- bytes: buf.subarray(start, contentEnd),
289
+ content: content,
290
+ children: children,
291
+ bytes: buf.subarray(start, end),
235
292
  };
236
293
 
237
- if (constructed) {
238
- node.children = [];
239
- var cp = contentStart;
240
- while (cp < contentEnd) {
241
- var child = _decodeTLV(buf, cp, contentEnd, depth + 1, maxDepth);
242
- node.children.push(child.node);
243
- cp = child.end;
294
+ // BER reassembly: a constructed OCTET STRING's segments concatenate into
295
+ // one primitive content, so every downstream reader sees the value a
296
+ // single-segment encoding would carry; `bytes` keeps the original wire
297
+ // range for raw round-trip surfaces. Nested constructed segments arrive
298
+ // already reassembled by the recursion (their nesting depth is capped on
299
+ // the way down — see the strDepth check above).
300
+ if (ber && constructed && tagClassBits === CLASS_UNIVERSAL && tagNumber === TAGS.OCTET_STRING) {
301
+ var segments = [];
302
+ for (var s = 0; s < children.length; s++) {
303
+ if (children[s].tagClass !== "universal" || children[s].tagNumber !== TAGS.OCTET_STRING) {
304
+ throw new Asn1Error("asn1/bad-constructed-string", "a constructed OCTET STRING segment must itself be an OCTET STRING");
305
+ }
306
+ segments.push(children[s].content);
244
307
  }
245
- } else {
246
- node.content = buf.subarray(contentStart, contentEnd);
308
+ node.content = Buffer.concat(segments);
309
+ node.constructed = false;
310
+ node.children = null;
311
+ }
312
+ // A context-tagged constructed node from a ber decode may be an IMPLICIT
313
+ // streamed string (the tag hides the underlying type), which only the
314
+ // schema-driven typed reader can identify — mark it so readers reassemble
315
+ // exactly those nodes and a strict decode's verdicts never change.
316
+ if (ber && constructed && tagClassBits === CLASS_CONTEXT && node.children) {
317
+ node.ber = true;
247
318
  }
248
- return { node: node, end: contentEnd };
319
+ return { node: node, end: end };
249
320
  }
250
321
 
251
322
  // ---- Node assertions ------------------------------------------------
@@ -375,13 +446,29 @@ function readOctetString(node) {
375
446
  // Read a [tag] IMPLICIT OCTET STRING — a context-class PRIMITIVE node whose
376
447
  // content is the octet-string body (the IMPLICIT tag replaces the universal one,
377
448
  // so there is no inner universal node). Primitive-only, like its universal
378
- // counterpart, so the BER constructed/streamed form is rejected. Used for the CMS
379
- // SignerIdentifier subjectKeyIdentifier [0] (RFC 5652 §5.3).
449
+ // counterpart, so a strict decode's constructed/streamed form is rejected.
450
+ // A node from a ber decode (the `ber` marker) may carry the streamed form —
451
+ // the context tag hides the underlying type from the decoder, so THIS reader
452
+ // is where a [tag] IMPLICIT OCTET STRING's segments reassemble (the shape CMS
453
+ // ciphertext streams as). Used for the CMS SignerIdentifier
454
+ // subjectKeyIdentifier [0] (RFC 5652 §5.3) and EncryptedContentInfo
455
+ // encryptedContent [0] (§6.1).
380
456
  function readOctetStringImplicit(node, tag) {
381
457
  if (node.tagClass !== "context" || node.tagNumber !== tag) {
382
458
  throw new Asn1Error("asn1/unexpected-tag", "readOctetStringImplicit: expected context tag [" + tag +
383
459
  "], got " + node.tagClass + "/" + node.tagNumber);
384
460
  }
461
+ if (node.constructed && node.ber === true) {
462
+ var segments = [];
463
+ for (var s = 0; s < node.children.length; s++) {
464
+ var seg = node.children[s];
465
+ if (seg.tagClass !== "universal" || seg.tagNumber !== TAGS.OCTET_STRING || seg.constructed || !seg.content) {
466
+ throw new Asn1Error("asn1/bad-constructed-string", "a constructed OCTET STRING segment must itself be an OCTET STRING");
467
+ }
468
+ segments.push(seg.content);
469
+ }
470
+ return Buffer.concat(segments);
471
+ }
385
472
  _expectPrimitive(node, "readOctetStringImplicit");
386
473
  return node.content;
387
474
  }
@@ -627,6 +714,18 @@ function encodeLength(n) {
627
714
  return Buffer.concat([Buffer.from([0x80 | bytes.length]), Buffer.from(bytes)]);
628
715
  }
629
716
 
717
+ // The DER of a universal SEQUENCE recovered from a decoded IMPLICIT [n] SEQUENCE
718
+ // `node`, whose context tag replaced the universal 0x30 on the wire (e.g. an RFC
719
+ // 4211 CertTemplate publicKey [6] SubjectPublicKeyInfo, or a POPOSigningKey
720
+ // poposkInput [0]): the field's value is imported / signed as a standalone SEQUENCE,
721
+ // not as the context-tagged wire form. A constructed node exposes its content as
722
+ // `children` (not `content`), so the content is those children's DER concatenated;
723
+ // the identifier becomes the SEQUENCE tag and the length is canonical DER.
724
+ function sequenceTlv(node) {
725
+ var content = node.content != null ? node.content : Buffer.concat((node.children || []).map(function (c) { return c.bytes; }));
726
+ return Buffer.concat([Buffer.from([TAGS.SEQUENCE | CONSTRUCTED_BIT]), encodeLength(content.length), content]);
727
+ }
728
+
630
729
  function encodeIdentifier(classBits, constructed, tagNumber) {
631
730
  var lead = classBits | (constructed ? CONSTRUCTED_BIT : 0);
632
731
  if (tagNumber < 0x1f) return Buffer.from([lead | tagNumber]);
@@ -848,6 +947,7 @@ module.exports = {
848
947
  encode: encodeTLV,
849
948
  encodeTLV: encodeTLV,
850
949
  encodeLength: encodeLength,
950
+ sequenceTlv: sequenceTlv,
851
951
  encodeIdentifier: encodeIdentifier,
852
952
  decodeOidContent: decodeOidContent,
853
953
  encodeOidContent: encodeOidContent,
package/lib/constants.js CHANGED
@@ -121,6 +121,22 @@ var LIMITS = {
121
121
  // work on an untrusted certificate bundle (a real chain is well under this;
122
122
  // the operator may override via opts.maxPathCerts).
123
123
  PATH_MAX_CERTS: 100,
124
+ // PKCS#12 container ceilings. A PFX carries lists at three altitudes
125
+ // (ContentInfos per AuthenticatedSafe, SafeBags per SafeContents,
126
+ // attributes per bag) and can chain fresh DER blobs inside OCTET STRINGs,
127
+ // where every re-decode would otherwise restart the depth cap from zero.
128
+ // A real keystore holds a handful of keys and certificates; thousands of
129
+ // elements or dozens of chained re-decodes is hostile input, not a store.
130
+ PKCS12_MAX_ELEMENTS: 1024,
131
+ PKCS12_MAX_REDECODES: 64,
132
+ PKCS12_MAX_BAG_DEPTH: 16,
133
+ // BER constructed-string reassembly copies each nesting level's payload, so
134
+ // nesting multiplies transient memory. Real producers segment one level
135
+ // deep; nesting past this cap is amplification, not a store.
136
+ BER_MAX_STRING_NESTING: 8,
137
+ // A single attribute's value SET — no deployed attribute carries more than
138
+ // a handful of values; a list at this scale is amplification.
139
+ ATTRIBUTE_MAX_VALUES: 256,
124
140
  };
125
141
 
126
142
  // Single-sourced from the package manifest so the reported version can
@@ -155,6 +155,17 @@ var TspError = defineClass("TspError", { withCause: true });
155
155
  // underlying leaf fault as `.cause`.
156
156
  var AttrCertError = defineClass("AttrCertError", { withCause: true });
157
157
 
158
+ // CrmfError — a byte sequence that is not a well-formed RFC 4211 CertReqMessages
159
+ // / CertReqMsg / CertRequest / CertTemplate. Carries the underlying leaf fault as
160
+ // `.cause`.
161
+ var CrmfError = defineClass("CrmfError", { withCause: true });
162
+
163
+ // Pkcs12Error — a byte sequence that is not a well-formed RFC 7292 PFX
164
+ // (AuthenticatedSafe / SafeContents / SafeBag), or a PFX violating a §4
165
+ // coherence rule (version, integrity mode, bag dispatch, MacData). Carries
166
+ // the underlying leaf fault as `.cause`.
167
+ var Pkcs12Error = defineClass("Pkcs12Error", { withCause: true });
168
+
158
169
  // PathError — a certification path that fails RFC 5280 §6 validation, or a
159
170
  // malformed input handed to the validator (an extension value that does not
160
171
  // decode, an empty path, an unsupported signature algorithm). Carries the
@@ -178,5 +189,7 @@ module.exports = {
178
189
  OcspError: OcspError,
179
190
  TspError: TspError,
180
191
  AttrCertError: AttrCertError,
192
+ CrmfError: CrmfError,
193
+ Pkcs12Error: Pkcs12Error,
181
194
  PathError: PathError,
182
195
  };
package/lib/oid.js CHANGED
@@ -103,6 +103,14 @@ var FAMILIES = {
103
103
  ocspBasic: 1, ocspNonce: 2, ocspCrl: 3, ocspResponse: 4, ocspNoCheck: 5,
104
104
  ocspArchiveCutoff: 6, ocspServiceLocator: 7, ocspPrefSigAlgs: 8, ocspExtendedRevoke: 9 } },
105
105
 
106
+ // CRMF (RFC 4211) registration controls (§6) and registration info (§7) on the
107
+ // id-pkip arc (id-pkix 5). id-regCtrl (id-pkip 1) names the control types a
108
+ // CertRequest carries; id-regInfo (id-pkip 2) names the registration-info types.
109
+ // The parser surfaces each control/info value RAW keyed by these names.
110
+ regCtrl: { base: [1, 3, 6, 1, 5, 5, 7, 5, 1], of: {
111
+ regToken: 1, authenticator: 2, pkiPublicationInfo: 3, pkiArchiveOptions: 4, oldCertID: 5, protocolEncrKey: 6 } },
112
+ regInfo: { base: [1, 3, 6, 1, 5, 5, 7, 5, 2], of: { utf8Pairs: 1, certReq: 2 } },
113
+
106
114
  // PKIX extended key purposes (id-kp, RFC 5280 §4.2.1.12). timeStamping is required
107
115
  // — critical and sole — on an RFC 3161 TSA signing certificate (§2.3).
108
116
  pkixKp: { base: [1, 3, 6, 1, 5, 5, 7, 3], of: {
@@ -120,13 +128,38 @@ var FAMILIES = {
120
128
  data: 1, signedData: 2, envelopedData: 3, signedAndEnvelopedData: 4,
121
129
  digestedData: 5, encryptedData: 6 } },
122
130
 
123
- // PKCS#9 attribute types — incl. the CMS signed-attribute OIDs (RFC 5652 §11).
131
+ // PKCS#9 attribute types — incl. the CMS signed-attribute OIDs (RFC 5652 §11)
132
+ // and the PKCS#12 bag attributes friendlyName / localKeyId (RFC 7292 §4.2).
124
133
  pkcs9: { base: [1, 2, 840, 113549, 1, 9], of: {
125
134
  emailAddress: 1, contentType: 3, messageDigest: 4, signingTime: 5,
126
- challengePassword: 7, extensionRequest: 14 } },
135
+ challengePassword: 7, extensionRequest: 14, friendlyName: 20, localKeyId: 21 } },
136
+
137
+ // PKCS#9 CertBag / CRLBag value discriminators (RFC 7292 §4.2.3-§4.2.4).
138
+ pkcs9CertTypes: { base: [1, 2, 840, 113549, 1, 9, 22], of: { x509Certificate: 1, sdsiCertificate: 2 } },
139
+ pkcs9CrlTypes: { base: [1, 2, 840, 113549, 1, 9, 23], of: { x509CRL: 1 } },
140
+
141
+ // PKCS#5 password-based schemes (RFC 8018) + the PBMAC1 MacData arm (RFC 9579).
142
+ pkcs5: { base: [1, 2, 840, 113549, 1, 5], of: { pbkdf2: 12, pbes2: 13, pbmac1: 14 } },
143
+
144
+ // PKCS#12 bag types (RFC 7292 §4.2, Appendix D).
145
+ pkcs12BagTypes: { base: [1, 2, 840, 113549, 1, 12, 10, 1], of: {
146
+ keyBag: 1, pkcs8ShroudedKeyBag: 2, certBag: 3, crlBag: 4, secretBag: 5, safeContentsBag: 6 } },
147
+
148
+ // PKCS#12 password-based encryption schemes (RFC 7292 Appendix C) — legacy
149
+ // PBE identifiers still emitted by deployed exporters; recognized so a
150
+ // shrouded bag's algorithm resolves to a name, never decrypted here.
151
+ pkcs12Pbe: { base: [1, 2, 840, 113549, 1, 12, 1], of: {
152
+ pbeWithSHAAnd128BitRC4: 1, pbeWithSHAAnd40BitRC4: 2,
153
+ "pbeWithSHAAnd3-KeyTripleDES-CBC": 3, "pbeWithSHAAnd2-KeyTripleDES-CBC": 4,
154
+ "pbeWithSHAAnd128BitRC2-CBC": 5, "pbeWithSHAAnd40BitRC2-CBC": 6 } },
155
+
156
+ // RSADSI digest / HMAC algorithms (RFC 8018 §B.1) — the PBKDF2 / PBMAC1 PRFs.
157
+ rsadsiDigest: { base: [1, 2, 840, 113549, 2], of: {
158
+ hmacWithSHA1: 7, hmacWithSHA224: 8, hmacWithSHA256: 9,
159
+ hmacWithSHA384: 10, hmacWithSHA512: 11 } },
127
160
 
128
161
  // S/MIME content types on the PKCS#9 smime arc (RFC 5652, RFC 3161): id-ct.
129
- smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4 } },
162
+ smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4, encKeyWithID: 21 } },
130
163
 
131
164
  // S/MIME authenticated attributes (id-aa, RFC 2634 / RFC 5035 / RFC 5816). The ESS
132
165
  // signing-certificate attributes bind a CMS / TSP SignerInfo to its signing cert;
@@ -149,6 +182,12 @@ var FAMILIES = {
149
182
  // nistHash covers only SHA-256 and above.
150
183
  oiwSecsig: { base: [1, 3, 14, 3, 2], of: { sha1: 26 } },
151
184
 
185
+ // NIST AES content-encryption algorithms (arc 2.16.840.1.101.3.4.1) — the
186
+ // modern PBES2 / CMS content ciphers a PKCS#12 or EnvelopedData names.
187
+ nistAes: { base: [2, 16, 840, 1, 101, 3, 4, 1], of: {
188
+ "aes128-CBC": 2, "aes128-GCM": 6, "aes192-CBC": 22, "aes192-GCM": 26,
189
+ "aes256-CBC": 42, "aes256-GCM": 46 } },
190
+
152
191
  // NIST hash functions (SHA-2, SHA-3, SHAKE).
153
192
  nistHash: { base: [2, 16, 840, 1, 101, 3, 4, 2], of: {
154
193
  sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake256: 12 } },
@@ -193,10 +193,18 @@ function resolveRsaPss(paramsBytes) {
193
193
  // A negative saltLength must be rejected: the OpenSSL-backed verify shim
194
194
  // reads -1/-2/-3 as RSA_PSS_SALTLEN_DIGEST/AUTO/MAX, and AUTO (-2) accepts
195
195
  // a signature of ANY salt length — defeating the salt-length binding.
196
- if (sl < 0n) throw E("path/unsupported-algorithm", "RSASSA-PSS saltLength must be non-negative");
196
+ // The upper bound keeps the value exact through Number conversion: the
197
+ // verifier binds to the salt length the certificate states, so a value
198
+ // that would round is not verifiable material (no real salt exceeds the
199
+ // modulus size, let alone this).
200
+ if (sl < 0n || sl > 2147483647n) throw E("path/unsupported-algorithm", "RSASSA-PSS saltLength must be a non-negative integer within the salt-length range");
197
201
  saltLength = Number(sl);
198
202
  } else if (f.tagNumber === 3) {
199
- trailer = Number(asn1.read.integer(f.children[0]));
203
+ // Compared for equality with 1 below — bound before conversion so an
204
+ // oversized value cannot round on its way to the comparison.
205
+ var tf = asn1.read.integer(f.children[0]);
206
+ if (tf < 0n || tf > 2147483647n) throw E("path/unsupported-algorithm", "unsupported RSASSA-PSS trailerField " + tf.toString());
207
+ trailer = Number(tf);
200
208
  }
201
209
  });
202
210
  if (hash === null) throw E("path/unsupported-algorithm", "RSASSA-PSS hashAlgorithm must be stated explicitly (the SHA-1 default is rejected)");
package/lib/schema-all.js CHANGED
@@ -11,9 +11,9 @@
11
11
  *
12
12
  * @intro
13
13
  * The schema family: a declarative ASN.1 structure-schema engine and the
14
- * per-format parsers built on it. Every format (X.509 certificates, CRLs,
15
- * PKCS#10 certification requests, PKCS#8 private keys, and CMS SignedData) is a
16
- * member that COMPOSES the
14
+ * per-format parsers built on it. Every format — from X.509 certificates
15
+ * and CRLs through CMS, OCSP, timestamps, and PKCS#12 stores; `all()`
16
+ * enumerates the registered set — is a member that COMPOSES the
17
17
  * shared engine and the shared PKIX sub-schemas (AlgorithmIdentifier, Name,
18
18
  * Extension), so a structural rule — bounds-checked positional reads,
19
19
  * optional / tagged field ordering, SET-OF uniqueness, fail-closed typed
@@ -36,9 +36,11 @@ var x509 = require("./schema-x509");
36
36
  var crl = require("./schema-crl");
37
37
  var csr = require("./schema-csr");
38
38
  var pkcs8 = require("./schema-pkcs8");
39
+ var pkcs12 = require("./schema-pkcs12");
39
40
  var cms = require("./schema-cms");
40
41
  var ocsp = require("./schema-ocsp");
41
42
  var tsp = require("./schema-tsp");
43
+ var crmf = require("./schema-crmf");
42
44
  var attrcert = require("./schema-attrcert");
43
45
  var frameworkError = require("./framework-error");
44
46
 
@@ -46,9 +48,12 @@ var SchemaError = frameworkError.SchemaError;
46
48
  var PemError = frameworkError.PemError;
47
49
 
48
50
  // The shared parse-entry opts for the orchestrator: label-agnostic PEM unwrap
49
- // (any block type routes), the schema/* error family. Detection needs DER, so
50
- // the coerced DER is what gets routed to the matched format.
51
- var ENTRY = { pemLabel: null, PemError: PemError, ErrorClass: SchemaError, prefix: "schema", what: "input" };
51
+ // (any block type routes), the schema/* error family. Detection needs a decoded
52
+ // root, so the coerced bytes are what gets routed to the matched format. The
53
+ // ber mode lets a BER-encoded PFX (the dominant real-world .p12 shape) decode
54
+ // far enough to detect; a BER input matching a strict-DER-only format still
55
+ // fails typed inside that format's own parse.
56
+ var ENTRY = { pemLabel: null, PemError: PemError, ErrorClass: SchemaError, prefix: "schema", what: "input", ber: true };
52
57
 
53
58
  // REGISTRY — each format is declarative data: a name, a `detect(root)` that
54
59
  // recognizes the decoded DER root as this format's outer structure, and the
@@ -83,6 +88,19 @@ var FORMATS = [
83
88
  detect: tsp.matches,
84
89
  parse: function (input) { return tsp.parse(input); },
85
90
  },
91
+ {
92
+ // CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg (RFC 4211 §3) — a
93
+ // SEQUENCE whose first CertReqMsg's CertRequest leads with a universal INTEGER
94
+ // (certReqId) then a universal SEQUENCE (certTemplate). Leads with a SEQUENCE
95
+ // like the signed-envelope trio and the ocsp-request, but the INTEGER-then-
96
+ // SEQUENCE pair at that depth is disjoint from both. Registered AHEAD of
97
+ // ocsp-request: a single-message CertReqMessages is the only remaining overlap
98
+ // with the ocsp-request detector, and crmf's deeper probe is the more specific.
99
+ name: "crmf",
100
+ module: crmf,
101
+ detect: crmf.matches,
102
+ parse: function (input) { return crmf.parse(input); },
103
+ },
86
104
  {
87
105
  // OCSPRequest ::= SEQUENCE { tbsRequest SEQUENCE, optionalSignature [0] EXPLICIT
88
106
  // OPTIONAL } — a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
@@ -103,6 +121,20 @@ var FORMATS = [
103
121
  detect: ocsp.matchesResponse,
104
122
  parse: function (input) { return ocsp.parseResponse(input); },
105
123
  },
124
+ {
125
+ // PKCS#12 PFX ::= SEQUENCE { version INTEGER, authSafe ContentInfo, macData
126
+ // OPTIONAL } — INTEGER-first like pkcs8, so the discriminators are deeper:
127
+ // children[1] is a ContentInfo (SEQUENCE of exactly 2: OID + [0] constructed —
128
+ // a shape a PrivateKeyInfo's AlgorithmIdentifier never presents) and
129
+ // children[2] is a MacData SEQUENCE or absent (pkcs8 requires an OCTET STRING
130
+ // there). Shape-disjoint from pkcs8, and registered ahead of it as the more
131
+ // specific probe (RFC 7292 §4). A BER-encoded PFX detects through the
132
+ // orchestrator's BER decode fallback and routes here.
133
+ name: "pkcs12",
134
+ module: pkcs12,
135
+ detect: pkcs12.matches,
136
+ parse: function (input) { return pkcs12.parse(input); },
137
+ },
106
138
  {
107
139
  // PKCS#8 PrivateKeyInfo / OneAsymmetricKey — SEQUENCE whose first child is an
108
140
  // INTEGER (version) and third an OCTET STRING (privateKey); disjoint from the
@@ -179,7 +211,7 @@ var FORMATS = [
179
211
  * The names of every registered format, in detection order.
180
212
  *
181
213
  * @example
182
- * pki.schema.all(); // → ["cms", "tsp", "ocsp-request", "ocsp-response", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
214
+ * pki.schema.all(); // → ["cms", "tsp", "crmf", "ocsp-request", "ocsp-response", "pkcs12", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
183
215
  */
184
216
  function all() { return FORMATS.map(function (f) { return f.name; }); }
185
217
 
@@ -221,9 +253,11 @@ module.exports = {
221
253
  crl: { parse: crl.parse, pemDecode: crl.pemDecode },
222
254
  csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
223
255
  pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
256
+ pkcs12: { parse: pkcs12.parse, pemDecode: pkcs12.pemDecode, pemEncode: pkcs12.pemEncode },
224
257
  cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
225
258
  ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode },
226
259
  tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode },
260
+ crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode },
227
261
  attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode },
228
262
  all: all,
229
263
  parse: parse,
package/lib/schema-cms.js CHANGED
@@ -618,7 +618,7 @@ var CONTENT_INFO = schema.seq([
618
618
  * @primitive pki.schema.cms.parse
619
619
  * @signature pki.schema.cms.parse(input) -> signedData
620
620
  * @since 0.1.10
621
- * @status experimental
621
+ * @status stable
622
622
  * @spec RFC 5652
623
623
  * @related pki.schema.parse, pki.schema.x509.parse
624
624
  *
@@ -642,7 +642,7 @@ var parse = pkix.makeParser({ pemLabel: "CMS", PemError: PemError, ErrorClass: C
642
642
  * @primitive pki.schema.cms.pemDecode
643
643
  * @signature pki.schema.cms.pemDecode(text, label?) -> Buffer
644
644
  * @since 0.1.10
645
- * @status experimental
645
+ * @status stable
646
646
  * @spec RFC 7468, RFC 5652
647
647
  * @related pki.schema.cms.parse
648
648
  *
@@ -658,7 +658,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CMS", Pe
658
658
  * @primitive pki.schema.cms.pemEncode
659
659
  * @signature pki.schema.cms.pemEncode(der, label?) -> string
660
660
  * @since 0.1.10
661
- * @status experimental
661
+ * @status stable
662
662
  * @spec RFC 7468
663
663
  * @related pki.schema.cms.pemDecode
664
664
  *
@@ -684,9 +684,28 @@ function matches(root) {
684
684
  return true;
685
685
  }
686
686
 
687
+ // Validate a bare EnvelopedData (RFC 5652 §6.1) — a universal SEQUENCE node —
688
+ // returning its structured value; throws a typed cms/* error on a malformed
689
+ // structure. Exposed for a composer that carries a bare EnvelopedData OUTSIDE a
690
+ // ContentInfo (an RFC 4211 CRMF encryptedKey [4] POPOPrivKey arm), which the
691
+ // content-type dispatch in `parse` cannot reach. The node MUST already be a
692
+ // universal SEQUENCE (an IMPLICIT [n] EnvelopedData is retagged by the caller).
693
+ function walkEnvelopedData(node) { return schema.walk(ENVELOPED_DATA, node, NS).result; }
694
+
695
+ // Validate a bare SignedData / EncryptedData node the same way — for a composer
696
+ // that holds an already-decoded content node and must not re-decode its bytes
697
+ // (an RFC 7292 PFX authSafe or encrypted safe, whose wire encoding may be BER
698
+ // that the strict `parse` entry would refuse). Same contract as
699
+ // walkEnvelopedData: the node is the bare structure, typed cms/* on rejection.
700
+ function walkSignedData(node) { return schema.walk(SIGNED_DATA, node, NS).result; }
701
+ function walkEncryptedData(node) { return schema.walk(ENCRYPTED_DATA, node, NS).result; }
702
+
687
703
  module.exports = {
688
704
  parse: parse,
689
705
  pemDecode: pemDecode,
690
706
  pemEncode: pemEncode,
691
707
  matches: matches,
708
+ walkEnvelopedData: walkEnvelopedData,
709
+ walkSignedData: walkSignedData,
710
+ walkEncryptedData: walkEncryptedData,
692
711
  };
package/lib/schema-crl.js CHANGED
@@ -190,7 +190,7 @@ var CERTIFICATE_LIST = pkix.signedEnvelope(NS, TBS_CERTLIST, {
190
190
  * @primitive pki.schema.crl.parse
191
191
  * @signature pki.schema.crl.parse(input) -> crl
192
192
  * @since 0.1.7
193
- * @status experimental
193
+ * @status stable
194
194
  * @spec RFC 5280
195
195
  * @related pki.schema.x509.parse, pki.schema.parse
196
196
  *
@@ -210,7 +210,7 @@ var parse = pkix.makeParser({ pemLabel: "X509 CRL", PemError: PemError, ErrorCla
210
210
  * @primitive pki.schema.crl.pemDecode
211
211
  * @signature pki.schema.crl.pemDecode(text, label?) -> Buffer
212
212
  * @since 0.1.7
213
- * @status experimental
213
+ * @status stable
214
214
  * @spec RFC 7468, RFC 5280
215
215
  * @related pki.schema.crl.parse
216
216
  *