@blamejs/pki 0.5.7 → 0.5.8
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 +405 -386
- package/MIGRATING.md +43 -0
- package/README.md +12 -12
- package/lib/acme.js +31 -31
- package/lib/asn1-der.js +10 -10
- package/lib/attrcert-sign.js +19 -19
- package/lib/byte-reader.js +6 -6
- package/lib/byte-writer.js +5 -5
- package/lib/cbor-det.js +27 -24
- package/lib/cmc-build.js +97 -92
- package/lib/cmc-verify.js +106 -30
- package/lib/cmp-build.js +25 -25
- package/lib/cmp-session.js +70 -70
- package/lib/cmp-verify.js +71 -57
- package/lib/cms-compress.js +7 -7
- package/lib/cms-decrypt.js +90 -72
- package/lib/cms-encrypt.js +32 -32
- package/lib/cms-sign.js +74 -55
- package/lib/cms-verify.js +97 -75
- package/lib/composite-sig.js +13 -13
- package/lib/constants.js +4 -4
- package/lib/crl-sign.js +22 -22
- package/lib/crl-verify.js +7 -6
- package/lib/crmf-sign.js +14 -14
- package/lib/csr-sign.js +8 -8
- package/lib/ct.js +37 -37
- package/lib/edwards-point.js +7 -7
- package/lib/est.js +98 -55
- package/lib/framework-error.js +5 -5
- package/lib/guard-all.js +3 -3
- package/lib/guard-async.js +4 -4
- package/lib/guard-bytes.js +79 -79
- package/lib/guard-compress.js +17 -17
- package/lib/guard-crypto.js +1 -1
- package/lib/guard-encoding.js +15 -15
- package/lib/guard-header.js +3 -3
- package/lib/guard-identifier.js +16 -16
- package/lib/guard-json.js +15 -15
- package/lib/guard-limits.js +7 -7
- package/lib/guard-name.js +81 -16
- package/lib/guard-parsed.js +80 -80
- package/lib/guard-range.js +19 -19
- package/lib/guard-secret.js +11 -10
- package/lib/guard-text.js +6 -6
- package/lib/guard-time.js +10 -10
- package/lib/hpke.js +18 -17
- package/lib/http-digest.js +35 -35
- package/lib/http-retry-after.js +13 -13
- package/lib/http-transport.js +20 -19
- package/lib/inspect.js +53 -53
- package/lib/ip-utils.js +2 -2
- package/lib/jose.js +13 -13
- package/lib/key.js +16 -16
- package/lib/lint.js +51 -51
- package/lib/merkle.js +51 -36
- package/lib/mime.js +18 -18
- package/lib/ocsp-verify.js +10 -10
- package/lib/ocsp.js +13 -13
- package/lib/oid.js +29 -29
- package/lib/path-validate.js +114 -113
- package/lib/pbes2.js +16 -16
- package/lib/pkcs12-build.js +53 -53
- package/lib/pki-build.js +21 -19
- package/lib/rc2.js +1 -1
- package/lib/rfc3339.js +5 -5
- package/lib/schema-all.js +31 -31
- package/lib/schema-attrcert.js +12 -12
- package/lib/schema-c509.js +144 -142
- package/lib/schema-cmc.js +58 -58
- package/lib/schema-cmp.js +43 -43
- package/lib/schema-cms.js +45 -45
- package/lib/schema-crl.js +7 -7
- package/lib/schema-crmf.js +28 -28
- package/lib/schema-csr.js +12 -12
- package/lib/schema-csrattrs.js +16 -16
- package/lib/schema-engine.js +18 -18
- package/lib/schema-ocsp.js +15 -15
- package/lib/schema-pkcs12.js +20 -20
- package/lib/schema-pkcs8.js +2 -2
- package/lib/schema-pkix.js +131 -126
- package/lib/schema-smime.js +19 -19
- package/lib/schema-tsp.js +12 -12
- package/lib/schema-x509.js +3 -3
- package/lib/shbs.js +18 -18
- package/lib/sign-scheme.js +13 -13
- package/lib/sigstore.js +10 -11
- package/lib/sleep.js +1 -1
- package/lib/smime.js +308 -96
- package/lib/tls-cert-compress.js +18 -18
- package/lib/trust.js +27 -27
- package/lib/tsp-sign.js +17 -17
- package/lib/validator-all.js +1 -1
- package/lib/validator-attcert.js +1 -1
- package/lib/validator-cose.js +43 -44
- package/lib/validator-keydesc.js +3 -3
- package/lib/validator-sig.js +13 -13
- package/lib/validator-tls.js +11 -11
- package/lib/validator-tpm.js +20 -19
- package/lib/webauthn-mds.js +66 -66
- package/lib/webauthn.js +33 -33
- package/lib/webcrypto.js +15 -15
- package/lib/x509-sign.js +13 -13
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/cms-decrypt.js
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
// @primitive pki.cms.decrypt block live in cms-verify.js, which re-exports this function.
|
|
7
7
|
//
|
|
8
8
|
// CMS EnvelopedData / AuthEnvelopedData / EncryptedData decryption (RFC 5652/5083/5084/3560/5753/
|
|
9
|
-
// 8418/9629/9936/3211/8018), the consuming side of pki.cms.encrypt. Three stages behind
|
|
10
|
-
// oracle-free choke point: (1)
|
|
11
|
-
// (2)
|
|
12
|
-
// decrypt+unpad). Every secret-dependent failure in stages 2-3 collapses to the
|
|
9
|
+
// 8418/9629/9936/3211/8018), the consuming side of pki.cms.encrypt. Three stages behind one
|
|
10
|
+
// oracle-free choke point: (1) select the recipient (identifier matching, typed + distinct),
|
|
11
|
+
// (2) acquire the CEK (per-arm unwrap/decap/derive), (3) open the content (AEAD verify / CBC
|
|
12
|
+
// decrypt+unpad). Every secret-dependent failure in stages 2-3 collapses to the single uniform
|
|
13
13
|
// verdict `cms/decrypt-failed` (Bleichenbacher / EFAIL / PWRI-check-byte oracle freedom); the
|
|
14
14
|
// ktri v1.5 arm substitutes a random CEK on any decode fault (RFC 3218 implicit rejection) so its
|
|
15
15
|
// failure emerges at stage 3 identically to every other bad-key path.
|
|
@@ -32,7 +32,7 @@ var KEM_CT_LENGTHS = schemaCms.KEM_CT_LENGTHS;
|
|
|
32
32
|
|
|
33
33
|
function O(n) { return oid.byName(n); }
|
|
34
34
|
function _err(code, message, cause) { return new CmsError(code, message, cause); }
|
|
35
|
-
// The
|
|
35
|
+
// The single uniform secret-dependent-failure verdict. No cause chaining that distinguishes the site.
|
|
36
36
|
function _fail() { return new CmsError("cms/decrypt-failed", "the CMS content could not be decrypted (uniform by design -- padding / integrity / key-unwrap failures are indistinguishable to defeat oracles)"); }
|
|
37
37
|
|
|
38
38
|
var CONTENT_KEYBITS = pbes2.CONTENT_KEYBITS; // content-encryption OID -> key bits (the shared PBES2 table)
|
|
@@ -50,13 +50,31 @@ async function decrypt(input, keyMaterial, opts) {
|
|
|
50
50
|
return decryptEnvelopedData(parsed, keyMaterial, opts, ct);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// The post-parse decrypt of an
|
|
54
|
-
// the CEK, open the content
|
|
55
|
-
// @internal --
|
|
56
|
-
// for a sibling composer that already holds a strictly-walked content node it must
|
|
53
|
+
// The post-parse decrypt of an already-parsed EnvelopedData / AuthEnvelopedData: select a recipient, acquire
|
|
54
|
+
// the CEK, open the content, oracle-free (every secret-dependent failure collapses to cms/decrypt-failed).
|
|
55
|
+
// @internal -- not re-exported on pki.cms. Taking the parsed structure in place of DER is a deliberate seam
|
|
56
|
+
// for a sibling composer that already holds a strictly-walked content node it must not re-serialize/re-parse:
|
|
57
57
|
// pki.pkcs12.open drives this off the pkcs12 parser's walkEnvelopedData result so an RFC 7292 privacy safe
|
|
58
|
-
// whose EnvelopedData is BER-encoded (indefinite lengths, some NSS output
|
|
59
|
-
// opens without hitting the public decrypt()'s strict-DER re-parse. Callers pass a
|
|
58
|
+
// whose EnvelopedData is BER-encoded (indefinite lengths, some NSS output the pkcs12 parser accepts)
|
|
59
|
+
// opens without hitting the public decrypt()'s strict-DER re-parse. Callers pass a trusted parse result only.
|
|
60
|
+
// `authenticated` answers a question about the content and the key that opened it. It
|
|
61
|
+
// cannot answer the question an operator hears in the word -- who sent this -- and no
|
|
62
|
+
// recipient type this toolkit supports authenticates an individual originator: a ktri or
|
|
63
|
+
// ephemeral-static kari message is minted by anyone holding the recipient's PUBLIC key,
|
|
64
|
+
// and a password or KEK message is minted by any co-recipient sharing the secret. The
|
|
65
|
+
// origin question therefore gets its own field instead of being left to the reader's
|
|
66
|
+
// inference. originatorInfo rides outside the AEAD's authenticated data, so it is a hint
|
|
67
|
+
// the sender chose, never evidence, and it is surfaced saying so rather than dropped.
|
|
68
|
+
// Every verdict this module returns carries all three, including the unauthenticated
|
|
69
|
+
// ciphers: a field present on some returns and absent on others is read as a capability
|
|
70
|
+
// difference, and the caller cannot tell "no origin" from "this path forgot to say".
|
|
71
|
+
function _originFields(res, authenticatedBy, originatorInfo) {
|
|
72
|
+
res.authenticatedBy = authenticatedBy || null;
|
|
73
|
+
res.originAuthenticated = false;
|
|
74
|
+
res.originatorInfo = originatorInfo || null;
|
|
75
|
+
return res;
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
async function decryptEnvelopedData(parsed, keyMaterial, opts, contentTypeName) {
|
|
61
79
|
opts = opts || {};
|
|
62
80
|
if (keyMaterial == null || typeof keyMaterial !== "object") throw _err("cms/bad-input", "decrypt requires a key-material object");
|
|
@@ -64,8 +82,8 @@ async function decryptEnvelopedData(parsed, keyMaterial, opts, contentTypeName)
|
|
|
64
82
|
var recips = parsed.recipientInfos || [];
|
|
65
83
|
var candidates = _selectCandidates(recips, keyMaterial, opts); // stage 1 (typed, distinct)
|
|
66
84
|
var eci = parsed.encryptedContentInfo;
|
|
67
|
-
// The content cipher's
|
|
68
|
-
// identifier with no key material at all
|
|
85
|
+
// The content cipher's mode is a structural property of the message, readable from the algorithm
|
|
86
|
+
// identifier with no key material at all, so it is decided here, before any recipient is tried.
|
|
69
87
|
// Deciding it per candidate would make a structural verdict depend on key acquisition succeeding
|
|
70
88
|
// first: several password or KEK recipients can match the same key material, so the distinct
|
|
71
89
|
// cms/unsupported-algorithm would be swallowed by the ambiguous-candidate loop below and the caller
|
|
@@ -78,18 +96,18 @@ async function decryptEnvelopedData(parsed, keyMaterial, opts, contentTypeName)
|
|
|
78
96
|
try {
|
|
79
97
|
_assertSupported(candidates[ci].ri, keyMaterial); // distinct-code reject (MQV / non-KEM ori)
|
|
80
98
|
var cek = await _acquireCek(candidates[ci].ri, keyMaterial, opts); // stage 2 (uniform)
|
|
81
|
-
// The recovered CEK is cleared only
|
|
82
|
-
// needs, so an earlier wipe would break decryption
|
|
99
|
+
// The recovered CEK is cleared only after the content is open: it is the key the next stage
|
|
100
|
+
// needs, so an earlier wipe would break decryption instead of protecting it. The returned object
|
|
83
101
|
// is built before the `finally` runs and holds the plaintext, not the key.
|
|
84
102
|
try {
|
|
85
103
|
var content = await _openContent(parsed, eci, cek, ct); // stage 3 (uniform)
|
|
86
|
-
return {
|
|
104
|
+
return _originFields({
|
|
87
105
|
content: content,
|
|
88
106
|
contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType,
|
|
89
107
|
recipientType: candidates[ci].ri.type, recipientIndex: candidates[ci].index,
|
|
90
108
|
contentEncryptionAlgorithm: eci.contentEncryptionAlgorithm.name || eci.contentEncryptionAlgorithm.oid,
|
|
91
109
|
authenticated: ct === "authEnvelopedData",
|
|
92
|
-
};
|
|
110
|
+
}, ct === "authEnvelopedData" ? "content-encryption-key" : null, parsed.originatorInfo);
|
|
93
111
|
} finally {
|
|
94
112
|
guard.secret.zeroize(cek, CmsError, "cms/bad-input", "the recovered content-encryption key");
|
|
95
113
|
}
|
|
@@ -101,7 +119,7 @@ async function decryptEnvelopedData(parsed, keyMaterial, opts, contentTypeName)
|
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
function _parse(input) {
|
|
104
|
-
// Always drive the strict parser over caller-supplied bytes
|
|
122
|
+
// Always drive the strict parser over caller-supplied bytes: an arbitrary object that merely
|
|
105
123
|
// carries a contentTypeName is not a trusted parse result and must not bypass DER validation.
|
|
106
124
|
return schemaCms.parse(_toDer(input));
|
|
107
125
|
}
|
|
@@ -111,7 +129,7 @@ function _toDer(input) {
|
|
|
111
129
|
throw _err("cms/bad-input", "input must be a DER Buffer, Uint8Array, or PEM string");
|
|
112
130
|
}
|
|
113
131
|
|
|
114
|
-
// ---- stage 1: recipient selection (
|
|
132
|
+
// ---- stage 1: recipient selection (not secret-dependent) -------------------
|
|
115
133
|
// Returns every recipient the key material may target. An explicit recipientIndex or an rid match
|
|
116
134
|
// yields exactly one; unlabelled pwri / kekri recipients yield all matches for the caller to try.
|
|
117
135
|
function _selectCandidates(recips, km, opts) {
|
|
@@ -198,12 +216,12 @@ async function _acquireCek(ri, km, opts) {
|
|
|
198
216
|
if (ri.type === "pwri") return await _pwriCek(ri, km, opts);
|
|
199
217
|
if (ri.type === "ori") return await _kemriCek(ri, km);
|
|
200
218
|
} catch (e) {
|
|
201
|
-
// Structural / resource / config faults keep their own code
|
|
219
|
+
// Structural / resource / config faults keep their own code; only secret-dependent failures
|
|
202
220
|
// (a bad unwrap, a wrong key, a padding fault) collapse to the uniform verdict.
|
|
203
221
|
if (e instanceof CmsError && _passThrough[e.code]) throw e;
|
|
204
222
|
throw _fail();
|
|
205
223
|
}
|
|
206
|
-
// Coverage residual: unreachable
|
|
224
|
+
// Coverage residual: unreachable, since _selectCandidates only returns one of the five handled types.
|
|
207
225
|
throw _err("cms/unsupported-recipient-type", "unsupported recipient type " + ri.type);
|
|
208
226
|
}
|
|
209
227
|
|
|
@@ -229,7 +247,7 @@ async function _ktriCek(ri, km) {
|
|
|
229
247
|
}
|
|
230
248
|
}
|
|
231
249
|
// Coverage residual: reachable only from a hostile message (our encrypt emits only OAEP; OpenSSL
|
|
232
|
-
// emits OAEP or rsaEncryption)
|
|
250
|
+
// emits OAEP or rsaEncryption): a fail-closed reject the fuzz harness exercises.
|
|
233
251
|
throw _err("cms/unsupported-algorithm", "unsupported ktri keyEncryptionAlgorithm " + kea.oid);
|
|
234
252
|
} finally {
|
|
235
253
|
_releaseKeyDer(k);
|
|
@@ -242,7 +260,7 @@ async function _kariCek(ri, km) {
|
|
|
242
260
|
// otherwise be an unprotected window, and it is not a narrow one: reading the wrap algorithm, the
|
|
243
261
|
// originator's key, the recipient's certificate and the matching RecipientEncryptedKey all parse
|
|
244
262
|
// attacker-supplied structure and all throw on malformed input. A message crafted to fail any one
|
|
245
|
-
// of them would be a way to leave the key copy in the heap
|
|
263
|
+
// of them would be a way to leave the key copy in the heap, the exact outcome the wipe is for.
|
|
246
264
|
var k = _normKeyDer(km.key);
|
|
247
265
|
// The agreement secret and the KEK derived from it are both allocated here; one `finally` clears
|
|
248
266
|
// whichever branch produced them, including when the unwrap below throws on a tampered key.
|
|
@@ -253,7 +271,7 @@ async function _kariCek(ri, km) {
|
|
|
253
271
|
var wrapAlg = _kariWrap(kea);
|
|
254
272
|
var scheme = kea.oid;
|
|
255
273
|
var origSpki = _originatorSpki(ri.originator);
|
|
256
|
-
// Unwrap
|
|
274
|
+
// Unwrap this recipient's own RecipientEncryptedKey (matched by rid), not element 0; a kari may list
|
|
257
275
|
// several recipients under one ephemeral key.
|
|
258
276
|
var rek = (km.cert != null && _kariRekFor(ri, x509.parse(_normCertDer(km.cert)))) || ri.recipientEncryptedKeys[0];
|
|
259
277
|
var kekBytes = WRAP_KEK_LENGTHS[wrapAlg.oid];
|
|
@@ -266,8 +284,8 @@ async function _kariCek(ri, km) {
|
|
|
266
284
|
mz = Buffer.from(await subtle.deriveBits({ name: mont.name, public: origPub }, recipPriv, null));
|
|
267
285
|
if (mz.every(function (x) { return x === 0; })) throw _fail();
|
|
268
286
|
var mzKey = await subtle.importKey("raw", mz, { name: "HKDF" }, false, ["deriveBits"]);
|
|
269
|
-
// RFC 8418 sec. 2.2: a present ukm
|
|
270
|
-
// entityUInfo
|
|
287
|
+
// RFC 8418 sec. 2.2: a present ukm serves as both the HKDF salt and the ECC-CMS-SharedInfo
|
|
288
|
+
// entityUInfo, so mirror the producer and both sides derive the same KEK as a conformant peer.
|
|
271
289
|
kek = Buffer.from(await subtle.deriveBits({ name: "HKDF", hash: mont.hkdf, salt: ukm || Buffer.alloc(0), info: _eccSharedInfo(wrapAlg.name, ukm, kekBytes) }, mzKey, kekBytes * 8));
|
|
272
290
|
} else {
|
|
273
291
|
// RFC 5753 sec. 7.1 permits the originator EC key to omit its curve parameters, inheriting the
|
|
@@ -313,11 +331,11 @@ async function _pwriCek(ri, km, opts) {
|
|
|
313
331
|
// override a built-in name, and a name-matched mode check would then admit a non-CBC inner cipher.
|
|
314
332
|
if (!innerBits || CONTENT_MODE[innerOid] !== "cbc") throw _err("cms/unsupported-algorithm", "unsupported pwri inner cipher");
|
|
315
333
|
var iv = asn1.read.octetString(inner.children[1]);
|
|
316
|
-
// The derived KEK is ours and is cleared once the unwrap has consumed it
|
|
334
|
+
// The derived KEK is ours and is cleared once the unwrap has consumed it, on the failing path
|
|
317
335
|
// too, which is the one an attacker induces by tampering with the wrapped key. The caller's
|
|
318
336
|
// password is not touched: passwordBytes returns a caller-supplied Buffer as-is.
|
|
319
|
-
// A string / Uint8Array password is encoded into a buffer
|
|
320
|
-
// copy
|
|
337
|
+
// A string / Uint8Array password is encoded into a buffer this toolkit allocated, a credential
|
|
338
|
+
// copy, so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
|
|
321
339
|
// borrowed and left intact.
|
|
322
340
|
var pw = pbes2.passwordBytesOwned(km.password, _err, "cms");
|
|
323
341
|
var kek;
|
|
@@ -336,18 +354,18 @@ async function _kemriCek(ri, km) {
|
|
|
336
354
|
var wcName = _mlkemName(k.kem.oid);
|
|
337
355
|
if (!wcName) throw _err("cms/unsupported-algorithm", "unsupported KEM " + k.kem.oid);
|
|
338
356
|
// Honor the declared key-derivation function: we implement HKDF-SHA256 (RFC 9629), so a message
|
|
339
|
-
// naming any other KDF is rejected
|
|
357
|
+
// naming any other KDF is rejected, never silently derived with the wrong one.
|
|
340
358
|
if (k.kdf.oid !== O("hkdfWithSha256")) throw _err("cms/unsupported-algorithm", "unsupported KEM key-derivation " + k.kdf.oid);
|
|
341
359
|
var wantCt = KEM_CT_LENGTHS[k.kem.oid];
|
|
342
360
|
var kemct = k.kemct;
|
|
343
|
-
// Coverage residual (both _fail re-checks): unreachable via a parsed message
|
|
361
|
+
// Coverage residual (both _fail re-checks): unreachable via a parsed message, because the strict parser
|
|
344
362
|
// already rejects a wrong kemct length (cms/bad-kem-ciphertext) and a kekLength != wrap size
|
|
345
363
|
// (cms/kek-length-mismatch). These are defense-in-depth on the consumer path (M32 / M29).
|
|
346
|
-
if (wantCt && kemct.length !== wantCt) throw _fail(); // M32: exact ct length
|
|
364
|
+
if (wantCt && kemct.length !== wantCt) throw _fail(); // M32: exact ct length before decap
|
|
347
365
|
var kekBytes = Number(k.kekLength);
|
|
348
366
|
var wrapAlg = k.wrap;
|
|
349
367
|
if (WRAP_KEK_LENGTHS[wrapAlg.oid] !== kekBytes) throw _fail(); // M29 re-check on the consumer path
|
|
350
|
-
// The decapsulation key copy is released as soon as the engine has imported it
|
|
368
|
+
// The decapsulation key copy is released as soon as the engine has imported it: the import is
|
|
351
369
|
// the only thing that reads it, so its lifetime does not need to span the decapsulation below.
|
|
352
370
|
var keyCopy = _normKeyDer(km.key);
|
|
353
371
|
var priv;
|
|
@@ -366,8 +384,8 @@ async function _kemriCek(ri, km) {
|
|
|
366
384
|
return await _aesKwUnwrap(kek, k.encryptedKey);
|
|
367
385
|
} finally {
|
|
368
386
|
// NIST SP 800-227 RS5 / sec. 4.2 and RFC 9629 sec. 7: the shared secret and the KEK it derives
|
|
369
|
-
// are destroyed as soon as they stop being needed. In a `finally`, so an unwrap that
|
|
370
|
-
// the common case under a wrong key or a tampered encryptedKey
|
|
387
|
+
// are destroyed as soon as they stop being needed. In a `finally`, so an unwrap that fails
|
|
388
|
+
// (the common case under a wrong key or a tampered encryptedKey) leaves nothing behind either;
|
|
371
389
|
// a wipe on the success path alone would keep the secret exactly when an attacker made it fail.
|
|
372
390
|
// Only buffers allocated here are wiped: km.key, the message bytes and the returned CEK are the
|
|
373
391
|
// caller's or the caller's to consume.
|
|
@@ -381,7 +399,7 @@ async function _kemriCek(ri, km) {
|
|
|
381
399
|
// The declared cipher MODE must match the container's authentication model: an AEAD cipher belongs in
|
|
382
400
|
// AuthEnvelopedData and a plain CBC cipher in EnvelopedData (RFC 5083 sec. 2.1 / RFC 5084 sec. 3). Key
|
|
383
401
|
// length alone does not separate them, so without this an EnvelopedData naming AES-GCM was opened as
|
|
384
|
-
// unauthenticated CBC while the result still reported the AEAD algorithm
|
|
402
|
+
// unauthenticated CBC while the result still reported the AEAD algorithm, telling the caller the
|
|
385
403
|
// content was authenticated when nothing had authenticated it. The mirror case dereferenced AEAD
|
|
386
404
|
// parameters the parser leaves null for a non-AEAD algorithm, faulting instead of failing closed.
|
|
387
405
|
// An unresolvable OID is left to _openContent, which names it as the unsupported algorithm it is.
|
|
@@ -403,10 +421,10 @@ async function _openContent(parsed, eci, cek, ct) {
|
|
|
403
421
|
// A null CEK (v1.5 implicit rejection) or a wrong-length CEK -> a fresh random key of the right
|
|
404
422
|
// length, so the failure surfaces here as the uniform verdict, never earlier.
|
|
405
423
|
//
|
|
406
|
-
// The substitute is
|
|
424
|
+
// The substitute is this function's own allocation, so this function clears it: reassigning the
|
|
407
425
|
// parameter does not change the caller's variable, so the caller's `finally` would wipe the
|
|
408
426
|
// original null / wrong-length value and leave the substitute behind. It is held separately
|
|
409
|
-
//
|
|
427
|
+
// and `cek` is left alone here, because on the normal path `cek` is the caller's recovered key
|
|
410
428
|
// and is still in use after this returns.
|
|
411
429
|
var substitute = null;
|
|
412
430
|
if (cek == null || cek.length !== keyBits / 8) { substitute = nodeCrypto.randomBytes(keyBits / 8); cek = substitute; }
|
|
@@ -426,7 +444,7 @@ async function _openContent(parsed, eci, cek, ct) {
|
|
|
426
444
|
}
|
|
427
445
|
}
|
|
428
446
|
function _gcmOpen(cek, nonce, ct, tag, aad, keyBits, icvLen) {
|
|
429
|
-
// Coverage residual: unreachable via a parsed message
|
|
447
|
+
// Coverage residual: unreachable via a parsed message, since the strict parser rejects any
|
|
430
448
|
// AuthEnvelopedData whose mac length != aes-ICVlen (cms/mac-length-mismatch) before we get here.
|
|
431
449
|
if (!tag || tag.length !== icvLen) throw _fail();
|
|
432
450
|
var d = nodeCrypto.createDecipheriv("aes-" + keyBits + "-gcm", cek, nonce, { authTagLength: icvLen });
|
|
@@ -447,14 +465,14 @@ async function _decryptEncryptedData(parsed, km, opts) {
|
|
|
447
465
|
var cek = guard.bytes.view(km.cek, CmsError, "cms/bad-input", "cek");
|
|
448
466
|
if (cek.length !== keyBits / 8) throw _err("cms/bad-input", "the supplied cek length does not match the content algorithm");
|
|
449
467
|
var iv = asn1.read.octetString(asn1.decode(alg.parameters));
|
|
450
|
-
try { return { content: pbes2.cbcDecrypt(cek, iv, eci.encryptedContent, keyBits), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "cek", recipientIndex: -1, contentEncryptionAlgorithm: alg.name || alg.oid, authenticated: false }; }
|
|
468
|
+
try { return _originFields({ content: pbes2.cbcDecrypt(cek, iv, eci.encryptedContent, keyBits), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "cek", recipientIndex: -1, contentEncryptionAlgorithm: alg.name || alg.oid, authenticated: false }, null, null); }
|
|
451
469
|
catch (_e) { throw _fail(); }
|
|
452
470
|
}
|
|
453
471
|
async function _decryptPbes2(parsed, eci, km, opts) {
|
|
454
472
|
if (km.password == null) throw _err("cms/bad-input", "this EncryptedData needs a { password }");
|
|
455
473
|
// The PBES2 parameters are attacker-controlled structure the strict parser surfaces raw (PBES2 is
|
|
456
474
|
// not an AEAD it validates): parse them behind a structural guard so a malformed shape is a typed
|
|
457
|
-
// cms/bad-input, never a raw dereference fault (the PBES2 structure is public
|
|
475
|
+
// cms/bad-input, never a raw dereference fault (the PBES2 structure is public, not a decrypt oracle).
|
|
458
476
|
var kdf, encOid, iv, pb;
|
|
459
477
|
try {
|
|
460
478
|
var params = pbes2.seqChildren(eci.contentEncryptionAlgorithm.parameters, 2, "PBES2 parameters", _err, "cms");
|
|
@@ -472,14 +490,14 @@ async function _decryptPbes2(parsed, eci, km, opts) {
|
|
|
472
490
|
if (!keyBits) throw _err("cms/unsupported-algorithm", "unsupported PBES2 content cipher " + encOid);
|
|
473
491
|
// The password-derived content key is ours; the caller's password buffer is not (passwordBytes
|
|
474
492
|
// passes a supplied Buffer straight through) and is left intact.
|
|
475
|
-
// A string / Uint8Array password is encoded into a buffer
|
|
476
|
-
// copy
|
|
493
|
+
// A string / Uint8Array password is encoded into a buffer this toolkit allocated, a credential
|
|
494
|
+
// copy, so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
|
|
477
495
|
// borrowed and left intact.
|
|
478
496
|
var pwE = pbes2.passwordBytesOwned(km.password, _err, "cms");
|
|
479
497
|
var key;
|
|
480
498
|
try { key = nodeCrypto.pbkdf2Sync(pwE.bytes, pb.salt, pb.iterations, keyBits / 8, pb.prfNode); }
|
|
481
499
|
finally { if (pwE.owned) guard.secret.zeroize(pwE.bytes, CmsError, "cms/bad-input", "the password encoding"); }
|
|
482
|
-
try { return { content: pbes2.cbcDecrypt(key, iv, eci.encryptedContent, keyBits), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "password", recipientIndex: -1, contentEncryptionAlgorithm: oid.name(encOid) || encOid, authenticated: false }; }
|
|
500
|
+
try { return _originFields({ content: pbes2.cbcDecrypt(key, iv, eci.encryptedContent, keyBits), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "password", recipientIndex: -1, contentEncryptionAlgorithm: oid.name(encOid) || encOid, authenticated: false }, null, null); }
|
|
483
501
|
catch (_e) { throw _fail(); }
|
|
484
502
|
finally { guard.secret.zeroize(key, CmsError, "cms/bad-input", "the password-derived content-encryption key"); }
|
|
485
503
|
}
|
|
@@ -487,19 +505,19 @@ async function _decryptPbes2(parsed, eci, km, opts) {
|
|
|
487
505
|
// ---- shared helpers (mirror cms-encrypt's builders) ------------------------
|
|
488
506
|
async function _aesKwUnwrap(kek, wrapped) {
|
|
489
507
|
var kekKey = await subtle.importKey("raw", kek, { name: "AES-KW" }, false, ["unwrapKey"]);
|
|
490
|
-
// Import the unwrapped key as HMAC, not AES-CBC: this function
|
|
491
|
-
// type is transient and the exported bytes are identical for an AES CEK
|
|
508
|
+
// Import the unwrapped key as HMAC, not AES-CBC: this function exports to raw bytes, so the CryptoKey
|
|
509
|
+
// type is transient and the exported bytes are identical for an AES CEK, while HMAC accepts any wrap
|
|
492
510
|
// length, so an AuthenticatedData MAC key that is not 16/24/32 octets (e.g. a 64-octet HMAC-SHA-512
|
|
493
511
|
// key from another implementation) is recovered instead of rejected before the MAC is even checked.
|
|
494
512
|
var raw = await subtle.unwrapKey("raw", wrapped, kekKey, { name: "AES-KW" }, { name: "HMAC", hash: "SHA-256" }, true, ["sign"]);
|
|
495
513
|
// exportKey allocates an ArrayBuffer holding the unwrapped key; the Buffer below is a copy of it,
|
|
496
514
|
// and the original would otherwise stay readable with nothing referencing it. That is a
|
|
497
515
|
// controllable allocation, not one of the runtime-internal copies the best-effort caveat covers.
|
|
498
|
-
// Only the intermediate is wiped
|
|
499
|
-
// the content, so wiping it here would destroy the result
|
|
516
|
+
// Only the intermediate is wiped: the returned Buffer is the key the caller must use to open
|
|
517
|
+
// the content, so wiping it here would destroy the result instead of protecting it.
|
|
500
518
|
var rawAb = await subtle.exportKey("raw", raw);
|
|
501
519
|
try {
|
|
502
|
-
// An explicit
|
|
520
|
+
// An explicit copy, not Buffer.from(arrayBuffer): that form returns a view sharing the
|
|
503
521
|
// ArrayBuffer's memory, so wiping the intermediate below would zero the key being returned.
|
|
504
522
|
var view = new Uint8Array(rawAb);
|
|
505
523
|
var out = Buffer.alloc(view.length);
|
|
@@ -522,14 +540,14 @@ function _kemOtherInfo(wrapName, kekBytes, ukm) {
|
|
|
522
540
|
return b.sequence(kids);
|
|
523
541
|
}
|
|
524
542
|
// RFC 3211 double-CBC unwrap (M26): decrypt pass 2 (IV = last block of pass 1), then pass 1
|
|
525
|
-
// (IV = first block), validate the count + complement check bytes
|
|
543
|
+
// (IV = first block), validate the count + complement check bytes, with uniform failure on mismatch.
|
|
526
544
|
function _pwriUnwrap(kek, wrapped, iv, keyBits) {
|
|
527
545
|
var blk = 16, alg = "aes-" + keyBits + "-cbc";
|
|
528
546
|
if (wrapped.length < 2 * blk || wrapped.length % blk !== 0) throw _fail();
|
|
529
547
|
var n = wrapped.length;
|
|
530
548
|
// pass2 = CBC(kek, iv2 = pass1[last block], pass1). Recover pass1's last block by decrypting the
|
|
531
|
-
// last ciphertext block (ECB) and XORing the previous ciphertext block
|
|
532
|
-
// block decrypt that needs no IV
|
|
549
|
+
// last ciphertext block (ECB) and XORing the previous ciphertext block, a standard CBC last-
|
|
550
|
+
// block decrypt that needs no IV, then CBC-decrypt pass2 under that recovered iv2 to get pass1.
|
|
533
551
|
var ecb = nodeCrypto.createDecipheriv("aes-" + keyBits + "-ecb", kek, Buffer.alloc(0)); ecb.setAutoPadding(false);
|
|
534
552
|
var lastDec = Buffer.concat([ecb.update(wrapped.subarray(n - blk)), ecb.final()]);
|
|
535
553
|
var iv2 = Buffer.alloc(blk);
|
|
@@ -538,7 +556,7 @@ function _pwriUnwrap(kek, wrapped, iv, keyBits) {
|
|
|
538
556
|
var pass1 = Buffer.concat([d1.update(wrapped), d1.final()]);
|
|
539
557
|
var d2 = nodeCrypto.createDecipheriv(alg, kek, iv); d2.setAutoPadding(false);
|
|
540
558
|
// body is the recovered plaintext block: it holds the CEK. The caller receives an independent
|
|
541
|
-
//
|
|
559
|
+
// copy, so the block is cleared on every exit, including the two validation rejects, which are
|
|
542
560
|
// the paths an attacker induces by tampering with the wrapped key.
|
|
543
561
|
var body = Buffer.concat([d2.update(pass1), d2.final()]);
|
|
544
562
|
try {
|
|
@@ -553,7 +571,7 @@ function _pwriUnwrap(kek, wrapped, iv, keyBits) {
|
|
|
553
571
|
guard.secret.zeroizeAll([body, pass1], CmsError, "cms/bad-input", "the PWRI plaintext block");
|
|
554
572
|
}
|
|
555
573
|
}
|
|
556
|
-
// Coverage residual (the unsupported-algorithm throw arm of each lookup below
|
|
574
|
+
// Coverage residual (the unsupported-algorithm throw arm of each lookup below: _hashW3c,
|
|
557
575
|
// _x963Hash, and _originatorSpki that follow): reachable only from a fully well-formed recipient that
|
|
558
576
|
// names an inner algorithm we do not implement (a non-registry OAEP hash / kari scheme / originator
|
|
559
577
|
// form). The producer never emits one; the fuzz harness (fuzz/cms-decrypt.fuzz.js) drives these arms
|
|
@@ -572,8 +590,8 @@ function _oaepHashFromParams(paramsBytes) {
|
|
|
572
590
|
mgfHash = _hashW3c(asn1.read.oid(mg.children[1].children[0]));
|
|
573
591
|
} else if (ch.tagNumber === 2) { label = asn1.read.octetString(ch.children[0].children[1]); } // pSourceAlgorithm [2]
|
|
574
592
|
});
|
|
575
|
-
// WebCrypto RSA-OAEP ties the MGF1 hash to the OAEP hash and supports only an empty label
|
|
576
|
-
//
|
|
593
|
+
// WebCrypto RSA-OAEP ties the MGF1 hash to the OAEP hash and supports only an empty label, so any
|
|
594
|
+
// parameter set we cannot faithfully honor is rejected, never silently ignored.
|
|
577
595
|
if (mgfHash != null && mgfHash !== hashName) throw _err("cms/unsupported-algorithm", "the OAEP MGF1 hash must equal the OAEP hash");
|
|
578
596
|
if (label != null && label.length > 0) throw _err("cms/unsupported-algorithm", "a non-empty OAEP label is not supported");
|
|
579
597
|
return hashName;
|
|
@@ -593,7 +611,7 @@ function _kariWrap(kea) {
|
|
|
593
611
|
}
|
|
594
612
|
function _originatorSpki(originator) {
|
|
595
613
|
// originator OriginatorIdentifierOrKey CHOICE; we require originatorKey [1] (RFC 5753 MUST for
|
|
596
|
-
// ephemeral-static). The parser surfaces { form, value:{ algorithm, publicKey } }
|
|
614
|
+
// ephemeral-static). The parser surfaces { form, value:{ algorithm, publicKey } }, so reconstruct
|
|
597
615
|
// the SPKI (SEQUENCE { algorithm, subjectPublicKey BIT STRING }) for importKey.
|
|
598
616
|
if (!originator || originator.form !== "originatorKey") throw _err("cms/unsupported-algorithm", "kari requires an originatorKey (ephemeral-static ECDH)");
|
|
599
617
|
var v = originator.value;
|
|
@@ -639,11 +657,11 @@ var DIGEST_WC = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
|
639
657
|
var MAC_KEY_MIN_OCTETS = 16;
|
|
640
658
|
function _isDerNull(p) { return Buffer.isBuffer(p) && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
|
|
641
659
|
|
|
642
|
-
// Verify a CMS AuthenticatedData (RFC 5652 sec. 9): recover the MAC key through the
|
|
660
|
+
// Verify a CMS AuthenticatedData (RFC 5652 sec. 9): recover the MAC key through the same RecipientInfo
|
|
643
661
|
// unwrap decrypt uses, recompute the HMAC over the sec. 9.2 preimage (the re-tagged [2] authAttrs SET
|
|
644
|
-
// OF when present, else the eContent value octets), constant-time-compare to the transmitted mac, and
|
|
645
|
-
//
|
|
646
|
-
// do-not-trust-originator). The content is returned
|
|
662
|
+
// OF when present, else the eContent value octets), constant-time-compare to the transmitted mac, and,
|
|
663
|
+
// when authAttrs are present, independently confirm message-digest == digest(content) (sec. 9.3
|
|
664
|
+
// do-not-trust-originator). The content is returned only after every check passes. Every
|
|
647
665
|
// secret-dependent failure collapses to the uniform cms/decrypt-failed (no unwrap-success oracle).
|
|
648
666
|
async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
649
667
|
var macAlg = parsed.macAlgorithm;
|
|
@@ -674,10 +692,10 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
|
674
692
|
_assertSupported(candidates[ci].ri, km);
|
|
675
693
|
var macKey = await _acquireCek(candidates[ci].ri, km, opts); // recover the MAC key (uniform failure)
|
|
676
694
|
// RFC 3218 implicit rejection: _ktriCek returns null on an RSA v1.5 unwrap fault, and a hostile
|
|
677
|
-
// ktri/pwri could convey a below-floor (128-bit) key; in
|
|
678
|
-
// key so the MAC verify still
|
|
695
|
+
// ktri/pwri could convey a below-floor (128-bit) key; in either case substitute a fresh random
|
|
696
|
+
// key so the MAC verify still runs and fails uniformly, never a fast-path that distinguishes an
|
|
679
697
|
// invalid/short unwrap from a MAC mismatch (Bleichenbacher / weak-key oracle freedom).
|
|
680
|
-
// The substitute is held
|
|
698
|
+
// The substitute is held separately, never overwriting macKey: a hostile recipient can
|
|
681
699
|
// unwrap to a non-null key that is merely too short, and assigning over it would drop the only
|
|
682
700
|
// reference to that recovered secret, leaving it readable while the cleanup cleared the random
|
|
683
701
|
// replacement instead. Both are this module's allocations and both are cleared below.
|
|
@@ -693,14 +711,14 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
|
693
711
|
var actual = Buffer.from(await subtle.digest(mdCheck.hash, content));
|
|
694
712
|
if (!actual.equals(mdCheck.declared)) throw _fail(); // sec. 9.3: recompute, never trust the originator's digest
|
|
695
713
|
}
|
|
696
|
-
return {
|
|
714
|
+
return _originFields({
|
|
697
715
|
content: Buffer.from(content),
|
|
698
716
|
contentType: parsed.encapContentInfo.eContentType, contentTypeName: oid.name(parsed.encapContentInfo.eContentType) || parsed.encapContentInfo.eContentType,
|
|
699
717
|
recipientType: candidates[ci].ri.type, recipientIndex: candidates[ci].index,
|
|
700
718
|
macAlgorithm: macAlg.name || macAlg.oid,
|
|
701
719
|
digestAlgorithm: parsed.digestAlgorithm ? (parsed.digestAlgorithm.name || parsed.digestAlgorithm.oid) : null,
|
|
702
720
|
authenticated: true,
|
|
703
|
-
};
|
|
721
|
+
}, "message-authentication-key", parsed.originatorInfo);
|
|
704
722
|
} finally {
|
|
705
723
|
guard.secret.zeroizeAll([macKey, macSubstitute], CmsError, "cms/bad-input", "the message-authentication key");
|
|
706
724
|
}
|
|
@@ -717,7 +735,7 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
|
717
735
|
// the frozen cms/unsupported-algorithm code, not a round-trip.
|
|
718
736
|
// * the detached (absent eContent) reject is the deferred-with-condition path (this producer always
|
|
719
737
|
// attaches the content); re-open when a detached AuthenticatedData caller appears.
|
|
720
|
-
// * `parsed.authAttrs || []` / `parsed.recipientInfos || []
|
|
738
|
+
// * `parsed.authAttrs || []` / `parsed.recipientInfos || []`: the parser always surfaces these as
|
|
721
739
|
// arrays (or rejects), so the `|| []` fallbacks never fire.
|
|
722
740
|
// * `oid.name(eContentType) || eContentType` fires only for an unregistered content type; `macAlg.name
|
|
723
741
|
// || macAlg.oid` and `digestAlgorithm.name || .oid` never reach the `.oid` fallback because the
|
|
@@ -725,17 +743,17 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
|
725
743
|
// * the `macKey == null` half of the random-key substitution fires only for a hand-crafted RSA v1.5
|
|
726
744
|
// ktri (this producer emits OAEP); its behaviour is identical to the tested below-floor path (a
|
|
727
745
|
// random key -> the MAC verify fails uniformly), so the < 16 vector covers the substitution.
|
|
728
|
-
// _normKeyDer(key) -> { der, owned }
|
|
729
|
-
// buffer is one
|
|
746
|
+
// _normKeyDer(key) -> { der, owned }: the recipient private key as PKCS#8 DER, and whether the
|
|
747
|
+
// buffer is one this module made.
|
|
730
748
|
//
|
|
731
749
|
// The distinction decides who may wipe it. A caller handing in their own Buffer keeps a live
|
|
732
750
|
// reference and will use it again; wiping that would destroy the key out from under them. The other
|
|
733
|
-
// two forms produce a
|
|
751
|
+
// two forms produce a new buffer here (a Uint8Array is copied, a PEM string is decoded), and that
|
|
734
752
|
// buffer is a second copy of a private key which nothing else can reach, so it lives until the
|
|
735
753
|
// garbage collector happens to reuse the page unless this module clears it.
|
|
736
754
|
//
|
|
737
|
-
// Returning the flag
|
|
738
|
-
// own copy of every key so we could uniformly wipe it would
|
|
755
|
+
// Returning the flag, in place of always copying, keeps the caller's buffer un-duplicated: making our
|
|
756
|
+
// own copy of every key so we could uniformly wipe it would add a copy of the secret to solve the
|
|
739
757
|
// problem of having one.
|
|
740
758
|
function _normKeyDer(key) {
|
|
741
759
|
if (Buffer.isBuffer(key)) return { der: key, owned: false };
|