@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.
- package/CHANGELOG.md +2 -0
- package/README.md +2 -0
- package/index.js +4 -0
- package/lib/cms-codec.js +685 -0
- package/lib/daemon.js +29 -4
- package/lib/mail-crypto-pgp.js +10 -9
- package/lib/mail-crypto-smime.js +15 -31
- package/lib/metrics.js +68 -8
- package/lib/stream-throttle.js +235 -0
- package/lib/subject.js +14 -10
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cms-codec.js
ADDED
|
@@ -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
|
+
};
|