@blamejs/pki 0.1.18 → 0.1.20

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,40 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.1.20 — 2026-07-09
8
+
9
+ An RFC 6962 Certificate Transparency SCT-list parser joins the toolkit.
10
+
11
+ ### Added
12
+
13
+ - pki.ct.parseSctList(extValue) — RFC 6962 SCT-list parsing. It decodes the SignedCertificateTimestampList extension value (the raw extnValue content an x509 or OCSP extension surfaces) into { scts, unknownScts }. Each scts entry is a fully decoded v1 SCT: version (0), logId (32-byte Buffer) plus logIdHex, timestamp (exact BigInt) plus timestampMs (a Number, or null above 2^53) plus timestampDate, extensions (raw Buffer), the hashAlg / sigAlg code points plus a named signatureAlgorithm, the raw signature, and rawSct (the full SerializedSCT body). A SerializedSCT whose version is not v1 is preserved opaque in unknownScts as { version, rawSct } rather than failing the list (RFC 6962 §3.3 gives each SerializedSCT its own length so unknown versions are skippable). The extension value is the §3.3 double DER OCTET STRING wrap over a TLS-encoded list, decoded with a bounded reader that validates the list and per-SCT framing and every internal length, and asserts a per-list byte and count cap before it iterates. The signature is never verified and the log id never recomputed. Malformed input fails closed with a typed ct/* (or leaf asn1/*) code.
14
+ - pki.ct.reconstructSignedData(entry, sct) — rebuilds the exact digitally-signed preimage a verifier hashes to check an SCT's signature (RFC 6962 §3.2), for a decoded v1 SCT. entry selects the log-entry form: { entryType: 0, leafCert } for an SCT delivered over TLS or OCSP (signed over the leaf certificate), or { entryType: 1, tbsCertificate, issuerKeyHash } for an SCT embedded in a certificate (signed over the issuer key hash and the precertificate TBS). The preimage reuses the parsed SCT's raw extensions byte-for-byte; a verifier hashes the returned bytes and checks the signature with the log's public key.
15
+ - The certificate-extension value registry gains the SCT-list decoder and the precertificate-poison decoder (the poison value is content-validated as ASN.1 NULL, not merely tag-checked).
16
+ - The OID registry gains the Certificate Transparency arc — the SCT-list, precertificate-poison, precertificate-signing-certificate, and OCSP SCT-list identifiers — so those extension OIDs resolve to names.
17
+ - The error taxonomy gains CtError, carrying a stable ct/* code.
18
+
19
+ ## v0.1.19 — 2026-07-09
20
+
21
+ An RFC 9810 Certificate Management Protocol message parser joins the pki.schema family.
22
+
23
+ ### Added
24
+
25
+ - 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.
26
+ - pki.schema.cmp.pemDecode(text, label?) / pemEncode(der, label?) — PEM handling for messages that transit text channels (default label CMP).
27
+ - 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.
28
+ - 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.
29
+ - The error taxonomy gains CmpError, carrying a stable cmp/* code.
30
+
31
+ ### Changed
32
+
33
+ - pki.schema.tsp parsing (parse, parseTstInfo, parseToken, pemDecode) is now stable.
34
+ - 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.
35
+
36
+ ### Fixed
37
+
38
+ - 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).
39
+ - 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.
40
+
7
41
  ## v0.1.18 — 2026-07-08
8
42
 
9
43
  An RFC 7292 PKCS#12 (PFX) store parser joins the pki.schema family.
package/README.md CHANGED
@@ -201,10 +201,12 @@ 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` |
207
+ | `pki.ct` | Parse RFC 6962 Certificate Transparency SCT lists — `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries in the SCT extension (a TLS-presentation-language payload inside the §3.3 double DER wrap) into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature; `reconstructSignedData` rebuilds the exact `digitally-signed` preimage for external verification. Structure decoded, crypto surfaced raw, bounded decode, fail-closed — `parseSctList`, `reconstructSignedData` |
206
208
  | `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 |
209
+ | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `CmpError` / `PathError` / `CtError`, each carrying a stable `code` in `domain/reason` form |
208
210
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
209
211
 
210
212
  ### CLI
package/index.js CHANGED
@@ -30,6 +30,7 @@ var oid = require("./lib/oid");
30
30
  var webcrypto = require("./lib/webcrypto");
31
31
  var schema = require("./lib/schema-all");
32
32
  var path = require("./lib/path-validate");
33
+ var ct = require("./lib/ct");
33
34
 
34
35
  module.exports = {
35
36
  version: constants.version,
@@ -46,6 +47,10 @@ module.exports = {
46
47
  // `path` is RFC 5280 §6 certification-path validation — pki.path.validate
47
48
  // runs the §6.1 state machine over an already-parsed path + a trust anchor.
48
49
  path: path,
50
+ // `ct` is RFC 6962 Certificate Transparency — pki.ct.parseSctList decodes the
51
+ // SCT-list extension a certificate / OCSP response carries; the signature is
52
+ // surfaced raw for external verification (pki.ct.reconstructSignedData).
53
+ ct: ct,
49
54
  // A ready W3C Crypto instance (globalThis.crypto shape) with the classes for
50
55
  // constructing more attached under the same namespace (pki.webcrypto.CryptoKey,
51
56
  // .SubtleCrypto, .Crypto, .WebCryptoError). PQC-first, classical-capable, zero-dep.
package/lib/constants.js CHANGED
@@ -117,6 +117,17 @@ var LIMITS = {
117
117
  PEM_MAX_BYTES: BYTES.mib(16),
118
118
  DER_MAX_INTEGER_BYTES: BYTES.kib(16),
119
119
  OID_MAX_SUBIDENTIFIER_BYTES: 32,
120
+ // Certificate Transparency SCT-list bounds (RFC 6962 §3.3). The outer sct_list
121
+ // vector carries a 2-byte length prefix, so a well-formed list body is at most
122
+ // 2^16-1 = 65535 bytes and the full TLS blob (prefix + body) is at most 65537.
123
+ // The byte cap sits at that structural maximum, so the largest conforming list
124
+ // is accepted while an oversized inner OCTET STRING (up to the DER document cap)
125
+ // is refused BEFORE the list is walked; the per-list count cap is likewise
126
+ // asserted before iteration so a hostile length prefix cannot drive unbounded
127
+ // work (the CVE-2022-0778 class: crafted bytes inside a certificate extension
128
+ // pinning a validator). A real chain carries 2-5 SCTs; 256 is far above policy.
129
+ SCT_MAX_BYTES: BYTES.kib(64) + 1,
130
+ SCT_MAX_COUNT: 256,
120
131
  // Certification-path length ceiling: bounds the per-cert asymmetric verify
121
132
  // work on an untrusted certificate bundle (a real chain is well under this;
122
133
  // the operator may override via opts.maxPathCerts).
package/lib/ct.js ADDED
@@ -0,0 +1,315 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.ct
6
+ * @nav Transparency
7
+ * @title CT
8
+ * @order 210
9
+ * @slug ct
10
+ *
11
+ * @intro
12
+ * Certificate Transparency SCT-list handling per RFC 6962. `parseSctList`
13
+ * decodes the `SignedCertificateTimestampList` an X.509 certificate (or an
14
+ * OCSP response) carries in the SCT extension into its individual signed
15
+ * certificate timestamps.
16
+ *
17
+ * The SCT payload is encoded in the TLS presentation language (RFC 8446 §3 /
18
+ * RFC 5246 §4 conventions) — positional, tag-less, fixed-width big-endian
19
+ * integers and length-prefixed opaque vectors — NOT ASN.1/DER. So this module
20
+ * owns a bounded big-endian TLS-struct reader rather than composing the DER
21
+ * schema engine; the only ASN.1 surface is the §3.3 double wrap (the
22
+ * extension value is a DER OCTET STRING whose content is another DER OCTET
23
+ * STRING whose content is the TLS list — the certificate/OCSP layer peels the
24
+ * outer, this module peels the inner).
25
+ *
26
+ * Structure is decoded, crypto is surfaced RAW: each SCT surfaces its `logId`
27
+ * (32 raw bytes — SHA-256 of the log's SPKI, never recomputed), the exact
28
+ * `timestamp` as a BigInt, the raw `extensions`, the named-but-not-interpreted
29
+ * `hashAlg`/`sigAlg` code points, and the raw `signature`. The parser NEVER
30
+ * verifies a signature, recomputes a LogID, or trusts a log — a verifier
31
+ * composes `webcrypto` over `reconstructSignedData(...)`, the exact
32
+ * `digitally-signed` preimage. DER-only carrier, fail-closed.
33
+ *
34
+ * @card
35
+ * Parse RFC 6962 Certificate Transparency SCT lists from a certificate or OCSP
36
+ * extension — per-SCT logId / timestamp (BigInt) / algorithm / raw signature,
37
+ * the signed-preimage reconstruction surfaced for external verification,
38
+ * bounded TLS-struct decode, fail-closed.
39
+ */
40
+
41
+ var asn1 = require("./asn1-der.js");
42
+ var constants = require("./constants.js");
43
+ var frameworkError = require("./framework-error.js");
44
+
45
+ var CtError = frameworkError.CtError;
46
+ var C = constants;
47
+
48
+ // RFC 5246 §7.4.1.4.1 code points — 1-byte, NOT OIDs. Surfaced named; an
49
+ // unknown code surfaces as its numeric byte with a null name (never rejected —
50
+ // off-profile-pair rejection is a verifier-tier log-conformance concern).
51
+ var HASH_ALGORITHMS = {
52
+ 0: "none", 1: "md5", 2: "sha1", 3: "sha224", 4: "sha256", 5: "sha384", 6: "sha512",
53
+ };
54
+ var SIGNATURE_ALGORITHMS = { 0: "anonymous", 1: "rsa", 2: "dsa", 3: "ecdsa" };
55
+
56
+ // A minimum viable v1 SCT body: version(1) + LogID(32) + timestamp(8) +
57
+ // empty-extensions(2) + digitally-signed{ hash(1) + sig(1) + empty-sig(2) } = 47.
58
+ var SCT_MIN_BODY = 47;
59
+ var LOGID_BYTES = 32;
60
+ var MAX_SAFE = 9007199254740991n; // 2^53 - 1; above this a Number loses precision
61
+
62
+ // ---- TlsReader — the one net-new primitive (an ENCODING layer, not a schema
63
+ // combinator): a bounded big-endian TLS-vector cursor with a hard [pos, end).
64
+ // A lying inner length can overrun only the current sub-reader's `end`, never
65
+ // the parent buffer, so bounds-before-slice is structural.
66
+ function TlsReader(buf, start, end) { this.buf = buf; this.pos = start; this.end = end; }
67
+ TlsReader.prototype._need = function (n, code) {
68
+ if (this.pos + n > this.end) {
69
+ throw new CtError(code || "ct/truncated", "need " + n + " byte(s), only " + (this.end - this.pos) + " remain");
70
+ }
71
+ };
72
+ TlsReader.prototype.u8 = function (code) { this._need(1, code); return this.buf[this.pos++]; };
73
+ TlsReader.prototype.u16 = function (code) { this._need(2, code); var v = this.buf.readUInt16BE(this.pos); this.pos += 2; return v; };
74
+ TlsReader.prototype.u24 = function (code) {
75
+ this._need(3, code);
76
+ var v = (this.buf[this.pos] << 16) | (this.buf[this.pos + 1] << 8) | this.buf[this.pos + 2];
77
+ this.pos += 3; return v;
78
+ };
79
+ TlsReader.prototype.u64 = function (code) { this._need(8, code); var v = this.buf.readBigUInt64BE(this.pos); this.pos += 8; return v; };
80
+ TlsReader.prototype.fixed = function (n, code) {
81
+ this._need(n, code);
82
+ var s = this.buf.subarray(this.pos, this.pos + n); this.pos += n; return s;
83
+ };
84
+ // opaque<min..max> — a length-prefixed vector (lenWidth-byte big-endian prefix).
85
+ // The length prefix itself is a plain read (a truncation there is ct/truncated);
86
+ // only a LYING length whose body overruns the bound carries the field's own code.
87
+ TlsReader.prototype.vector = function (lenWidth, min, max, code) {
88
+ var len = lenWidth === 3 ? this.u24() : this.u16();
89
+ if (len < min) throw new CtError(code, "vector length " + len + " below minimum " + min);
90
+ if (max != null && len > max) throw new CtError(code, "vector length " + len + " above maximum " + max);
91
+ this._need(len, code);
92
+ var s = this.buf.subarray(this.pos, this.pos + len); this.pos += len; return s;
93
+ };
94
+ TlsReader.prototype.subReader = function (len, code) {
95
+ this._need(len, code);
96
+ var r = new TlsReader(this.buf, this.pos, this.pos + len); this.pos += len; return r;
97
+ };
98
+ TlsReader.prototype.remaining = function () { return this.end - this.pos; };
99
+ TlsReader.prototype.atEnd = function () { return this.pos === this.end; };
100
+
101
+ // Peel the RFC 6962 §3.3 inner DER OCTET STRING (the certificate/OCSP layer
102
+ // already peeled the outer extnValue OCTET STRING). Rides the strict codec, so
103
+ // an indefinite length / constructed OCTET STRING / trailing bytes / single
104
+ // wrap all fail closed here; the asn1/* fault attaches as `.cause`.
105
+ function _peelInner(extValue) {
106
+ var node;
107
+ try { node = asn1.decode(extValue); }
108
+ catch (e) { throw new CtError("ct/bad-der", "the SCT-list extension value is not valid DER (RFC 6962 §3.3)", e); }
109
+ try { return asn1.read.octetString(node); }
110
+ catch (e) { throw new CtError("ct/bad-der", "the SCT-list extension value must be a DER OCTET STRING wrapping the TLS list (RFC 6962 §3.3)", e); }
111
+ }
112
+
113
+ function _toBuffer(v, field) {
114
+ if (Buffer.isBuffer(v)) return v;
115
+ if (v instanceof Uint8Array) return Buffer.from(v);
116
+ throw new CtError("ct/bad-input", field + " must be a Buffer or Uint8Array");
117
+ }
118
+
119
+ // Parse one SerializedSCT body inside a sub-reader bounded to the element (so a
120
+ // lying extensions/signature length overruns the element, never the list). A
121
+ // version this parser does not define is preserved OPAQUE, not rejected: RFC 6962
122
+ // §3.3 gives every SerializedSCT its own length prefix precisely so a client "can
123
+ // still parse old SCTs while skipping over new SCTs whose versions they don't
124
+ // understand" — so an unknown version yields { unknown, version, rawSct } and the
125
+ // v1-specific field decode (and the 47-byte floor) is skipped.
126
+ function _parseSct(r, sctLen) {
127
+ var bodyStart = r.pos;
128
+ var version = r.u8(); // sctLen >= 1 is guaranteed by the SerializedSCT<1..> check
129
+ if (version !== 0) {
130
+ return { unknown: true, version: version, rawSct: r.buf.subarray(bodyStart, r.end) };
131
+ }
132
+ if (sctLen < SCT_MIN_BODY) {
133
+ throw new CtError("ct/sct-too-short", "a v1 SCT body is at least " + SCT_MIN_BODY + " bytes, got " + sctLen + " (RFC 6962 §3.2)");
134
+ }
135
+ var logId = r.fixed(LOGID_BYTES);
136
+ var timestamp = r.u64();
137
+ var extensions = r.vector(2, 0, null, "ct/ext-overrun");
138
+ var hashAlg = r.u8();
139
+ var sigAlg = r.u8();
140
+ var signature = r.vector(2, 0, null, "ct/sig-overrun");
141
+ if (!r.atEnd()) {
142
+ throw new CtError("ct/sct-trailing-bytes", (r.end - r.pos) + " byte(s) left in a SerializedSCT after the signature (RFC 6962 §3.3)");
143
+ }
144
+ var timestampMs = timestamp <= MAX_SAFE ? Number(timestamp) : null;
145
+ return {
146
+ version: 0,
147
+ logId: logId, logIdHex: logId.toString("hex"),
148
+ timestamp: timestamp,
149
+ timestampMs: timestampMs,
150
+ timestampDate: new Date(timestampMs != null ? timestampMs : Number(timestamp)),
151
+ extensions: extensions,
152
+ hashAlg: hashAlg, sigAlg: sigAlg,
153
+ signatureAlgorithm: {
154
+ hash: hashAlg, hashName: HASH_ALGORITHMS[hashAlg] || null,
155
+ signature: sigAlg, signatureName: SIGNATURE_ALGORITHMS[sigAlg] || null,
156
+ },
157
+ signature: signature,
158
+ rawSct: r.buf.subarray(bodyStart, r.end),
159
+ };
160
+ }
161
+
162
+ /**
163
+ * @primitive pki.ct.parseSctList
164
+ * @signature pki.ct.parseSctList(extValue) -> { scts, unknownScts }
165
+ * @since 0.1.20
166
+ * @status experimental
167
+ * @spec RFC 6962, RFC 5246, RFC 8446
168
+ * @related pki.ct.reconstructSignedData, pki.schema.x509.parse
169
+ *
170
+ * Parse the value of an RFC 6962 SCT-list extension (the raw `extnValue`
171
+ * content an `x509.parse` / OCSP extension already surfaces) into
172
+ * `{ scts, unknownScts }`. Each entry of `scts` is a fully decoded v1 SCT:
173
+ * `version` (0), `logId` (32-byte Buffer) + `logIdHex`, `timestamp` (BigInt,
174
+ * exact) + `timestampMs` (Number or `null` above 2^53) + `timestampDate`,
175
+ * `extensions` (raw Buffer), `hashAlg` / `sigAlg` (1-byte code points) + a named
176
+ * `signatureAlgorithm`, the raw `signature` Buffer, and `rawSct` (the full
177
+ * SerializedSCT body). A SerializedSCT whose version this parser does not define
178
+ * is preserved OPAQUE in `unknownScts` as `{ version, rawSct }` rather than
179
+ * failing the list — RFC 6962 §3.3 frames each SerializedSCT with its own length
180
+ * so unknown versions are skippable (forward compatibility).
181
+ *
182
+ * The extension value is a DER `OCTET STRING` wrapping the TLS-encoded list
183
+ * (RFC 6962 §3.3 double wrap); everything below that peel is TLS presentation
184
+ * language, decoded with a bounded cursor. Structure is decoded, crypto is
185
+ * surfaced RAW — the signature is never verified and the LogID never recomputed.
186
+ *
187
+ * Throws `CtError` with a stable `ct/*` code on any malformed input (a bad inner
188
+ * DER wrap is `ct/bad-der` with the `asn1/*` fault as `.cause`), never a raw
189
+ * `TypeError`.
190
+ *
191
+ * @example
192
+ * var cert = pki.schema.x509.parse(pem);
193
+ * var sctOid = pki.oid.byName("signedCertificateTimestampList");
194
+ * var ext = (cert.extensions || []).find(function (e) { return e.oid === sctOid; });
195
+ * if (ext) {
196
+ * var list = pki.ct.parseSctList(ext.value);
197
+ * list.scts[0].logIdHex; // the log's key id
198
+ * list.scts[0].timestamp; // exact BigInt ms since epoch
199
+ * }
200
+ */
201
+ function parseSctList(extValue) {
202
+ if (!Buffer.isBuffer(extValue) && !(extValue instanceof Uint8Array)) {
203
+ throw new CtError("ct/bad-input", "parseSctList expects the SCT-list extension value as a Buffer or Uint8Array");
204
+ }
205
+ var blob = _peelInner(Buffer.isBuffer(extValue) ? extValue : Buffer.from(extValue));
206
+ if (blob.length > C.LIMITS.SCT_MAX_BYTES) {
207
+ throw new CtError("ct/too-large", "SCT list " + blob.length + " bytes exceeds the cap " + C.LIMITS.SCT_MAX_BYTES);
208
+ }
209
+ var outer = new TlsReader(blob, 0, blob.length);
210
+ var listLen = outer.u16("ct/bad-list");
211
+ if (listLen + 2 !== blob.length) {
212
+ throw new CtError("ct/bad-list", "the SCT list declared length " + listLen + " does not match the " + (blob.length - 2) + " byte(s) present (RFC 6962 §3.3)");
213
+ }
214
+ if (listLen < 1) {
215
+ throw new CtError("ct/empty-list", "an SCT list must contain at least one SCT (RFC 6962 §3.3)");
216
+ }
217
+ var scts = [], unknownScts = [];
218
+ while (!outer.atEnd()) {
219
+ if (outer.remaining() < 2) {
220
+ throw new CtError("ct/list-trailing-bytes", "a dangling partial element after the last complete SCT (RFC 6962 §3.3)");
221
+ }
222
+ var sctLen = outer.u16("ct/list-trailing-bytes");
223
+ if (sctLen < 1) {
224
+ throw new CtError("ct/sct-empty", "a SerializedSCT must be non-empty (RFC 6962 §3.3)");
225
+ }
226
+ if (outer.remaining() < sctLen) {
227
+ throw new CtError("ct/list-trailing-bytes", "a SerializedSCT length " + sctLen + " overruns the list (RFC 6962 §3.3)");
228
+ }
229
+ var one = _parseSct(outer.subReader(sctLen, "ct/list-trailing-bytes"), sctLen);
230
+ if (one.unknown) unknownScts.push({ version: one.version, rawSct: one.rawSct });
231
+ else scts.push(one);
232
+ // The cap bounds the TOTAL element count (known + preserved-unknown) before it
233
+ // can drive unbounded per-element work.
234
+ if (scts.length + unknownScts.length > C.LIMITS.SCT_MAX_COUNT) {
235
+ throw new CtError("ct/too-many-scts", "SCT count exceeds the cap " + C.LIMITS.SCT_MAX_COUNT);
236
+ }
237
+ }
238
+ return { scts: scts, unknownScts: unknownScts };
239
+ }
240
+
241
+ function _u24Bytes(n) {
242
+ if (n < 1 || n > 0xffffff) {
243
+ throw new CtError("ct/bad-tbs-length", "a certificate / TBSCertificate length must be in 1..2^24-1, got " + n + " (RFC 6962 §3.1)");
244
+ }
245
+ return Buffer.from([(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]);
246
+ }
247
+
248
+ /**
249
+ * @primitive pki.ct.reconstructSignedData
250
+ * @signature pki.ct.reconstructSignedData(entry, sct) -> Buffer
251
+ * @since 0.1.20
252
+ * @status experimental
253
+ * @spec RFC 6962
254
+ * @related pki.ct.parseSctList
255
+ *
256
+ * Rebuild the exact `digitally-signed` preimage bytes an external verifier
257
+ * hashes to check an SCT's signature (RFC 6962 §3.2), for a parsed `sct`.
258
+ * `entry` selects the log-entry arm:
259
+ * - `{ entryType: 0, leafCert: <DER Buffer> }` — an SCT delivered over TLS /
260
+ * OCSP, signed over `x509_entry(0)` with the leaf certificate.
261
+ * - `{ entryType: 1, tbsCertificate: <DER Buffer>, issuerKeyHash: <32B> }` —
262
+ * an SCT EMBEDDED in a certificate, signed over `precert_entry(1)` with the
263
+ * issuer key hash + the precertificate TBS (the TBS with only the SCT
264
+ * extension removed). `issuerKeyHash` is SHA-256 of the issuer's SPKI DER.
265
+ *
266
+ * The preimage reuses the parsed SCT's raw `extensions` byte-for-byte and
267
+ * re-emits the fixed-width scalars canonically. This never verifies anything —
268
+ * a verifier hashes the returned bytes and checks the signature with the log's
269
+ * public key (compose `webcrypto`). Throws `CtError` (`ct/bad-entry-type`,
270
+ * `ct/bad-issuer-key-hash`, `ct/bad-tbs-length`) on a malformed entry.
271
+ *
272
+ * @example
273
+ * var sct = pki.ct.parseSctList(sctExtValue).scts[0];
274
+ * var preimage = pki.ct.reconstructSignedData({ entryType: 0, leafCert: der }, sct);
275
+ * // hash `preimage` + verify against the log's public key at the verify layer
276
+ */
277
+ function reconstructSignedData(entry, sct) {
278
+ entry = entry || {};
279
+ var entryType = entry.entryType;
280
+ if (entryType !== 0 && entryType !== 1) {
281
+ throw new CtError("ct/bad-entry-type", "entryType must be x509_entry(0) or precert_entry(1), got " + entryType + " (RFC 6962 §3.1)");
282
+ }
283
+ // A fully decoded v1 SCT (from parseSctList().scts[]) — not an opaque
284
+ // unknownScts entry, whose body layout is undefined and cannot be signed over.
285
+ if (!sct || typeof sct.timestamp !== "bigint" || sct.version !== 0) {
286
+ throw new CtError("ct/bad-input", "reconstructSignedData expects a decoded v1 SCT from parseSctList().scts[]");
287
+ }
288
+ var parts = [];
289
+ parts.push(Buffer.from([sct.version & 0xff])); // Version — v1(0)
290
+ parts.push(Buffer.from([0])); // SignatureType — certificate_timestamp(0)
291
+ var ts = Buffer.alloc(8); ts.writeBigUInt64BE(BigInt(sct.timestamp)); parts.push(ts); // uint64 timestamp
292
+ parts.push(Buffer.from([(entryType >> 8) & 0xff, entryType & 0xff])); // LogEntryType (2 bytes BE)
293
+ if (entryType === 0) {
294
+ var cert = _toBuffer(entry.leafCert, "leafCert");
295
+ parts.push(_u24Bytes(cert.length)); parts.push(cert); // ASN.1Cert<1..2^24-1>
296
+ } else {
297
+ var ikh = _toBuffer(entry.issuerKeyHash, "issuerKeyHash");
298
+ if (ikh.length !== 32) {
299
+ throw new CtError("ct/bad-issuer-key-hash", "issuer_key_hash must be exactly 32 bytes (SHA-256 of the issuer SPKI), got " + ikh.length + " (RFC 6962 §3.2)");
300
+ }
301
+ var tbs = _toBuffer(entry.tbsCertificate, "tbsCertificate");
302
+ parts.push(ikh);
303
+ parts.push(_u24Bytes(tbs.length)); parts.push(tbs); // PreCert.tbs_certificate<1..2^24-1>
304
+ }
305
+ var ext = _toBuffer(sct.extensions, "sct.extensions"); // reuse the parsed raw bytes, never re-encode
306
+ parts.push(Buffer.from([(ext.length >> 8) & 0xff, ext.length & 0xff])); parts.push(ext); // CtExtensions
307
+ return Buffer.concat(parts);
308
+ }
309
+
310
+ module.exports = {
311
+ parseSctList: parseSctList,
312
+ reconstructSignedData: reconstructSignedData,
313
+ HASH_ALGORITHMS: HASH_ALGORITHMS,
314
+ SIGNATURE_ALGORITHMS: SIGNATURE_ALGORITHMS,
315
+ };
@@ -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
@@ -173,6 +179,15 @@ var Pkcs12Error = defineClass("Pkcs12Error", { withCause: true });
173
179
  // `.cause`.
174
180
  var PathError = defineClass("PathError", { withCause: true });
175
181
 
182
+ // CtError — a byte sequence that is not a well-formed RFC 6962 Certificate
183
+ // Transparency SCT list: a malformed inner DER OCTET-STRING wrap, or a TLS
184
+ // presentation-language framing violation (a lying vector length, a field read
185
+ // past its bound, a truncated element). An SCT whose version this parser does
186
+ // not define is NOT an error — RFC 6962 §3.3 makes unknown versions skippable,
187
+ // so they are preserved opaque. Carries the underlying leaf fault (e.g. the
188
+ // inner `asn1/*` decode error) as `.cause`.
189
+ var CtError = defineClass("CtError", { withCause: true });
190
+
176
191
  module.exports = {
177
192
  PkiError: PkiError,
178
193
  defineClass: defineClass,
@@ -191,5 +206,7 @@ module.exports = {
191
206
  AttrCertError: AttrCertError,
192
207
  CrmfError: CrmfError,
193
208
  Pkcs12Error: Pkcs12Error,
209
+ CmpError: CmpError,
194
210
  PathError: PathError,
211
+ CtError: CtError,
195
212
  };
package/lib/oid.js CHANGED
@@ -116,6 +116,15 @@ var FAMILIES = {
116
116
  pkixKp: { base: [1, 3, 6, 1, 5, 5, 7, 3], of: {
117
117
  serverAuth: 1, clientAuth: 2, codeSigning: 3, emailProtection: 4, timeStamping: 8, ocspSigning: 9 } },
118
118
 
119
+ // Google Certificate Transparency (RFC 6962) on the 1.3.6.1.4.1.11129.2.4 arc:
120
+ // the SCT-list X.509 extension (§3.3), the precertificate poison (§3.1), the
121
+ // precert-signing EKU (§3.1, naming only), and the OCSP-delivered SCT list
122
+ // (§3.3). The SCT payload itself is TLS presentation language, not DER — it is
123
+ // parsed by lib/ct.js (pki.ct), never routed through the DER schema engine.
124
+ ct: { base: [1, 3, 6, 1, 4, 1, 11129, 2, 4], of: {
125
+ signedCertificateTimestampList: 2, precertificatePoison: 3,
126
+ precertificateSigningCert: 4, ocspSignedCertificateTimestampList: 5 } },
127
+
119
128
  // PKCS#1 RSA public-key + RSASSA signature algorithms.
120
129
  rsa: { base: [1, 2, 840, 113549, 1, 1], of: {
121
130
  rsaEncryption: 1, rsaesOaep: 7, mgf1: 8, rsassaPss: 10,
@@ -141,6 +150,21 @@ var FAMILIES = {
141
150
  // PKCS#5 password-based schemes (RFC 8018) + the PBMAC1 MacData arm (RFC 9579).
142
151
  pkcs5: { base: [1, 2, 840, 113549, 1, 5], of: { pbkdf2: 12, pbes2: 13, pbmac1: 14 } },
143
152
 
153
+ // CMP InfoTypeAndValue types — id-it under id-pkix (RFC 9810 §5.3.19; the
154
+ // PKIXCMP-2023 module assigns these leaves, 8/9 unassigned).
155
+ idIt: { base: [1, 3, 6, 1, 5, 5, 7, 4], of: {
156
+ caProtEncCert: 1, signKeyPairTypes: 2, encKeyPairTypes: 3, preferredSymmAlg: 4,
157
+ caKeyUpdateInfo: 5, currentCRL: 6, unsupportedOIDs: 7, keyPairParamReq: 10,
158
+ keyPairParamRep: 11, revPassphrase: 12, implicitConfirm: 13, confirmWaitTime: 14,
159
+ origPKIMessage: 15, suppLangTags: 16, caCerts: 17, rootCaKeyUpdate: 18,
160
+ certReqTemplate: 19, rootCaCert: 20, certProfile: 21, crlStatusList: 22,
161
+ crls: 23, kemCiphertextInfo: 24 } },
162
+
163
+ // CMP message-protection MAC algorithms on the Entrust arc (RFC 9810
164
+ // §5.1.3.1/.2/.4; RFC 9481 §6.1.1).
165
+ entrustAlg: { base: [1, 2, 840, 113533, 7, 66], of: {
166
+ passwordBasedMac: 13, kemBasedMac: 16, dhBasedMac: 30 } },
167
+
144
168
  // PKCS#12 bag types (RFC 7292 §4.2, Appendix D).
145
169
  pkcs12BagTypes: { base: [1, 2, 840, 113549, 1, 12, 10, 1], of: {
146
170
  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 = {