@blamejs/pki 0.5.6 → 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 +419 -378
- package/MIGRATING.md +65 -0
- package/README.md +12 -12
- package/lib/acme.js +31 -31
- package/lib/asn1-der.js +10 -10
- package/lib/attrcert-sign.js +24 -20
- 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 +104 -104
- package/lib/cmc-verify.js +108 -32
- package/lib/cmp-build.js +31 -26
- package/lib/cmp-session.js +74 -72
- package/lib/cmp-verify.js +72 -58
- package/lib/cms-compress.js +8 -9
- package/lib/cms-decrypt.js +92 -76
- package/lib/cms-encrypt.js +33 -34
- package/lib/cms-sign.js +100 -54
- package/lib/cms-verify.js +141 -82
- package/lib/composite-sig.js +13 -13
- package/lib/constants.js +4 -4
- package/lib/crl-sign.js +31 -25
- package/lib/crl-verify.js +7 -6
- package/lib/crmf-sign.js +19 -15
- package/lib/csr-sign.js +13 -9
- package/lib/ct.js +37 -37
- package/lib/edwards-point.js +7 -7
- package/lib/est.js +103 -59
- 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 +378 -15
- 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 +144 -75
- 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 +32 -20
- package/lib/oid.js +29 -29
- package/lib/path-validate.js +114 -113
- package/lib/pbes2.js +16 -16
- package/lib/pkcs12-build.js +71 -56
- package/lib/pki-build.js +23 -22
- 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 +169 -36
- 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 +19 -16
- 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 +22 -18
- 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 +21 -20
- package/lib/webauthn-mds.js +67 -67
- package/lib/webauthn.js +34 -34
- package/lib/webcrypto.js +15 -15
- package/lib/x509-sign.js +24 -15
- 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,18 +119,17 @@ 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
|
}
|
|
108
126
|
function _toDer(input) {
|
|
109
|
-
if (Buffer.isBuffer(input)) return input;
|
|
110
|
-
if (input instanceof Uint8Array) return Buffer.from(input);
|
|
127
|
+
if (Buffer.isBuffer(input) || input instanceof Uint8Array) return guard.bytes.snapshot(input, CmsError, "cms/bad-input", "input");
|
|
111
128
|
if (typeof input === "string") { try { return schemaCms.pemDecode(input); } catch (e) { throw _err("cms/bad-input", "the CMS PEM could not be decoded", e); } }
|
|
112
129
|
throw _err("cms/bad-input", "input must be a DER Buffer, Uint8Array, or PEM string");
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
// ---- stage 1: recipient selection (
|
|
132
|
+
// ---- stage 1: recipient selection (not secret-dependent) -------------------
|
|
116
133
|
// Returns every recipient the key material may target. An explicit recipientIndex or an rid match
|
|
117
134
|
// yields exactly one; unlabelled pwri / kekri recipients yield all matches for the caller to try.
|
|
118
135
|
function _selectCandidates(recips, km, opts) {
|
|
@@ -199,12 +216,12 @@ async function _acquireCek(ri, km, opts) {
|
|
|
199
216
|
if (ri.type === "pwri") return await _pwriCek(ri, km, opts);
|
|
200
217
|
if (ri.type === "ori") return await _kemriCek(ri, km);
|
|
201
218
|
} catch (e) {
|
|
202
|
-
// Structural / resource / config faults keep their own code
|
|
219
|
+
// Structural / resource / config faults keep their own code; only secret-dependent failures
|
|
203
220
|
// (a bad unwrap, a wrong key, a padding fault) collapse to the uniform verdict.
|
|
204
221
|
if (e instanceof CmsError && _passThrough[e.code]) throw e;
|
|
205
222
|
throw _fail();
|
|
206
223
|
}
|
|
207
|
-
// Coverage residual: unreachable
|
|
224
|
+
// Coverage residual: unreachable, since _selectCandidates only returns one of the five handled types.
|
|
208
225
|
throw _err("cms/unsupported-recipient-type", "unsupported recipient type " + ri.type);
|
|
209
226
|
}
|
|
210
227
|
|
|
@@ -230,7 +247,7 @@ async function _ktriCek(ri, km) {
|
|
|
230
247
|
}
|
|
231
248
|
}
|
|
232
249
|
// Coverage residual: reachable only from a hostile message (our encrypt emits only OAEP; OpenSSL
|
|
233
|
-
// emits OAEP or rsaEncryption)
|
|
250
|
+
// emits OAEP or rsaEncryption): a fail-closed reject the fuzz harness exercises.
|
|
234
251
|
throw _err("cms/unsupported-algorithm", "unsupported ktri keyEncryptionAlgorithm " + kea.oid);
|
|
235
252
|
} finally {
|
|
236
253
|
_releaseKeyDer(k);
|
|
@@ -243,7 +260,7 @@ async function _kariCek(ri, km) {
|
|
|
243
260
|
// otherwise be an unprotected window, and it is not a narrow one: reading the wrap algorithm, the
|
|
244
261
|
// originator's key, the recipient's certificate and the matching RecipientEncryptedKey all parse
|
|
245
262
|
// attacker-supplied structure and all throw on malformed input. A message crafted to fail any one
|
|
246
|
-
// 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.
|
|
247
264
|
var k = _normKeyDer(km.key);
|
|
248
265
|
// The agreement secret and the KEK derived from it are both allocated here; one `finally` clears
|
|
249
266
|
// whichever branch produced them, including when the unwrap below throws on a tampered key.
|
|
@@ -254,7 +271,7 @@ async function _kariCek(ri, km) {
|
|
|
254
271
|
var wrapAlg = _kariWrap(kea);
|
|
255
272
|
var scheme = kea.oid;
|
|
256
273
|
var origSpki = _originatorSpki(ri.originator);
|
|
257
|
-
// Unwrap
|
|
274
|
+
// Unwrap this recipient's own RecipientEncryptedKey (matched by rid), not element 0; a kari may list
|
|
258
275
|
// several recipients under one ephemeral key.
|
|
259
276
|
var rek = (km.cert != null && _kariRekFor(ri, x509.parse(_normCertDer(km.cert)))) || ri.recipientEncryptedKeys[0];
|
|
260
277
|
var kekBytes = WRAP_KEK_LENGTHS[wrapAlg.oid];
|
|
@@ -267,8 +284,8 @@ async function _kariCek(ri, km) {
|
|
|
267
284
|
mz = Buffer.from(await subtle.deriveBits({ name: mont.name, public: origPub }, recipPriv, null));
|
|
268
285
|
if (mz.every(function (x) { return x === 0; })) throw _fail();
|
|
269
286
|
var mzKey = await subtle.importKey("raw", mz, { name: "HKDF" }, false, ["deriveBits"]);
|
|
270
|
-
// RFC 8418 sec. 2.2: a present ukm
|
|
271
|
-
// 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.
|
|
272
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));
|
|
273
290
|
} else {
|
|
274
291
|
// RFC 5753 sec. 7.1 permits the originator EC key to omit its curve parameters, inheriting the
|
|
@@ -314,11 +331,11 @@ async function _pwriCek(ri, km, opts) {
|
|
|
314
331
|
// override a built-in name, and a name-matched mode check would then admit a non-CBC inner cipher.
|
|
315
332
|
if (!innerBits || CONTENT_MODE[innerOid] !== "cbc") throw _err("cms/unsupported-algorithm", "unsupported pwri inner cipher");
|
|
316
333
|
var iv = asn1.read.octetString(inner.children[1]);
|
|
317
|
-
// 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
|
|
318
335
|
// too, which is the one an attacker induces by tampering with the wrapped key. The caller's
|
|
319
336
|
// password is not touched: passwordBytes returns a caller-supplied Buffer as-is.
|
|
320
|
-
// A string / Uint8Array password is encoded into a buffer
|
|
321
|
-
// 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
|
|
322
339
|
// borrowed and left intact.
|
|
323
340
|
var pw = pbes2.passwordBytesOwned(km.password, _err, "cms");
|
|
324
341
|
var kek;
|
|
@@ -337,18 +354,18 @@ async function _kemriCek(ri, km) {
|
|
|
337
354
|
var wcName = _mlkemName(k.kem.oid);
|
|
338
355
|
if (!wcName) throw _err("cms/unsupported-algorithm", "unsupported KEM " + k.kem.oid);
|
|
339
356
|
// Honor the declared key-derivation function: we implement HKDF-SHA256 (RFC 9629), so a message
|
|
340
|
-
// naming any other KDF is rejected
|
|
357
|
+
// naming any other KDF is rejected, never silently derived with the wrong one.
|
|
341
358
|
if (k.kdf.oid !== O("hkdfWithSha256")) throw _err("cms/unsupported-algorithm", "unsupported KEM key-derivation " + k.kdf.oid);
|
|
342
359
|
var wantCt = KEM_CT_LENGTHS[k.kem.oid];
|
|
343
360
|
var kemct = k.kemct;
|
|
344
|
-
// 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
|
|
345
362
|
// already rejects a wrong kemct length (cms/bad-kem-ciphertext) and a kekLength != wrap size
|
|
346
363
|
// (cms/kek-length-mismatch). These are defense-in-depth on the consumer path (M32 / M29).
|
|
347
|
-
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
|
|
348
365
|
var kekBytes = Number(k.kekLength);
|
|
349
366
|
var wrapAlg = k.wrap;
|
|
350
367
|
if (WRAP_KEK_LENGTHS[wrapAlg.oid] !== kekBytes) throw _fail(); // M29 re-check on the consumer path
|
|
351
|
-
// 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
|
|
352
369
|
// the only thing that reads it, so its lifetime does not need to span the decapsulation below.
|
|
353
370
|
var keyCopy = _normKeyDer(km.key);
|
|
354
371
|
var priv;
|
|
@@ -367,8 +384,8 @@ async function _kemriCek(ri, km) {
|
|
|
367
384
|
return await _aesKwUnwrap(kek, k.encryptedKey);
|
|
368
385
|
} finally {
|
|
369
386
|
// NIST SP 800-227 RS5 / sec. 4.2 and RFC 9629 sec. 7: the shared secret and the KEK it derives
|
|
370
|
-
// are destroyed as soon as they stop being needed. In a `finally`, so an unwrap that
|
|
371
|
-
// 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;
|
|
372
389
|
// a wipe on the success path alone would keep the secret exactly when an attacker made it fail.
|
|
373
390
|
// Only buffers allocated here are wiped: km.key, the message bytes and the returned CEK are the
|
|
374
391
|
// caller's or the caller's to consume.
|
|
@@ -382,7 +399,7 @@ async function _kemriCek(ri, km) {
|
|
|
382
399
|
// The declared cipher MODE must match the container's authentication model: an AEAD cipher belongs in
|
|
383
400
|
// AuthEnvelopedData and a plain CBC cipher in EnvelopedData (RFC 5083 sec. 2.1 / RFC 5084 sec. 3). Key
|
|
384
401
|
// length alone does not separate them, so without this an EnvelopedData naming AES-GCM was opened as
|
|
385
|
-
// 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
|
|
386
403
|
// content was authenticated when nothing had authenticated it. The mirror case dereferenced AEAD
|
|
387
404
|
// parameters the parser leaves null for a non-AEAD algorithm, faulting instead of failing closed.
|
|
388
405
|
// An unresolvable OID is left to _openContent, which names it as the unsupported algorithm it is.
|
|
@@ -404,10 +421,10 @@ async function _openContent(parsed, eci, cek, ct) {
|
|
|
404
421
|
// A null CEK (v1.5 implicit rejection) or a wrong-length CEK -> a fresh random key of the right
|
|
405
422
|
// length, so the failure surfaces here as the uniform verdict, never earlier.
|
|
406
423
|
//
|
|
407
|
-
// The substitute is
|
|
424
|
+
// The substitute is this function's own allocation, so this function clears it: reassigning the
|
|
408
425
|
// parameter does not change the caller's variable, so the caller's `finally` would wipe the
|
|
409
426
|
// original null / wrong-length value and leave the substitute behind. It is held separately
|
|
410
|
-
//
|
|
427
|
+
// and `cek` is left alone here, because on the normal path `cek` is the caller's recovered key
|
|
411
428
|
// and is still in use after this returns.
|
|
412
429
|
var substitute = null;
|
|
413
430
|
if (cek == null || cek.length !== keyBits / 8) { substitute = nodeCrypto.randomBytes(keyBits / 8); cek = substitute; }
|
|
@@ -427,7 +444,7 @@ async function _openContent(parsed, eci, cek, ct) {
|
|
|
427
444
|
}
|
|
428
445
|
}
|
|
429
446
|
function _gcmOpen(cek, nonce, ct, tag, aad, keyBits, icvLen) {
|
|
430
|
-
// Coverage residual: unreachable via a parsed message
|
|
447
|
+
// Coverage residual: unreachable via a parsed message, since the strict parser rejects any
|
|
431
448
|
// AuthEnvelopedData whose mac length != aes-ICVlen (cms/mac-length-mismatch) before we get here.
|
|
432
449
|
if (!tag || tag.length !== icvLen) throw _fail();
|
|
433
450
|
var d = nodeCrypto.createDecipheriv("aes-" + keyBits + "-gcm", cek, nonce, { authTagLength: icvLen });
|
|
@@ -448,14 +465,14 @@ async function _decryptEncryptedData(parsed, km, opts) {
|
|
|
448
465
|
var cek = guard.bytes.view(km.cek, CmsError, "cms/bad-input", "cek");
|
|
449
466
|
if (cek.length !== keyBits / 8) throw _err("cms/bad-input", "the supplied cek length does not match the content algorithm");
|
|
450
467
|
var iv = asn1.read.octetString(asn1.decode(alg.parameters));
|
|
451
|
-
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); }
|
|
452
469
|
catch (_e) { throw _fail(); }
|
|
453
470
|
}
|
|
454
471
|
async function _decryptPbes2(parsed, eci, km, opts) {
|
|
455
472
|
if (km.password == null) throw _err("cms/bad-input", "this EncryptedData needs a { password }");
|
|
456
473
|
// The PBES2 parameters are attacker-controlled structure the strict parser surfaces raw (PBES2 is
|
|
457
474
|
// not an AEAD it validates): parse them behind a structural guard so a malformed shape is a typed
|
|
458
|
-
// 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).
|
|
459
476
|
var kdf, encOid, iv, pb;
|
|
460
477
|
try {
|
|
461
478
|
var params = pbes2.seqChildren(eci.contentEncryptionAlgorithm.parameters, 2, "PBES2 parameters", _err, "cms");
|
|
@@ -473,14 +490,14 @@ async function _decryptPbes2(parsed, eci, km, opts) {
|
|
|
473
490
|
if (!keyBits) throw _err("cms/unsupported-algorithm", "unsupported PBES2 content cipher " + encOid);
|
|
474
491
|
// The password-derived content key is ours; the caller's password buffer is not (passwordBytes
|
|
475
492
|
// passes a supplied Buffer straight through) and is left intact.
|
|
476
|
-
// A string / Uint8Array password is encoded into a buffer
|
|
477
|
-
// 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
|
|
478
495
|
// borrowed and left intact.
|
|
479
496
|
var pwE = pbes2.passwordBytesOwned(km.password, _err, "cms");
|
|
480
497
|
var key;
|
|
481
498
|
try { key = nodeCrypto.pbkdf2Sync(pwE.bytes, pb.salt, pb.iterations, keyBits / 8, pb.prfNode); }
|
|
482
499
|
finally { if (pwE.owned) guard.secret.zeroize(pwE.bytes, CmsError, "cms/bad-input", "the password encoding"); }
|
|
483
|
-
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); }
|
|
484
501
|
catch (_e) { throw _fail(); }
|
|
485
502
|
finally { guard.secret.zeroize(key, CmsError, "cms/bad-input", "the password-derived content-encryption key"); }
|
|
486
503
|
}
|
|
@@ -488,19 +505,19 @@ async function _decryptPbes2(parsed, eci, km, opts) {
|
|
|
488
505
|
// ---- shared helpers (mirror cms-encrypt's builders) ------------------------
|
|
489
506
|
async function _aesKwUnwrap(kek, wrapped) {
|
|
490
507
|
var kekKey = await subtle.importKey("raw", kek, { name: "AES-KW" }, false, ["unwrapKey"]);
|
|
491
|
-
// Import the unwrapped key as HMAC, not AES-CBC: this function
|
|
492
|
-
// 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
|
|
493
510
|
// length, so an AuthenticatedData MAC key that is not 16/24/32 octets (e.g. a 64-octet HMAC-SHA-512
|
|
494
511
|
// key from another implementation) is recovered instead of rejected before the MAC is even checked.
|
|
495
512
|
var raw = await subtle.unwrapKey("raw", wrapped, kekKey, { name: "AES-KW" }, { name: "HMAC", hash: "SHA-256" }, true, ["sign"]);
|
|
496
513
|
// exportKey allocates an ArrayBuffer holding the unwrapped key; the Buffer below is a copy of it,
|
|
497
514
|
// and the original would otherwise stay readable with nothing referencing it. That is a
|
|
498
515
|
// controllable allocation, not one of the runtime-internal copies the best-effort caveat covers.
|
|
499
|
-
// Only the intermediate is wiped
|
|
500
|
-
// 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.
|
|
501
518
|
var rawAb = await subtle.exportKey("raw", raw);
|
|
502
519
|
try {
|
|
503
|
-
// An explicit
|
|
520
|
+
// An explicit copy, not Buffer.from(arrayBuffer): that form returns a view sharing the
|
|
504
521
|
// ArrayBuffer's memory, so wiping the intermediate below would zero the key being returned.
|
|
505
522
|
var view = new Uint8Array(rawAb);
|
|
506
523
|
var out = Buffer.alloc(view.length);
|
|
@@ -523,14 +540,14 @@ function _kemOtherInfo(wrapName, kekBytes, ukm) {
|
|
|
523
540
|
return b.sequence(kids);
|
|
524
541
|
}
|
|
525
542
|
// RFC 3211 double-CBC unwrap (M26): decrypt pass 2 (IV = last block of pass 1), then pass 1
|
|
526
|
-
// (IV = first block), validate the count + complement check bytes
|
|
543
|
+
// (IV = first block), validate the count + complement check bytes, with uniform failure on mismatch.
|
|
527
544
|
function _pwriUnwrap(kek, wrapped, iv, keyBits) {
|
|
528
545
|
var blk = 16, alg = "aes-" + keyBits + "-cbc";
|
|
529
546
|
if (wrapped.length < 2 * blk || wrapped.length % blk !== 0) throw _fail();
|
|
530
547
|
var n = wrapped.length;
|
|
531
548
|
// pass2 = CBC(kek, iv2 = pass1[last block], pass1). Recover pass1's last block by decrypting the
|
|
532
|
-
// last ciphertext block (ECB) and XORing the previous ciphertext block
|
|
533
|
-
// 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.
|
|
534
551
|
var ecb = nodeCrypto.createDecipheriv("aes-" + keyBits + "-ecb", kek, Buffer.alloc(0)); ecb.setAutoPadding(false);
|
|
535
552
|
var lastDec = Buffer.concat([ecb.update(wrapped.subarray(n - blk)), ecb.final()]);
|
|
536
553
|
var iv2 = Buffer.alloc(blk);
|
|
@@ -539,7 +556,7 @@ function _pwriUnwrap(kek, wrapped, iv, keyBits) {
|
|
|
539
556
|
var pass1 = Buffer.concat([d1.update(wrapped), d1.final()]);
|
|
540
557
|
var d2 = nodeCrypto.createDecipheriv(alg, kek, iv); d2.setAutoPadding(false);
|
|
541
558
|
// body is the recovered plaintext block: it holds the CEK. The caller receives an independent
|
|
542
|
-
//
|
|
559
|
+
// copy, so the block is cleared on every exit, including the two validation rejects, which are
|
|
543
560
|
// the paths an attacker induces by tampering with the wrapped key.
|
|
544
561
|
var body = Buffer.concat([d2.update(pass1), d2.final()]);
|
|
545
562
|
try {
|
|
@@ -554,7 +571,7 @@ function _pwriUnwrap(kek, wrapped, iv, keyBits) {
|
|
|
554
571
|
guard.secret.zeroizeAll([body, pass1], CmsError, "cms/bad-input", "the PWRI plaintext block");
|
|
555
572
|
}
|
|
556
573
|
}
|
|
557
|
-
// Coverage residual (the unsupported-algorithm throw arm of each lookup below
|
|
574
|
+
// Coverage residual (the unsupported-algorithm throw arm of each lookup below: _hashW3c,
|
|
558
575
|
// _x963Hash, and _originatorSpki that follow): reachable only from a fully well-formed recipient that
|
|
559
576
|
// names an inner algorithm we do not implement (a non-registry OAEP hash / kari scheme / originator
|
|
560
577
|
// form). The producer never emits one; the fuzz harness (fuzz/cms-decrypt.fuzz.js) drives these arms
|
|
@@ -573,8 +590,8 @@ function _oaepHashFromParams(paramsBytes) {
|
|
|
573
590
|
mgfHash = _hashW3c(asn1.read.oid(mg.children[1].children[0]));
|
|
574
591
|
} else if (ch.tagNumber === 2) { label = asn1.read.octetString(ch.children[0].children[1]); } // pSourceAlgorithm [2]
|
|
575
592
|
});
|
|
576
|
-
// WebCrypto RSA-OAEP ties the MGF1 hash to the OAEP hash and supports only an empty label
|
|
577
|
-
//
|
|
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.
|
|
578
595
|
if (mgfHash != null && mgfHash !== hashName) throw _err("cms/unsupported-algorithm", "the OAEP MGF1 hash must equal the OAEP hash");
|
|
579
596
|
if (label != null && label.length > 0) throw _err("cms/unsupported-algorithm", "a non-empty OAEP label is not supported");
|
|
580
597
|
return hashName;
|
|
@@ -594,7 +611,7 @@ function _kariWrap(kea) {
|
|
|
594
611
|
}
|
|
595
612
|
function _originatorSpki(originator) {
|
|
596
613
|
// originator OriginatorIdentifierOrKey CHOICE; we require originatorKey [1] (RFC 5753 MUST for
|
|
597
|
-
// ephemeral-static). The parser surfaces { form, value:{ algorithm, publicKey } }
|
|
614
|
+
// ephemeral-static). The parser surfaces { form, value:{ algorithm, publicKey } }, so reconstruct
|
|
598
615
|
// the SPKI (SEQUENCE { algorithm, subjectPublicKey BIT STRING }) for importKey.
|
|
599
616
|
if (!originator || originator.form !== "originatorKey") throw _err("cms/unsupported-algorithm", "kari requires an originatorKey (ephemeral-static ECDH)");
|
|
600
617
|
var v = originator.value;
|
|
@@ -640,11 +657,11 @@ var DIGEST_WC = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
|
640
657
|
var MAC_KEY_MIN_OCTETS = 16;
|
|
641
658
|
function _isDerNull(p) { return Buffer.isBuffer(p) && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
|
|
642
659
|
|
|
643
|
-
// 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
|
|
644
661
|
// unwrap decrypt uses, recompute the HMAC over the sec. 9.2 preimage (the re-tagged [2] authAttrs SET
|
|
645
|
-
// OF when present, else the eContent value octets), constant-time-compare to the transmitted mac, and
|
|
646
|
-
//
|
|
647
|
-
// 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
|
|
648
665
|
// secret-dependent failure collapses to the uniform cms/decrypt-failed (no unwrap-success oracle).
|
|
649
666
|
async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
650
667
|
var macAlg = parsed.macAlgorithm;
|
|
@@ -675,10 +692,10 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
|
675
692
|
_assertSupported(candidates[ci].ri, km);
|
|
676
693
|
var macKey = await _acquireCek(candidates[ci].ri, km, opts); // recover the MAC key (uniform failure)
|
|
677
694
|
// RFC 3218 implicit rejection: _ktriCek returns null on an RSA v1.5 unwrap fault, and a hostile
|
|
678
|
-
// ktri/pwri could convey a below-floor (128-bit) key; in
|
|
679
|
-
// 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
|
|
680
697
|
// invalid/short unwrap from a MAC mismatch (Bleichenbacher / weak-key oracle freedom).
|
|
681
|
-
// The substitute is held
|
|
698
|
+
// The substitute is held separately, never overwriting macKey: a hostile recipient can
|
|
682
699
|
// unwrap to a non-null key that is merely too short, and assigning over it would drop the only
|
|
683
700
|
// reference to that recovered secret, leaving it readable while the cleanup cleared the random
|
|
684
701
|
// replacement instead. Both are this module's allocations and both are cleared below.
|
|
@@ -694,14 +711,14 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
|
694
711
|
var actual = Buffer.from(await subtle.digest(mdCheck.hash, content));
|
|
695
712
|
if (!actual.equals(mdCheck.declared)) throw _fail(); // sec. 9.3: recompute, never trust the originator's digest
|
|
696
713
|
}
|
|
697
|
-
return {
|
|
714
|
+
return _originFields({
|
|
698
715
|
content: Buffer.from(content),
|
|
699
716
|
contentType: parsed.encapContentInfo.eContentType, contentTypeName: oid.name(parsed.encapContentInfo.eContentType) || parsed.encapContentInfo.eContentType,
|
|
700
717
|
recipientType: candidates[ci].ri.type, recipientIndex: candidates[ci].index,
|
|
701
718
|
macAlgorithm: macAlg.name || macAlg.oid,
|
|
702
719
|
digestAlgorithm: parsed.digestAlgorithm ? (parsed.digestAlgorithm.name || parsed.digestAlgorithm.oid) : null,
|
|
703
720
|
authenticated: true,
|
|
704
|
-
};
|
|
721
|
+
}, "message-authentication-key", parsed.originatorInfo);
|
|
705
722
|
} finally {
|
|
706
723
|
guard.secret.zeroizeAll([macKey, macSubstitute], CmsError, "cms/bad-input", "the message-authentication key");
|
|
707
724
|
}
|
|
@@ -718,7 +735,7 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
|
718
735
|
// the frozen cms/unsupported-algorithm code, not a round-trip.
|
|
719
736
|
// * the detached (absent eContent) reject is the deferred-with-condition path (this producer always
|
|
720
737
|
// attaches the content); re-open when a detached AuthenticatedData caller appears.
|
|
721
|
-
// * `parsed.authAttrs || []` / `parsed.recipientInfos || []
|
|
738
|
+
// * `parsed.authAttrs || []` / `parsed.recipientInfos || []`: the parser always surfaces these as
|
|
722
739
|
// arrays (or rejects), so the `|| []` fallbacks never fire.
|
|
723
740
|
// * `oid.name(eContentType) || eContentType` fires only for an unregistered content type; `macAlg.name
|
|
724
741
|
// || macAlg.oid` and `digestAlgorithm.name || .oid` never reach the `.oid` fallback because the
|
|
@@ -726,17 +743,17 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
|
726
743
|
// * the `macKey == null` half of the random-key substitution fires only for a hand-crafted RSA v1.5
|
|
727
744
|
// ktri (this producer emits OAEP); its behaviour is identical to the tested below-floor path (a
|
|
728
745
|
// random key -> the MAC verify fails uniformly), so the < 16 vector covers the substitution.
|
|
729
|
-
// _normKeyDer(key) -> { der, owned }
|
|
730
|
-
// 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.
|
|
731
748
|
//
|
|
732
749
|
// The distinction decides who may wipe it. A caller handing in their own Buffer keeps a live
|
|
733
750
|
// reference and will use it again; wiping that would destroy the key out from under them. The other
|
|
734
|
-
// two forms produce a
|
|
751
|
+
// two forms produce a new buffer here (a Uint8Array is copied, a PEM string is decoded), and that
|
|
735
752
|
// buffer is a second copy of a private key which nothing else can reach, so it lives until the
|
|
736
753
|
// garbage collector happens to reuse the page unless this module clears it.
|
|
737
754
|
//
|
|
738
|
-
// Returning the flag
|
|
739
|
-
// 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
|
|
740
757
|
// problem of having one.
|
|
741
758
|
function _normKeyDer(key) {
|
|
742
759
|
if (Buffer.isBuffer(key)) return { der: key, owned: false };
|
|
@@ -754,8 +771,7 @@ function _releaseKeyDer(k) {
|
|
|
754
771
|
if (k && k.owned) guard.secret.zeroize(k.der, CmsError, "cms/bad-input", "the recipient private-key copy");
|
|
755
772
|
}
|
|
756
773
|
function _normCertDer(cert) {
|
|
757
|
-
if (Buffer.isBuffer(cert)) return cert;
|
|
758
|
-
if (cert instanceof Uint8Array) return Buffer.from(cert);
|
|
774
|
+
if (Buffer.isBuffer(cert) || cert instanceof Uint8Array) return guard.bytes.snapshot(cert, CmsError, "cms/bad-input", "the recipient certificate");
|
|
759
775
|
if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cms/bad-input", "the recipient certificate PEM could not be decoded", e); } }
|
|
760
776
|
throw _err("cms/bad-input", "the recipient certificate must be a DER Buffer or PEM string");
|
|
761
777
|
}
|