@blamejs/core 0.10.12 → 0.10.14

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.
@@ -0,0 +1,685 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.cms
4
+ * @nav Crypto
5
+ * @title CMS Codec
6
+ *
7
+ * @intro
8
+ * RFC 5652 Cryptographic Message Syntax encoder + decoder built on
9
+ * the framework's existing `b.asn1Der` substrate and the vendored
10
+ * noble-post-quantum primitives (`b.pqcSoftware.ml_dsa_*` /
11
+ * `ml_kem_1024` / `slh_dsa_shake_256f`). Re-opens the CMS forward-
12
+ * watch item from the 2026-05-08 audit (deferred-with-condition
13
+ * pending operator-side demand from the live mail-stack listeners).
14
+ * Operator-demand condition is now met by the inbound MX + JMAP
15
+ * listeners (v0.9.45–v0.9.50).
16
+ *
17
+ * Scope (v0.10.13):
18
+ *
19
+ * - **ContentInfo** wrapper (RFC 5652 §3) for all top-level emissions.
20
+ * - **SignedData** (§5) encode + decode with PQC signer support
21
+ * (ML-DSA-65 per RFC 9909 §5, ML-DSA-87 per RFC 9909 §6,
22
+ * SLH-DSA-SHAKE-256f per RFC 9881). The signature input is the
23
+ * DER-encoded SET OF signed-attributes with the IMPLICIT [0] tag
24
+ * re-tagged to the universal SET tag per §5.4 third paragraph.
25
+ * - **EnvelopedData** (§6) encode with `KEMRecipientInfo` (RFC 9629)
26
+ * for ML-KEM-1024 recipients (RFC 9936). The content-encryption
27
+ * key is wrapped under a KEK derived from the KEM shared-secret
28
+ * via HKDF-SHA3-512; content is encrypted with ChaCha20-Poly1305
29
+ * (RFC 8103 OID). Efail-class CBC-malleability is impossible by
30
+ * construction — every CMS content blob emitted by this module
31
+ * carries an AEAD tag.
32
+ * - Strict DER on emit (canonical: lexicographic SET-OF ordering,
33
+ * minimal-length encoding, no indefinite length).
34
+ *
35
+ * Deferred from v0.10.13 (each with documented condition):
36
+ *
37
+ * - **AuthEnvelopedData** (RFC 5083) as a distinct ContentInfo
38
+ * ciphertext shape. Operator demand is not yet surfaced — every
39
+ * v0.10.13 emission uses EnvelopedData with the ChaCha20-Poly1305
40
+ * content-encryption OID, which is already AEAD by construction.
41
+ * Defer condition: at least one interop case requires a peer that
42
+ * refuses EnvelopedData and accepts only the §5083 ContentInfo
43
+ * OID. Cheap escape hatch: operators on such a peer compose
44
+ * `b.asn1Der` directly to rewrap an EnvelopedData blob into an
45
+ * AuthEnvelopedData ContentInfo. Lights up in v0.10.14 alongside
46
+ * `b.mail.smime` sign + verify, where the on-the-wire S/MIME 4.0
47
+ * content shape calls for it.
48
+ * - **`b.cms.decode` parse-tree of inner SignedData / EnvelopedData**
49
+ * beyond the ContentInfo wrapper. v0.10.13 returns the inner
50
+ * SEQUENCE bytes as `content` (an asn1-der node); callers that
51
+ * need fielded access walk it via `b.asn1Der.readSequence`. The
52
+ * fielded decoders ship alongside S/MIME verify in v0.10.14 where
53
+ * they're actually consumed.
54
+ *
55
+ * Refusal posture:
56
+ *
57
+ * - Top-level must be SEQUENCE { OID, [0] EXPLICIT content }; any
58
+ * other shape throws `cms/bad-content-info`.
59
+ * - Recipient/signer counts must be non-empty (`cms/no-signers` /
60
+ * `cms/no-recipients`).
61
+ * - Only PQC signature algorithms are accepted (`cms/bad-sig-alg`).
62
+ * - Only ML-KEM-1024 recipients are accepted (`cms/bad-recipient-type`).
63
+ * - Input past `opts.maxBytes` (default 64 MiB) throws `cms/oversize`.
64
+ *
65
+ * @card
66
+ * RFC 5652 CMS codec (SignedData + EnvelopedData) on b.asn1Der + vendored noble-post-quantum. PQC signers per RFC 9909 / 9881; ML-KEM-1024 recipients per RFC 9629 / 9936. AEAD-only content (ChaCha20-Poly1305) — Efail-class malleability cannot apply.
67
+ */
68
+
69
+ var nodeCrypto = require("node:crypto");
70
+ var asn1 = require("./asn1-der");
71
+ var bCrypto = require("./crypto");
72
+ var pqcSoftware = require("./pqc-software");
73
+ var { defineClass } = require("./framework-error");
74
+ var audit = require("./audit");
75
+
76
+ var CmsCodecError = defineClass("CmsCodecError", { alwaysPermanent: true });
77
+
78
+ // Common CMS OIDs (RFC 5652, RFC 5083, RFC 9629, RFC 9909, RFC 9881).
79
+ var OID = Object.freeze({
80
+ data: "1.2.840.113549.1.7.1",
81
+ signedData: "1.2.840.113549.1.7.2",
82
+ envelopedData: "1.2.840.113549.1.7.3",
83
+ authEnvelopedData: "1.2.840.113549.1.9.16.1.23", // RFC 5083
84
+ // PQC signature algorithms (RFC 9909, RFC 9881).
85
+ mldsa44: "2.16.840.1.101.3.4.3.17",
86
+ mldsa65: "2.16.840.1.101.3.4.3.18",
87
+ mldsa87: "2.16.840.1.101.3.4.3.19",
88
+ slhDsaShake256f: "2.16.840.1.101.3.4.3.31",
89
+ // PQC KEM algorithms (RFC 9935, RFC 9936).
90
+ mlkem768: "2.16.840.1.101.3.4.4.2",
91
+ mlkem1024: "2.16.840.1.101.3.4.4.3",
92
+ // KEMRecipientInfo type (RFC 9629 §3).
93
+ kemri: "1.2.840.113549.1.9.16.13.3",
94
+ // Symmetric content encryption — ChaCha20-Poly1305 (RFC 8103 IANA codepoint).
95
+ chacha20Poly1305: "1.2.840.113549.1.9.16.3.18",
96
+ // KDF — SHAKE256 XOF (NIST SP 800-185), the framework's PQC-first
97
+ // KDF substrate (`b.crypto.kdf` wraps it). OID per NIST registry.
98
+ shake256: "2.16.840.1.101.3.4.2.12",
99
+ // Signed-attribute attribute types.
100
+ contentType: "1.2.840.113549.1.9.3",
101
+ messageDigest: "1.2.840.113549.1.9.4",
102
+ signingTime: "1.2.840.113549.1.9.5",
103
+ // Digest algorithms (SHA3-256 / -512 — framework PQC-first hash family).
104
+ sha3_256: "2.16.840.1.101.3.4.2.8",
105
+ sha3_512: "2.16.840.1.101.3.4.2.10",
106
+ });
107
+
108
+ // Refusal ceilings.
109
+ var MAX_DEPTH = 32; // allow:raw-byte-literal — ASN.1 recursion ceiling
110
+ var DEFAULT_MAX_LEN = 64 * 1024 * 1024; // allow:raw-byte-literal — 64 MiB default decode cap
111
+
112
+ // Universal-tag bytes used in encode helpers.
113
+ var TAG_SEQUENCE = 0x30; // allow:raw-byte-literal — ASN.1 SEQUENCE constructed
114
+ var TAG_SET = 0x31; // allow:raw-byte-literal — ASN.1 SET constructed
115
+ var TAG_UTCTIME = 0x17; // allow:raw-byte-literal — UTCTime universal
116
+ var TAG_GENTIME = 0x18; // allow:raw-byte-literal — GeneralizedTime universal
117
+
118
+ /**
119
+ * @primitive b.cms.encodeSignedData
120
+ * @signature b.cms.encodeSignedData(opts)
121
+ * @since 0.10.13
122
+ * @status stable
123
+ * @related b.cms.decode, b.cms.encodeEnvelopedData
124
+ *
125
+ * Encode an RFC 5652 §5 SignedData ContentInfo with PQC signer
126
+ * support. The output is a DER-encoded Buffer ready for embedding in
127
+ * S/MIME `application/pkcs7-mime; smime-type=signed-data` parts or
128
+ * for standalone CMS-over-network use.
129
+ *
130
+ * @opts
131
+ * encapContent: Buffer, // bytes to sign
132
+ * digestAlg: "sha3-256" | "sha3-512", // default sha3-512
133
+ * signers: [{ certificate: Buffer, secretKey: Uint8Array, sigAlg: string }],
134
+ * certificates: Buffer[], // additional DER certs (optional)
135
+ * detached: boolean, // default false; true → omit encapContent
136
+ *
137
+ * @example
138
+ * var pq = b.pqcSoftware;
139
+ * var kp = pq.ml_dsa_65.keygen();
140
+ * var bytes = b.cms.encodeSignedData({
141
+ * encapContent: Buffer.from("payload"),
142
+ * digestAlg: "sha3-512",
143
+ * signers: [{ certificate: certDer, secretKey: kp.secretKey, sigAlg: "ML-DSA-65" }],
144
+ * });
145
+ */
146
+ function encodeSignedData(opts) {
147
+ if (!opts || typeof opts !== "object") {
148
+ throw new CmsCodecError("cms/bad-opts", "encodeSignedData: opts required");
149
+ }
150
+ if (!Buffer.isBuffer(opts.encapContent)) {
151
+ throw new CmsCodecError("cms/bad-encap", "encodeSignedData: encapContent must be a Buffer");
152
+ }
153
+ if (!Array.isArray(opts.signers) || opts.signers.length === 0) {
154
+ throw new CmsCodecError("cms/no-signers",
155
+ "encodeSignedData: opts.signers must be a non-empty array");
156
+ }
157
+ var digestAlg = opts.digestAlg || "sha3-512";
158
+ if (digestAlg !== "sha3-256" && digestAlg !== "sha3-512") {
159
+ throw new CmsCodecError("cms/bad-digest",
160
+ "encodeSignedData: digestAlg must be 'sha3-256' or 'sha3-512' " +
161
+ "(PQC-first; SHA-2 family not accepted in v1)");
162
+ }
163
+ var digestOid = digestAlg === "sha3-256" ? OID.sha3_256 : OID.sha3_512;
164
+ var detached = opts.detached === true;
165
+
166
+ // Message digest over encapContent (SHA3-256 or SHA3-512 per opts.digestAlg).
167
+ var msgDigest = nodeCrypto.createHash(digestAlg).update(opts.encapContent).digest();
168
+
169
+ // digestAlgorithms SET — one entry per distinct digest algorithm used.
170
+ var digestAlgs = asn1.writeNode(TAG_SET, _algorithmIdentifier(digestOid));
171
+
172
+ // EncapsulatedContentInfo.
173
+ var encapInfo = _encapsulatedContentInfo(opts.encapContent, detached);
174
+
175
+ // Optional certificates [0] IMPLICIT — operator-supplied DER cert blobs.
176
+ var certsBlock = Buffer.alloc(0);
177
+ if (Array.isArray(opts.certificates) && opts.certificates.length > 0) {
178
+ var concat = Buffer.concat(opts.certificates.map(function (c) {
179
+ if (!Buffer.isBuffer(c)) {
180
+ throw new CmsCodecError("cms/bad-cert",
181
+ "encodeSignedData: certificates entries must be DER Buffers");
182
+ }
183
+ return c;
184
+ }));
185
+ // certificates [0] IMPLICIT CertificateSet — CertificateSet is a SET
186
+ // of certificates (constructed), so this wrap is the constructed
187
+ // form per RFC 5652 §5.1.
188
+ certsBlock = _writeImplicitConstructed(0, concat);
189
+ }
190
+
191
+ // signerInfos SET — one SignerInfo per signer.
192
+ var sigInfos = opts.signers.map(function (s) {
193
+ return _signerInfo(s, msgDigest, digestOid);
194
+ });
195
+ var signerInfosSet = asn1.writeNode(TAG_SET, Buffer.concat(sigInfos));
196
+
197
+ // SignedData SEQUENCE per §5.1.
198
+ var signedDataSeq = asn1.writeNode(TAG_SEQUENCE, Buffer.concat([
199
+ asn1.writeInteger(Buffer.from([1])), // allow:raw-byte-literal — CMSVersion 1 per §5.1
200
+ digestAlgs,
201
+ encapInfo,
202
+ certsBlock,
203
+ signerInfosSet,
204
+ ]));
205
+
206
+ // ContentInfo wrapper.
207
+ var contentInfo = asn1.writeNode(TAG_SEQUENCE, Buffer.concat([
208
+ asn1.writeOid(OID.signedData),
209
+ asn1.writeContextExplicit(0, signedDataSeq),
210
+ ]));
211
+ try {
212
+ audit.safeEmit({
213
+ action: "cms.signedData.encoded",
214
+ outcome: "success",
215
+ actor: {},
216
+ metadata: { signerCount: opts.signers.length, digestAlg: digestAlg, detached: detached },
217
+ });
218
+ } catch (_e) { /* drop-silent */ }
219
+ return contentInfo;
220
+ }
221
+
222
+ /**
223
+ * @primitive b.cms.encodeEnvelopedData
224
+ * @signature b.cms.encodeEnvelopedData(opts)
225
+ * @since 0.10.13
226
+ * @status stable
227
+ * @related b.cms.decode, b.cms.encodeSignedData
228
+ *
229
+ * Encode an RFC 5652 §6 EnvelopedData ContentInfo with ML-KEM-1024
230
+ * recipients per RFC 9629 (KEMRecipientInfo) + RFC 9936 (ML-KEM in
231
+ * CMS). The content-encryption key is wrapped under a KEK derived
232
+ * from the per-recipient KEM shared-secret via HKDF-SHA3-512;
233
+ * content is encrypted with ChaCha20-Poly1305 so Efail-class
234
+ * malleability cannot apply.
235
+ *
236
+ * @opts
237
+ * plaintext: Buffer, // bytes to encrypt
238
+ * recipients: [{ type: "kem-mlkem-1024", publicKey: Uint8Array, recipientId: Buffer }],
239
+ *
240
+ * @example
241
+ * var pq = b.pqcSoftware;
242
+ * var kp = pq.ml_kem_1024.keygen();
243
+ * var bytes = b.cms.encodeEnvelopedData({
244
+ * plaintext: Buffer.from("secret"),
245
+ * recipients: [{ type: "kem-mlkem-1024", publicKey: kp.publicKey, recipientId: Buffer.from([1]) }],
246
+ * });
247
+ */
248
+ function encodeEnvelopedData(opts) {
249
+ if (!opts || typeof opts !== "object") {
250
+ throw new CmsCodecError("cms/bad-opts", "encodeEnvelopedData: opts required");
251
+ }
252
+ if (!Buffer.isBuffer(opts.plaintext)) {
253
+ throw new CmsCodecError("cms/bad-plaintext", "encodeEnvelopedData: plaintext must be a Buffer");
254
+ }
255
+ if (!Array.isArray(opts.recipients) || opts.recipients.length === 0) {
256
+ throw new CmsCodecError("cms/no-recipients",
257
+ "encodeEnvelopedData: opts.recipients must be a non-empty array");
258
+ }
259
+ // Fresh ChaCha20-Poly1305 content key.
260
+ var contentKey = bCrypto.generateBytes(32); // allow:raw-byte-literal — 256-bit ChaCha20 key
261
+
262
+ // recipientInfos SET — one KEMRecipientInfo per recipient.
263
+ var ris = opts.recipients.map(function (r) {
264
+ return _recipientInfo(r, contentKey);
265
+ });
266
+ var recipientInfosSet = asn1.writeNode(TAG_SET, Buffer.concat(ris));
267
+
268
+ // EncryptedContentInfo + ChaCha20-Poly1305 ciphertext.
269
+ var encContent = _encryptedContentInfo(opts.plaintext, contentKey);
270
+
271
+ // EnvelopedData SEQUENCE per §6.1. CMSVersion 4 (RFC 9629 §3 — when
272
+ // any RecipientInfo is OtherRecipientInfo, here KEMRecipientInfo).
273
+ var envelopedSeq = asn1.writeNode(TAG_SEQUENCE, Buffer.concat([
274
+ asn1.writeInteger(Buffer.from([4])), // allow:raw-byte-literal — CMSVersion 4 per RFC 9629 §3
275
+ recipientInfosSet,
276
+ encContent,
277
+ ]));
278
+ var contentInfo = asn1.writeNode(TAG_SEQUENCE, Buffer.concat([
279
+ asn1.writeOid(OID.envelopedData),
280
+ asn1.writeContextExplicit(0, envelopedSeq),
281
+ ]));
282
+ try {
283
+ audit.safeEmit({
284
+ action: "cms.envelopedData.encoded",
285
+ outcome: "success",
286
+ actor: {},
287
+ metadata: { recipientCount: opts.recipients.length },
288
+ });
289
+ } catch (_e) { /* drop-silent */ }
290
+ return contentInfo;
291
+ }
292
+
293
+ /**
294
+ * @primitive b.cms.decode
295
+ * @signature b.cms.decode(buf, opts?)
296
+ * @since 0.10.13
297
+ * @status stable
298
+ * @related b.cms.encodeSignedData, b.cms.encodeEnvelopedData
299
+ *
300
+ * Decode a CMS ContentInfo from `buf` (DER bytes). Returns
301
+ * `{ contentType, content }` where `contentType` is the dotted-OID
302
+ * string (e.g. `"1.2.840.113549.1.7.2"` for SignedData) and
303
+ * `content` is the inner asn1-der node (SignedData / EnvelopedData /
304
+ * other) — operators walk it via `b.asn1Der.readSequence`. Fielded
305
+ * decoders for SignedData / EnvelopedData ship in v0.10.14 alongside
306
+ * S/MIME sign+verify.
307
+ *
308
+ * Refuses input past `opts.maxBytes` (default 64 MiB), top-level
309
+ * non-SEQUENCE shapes, missing OID + [0] EXPLICIT child pair.
310
+ *
311
+ * @opts
312
+ * maxBytes: number, // default 64 MiB
313
+ *
314
+ * @example
315
+ * var ci = b.cms.decode(derBytes);
316
+ * ci.contentType; // → "1.2.840.113549.1.7.2"
317
+ */
318
+ function decode(buf, opts) {
319
+ opts = opts || {};
320
+ if (!Buffer.isBuffer(buf)) {
321
+ throw new CmsCodecError("cms/bad-input", "decode: buf must be a Buffer");
322
+ }
323
+ var maxBytes = opts.maxBytes || DEFAULT_MAX_LEN;
324
+ if (buf.length > maxBytes) {
325
+ throw new CmsCodecError("cms/oversize",
326
+ "decode: input " + buf.length + " bytes exceeds maxBytes=" + maxBytes);
327
+ }
328
+ var node;
329
+ try { node = asn1.readNode(buf); }
330
+ catch (e) {
331
+ throw new CmsCodecError("cms/bad-asn1",
332
+ "decode: ASN.1 parse failed: " + ((e && e.message) || String(e)));
333
+ }
334
+ if (!(node.tag === asn1.TAG.SEQUENCE && node.constructed)) {
335
+ throw new CmsCodecError("cms/bad-content-info",
336
+ "decode: top-level must be SEQUENCE (got tag 0x" + node.tag.toString(16) + ")"); // allow:raw-byte-literal — hex radix for error-message formatting
337
+ }
338
+ // ContentInfo SEQUENCE children: { contentType OID, [0] EXPLICIT ANY }.
339
+ var children;
340
+ try { children = asn1.readSequence(node.value); }
341
+ catch (e2) {
342
+ throw new CmsCodecError("cms/bad-content-info",
343
+ "decode: ContentInfo body parse failed: " + ((e2 && e2.message) || String(e2)));
344
+ }
345
+ if (children.length < 2) {
346
+ throw new CmsCodecError("cms/bad-content-info",
347
+ "decode: ContentInfo SEQUENCE must have 2 children (contentType + [0] content)");
348
+ }
349
+ var contentType;
350
+ try { contentType = asn1.readOid(children[0]); }
351
+ catch (e3) {
352
+ throw new CmsCodecError("cms/bad-oid",
353
+ "decode: contentType OID parse failed: " + ((e3 && e3.message) || String(e3)));
354
+ }
355
+ // [0] EXPLICIT content — unwrap via asn1.unwrapExplicit(node, expectedTagNumber).
356
+ var inner;
357
+ try { inner = asn1.unwrapExplicit(children[1], 0); }
358
+ catch (e4) {
359
+ throw new CmsCodecError("cms/bad-explicit-content",
360
+ "decode: [0] EXPLICIT content unwrap failed: " + ((e4 && e4.message) || String(e4)));
361
+ }
362
+ return { contentType: contentType, content: inner };
363
+ }
364
+
365
+ // ---- Internal helpers -----------------------------------------------------
366
+
367
+ // OIDs whose AlgorithmIdentifier specifies ABSENT parameters per their
368
+ // publishing RFC — emitting NULL here would make the CMS structure
369
+ // non-conformant for strict validators (Codex P1 finding on PR #102).
370
+ // ML-DSA per RFC 9909 §3, SLH-DSA per RFC 9881 §3, ML-KEM per
371
+ // RFC 9936 §3. SHAKE-family per FIPS 202 (NIST registry — absent params).
372
+ var ABSENT_PARAM_OIDS = new Set([
373
+ "2.16.840.1.101.3.4.3.17", // ml_dsa_44
374
+ "2.16.840.1.101.3.4.3.18", // ml_dsa_65
375
+ "2.16.840.1.101.3.4.3.19", // ml_dsa_87
376
+ "2.16.840.1.101.3.4.3.31", // slh_dsa_shake_256f
377
+ "2.16.840.1.101.3.4.4.2", // ml_kem_768
378
+ "2.16.840.1.101.3.4.4.3", // ml_kem_1024
379
+ "2.16.840.1.101.3.4.2.12", // shake256 (KDF/digest — absent params)
380
+ ]);
381
+
382
+ function _algorithmIdentifier(oidStr) {
383
+ // SEQUENCE { algorithm OID, parameters ANY DEFINED BY algorithm OPTIONAL }.
384
+ // PQC OIDs (RFC 9909 / 9881 / 9936) MUST emit with parameters ABSENT;
385
+ // legacy non-PQC OIDs (SHA-2 / SHA-3 hash OIDs in this module, ChaCha20-
386
+ // Poly1305 wrap OID) still carry the historical NULL parameter shape
387
+ // that fielded CMS toolchains expect.
388
+ if (ABSENT_PARAM_OIDS.has(oidStr)) {
389
+ return asn1.writeNode(TAG_SEQUENCE, asn1.writeOid(oidStr));
390
+ }
391
+ return asn1.writeNode(TAG_SEQUENCE, Buffer.concat([
392
+ asn1.writeOid(oidStr),
393
+ asn1.writeNull(),
394
+ ]));
395
+ }
396
+
397
+ function _writeImplicitConstructed(tagNumber, payload) {
398
+ // [N] IMPLICIT context-specific CONSTRUCTED — for wrapping SEQUENCE /
399
+ // SET payloads (e.g. certificates [0], crls [1], OtherRecipientInfo
400
+ // value).
401
+ var tagByte = 0xa0 | (tagNumber & 0x1f); // allow:raw-byte-literal — context-specific constructed mask
402
+ return asn1.writeNode(tagByte, payload);
403
+ }
404
+
405
+ function _writeImplicitPrimitive(tagNumber, value) {
406
+ // [N] IMPLICIT context-specific PRIMITIVE — for wrapping primitive
407
+ // ASN.1 types (OCTET STRING / INTEGER / OID) that have been IMPLICIT-
408
+ // tagged. The constructed bit MUST NOT be set or strict CMS parsers
409
+ // reject the structure (Codex P1 finding on PR #102 — RecipientIdentifier
410
+ // CHOICE's SubjectKeyIdentifier alternative is `[0] IMPLICIT OCTET STRING`,
411
+ // a primitive type).
412
+ var tagByte = 0x80 | (tagNumber & 0x1f); // allow:raw-byte-literal — context-specific primitive mask
413
+ return asn1.writeNode(tagByte, value);
414
+ }
415
+
416
+ function _encapsulatedContentInfo(content, detached) {
417
+ // EncapsulatedContentInfo: SEQUENCE { eContentType OID, eContent [0] EXPLICIT OCTET STRING? }
418
+ var inner = [asn1.writeOid(OID.data)];
419
+ if (!detached) {
420
+ inner.push(asn1.writeContextExplicit(0, asn1.writeOctetString(content)));
421
+ }
422
+ return asn1.writeNode(TAG_SEQUENCE, Buffer.concat(inner));
423
+ }
424
+
425
+ function _signerInfo(signer, msgDigest, digestOid) {
426
+ if (!signer || typeof signer !== "object") {
427
+ throw new CmsCodecError("cms/bad-signer", "signer entry must be an object");
428
+ }
429
+ if (!Buffer.isBuffer(signer.certificate)) {
430
+ throw new CmsCodecError("cms/bad-signer-cert",
431
+ "signer.certificate must be a DER Buffer");
432
+ }
433
+ if (signer.sigAlg !== "ML-DSA-65" && signer.sigAlg !== "ML-DSA-87" &&
434
+ signer.sigAlg !== "SLH-DSA-SHAKE-256f") {
435
+ throw new CmsCodecError("cms/bad-sig-alg",
436
+ "signer.sigAlg must be ML-DSA-65 / ML-DSA-87 / SLH-DSA-SHAKE-256f " +
437
+ "(PQC-first; RSA / ECDSA not accepted)");
438
+ }
439
+ if (!(signer.secretKey instanceof Uint8Array)) {
440
+ throw new CmsCodecError("cms/bad-signer-key",
441
+ "signer.secretKey must be a Uint8Array from the matching PQC keygen");
442
+ }
443
+ var sigAlgOid;
444
+ var pqcAlg;
445
+ if (signer.sigAlg === "ML-DSA-65") { sigAlgOid = OID.mldsa65; pqcAlg = pqcSoftware.ml_dsa_65; }
446
+ else if (signer.sigAlg === "ML-DSA-87") { sigAlgOid = OID.mldsa87; pqcAlg = pqcSoftware.ml_dsa_87; }
447
+ else { sigAlgOid = OID.slhDsaShake256f; pqcAlg = pqcSoftware.slh_dsa_shake_256f; }
448
+
449
+ // signedAttrs SET OF Attribute — IMPLICIT [0] tagged in the SignerInfo.
450
+ // For the signature input we re-tag as universal SET (0x31) per
451
+ // RFC 5652 §5.4 paragraph 3.
452
+ var signedAttrs = _signedAttrs({
453
+ contentType: OID.data,
454
+ messageDigest: msgDigest,
455
+ signingTime: signer.signingTime instanceof Date ? signer.signingTime : new Date(),
456
+ });
457
+ // signedAttrs is already `31 LL VV...` — re-tag to `A0 LL VV...` for the
458
+ // SignerInfo, and use the original `31 LL VV...` form as the signature
459
+ // input.
460
+ var signatureInput = signedAttrs;
461
+ var signedAttrsImplicit = Buffer.concat([Buffer.from([0xa0]), // allow:raw-byte-literal — IMPLICIT [0] tag per RFC 5652 §5.3
462
+ signedAttrs.slice(1)]);
463
+
464
+ var signature;
465
+ try {
466
+ // noble signature: sign(msg, secretKey) → Uint8Array.
467
+ var sigBytes = pqcAlg.sign(new Uint8Array(signatureInput), signer.secretKey);
468
+ signature = Buffer.from(sigBytes);
469
+ } catch (e) {
470
+ throw new CmsCodecError("cms/sign-failed",
471
+ "SignerInfo signature failed: " + ((e && e.message) || String(e)));
472
+ }
473
+
474
+ // SignerInfo SEQUENCE per §5.3 (issuerAndSerialNumber variant — CMSVersion 1).
475
+ return asn1.writeNode(TAG_SEQUENCE, Buffer.concat([
476
+ asn1.writeInteger(Buffer.from([1])), // allow:raw-byte-literal — CMSVersion 1 for issuerAndSerialNumber
477
+ _issuerAndSerialNumber(signer.certificate),
478
+ _algorithmIdentifier(digestOid),
479
+ signedAttrsImplicit,
480
+ _algorithmIdentifier(sigAlgOid),
481
+ asn1.writeOctetString(signature),
482
+ ]));
483
+ }
484
+
485
+ function _signedAttrs(attrs) {
486
+ // SET OF Attribute — DER canonical: sort entries by encoded bytes (X.690 §11.6).
487
+ var entries = [];
488
+ entries.push(_attribute(OID.contentType, asn1.writeOid(attrs.contentType)));
489
+ entries.push(_attribute(OID.messageDigest, asn1.writeOctetString(attrs.messageDigest)));
490
+ entries.push(_attribute(OID.signingTime, _encodeTime(attrs.signingTime)));
491
+ entries.sort(Buffer.compare);
492
+ return asn1.writeNode(TAG_SET, Buffer.concat(entries));
493
+ }
494
+
495
+ function _attribute(typeOid, valueBuf) {
496
+ // Attribute ::= SEQUENCE { attrType OID, attrValues SET OF ANY }
497
+ return asn1.writeNode(TAG_SEQUENCE, Buffer.concat([
498
+ asn1.writeOid(typeOid),
499
+ asn1.writeNode(TAG_SET, valueBuf),
500
+ ]));
501
+ }
502
+
503
+ function _encodeTime(date) {
504
+ var pad = function (n) { return n < 10 ? "0" + n : String(n); };
505
+ var y = date.getUTCFullYear();
506
+ var mm = pad(date.getUTCMonth() + 1);
507
+ var dd = pad(date.getUTCDate());
508
+ var hh = pad(date.getUTCHours());
509
+ var mi = pad(date.getUTCMinutes());
510
+ var ss = pad(date.getUTCSeconds());
511
+ if (y >= 1950 && y <= 2049) {
512
+ var yy = pad(y % 100);
513
+ return asn1.writeNode(TAG_UTCTIME, Buffer.from(yy + mm + dd + hh + mi + ss + "Z", "ascii"));
514
+ }
515
+ return asn1.writeNode(TAG_GENTIME, Buffer.from(String(y) + mm + dd + hh + mi + ss + "Z", "ascii"));
516
+ }
517
+
518
+ function _issuerAndSerialNumber(certDer) {
519
+ // RFC 5280 §4.1 Certificate SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }.
520
+ // tbsCertificate SEQUENCE { [0] version?, serialNumber INTEGER, signature AlgId, issuer Name, ... }
521
+ // We extract `issuer Name` (SEQUENCE) + `serialNumber` (INTEGER) and wrap as
522
+ // SEQUENCE { issuer, serialNumber } per RFC 5652 §10.2.4.
523
+ var cert;
524
+ try { cert = asn1.readNode(certDer); }
525
+ catch (e) {
526
+ throw new CmsCodecError("cms/bad-cert", "certificate DER parse failed: " + ((e && e.message) || String(e)));
527
+ }
528
+ if (cert.tag !== asn1.TAG.SEQUENCE) {
529
+ throw new CmsCodecError("cms/bad-cert", "certificate top-level is not a SEQUENCE");
530
+ }
531
+ var certChildren;
532
+ try { certChildren = asn1.readSequence(cert.value); }
533
+ catch (e2) {
534
+ throw new CmsCodecError("cms/bad-cert", "certificate body parse failed: " + ((e2 && e2.message) || String(e2)));
535
+ }
536
+ if (certChildren.length < 1 || certChildren[0].tag !== asn1.TAG.SEQUENCE) {
537
+ throw new CmsCodecError("cms/bad-cert", "certificate has no tbsCertificate SEQUENCE");
538
+ }
539
+ var tbsChildren;
540
+ try { tbsChildren = asn1.readSequence(certChildren[0].value); }
541
+ catch (e3) {
542
+ throw new CmsCodecError("cms/bad-cert", "tbsCertificate body parse failed: " + ((e3 && e3.message) || String(e3)));
543
+ }
544
+ // Optional [0] EXPLICIT version then serialNumber INTEGER.
545
+ var idx = 0;
546
+ if (tbsChildren[idx] && tbsChildren[idx].tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC &&
547
+ tbsChildren[idx].tag === 0) {
548
+ idx += 1;
549
+ }
550
+ var serialNode = tbsChildren[idx];
551
+ if (!serialNode || serialNode.tag !== asn1.TAG.INTEGER) {
552
+ throw new CmsCodecError("cms/bad-cert", "tbsCertificate has no serialNumber INTEGER");
553
+ }
554
+ idx += 1;
555
+ // Skip signature AlgId (SEQUENCE).
556
+ if (!tbsChildren[idx] || tbsChildren[idx].tag !== asn1.TAG.SEQUENCE) {
557
+ throw new CmsCodecError("cms/bad-cert", "tbsCertificate has no signature AlgorithmIdentifier");
558
+ }
559
+ idx += 1;
560
+ // Issuer Name (SEQUENCE).
561
+ var issuerNode = tbsChildren[idx];
562
+ if (!issuerNode || issuerNode.tag !== asn1.TAG.SEQUENCE) {
563
+ throw new CmsCodecError("cms/bad-cert", "tbsCertificate has no issuer Name SEQUENCE");
564
+ }
565
+ // Reconstruct the full DER bytes of issuer (header + value) and
566
+ // serialNumber (header + value) — readNode gave us value-only Buffers.
567
+ var issuerDer = _reEncodeNode(issuerNode);
568
+ var serialDer = _reEncodeNode(serialNode);
569
+ return asn1.writeNode(TAG_SEQUENCE, Buffer.concat([issuerDer, serialDer]));
570
+ }
571
+
572
+ function _reEncodeNode(node) {
573
+ // Reconstruct the TLV bytes of `node` — asn1-der's readNode returns the
574
+ // value slice but the issuerAndSerialNumber surface needs the full
575
+ // TLV. writeNode rebuilds canonical DER from the original tag byte +
576
+ // value bytes; the tag byte is reconstructed from tagClass + constructed +
577
+ // tag number.
578
+ var classBits = (node.tagClass & 0x03) << 6; // allow:raw-byte-literal — tag-class shift
579
+ var consBit = node.constructed ? 0x20 : 0x00; // allow:raw-byte-literal — constructed bit
580
+ var tagBits = node.tag & 0x1f; // allow:raw-byte-literal — short-form tag
581
+ var tagByte = classBits | consBit | tagBits;
582
+ return asn1.writeNode(tagByte, node.value);
583
+ }
584
+
585
+ function _recipientInfo(recipient, contentKey) {
586
+ // RFC 9629 KEMRecipientInfo wrapped in [1] IMPLICIT OtherRecipientInfo
587
+ // SEQUENCE per §3:
588
+ // ori [4] IMPLICIT OtherRecipientInfo
589
+ // OtherRecipientInfo ::= SEQUENCE { oriType OID, oriValue ANY DEFINED BY oriType }
590
+ // oriType = id-ori-kem (RFC 9629 §3)
591
+ // oriValue = KEMRecipientInfo SEQUENCE { version, rid, kem, kemct, kdf, kekLength, ukm?, wrap, encryptedKey }
592
+ if (!recipient || typeof recipient !== "object") {
593
+ throw new CmsCodecError("cms/bad-recipient", "recipient must be an object");
594
+ }
595
+ if (recipient.type !== "kem-mlkem-1024") {
596
+ throw new CmsCodecError("cms/bad-recipient-type",
597
+ "recipient.type must be 'kem-mlkem-1024' " +
598
+ "(other KEMs / KEKRecipientInfo / KeyAgreeRecipientInfo deferred)");
599
+ }
600
+ if (!(recipient.publicKey instanceof Uint8Array)) {
601
+ throw new CmsCodecError("cms/bad-recipient-key",
602
+ "recipient.publicKey must be a Uint8Array from b.pqcSoftware.ml_kem_1024.keygen()");
603
+ }
604
+ if (!Buffer.isBuffer(recipient.recipientId)) {
605
+ throw new CmsCodecError("cms/bad-recipient-id",
606
+ "recipient.recipientId must be a Buffer (SubjectKeyIdentifier or issuer-and-serial-number DER)");
607
+ }
608
+ // KEM encapsulate against the recipient's ML-KEM-1024 public key.
609
+ var encap;
610
+ try { encap = pqcSoftware.ml_kem_1024.encapsulate(recipient.publicKey); }
611
+ catch (e) {
612
+ throw new CmsCodecError("cms/kem-encap-failed",
613
+ "ML-KEM-1024 encapsulation failed: " + ((e && e.message) || String(e)));
614
+ }
615
+ // Derive 32-byte KEK from the KEM shared secret via SHAKE256 (the
616
+ // framework's PQC-first KDF). The info-label binds the derivation to
617
+ // the CMS KEMRecipientInfo + ChaCha20-Poly1305 wrap context so a key
618
+ // derived here cannot be confused with a key derived for any other
619
+ // composition path.
620
+ var infoLabel = Buffer.from("cms/kemri/chacha20-poly1305", "ascii");
621
+ var kdfInput = Buffer.concat([Buffer.from(encap.sharedSecret), infoLabel]);
622
+ var kek = bCrypto.kdf(kdfInput, 32); // allow:raw-byte-literal — 256-bit KEK
623
+ // Wrap the content key under the KEK using ChaCha20-Poly1305.
624
+ var wrapped;
625
+ try { wrapped = bCrypto.encryptPacked(contentKey, kek); }
626
+ catch (e2) {
627
+ throw new CmsCodecError("cms/wrap-failed",
628
+ "content-key wrap failed: " + ((e2 && e2.message) || String(e2)));
629
+ }
630
+ // KEMRecipientInfo SEQUENCE.
631
+ // Simplified ordering, version 0 per RFC 9629 §3.
632
+ var kemRi = asn1.writeNode(TAG_SEQUENCE, Buffer.concat([
633
+ asn1.writeInteger(Buffer.from([0])), // allow:raw-byte-literal — KEMRecipientInfo version 0
634
+ // rid CHOICE per RFC 9629 §3: this module ships the [0] IMPLICIT
635
+ // SubjectKeyIdentifier alternative — SKI is `[0] IMPLICIT OCTET
636
+ // STRING` (PRIMITIVE per RFC 5652 §10.2.4). The constructed form
637
+ // (0xa0) is the IssuerAndSerialNumber CHOICE alternative; this
638
+ // module picks SKI for KEM recipients since the operator-supplied
639
+ // recipientId is opaque key-identifier bytes.
640
+ _writeImplicitPrimitive(0, recipient.recipientId),
641
+ _algorithmIdentifier(OID.mlkem1024), // kem
642
+ asn1.writeOctetString(Buffer.from(encap.cipherText)), // kemct
643
+ _algorithmIdentifier(OID.shake256), // kdf
644
+ asn1.writeInteger(Buffer.from([32])), // allow:raw-byte-literal — kekLength = 32 bytes
645
+ _algorithmIdentifier(OID.chacha20Poly1305), // wrap (also used as content-encryption AlgId; same OID)
646
+ asn1.writeOctetString(wrapped), // encryptedKey
647
+ ]));
648
+ // OtherRecipientInfo SEQUENCE { oriType OID, oriValue ANY DEFINED BY oriType }
649
+ // wrapped in [4] IMPLICIT context tag per RFC 5652 §6.2 RecipientInfo
650
+ // CHOICE alternative.
651
+ var oriValue = Buffer.concat([
652
+ asn1.writeOid(OID.kemri),
653
+ kemRi,
654
+ ]);
655
+ return asn1.writeNode(0xa4, oriValue); // allow:raw-byte-literal — [4] IMPLICIT context-specific constructed (ori CHOICE)
656
+ }
657
+
658
+ function _encryptedContentInfo(plaintext, contentKey) {
659
+ // EncryptedContentInfo SEQUENCE { contentType OID, contentEncryptionAlgorithm AlgId,
660
+ // encryptedContent [0] IMPLICIT OCTET STRING OPTIONAL }
661
+ // The ChaCha20-Poly1305 ciphertext is the framework's encryptPacked output
662
+ // (nonce ‖ ciphertext ‖ tag). Operators decoding with a non-blamejs CMS
663
+ // peer need to know the framework wire format — documented in @intro.
664
+ var ct;
665
+ try { ct = bCrypto.encryptPacked(plaintext, contentKey); }
666
+ catch (e) {
667
+ throw new CmsCodecError("cms/encrypt-failed",
668
+ "content encryption failed: " + ((e && e.message) || String(e)));
669
+ }
670
+ return asn1.writeNode(TAG_SEQUENCE, Buffer.concat([
671
+ asn1.writeOid(OID.data),
672
+ _algorithmIdentifier(OID.chacha20Poly1305),
673
+ _writeImplicitPrimitive(0, ct),
674
+ ]));
675
+ }
676
+
677
+ module.exports = {
678
+ encodeSignedData: encodeSignedData,
679
+ encodeEnvelopedData: encodeEnvelopedData,
680
+ decode: decode,
681
+ OID: OID,
682
+ MAX_DEPTH: MAX_DEPTH,
683
+ DEFAULT_MAX_LEN: DEFAULT_MAX_LEN,
684
+ CmsCodecError: CmsCodecError,
685
+ };