@blamejs/pki 0.3.5 → 0.3.6
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 +8 -0
- package/README.md +1 -1
- package/lib/cmp-build.js +185 -15
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ 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.3.6 — 2026-07-17
|
|
8
|
+
|
|
9
|
+
pki.cmp.build gains the CA/responder side -- certificate, revocation, key-recovery, general, error, poll, and confirmation responses complete the RFC 9810 message surface.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.cmp.build message.body now accepts the CA/responder-side arms: ip / cp / kup / ccp (a CertRepMessage -- caPubs plus a response of CertResponse entries, each carrying a PKIStatusInfo and, under a granting status, a certifiedKeyPair), rp (a RevRepContent), krp (a KeyRecRepContent), genp (a general response), error (an ErrorMsgContent), pollRep (a poll response), and pkiconf (the final confirmation). They reuse the request-side header, envelope, ProtectedPart, and signature / PBMAC1 protection, and round-trip through pki.schema.cmp.parse. The RFC 9810 section 5.3.4 rules are enforced (a certifiedKeyPair only under a granting status and never with a failInfo, a validated certificate CHOICE, a single-response ccp). The private-key-transport / KEM encrypted forms ride a pre-encoded escape hatch.
|
|
14
|
+
|
|
7
15
|
## v0.3.5 — 2026-07-17
|
|
8
16
|
|
|
9
17
|
pki.cmp.build assembles protected RFC 9810 CMP PKIMessages -- certificate requests, confirmations, revocations, and general messages, protected by a sender-key signature or a PBMAC1 shared secret.
|
package/README.md
CHANGED
|
@@ -226,7 +226,7 @@ is callable today; nothing below is a stub.
|
|
|
226
226
|
| `pki.csr` | PKCS#10 certification-request issuance (RFC 2986 / RFC 2985) — `sign(spec, key, opts)` builds and signs a `CertificationRequest`: a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER; may be empty), the public key being certified, an optional `extensionRequest` (requested v3 extensions — subject alternative names, key usage, extended key usage, basic constraints, certificate policies, subject key identifier, or an array of pre-encoded Extension DER — that a CA copies into the issued certificate), and an optional `challengePassword`. `key` (or `{ key }`) is the subject's own private key: the request is self-signed to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the request is returned (what `openssl req -verify` checks). The signature algorithm is resolved from the subject key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. Returns DER, or a PEM `CERTIFICATE REQUEST` with `opts.pem`; malformed input throws a typed `CsrError`. Parsing stays at `pki.schema.csr.parse` — `sign` |
|
|
227
227
|
| `pki.attrcert` | RFC 5755 attribute-certificate issuance — `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` public-key-certificate reference, a `fromCertificate` binding derived from a certificate, or an object digest), the validity window (GeneralizedTime), an optional serial (positive, ≤ 20 octets; randomly generated when omitted), the `attributes` (the privilege syntaxes — role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo — or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, or pre-encoded Extension DER, each with its RFC 5755 criticality). An attribute certificate is never self-signed — the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. Parsing stays at `pki.schema.attrcert.parse` — `sign` |
|
|
228
228
|
| `pki.crmf` | RFC 4211 certificate-request-message issuance — `build(spec, key, opts)` assembles a `CertReqMessages`: a `spec` of `certReqId` (default 0; the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested certificate fields (`subject`, `publicKey` — the SPKI DER of the key being certified — `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken / authenticator / utf8Pairs / oldCertID / protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key — the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` (verified before the message is returned), exactly as a PKCS#10 CSR proves possession; a complete template signs the `CertRequest`, an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. Parsing stays at `pki.schema.crmf.parse` — `build` |
|
|
229
|
-
| `pki.cmp` | RFC 9810 Certificate Management Protocol message building — `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` / `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata (`transactionID`, `senderNonce` / `recipNonce`, `messageTime` as a GeneralizedTime, `senderKID` / `recipKID`, `freeText`, `generalInfo`); `message.body` is a single-key object naming the
|
|
229
|
+
| `pki.cmp` | RFC 9810 Certificate Management Protocol message building — `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` / `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata (`transactionID`, `senderNonce` / `recipNonce`, `messageTime` as a GeneralizedTime, `senderKID` / `recipKID`, `freeText`, `generalInfo`); `message.body` is a single-key object naming the arm. Request-side: `ir` / `cr` / `kur` (a `CertReqMessages` spec delegated to `pki.crmf.build`), `p10cr` (a PKCS#10 `CertificationRequest`), `certConf`, `pollReq`, `genm`, `rr`. CA/responder-side: `ip` / `cp` / `kup` / `ccp` (a `CertRepMessage` — `caPubs` and `response` entries carrying a `PKIStatusInfo` and, under a granting status, a `certifiedKeyPair`), `rp` (revocation response), `genp`, `error`, `pollRep`, `krp` (key-recovery response), `pkiconf`. Protection is exactly one of `opts.{ key, cert }` — a signature over the message under the sender key, the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch — or `opts.mac` — a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived). The protection covers the exact DER of the virtual `ProtectedPart` (the header and body) and is self-verified before the message is returned; the `protectionAlg` is derived, never caller-set, so the message the parser accepts is coherent by construction. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. Parsing stays at `pki.schema.cmp.parse` — `build` |
|
|
230
230
|
| `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. **Content encryption** (RFC 5652/5083/5084/9629): `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData — recipients auto-dispatch off the certificate key to key-transport (RSAES-OAEP; v1.5 never emitted), key-agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key-wrap, password (PBKDF2 + RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936) — one fresh content key wrapped for every recipient. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher / EFAIL / password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. **Compression** (RFC 3274): `compress(content, opts)` / `decompress(input, opts)` produce and consume a CompressedData (ZLIB, version 0, id-alg-zlibCompress); decompress bounds the uncompressed output at 16 MiB and stops before it is materialized, so a decompression bomb fails closed as `cms/decompress-too-large` — a size transform with no integrity/confidentiality (RFC 8551 §2.4.5). Fail-closed with typed `cms/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
|
|
231
231
|
| `pki.smime` | RFC 8551 S/MIME message assembly, verification, encryption, and compression over the CMS layer — `sign(content, signers, opts)` wraps a MIME entity as a signed S/MIME message in either form: `multipart/signed` (clear-signed — the content stays readable in any MUA, a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`) or `application/pkcs7-mime; smime-type=signed-data` (opaque — the whole entity is a base64 CMS SignedData). The signed bytes are the entity's RFC 8551 §3.1.1 canonical form (CRLF line endings); `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies and a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one — `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat); the `smime-type` is derived from the CMS body, not the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt` — any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through (algorithm-agnostic). Like `cms.verify`, `verify` returns the per-signer cryptographic verdict plus the recovered content; chaining a signer to a trust anchor is the caller's `pki.path.validate` step. `compress(content, opts)` / `decompress(message, opts)` add the opaque `application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z` frame (RFC 8551 §3.6, RFC 3274) — a size transform with no integrity/confidentiality (§2.4.5), decompress bounded against a bomb; the recovered content, which may itself be signed or enveloped, is returned for the caller to re-verify. Bidirectionally interoperable with `openssl smime` / `openssl cms`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
|
|
232
232
|
| `pki.tsp` | RFC 3161 Time-Stamp Protocol — `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData (over `pki.cms.sign`) whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` (with optional accuracy / nonce / ordering), plus the RFC 3161 §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` / `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), `response` / `parseResponse` the TimeStampResp a TSA returns — a granted status wrapping a token, or a rejection with PKIStatus and failure info, the §2.4.2 status↔token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and — with a trust anchor supplied — full certification-path validation of the TSA certificate at the token's `genTime`, returning `{ valid, genTime, serialNumber, tstInfo, … }` — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
|
package/lib/cmp-build.js
CHANGED
|
@@ -32,6 +32,7 @@ var oid = require("./oid");
|
|
|
32
32
|
var cmp = require("./schema-cmp");
|
|
33
33
|
var crmf = require("./crmf-sign");
|
|
34
34
|
var csr = require("./schema-csr");
|
|
35
|
+
var crl = require("./schema-crl");
|
|
35
36
|
var x509 = require("./schema-x509");
|
|
36
37
|
var signScheme = require("./sign-scheme");
|
|
37
38
|
var pkix = require("./schema-pkix");
|
|
@@ -58,10 +59,17 @@ var KNOWN_HEADER_KEYS = {
|
|
|
58
59
|
sender: 1, recipient: 1, pvno: 1, messageTime: 1, senderKID: 1, recipKID: 1,
|
|
59
60
|
transactionID: 1, senderNonce: 1, recipNonce: 1, freeText: 1, generalInfo: 1,
|
|
60
61
|
};
|
|
61
|
-
var KNOWN_BODY_KEYS = {
|
|
62
|
+
var KNOWN_BODY_KEYS = {
|
|
63
|
+
ir: 1, cr: 1, kur: 1, p10cr: 1, certConf: 1, pollReq: 1, genm: 1, rr: 1, // request-side
|
|
64
|
+
ip: 1, cp: 1, kup: 1, ccp: 1, krp: 1, rp: 1, genp: 1, error: 1, pollRep: 1, pkiconf: 1, // CA/responder-side
|
|
65
|
+
};
|
|
62
66
|
// The PKIBody arm -> its EXPLICIT context tag number (schema-cmp.js BODY_ARMS). rr is [11], NOT [15].
|
|
63
|
-
var BODY_TAG = {
|
|
67
|
+
var BODY_TAG = {
|
|
68
|
+
ir: 0, cr: 2, p10cr: 4, kur: 7, rr: 11, genm: 21, certConf: 24, pollReq: 25,
|
|
69
|
+
ip: 1, cp: 3, kup: 8, krp: 10, rp: 12, ccp: 14, pkiconf: 19, genp: 22, error: 23, pollRep: 26,
|
|
70
|
+
};
|
|
64
71
|
var CRMF_BODY = { ir: 1, cr: 1, kur: 1 }; // arms whose content is a CertReqMessages via pki.crmf.build
|
|
72
|
+
var CERT_REP_ARM = { ip: 1, cp: 1, kup: 1, ccp: 1 }; // arms carrying a CertRepMessage
|
|
65
73
|
var KNOWN_OPTS_KEYS = { key: 1, cert: 1, mac: 1, extraCerts: 1, pem: 1, pss: 1, digestAlgorithm: 1 };
|
|
66
74
|
var KNOWN_MAC_KEYS = { secret: 1, salt: 1, iterationCount: 1, prf: 1, keyLength: 1, algorithm: 1 };
|
|
67
75
|
|
|
@@ -186,20 +194,23 @@ function _reqIdInt(v, code, what) {
|
|
|
186
194
|
}
|
|
187
195
|
|
|
188
196
|
// PKIFailureInfo ::= BIT STRING (named bits, RFC 9810 sec. 5.2.3) -- a minimal NamedBitList from bit names.
|
|
189
|
-
function _encodeFailInfo(names) {
|
|
190
|
-
if (!Array.isArray(names)) throw _err(
|
|
197
|
+
function _encodeFailInfo(names, code) {
|
|
198
|
+
if (!Array.isArray(names)) throw _err(code, "statusInfo.failInfo must be an array of PKIFailureInfo bit names");
|
|
191
199
|
return b.namedBitString(names.map(function (n) {
|
|
192
|
-
if (typeof n !== "string" || FAIL_INFO_INDEX[n] === undefined) throw _err(
|
|
200
|
+
if (typeof n !== "string" || FAIL_INFO_INDEX[n] === undefined) throw _err(code, "unknown PKIFailureInfo bit " + JSON.stringify(n));
|
|
193
201
|
return FAIL_INFO_INDEX[n];
|
|
194
202
|
}));
|
|
195
203
|
}
|
|
196
204
|
|
|
197
|
-
// PKIStatusInfo ::= SEQUENCE { status PKIStatus INTEGER, statusString PKIFreeText OPTIONAL, failInfo BIT STRING
|
|
198
|
-
|
|
199
|
-
|
|
205
|
+
// PKIStatusInfo ::= SEQUENCE { status PKIStatus INTEGER, statusString PKIFreeText OPTIONAL, failInfo BIT STRING
|
|
206
|
+
// OPTIONAL }. `code` is the caller's typed error (default cmp/bad-status-info; certConf passes cmp/bad-cert-status).
|
|
207
|
+
function _encodePkiStatusInfo(si, code) {
|
|
208
|
+
code = code || "cmp/bad-status-info";
|
|
209
|
+
if (!si || typeof si !== "object" || Buffer.isBuffer(si)) throw _err(code, "a PKIStatusInfo must be an object { status, statusString?, failInfo? }");
|
|
210
|
+
if (typeof si.status !== "number" || !Number.isInteger(si.status) || si.status < 0 || si.status > 6) throw _err(code, "statusInfo.status must be a PKIStatus 0..6 (RFC 9810 sec. 5.2.3)");
|
|
200
211
|
var children = [b.integer(BigInt(si.status))];
|
|
201
|
-
if (si.statusString != null) children.push(_encodePkiFreeText(si.statusString,
|
|
202
|
-
if (si.failInfo != null) children.push(_encodeFailInfo(si.failInfo)); // strict order: status -> statusString -> failInfo (both optionals untagged)
|
|
212
|
+
if (si.statusString != null) children.push(_encodePkiFreeText(si.statusString, code, "statusInfo.statusString"));
|
|
213
|
+
if (si.failInfo != null) children.push(_encodeFailInfo(si.failInfo, code)); // strict order: status -> statusString -> failInfo (both optionals untagged)
|
|
203
214
|
return b.sequence(children);
|
|
204
215
|
}
|
|
205
216
|
|
|
@@ -211,7 +222,7 @@ function _encodeCertStatus(cs, state) {
|
|
|
211
222
|
_reqOctets(cs.certHash, "certConf certHash"),
|
|
212
223
|
b.integer(_reqIdInt(cs.certReqId, "cmp/bad-cert-status", "CertStatus certReqId")), // signed, -1 legal, unbounded
|
|
213
224
|
];
|
|
214
|
-
if (cs.statusInfo != null) children.push(_encodePkiStatusInfo(cs.statusInfo));
|
|
225
|
+
if (cs.statusInfo != null) children.push(_encodePkiStatusInfo(cs.statusInfo, "cmp/bad-cert-status"));
|
|
215
226
|
if (cs.hashAlg != null) {
|
|
216
227
|
if (typeof cs.hashAlg !== "string" || !CERT_CONF_HASH_ALGS[cs.hashAlg]) throw _err("cmp/bad-name", "certConf hashAlg must be a hash algorithm (sha256 / sha384 / sha512 / sha3-256 / sha3-512 / sha1); got " + JSON.stringify(cs.hashAlg));
|
|
217
228
|
children.push(b.explicit(0, _algIdNoParams(cs.hashAlg)));
|
|
@@ -264,6 +275,146 @@ function _encodeRevReqContent(list) {
|
|
|
264
275
|
return b.sequence(list.map(_encodeRevDetails));
|
|
265
276
|
}
|
|
266
277
|
|
|
278
|
+
// ---- response-side (CA/responder) PKIBody arm content encoders ----
|
|
279
|
+
|
|
280
|
+
// CertOrEncCert ::= CHOICE { certificate [0] EXPLICIT CMPCertificate, encryptedCert [1] EXPLICIT EncryptedKey }.
|
|
281
|
+
function _encodeCertOrEncCert(coec, state) {
|
|
282
|
+
if (!coec || typeof coec !== "object" || Buffer.isBuffer(coec)) throw _err("cmp/bad-cert-response", "certifiedKeyPair must carry certificate or encryptedCert");
|
|
283
|
+
if (coec.certificate != null && coec.encryptedCert != null) throw _err("cmp/bad-cert-response", "certOrEncCert is a CHOICE: supply exactly one of certificate or encryptedCert, not both");
|
|
284
|
+
if (coec.certificate != null) {
|
|
285
|
+
var certDer = _b.reqDer(coec.certificate, "certifiedKeyPair.certificate (a Certificate DER)");
|
|
286
|
+
try { x509.parse(certDer); } catch (e) { if (e instanceof CmpError) throw e; throw _err("cmp/bad-cert-response", "certifiedKeyPair.certificate is not a valid X.509 certificate", e); }
|
|
287
|
+
return b.explicit(0, b.raw(certDer)); // certificate [0] EXPLICIT
|
|
288
|
+
}
|
|
289
|
+
if (coec.encryptedCert != null) {
|
|
290
|
+
// encryptedCert [1] wraps an EncryptedKey (pre-encoded DER hatch in v1). Like the privateKey, only the
|
|
291
|
+
// EnvelopedData [0] form is cmp2021 -- the deprecated EncryptedValue (a universal SEQUENCE) is cmp2000.
|
|
292
|
+
var ec = _b.reqDer(coec.encryptedCert, "certifiedKeyPair.encryptedCert (a pre-encoded EncryptedKey DER)");
|
|
293
|
+
var ecNode;
|
|
294
|
+
try { ecNode = asn1.decode(ec); } catch (e) { throw _err("cmp/bad-cert-response", "certifiedKeyPair.encryptedCert is not valid DER", e); }
|
|
295
|
+
if (ecNode.tagClass === "context" && ecNode.tagNumber === 0) state.usesCmp2021 = true;
|
|
296
|
+
return b.explicit(1, b.raw(ec));
|
|
297
|
+
}
|
|
298
|
+
throw _err("cmp/bad-cert-response", "certifiedKeyPair must carry certificate or encryptedCert");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// CertifiedKeyPair ::= SEQUENCE { certOrEncCert, privateKey [0] EXPLICIT OPTIONAL, publicationInfo [1] EXPLICIT OPTIONAL }.
|
|
302
|
+
function _encodeCertifiedKeyPair(ckp, state) {
|
|
303
|
+
if (!ckp || typeof ckp !== "object" || Buffer.isBuffer(ckp)) throw _err("cmp/bad-cert-response", "certifiedKeyPair must be an object");
|
|
304
|
+
var children = [_encodeCertOrEncCert(ckp, state)]; // certificate / encryptedCert are direct keys (the parse-result shape)
|
|
305
|
+
if (ckp.privateKey != null) {
|
|
306
|
+
var pk = _b.reqDer(ckp.privateKey, "certifiedKeyPair.privateKey (a pre-encoded EncryptedKey DER)");
|
|
307
|
+
children.push(b.explicit(0, b.raw(pk)));
|
|
308
|
+
// EncryptedKey ::= CHOICE { encryptedValue EncryptedValue (a universal SEQUENCE, the deprecated cmp2000
|
|
309
|
+
// form), envelopedData [0] (a cmp2021 feature). Only the envelopedData form bumps pvno to cmp2021(3).
|
|
310
|
+
var pkNode;
|
|
311
|
+
try { pkNode = asn1.decode(pk); } catch (e) { throw _err("cmp/bad-cert-response", "certifiedKeyPair.privateKey is not valid DER", e); }
|
|
312
|
+
if (pkNode.tagClass === "context" && pkNode.tagNumber === 0) state.usesCmp2021 = true;
|
|
313
|
+
}
|
|
314
|
+
if (ckp.publicationInfo != null) children.push(b.explicit(1, b.raw(_b.reqDer(ckp.publicationInfo, "certifiedKeyPair.publicationInfo (a pre-encoded DER)"))));
|
|
315
|
+
return b.sequence(children);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// CertResponse ::= SEQUENCE { certReqId INTEGER, status PKIStatusInfo, certifiedKeyPair OPTIONAL, rspInfo OCTET STRING OPTIONAL }.
|
|
319
|
+
function _encodeCertResponse(cr, state) {
|
|
320
|
+
if (!cr || typeof cr !== "object" || Buffer.isBuffer(cr)) throw _err("cmp/bad-cert-response", "each CertResponse must be an object");
|
|
321
|
+
if (cr.status == null) throw _err("cmp/bad-cert-response", "CertResponse.status (a PKIStatusInfo) is required");
|
|
322
|
+
var hasCkp = cr.certifiedKeyPair != null;
|
|
323
|
+
// RFC 9810 sec. 5.3.4: failInfo XOR certifiedKeyPair; a certifiedKeyPair is allowed only under a granting
|
|
324
|
+
// status (accepted 0 / grantedWithMods 1).
|
|
325
|
+
if (hasCkp && cr.status.failInfo != null) throw _err("cmp/bad-cert-response", "a CertResponse must not carry both failInfo and certifiedKeyPair");
|
|
326
|
+
if (hasCkp && cr.status.status !== 0 && cr.status.status !== 1) throw _err("cmp/bad-cert-response", "a CertResponse certifiedKeyPair is allowed only under a granting status (accepted / grantedWithMods)");
|
|
327
|
+
var children = [b.integer(_reqIdInt(cr.certReqId, "cmp/bad-cert-response", "CertResponse certReqId")), _encodePkiStatusInfo(cr.status, "cmp/bad-cert-response")];
|
|
328
|
+
if (hasCkp) children.push(_encodeCertifiedKeyPair(cr.certifiedKeyPair, state));
|
|
329
|
+
if (cr.rspInfo != null) children.push(_reqOctets(cr.rspInfo, "CertResponse.rspInfo"));
|
|
330
|
+
return b.sequence(children);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// CertRepMessage ::= SEQUENCE { caPubs [1] EXPLICIT SEQUENCE OF CMPCertificate OPTIONAL, response SEQUENCE OF CertResponse }.
|
|
334
|
+
function _encodeCertRepMessage(spec, state, arm) {
|
|
335
|
+
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("cmp/bad-cert-rep", arm + " must be an object { caPubs?, response }");
|
|
336
|
+
if (!Array.isArray(spec.response)) throw _err("cmp/bad-cert-rep", arm + ".response must be an array of CertResponse");
|
|
337
|
+
if (arm === "ccp" && spec.response.length !== 1) throw _err("cmp/bad-cert-rep", "a ccp CertRepMessage carries exactly one CertResponse (RFC 9810 App. D)");
|
|
338
|
+
var children = [];
|
|
339
|
+
if (spec.caPubs != null) { // caPubs [1] PRECEDES response (the optional-first x509-version shape)
|
|
340
|
+
if (!Array.isArray(spec.caPubs) || !spec.caPubs.length) throw _err("cmp/bad-cert-rep", arm + ".caPubs must be a non-empty array of certificate DERs");
|
|
341
|
+
children.push(b.explicit(1, b.sequence(spec.caPubs.map(function (c) { return _certRaw(c, "cmp/bad-cert-rep", "a caPubs entry"); }))));
|
|
342
|
+
}
|
|
343
|
+
children.push(b.sequence(spec.response.map(function (cr) { return _encodeCertResponse(cr, state); })));
|
|
344
|
+
return b.sequence(children);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// A raw Certificate DER, validated via the real parser before it is embedded.
|
|
348
|
+
function _certRaw(c, code, what) {
|
|
349
|
+
var der = _b.reqDer(c, what + " (a Certificate DER)");
|
|
350
|
+
try { x509.parse(der); } catch (e) { if (e instanceof CmpError) throw e; throw _err(code, what + " is not a valid X.509 certificate", e); }
|
|
351
|
+
return b.raw(der);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// RevRepContent ::= SEQUENCE { status SEQ OF PKIStatusInfo (min 1), revCerts [0] EXPLICIT SEQ OF CertId OPTIONAL, crls [1] EXPLICIT SEQ OF CertificateList OPTIONAL }.
|
|
355
|
+
function _encodeCertIdRr(cid) {
|
|
356
|
+
if (!cid || typeof cid !== "object" || Buffer.isBuffer(cid) || cid.issuer == null || cid.serialNumber == null) throw _err("cmp/bad-rev-rep", "each revCerts CertId must be { issuer, serialNumber }");
|
|
357
|
+
return b.sequence([_b.encodeGeneralName(cid.issuer), b.integer(_reqIdInt(cid.serialNumber, "cmp/bad-rev-rep", "CertId serialNumber"))]);
|
|
358
|
+
}
|
|
359
|
+
function _encodeRevRepContent(spec) {
|
|
360
|
+
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("cmp/bad-rev-rep", "rp must be an object { status, revCerts?, crls? }");
|
|
361
|
+
if (!Array.isArray(spec.status) || !spec.status.length) throw _err("cmp/bad-rev-rep", "rp.status must be a non-empty array of PKIStatusInfo");
|
|
362
|
+
var children = [b.sequence(spec.status.map(function (si) { return _encodePkiStatusInfo(si, "cmp/bad-rev-rep"); }))];
|
|
363
|
+
if (spec.revCerts != null) {
|
|
364
|
+
if (!Array.isArray(spec.revCerts) || !spec.revCerts.length) throw _err("cmp/bad-rev-rep", "rp.revCerts must be a non-empty array of CertId");
|
|
365
|
+
children.push(b.explicit(0, b.sequence(spec.revCerts.map(_encodeCertIdRr))));
|
|
366
|
+
}
|
|
367
|
+
if (spec.crls != null) {
|
|
368
|
+
if (!Array.isArray(spec.crls) || !spec.crls.length) throw _err("cmp/bad-rev-rep", "rp.crls must be a non-empty array of CertificateList DERs");
|
|
369
|
+
children.push(b.explicit(1, b.sequence(spec.crls.map(function (c) {
|
|
370
|
+
var der = _b.reqDer(c, "crls entry (a CertificateList DER)");
|
|
371
|
+
try { crl.parse(der); } // each crls entry MUST be a valid X.509 CRL (CertificateList)
|
|
372
|
+
catch (e) { if (e instanceof CmpError) throw e; throw _err("cmp/bad-rev-rep", "a crls entry is not a valid CRL (CertificateList)", e); }
|
|
373
|
+
return b.raw(der);
|
|
374
|
+
}))));
|
|
375
|
+
}
|
|
376
|
+
return b.sequence(children);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ErrorMsgContent ::= SEQUENCE { pKIStatusInfo, errorCode INTEGER OPTIONAL, errorDetails PKIFreeText OPTIONAL }.
|
|
380
|
+
function _encodeErrorMsgContent(spec) {
|
|
381
|
+
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("cmp/bad-error", "error must be an object { pKIStatusInfo, errorCode?, errorDetails? }");
|
|
382
|
+
if (spec.pKIStatusInfo == null) throw _err("cmp/bad-error", "error.pKIStatusInfo is required");
|
|
383
|
+
var children = [_encodePkiStatusInfo(spec.pKIStatusInfo, "cmp/bad-error")];
|
|
384
|
+
if (spec.errorCode != null) children.push(b.integer(_reqIdInt(spec.errorCode, "cmp/bad-error", "error.errorCode")));
|
|
385
|
+
if (spec.errorDetails != null) children.push(_encodePkiFreeText(spec.errorDetails, "cmp/bad-error", "error.errorDetails"));
|
|
386
|
+
return b.sequence(children);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// PollRepContent ::= SEQUENCE OF SEQUENCE { certReqId INTEGER, checkAfter INTEGER (seconds, >= 0), reason PKIFreeText OPTIONAL }.
|
|
390
|
+
function _encodePollRepContent(list) {
|
|
391
|
+
if (!Array.isArray(list) || !list.length) throw _err("cmp/bad-poll-rep", "pollRep must be a non-empty array of { certReqId, checkAfter, reason? }");
|
|
392
|
+
return b.sequence(list.map(function (pr) {
|
|
393
|
+
if (!pr || typeof pr !== "object" || Buffer.isBuffer(pr)) throw _err("cmp/bad-poll-rep", "each pollRep entry must be an object");
|
|
394
|
+
if (typeof pr.checkAfter !== "number" || !Number.isInteger(pr.checkAfter) || pr.checkAfter < 0 || pr.checkAfter > 0x7fffffff) throw _err("cmp/bad-poll-rep", "pollRep checkAfter must be a non-negative uint31 delay in seconds (RFC 9810 sec. 5.3.22)");
|
|
395
|
+
var kids = [b.integer(_reqIdInt(pr.certReqId, "cmp/bad-poll-rep", "pollRep certReqId")), b.integer(BigInt(pr.checkAfter))];
|
|
396
|
+
if (pr.reason != null) kids.push(_encodePkiFreeText(pr.reason, "cmp/bad-poll-rep", "pollRep reason"));
|
|
397
|
+
return b.sequence(kids);
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// KeyRecRepContent ::= SEQUENCE { status PKIStatusInfo, newSigCert [0] EXPLICIT CMPCertificate OPTIONAL, caCerts [1] EXPLICIT SEQ OF CMPCertificate OPTIONAL, keyPairHist [2] EXPLICIT SEQ OF CertifiedKeyPair OPTIONAL }.
|
|
402
|
+
function _encodeKeyRecRepContent(spec, state) {
|
|
403
|
+
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("cmp/bad-key-rec-rep", "krp must be an object { status, newSigCert?, caCerts?, keyPairHist? }");
|
|
404
|
+
if (spec.status == null) throw _err("cmp/bad-key-rec-rep", "krp.status is required");
|
|
405
|
+
var children = [_encodePkiStatusInfo(spec.status, "cmp/bad-key-rec-rep")];
|
|
406
|
+
if (spec.newSigCert != null) children.push(b.explicit(0, _certRaw(spec.newSigCert, "cmp/bad-key-rec-rep", "krp.newSigCert")));
|
|
407
|
+
if (spec.caCerts != null) {
|
|
408
|
+
if (!Array.isArray(spec.caCerts) || !spec.caCerts.length) throw _err("cmp/bad-key-rec-rep", "krp.caCerts must be a non-empty array of certificate DERs");
|
|
409
|
+
children.push(b.explicit(1, b.sequence(spec.caCerts.map(function (c) { return _certRaw(c, "cmp/bad-key-rec-rep", "a krp.caCerts entry"); }))));
|
|
410
|
+
}
|
|
411
|
+
if (spec.keyPairHist != null) {
|
|
412
|
+
if (!Array.isArray(spec.keyPairHist) || !spec.keyPairHist.length) throw _err("cmp/bad-key-rec-rep", "krp.keyPairHist must be a non-empty array of CertifiedKeyPair");
|
|
413
|
+
children.push(b.explicit(2, b.sequence(spec.keyPairHist.map(function (ckp) { return _encodeCertifiedKeyPair(ckp, state); }))));
|
|
414
|
+
}
|
|
415
|
+
return b.sequence(children);
|
|
416
|
+
}
|
|
417
|
+
|
|
267
418
|
// Dispatch the single-key body object to its EXPLICIT-tagged arm. Returns { bodyTLV, usesCmp2021 }.
|
|
268
419
|
function _encodeBody(bodySpec, key, opts) {
|
|
269
420
|
if (!bodySpec || typeof bodySpec !== "object" || Buffer.isBuffer(bodySpec)) throw _err("cmp/bad-input", "message.body must be a single-key object");
|
|
@@ -299,8 +450,23 @@ function _encodeBody(bodySpec, key, opts) {
|
|
|
299
450
|
inner = _encodePollReqContent(bodySpec.pollReq);
|
|
300
451
|
} else if (arm === "genm") {
|
|
301
452
|
inner = _encodeGenMsgContent(bodySpec.genm);
|
|
302
|
-
} else
|
|
453
|
+
} else if (arm === "rr") {
|
|
303
454
|
inner = _encodeRevReqContent(bodySpec.rr);
|
|
455
|
+
} else if (CERT_REP_ARM[arm]) { // ip / cp / kup / ccp -- CertRepMessage
|
|
456
|
+
inner = _encodeCertRepMessage(bodySpec[arm], state, arm);
|
|
457
|
+
} else if (arm === "rp") {
|
|
458
|
+
inner = _encodeRevRepContent(bodySpec.rp);
|
|
459
|
+
} else if (arm === "genp") { // GenRepContent == GenMsgContent
|
|
460
|
+
inner = _encodeGenMsgContent(bodySpec.genp);
|
|
461
|
+
} else if (arm === "error") {
|
|
462
|
+
inner = _encodeErrorMsgContent(bodySpec.error);
|
|
463
|
+
} else if (arm === "pollRep") {
|
|
464
|
+
inner = _encodePollRepContent(bodySpec.pollRep);
|
|
465
|
+
} else if (arm === "krp") {
|
|
466
|
+
inner = _encodeKeyRecRepContent(bodySpec.krp, state);
|
|
467
|
+
} else { // pkiconf -- PKIConfirmContent ::= NULL
|
|
468
|
+
if (bodySpec.pkiconf !== null && bodySpec.pkiconf !== true) throw _err("cmp/bad-input", "pkiconf takes null or true (PKIConfirmContent is NULL)");
|
|
469
|
+
inner = b.nullValue();
|
|
304
470
|
}
|
|
305
471
|
return Promise.resolve({ bodyTLV: b.explicit(tag, inner), usesCmp2021: state.usesCmp2021 });
|
|
306
472
|
}
|
|
@@ -463,9 +629,13 @@ function _collectExtraCerts(opts, protCertDer) {
|
|
|
463
629
|
* Build an RFC 9810 CMP `PKIMessage` -- the producing-side inverse of `pki.schema.cmp.parse`. `message` is
|
|
464
630
|
* `{ header, body }`: `header` carries the `sender` / `recipient` GeneralNames plus optional transaction
|
|
465
631
|
* metadata (`transactionID`, `senderNonce`, `messageTime`, `freeText`, `generalInfo`, ...); `body` is a
|
|
466
|
-
* single-key object naming the
|
|
467
|
-
*
|
|
468
|
-
* `{ certConf }`, `{ pollReq }`, `{ genm }`,
|
|
632
|
+
* single-key object naming the arm. Request-side: `{ ir }` / `{ cr }` / `{ kur }` (a `CertReqMessages` spec
|
|
633
|
+
* delegated to `pki.crmf.build`; the proof-of-possession key is `key` on the arm spec), `{ p10cr }` (a PKCS#10
|
|
634
|
+
* CertificationRequest DER), `{ certConf }`, `{ pollReq }`, `{ genm }`, `{ rr }`. CA/responder-side: `{ ip }` /
|
|
635
|
+
* `{ cp }` / `{ kup }` / `{ ccp }` (a `CertRepMessage` -- `caPubs` + `response` of `CertResponse` each with a
|
|
636
|
+
* `PKIStatusInfo` and, under a granting status, a `certifiedKeyPair`), `{ rp }` (`RevRepContent`), `{ genp }`,
|
|
637
|
+
* `{ error }` (`ErrorMsgContent`), `{ pollRep }`, `{ krp }` (`KeyRecRepContent`), `{ pkiconf }` (NULL). The
|
|
638
|
+
* message is protected: `opts` carries EXACTLY
|
|
469
639
|
* ONE of `{ key, cert }` (a signature under the sender key over the message -- any registry algorithm, RSA
|
|
470
640
|
* / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite, resolved from the certificate) or `{ mac }` (a PBMAC1
|
|
471
641
|
* shared-secret MAC). The protection is computed over the exact DER of the virtual
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:552d3506-7a4d-4927-bd2e-3faadaa00c02",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-17T19:
|
|
8
|
+
"timestamp": "2026-07-17T19:50:12.519Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.3.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.3.6",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.6",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.3.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.3.6",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.3.
|
|
57
|
+
"ref": "@blamejs/pki@0.3.6",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|