@blamejs/pki 0.1.18 → 0.1.19

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,28 @@ 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.19 — 2026-07-09
8
+
9
+ An RFC 9810 Certificate Management Protocol message parser joins the pki.schema family.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.cmp.parse(input) — RFC 9810 PKIMessage parsing. It decodes a DER Buffer or PEM into { header, headerBytes, body, bodyBytes, protection, extraCerts }. The header carries pvno (1..3), validated sender / recipient GeneralNames (the anonymous NULL-DN accepted), and the optional messageTime (GeneralizedTime only), protectionAlg, senderKID / recipKID, transactionID, senderNonce / recipNonce, freeText, and generalInfo (recognized id-it values are syntax-checked). The body is { arm, tag, bytes, decoded? }: ir / cr / kur / krr / ccr decode through the CRMF parser; ip / cp / kup / ccp decode to a certificate-response structure (an encrypted certificate's EnvelopedData decodes through the CMS parser; the deprecated EncryptedValue arm and caPubs surface raw, conferring no trust); krp decodes to a key-recovery structure ({ status, newSigCert, caCerts, keyPairHist }); rr / rp, genm / genp, error, certConf (an empty confirmation is the legal reject-all), and pollReq / pollRep decode structurally; pkiconf decodes to null; every other defined arm — p10cr, the challenge-response and announcement arms, and nested (never auto-recursed) — surfaces raw. certReqId values are big integers and accept the protocol's -1 sentinel. The two cross-field coherence rules (protection bits and protectionAlg present together or absent together; a certConf hashAlg requires version cmp2021) are enforced. Protection is surfaced, not verified: headerBytes and bodyBytes are the exact wire slices, so a verifier reconstructs the protected part as a DER SEQUENCE wrapping them and checks the MAC or signature. Malformed input fails closed with a typed cmp/* or asn1/* code.
14
+ - pki.schema.cmp.pemDecode(text, label?) / pemEncode(der, label?) — PEM handling for messages that transit text channels (default label CMP).
15
+ - pki.schema.crmf.parse now surfaces every CertTemplate field, including serialNumber and the issuer/subject unique identifiers. RFC 4211's rule that a certificate request must omit the CA-assigned fields (serialNumber, signingAlg) and the deprecated unique identifiers moved from the shared CertTemplate structure to the request layer, so a request that sets them still fails closed while the same structure can identify an existing certificate — serialNumber and issuer present — inside a CMP revocation.
16
+ - The OID registry gains the CMP id-it information types and the message-protection MAC algorithm identifiers (passwordBasedMac, dhBasedMac, kemBasedMac), so a parsed message's info types and protection algorithm resolve to names.
17
+ - The error taxonomy gains CmpError, carrying a stable cmp/* code.
18
+
19
+ ### Changed
20
+
21
+ - pki.schema.tsp parsing (parse, parseTstInfo, parseToken, pemDecode) is now stable.
22
+ - The npm-publish vulnerability scan reads the committed lockfiles (the dev and build toolchain that runs during a publish) instead of the runtime SBOM, which is empty by construction because the package ships zero runtime dependencies.
23
+
24
+ ### Fixed
25
+
26
+ - Certification-path validation bounds the BasicConstraints pathLenConstraint and the PolicyConstraints / InhibitAnyPolicy skip counts before narrowing them to a number, so a certificate carrying a value past the safe-integer range is rejected rather than having the counter round silently to the wrong value (the same exact-or-rejected rule the RSASSA-PSS salt length and PKCS#12 iteration count follow).
27
+ - Certification-path validation rejects a non-empty DER NULL in an RSASSA-PSS hash AlgorithmIdentifier's parameters — a NULL must carry empty content (X.690 8.8.2), so the previous tag-only check accepted a malformed encoding it now fails closed.
28
+
7
29
  ## v0.1.18 — 2026-07-08
8
30
 
9
31
  An RFC 7292 PKCS#12 (PFX) store parser joins the pki.schema family.
package/README.md CHANGED
@@ -201,10 +201,11 @@ is callable today; nothing below is a stub.
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
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
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.cmp` | Parse DER / PEM RFC 9810 Certificate Management Protocol messages (PKIMessage) — the header (version, sender / recipient incl. the anonymous NULL-DN, nonces, transaction id, general info), the 27-arm body (certificate requests via the CRMF parser, an encrypted certificate's EnvelopedData via CMS, response / revocation / confirmation / error / support / polling arms structural, the rest raw), and the exact `headerBytes` / `bodyBytes` slices an external verifier reconstructs the protected part from; the CMP-before-OCSP dispatch order is enforced, fail-closed — `parse`, `pemDecode`, `pemEncode` |
204
205
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
205
206
  | `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` |
206
207
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
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 |
208
+ | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `CmpError` / `PathError`, each carrying a stable `code` in `domain/reason` form |
208
209
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
209
210
 
210
211
  ### CLI
@@ -166,6 +166,12 @@ var CrmfError = defineClass("CrmfError", { withCause: true });
166
166
  // the underlying leaf fault as `.cause`.
167
167
  var Pkcs12Error = defineClass("Pkcs12Error", { withCause: true });
168
168
 
169
+ // CmpError — a byte sequence that is not a well-formed RFC 9810 PKIMessage
170
+ // (PKIHeader / PKIBody / protection / extraCerts), or one violating a §5
171
+ // coherence rule (protection⇔protectionAlg, certConf-hashAlg⇔pvno). Carries
172
+ // the underlying leaf fault as `.cause`.
173
+ var CmpError = defineClass("CmpError", { withCause: true });
174
+
169
175
  // PathError — a certification path that fails RFC 5280 §6 validation, or a
170
176
  // malformed input handed to the validator (an extension value that does not
171
177
  // decode, an empty path, an unsupported signature algorithm). Carries the
@@ -191,5 +197,6 @@ module.exports = {
191
197
  AttrCertError: AttrCertError,
192
198
  CrmfError: CrmfError,
193
199
  Pkcs12Error: Pkcs12Error,
200
+ CmpError: CmpError,
194
201
  PathError: PathError,
195
202
  };
package/lib/oid.js CHANGED
@@ -141,6 +141,21 @@ var FAMILIES = {
141
141
  // PKCS#5 password-based schemes (RFC 8018) + the PBMAC1 MacData arm (RFC 9579).
142
142
  pkcs5: { base: [1, 2, 840, 113549, 1, 5], of: { pbkdf2: 12, pbes2: 13, pbmac1: 14 } },
143
143
 
144
+ // CMP InfoTypeAndValue types — id-it under id-pkix (RFC 9810 §5.3.19; the
145
+ // PKIXCMP-2023 module assigns these leaves, 8/9 unassigned).
146
+ idIt: { base: [1, 3, 6, 1, 5, 5, 7, 4], of: {
147
+ caProtEncCert: 1, signKeyPairTypes: 2, encKeyPairTypes: 3, preferredSymmAlg: 4,
148
+ caKeyUpdateInfo: 5, currentCRL: 6, unsupportedOIDs: 7, keyPairParamReq: 10,
149
+ keyPairParamRep: 11, revPassphrase: 12, implicitConfirm: 13, confirmWaitTime: 14,
150
+ origPKIMessage: 15, suppLangTags: 16, caCerts: 17, rootCaKeyUpdate: 18,
151
+ certReqTemplate: 19, rootCaCert: 20, certProfile: 21, crlStatusList: 22,
152
+ crls: 23, kemCiphertextInfo: 24 } },
153
+
154
+ // CMP message-protection MAC algorithms on the Entrust arc (RFC 9810
155
+ // §5.1.3.1/.2/.4; RFC 9481 §6.1.1).
156
+ entrustAlg: { base: [1, 2, 840, 113533, 7, 66], of: {
157
+ passwordBasedMac: 13, kemBasedMac: 16, dhBasedMac: 30 } },
158
+
144
159
  // PKCS#12 bag types (RFC 7292 §4.2, Appendix D).
145
160
  pkcs12BagTypes: { base: [1, 2, 840, 113549, 1, 12, 10, 1], of: {
146
161
  keyBag: 1, pkcs8ShroudedKeyBag: 2, certBag: 3, crlBag: 4, secretBag: 5, safeContentsBag: 6 } },
@@ -144,6 +144,10 @@ function hashAlgOid(seq) {
144
144
  if (seq.children.length === 2) {
145
145
  var p = seq.children[1];
146
146
  if (p.tagClass !== "universal" || p.tagNumber !== asn1.TAGS.NULL) throw E("path/unsupported-algorithm", "hash AlgorithmIdentifier parameters must be NULL or absent (RFC 4055)");
147
+ // A NULL is well-formed only with empty content (X.690 §8.8.2); the tag check
148
+ // alone would accept a non-empty NULL as valid parameters.
149
+ try { asn1.read.nullValue(p); }
150
+ catch (e) { throw E("path/unsupported-algorithm", "hash AlgorithmIdentifier NULL parameters must have empty content (RFC 4055)", e); }
147
151
  }
148
152
  return o;
149
153
  }
package/lib/schema-all.js CHANGED
@@ -41,6 +41,7 @@ var cms = require("./schema-cms");
41
41
  var ocsp = require("./schema-ocsp");
42
42
  var tsp = require("./schema-tsp");
43
43
  var crmf = require("./schema-crmf");
44
+ var cmp = require("./schema-cmp");
44
45
  var attrcert = require("./schema-attrcert");
45
46
  var frameworkError = require("./framework-error");
46
47
 
@@ -101,12 +102,28 @@ var FORMATS = [
101
102
  detect: crmf.matches,
102
103
  parse: function (input) { return crmf.parse(input); },
103
104
  },
105
+ {
106
+ // PKIMessage ::= SEQUENCE { header PKIHeader, body PKIBody [0..26],
107
+ // protection [0]?, extraCerts [1]? } (RFC 9810 §5.1) — a SEQUENCE of 2-4
108
+ // whose first child is a >=3-child SEQUENCE leading with a bare INTEGER
109
+ // (pvno) and whose second child is context-constructed [0..26]. ORDER IS
110
+ // LOAD-BEARING here: a 2-child PKIMessage whose body is ir [0] also
111
+ // satisfies the shallow ocsp-request probe below (k[0] SEQUENCE +
112
+ // k[1] context-[0]), while no OCSPRequest satisfies this detector (its
113
+ // tbsRequest never leads with a bare INTEGER) — so cmp MUST sit ahead of
114
+ // ocsp-request for the pair to dispatch deterministically.
115
+ name: "cmp",
116
+ module: cmp,
117
+ detect: cmp.matches,
118
+ parse: function (input) { return cmp.parse(input); },
119
+ },
104
120
  {
105
121
  // OCSPRequest ::= SEQUENCE { tbsRequest SEQUENCE, optionalSignature [0] EXPLICIT
106
122
  // OPTIONAL } — a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
107
123
  // Leads with a SEQUENCE like the signed-envelope trio, but is excluded by arity
108
124
  // (the trio is EXACTLY 3 children; an OCSPRequest is 1-2). Checked AFTER tsp,
109
- // whose detector is a strict refinement (RFC 6960 §4.1.1).
125
+ // whose detector is a strict refinement (RFC 6960 §4.1.1), and AFTER cmp,
126
+ // whose 2-child ir-body shape this probe would otherwise shadow.
110
127
  name: "ocsp-request",
111
128
  module: ocsp,
112
129
  detect: ocsp.matchesRequest,
@@ -211,7 +228,7 @@ var FORMATS = [
211
228
  * The names of every registered format, in detection order.
212
229
  *
213
230
  * @example
214
- * pki.schema.all(); // → ["cms", "tsp", "crmf", "ocsp-request", "ocsp-response", "pkcs12", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
231
+ * pki.schema.all(); // → ["cms", "tsp", "crmf", "cmp", "ocsp-request", "ocsp-response", "pkcs12", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
215
232
  */
216
233
  function all() { return FORMATS.map(function (f) { return f.name; }); }
217
234
 
@@ -258,6 +275,7 @@ module.exports = {
258
275
  ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode },
259
276
  tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode },
260
277
  crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode },
278
+ cmp: { parse: cmp.parse, pemDecode: cmp.pemDecode, pemEncode: cmp.pemEncode },
261
279
  attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode },
262
280
  all: all,
263
281
  parse: parse,
@@ -353,11 +353,11 @@ function matches(root) {
353
353
  if (!acinfo) return false;
354
354
  var k = acinfo.children;
355
355
  if (!k || k.length < 7) return false;
356
- if (!(k[0].tagClass === "universal" && k[0].tagNumber === TAGS.INTEGER)) return false;
357
- if (!(k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE && k[1].children)) return false;
356
+ if (!schema.isUniversal(k[0], TAGS.INTEGER)) return false;
357
+ if (!(schema.isUniversal(k[1], TAGS.SEQUENCE) && k[1].children)) return false;
358
358
  var v = k[5];
359
- if (!(v && v.tagClass === "universal" && v.tagNumber === TAGS.SEQUENCE && v.children && v.children.length === 2)) return false;
360
- return v.children.every(function (t) { return t.tagClass === "universal" && t.tagNumber === TAGS.GENERALIZED_TIME; });
359
+ if (!(schema.isUniversal(v, TAGS.SEQUENCE) && v.children && v.children.length === 2)) return false;
360
+ return v.children.every(function (t) { return schema.isUniversal(t, TAGS.GENERALIZED_TIME); });
361
361
  }
362
362
 
363
363
  // matchesV1(root): the obsolete AttributeCertificateV1 — its acInfo (version DEFAULT
@@ -370,8 +370,8 @@ function matchesV1(root) {
370
370
  if (!acinfo) return false;
371
371
  var k = acinfo.children;
372
372
  if (!k || k.length < 6) return false;
373
- if (!(k[0].tagClass === "context" && (k[0].tagNumber === 0 || k[0].tagNumber === 1) && k[0].children)) return false;
374
- return !!k[1] && k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE;
373
+ if (!(schema.isContextOneOf(k[0], [0, 1]) && k[0].children)) return false;
374
+ return schema.isUniversal(k[1], TAGS.SEQUENCE);
375
375
  }
376
376
 
377
377
  module.exports = {