@blamejs/pki 0.3.4 → 0.3.5

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,15 @@ 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.5 — 2026-07-17
8
+
9
+ 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.
10
+
11
+ ### Added
12
+
13
+ - pki.cmp.build(message, opts) assembles a protected RFC 9810 CMP PKIMessage. message.header carries the sender / recipient GeneralNames (including the anonymous NULL-DN) and optional transaction metadata; message.body is a single-key arm -- ir / cr / kur (a CertReqMessages via pki.crmf.build), p10cr (a PKCS#10 CertificationRequest), certConf, pollReq, genm, or rr. Protection is exactly one of opts.{ key, cert } (a signature under the sender key, algorithm resolved from the certificate -- RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite) or opts.mac (a PBMAC1 shared-secret HMAC, RFC 9481 / RFC 9579), computed over the exact ProtectedPart DER and self-verified before return. Returns DER, or a PEM CMP block with opts.pem; malformed input throws a typed CmpError. Message parsing remains pki.schema.cmp.parse.
14
+ - pki.crmf.buildCertTemplate(template) encodes a bare RFC 4211 CertTemplate (subject, public key, validity, requested extensions, version) to canonical DER -- the certTemplate interior of pki.crmf.build, exposed for the CMP rr revocation body whose certDetails names the certificate to revoke.
15
+
7
16
  ## v0.3.4 — 2026-07-17
8
17
 
9
18
  pki.schema.c509.encode produces C509 CBOR certificates -- a DER X.509 certificate compresses to a compact, byte-exact-invertible type-3 C509, and a deterministic-CBOR encoder joins pki.cbor.
package/README.md CHANGED
@@ -226,6 +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 request or transaction arm — `ir` / `cr` / `kur` (a `CertReqMessages` spec delegated to `pki.crmf.build`), `p10cr` (a PKCS#10 `CertificationRequest`), `certConf`, `pollReq`, `genm`, or `rr`. 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` |
229
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` |
230
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` |
231
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/index.js CHANGED
@@ -44,6 +44,7 @@ var x509 = require("./lib/x509-sign");
44
44
  var csr = require("./lib/csr-sign");
45
45
  var attrcert = require("./lib/attrcert-sign");
46
46
  var crmf = require("./lib/crmf-sign");
47
+ var cmp = require("./lib/cmp-build");
47
48
  var merkle = require("./lib/merkle");
48
49
  var shbs = require("./lib/shbs");
49
50
  var hpke = require("./lib/hpke");
@@ -103,6 +104,10 @@ module.exports = {
103
104
  // assembles a CertReqMessages (a CertTemplate + a signature proof of possession) for a CMP
104
105
  // or EST enrollment. Parsing lives at pki.schema.crmf.parse.
105
106
  crmf: crmf,
107
+ // `cmp` is the RFC 9810 Certificate Management Protocol producing side -- pki.cmp.build assembles a
108
+ // protected PKIMessage (a certificate request / confirmation / revocation / general message).
109
+ // Parsing lives at pki.schema.cmp.parse.
110
+ cmp: cmp,
106
111
  // `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
107
112
  // pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
108
113
  // (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
@@ -0,0 +1,490 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+
5
+ /**
6
+ * @module pki.cmp
7
+ * @nav Signing
8
+ * @title Certificate management protocol messages
9
+ * @intro The RFC 9810 Certificate Management Protocol producing side. `pki.cmp.build` assembles a
10
+ * `PKIMessage` -- a `PKIHeader` (sender / recipient / transaction metadata), a `PKIBody` carrying one
11
+ * request or transaction (an `ir` / `cr` / `kur` certificate request via `pki.crmf.build`, a `p10cr`
12
+ * PKCS#10 via `pki.csr.sign`, or a `certConf` / `pollReq` / `genm` / `rr`), and an optional
13
+ * `PKIProtection` computed over the message. Protection is a signature under the sender key (any
14
+ * registry algorithm) or a PBMAC1 shared-secret MAC. The emitted message round-trips through
15
+ * `pki.schema.cmp.parse` and its protection verifies. Parsing lives at `pki.schema.cmp.parse`.
16
+ * @spec RFC 9810
17
+ * @card Build a CMP PKIMessage with signature or PBMAC1 protection.
18
+ */
19
+ //
20
+ // RFC 9810 Appendix A is DEFINITIONS EXPLICIT TAGS: every context tag in the PKIMessage envelope --
21
+ // each PKIHeader [0..8] optional, every PKIBody [n] arm, protection [0], extraCerts [1] -- is an EXPLICIT
22
+ // wrapper (asn1.build.explicit), the exact inverse of the RFC 4211 CRMF interior (IMPLICIT TAGS). The two
23
+ // IMPLICIT islands are foreign imports produced wholesale by pki.crmf.build (the CertReqMessages /
24
+ // CertTemplate interior) and embedded verbatim, never re-encoded here. Protection is computed over the
25
+ // byte-exact DER of the virtual ProtectedPart ::= SEQUENCE { header, body }, built from the same header
26
+ // and body TLVs placed in the envelope. The signature scheme resolves from the sender key through the
27
+ // shared sign-scheme registry; the Name / GeneralName / SPKI encoders and the post-sign self-check are the
28
+ // shared lib/pki-build primitives, bound to the cmp namespace.
29
+
30
+ var asn1 = require("./asn1-der");
31
+ var oid = require("./oid");
32
+ var cmp = require("./schema-cmp");
33
+ var crmf = require("./crmf-sign");
34
+ var csr = require("./schema-csr");
35
+ var x509 = require("./schema-x509");
36
+ var signScheme = require("./sign-scheme");
37
+ var pkix = require("./schema-pkix");
38
+ var pkiBuild = require("./pki-build");
39
+ var webcrypto = require("./webcrypto");
40
+ var constants = require("./constants");
41
+ var guard = require("./guard-all");
42
+ var frameworkError = require("./framework-error");
43
+
44
+ var CmpError = frameworkError.CmpError;
45
+ var b = asn1.build;
46
+ function _err(code, message, cause) { return new CmpError(code, message, cause); }
47
+ function _signE(kind, message, cause) { return new CmpError("cmp/" + kind, message, cause); }
48
+ function O(n) { return oid.byName(n); }
49
+
50
+ var NS = pkix.makeNS("cmp", CmpError, oid);
51
+ var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
52
+ var _b = pkiBuild.makeBuilder({
53
+ ErrorClass: CmpError, prefix: "cmp", O: O, NS: NS,
54
+ NAME_SCHEMA: pkix.name(NS), SPKI_SCHEMA: pkix.spki(NS), EXT_DECODERS: EXT_DECODERS,
55
+ });
56
+
57
+ var KNOWN_HEADER_KEYS = {
58
+ sender: 1, recipient: 1, pvno: 1, messageTime: 1, senderKID: 1, recipKID: 1,
59
+ transactionID: 1, senderNonce: 1, recipNonce: 1, freeText: 1, generalInfo: 1,
60
+ };
61
+ var KNOWN_BODY_KEYS = { ir: 1, cr: 1, kur: 1, p10cr: 1, certConf: 1, pollReq: 1, genm: 1, rr: 1 };
62
+ // The PKIBody arm -> its EXPLICIT context tag number (schema-cmp.js BODY_ARMS). rr is [11], NOT [15].
63
+ var BODY_TAG = { ir: 0, cr: 2, p10cr: 4, kur: 7, rr: 11, genm: 21, certConf: 24, pollReq: 25 };
64
+ var CRMF_BODY = { ir: 1, cr: 1, kur: 1 }; // arms whose content is a CertReqMessages via pki.crmf.build
65
+ var KNOWN_OPTS_KEYS = { key: 1, cert: 1, mac: 1, extraCerts: 1, pem: 1, pss: 1, digestAlgorithm: 1 };
66
+ var KNOWN_MAC_KEYS = { secret: 1, salt: 1, iterationCount: 1, prf: 1, keyLength: 1, algorithm: 1 };
67
+
68
+ var PBMAC1_DEFAULT_ITER = 100000;
69
+ var PBMAC1_DEFAULT_SALT_BYTES = 16;
70
+ var PBMAC1_DEFAULT_KEYLEN = 32; // bytes -- HMAC-SHA256 key
71
+ var PBMAC1_MAX_KEYLEN = 1024; // bytes -- an HMAC key beyond a hash block is pointless; bound the work
72
+ var PBMAC1_MIN_ITER = 1000; // RFC 8018 sec. 4.2 recommended minimum -- reject a trivially weak count
73
+ var PBMAC1_PRF = { "SHA-256": "hmacWithSHA256", "SHA-384": "hmacWithSHA384", "SHA-512": "hmacWithSHA512" };
74
+ var PBMAC1_MAC_OID = { "SHA-256": "hmacWithSHA256", "SHA-384": "hmacWithSHA384", "SHA-512": "hmacWithSHA512" };
75
+ // PKIFailureInfo named bits (RFC 9810 sec. 5.2.3), position = bit index -- mirrors schema-cmp's decode list;
76
+ // the build -> parse round-trip cross-checks the positions against the parser.
77
+ var FAIL_INFO_NAMES = ["badAlg", "badMessageCheck", "badRequest", "badTime", "badCertId", "badDataFormat",
78
+ "wrongAuthority", "incorrectData", "missingTimeStamp", "badPOP", "certRevoked", "certConfirmed",
79
+ "wrongIntegrity", "badRecipientNonce", "timeNotAvailable", "unacceptedPolicy", "unacceptedExtension",
80
+ "addInfoNotAvailable", "badSenderNonce", "badCertTemplate", "signerNotTrusted", "transactionIdInUse",
81
+ "unsupportedVersion", "notAuthorized", "systemUnavail", "systemFailure", "duplicateCertReq"];
82
+ var FAIL_INFO_INDEX = {};
83
+ FAIL_INFO_NAMES.forEach(function (n, i) { FAIL_INFO_INDEX[n] = i; });
84
+ // CertStatus.hashAlg names the hash used to compute certHash -- restrict it to hash algorithms, not any OID.
85
+ var CERT_CONF_HASH_ALGS = { sha1: 1, sha256: 1, sha384: 1, sha512: 1, "sha3-256": 1, "sha3-512": 1 };
86
+
87
+ // ---- small shared encoders (byte-exact inverses of schema-cmp.js readers) ----
88
+
89
+ function _reqOctets(v, what) {
90
+ var buf = _b.reqDer(v, what);
91
+ return b.octetString(buf);
92
+ }
93
+
94
+ // PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String -- non-empty, every element UTF8String.
95
+ function _encodePkiFreeText(strings, code, what) {
96
+ if (!Array.isArray(strings) || !strings.length) throw _err(code, what + " must be a non-empty array of strings");
97
+ return b.sequence(strings.map(function (s) {
98
+ if (typeof s !== "string") throw _err(code, what + " entries must be strings");
99
+ return b.utf8(s);
100
+ }));
101
+ }
102
+
103
+ // A recognized fixed-syntax id-it value (RFC 9810 sec. 5.1.1.1/.2/.4): implicitConfirm -> NULL,
104
+ // confirmWaitTime -> GeneralizedTime, certProfile -> non-empty SEQUENCE OF UTF8String. An unknown id-it
105
+ // carries a pre-encoded infoValue DER (or none).
106
+ function _encodeInfoValue(itav) {
107
+ var name = itav.infoType;
108
+ if (name === "implicitConfirm") {
109
+ if (itav.infoValue != null) throw _err("cmp/bad-info-value", "implicitConfirm carries a NULL infoValue");
110
+ return b.nullValue();
111
+ }
112
+ if (name === "confirmWaitTime") {
113
+ guard.time.assertValid(itav.infoValue, _err, "cmp/bad-info-value", "confirmWaitTime infoValue"); // reject a non-Date / Invalid Date
114
+ return b.generalizedTime(itav.infoValue);
115
+ }
116
+ if (name === "certProfile") {
117
+ return _encodePkiFreeText(itav.infoValue, "cmp/bad-info-value", "certProfile");
118
+ }
119
+ if (itav.infoValue == null) return null;
120
+ return b.raw(_b.reqDer(itav.infoValue, "infoValue (a pre-encoded DER value)"));
121
+ }
122
+
123
+ // InfoTypeAndValue ::= SEQUENCE { infoType OID, infoValue ANY DEFINED BY infoType OPTIONAL }.
124
+ function _encodeInfoTypeAndValue(itav) {
125
+ if (!itav || typeof itav !== "object" || Buffer.isBuffer(itav)) throw _err("cmp/bad-info-value", "an InfoTypeAndValue must be an object { infoType, infoValue? }");
126
+ if (typeof itav.infoType !== "string" || O(itav.infoType) == null) throw _err("cmp/bad-name", "unknown infoType " + JSON.stringify(itav.infoType));
127
+ var children = [b.oid(O(itav.infoType))];
128
+ var val = _encodeInfoValue(itav);
129
+ if (val != null) children.push(val);
130
+ return b.sequence(children);
131
+ }
132
+
133
+ // generalInfo [8] / genm content: SEQUENCE SIZE (1..MAX) OF InfoTypeAndValue.
134
+ function _encodeGeneralInfo(itavs, code, what) {
135
+ if (!Array.isArray(itavs) || !itavs.length) throw _err(code, what + " must be a non-empty array of InfoTypeAndValue");
136
+ return b.sequence(itavs.map(_encodeInfoTypeAndValue));
137
+ }
138
+
139
+ // ---- PKIHeader ----
140
+
141
+ // An AlgorithmIdentifier { algorithm OID, parameters ANY OPTIONAL } for a bare/absent-params digest.
142
+ function _algIdNoParams(name) { return b.sequence([b.oid(O(name))]); }
143
+ // An HMAC AlgorithmIdentifier carries NULL parameters (RFC 8018 App. B.1.1 / RFC 4231), unlike a bare SHA-2
144
+ // digest algId (ABSENT parameters, RFC 5754 sec. 2). Used for the PBMAC1 prf + messageAuthScheme.
145
+ function _hmacAlgId(name) { return b.sequence([b.oid(O(name)), b.nullValue()]); }
146
+
147
+ // PKIHeader ::= SEQUENCE { pvno INTEGER, sender GeneralName, recipient GeneralName, EXPLICIT [0..8] }.
148
+ // protectionAlgDer is the DERIVED AlgorithmIdentifier ([1]), present iff the message is protected. Returns
149
+ // the exact headerTLV, built ONCE and reused in both the envelope and the ProtectedPart (RFC 9810 5.1.3).
150
+ function _encodeHeader(headerSpec, protectionAlgDer, pvno) {
151
+ if (!headerSpec || typeof headerSpec !== "object" || Buffer.isBuffer(headerSpec)) throw _err("cmp/bad-input", "message.header must be an object");
152
+ Object.keys(headerSpec).forEach(function (k) { if (!KNOWN_HEADER_KEYS[k]) throw _err("cmp/bad-input", "unknown header field " + JSON.stringify(k)); });
153
+ if (headerSpec.sender == null) throw _err("cmp/bad-input", "message.header.sender is required (GeneralName)");
154
+ if (headerSpec.recipient == null) throw _err("cmp/bad-input", "message.header.recipient is required (GeneralName)");
155
+
156
+ var children = [
157
+ b.integer(BigInt(pvno)),
158
+ _b.encodeGeneralName(headerSpec.sender),
159
+ _b.encodeGeneralName(headerSpec.recipient),
160
+ ];
161
+ // EXPLICIT [0..8] optionals, ascending, at most once each.
162
+ if (headerSpec.messageTime != null) {
163
+ guard.time.assertValid(headerSpec.messageTime, _err, "cmp/bad-input", "header.messageTime"); // reject a non-Date / Invalid Date
164
+ children.push(b.explicit(0, b.generalizedTime(headerSpec.messageTime))); // GeneralizedTime ONLY (never UTCTime)
165
+ }
166
+ if (protectionAlgDer != null) children.push(b.explicit(1, b.raw(protectionAlgDer)));
167
+ if (headerSpec.senderKID != null) children.push(b.explicit(2, _reqOctets(headerSpec.senderKID, "header.senderKID")));
168
+ if (headerSpec.recipKID != null) children.push(b.explicit(3, _reqOctets(headerSpec.recipKID, "header.recipKID")));
169
+ if (headerSpec.transactionID != null) children.push(b.explicit(4, _reqOctets(headerSpec.transactionID, "header.transactionID")));
170
+ if (headerSpec.senderNonce != null) children.push(b.explicit(5, _reqOctets(headerSpec.senderNonce, "header.senderNonce")));
171
+ if (headerSpec.recipNonce != null) children.push(b.explicit(6, _reqOctets(headerSpec.recipNonce, "header.recipNonce")));
172
+ if (headerSpec.freeText != null) children.push(b.explicit(7, _encodePkiFreeText(headerSpec.freeText, "cmp/bad-freetext", "header.freeText")));
173
+ if (headerSpec.generalInfo != null) children.push(b.explicit(8, _encodeGeneralInfo(headerSpec.generalInfo, "cmp/bad-general-info", "header.generalInfo")));
174
+ return b.sequence(children);
175
+ }
176
+
177
+ // ---- PKIBody arm content encoders ----
178
+
179
+ // A certReqId is an UNBOUNDED INTEGER (RFC 9483 -1 sentinel, no upper bound): accept a safe-integer number
180
+ // or a bigint (a large value beyond 2^53), reject a non-integer / other type. Returns a BigInt for b.integer.
181
+ function _reqIdInt(v, code, what) {
182
+ if (typeof v === "bigint") return v;
183
+ // Number.isSafeInteger (not isInteger): a value above 2^53 is imprecise as a Number, so it MUST be a bigint.
184
+ if (typeof v === "number" && Number.isSafeInteger(v)) return BigInt(v);
185
+ throw _err(code, what + " must be an integer (a safe-integer number, or a bigint for a large value)");
186
+ }
187
+
188
+ // 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("cmp/bad-cert-status", "statusInfo.failInfo must be an array of PKIFailureInfo bit names");
191
+ return b.namedBitString(names.map(function (n) {
192
+ if (typeof n !== "string" || FAIL_INFO_INDEX[n] === undefined) throw _err("cmp/bad-cert-status", "unknown PKIFailureInfo bit " + JSON.stringify(n));
193
+ return FAIL_INFO_INDEX[n];
194
+ }));
195
+ }
196
+
197
+ // PKIStatusInfo ::= SEQUENCE { status PKIStatus INTEGER, statusString PKIFreeText OPTIONAL, failInfo BIT STRING OPTIONAL }.
198
+ function _encodePkiStatusInfo(si) {
199
+ if (typeof si.status !== "number" || !Number.isInteger(si.status)) throw _err("cmp/bad-cert-status", "statusInfo.status must be a PKIStatus integer");
200
+ var children = [b.integer(BigInt(si.status))];
201
+ if (si.statusString != null) children.push(_encodePkiFreeText(si.statusString, "cmp/bad-cert-status", "statusInfo.statusString"));
202
+ if (si.failInfo != null) children.push(_encodeFailInfo(si.failInfo)); // strict order: status -> statusString -> failInfo (both optionals untagged)
203
+ return b.sequence(children);
204
+ }
205
+
206
+ // CertStatus ::= SEQUENCE { certHash OCTET STRING, certReqId INTEGER, statusInfo PKIStatusInfo OPTIONAL,
207
+ // hashAlg [0] EXPLICIT AlgorithmIdentifier OPTIONAL }. hashAlg present => cmp2021 (pvno bump).
208
+ function _encodeCertStatus(cs, state) {
209
+ if (!cs || typeof cs !== "object" || Buffer.isBuffer(cs)) throw _err("cmp/bad-cert-status", "each CertStatus must be an object");
210
+ var children = [
211
+ _reqOctets(cs.certHash, "certConf certHash"),
212
+ b.integer(_reqIdInt(cs.certReqId, "cmp/bad-cert-status", "CertStatus certReqId")), // signed, -1 legal, unbounded
213
+ ];
214
+ if (cs.statusInfo != null) children.push(_encodePkiStatusInfo(cs.statusInfo));
215
+ if (cs.hashAlg != null) {
216
+ 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
+ children.push(b.explicit(0, _algIdNoParams(cs.hashAlg)));
218
+ state.usesCmp2021 = true;
219
+ }
220
+ return b.sequence(children);
221
+ }
222
+
223
+ // CertConfirmContent ::= SEQUENCE OF CertStatus (empty legal, no SIZE floor).
224
+ function _encodeCertConfirmContent(list, state) {
225
+ if (!Array.isArray(list)) throw _err("cmp/bad-cert-status", "certConf must be an array of CertStatus");
226
+ return b.sequence(list.map(function (cs) { return _encodeCertStatus(cs, state); }));
227
+ }
228
+
229
+ // PollReqContent ::= SEQUENCE OF SEQUENCE { certReqId INTEGER } (-1 legal).
230
+ function _encodePollReqContent(list) {
231
+ if (!Array.isArray(list)) throw _err("cmp/bad-poll-req", "pollReq must be an array of { certReqId }");
232
+ return b.sequence(list.map(function (pr) {
233
+ if (!pr || typeof pr !== "object" || Buffer.isBuffer(pr)) throw _err("cmp/bad-poll-req", "each pollReq entry must be { certReqId: <integer> }");
234
+ return b.sequence([b.integer(_reqIdInt(pr.certReqId, "cmp/bad-poll-req", "pollReq certReqId"))]);
235
+ }));
236
+ }
237
+
238
+ // GenMsgContent ::= SEQUENCE OF InfoTypeAndValue (empty legal).
239
+ function _encodeGenMsgContent(list) {
240
+ if (!Array.isArray(list)) throw _err("cmp/bad-info-type-and-value", "genm must be an array of InfoTypeAndValue");
241
+ return b.sequence(list.map(_encodeInfoTypeAndValue));
242
+ }
243
+
244
+ // RevReqContent ::= SEQUENCE OF RevDetails { certDetails CertTemplate, crlEntryDetails Extensions OPTIONAL }.
245
+ function _encodeRevDetails(rd) {
246
+ if (!rd || typeof rd !== "object" || Buffer.isBuffer(rd)) throw _err("cmp/bad-rev-req", "each RevDetails must be an object { certDetails, crlEntryDetails? }");
247
+ if (rd.certDetails == null) throw _err("cmp/bad-rev-req", "RevDetails.certDetails (a CertTemplate) is required");
248
+ var certDetails;
249
+ if (Buffer.isBuffer(rd.certDetails) || rd.certDetails instanceof Uint8Array) {
250
+ certDetails = b.raw(_b.reqDer(rd.certDetails, "RevDetails.certDetails (a pre-encoded CertTemplate DER)"));
251
+ } else {
252
+ // A revocation must identify the certificate to revoke by issuer + serialNumber (RFC 9810 App. D sec.
253
+ // 5.3.2); the pre-encoded-CertTemplate hatch above is the escape for an advanced identification.
254
+ if (rd.certDetails.issuer == null || rd.certDetails.serialNumber == null) throw _err("cmp/bad-rev-req", "a revocation certDetails must identify the certificate by issuer and serialNumber");
255
+ certDetails = b.raw(crmf.buildCertTemplate(rd.certDetails));
256
+ }
257
+ var children = [certDetails];
258
+ if (rd.crlEntryDetails != null) children.push(b.raw(_b.reqDer(rd.crlEntryDetails, "RevDetails.crlEntryDetails (a pre-encoded Extensions DER)")));
259
+ return b.sequence(children);
260
+ }
261
+
262
+ function _encodeRevReqContent(list) {
263
+ if (!Array.isArray(list) || !list.length) throw _err("cmp/bad-rev-req", "rr must be a non-empty array of RevDetails");
264
+ return b.sequence(list.map(_encodeRevDetails));
265
+ }
266
+
267
+ // Dispatch the single-key body object to its EXPLICIT-tagged arm. Returns { bodyTLV, usesCmp2021 }.
268
+ function _encodeBody(bodySpec, key, opts) {
269
+ if (!bodySpec || typeof bodySpec !== "object" || Buffer.isBuffer(bodySpec)) throw _err("cmp/bad-input", "message.body must be a single-key object");
270
+ var keys = Object.keys(bodySpec);
271
+ if (keys.length !== 1) throw _err("cmp/bad-input", "message.body must have exactly one arm, got " + keys.length);
272
+ var arm = keys[0];
273
+ if (!KNOWN_BODY_KEYS[arm]) throw _err("cmp/bad-input", "unknown body arm " + JSON.stringify(arm));
274
+ var tag = BODY_TAG[arm], state = { usesCmp2021: false };
275
+
276
+ if (CRMF_BODY[arm]) {
277
+ // The CRMF proof of possession is signed with the REQUESTED key's private half, which is distinct from
278
+ // the message-protection key. Take it from a `key` field on the request spec; default to the protection
279
+ // key only for a self-request (the client certifying the very key it protects with).
280
+ var reqSpec = bodySpec[arm], popKey = key;
281
+ if (reqSpec && typeof reqSpec === "object" && !Buffer.isBuffer(reqSpec) && "key" in reqSpec) {
282
+ popKey = reqSpec.key;
283
+ reqSpec = Object.assign({}, reqSpec);
284
+ delete reqSpec.key;
285
+ }
286
+ return crmf.build(reqSpec, popKey, {}).then(function (crmfDer) {
287
+ return { bodyTLV: b.explicit(tag, b.raw(crmfDer)), usesCmp2021: false };
288
+ });
289
+ }
290
+ var inner;
291
+ if (arm === "p10cr") {
292
+ var csrDer = _b.reqDer(bodySpec.p10cr, "p10cr (a CertificationRequest DER)");
293
+ try { csr.parse(csrDer); } // validate the embedded PKCS#10 via the real parser (fail-closed on a non-CSR)
294
+ catch (e) { if (e instanceof CmpError) throw e; throw _err("cmp/bad-input", "p10cr is not a valid PKCS#10 CertificationRequest", e); }
295
+ inner = b.raw(csrDer);
296
+ } else if (arm === "certConf") {
297
+ inner = _encodeCertConfirmContent(bodySpec.certConf, state);
298
+ } else if (arm === "pollReq") {
299
+ inner = _encodePollReqContent(bodySpec.pollReq);
300
+ } else if (arm === "genm") {
301
+ inner = _encodeGenMsgContent(bodySpec.genm);
302
+ } else { // rr
303
+ inner = _encodeRevReqContent(bodySpec.rr);
304
+ }
305
+ return Promise.resolve({ bodyTLV: b.explicit(tag, inner), usesCmp2021: state.usesCmp2021 });
306
+ }
307
+
308
+ // ---- protection ----
309
+
310
+ // PBMAC1 protectionAlg: id-PBMAC1 with PBMAC1-params { keyDerivationFunc PBKDF2{salt,iter,keyLength,prf},
311
+ // messageAuthScheme HMAC } -- the byte-exact inverse of schema-pkcs12.js PBMAC1_PARAMS / PBKDF2_PARAMS.
312
+ function _encodePbmac1AlgId(pd) {
313
+ var pbkdf2Params = [b.octetString(pd.salt), b.integer(BigInt(pd.iterationCount)), b.integer(BigInt(pd.keyLength))];
314
+ // prf: omit iff the DEFAULT algid-hmacWithSHA1; else the prf AlgorithmIdentifier with ABSENT params.
315
+ // nosemgrep: pki-non-constant-time-secret-compare -- prfName is a PRF algorithm name, not a secret
316
+ if (pd.prfName !== "hmacWithSHA1") pbkdf2Params.push(_hmacAlgId(pd.prfName));
317
+ var pbkdf2AlgId = b.sequence([b.oid(O("pbkdf2")), b.sequence(pbkdf2Params)]);
318
+ var pbmac1Params = b.sequence([pbkdf2AlgId, _hmacAlgId(pd.macName)]);
319
+ return b.sequence([b.oid(O("pbmac1")), pbmac1Params]);
320
+ }
321
+
322
+ // Resolve the protection selector to { protectionAlgDer, computeBits(protectedPartDer)->Promise<Buffer> },
323
+ // senderSpki, senderScheme } BEFORE the header is built (protectionAlg is opts-derived, not header-derived).
324
+ function _resolveProtection(opts) {
325
+ var hasSig = opts.key != null || opts.cert != null;
326
+ var hasMac = opts.mac != null;
327
+ if (hasSig && hasMac) throw _err("cmp/bad-input", "supply exactly one of { key, cert } (signature) or { mac } (PBMAC1), not both");
328
+ if (!hasSig && !hasMac) throw _err("cmp/bad-input", "a PKIMessage requires protection: supply { key, cert } for a signature or { mac } for PBMAC1");
329
+
330
+ if (hasSig) {
331
+ if (opts.key == null || opts.cert == null) throw _err("cmp/bad-input", "signature protection requires both opts.key (the private key) and opts.cert (the signer certificate)");
332
+ var certDer = _b.reqDer(opts.cert, "opts.cert (the signer certificate DER)");
333
+ var senderSpki;
334
+ try { senderSpki = x509.parse(certDer).subjectPublicKeyInfo.bytes; }
335
+ catch (e) { throw _err("cmp/bad-input", "opts.cert is not a valid X.509 certificate", e); }
336
+ _b.assertValidSpki(senderSpki, "the sender certificate SPKI");
337
+ var scheme = signScheme.resolveSignScheme(_b.certLikeFromSpki(senderSpki), { combinedRsaSig: true, pss: opts.pss, digestAlgorithm: opts.digestAlgorithm }, true, _signE);
338
+ return {
339
+ protectionAlgDer: scheme.sigAlgId,
340
+ certDer: certDer,
341
+ computeBits: function (protectedPartDer) {
342
+ return Promise.resolve(signScheme.signOverTbs(scheme, opts.key, protectedPartDer, _signE)).then(function (sig) {
343
+ return Promise.resolve(_b.assertSignatureVerifies(protectedPartDer, sig, senderSpki, scheme)).then(function () {
344
+ return b.bitString(sig, 0);
345
+ });
346
+ });
347
+ },
348
+ };
349
+ }
350
+
351
+ // PBMAC1
352
+ var m = opts.mac;
353
+ if (!m || typeof m !== "object" || Buffer.isBuffer(m)) throw _err("cmp/bad-input", "opts.mac must be an object { secret, salt?, iterationCount?, prf?, keyLength? }");
354
+ Object.keys(m).forEach(function (k) { if (!KNOWN_MAC_KEYS[k]) throw _err("cmp/bad-input", "unknown opts.mac field " + JSON.stringify(k)); });
355
+ if (m.algorithm != null && m.algorithm !== "pbmac1") throw _err("cmp/unsupported-algorithm", "opts.mac.algorithm " + JSON.stringify(m.algorithm) + " is not supported (v1 ships pbmac1; passwordBasedMac is deferred)");
356
+ var secret = m.secret;
357
+ if (typeof secret !== "string" || !secret) {
358
+ if (!Buffer.isBuffer(secret) || !secret.length) throw _err("cmp/bad-input", "opts.mac.secret must be a non-empty string or Buffer");
359
+ }
360
+ var secretBuf = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, "utf8");
361
+ var prf = m.prf || "SHA-256";
362
+ if (!PBMAC1_PRF[prf]) throw _err("cmp/bad-input", "opts.mac.prf must be SHA-256 / SHA-384 / SHA-512");
363
+ var iterationCount = m.iterationCount != null ? m.iterationCount : PBMAC1_DEFAULT_ITER;
364
+ if (typeof iterationCount !== "number" || !Number.isInteger(iterationCount) || iterationCount < PBMAC1_MIN_ITER) throw _err("cmp/bad-input", "opts.mac.iterationCount must be at least " + PBMAC1_MIN_ITER + " (RFC 8018 sec. 4.2)");
365
+ // Bound the PBKDF2 work factors BEFORE deriving -- a huge iterationCount / keyLength is self-inflicted work.
366
+ if (iterationCount > constants.LIMITS.PBKDF2_MAX_ITERATIONS) throw _err("cmp/bad-input", "opts.mac.iterationCount exceeds the PBKDF2 work-factor cap " + constants.LIMITS.PBKDF2_MAX_ITERATIONS);
367
+ var keyLength = m.keyLength != null ? m.keyLength : PBMAC1_DEFAULT_KEYLEN;
368
+ if (typeof keyLength !== "number" || !Number.isInteger(keyLength) || keyLength < 1) throw _err("cmp/bad-input", "opts.mac.keyLength must be a positive integer (bytes)");
369
+ if (keyLength > PBMAC1_MAX_KEYLEN) throw _err("cmp/bad-input", "opts.mac.keyLength exceeds the cap " + PBMAC1_MAX_KEYLEN + " bytes");
370
+ var salt = m.salt != null ? _b.reqDer(m.salt, "opts.mac.salt") : Buffer.from(webcrypto.webcrypto.getRandomValues(new Uint8Array(PBMAC1_DEFAULT_SALT_BYTES)));
371
+ if (salt.length > constants.LIMITS.PBKDF2_MAX_SALT) throw _err("cmp/bad-input", "opts.mac.salt exceeds " + constants.LIMITS.PBKDF2_MAX_SALT + " bytes");
372
+ var macDesc = { salt: salt, iterationCount: iterationCount, keyLength: keyLength, prfName: PBMAC1_PRF[prf], macName: PBMAC1_MAC_OID[prf] };
373
+
374
+ return {
375
+ protectionAlgDer: _encodePbmac1AlgId(macDesc),
376
+ certDer: null,
377
+ computeBits: function (protectedPartDer) {
378
+ return _pbmac1(secretBuf, salt, iterationCount, keyLength, prf, protectedPartDer).then(function (mac) {
379
+ return b.bitString(mac, 0);
380
+ });
381
+ },
382
+ };
383
+ }
384
+
385
+ // PBKDF2-derive an HMAC key from the shared secret, then HMAC the ProtectedPart (both via the engine).
386
+ function _pbmac1(secretBuf, salt, iterationCount, keyLength, prf, message) {
387
+ var subtle = webcrypto.webcrypto.subtle;
388
+ return subtle.importKey("raw", secretBuf, { name: "PBKDF2" }, false, ["deriveBits"]).then(function (baseKey) {
389
+ return subtle.deriveBits({ name: "PBKDF2", salt: salt, iterations: iterationCount, hash: prf }, baseKey, keyLength * 8);
390
+ }).then(function (bits) {
391
+ return subtle.importKey("raw", Buffer.from(bits), { name: "HMAC", hash: prf }, false, ["sign"]).then(function (hmacKey) {
392
+ return subtle.sign({ name: "HMAC" }, hmacKey, message);
393
+ });
394
+ }).then(function (sig) { return Buffer.from(sig); });
395
+ }
396
+
397
+ // ---- orchestrator ----
398
+
399
+ function build(message, opts) {
400
+ return Promise.resolve().then(function () { return _build(message, opts); });
401
+ }
402
+
403
+ function _build(message, opts) {
404
+ opts = opts || {};
405
+ if (!message || typeof message !== "object" || Buffer.isBuffer(message)) throw _err("cmp/bad-input", "the PKIMessage spec must be an object { header, body }");
406
+ Object.keys(message).forEach(function (k) { if (k !== "header" && k !== "body") throw _err("cmp/bad-input", "unknown message field " + JSON.stringify(k) + " (a message carries only header + body)"); });
407
+ Object.keys(opts).forEach(function (k) { if (!KNOWN_OPTS_KEYS[k]) throw _err("cmp/bad-input", "unknown opts field " + JSON.stringify(k)); });
408
+ if (message.header == null) throw _err("cmp/bad-input", "message.header is required");
409
+ if (message.body == null) throw _err("cmp/bad-input", "message.body is required");
410
+
411
+ var prot = _resolveProtection(opts);
412
+ var senderKey = opts.key;
413
+
414
+ return Promise.resolve(_encodeBody(message.body, senderKey, opts)).then(function (bodyResult) {
415
+ var bodyTLV = bodyResult.bodyTLV;
416
+ var pvno = message.header.pvno != null ? message.header.pvno : 2;
417
+ if (typeof pvno !== "number" || !Number.isInteger(pvno)) throw _err("cmp/bad-input", "header.pvno must be an integer");
418
+ if (bodyResult.usesCmp2021 && pvno < 3) pvno = 3; // a certConf hashAlg forces cmp2021(3)
419
+
420
+ var headerTLV = _encodeHeader(message.header, prot.protectionAlgDer, pvno); // built ONCE
421
+ var protectedPartDer = b.sequence([headerTLV, bodyTLV]); // virtual ProtectedPart ::= SEQUENCE { header, body }
422
+
423
+ return prot.computeBits(protectedPartDer).then(function (protectionBits) {
424
+ var msgChildren = [headerTLV, bodyTLV, b.explicit(0, protectionBits)]; // reuse the SAME header/body TLVs
425
+ var extraCertsDers = _collectExtraCerts(opts, prot.certDer);
426
+ if (extraCertsDers.length) msgChildren.push(b.explicit(1, b.sequence(extraCertsDers)));
427
+ var der = b.sequence(msgChildren);
428
+
429
+ cmp.parse(der); // round-trip re-validation: the emitted message MUST parse (protection<=>protectionAlg, pvno, freeText, etc.)
430
+ if (opts.pem != null) {
431
+ if (opts.pem === true) return cmp.pemEncode(der, "CMP");
432
+ if (typeof opts.pem !== "string" || !opts.pem) throw _err("cmp/bad-input", "opts.pem must be true or a non-empty PEM label string");
433
+ return cmp.pemEncode(der, opts.pem);
434
+ }
435
+ return der;
436
+ });
437
+ });
438
+ }
439
+
440
+ function _collectExtraCerts(opts, protCertDer) {
441
+ var out = [];
442
+ if (protCertDer != null) out.push(b.raw(protCertDer));
443
+ if (opts.extraCerts != null) {
444
+ if (!Array.isArray(opts.extraCerts)) throw _err("cmp/bad-extra-certs", "opts.extraCerts must be an array of certificate DERs");
445
+ opts.extraCerts.forEach(function (c) {
446
+ var der = _b.reqDer(c, "extraCerts entry (a Certificate DER)");
447
+ try { x509.parse(der); } // each extraCerts entry MUST be a valid X.509 certificate (RFC 9810 sec. 5.1)
448
+ catch (e) { if (e instanceof CmpError) throw e; throw _err("cmp/bad-extra-certs", "an extraCerts entry is not a valid X.509 certificate", e); }
449
+ out.push(b.raw(der));
450
+ });
451
+ }
452
+ return out;
453
+ }
454
+
455
+ /**
456
+ * @primitive pki.cmp.build
457
+ * @signature pki.cmp.build(message, opts?) -> Promise<Buffer|string>
458
+ * @since 0.3.5
459
+ * @status experimental
460
+ * @spec RFC 9810, RFC 9481, RFC 9579
461
+ * @related pki.schema.cmp.parse
462
+ *
463
+ * Build an RFC 9810 CMP `PKIMessage` -- the producing-side inverse of `pki.schema.cmp.parse`. `message` is
464
+ * `{ header, body }`: `header` carries the `sender` / `recipient` GeneralNames plus optional transaction
465
+ * metadata (`transactionID`, `senderNonce`, `messageTime`, `freeText`, `generalInfo`, ...); `body` is a
466
+ * single-key object naming the request or transaction arm -- `{ ir }` / `{ cr }` / `{ kur }` (a
467
+ * `CertReqMessages` spec delegated to `pki.crmf.build`), `{ p10cr }` (a PKCS#10 CertificationRequest DER),
468
+ * `{ certConf }`, `{ pollReq }`, `{ genm }`, or `{ rr }`. The message is protected: `opts` carries EXACTLY
469
+ * ONE of `{ key, cert }` (a signature under the sender key over the message -- any registry algorithm, RSA
470
+ * / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite, resolved from the certificate) or `{ mac }` (a PBMAC1
471
+ * shared-secret MAC). The protection is computed over the exact DER of the virtual
472
+ * `ProtectedPart ::= SEQUENCE { header, body }` and self-verified before the message is returned. The
473
+ * emitted PKIMessage round-trips byte-identically through `pki.schema.cmp.parse`.
474
+ *
475
+ * @opts
476
+ * - `key` (Buffer|CryptoKey) + `cert` (Buffer) -- signature protection under the sender key; `cert` is
477
+ * the signer certificate (its SPKI resolves the algorithm) and is placed in `extraCerts`.
478
+ * - `mac` ({ secret, salt?, iterationCount?, prf?, keyLength? }) -- PBMAC1 protection from a shared secret.
479
+ * - `extraCerts` (array of Buffer) -- additional certificates to carry in `extraCerts [1]`.
480
+ * - `pem` (boolean|string) -- return a PEM `CMP` block instead of DER.
481
+ * - `pss` (boolean) / `digestAlgorithm` (string) -- signature-protection algorithm options.
482
+ *
483
+ * @example
484
+ * var der = await pki.cmp.build(
485
+ * { header: { sender: { directoryName: "CN=client" }, recipient: { directoryName: "CN=CA" } },
486
+ * body: { p10cr: csrDer } },
487
+ * { key: signerKeyPkcs8, cert: signerCertDer });
488
+ * pki.schema.cmp.parse(der).body.arm; // "p10cr"
489
+ */
490
+ module.exports = { build: build };
package/lib/crmf-sign.js CHANGED
@@ -47,6 +47,7 @@ var _b = pkiBuild.makeBuilder({
47
47
 
48
48
  var KNOWN_SPEC_KEYS = { certReqId: 1, certTemplate: 1, controls: 1, regInfo: 1, pop: 1 };
49
49
  var KNOWN_TEMPLATE_KEYS = { version: 1, subject: 1, publicKey: 1, validity: 1, extensions: 1, issuer: 1 };
50
+ var REVOCATION_TEMPLATE_KEYS = { version: 1, subject: 1, publicKey: 1, validity: 1, extensions: 1, issuer: 1, serialNumber: 1 };
50
51
  // The controls (RFC 4211 sec. 6) and regInfo (sec. 7) are DISJOINT AttributeTypeAndValue namespaces --
51
52
  // regToken/authenticator/oldCertID/protocolEncrKey are controls; utf8Pairs is regInfo -- so the object
52
53
  // form validates each key against its own field's registry (a control name is not a valid regInfo, and
@@ -77,14 +78,18 @@ function _encodeOptionalValidity(validity) {
77
78
  // CertTemplate ::= SEQUENCE { [0..9] all IMPLICIT OPTIONAL }. A REQUEST omits serialNumber [1] / signingAlg
78
79
  // [2] / issuerUID [7] / subjectUID [8] (CA-assigned or deprecated, RFC 4211 sec. 5) -- the builder never
79
80
  // emits them. Fields are emitted in ascending tag order.
80
- function _encodeCertTemplate(tpl) {
81
+ function _encodeCertTemplate(tpl, opts) {
81
82
  if (!tpl || typeof tpl !== "object" || Buffer.isBuffer(tpl)) throw _err("crmf/bad-cert-template", "certTemplate must be an object");
82
- Object.keys(tpl).forEach(function (k) { if (!KNOWN_TEMPLATE_KEYS[k]) throw _err("crmf/bad-input", "unknown certTemplate field " + JSON.stringify(k)); });
83
+ // A REQUEST template omits serialNumber (CA-assigned, RFC 4211 sec. 5); a REVOCATION template (CMP rr,
84
+ // RFC 9810 sec. 5.3.9) carries serialNumber [1] to name the certificate to revoke -- allowed only then.
85
+ var allowed = (opts && opts.revocation) ? REVOCATION_TEMPLATE_KEYS : KNOWN_TEMPLATE_KEYS;
86
+ Object.keys(tpl).forEach(function (k) { if (!allowed[k]) throw _err("crmf/bad-input", "unknown certTemplate field " + JSON.stringify(k)); });
83
87
  var fields = [];
84
88
  if (tpl.version != null) {
85
89
  if (tpl.version !== 2) throw _err("crmf/bad-version", "certTemplate version MUST be 2 (v3) if supplied (RFC 4211 sec. 5)");
86
90
  fields.push(b.implicit(0, b.integer(2n))); // version [0]
87
91
  }
92
+ if (tpl.serialNumber != null) fields.push(b.implicit(1, _b.serialInteger(tpl.serialNumber))); // serialNumber [1] (revocation only)
88
93
  // issuer [3] / subject [5] are EXPLICIT: Name is a CHOICE, and X.680 sec. 31.2.7 forces a context tag on
89
94
  // a CHOICE to EXPLICIT even under the module's IMPLICIT TAGS default (the [3]/[5] wraps the RDNSequence
90
95
  // SEQUENCE, it does not replace its tag). The parser accepts both forms; this is the conformant one.
@@ -272,4 +277,24 @@ function _build(spec, key, opts) {
272
277
  });
273
278
  }
274
279
 
275
- module.exports = { build: build };
280
+ /**
281
+ * @primitive pki.crmf.buildCertTemplate
282
+ * @signature pki.crmf.buildCertTemplate(template) -> Buffer
283
+ * @since 0.3.5
284
+ * @status experimental
285
+ * @spec RFC 4211
286
+ * @related pki.crmf.build
287
+ *
288
+ * Encode a bare RFC 4211 `CertTemplate` (the requested-certificate fields -- `subject`, `publicKey`,
289
+ * `validity`, requested `extensions`, an optional `version` 2, `issuer`) to canonical DER. This is the
290
+ * `certTemplate` interior of `pki.crmf.build`, exposed for the RFC 9810 CMP `rr` (revocation request) body,
291
+ * whose `RevDetails.certDetails` carries a `CertTemplate` naming the certificate to revoke. Returns the DER
292
+ * `Buffer`; a malformed template throws a typed `CrmfError`.
293
+ *
294
+ * @example
295
+ * var tpl = pki.crmf.buildCertTemplate({ serialNumber: 42n, issuer: "CN=CA" });
296
+ * pki.asn1.decode(tpl).tagNumber === pki.asn1.TAGS.SEQUENCE; // the CertTemplate SEQUENCE
297
+ */
298
+ function buildCertTemplate(template) { return _encodeCertTemplate(template, { revocation: true }).der; }
299
+
300
+ module.exports = { build: build, buildCertTemplate: buildCertTemplate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:4cfd38cc-6772-42e6-bed2-46e04c2a950e",
5
+ "serialNumber": "urn:uuid:9ded865a-a922-4de1-a8f0-bd02d3b29a2e",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-17T17:26:06.040Z",
8
+ "timestamp": "2026-07-17T19:09:09.529Z",
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.4",
22
+ "bom-ref": "@blamejs/pki@0.3.5",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.3.4",
25
+ "version": "0.3.5",
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.4",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.3.5",
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.4",
57
+ "ref": "@blamejs/pki@0.3.5",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]