@blamejs/pki 0.6.13 → 0.6.15
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 +22 -1
- package/README.md +1 -1
- package/index.js +1 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +59 -233
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cms-encrypt.js
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
-
//
|
|
5
|
-
// @internal -- the pki.cms.encrypt implementation. The operator-facing @module pki.cms + the
|
|
4
|
+
// @internal
|
|
6
5
|
// @primitive pki.cms.encrypt block live in cms-verify.js, which re-exports this function (the
|
|
7
|
-
// cms-sign.js model).
|
|
8
|
-
//
|
|
9
|
-
// CMS EnvelopedData / AuthEnvelopedData / EncryptedData production (RFC 5652/5083/5084/3560/5753/
|
|
10
|
-
// 8418/9629/9936/3211/8018), the producing side of pki.cms.decrypt. It is the crypto layer over
|
|
11
|
-
// the shipped strict parser (schema-cms.js): one fresh content-encryption key is wrapped for
|
|
12
|
-
// every recipient (RFC 5652 sec. 6.1), each RecipientInfo arm dispatched off the recipient's key
|
|
13
|
-
// material (RSA -> ktri OAEP; EC -> kari stdDH; X25519/X448 -> kari HKDF; ML-KEM -> ori/KEMRI;
|
|
14
|
-
// password -> pwri; symmetric KEK -> kekri) through the OID-keyed registry, never a hardcoded
|
|
15
|
-
// switch. AEAD (AES-GCM) content is the default and yields AuthEnvelopedData; CBC yields
|
|
16
|
-
// EnvelopedData. PKCS#1 v1.5 is never emitted.
|
|
17
6
|
|
|
18
7
|
var nodeCrypto = require("crypto");
|
|
19
8
|
var asn1 = require("./asn1-der");
|
|
20
|
-
var pkiBuild = require("./pki-build");
|
|
9
|
+
var pkiBuild = require("./pki-build");
|
|
21
10
|
var oid = require("./oid");
|
|
22
11
|
var x509 = require("./schema-x509");
|
|
23
12
|
var schemaCms = require("./schema-cms");
|
|
@@ -36,19 +25,14 @@ var WRAP_KEK_LENGTHS = schemaCms.WRAP_KEK_LENGTHS;
|
|
|
36
25
|
function O(n) { return oid.byName(n); }
|
|
37
26
|
function _err(code, message, cause) { return new CmsError(code, message, cause); }
|
|
38
27
|
|
|
39
|
-
// An AlgorithmIdentifier { OID } (absent params) or { OID, NULL }.
|
|
40
28
|
function _algId(name, shape) { return shape === "null" ? b.sequence([b.oid(O(name)), b.nullValue()]) : b.sequence([b.oid(O(name))]); }
|
|
41
29
|
|
|
42
|
-
// A certificate descriptor -> raw DER (the recipient cert is parsed for dispatch + rid; the caller
|
|
43
|
-
// supplies bytes, not a re-encoded parse).
|
|
44
30
|
function _normCertDer(cert, what) {
|
|
45
31
|
if (guard.bytes.isByteSource(cert)) return guard.bytes.snapshotSource(cert, CmsError, "cms/bad-input", what || "a certificate");
|
|
46
32
|
if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cms/bad-input", (what || "a certificate") + " PEM could not be decoded", e); } }
|
|
47
33
|
throw _err("cms/bad-input", (what || "a certificate") + " must be a DER Buffer, Uint8Array, or PEM string");
|
|
48
34
|
}
|
|
49
35
|
|
|
50
|
-
// ---- content-encryption algorithms (registry, not switch) ------------------
|
|
51
|
-
// name -> { oid, keyBits, aead }. AEAD (GCM) -> AuthEnvelopedData; CBC -> EnvelopedData.
|
|
52
36
|
var CONTENT_ALGS = {
|
|
53
37
|
"aes-128-gcm": { oid: "aes128-GCM", keyBits: 128, aead: true },
|
|
54
38
|
"aes-192-gcm": { oid: "aes192-GCM", keyBits: 192, aead: true },
|
|
@@ -58,34 +42,24 @@ var CONTENT_ALGS = {
|
|
|
58
42
|
"aes-256-cbc": { oid: "aes256-CBC", keyBits: 256, aead: false },
|
|
59
43
|
};
|
|
60
44
|
|
|
61
|
-
// The AES key-wrap OID for a KEK of `keyBytes` octets (16/24/32 -> aes128/192/256-wrap).
|
|
62
45
|
function _wrapOidForKek(keyBytes) {
|
|
63
46
|
if (keyBytes === 16) return "aes128-wrap";
|
|
64
47
|
if (keyBytes === 24) return "aes192-wrap";
|
|
65
48
|
if (keyBytes === 32) return "aes256-wrap";
|
|
66
|
-
// Coverage residual: unreachable via the API, since a KEK/CEK is always an AES key size (16/24/32);
|
|
67
|
-
// a defensive throw for a future caller that hands an off-size key.
|
|
68
49
|
throw _err("cms/bad-input", "no AES key-wrap algorithm for a " + keyBytes + "-octet key-encryption key");
|
|
69
50
|
}
|
|
70
51
|
|
|
71
|
-
// GCMParameters ::= SEQUENCE { aes-nonce OCTET STRING, aes-ICVlen INTEGER DEFAULT 12 }, where the
|
|
72
|
-
// DEFAULT 12 is omitted on emit (RFC 5084 sec. 3.2 / canonical DER).
|
|
73
52
|
function _gcmParams(nonce, icvLen) {
|
|
74
53
|
var kids = [b.octetString(nonce)];
|
|
75
54
|
if (icvLen !== 12) kids.push(b.integer(BigInt(icvLen)));
|
|
76
55
|
return b.sequence(kids);
|
|
77
56
|
}
|
|
78
57
|
|
|
79
|
-
// The keyIdentifier option selects the RecipientIdentifier form. "issuerAndSerial" is the documented
|
|
80
|
-
// default (its RFC name issuerAndSerialNumber is accepted too); reject anything else instead of
|
|
81
|
-
// silently emitting issuerAndSerialNumber, so a typo surfaces instead of a rid the caller never asked for.
|
|
82
58
|
function _assertKeyIdentifier(form) {
|
|
83
59
|
if (form != null && form !== "issuerAndSerial" && form !== "issuerAndSerialNumber" && form !== "subjectKeyIdentifier") {
|
|
84
60
|
throw _err("cms/bad-input", "unsupported keyIdentifier " + JSON.stringify(form) + " (use \"issuerAndSerial\" or \"subjectKeyIdentifier\")");
|
|
85
61
|
}
|
|
86
62
|
}
|
|
87
|
-
// The RecipientIdentifier (rid) for a parsed cert: issuerAndSerialNumber (default) or
|
|
88
|
-
// subjectKeyIdentifier [0] IMPLICIT. Both forms per RFC 5652 sec. 6.2.1.
|
|
89
63
|
function _rid(cert, form) {
|
|
90
64
|
_assertKeyIdentifier(form);
|
|
91
65
|
if (form === "subjectKeyIdentifier") {
|
|
@@ -95,11 +69,6 @@ function _rid(cert, form) {
|
|
|
95
69
|
}
|
|
96
70
|
return { node: b.sequence([b.raw(cert.issuer.bytes), b.integer(cert.serialNumber)]), riVersion: 0 };
|
|
97
71
|
}
|
|
98
|
-
// Coverage residual (the malformed-extension catch arms in _skiOf + _assertKeyUsage, and the
|
|
99
|
-
// unsupported-curve / low-order / unsupported-KEM-cert throws further below): these validate the
|
|
100
|
-
// CALLER's own recipient certificate at config time (tier-1 THROW). They fire only when a caller
|
|
101
|
-
// supplies a certificate whose SKI/keyUsage extension is malformed, or whose key is an unsupported
|
|
102
|
-
// curve, a low-order Montgomery point, or an unsupported KEM: inputs the toolkit never produces.
|
|
103
72
|
function _skiOf(cert) {
|
|
104
73
|
var exts = cert.extensions || [];
|
|
105
74
|
for (var i = 0; i < exts.length; i++) if (exts[i].name === "subjectKeyIdentifier" && exts[i].value != null) {
|
|
@@ -108,21 +77,15 @@ function _skiOf(cert) {
|
|
|
108
77
|
return null;
|
|
109
78
|
}
|
|
110
79
|
|
|
111
|
-
// keyUsage bit assertion (M9/M15): a recipient cert WITH a keyUsage extension MUST assert `bitName`.
|
|
112
|
-
// Through the shared reader, which applies the NamedBitList rules a local bit test does not: DER
|
|
113
|
-
// drops trailing zero bits (X.690 sec. 11.2.2) and sec. 4.2.1.3 requires at least one bit set, so
|
|
114
|
-
// reading the bits here would accept as a recipient a certificate the issuing side calls malformed.
|
|
115
80
|
function _assertKeyUsage(cert, bitName, arm) {
|
|
116
81
|
var ku = pkix.keyUsageOf(_KU_NS, cert, _err, "cms/bad-input", "recipient certificate's");
|
|
117
82
|
if (ku && ku[bitName] !== true) throw _err("cms/bad-key-usage", "the " + arm + " recipient certificate's keyUsage does not assert " + bitName);
|
|
118
83
|
}
|
|
119
84
|
|
|
120
|
-
// ---- ktri (RSA) : RSAES-OAEP, SHA-256 default (v1.5 never emitted) ---------
|
|
121
85
|
var OAEP_HASH = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
122
86
|
function _oaepParams(hashName) {
|
|
123
87
|
var hAlg = _algId(hashName, "null");
|
|
124
88
|
var mgf = b.sequence([b.oid(O("mgf1")), hAlg]);
|
|
125
|
-
// pSourceAlgorithm [2] DEFAULT pSpecifiedEmpty: the empty-label default MUST be omitted (X.690).
|
|
126
89
|
return b.sequence([b.explicit(0, hAlg), b.explicit(1, mgf)]);
|
|
127
90
|
}
|
|
128
91
|
async function _buildKtri(cek, cert, opts) {
|
|
@@ -136,19 +99,14 @@ async function _buildKtri(cek, cert, opts) {
|
|
|
136
99
|
return { tag: null, _riVersion: rid.riVersion, node: b.sequence([b.integer(BigInt(rid.riVersion)), rid.node, keyEncAlg, b.octetString(encryptedKey)]) };
|
|
137
100
|
}
|
|
138
101
|
|
|
139
|
-
|
|
140
|
-
var EC_KA = {}; // recipient-curve OID -> { curve, x963: {hash, scheme}, coordLen }
|
|
102
|
+
var EC_KA = {};
|
|
141
103
|
EC_KA[O("prime256v1")] = { curve: "P-256", hash: "SHA-256", scheme: "dhSinglePass-stdDH-sha256kdf-scheme" };
|
|
142
104
|
EC_KA[O("secp384r1")] = { curve: "P-384", hash: "SHA-384", scheme: "dhSinglePass-stdDH-sha384kdf-scheme" };
|
|
143
105
|
EC_KA[O("secp521r1")] = { curve: "P-521", hash: "SHA-512", scheme: "dhSinglePass-stdDH-sha512kdf-scheme" };
|
|
144
|
-
var MONT_KA = {};
|
|
106
|
+
var MONT_KA = {};
|
|
145
107
|
MONT_KA[O("X25519")] = { name: "X25519", hkdf: "SHA-256", scheme: "dhSinglePass-stdDH-hkdf-sha256-scheme" };
|
|
146
108
|
MONT_KA[O("X448")] = { name: "X448", hkdf: "SHA-512", scheme: "dhSinglePass-stdDH-hkdf-sha512-scheme" };
|
|
147
109
|
|
|
148
|
-
// ECC-CMS-SharedInfo ::= SEQUENCE { keyInfo AlgorithmIdentifier (the wrap, params ABSENT),
|
|
149
|
-
// entityUInfo [0] EXPLICIT OCTET STRING OPTIONAL, suppPubInfo [2] EXPLICIT OCTET STRING }, where
|
|
150
|
-
// suppPubInfo is the KEK length in bits, 4-octet big-endian (RFC 5753 sec. 7.2). One builder,
|
|
151
|
-
// shared by encrypt + decrypt so the two sides cannot diverge.
|
|
152
110
|
function _eccSharedInfo(wrapName, ukm, kekBytes) {
|
|
153
111
|
var kids = [_algId(wrapName, "absent")];
|
|
154
112
|
if (ukm) kids.push(b.explicit(0, b.octetString(ukm)));
|
|
@@ -161,9 +119,6 @@ async function _buildKari(cek, cert, opts) {
|
|
|
161
119
|
var keyAlg = cert.subjectPublicKeyInfo.algorithm;
|
|
162
120
|
var wrapName = _wrapOidForKek(cek.length);
|
|
163
121
|
var ukm = opts.ukm ? guard.bytes.view(opts.ukm, CmsError, "cms/bad-input", "ukm") : null;
|
|
164
|
-
// z / mz (the raw agreement secret) and the derived kek are all allocated here and all secret.
|
|
165
|
-
// Declared together so one `finally` clears whichever the chosen branch produced, including when
|
|
166
|
-
// a later encoding step throws (RFC 9629 sec. 7 asks the same of the KEM builder below).
|
|
167
122
|
var ecdhPub, origKeyAlgId, kek, z, mz;
|
|
168
123
|
try {
|
|
169
124
|
if (keyAlg.oid === O("ecPublicKey")) {
|
|
@@ -186,23 +141,14 @@ async function _buildKari(cek, cert, opts) {
|
|
|
186
141
|
mz = Buffer.from(await subtle.deriveBits({ name: mka.name, public: rPub }, meph.privateKey, null));
|
|
187
142
|
if (mz.every(function (x) { return x === 0; })) throw _err("cms/bad-input", "the X25519/X448 shared secret is all-zero (low-order point)");
|
|
188
143
|
var mzKey = await subtle.importKey("raw", mz, { name: "HKDF" }, false, ["deriveBits"]);
|
|
189
|
-
// RFC 8418 sec. 2.2: when a ukm is present it is used as both the HKDF salt and the
|
|
190
|
-
// ECC-CMS-SharedInfo entityUInfo (the HKDF info). Omitting it from the info diverges the KEK
|
|
191
|
-
// from any conformant peer that reads the transmitted ukm.
|
|
192
144
|
kek = Buffer.from(await subtle.deriveBits({ name: "HKDF", hash: mka.hkdf, salt: ukm || Buffer.alloc(0), info: _eccSharedInfo(wrapName, ukm, cek.length) }, mzKey, cek.length * 8));
|
|
193
145
|
} else {
|
|
194
|
-
// Coverage residual: unreachable, since _buildRecipient routes only ecPublicKey / X25519 / X448
|
|
195
|
-
// keys into _buildKari; a defensive throw against a future dispatch change.
|
|
196
146
|
throw _err("cms/unsupported-algorithm", "unsupported recipient key algorithm for kari");
|
|
197
147
|
}
|
|
198
148
|
var encryptedKey = await _aesKwWrap(kek, cek);
|
|
199
|
-
// originatorKey [1] IMPLICIT OriginatorPublicKey { algorithm, publicKey BIT STRING }.
|
|
200
149
|
var origSpki = asn1.decode(origKeyAlgId.spki);
|
|
201
|
-
var origPubBits = origSpki.children[1];
|
|
150
|
+
var origPubBits = origSpki.children[1];
|
|
202
151
|
var originatorKey = b.contextConstructed(1, Buffer.concat([origSpki.children[0].bytes, origPubBits.bytes]));
|
|
203
|
-
// KeyAgreeRecipientIdentifier CHOICE { issuerAndSerialNumber, rKeyId [0] IMPLICIT
|
|
204
|
-
// RecipientKeyIdentifier }, where the SKI form wraps a SEQUENCE (rKeyId), unlike ktri's bare
|
|
205
|
-
// subjectKeyIdentifier [0] IMPLICIT OCTET STRING.
|
|
206
152
|
var ridNode;
|
|
207
153
|
_assertKeyIdentifier(opts.keyIdentifier);
|
|
208
154
|
if (opts.keyIdentifier === "subjectKeyIdentifier") {
|
|
@@ -212,21 +158,17 @@ async function _buildKari(cek, cert, opts) {
|
|
|
212
158
|
} else {
|
|
213
159
|
ridNode = b.sequence([b.raw(cert.issuer.bytes), b.integer(cert.serialNumber)]);
|
|
214
160
|
}
|
|
215
|
-
var rek = b.sequence([b.sequence([ridNode, b.octetString(encryptedKey)])]);
|
|
161
|
+
var rek = b.sequence([b.sequence([ridNode, b.octetString(encryptedKey)])]);
|
|
216
162
|
var kekAlg = b.sequence([b.oid(O(origKeyAlgId.scheme)), _algId(wrapName, "absent")]);
|
|
217
163
|
var kariKids = [b.integer(3n), b.explicit(0, originatorKey)];
|
|
218
164
|
if (ukm) kariKids.push(b.explicit(1, b.octetString(ukm)));
|
|
219
165
|
kariKids.push(kekAlg, rek);
|
|
220
166
|
return { tag: 1, node: b.sequence(kariKids) };
|
|
221
167
|
} finally {
|
|
222
|
-
// Buffer.from(arrayBuffer) is a VIEW over the engine's returned buffer, so clearing z / mz
|
|
223
|
-
// clears that allocation too. The CEK belongs to the caller (the message, not this recipient)
|
|
224
|
-
// and is wiped once at the end of encrypt; the ephemeral PUBLIC key is not secret.
|
|
225
168
|
guard.secret.zeroizeAll([z, mz, kek], CmsError, "cms/bad-input", "the key-agreement shared secret");
|
|
226
169
|
}
|
|
227
170
|
}
|
|
228
171
|
|
|
229
|
-
// ---- kekri (symmetric KEK) : AES-KW --------------------------------------
|
|
230
172
|
async function _buildKekri(cek, desc) {
|
|
231
173
|
var kek = guard.bytes.view(desc.kek, CmsError, "cms/bad-input", "kek");
|
|
232
174
|
if (desc.kekId == null) throw _err("cms/bad-input", "a kek recipient needs a kekId");
|
|
@@ -236,33 +178,20 @@ async function _buildKekri(cek, desc) {
|
|
|
236
178
|
return { tag: 2, node: b.sequence([b.integer(4n), kekid, _algId(wrapName, "absent"), b.octetString(encryptedKey)]) };
|
|
237
179
|
}
|
|
238
180
|
|
|
239
|
-
// A PBKDF2 iterationCount MUST be a positive integer within the same cap the decryptor enforces, so a
|
|
240
|
-
// ---- pwri (password) : PBKDF2 + RFC 3211 double-CBC PWRI-KEK ---------------
|
|
241
181
|
async function _buildPwri(cek, desc) {
|
|
242
|
-
// A string / Uint8Array password is encoded into a buffer this toolkit allocated, a credential
|
|
243
|
-
// copy, so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
|
|
244
|
-
// borrowed and left intact.
|
|
245
|
-
// The option validation runs before the password is encoded: a rejected iteration count or salt
|
|
246
|
-
// would otherwise abandon an owned credential copy on the way out.
|
|
247
182
|
var iterations = pbes2.assertIterations(desc.iterations == null ? 600000 : desc.iterations, _err, "cms");
|
|
248
183
|
var salt = desc.salt ? pbes2.assertSalt(guard.bytes.view(desc.salt, CmsError, "cms/bad-input", "salt"), _err, "cms") : nodeCrypto.randomBytes(16);
|
|
249
184
|
var prf = desc.prf || "hmacWithSHA256";
|
|
250
|
-
_prfHash(prf);
|
|
251
|
-
var innerKeyBytes = 32;
|
|
185
|
+
_prfHash(prf);
|
|
186
|
+
var innerKeyBytes = 32;
|
|
252
187
|
var pwOwn = pbes2.passwordBytesOwned(desc.password, _err, "cms");
|
|
253
188
|
var password = pwOwn.bytes;
|
|
254
189
|
var kekKey = await subtle.importKey("raw", password, { name: "PBKDF2" }, false, ["deriveBits"]);
|
|
255
|
-
// The derived KEK is this function's allocation and is cleared below. `password` is not: when the
|
|
256
|
-
// caller passes a Buffer, passwordBytes hands back that very buffer, so wiping it would destroy
|
|
257
|
-
// the caller's own memory, the one failure this rule must never cause.
|
|
258
190
|
var kek = Buffer.from(await subtle.deriveBits({ name: "PBKDF2", hash: _prfHash(prf), salt: salt, iterations: iterations }, kekKey, innerKeyBytes * 8));
|
|
259
191
|
if (pwOwn.owned) guard.secret.zeroize(password, CmsError, "cms/bad-input", "the password encoding");
|
|
260
192
|
try {
|
|
261
|
-
// The RFC 3211 double-CBC wrap under an inner AES-256-CBC whose IV is carried in the
|
|
262
|
-
// keyEncryptionAlgorithm parameter (id-alg-PWRI-KEK parameter = the inner cipher AlgorithmIdentifier).
|
|
263
193
|
var iv = nodeCrypto.randomBytes(16);
|
|
264
194
|
var encryptedKey = _pwriWrapIv(kek, cek, iv);
|
|
265
|
-
// PBKDF2-params as keyDerivationAlgorithm [0] IMPLICIT.
|
|
266
195
|
var kdfParams = pbes2.pbkdf2ParamsSeq(salt, iterations, prf);
|
|
267
196
|
var kdfAlg = b.contextConstructed(0, Buffer.concat([b.oid(O("pbkdf2")), kdfParams]));
|
|
268
197
|
var keyEncAlg = b.sequence([b.oid(O("id-alg-PWRI-KEK")), b.sequence([b.oid(O("aes256-CBC")), b.octetString(iv)])]);
|
|
@@ -272,15 +201,12 @@ async function _buildPwri(cek, desc) {
|
|
|
272
201
|
}
|
|
273
202
|
}
|
|
274
203
|
|
|
275
|
-
|
|
276
|
-
var KEM_WRAP = {}; // ML-KEM OID -> wrap name (RFC 9936 sec. 2.2.1)
|
|
204
|
+
var KEM_WRAP = {};
|
|
277
205
|
KEM_WRAP[O("id-ml-kem-512")] = "aes128-wrap";
|
|
278
206
|
KEM_WRAP[O("id-ml-kem-768")] = "aes256-wrap";
|
|
279
207
|
KEM_WRAP[O("id-ml-kem-1024")] = "aes256-wrap";
|
|
280
208
|
var KEM_WC = {}; KEM_WC[O("id-ml-kem-512")] = "ML-KEM-512"; KEM_WC[O("id-ml-kem-768")] = "ML-KEM-768"; KEM_WC[O("id-ml-kem-1024")] = "ML-KEM-1024";
|
|
281
209
|
|
|
282
|
-
// CMSORIforKEMOtherInfo ::= SEQUENCE { wrap AlgorithmIdentifier, kekLength INTEGER,
|
|
283
|
-
// ukm [0] EXPLICIT OCTET STRING OPTIONAL }, the RFC 9629 sec. 5 KDF info, one builder both sides.
|
|
284
210
|
function _kemOtherInfo(wrapName, kekBytes, ukm) {
|
|
285
211
|
var kids = [_algId(wrapName, "absent"), b.integer(BigInt(kekBytes))];
|
|
286
212
|
if (ukm) kids.push(b.explicit(0, b.octetString(ukm)));
|
|
@@ -310,46 +236,30 @@ async function _buildKemri(cek, cert, opts) {
|
|
|
310
236
|
var kemri = b.sequence(kemriKids);
|
|
311
237
|
return { tag: 4, node: b.sequence([b.oid(O("kem")), kemri]) };
|
|
312
238
|
} finally {
|
|
313
|
-
// RFC 9629 sec. 7 asks the SENDER to discard the shared secret and KEK once the recipient entry
|
|
314
|
-
// is built, and to use a fresh secret per recipient, so this runs per call and not once at
|
|
315
|
-
// the end of a multi-recipient message. In a `finally`, so a wrap or encoding failure does not
|
|
316
|
-
// leave them behind. The CEK is the caller's and is wiped by no one here; kemct is public.
|
|
317
|
-
// kem.sharedKey is the ArrayBuffer the engine returned and ss is this function's copy of it;
|
|
318
|
-
// both hold the secret, so both are cleared. A Uint8Array view aliases the buffer's bytes, so
|
|
319
|
-
// wiping the view wipes the buffer itself. kem.ciphertext is public and stays.
|
|
320
239
|
guard.secret.zeroizeAll([ss, kek, kem.sharedKey ? new Uint8Array(kem.sharedKey) : null, kekAb ? new Uint8Array(kekAb) : null],
|
|
321
240
|
CmsError, "cms/bad-input", "the KEM shared secret");
|
|
322
241
|
}
|
|
323
242
|
}
|
|
324
243
|
|
|
325
|
-
// AES-KW wrap of the CEK under a raw KEK.
|
|
326
244
|
async function _aesKwWrap(kek, cek) {
|
|
327
245
|
var kekKey = await subtle.importKey("raw", kek, { name: "AES-KW" }, false, ["wrapKey"]);
|
|
328
246
|
var cekKey = await subtle.importKey("raw", cek, { name: "AES-CBC" }, true, ["encrypt", "decrypt"]);
|
|
329
247
|
return Buffer.from(await subtle.wrapKey("raw", cekKey, kekKey, { name: "AES-KW" }));
|
|
330
248
|
}
|
|
331
249
|
|
|
332
|
-
// RFC 3211 sec. 2.3.1 wrap formatting + double-CBC (sec. 2.3.2).
|
|
333
250
|
function _pwriFormat(cek) {
|
|
334
251
|
var count = cek.length;
|
|
335
|
-
// Coverage residual: for the AES CEK sizes the toolkit produces (16/24/32), `count` is always in
|
|
336
|
-
// range, `body` (4 + count) is never a 16-octet multiple, and `body + padLen` always reaches the
|
|
337
|
-
// 2-block minimum -- so the out-of-range throw and the two zero/underflow pad arms never fire.
|
|
338
252
|
if (count < 1 || count > 255) throw _err("cms/bad-input", "the CEK length is out of the RFC 3211 range");
|
|
339
253
|
var check = Buffer.from([count, cek[0] ^ 0xff, cek[1] ^ 0xff, cek[2] ^ 0xff]);
|
|
340
254
|
var body = Buffer.concat([check, cek]);
|
|
341
255
|
var blk = 16;
|
|
342
256
|
var padLen = body.length % blk === 0 ? 0 : blk - (body.length % blk);
|
|
343
257
|
if (body.length + padLen < 2 * blk) padLen += (2 * blk - (body.length + padLen));
|
|
344
|
-
// and each hold a plaintext copy of the CEK; only the padded result is returned, so
|
|
345
|
-
// the intermediates are cleared here and never abandoned.
|
|
346
258
|
var wrapped = Buffer.concat([body, nodeCrypto.randomBytes(padLen)]);
|
|
347
259
|
guard.secret.zeroizeAll([check, body], CmsError, "cms/bad-input", "a PWRI formatting intermediate");
|
|
348
260
|
return wrapped;
|
|
349
261
|
}
|
|
350
262
|
function _pwriWrapIv(kek, cek, iv) {
|
|
351
|
-
// wk is the formatted plaintext -- a complete copy of the CEK -- and is cleared once the first
|
|
352
|
-
// encryption pass has consumed it. pass1 is already ciphertext.
|
|
353
263
|
var wk = _pwriFormat(cek);
|
|
354
264
|
try {
|
|
355
265
|
var c1 = nodeCrypto.createCipheriv("aes-256-cbc", kek, iv); c1.setAutoPadding(false);
|
|
@@ -365,27 +275,11 @@ function _pwriWrapIv(kek, cek, iv) {
|
|
|
365
275
|
var PRF_HASH = { hmacWithSHA1: "SHA-1", hmacWithSHA256: "SHA-256", hmacWithSHA384: "SHA-384", hmacWithSHA512: "SHA-512" };
|
|
366
276
|
function _prfHash(prf) { if (!PRF_HASH[prf]) throw _err("cms/bad-input", "unsupported pwri prf " + JSON.stringify(prf)); return PRF_HASH[prf]; }
|
|
367
277
|
|
|
368
|
-
// ---- recipient dispatch ----------------------------------------------------
|
|
369
|
-
// The fields a recipient descriptor carries, PER ARM. Each arm reads a disjoint set, and every
|
|
370
|
-
// field beyond the selector is optional with a default, so an unrecognized name is missed by
|
|
371
|
-
// nothing and the recipient is built with the shipped parameters instead: `oaepHsh` for `oaepHash`
|
|
372
|
-
// encrypts the content key under the default SHA-256 OAEP, and a misspelled `iterations`, `salt`
|
|
373
|
-
// or `prf` on a password recipient derives the key at the default work factor. A union table would
|
|
374
|
-
// admit another arm's fields, which the chosen arm never reads.
|
|
375
278
|
var _PWRI_KEYS = { password: 1, iterations: 1, prf: 1, salt: 1 };
|
|
376
279
|
var _KEKRI_KEYS = { kek: 1, kekId: 1 };
|
|
377
|
-
// A certificate recipient dispatches on the certificate's KEY ALGORITHM, and the arms read
|
|
378
|
-
// different fields: ktri (RSA) reads oaepHash and never ukm; kari (ECDH / X25519 / X448) and kemri
|
|
379
|
-
// (ML-KEM) read ukm and never oaepHash. One table across all three accepted `oaepHash` on an EC
|
|
380
|
-
// certificate, where nothing reads it, so the content key was wrapped under the arm's own defaults
|
|
381
|
-
// while the caller had named a hash. Checked after the key algorithm resolves, since that is what
|
|
382
|
-
// selects the arm.
|
|
383
280
|
var _KTRI_RECIPIENT_KEYS = { cert: 1, oaepHash: 1, keyIdentifier: 1 };
|
|
384
281
|
var _AGREE_RECIPIENT_KEYS = { cert: 1, keyIdentifier: 1, ukm: 1 };
|
|
385
282
|
|
|
386
|
-
// The three top-level options that are per-recipient DEFAULTS rather than message-wide settings.
|
|
387
|
-
// Which of them any given message actually consumes depends on the recipient arms it selects, so
|
|
388
|
-
// they are checked against the arms after those resolve, not against a fixed list.
|
|
389
283
|
var _RECIPIENT_DEFAULTS = ["oaepHash", "keyIdentifier", "ukm"];
|
|
390
284
|
|
|
391
285
|
function _assertRecipientKeys(desc, known, arm) {
|
|
@@ -395,9 +289,6 @@ function _assertRecipientKeys(desc, known, arm) {
|
|
|
395
289
|
});
|
|
396
290
|
}
|
|
397
291
|
|
|
398
|
-
// A default no selected recipient reads is discarded exactly as a misspelling would be: `oaepHash`
|
|
399
|
-
// on a message whose only recipient is a password recipient names a wrapping the message does not
|
|
400
|
-
// perform. Run after every recipient's arm has resolved and BEFORE anything is emitted.
|
|
401
292
|
function _assertDefaultsConsumed(opts, consumed) {
|
|
402
293
|
for (var i = 0; i < _RECIPIENT_DEFAULTS.length; i++) {
|
|
403
294
|
var name = _RECIPIENT_DEFAULTS[i];
|
|
@@ -412,9 +303,6 @@ function _assertDefaultsConsumed(opts, consumed) {
|
|
|
412
303
|
|
|
413
304
|
async function _buildRecipient(cek, desc, opts, consumed) {
|
|
414
305
|
if (desc == null || typeof desc !== "object") throw _err("cms/bad-input", "each recipient must be a descriptor object");
|
|
415
|
-
// Checked per arm, on the same selector the dispatch below uses, so the table that runs is the
|
|
416
|
-
// one belonging to the arm that will build. `consumed` records which top-level defaults the
|
|
417
|
-
// selected arms read, so a default no recipient consumes can be refused once they all resolve.
|
|
418
306
|
if (desc.password != null) { _assertRecipientKeys(desc, _PWRI_KEYS, "password"); return _buildPwri(cek, desc); }
|
|
419
307
|
if (desc.kek != null) { _assertRecipientKeys(desc, _KEKRI_KEYS, "KEK"); return _buildKekri(cek, desc); }
|
|
420
308
|
if (desc.cert != null) {
|
|
@@ -436,10 +324,6 @@ async function _buildRecipient(cek, desc, opts, consumed) {
|
|
|
436
324
|
}
|
|
437
325
|
throw _err("cms/bad-input", "a recipient needs { cert }, { password }, or { kek, kekId }");
|
|
438
326
|
}
|
|
439
|
-
// Each default is taken from the descriptor when it supplies one and from the top-level options
|
|
440
|
-
// otherwise. That one choice is also the record of which top-level defaults the recipient CONSUMED:
|
|
441
|
-
// an arm that does not read the name never consumes it, and neither does one whose descriptor
|
|
442
|
-
// overrode it, so recording it from the arm's table alone would accept a value nothing read.
|
|
443
327
|
function _pickDefault(desc, opts, name, known, consumed, nullish) {
|
|
444
328
|
var supplied = desc[name], fromDesc = nullish ? supplied != null : !!supplied;
|
|
445
329
|
if (!fromDesc && known[name] && consumed && opts[name] != null) consumed[name] = 1;
|
|
@@ -452,29 +336,18 @@ function mergeOpts(opts, desc, known, consumed) {
|
|
|
452
336
|
ukm: _pickDefault(desc, opts, "ukm", known, consumed, true),
|
|
453
337
|
};
|
|
454
338
|
}
|
|
455
|
-
// RecipientInfo CHOICE: ktri untagged; kari [1], kekri [2], pwri [3], ori [4], all IMPLICIT,
|
|
456
|
-
// so the arm's SEQUENCE tag is replaced by the context tag.
|
|
457
339
|
function _taggedRecipient(r) {
|
|
458
340
|
if (r.tag == null) return r.node;
|
|
459
341
|
return b.contextConstructed(r.tag, r.node.subarray(_tlvHeaderLen(r.node)));
|
|
460
342
|
}
|
|
461
343
|
|
|
462
|
-
// EnvelopedData version (RFC 5652 sec. 6.1): any pwri/ori -> 3; else any ri v2 or a kari/kekri
|
|
463
|
-
// (v3/v4) -> 2; else 0. We never emit originatorInfo/unprotectedAttrs by default.
|
|
464
344
|
function _envelopedVersion(recips, hasUnprotected) {
|
|
465
|
-
var anyOri = recips.some(function (r) { return r.tag === 4 || r.tag === 3; });
|
|
345
|
+
var anyOri = recips.some(function (r) { return r.tag === 4 || r.tag === 3; });
|
|
466
346
|
if (anyOri) return 3;
|
|
467
347
|
var forcesTwo = hasUnprotected || recips.some(function (r) { return r.tag === 1 || r.tag === 2 || (r.tag == null && r._riVersion === 2); });
|
|
468
348
|
return forcesTwo ? 2 : 0;
|
|
469
349
|
}
|
|
470
350
|
|
|
471
|
-
// The two exported verbs have DISJOINT option sets, so they get separate tables: one shared table
|
|
472
|
-
// would admit every other verb's keys, which is the hole rather than the door. Each is the union of
|
|
473
|
-
// what the verb reads THROUGH ITS DELEGATIONS -- encrypt never names oaepHash, keyIdentifier or ukm
|
|
474
|
-
// itself, but hands opts to the per-recipient builders that do, and to _emit for pem.
|
|
475
|
-
// Authenticated attributes live in an AuthEnvelopedData, which only an AEAD content algorithm
|
|
476
|
-
// produces: a CBC algorithm selects EnvelopedData, which has no field for them and is not handed
|
|
477
|
-
// opts at all. Requested there, they named a protection the message does not carry.
|
|
478
351
|
var _ENCRYPT_OPTS_AEAD = {
|
|
479
352
|
contentEncryptionAlgorithm: 1, contentType: 1, authAttrs: 1, oaepHash: 1, keyIdentifier: 1,
|
|
480
353
|
ukm: 1, pem: 1,
|
|
@@ -482,8 +355,6 @@ var _ENCRYPT_OPTS_AEAD = {
|
|
|
482
355
|
var _ENCRYPT_OPTS_CBC = {
|
|
483
356
|
contentEncryptionAlgorithm: 1, contentType: 1, oaepHash: 1, keyIdentifier: 1, ukm: 1, pem: 1,
|
|
484
357
|
};
|
|
485
|
-
// authenticatedAttributes: false MACs the content octets directly, and the attribute set is what
|
|
486
|
-
// carries the digest: neither the digest algorithm nor any extra attribute is read on that branch.
|
|
487
358
|
var _AUTHENTICATE_OPTS_ATTRS = {
|
|
488
359
|
macAlgorithm: 1, digestAlgorithm: 1, authenticatedAttributes: 1, authAttrs: 1, contentType: 1,
|
|
489
360
|
keyIdentifier: 1, oaepHash: 1, ukm: 1, pem: 1,
|
|
@@ -494,12 +365,6 @@ var _AUTHENTICATE_OPTS_BARE = {
|
|
|
494
365
|
};
|
|
495
366
|
|
|
496
367
|
function encrypt(content, recipients, opts) {
|
|
497
|
-
// The options and recipients are copied BEFORE the content is classified. Classifying reads
|
|
498
|
-
// content[Symbol.asyncIterator] -- a caller accessor that runs in-realm -- which could otherwise
|
|
499
|
-
// change the content type, the output encoding, or a recipient after the call has begun. With the
|
|
500
|
-
// copies taken first, that probe cannot reach them; the streamed sign path copies the same way. The
|
|
501
|
-
// copy runs inside guard-async's boundary, so a fault in it leaves as a rejection, and the copies
|
|
502
|
-
// (which may hold a symmetric key or a password) are cleared on every path out.
|
|
503
368
|
return guard.bytes.fixedCall(CmsError, "cms/bad-input", [
|
|
504
369
|
[recipients, "recipients"], [opts, "pki.cms.encrypt options"],
|
|
505
370
|
], function (recipientsCopy, optsCopy) {
|
|
@@ -509,8 +374,6 @@ function encrypt(content, recipients, opts) {
|
|
|
509
374
|
|
|
510
375
|
async function _encryptImpl(content, recipients, opts) {
|
|
511
376
|
opts = opts || {};
|
|
512
|
-
// The content algorithm selects the structure, so it is resolved before the door and the table
|
|
513
|
-
// that runs is the one belonging to the structure this call will emit.
|
|
514
377
|
var algName = opts.contentEncryptionAlgorithm || "aes-256-gcm";
|
|
515
378
|
var ca = CONTENT_ALGS[algName];
|
|
516
379
|
if (!ca) throw _err("cms/bad-input", "unsupported contentEncryptionAlgorithm " + JSON.stringify(algName));
|
|
@@ -521,19 +384,12 @@ async function _encryptImpl(content, recipients, opts) {
|
|
|
521
384
|
? " for a CBC content algorithm; authenticated attributes are carried only by an AuthEnvelopedData, which an AEAD algorithm produces"
|
|
522
385
|
: "");
|
|
523
386
|
});
|
|
524
|
-
// An async-iterable content streams the plaintext through the cipher without buffering it whole; a byte
|
|
525
|
-
// source takes the one-shot path. asyncStreamOf reads Symbol.asyncIterator once and wins only when the
|
|
526
|
-
// value is not already a byte source, so a Buffer with a stray asyncIterator stays a Buffer.
|
|
527
387
|
var contentStream = guard.bytes.asyncStreamOf(content);
|
|
528
388
|
var contentBytes = contentStream ? null : guard.bytes.view(content, CmsError, "cms/bad-input", "content");
|
|
529
389
|
var contentType = opts.contentType || "data";
|
|
530
390
|
var cek = nodeCrypto.randomBytes(ca.keyBits / 8);
|
|
531
391
|
|
|
532
|
-
// The CEK protects the content for every recipient, so unlike a per-recipient shared secret it is
|
|
533
|
-
// cleared once, after the last recipient entry is built and the content is encrypted. Wiping it
|
|
534
|
-
// inside the recipient loop would destroy the key the remaining recipients must be given.
|
|
535
392
|
try {
|
|
536
|
-
// EncryptedData: a single non-array { cek } or { password } descriptor, no RecipientInfos.
|
|
537
393
|
if (!Array.isArray(recipients)) return await _encryptedData(contentBytes, contentStream, recipients, ca, contentType, opts, cek);
|
|
538
394
|
|
|
539
395
|
if (!recipients.length) throw _err("cms/bad-input", "at least one recipient is required (RFC 5652 sec. 6.1)");
|
|
@@ -555,9 +411,6 @@ function _emit(inner, ctName, opts) {
|
|
|
555
411
|
return opts.pem ? schemaCms.pemEncode(ci, "CMS") : ci;
|
|
556
412
|
}
|
|
557
413
|
|
|
558
|
-
// Run a streamed plaintext through the content cipher, translating the engine's malformed-streamed-content
|
|
559
|
-
// domain into this verb's cms/bad-input via the shared guard, so pki.cms.encrypt keeps its typed CmsError
|
|
560
|
-
// contract (the streamed sign/verify paths translate the same way).
|
|
561
414
|
async function _encStream(spec, stream) {
|
|
562
415
|
try {
|
|
563
416
|
return await subtle.encryptStream(spec, stream);
|
|
@@ -568,9 +421,6 @@ async function _encStream(spec, stream) {
|
|
|
568
421
|
|
|
569
422
|
async function _envelopedData(contentBytes, contentStream, cek, ca, contentType, riNodes, recips) {
|
|
570
423
|
var iv = nodeCrypto.randomBytes(16);
|
|
571
|
-
// A streamed plaintext runs through the CBC cipher incrementally (assembling only the ciphertext, whose
|
|
572
|
-
// length the encryptedContent OCTET STRING needs); a Buffer takes the one-shot path. Both produce the
|
|
573
|
-
// same wire bytes for the same plaintext, so the emitted DER is identical to the buffered form.
|
|
574
424
|
var enc = contentStream
|
|
575
425
|
? (await _encStream({ algNode: "aes-" + ca.keyBits + "-cbc", key: cek, iv: iv }, contentStream)).ciphertext
|
|
576
426
|
: pbes2.cbcEncrypt(cek, iv, contentBytes, ca.keyBits, CmsError, "cms/bad-input");
|
|
@@ -578,23 +428,7 @@ async function _envelopedData(contentBytes, contentStream, cek, ca, contentType,
|
|
|
578
428
|
return b.sequence([b.integer(BigInt(_envelopedVersion(recips, false))), b.setOf(riNodes), eci]);
|
|
579
429
|
}
|
|
580
430
|
|
|
581
|
-
// Every authenticated attribute this module MACs -- auto-built or caller-supplied, in either content
|
|
582
|
-
// type that carries them -- MUST be a well-formed Attribute SEQUENCE { type OBJECT IDENTIFIER,
|
|
583
|
-
// non-empty SET OF value } with no repeated type (RFC 5652 sec. 5.3). Rejecting here means a
|
|
584
|
-
// malformed attribute never reaches the wire; leaving it to the wire means the operator learns from
|
|
585
|
-
// a peer's parser that the message they built is unreadable, with the MAC already computed over it.
|
|
586
|
-
//
|
|
587
|
-
// `forbidden` names an attribute type this particular content type must not emit, with the clause
|
|
588
|
-
// that says so. It is a parameter rather than a constant because the same attribute is required in
|
|
589
|
-
// one content type and discouraged in the other: AuthenticatedData MACs its own message-digest by
|
|
590
|
-
// design (RFC 5652 sec. 9.2), while for AuthEnvelopedData that value is the unencrypted hash of a
|
|
591
|
-
// plaintext the message exists to conceal (RFC 5083 sec. 2.1 / sec. 5).
|
|
592
431
|
function _assertAuthAttrs(pairs, forbidden) {
|
|
593
|
-
// Reject a sparse/nullish attribute array as a typed error before b.setOf reaches it as a native one.
|
|
594
|
-
// This gate runs before the setOf on both the AuthEnveloped and AuthenticatedData paths.
|
|
595
|
-
// Validate AND SNAPSHOT: reqDenseArray returns a fresh dense copy, and it is that copy the caller must
|
|
596
|
-
// iterate and encode. Iterating the original `pairs` would let an indexed getter that sparsified it
|
|
597
|
-
// during validation slip a hole into b.setOf as a native error; the returned snapshot cannot.
|
|
598
432
|
var dense = pkiBuild.reqDenseArray(pairs, "authAttrs", _err, "cms/bad-input");
|
|
599
433
|
var seenTypes = {};
|
|
600
434
|
dense.forEach(function (p) {
|
|
@@ -614,9 +448,6 @@ function _assertAuthAttrs(pairs, forbidden) {
|
|
|
614
448
|
return dense;
|
|
615
449
|
}
|
|
616
450
|
|
|
617
|
-
// The one attribute AuthEnvelopedData declines to emit. Its value is the unencrypted one-way hash of
|
|
618
|
-
// the plaintext, so disclosing it enables content tracking and confirms a guessed plaintext against
|
|
619
|
-
// a message that was encrypted to prevent exactly that.
|
|
620
451
|
var AUTH_ENVELOPED_FORBIDDEN = {
|
|
621
452
|
oid: O("messageDigest"),
|
|
622
453
|
why: "authAttrs must not carry the message-digest attribute in an AuthEnvelopedData: its value is the unencrypted hash of the plaintext, which enables content tracking and confirmation of a guessed plaintext (RFC 5083 sec. 2.1, sec. 5)",
|
|
@@ -625,22 +456,14 @@ var AUTH_ENVELOPED_FORBIDDEN = {
|
|
|
625
456
|
async function _authEnvelopedData(contentBytes, contentStream, cek, ca, contentType, opts, riNodes, recips) {
|
|
626
457
|
var nonce = nodeCrypto.randomBytes(12);
|
|
627
458
|
var authAttrsDer = null, aad = Buffer.alloc(0);
|
|
628
|
-
// RFC 5083 sec. 2.1: authAttrs MUST be present when the content type is not id-data. Emitting an
|
|
629
|
-
// AuthEnvelopedData without them for a non-data type produces a message our own strict parser rejects.
|
|
630
459
|
if (contentType !== "data" && !(opts.authAttrs && opts.authAttrs.length)) {
|
|
631
460
|
throw _err("cms/bad-input", "AuthEnvelopedData with a non-data contentType requires authAttrs (RFC 5083 sec. 2.1)");
|
|
632
461
|
}
|
|
633
462
|
if (opts.authAttrs && opts.authAttrs.length) {
|
|
634
463
|
var denseAttrs = _assertAuthAttrs(opts.authAttrs, AUTH_ENVELOPED_FORBIDDEN);
|
|
635
|
-
// authAttrs are transmitted [1] IMPLICIT but MACed under the EXPLICIT SET OF tag (RFC 5083 sec. 2.2).
|
|
636
464
|
var setOf = b.setOf(denseAttrs);
|
|
637
465
|
aad = setOf; authAttrsDer = b.contextConstructed(1, setOf.subarray(_tlvHeaderLen(setOf)));
|
|
638
466
|
}
|
|
639
|
-
// A 16-octet (128-bit) GCM tag, the strongest ICV and what OpenSSL emits, so the message
|
|
640
|
-
// interops across OpenSSL 3.5 / 4.x. The aes-ICVlen (16) is carried explicitly (RFC 5084 sec. 3.2
|
|
641
|
-
// omits it only when it equals the DEFAULT 12); it MUST equal the mac octet length (M42).
|
|
642
|
-
// A streamed plaintext runs through the GCM cipher incrementally; the tag is produced at final() over the
|
|
643
|
-
// same bytes, so the ciphertext + tag equal the one-shot form for the same plaintext and AAD.
|
|
644
467
|
var g;
|
|
645
468
|
if (contentStream) {
|
|
646
469
|
var r = await _encStream({ algNode: "aes-" + ca.keyBits + "-gcm", key: cek, iv: nonce, aad: aad, authTagLength: 16 }, contentStream);
|
|
@@ -656,23 +479,12 @@ async function _authEnvelopedData(contentBytes, contentStream, cek, ca, contentT
|
|
|
656
479
|
return b.sequence(kids);
|
|
657
480
|
}
|
|
658
481
|
|
|
659
|
-
// The single descriptor an EncryptedData takes, PER ARM. `cek` and `password` select the arm and
|
|
660
|
-
// the arms read disjoint sets, so a union would admit the other arm's fields: `{ cek, iterations }`
|
|
661
|
-
// supplies a work factor to a branch that derives nothing, and the caller's parameter is discarded
|
|
662
|
-
// exactly as a misspelling would be. `pem` is read after both arms, so it belongs to each.
|
|
663
|
-
// pem selects the output encoding of the whole call and is an OPTION, the third argument, on every
|
|
664
|
-
// other verb here. A second spelling on the descriptor was read by the password arm alone, so the
|
|
665
|
-
// cek arm accepted it and returned DER; there is now one place to ask for PEM.
|
|
666
482
|
var _ENCRYPTED_DATA_CEK_KEYS = { cek: 1 };
|
|
667
483
|
var _ENCRYPTED_DATA_PW_KEYS = { password: 1, iterations: 1, salt: 1, prf: 1 };
|
|
668
484
|
|
|
669
485
|
async function _encryptedData(contentBytes, contentStream, desc, ca, contentType, opts, cek) {
|
|
670
486
|
if (ca.aead) throw _err("cms/bad-input", "EncryptedData supports only CBC content encryption");
|
|
671
|
-
// An EncryptedData carries no RecipientInfos at all, so nothing here reads a per-recipient
|
|
672
|
-
// default. This branch returns before the recipient loop, so it answers for them itself.
|
|
673
487
|
_assertDefaultsConsumed(opts, {});
|
|
674
|
-
// Checked per arm, on the same selector the dispatch below uses, so the table that runs belongs
|
|
675
|
-
// to the branch that will encrypt.
|
|
676
488
|
if (desc != null && typeof desc === "object") {
|
|
677
489
|
var edKeys = desc.cek != null ? _ENCRYPTED_DATA_CEK_KEYS : _ENCRYPTED_DATA_PW_KEYS;
|
|
678
490
|
var edArm = desc.cek != null ? "cek" : "password";
|
|
@@ -701,16 +513,11 @@ async function _encryptedData(contentBytes, contentStream, desc, ca, contentType
|
|
|
701
513
|
}
|
|
702
514
|
|
|
703
515
|
async function _encryptedDataPbes2(contentBytes, contentStream, desc, ca, contentType, iv, opts) {
|
|
704
|
-
// A string / Uint8Array password is encoded into a buffer this toolkit allocated, a credential
|
|
705
|
-
// copy, so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
|
|
706
|
-
// borrowed and left intact.
|
|
707
516
|
var pwOwn2 = pbes2.passwordBytesOwned(desc.password, _err, "cms");
|
|
708
517
|
var password = pwOwn2.bytes;
|
|
709
518
|
var iterations = pbes2.assertIterations(desc.iterations == null ? 600000 : desc.iterations, _err, "cms");
|
|
710
519
|
var salt = desc.salt ? pbes2.assertSalt(guard.bytes.view(desc.salt, CmsError, "cms/bad-input", "salt"), _err, "cms") : nodeCrypto.randomBytes(16);
|
|
711
520
|
var prf = desc.prf || "hmacWithSHA256";
|
|
712
|
-
// The password-derived content key is this function's allocation; `password` may be the caller's
|
|
713
|
-
// own buffer (passwordBytes passes a Buffer straight through) and is left alone.
|
|
714
521
|
var key = nodeCrypto.pbkdf2Sync(password, salt, iterations, ca.keyBits / 8, pbes2.prfNodeByName(prf, _err, "cms"));
|
|
715
522
|
if (pwOwn2.owned) guard.secret.zeroize(password, CmsError, "cms/bad-input", "the password encoding");
|
|
716
523
|
try {
|
|
@@ -726,7 +533,6 @@ async function _encryptedDataPbes2(contentBytes, contentStream, desc, ca, conten
|
|
|
726
533
|
}
|
|
727
534
|
}
|
|
728
535
|
|
|
729
|
-
// ---- content-encryption primitives ----------------------------------------
|
|
730
536
|
function _gcmEncrypt(key, nonce, plaintext, aad, keyBits, tagLen) {
|
|
731
537
|
var c = nodeCrypto.createCipheriv("aes-" + keyBits + "-gcm", key, nonce, { authTagLength: tagLen });
|
|
732
538
|
if (aad && aad.length) c.setAAD(aad);
|
|
@@ -734,38 +540,23 @@ function _gcmEncrypt(key, nonce, plaintext, aad, keyBits, tagLen) {
|
|
|
734
540
|
return { ct: ct, tag: c.getAuthTag() };
|
|
735
541
|
}
|
|
736
542
|
|
|
737
|
-
// The length of a DER TLV's tag+length header (so the SET OF re-tag drops the tag byte(s)).
|
|
738
543
|
function _tlvHeaderLen(der) {
|
|
739
544
|
var lenByte = der[1];
|
|
740
545
|
if (lenByte < 0x80) return 2;
|
|
741
546
|
return 2 + (lenByte & 0x7f);
|
|
742
547
|
}
|
|
743
548
|
|
|
744
|
-
// ---- pki.cms.authenticate (RFC 5652 sec. 9 AuthenticatedData) ---------------
|
|
745
|
-
// AuthenticatedData carries CLEARTEXT content plus a MAC (no content encryption). A single fresh HMAC
|
|
746
|
-
// key is minted, wrapped for every recipient with the EXACT RecipientInfo model EnvelopedData uses for
|
|
747
|
-
// the CEK, and the MAC (HMAC-SHA-2) covers either the re-tagged [2] authAttrs SET OF (default) or the
|
|
748
|
-
// eContent value octets directly. Homes here (not a new module) so the recipient-wrap helpers are
|
|
749
|
-
// module-local; the verify half folds into pki.cms.decrypt.
|
|
750
549
|
|
|
751
|
-
// The macAlgorithm registry: an OID name + the WebCrypto/node hashes (data-driven, not a switch).
|
|
752
550
|
var MAC_ALGS = {
|
|
753
551
|
"hmac-sha256": { oid: "hmacWithSHA256", wc: "SHA-256", node: "sha256" },
|
|
754
552
|
"hmac-sha384": { oid: "hmacWithSHA384", wc: "SHA-384", node: "sha384" },
|
|
755
553
|
"hmac-sha512": { oid: "hmacWithSHA512", wc: "SHA-512", node: "sha512" },
|
|
756
554
|
};
|
|
757
|
-
// The fresh MAC key length: 32 octets is AES-KW-compatible (16/24/32), so it wraps through every
|
|
758
|
-
// recipient arm (kari/kekri/kemri import the wrapped key as AES-CBC) with no codec change, and a
|
|
759
|
-
// 32-octet conveyed key is a valid HMAC key for SHA-256/384/512 (RFC 2104).
|
|
760
555
|
var MAC_KEY_OCTETS = 32;
|
|
761
|
-
// The message-digest hashes the AuthenticatedData verify path (cms-decrypt DIGEST_WC) can consume --
|
|
762
|
-
// the producer never emits a digestAlgorithm its own verifier cannot recompute (the no-orphan rule).
|
|
763
556
|
var SUPPORTED_DIGEST = { sha256: 1, sha384: 1, sha512: 1 };
|
|
764
557
|
|
|
765
558
|
async function authenticate(content, recipients, opts) {
|
|
766
559
|
opts = opts || {};
|
|
767
|
-
// The attribute branch is selected once, and the door and the MAC below both act on that one
|
|
768
|
-
// decision, so the table that runs belongs to the branch that will compute the MAC.
|
|
769
560
|
var withAttrs = opts.authenticatedAttributes !== false;
|
|
770
561
|
guard.identifier.assertKnownKeys(opts, withAttrs ? _AUTHENTICATE_OPTS_ATTRS : _AUTHENTICATE_OPTS_BARE,
|
|
771
562
|
_err, "cms/bad-input", function (k) {
|
|
@@ -780,11 +571,8 @@ async function authenticate(content, recipients, opts) {
|
|
|
780
571
|
if (!mac) throw _err("cms/bad-input", "unsupported macAlgorithm " + JSON.stringify(macName) + " (hmac-sha256/384/512)");
|
|
781
572
|
if (!Array.isArray(recipients) || !recipients.length) throw _err("cms/bad-input", "at least one recipient is required (RFC 5652 sec. 9.1)");
|
|
782
573
|
var contentType = opts.contentType || "data";
|
|
783
|
-
// RFC 5652 sec. 9.1: authAttrs MUST be present when the eContentType is not id-data.
|
|
784
574
|
if (contentType !== "data" && !withAttrs) throw _err("cms/bad-input", "AuthenticatedData with a non-data contentType requires authenticated attributes (RFC 5652 sec. 9.1)");
|
|
785
575
|
|
|
786
|
-
// The MAC key is this path's content-encryption key: every recipient is given it, so it is cleared
|
|
787
|
-
// once at the end and not per recipient, and only after the MAC has been computed over it.
|
|
788
576
|
var macKey = nodeCrypto.randomBytes(MAC_KEY_OCTETS);
|
|
789
577
|
try {
|
|
790
578
|
var recips = [];
|
|
@@ -795,12 +583,8 @@ async function authenticate(content, recipients, opts) {
|
|
|
795
583
|
|
|
796
584
|
var digestAlgTagged = null, authAttrsDer = null, preimage;
|
|
797
585
|
if (withAttrs) {
|
|
798
|
-
// Inside the branch that uses it: the attribute set is what carries the digest, so on the
|
|
799
|
-
// other branch there is no digest to name and opts.digestAlgorithm is refused at the door.
|
|
800
586
|
var digestName = opts.digestAlgorithm || mac.node;
|
|
801
587
|
if (!SUPPORTED_DIGEST[digestName]) throw _err("cms/bad-input", "unsupported digestAlgorithm " + JSON.stringify(digestName) + " (sha256/384/512)");
|
|
802
|
-
// sec. 9.2: content-type (== eContentType) + message-digest (== digest(content)) attributes,
|
|
803
|
-
// SET-OF-sorted, MACed under the EXPLICIT SET OF tag (0x31) but transmitted [2] IMPLICIT (0xA2).
|
|
804
588
|
var mdDigest = nodeCrypto.createHash(digestName).update(contentBytes).digest();
|
|
805
589
|
var pairs = [
|
|
806
590
|
b.sequence([b.oid(O("contentType")), b.setOf([b.oid(O(contentType))])]),
|
|
@@ -809,11 +593,11 @@ async function authenticate(content, recipients, opts) {
|
|
|
809
593
|
if (opts.authAttrs && opts.authAttrs.length) pairs = pairs.concat(opts.authAttrs);
|
|
810
594
|
pairs = _assertAuthAttrs(pairs, null);
|
|
811
595
|
var setOf = b.setOf(pairs);
|
|
812
|
-
preimage = setOf;
|
|
813
|
-
authAttrsDer = b.contextConstructed(2, setOf.subarray(_tlvHeaderLen(setOf)));
|
|
814
|
-
digestAlgTagged = b.contextConstructed(1, b.oid(O(digestName)));
|
|
596
|
+
preimage = setOf;
|
|
597
|
+
authAttrsDer = b.contextConstructed(2, setOf.subarray(_tlvHeaderLen(setOf)));
|
|
598
|
+
digestAlgTagged = b.contextConstructed(1, b.oid(O(digestName)));
|
|
815
599
|
} else {
|
|
816
|
-
preimage = contentBytes;
|
|
600
|
+
preimage = contentBytes;
|
|
817
601
|
}
|
|
818
602
|
|
|
819
603
|
var hmacKey = await subtle.importKey("raw", macKey, { name: "HMAC", hash: mac.wc }, false, ["sign"]);
|
|
@@ -826,10 +610,6 @@ async function authenticate(content, recipients, opts) {
|
|
|
826
610
|
if (authAttrsDer) kids.push(authAttrsDer);
|
|
827
611
|
kids.push(b.octetString(macValue));
|
|
828
612
|
var ci = b.sequence([b.oid(O("authData")), b.explicit(0, b.sequence(kids))]);
|
|
829
|
-
// Self-verify: the emitted AuthenticatedData MUST re-parse clean through the strict parser, so a
|
|
830
|
-
// caller-supplied authenticated attribute with a recognized type but extra RFC 5652 constraints (a
|
|
831
|
-
// misplaced type, a signing-time whose value is not a Time) is rejected HERE at build time rather
|
|
832
|
-
// than by the recipient's parser after the fact.
|
|
833
613
|
try { schemaCms.parse(ci); } catch (e) { throw (e instanceof CmsError) ? e : _err("cms/bad-input", "the supplied authenticated attributes produced an invalid AuthenticatedData", e); }
|
|
834
614
|
return opts.pem ? schemaCms.pemEncode(ci, "CMS") : ci;
|
|
835
615
|
} finally {
|