@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.
Files changed (104) hide show
  1. package/CHANGELOG.md +419 -378
  2. package/MIGRATING.md +65 -0
  3. package/README.md +12 -12
  4. package/lib/acme.js +31 -31
  5. package/lib/asn1-der.js +10 -10
  6. package/lib/attrcert-sign.js +24 -20
  7. package/lib/byte-reader.js +6 -6
  8. package/lib/byte-writer.js +5 -5
  9. package/lib/cbor-det.js +27 -24
  10. package/lib/cmc-build.js +104 -104
  11. package/lib/cmc-verify.js +108 -32
  12. package/lib/cmp-build.js +31 -26
  13. package/lib/cmp-session.js +74 -72
  14. package/lib/cmp-verify.js +72 -58
  15. package/lib/cms-compress.js +8 -9
  16. package/lib/cms-decrypt.js +92 -76
  17. package/lib/cms-encrypt.js +33 -34
  18. package/lib/cms-sign.js +100 -54
  19. package/lib/cms-verify.js +141 -82
  20. package/lib/composite-sig.js +13 -13
  21. package/lib/constants.js +4 -4
  22. package/lib/crl-sign.js +31 -25
  23. package/lib/crl-verify.js +7 -6
  24. package/lib/crmf-sign.js +19 -15
  25. package/lib/csr-sign.js +13 -9
  26. package/lib/ct.js +37 -37
  27. package/lib/edwards-point.js +7 -7
  28. package/lib/est.js +103 -59
  29. package/lib/framework-error.js +5 -5
  30. package/lib/guard-all.js +3 -3
  31. package/lib/guard-async.js +4 -4
  32. package/lib/guard-bytes.js +378 -15
  33. package/lib/guard-compress.js +17 -17
  34. package/lib/guard-crypto.js +1 -1
  35. package/lib/guard-encoding.js +15 -15
  36. package/lib/guard-header.js +3 -3
  37. package/lib/guard-identifier.js +16 -16
  38. package/lib/guard-json.js +15 -15
  39. package/lib/guard-limits.js +7 -7
  40. package/lib/guard-name.js +81 -16
  41. package/lib/guard-parsed.js +144 -75
  42. package/lib/guard-range.js +19 -19
  43. package/lib/guard-secret.js +11 -10
  44. package/lib/guard-text.js +6 -6
  45. package/lib/guard-time.js +10 -10
  46. package/lib/hpke.js +18 -17
  47. package/lib/http-digest.js +35 -35
  48. package/lib/http-retry-after.js +13 -13
  49. package/lib/http-transport.js +20 -19
  50. package/lib/inspect.js +53 -53
  51. package/lib/ip-utils.js +2 -2
  52. package/lib/jose.js +13 -13
  53. package/lib/key.js +16 -16
  54. package/lib/lint.js +51 -51
  55. package/lib/merkle.js +51 -36
  56. package/lib/mime.js +18 -18
  57. package/lib/ocsp-verify.js +10 -10
  58. package/lib/ocsp.js +32 -20
  59. package/lib/oid.js +29 -29
  60. package/lib/path-validate.js +114 -113
  61. package/lib/pbes2.js +16 -16
  62. package/lib/pkcs12-build.js +71 -56
  63. package/lib/pki-build.js +23 -22
  64. package/lib/rc2.js +1 -1
  65. package/lib/rfc3339.js +5 -5
  66. package/lib/schema-all.js +31 -31
  67. package/lib/schema-attrcert.js +12 -12
  68. package/lib/schema-c509.js +144 -142
  69. package/lib/schema-cmc.js +58 -58
  70. package/lib/schema-cmp.js +43 -43
  71. package/lib/schema-cms.js +169 -36
  72. package/lib/schema-crl.js +7 -7
  73. package/lib/schema-crmf.js +28 -28
  74. package/lib/schema-csr.js +12 -12
  75. package/lib/schema-csrattrs.js +16 -16
  76. package/lib/schema-engine.js +18 -18
  77. package/lib/schema-ocsp.js +15 -15
  78. package/lib/schema-pkcs12.js +20 -20
  79. package/lib/schema-pkcs8.js +2 -2
  80. package/lib/schema-pkix.js +131 -126
  81. package/lib/schema-smime.js +19 -19
  82. package/lib/schema-tsp.js +12 -12
  83. package/lib/schema-x509.js +3 -3
  84. package/lib/shbs.js +18 -18
  85. package/lib/sign-scheme.js +19 -16
  86. package/lib/sigstore.js +10 -11
  87. package/lib/sleep.js +1 -1
  88. package/lib/smime.js +308 -96
  89. package/lib/tls-cert-compress.js +18 -18
  90. package/lib/trust.js +27 -27
  91. package/lib/tsp-sign.js +22 -18
  92. package/lib/validator-all.js +1 -1
  93. package/lib/validator-attcert.js +1 -1
  94. package/lib/validator-cose.js +43 -44
  95. package/lib/validator-keydesc.js +3 -3
  96. package/lib/validator-sig.js +13 -13
  97. package/lib/validator-tls.js +11 -11
  98. package/lib/validator-tpm.js +21 -20
  99. package/lib/webauthn-mds.js +67 -67
  100. package/lib/webauthn.js +34 -34
  101. package/lib/webcrypto.js +15 -15
  102. package/lib/x509-sign.js +24 -15
  103. package/package.json +3 -2
  104. package/sbom.cdx.json +6 -6
@@ -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 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
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 ONE uniform secret-dependent-failure verdict. No cause chaining that distinguishes the site.
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 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 (rather than 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:
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 -- accepted by the pkcs12 parser)
59
- // opens without hitting the public decrypt()'s strict-DER re-parse. Callers pass a TRUSTED parse result only.
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 MODE is a structural property of the message -- readable from the algorithm
68
- // identifier with no key material at all -- so it is decided HERE, before any recipient is tried.
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 AFTER the content is open -- it is the key the next stage
82
- // needs, so an earlier wipe would break decryption rather than protect it. The returned object
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 -- an arbitrary object that merely
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 (NOT secret-dependent) -------------------
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 -- only SECRET-DEPENDENT failures
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 -- _selectCandidates only returns one of the five handled types.
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) -- a fail-closed reject the fuzz harness exercises.
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 -- the exact outcome the wipe is for.
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 THIS recipient's RecipientEncryptedKey (matched by rid), not element 0 -- a kari may list
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 is used BOTH as the HKDF salt AND as the ECC-CMS-SharedInfo
271
- // entityUInfo -- mirror the producer so both sides derive the same KEK as a conformant peer.
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 -- on the failing path
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 THIS toolkit allocated -- a credential
321
- // copy -- so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
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 rather than silently derived with the wrong one.
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 -- the strict parser
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 BEFORE decap
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 -- the import is
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 FAILS --
371
- // the common case under a wrong key or a tampered encryptedKey -- leaves nothing behind either;
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 -- telling the caller the
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 THIS function's allocation, so this function clears it: reassigning the
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
- // rather than wiping `cek` here, because on the normal path `cek` is the CALLER's recovered key
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 -- the strict parser rejects any
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 -- not a decrypt oracle).
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 THIS toolkit allocated -- a credential
477
- // copy -- so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
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 EXPORTS to raw bytes, so the CryptoKey
492
- // type is transient and the exported bytes are identical for an AES CEK -- but HMAC accepts any wrap
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 -- the returned Buffer IS the key the caller must use to open
500
- // the content, so wiping it here would destroy the result rather than protect it.
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 COPY, not Buffer.from(arrayBuffer) -- that form returns a VIEW sharing the
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 -- uniform failure on mismatch.
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 -- a standard CBC last-
533
- // block decrypt that needs no IV -- then CBC-decrypt pass2 under that recovered iv2 to get pass1.
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
- // COPY, so the block is cleared on every exit -- including the two validation rejects, which are
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 -- _hashW3c,
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 -- reject,
577
- // rather than silently ignore, any parameter set we cannot faithfully honor.
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 } } -- reconstruct
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 SAME RecipientInfo
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
- // -- when authAttrs are present -- INDEPENDENTLY confirm message-digest == digest(content) (sec. 9.3
647
- // do-not-trust-originator). The content is returned ONLY after every check passes. Every
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 EITHER case substitute a fresh random
679
- // key so the MAC verify still RUNS and fails uniformly -- never a fast-path that distinguishes an
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 SEPARATELY rather than overwriting macKey: a hostile recipient can
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 || []` -- the parser always surfaces these as
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 } -- the recipient private key as PKCS#8 DER, and whether the
730
- // buffer is one THIS module made.
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 NEW buffer here -- a Uint8Array is copied, a PEM string is decoded -- and that
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 rather than always copying keeps the caller's buffer un-duplicated: making our
739
- // own copy of every key so we could uniformly wipe it would ADD a copy of the secret to solve the
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
  }