@blamejs/pki 0.6.14 → 0.6.16

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 (112) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/lib/acme.js +59 -815
  3. package/lib/asn1-der.js +7 -245
  4. package/lib/attrcert-sign.js +12 -260
  5. package/lib/attrcert-verify.js +1 -7
  6. package/lib/byte-reader.js +0 -8
  7. package/lib/byte-writer.js +0 -11
  8. package/lib/cbor-det.js +0 -54
  9. package/lib/cmc-build.js +15 -357
  10. package/lib/cmc-verify.js +6 -239
  11. package/lib/cmp-build.js +41 -317
  12. package/lib/cmp-session.js +73 -760
  13. package/lib/cmp-verify.js +32 -488
  14. package/lib/cms-compress.js +3 -32
  15. package/lib/cms-decrypt.js +29 -294
  16. package/lib/cms-encrypt.js +14 -234
  17. package/lib/cms-sign.js +29 -324
  18. package/lib/cms-verify.js +9 -500
  19. package/lib/composite-kem.js +2 -102
  20. package/lib/composite-sig.js +1 -77
  21. package/lib/constants.js +0 -249
  22. package/lib/crl-sign.js +9 -237
  23. package/lib/crl-verify.js +1 -10
  24. package/lib/crmf-sign.js +19 -198
  25. package/lib/crmf-verify.js +1 -12
  26. package/lib/csr-sign.js +3 -34
  27. package/lib/csr-verify.js +1 -12
  28. package/lib/ct.js +28 -253
  29. package/lib/edwards-point.js +10 -44
  30. package/lib/est.js +28 -764
  31. package/lib/framework-error.js +4 -182
  32. package/lib/guard-all.js +1 -58
  33. package/lib/guard-async.js +1 -36
  34. package/lib/guard-bytes.js +6 -650
  35. package/lib/guard-compress.js +5 -102
  36. package/lib/guard-crypto.js +1 -40
  37. package/lib/guard-der.js +2 -93
  38. package/lib/guard-encoding.js +2 -60
  39. package/lib/guard-header.js +1 -36
  40. package/lib/guard-identifier.js +7 -531
  41. package/lib/guard-intrinsic.js +1 -150
  42. package/lib/guard-json.js +10 -73
  43. package/lib/guard-limits.js +1 -77
  44. package/lib/guard-list.js +1 -49
  45. package/lib/guard-name.js +3 -209
  46. package/lib/guard-parsed.js +7 -335
  47. package/lib/guard-range.js +1 -71
  48. package/lib/guard-secret.js +1 -102
  49. package/lib/guard-text.js +1 -64
  50. package/lib/guard-time.js +1 -97
  51. package/lib/hpke.js +3 -105
  52. package/lib/http-digest.js +14 -142
  53. package/lib/http-retry-after.js +1 -47
  54. package/lib/http-transport.js +21 -131
  55. package/lib/inspect.js +30 -284
  56. package/lib/ip-utils.js +6 -47
  57. package/lib/jose.js +7 -142
  58. package/lib/key.js +2 -67
  59. package/lib/lint.js +9 -194
  60. package/lib/merkle.js +1 -27
  61. package/lib/mime.js +9 -82
  62. package/lib/ocsp-request-verify.js +1 -14
  63. package/lib/ocsp-verify.js +1 -82
  64. package/lib/ocsp.js +11 -169
  65. package/lib/oid.js +5 -337
  66. package/lib/path-validate.js +103 -1492
  67. package/lib/pbes2.js +2 -89
  68. package/lib/pkcs12-build.js +31 -379
  69. package/lib/pki-build.js +64 -290
  70. package/lib/rc2.js +2 -18
  71. package/lib/rfc3339.js +4 -27
  72. package/lib/scep.js +140 -286
  73. package/lib/schema-all.js +0 -122
  74. package/lib/schema-attrcert.js +4 -146
  75. package/lib/schema-c509.js +153 -802
  76. package/lib/schema-cmc.js +5 -220
  77. package/lib/schema-cmp.js +2 -223
  78. package/lib/schema-cms.js +13 -427
  79. package/lib/schema-crl.js +5 -68
  80. package/lib/schema-crmf.js +6 -167
  81. package/lib/schema-csr.js +1 -54
  82. package/lib/schema-csrattrs.js +2 -87
  83. package/lib/schema-engine.js +0 -152
  84. package/lib/schema-ocsp.js +0 -117
  85. package/lib/schema-pkcs12.js +2 -93
  86. package/lib/schema-pkcs8.js +3 -35
  87. package/lib/schema-pkix.js +24 -655
  88. package/lib/schema-smime.js +0 -54
  89. package/lib/schema-tsp.js +3 -99
  90. package/lib/schema-x509.js +4 -63
  91. package/lib/shbs.js +11 -95
  92. package/lib/sign-scheme.js +2 -97
  93. package/lib/sigstore.js +9 -169
  94. package/lib/sleep.js +2 -8
  95. package/lib/smime.js +15 -408
  96. package/lib/tls-cert-compress.js +0 -62
  97. package/lib/trust.js +19 -185
  98. package/lib/tsp-sign.js +16 -270
  99. package/lib/validator-all.js +1 -30
  100. package/lib/validator-attcert.js +2 -46
  101. package/lib/validator-cose.js +2 -153
  102. package/lib/validator-keydesc.js +1 -37
  103. package/lib/validator-sig.js +1 -46
  104. package/lib/validator-tls.js +1 -88
  105. package/lib/validator-tpm.js +19 -122
  106. package/lib/verify-seam.js +1 -24
  107. package/lib/webauthn-mds.js +3 -400
  108. package/lib/webauthn.js +12 -833
  109. package/lib/webcrypto.js +5 -485
  110. package/lib/x509-sign.js +4 -108
  111. package/package.json +1 -1
  112. package/sbom.cdx.json +6 -6
@@ -1,18 +1,8 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
3
  "use strict";
4
- //
5
- // @internal -- the pki.cms.decrypt implementation. The operator-facing @module pki.cms + the
4
+ // @internal
6
5
  // @primitive pki.cms.decrypt block live in cms-verify.js, which re-exports this function.
7
- //
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
13
- // verdict `cms/decrypt-failed` (Bleichenbacher / EFAIL / PWRI-check-byte oracle freedom); the
14
- // ktri v1.5 arm substitutes a random CEK on any decode fault (RFC 3218 implicit rejection) so its
15
- // failure emerges at stage 3 identically to every other bad-key path.
16
6
 
17
7
  var nodeCrypto = require("crypto");
18
8
  var asn1 = require("./asn1-der");
@@ -31,20 +21,15 @@ var subtle = webcrypto.webcrypto.subtle;
31
21
  var CmsError = frameworkError.CmsError;
32
22
  var WRAP_KEK_LENGTHS = schemaCms.WRAP_KEK_LENGTHS;
33
23
  var KEM_CT_LENGTHS = schemaCms.KEM_CT_LENGTHS;
34
- // The parser's own AEAD table: the single place that decides whether a content-encryption OID names
35
- // an authenticated encryption algorithm, so the container-vs-mode rule below cannot drift from the
36
- // parameter validation that accepted the message.
37
24
  var AEAD_ALGS = schemaCms.AEAD_ALGS;
38
25
 
39
26
  function O(n) { return oid.byName(n); }
40
27
  function _err(code, message, cause) { return new CmsError(code, message, cause); }
41
- // The single uniform secret-dependent-failure verdict. No cause chaining that distinguishes the site.
42
28
  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)"); }
43
29
 
44
- var CONTENT_KEYBITS = pbes2.CONTENT_KEYBITS; // content-encryption OID -> key bits (the shared PBES2 table)
45
- var CONTENT_MODE = pbes2.CONTENT_MODE; // the same rows' cipher mode ("cbc" | "gcm")
30
+ var CONTENT_KEYBITS = pbes2.CONTENT_KEYBITS;
31
+ var CONTENT_MODE = pbes2.CONTENT_MODE;
46
32
 
47
- // ---- entry -----------------------------------------------------------------
48
33
  async function decrypt(input, keyMaterial, opts) {
49
34
  opts = opts || {};
50
35
  if (keyMaterial == null || typeof keyMaterial !== "object") throw _err("cms/bad-input", "decrypt requires a key-material object");
@@ -56,24 +41,7 @@ async function decrypt(input, keyMaterial, opts) {
56
41
  return decryptEnvelopedData(parsed, keyMaterial, opts, ct);
57
42
  }
58
43
 
59
- // The post-parse decrypt of an already-parsed EnvelopedData / AuthEnvelopedData: select a recipient, acquire
60
- // the CEK, open the content, oracle-free (every secret-dependent failure collapses to cms/decrypt-failed).
61
- // @internal -- not re-exported on pki.cms. Taking the parsed structure in place of DER is a deliberate seam
62
- // for a sibling composer that already holds a strictly-walked content node it must not re-serialize/re-parse:
63
- // pki.pkcs12.open drives this off the pkcs12 parser's walkEnvelopedData result so an RFC 7292 privacy safe
64
- // whose EnvelopedData is BER-encoded (indefinite lengths, some NSS output the pkcs12 parser accepts)
65
- // opens without hitting the public decrypt()'s strict-DER re-parse. Callers pass a trusted parse result only.
66
- // `authenticated` answers a question about the content and the key that opened it. It
67
- // cannot answer the question an operator hears in the word -- who sent this -- and no
68
- // recipient type this toolkit supports authenticates an individual originator: a ktri or
69
- // ephemeral-static kari message is minted by anyone holding the recipient's PUBLIC key,
70
- // and a password or KEK message is minted by any co-recipient sharing the secret. The
71
- // origin question therefore gets its own field instead of being left to the reader's
72
- // inference. originatorInfo rides outside the AEAD's authenticated data, so it is a hint
73
- // the sender chose, never evidence, and it is surfaced saying so rather than dropped.
74
- // Every verdict this module returns carries all three, including the unauthenticated
75
- // ciphers: a field present on some returns and absent on others is read as a capability
76
- // difference, and the caller cannot tell "no origin" from "this path forgot to say".
44
+ // @internal
77
45
  function _originFields(res, authenticatedBy, originatorInfo) {
78
46
  res.authenticatedBy = authenticatedBy || null;
79
47
  res.originAuthenticated = false;
@@ -86,31 +54,16 @@ async function decryptEnvelopedData(parsed, keyMaterial, opts, contentTypeName)
86
54
  if (keyMaterial == null || typeof keyMaterial !== "object") throw _err("cms/bad-input", "decrypt requires a key-material object");
87
55
  var ct = contentTypeName || "envelopedData";
88
56
  var recips = parsed.recipientInfos || [];
89
- var candidates = _selectCandidates(recips, keyMaterial, opts); // stage 1 (typed, distinct)
57
+ var candidates = _selectCandidates(recips, keyMaterial, opts);
90
58
  var eci = parsed.encryptedContentInfo;
91
- // The content cipher's mode is a structural property of the message, readable from the algorithm
92
- // identifier with no key material at all, so it is decided here, before any recipient is tried.
93
- // Deciding it per candidate would make a structural verdict depend on key acquisition succeeding
94
- // first: several password or KEK recipients can match the same key material, so the distinct
95
- // cms/unsupported-algorithm would be swallowed by the ambiguous-candidate loop below and the caller
96
- // told their key was wrong, after repeating a PBKDF2 derivation for every candidate to learn it.
97
59
  _assertContentCipherMode(eci, ct);
98
- // Password- and KEK-based recipients carry no rid, so several may match the same key material; try
99
- // each until one yields a CEK that opens the content. A single candidate propagates its exact
100
- // verdict; multiple ambiguous candidates collapse to the uniform verdict so nothing is leaked.
101
60
  for (var ci = 0; ci < candidates.length; ci++) {
102
61
  try {
103
- _assertSupported(candidates[ci].ri, keyMaterial); // distinct-code reject (MQV / non-KEM ori)
104
- var cek = await _acquireCek(candidates[ci].ri, keyMaterial, opts); // stage 2 (uniform)
105
- // The recovered CEK is cleared only after the content is open: it is the key the next stage
106
- // needs, so an earlier wipe would break decryption instead of protecting it. The returned object
107
- // is built before the `finally` runs and holds the plaintext, not the key.
62
+ _assertSupported(candidates[ci].ri, keyMaterial);
63
+ var cek = await _acquireCek(candidates[ci].ri, keyMaterial, opts);
108
64
  try {
109
- // Lazy CBC streaming (the memory win) is safe only with one candidate; several ambiguous
110
- // candidates need the content decrypt to run eagerly so a wrong recipient is rejected and the
111
- // next tried, exactly as the buffered path disambiguates.
112
65
  var streamMode = opts.stream === true ? (candidates.length > 1 ? "eager" : "lazy") : null;
113
- var content = await _openContent(parsed, eci, cek, ct, streamMode); // stage 3 (uniform)
66
+ var content = await _openContent(parsed, eci, cek, ct, streamMode);
114
67
  return _originFields({
115
68
  content: content,
116
69
  contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType,
@@ -122,15 +75,13 @@ async function decryptEnvelopedData(parsed, keyMaterial, opts, contentTypeName)
122
75
  guard.secret.zeroize(cek, CmsError, "cms/bad-input", "the recovered content-encryption key");
123
76
  }
124
77
  } catch (e) {
125
- if (candidates.length === 1) throw e; // one candidate: preserve its exact (distinct or uniform) verdict
78
+ if (candidates.length === 1) throw e;
126
79
  }
127
80
  }
128
- throw _fail(); // every ambiguous candidate failed -> uniform, leaking nothing about which
81
+ throw _fail();
129
82
  }
130
83
 
131
84
  function _parse(input) {
132
- // Always drive the strict parser over caller-supplied bytes: an arbitrary object that merely
133
- // carries a contentTypeName is not a trusted parse result and must not bypass DER validation.
134
85
  return schemaCms.parse(_toDer(input));
135
86
  }
136
87
  function _toDer(input) {
@@ -139,9 +90,6 @@ function _toDer(input) {
139
90
  throw _err("cms/bad-input", "input must be a DER Buffer, Uint8Array, or PEM string");
140
91
  }
141
92
 
142
- // ---- stage 1: recipient selection (not secret-dependent) -------------------
143
- // Returns every recipient the key material may target. An explicit recipientIndex or an rid match
144
- // yields exactly one; unlabeled pwri / kekri recipients yield all matches for the caller to try.
145
93
  function _selectCandidates(recips, km, opts) {
146
94
  if (!recips.length) throw _err("cms/no-matching-recipient", "the message carries no RecipientInfos");
147
95
  if (opts.recipientIndex != null) {
@@ -155,7 +103,7 @@ function _selectCandidates(recips, km, opts) {
155
103
  for (var k = 0; k < recips.length; k++) {
156
104
  var r = recips[k];
157
105
  if (want === "asym" && (r.type === "ktri" || r.type === "kari" || r.type === "ori")) {
158
- if (cert && _ridMatches(r, cert)) out.push({ ri: r, index: k }); // a cert may match several recipients
106
+ if (cert && _ridMatches(r, cert)) out.push({ ri: r, index: k });
159
107
  } else if (want === "pwri" && r.type === "pwri") { out.push({ ri: r, index: k }); }
160
108
  else if (want === "kekri" && r.type === "kekri") {
161
109
  if (km.kekId == null || _kekIdMatches(r, km.kekId)) out.push({ ri: r, index: k });
@@ -171,7 +119,7 @@ function _riKindForKey(km) {
171
119
  throw _err("cms/bad-input", "key material needs { key, cert }, { password }, { kek }, or { cek }");
172
120
  }
173
121
  function _assertSupported(ri, km) {
174
- if (ri.type === "kari" && ri.keyEncryptionAlgorithm && intrinsic.stringIndexOf(intrinsic.toLowerCase(ri.keyEncryptionAlgorithm.name || ""), "mqv") !== -1) throw _err("cms/unsupported-algorithm", "ECMQV kari is not supported"); // /mqv/i (ASCII name)
122
+ if (ri.type === "kari" && ri.keyEncryptionAlgorithm && intrinsic.stringIndexOf(intrinsic.toLowerCase(ri.keyEncryptionAlgorithm.name || ""), "mqv") !== -1) throw _err("cms/unsupported-algorithm", "ECMQV kari is not supported");
175
123
  if (ri.type === "ori") {
176
124
  if (ri.oriType !== O("kem")) throw _err("cms/unsupported-recipient-type", "unsupported OtherRecipientInfo type " + ri.oriType);
177
125
  if (ri.kemri && ri.kemri.kem && ri.kemri.kem.oid === O("id-kem-rsa")) throw _err("cms/unsupported-algorithm", "RSA-KEM is not supported");
@@ -180,8 +128,6 @@ function _assertSupported(ri, km) {
180
128
  }
181
129
 
182
130
  function _ridMatches(ri, cert) {
183
- // A kari carries a RecipientEncryptedKey per agreeing recipient (RFC 5652 sec. 6.2.2); this
184
- // recipient may be at ANY position, so match against every rek, not just element 0.
185
131
  if (ri.type === "kari") return !!_kariRekFor(ri, cert);
186
132
  var rid = ri.rid || (ri.kemri && ri.kemri.rid);
187
133
  if (!rid) return false;
@@ -198,7 +144,7 @@ function _ridEq(rid, cert) {
198
144
  catch (_e) { return false; }
199
145
  }
200
146
  if (rid.subjectKeyIdentifier) { var ski = _skiOf(cert); return !!ski && Buffer.compare(ski, rid.subjectKeyIdentifier) === 0; }
201
- return false; // coverage residual: unreachable -- a parsed rid is always issuerAndSerialNumber or a SKI form
147
+ return false;
202
148
  }
203
149
  function _skiOf(cert) {
204
150
  var exts = cert.extensions || [];
@@ -213,11 +159,8 @@ function _kekIdMatches(ri, kekId) {
213
159
  return Buffer.compare(id, guard.bytes.view(kekId, CmsError, "cms/bad-input", "kekId")) === 0;
214
160
  }
215
161
 
216
- // Codes that name a structural / resource / config fault and MUST NOT be masked by the uniform
217
- // secret-dependent verdict (they leak nothing about the key or plaintext).
218
162
  var _passThrough = { "cms/unsupported-algorithm": 1, "cms/unsupported-recipient-type": 1, "cms/bad-input": 1, "cms/iteration-limit": 1, "cms/missing-key-derivation": 1, "cms/no-encrypted-content": 1 };
219
163
 
220
- // ---- stage 2: acquire the CEK (uniform failure) ----------------------------
221
164
  async function _acquireCek(ri, km, opts) {
222
165
  try {
223
166
  if (ri.type === "ktri") return await _ktriCek(ri, km);
@@ -226,21 +169,15 @@ async function _acquireCek(ri, km, opts) {
226
169
  if (ri.type === "pwri") return await _pwriCek(ri, km, opts);
227
170
  if (ri.type === "ori") return await _kemriCek(ri, km);
228
171
  } catch (e) {
229
- // Structural / resource / config faults keep their own code; only secret-dependent failures
230
- // (a bad unwrap, a wrong key, a padding fault) collapse to the uniform verdict.
231
172
  if (e instanceof CmsError && _passThrough[e.code]) throw e;
232
173
  throw _fail();
233
174
  }
234
- // Coverage residual: unreachable, since _selectCandidates only returns one of the five handled types.
235
175
  throw _err("cms/unsupported-recipient-type", "unsupported recipient type " + ri.type);
236
176
  }
237
177
 
238
- // ktri: OAEP or PKCS#1 v1.5 (v1.5 = decrypt-only + RFC 3218 implicit rejection).
239
178
  async function _ktriCek(ri, km) {
240
179
  var kea = ri.keyEncryptionAlgorithm;
241
180
  var k = _normKeyDer(km.key);
242
- // The wipe is in a `finally` so it happens on the reject path too: a message crafted to fail the
243
- // unwrap must not be a way to leave the key copy in memory.
244
181
  try {
245
182
  if (kea.oid === O("rsaesOaep")) {
246
183
  var hash = _oaepHashFromParams(kea.parameters);
@@ -248,32 +185,20 @@ async function _ktriCek(ri, km) {
248
185
  return Buffer.from(await subtle.decrypt({ name: "RSA-OAEP" }, pub, ri.encryptedKey));
249
186
  }
250
187
  if (kea.oid === O("rsaEncryption")) {
251
- // RFC 3218 sec. 2.3.2 implicit rejection: NEVER surface a v1.5 failure here. Any decode fault
252
- // yields a fresh random CEK of the content-alg length; the mismatch emerges at stage 3.
253
188
  var keyObj = nodeCrypto.createPrivateKey({ key: k.der, format: "der", type: "pkcs8" });
254
189
  try { return nodeCrypto.privateDecrypt({ key: keyObj, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }, ri.encryptedKey); }
255
190
  catch (_e) {
256
- return null; // signal: use a random CEK (length decided at open time)
191
+ return null;
257
192
  }
258
193
  }
259
- // Coverage residual: reachable only from a hostile message (our encrypt emits only OAEP; OpenSSL
260
- // emits OAEP or rsaEncryption): a fail-closed reject the fuzz harness exercises.
261
194
  throw _err("cms/unsupported-algorithm", "unsupported ktri keyEncryptionAlgorithm " + kea.oid);
262
195
  } finally {
263
196
  _releaseKeyDer(k);
264
197
  }
265
198
  }
266
199
 
267
- // kari: reconstruct Z from the originatorKey + recipient private key, KDF -> KEK, AES-KW unwrap.
268
200
  async function _kariCek(ri, km) {
269
- // The copy is taken and the protected region opens IMMEDIATELY. Everything between them would
270
- // otherwise be an unprotected window, and it is not a narrow one: reading the wrap algorithm, the
271
- // originator's key, the recipient's certificate and the matching RecipientEncryptedKey all parse
272
- // attacker-supplied structure and all throw on malformed input. A message crafted to fail any one
273
- // of them would be a way to leave the key copy in the heap, the exact outcome the wipe is for.
274
201
  var k = _normKeyDer(km.key);
275
- // The agreement secret and the KEK derived from it are both allocated here; one `finally` clears
276
- // whichever branch produced them, including when the unwrap below throws on a tampered key.
277
202
  var kek, z, mz;
278
203
  try {
279
204
  var keyDer = k.der;
@@ -281,8 +206,6 @@ async function _kariCek(ri, km) {
281
206
  var wrapAlg = _kariWrap(kea);
282
207
  var scheme = kea.oid;
283
208
  var origSpki = _originatorSpki(ri.originator);
284
- // Unwrap this recipient's own RecipientEncryptedKey (matched by rid), not element 0; a kari may list
285
- // several recipients under one ephemeral key.
286
209
  var rek = (km.cert != null && _kariRekFor(ri, x509.parse(_normCertDer(km.cert)))) || ri.recipientEncryptedKeys[0];
287
210
  var kekBytes = WRAP_KEK_LENGTHS[wrapAlg.oid];
288
211
  if (!kekBytes) throw _err("cms/unsupported-algorithm", "unsupported kari key-wrap");
@@ -294,13 +217,8 @@ async function _kariCek(ri, km) {
294
217
  mz = Buffer.from(await subtle.deriveBits({ name: mont.name, public: origPub }, recipPriv, null));
295
218
  if (mz.every(function (x) { return x === 0; })) throw _fail();
296
219
  var mzKey = await subtle.importKey("raw", mz, { name: "HKDF" }, false, ["deriveBits"]);
297
- // RFC 8418 sec. 2.2: a present ukm serves as both the HKDF salt and the ECC-CMS-SharedInfo
298
- // entityUInfo, so mirror the producer and both sides derive the same KEK as a conformant peer.
299
220
  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));
300
221
  } else {
301
- // RFC 5753 sec. 7.1 permits the originator EC key to omit its curve parameters, inheriting the
302
- // curve from the recipient's certificate; resolve the curve from the recipient (authoritative)
303
- // and rebuild the originator SPKI with explicit parameters so importKey can consume it.
304
222
  var origAlg = asn1.decode(origSpki).children[0];
305
223
  var origHasParams = origAlg.children.length > 1;
306
224
  var curveOid = (km.cert != null && _ecCurveFromCert(km.cert)) || (origHasParams ? asn1.read.oid(origAlg.children[1]) : null);
@@ -320,13 +238,11 @@ async function _kariCek(ri, km) {
320
238
  }
321
239
  }
322
240
 
323
- // kekri: AES-KW unwrap under the caller-supplied KEK.
324
241
  async function _kekriCek(ri, km) {
325
242
  var kek = guard.bytes.view(km.kek, CmsError, "cms/bad-input", "kek");
326
243
  return await _aesKwUnwrap(kek, ri.encryptedKey);
327
244
  }
328
245
 
329
- // pwri: PBKDF2 -> KEK, RFC 3211 double-CBC unwrap.
330
246
  async function _pwriCek(ri, km, opts) {
331
247
  var kdf = ri.keyDerivationAlgorithm;
332
248
  if (!kdf) throw _err("cms/missing-key-derivation", "the pwri recipient has no keyDerivationAlgorithm (externally-supplied KEK is not supported)");
@@ -334,19 +250,11 @@ async function _pwriCek(ri, km, opts) {
334
250
  var pb = pbes2.parsePbkdf2Params(kdf.parameters, opts, _err, "cms");
335
251
  var kea = ri.keyEncryptionAlgorithm;
336
252
  if (kea.oid !== O("id-alg-PWRI-KEK")) throw _err("cms/unsupported-algorithm", "unsupported pwri key-encryption " + kea.oid);
337
- var inner = asn1.decode(kea.parameters); // inner AES-CBC AlgorithmIdentifier
253
+ var inner = asn1.decode(kea.parameters);
338
254
  var innerOid = asn1.read.oid(inner.children[0]);
339
255
  var innerBits = CONTENT_KEYBITS[innerOid];
340
- // The mode comes from the OID-keyed table, not from the OID's display NAME: pki.oid.register() can
341
- // override a built-in name, and a name-matched mode check would then admit a non-CBC inner cipher.
342
256
  if (!innerBits || CONTENT_MODE[innerOid] !== "cbc") throw _err("cms/unsupported-algorithm", "unsupported pwri inner cipher");
343
257
  var iv = asn1.read.octetString(inner.children[1]);
344
- // The derived KEK is ours and is cleared once the unwrap has consumed it, on the failing path
345
- // too, which is the one an attacker induces by tampering with the wrapped key. The caller's
346
- // password is not touched: passwordBytes returns a caller-supplied Buffer as-is.
347
- // A string / Uint8Array password is encoded into a buffer this toolkit allocated, a credential
348
- // copy, so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
349
- // borrowed and left intact.
350
258
  var pw = pbes2.passwordBytesOwned(km.password, _err, "cms");
351
259
  var kek;
352
260
  try { kek = nodeCrypto.pbkdf2Sync(pw.bytes, pb.salt, pb.iterations, innerBits / 8, pb.prfNode); }
@@ -358,34 +266,23 @@ async function _pwriCek(ri, km, opts) {
358
266
  }
359
267
  }
360
268
 
361
- // kemri (ML-KEM ori): decapsulate -> ss, HKDF(CMSORIforKEMOtherInfo) -> KEK, AES-KW unwrap.
362
269
  async function _kemriCek(ri, km) {
363
270
  var k = ri.kemri;
364
271
  var wcName = _mlkemName(k.kem.oid);
365
272
  if (!wcName) throw _err("cms/unsupported-algorithm", "unsupported KEM " + k.kem.oid);
366
- // Honor the declared key-derivation function: we implement HKDF-SHA256 (RFC 9629), so a message
367
- // naming any other KDF is rejected, never silently derived with the wrong one.
368
273
  if (k.kdf.oid !== O("hkdfWithSha256")) throw _err("cms/unsupported-algorithm", "unsupported KEM key-derivation " + k.kdf.oid);
369
274
  var wantCt = KEM_CT_LENGTHS[k.kem.oid];
370
275
  var kemct = k.kemct;
371
- // Coverage residual (both _fail re-checks): unreachable via a parsed message, because the strict parser
372
- // already rejects a wrong kemct length (cms/bad-kem-ciphertext) and a kekLength != wrap size
373
- // (cms/kek-length-mismatch). These are defense-in-depth on the consumer path (M32 / M29).
374
- if (wantCt && kemct.length !== wantCt) throw _fail(); // M32: exact ct length before decap
276
+ if (wantCt && kemct.length !== wantCt) throw _fail();
375
277
  var kekBytes = Number(k.kekLength);
376
278
  var wrapAlg = k.wrap;
377
- if (WRAP_KEK_LENGTHS[wrapAlg.oid] !== kekBytes) throw _fail(); // M29 re-check on the consumer path
378
- // The decapsulation key copy is released as soon as the engine has imported it: the import is
379
- // the only thing that reads it, so its lifetime does not need to span the decapsulation below.
279
+ if (WRAP_KEK_LENGTHS[wrapAlg.oid] !== kekBytes) throw _fail();
380
280
  var keyCopy = _normKeyDer(km.key);
381
281
  var priv;
382
282
  try { priv = await subtle.importKey("pkcs8", keyCopy.der, { name: wcName }, false, ["decapsulateBits"]); }
383
283
  finally { _releaseKeyDer(keyCopy); }
384
284
  var ss = null, kek = null, ssAb = null, kekAb = null;
385
285
  try {
386
- // The engine hands back an ArrayBuffer it allocated, and the Buffer below is a copy of it. Both
387
- // hold the secret, so both are wiped: clearing only the copy this function made would leave the
388
- // engine's return value readable, which is the same omission one level up.
389
286
  ssAb = await subtle.decapsulateBits({ name: wcName }, priv, kemct);
390
287
  ss = Buffer.from(ssAb);
391
288
  var ssKey = await subtle.importKey("raw", ss, { name: "HKDF" }, false, ["deriveBits"]);
@@ -393,32 +290,11 @@ async function _kemriCek(ri, km) {
393
290
  kek = Buffer.from(kekAb);
394
291
  return await _aesKwUnwrap(kek, k.encryptedKey);
395
292
  } finally {
396
- // NIST SP 800-227 RS5 / sec. 4.2 and RFC 9629 sec. 7: the shared secret and the KEK it derives
397
- // are destroyed as soon as they stop being needed. In a `finally`, so an unwrap that fails
398
- // (the common case under a wrong key or a tampered encryptedKey) leaves nothing behind either;
399
- // a wipe on the success path alone would keep the secret exactly when an attacker made it fail.
400
- // Only buffers allocated here are wiped: km.key, the message bytes and the returned CEK are the
401
- // caller's or the caller's to consume.
402
- // A Uint8Array view aliases the ArrayBuffer's own bytes, so wiping the view wipes the buffer
403
- // the engine returned -- no further copy is made in order to destroy one.
404
293
  guard.secret.zeroizeAll([ss, kek, ssAb ? new Uint8Array(ssAb) : null, kekAb ? new Uint8Array(kekAb) : null],
405
294
  CmsError, "cms/bad-input", "the KEM shared secret");
406
295
  }
407
296
  }
408
297
 
409
- // The declared cipher MODE must match the container's authentication model: an AEAD cipher belongs in
410
- // AuthEnvelopedData and a plain CBC cipher in EnvelopedData (RFC 5083 sec. 2). Key length alone does
411
- // not separate them, so without this an EnvelopedData naming AES-GCM was opened as unauthenticated CBC
412
- // while the result still reported the AEAD algorithm, telling the caller the content was authenticated
413
- // when nothing had authenticated it. The mirror case dereferenced AEAD parameters the parser leaves
414
- // null for a non-AEAD algorithm, faulting instead of failing closed.
415
- //
416
- // The line drawn here is AEAD-vs-not, which is the line RFC 5083 draws. GCM-vs-CBC is a narrower
417
- // line this toolkit draws, because AES-GCM is the only AEAD it implements, and stating the narrower
418
- // one as though the RFC required it sends an operator to argue with a specification that says the
419
- // opposite: RFC 5084 sec. 3.1 defines AES-CCM for this content type. So an AEAD reaching the right
420
- // container passes here even when decryption will later decline it, and it is _openContent that says
421
- // why, by name. An unresolvable OID is left to _openContent too, as the unsupported algorithm it is.
422
298
  function _assertContentCipherMode(eci, ct) {
423
299
  var oidStr = eci.contentEncryptionAlgorithm.oid;
424
300
  var isAead = intrinsic.hasOwn(AEAD_ALGS, oidStr);
@@ -431,51 +307,26 @@ function _assertContentCipherMode(eci, ct) {
431
307
  throw _err("cms/unsupported-algorithm", "contentEncryptionAlgorithm " + oidStr + " is an authenticated encryption algorithm, which belongs in an authEnvelopedData rather than a " + ct + " (RFC 5083 sec. 2)");
432
308
  }
433
309
 
434
- // ---- stage 3: open the content (uniform failure) ---------------------------
435
310
  async function _openContent(parsed, eci, cek, ct, streamMode) {
436
311
  var alg = eci.contentEncryptionAlgorithm;
437
312
  var keyBits = intrinsic.hasOwn(CONTENT_KEYBITS, alg.oid) ? CONTENT_KEYBITS[alg.oid] : 0;
438
313
  if (!keyBits) {
439
- // AES-CCM parses completely here -- registered OIDs, the sec. 3.1 nonce bound, the seven legal
440
- // ICV lengths -- and is then declined, because AES-GCM is the only AEAD this toolkit implements
441
- // for content encryption. Saying only "unsupported <oid>" is what an operator also sees for an
442
- // OID nothing in the toolkit recognizes, and the two call for different things: one is a
443
- // capability gap to route around, the other is a malformed or exotic message.
444
314
  if (intrinsic.hasOwn(AEAD_ALGS, alg.oid) && AEAD_ALGS[alg.oid] === "ccm") {
445
315
  throw _err("cms/unsupported-algorithm", "contentEncryptionAlgorithm " + alg.oid + " is AES-CCM. RFC 5084 sec. 3.1 permits it for this content type; this toolkit implements AES-GCM only for content encryption");
446
316
  }
447
317
  throw _err("cms/unsupported-algorithm", "unsupported contentEncryptionAlgorithm " + alg.oid);
448
318
  }
449
319
  if (eci.encryptedContent == null) throw _err("cms/no-encrypted-content", "the message has no encryptedContent (detached; supply it out of band)");
450
- // A null CEK (v1.5 implicit rejection) or a wrong-length CEK -> a fresh random key of the right
451
- // length, so the failure surfaces here as the uniform verdict, never earlier.
452
- //
453
- // The substitute is this function's own allocation, so this function clears it: reassigning the
454
- // parameter does not change the caller's variable, so the caller's `finally` would wipe the
455
- // original null / wrong-length value and leave the substitute behind. It is held separately
456
- // and `cek` is left alone here, because on the normal path `cek` is the caller's recovered key
457
- // and is still in use after this returns.
458
320
  var substitute = null;
459
321
  if (cek == null || cek.length !== keyBits / 8) { substitute = nodeCrypto.randomBytes(keyBits / 8); cek = substitute; }
460
322
  try {
461
323
  if (ct === "authEnvelopedData") {
462
324
  var aad = parsed.authAttrsBytes != null ? _explicitSetOf(parsed.authAttrsBytes) : Buffer.alloc(0);
463
- // GCM authenticates before it yields: the tag is verified here, so a forged message throws
464
- // cms/decrypt-failed now and the streamed form never yields a chunk of unverified plaintext
465
- // (RFC 5083 sec. 1). The stream is therefore a chunker over the already-verified plaintext -- no
466
- // memory win for GCM, which is the correct trade for the integrity guarantee.
467
325
  var pt = _gcmOpen(cek, parsed.aead.nonce, eci.encryptedContent, parsed.mac, aad, keyBits, parsed.aead.icvLen);
468
326
  return streamMode ? _bufferChunks(pt) : pt;
469
327
  }
470
328
  var iv = asn1.read.octetString(asn1.decode(alg.parameters));
471
329
  if (iv.length !== 16) throw _fail();
472
- // A lazy CBC stream defers the padding check to iteration. The ambiguous-candidate loop cannot use a
473
- // deferred check to reject a wrong recipient and try the next, so lazy streaming is used ONLY when a
474
- // single candidate remains (a wrong key then has nothing to fall back to, so its uniform failure at
475
- // iteration is the right verdict). "lazy" is the real memory win: the whole plaintext is never held,
476
- // and the caller opted into receiving chunks before the final() padding check. With several ambiguous
477
- // candidates the decrypt runs eagerly HERE -- it IS the disambiguator -- and the validated plaintext
478
- // is then chunked, so the fallback the buffered path relies on is preserved.
479
330
  if (streamMode === "lazy") return _cbcStream(cek, iv, eci.encryptedContent, keyBits);
480
331
  if (streamMode === "eager") return _bufferChunks(pbes2.cbcDecrypt(cek, iv, eci.encryptedContent, keyBits, CmsError, "cms/decrypt-failed"));
481
332
  return pbes2.cbcDecrypt(cek, iv, eci.encryptedContent, keyBits, CmsError, "cms/decrypt-failed");
@@ -487,22 +338,13 @@ async function _openContent(parsed, eci, cek, ct, streamMode) {
487
338
  }
488
339
  }
489
340
  function _gcmOpen(cek, nonce, ct, tag, aad, keyBits, icvLen) {
490
- // Coverage residual: unreachable via a parsed message, since the strict parser rejects any
491
- // AuthEnvelopedData whose mac length != aes-ICVlen (cms/mac-length-mismatch) before we get here.
492
341
  if (!tag || tag.length !== icvLen) throw _fail();
493
342
  var d = nodeCrypto.createDecipheriv("aes-" + keyBits + "-gcm", cek, nonce, { authTagLength: icvLen });
494
343
  d.setAuthTag(tag);
495
344
  if (aad && aad.length) d.setAAD(aad);
496
- // final() throws on a tag mismatch, and update() has already produced the whole plaintext by
497
- // then. RFC 5083 sec. 1 requires a receiver whose integrity check fails to DESTROY the plaintext,
498
- // not merely withhold it, so the intermediate is cleared on both exits.
499
345
  return guard.secret.cipherFinish(d, ct, CmsError, "cms/decrypt-failed", "the recovered content");
500
346
  }
501
347
 
502
- // ---- streaming content (opts.stream): `content` becomes an async iterable of plaintext chunks -------
503
- // The already-verified GCM plaintext, yielded a chunk at a time so the streamed and buffered forms have
504
- // the same `content` shape. GCM holds the whole plaintext (it was verified before this ran), so this is
505
- // a chunker, not a memory win -- the win is CBC's, below.
506
348
  async function* _bufferChunks(buf) {
507
349
  var chunk = C.BYTES.kib(64);
508
350
  for (var off = 0; off < buf.length; off += chunk) {
@@ -510,11 +352,6 @@ async function* _bufferChunks(buf) {
510
352
  yield intrinsic.subarray(buf, off, end < buf.length ? end : buf.length);
511
353
  }
512
354
  }
513
- // The CBC streaming content. The engine decipher is created EAGERLY (subtle.decryptStream copies the key
514
- // at creation), so the caller's post-open `cek` zeroize does not disturb it; the drive is lazy. A wrong
515
- // key or bad padding surfaces at the engine's final() as an untyped throw, which this collapses to the
516
- // uniform cms/decrypt-failed -- the same verdict the buffered path returns, so the stream never leaks a
517
- // distinct failure reason. `await`, not `.then`: the drive dispatches through no caller-replaceable method.
518
355
  function _cbcStream(key, iv, ct, keyBits) {
519
356
  return _translateCbcStream(subtle.decryptStream({ algNode: "aes-" + keyBits + "-cbc", key: key, iv: iv }, ct));
520
357
  }
@@ -523,7 +360,6 @@ async function* _translateCbcStream(engineIter) {
523
360
  catch (_e) { throw _fail(); }
524
361
  }
525
362
 
526
- // ---- EncryptedData (sec. 8) ------------------------------------------------
527
363
  async function _decryptEncryptedData(parsed, km, opts) {
528
364
  var eci = parsed.encryptedContentInfo;
529
365
  var alg = eci.contentEncryptionAlgorithm;
@@ -540,9 +376,6 @@ async function _decryptEncryptedData(parsed, km, opts) {
540
376
  }
541
377
  async function _decryptPbes2(parsed, eci, km, opts) {
542
378
  if (km.password == null) throw _err("cms/bad-input", "this EncryptedData needs a { password }");
543
- // The PBES2 parameters are attacker-controlled structure the strict parser surfaces raw (PBES2 is
544
- // not an AEAD it validates): parse them behind a structural guard so a malformed shape is a typed
545
- // cms/bad-input, never a raw dereference fault (the PBES2 structure is public, not a decrypt oracle).
546
379
  var kdf, encOid, iv, pb;
547
380
  try {
548
381
  var params = pbes2.seqChildren(eci.contentEncryptionAlgorithm.parameters, 2, "PBES2 parameters", _err, "cms");
@@ -558,11 +391,6 @@ async function _decryptPbes2(parsed, eci, km, opts) {
558
391
  }
559
392
  var keyBits = CONTENT_KEYBITS[encOid];
560
393
  if (!keyBits) throw _err("cms/unsupported-algorithm", "unsupported PBES2 content cipher " + encOid);
561
- // The password-derived content key is ours; the caller's password buffer is not (passwordBytes
562
- // passes a supplied Buffer straight through) and is left intact.
563
- // A string / Uint8Array password is encoded into a buffer this toolkit allocated, a credential
564
- // copy, so it is cleared once the derivation has consumed it. A caller-supplied Buffer is
565
- // borrowed and left intact.
566
394
  var pwE = pbes2.passwordBytesOwned(km.password, _err, "cms");
567
395
  var key;
568
396
  try { key = nodeCrypto.pbkdf2Sync(pwE.bytes, pb.salt, pb.iterations, keyBits / 8, pb.prfNode); }
@@ -572,23 +400,11 @@ async function _decryptPbes2(parsed, eci, km, opts) {
572
400
  finally { guard.secret.zeroize(key, CmsError, "cms/bad-input", "the password-derived content-encryption key"); }
573
401
  }
574
402
 
575
- // ---- shared helpers (mirror cms-encrypt's builders) ------------------------
576
403
  async function _aesKwUnwrap(kek, wrapped) {
577
404
  var kekKey = await subtle.importKey("raw", kek, { name: "AES-KW" }, false, ["unwrapKey"]);
578
- // Import the unwrapped key as HMAC, not AES-CBC: this function exports to raw bytes, so the CryptoKey
579
- // type is transient and the exported bytes are identical for an AES CEK, while HMAC accepts any wrap
580
- // length, so an AuthenticatedData MAC key that is not 16/24/32 octets (e.g. a 64-octet HMAC-SHA-512
581
- // key from another implementation) is recovered instead of rejected before the MAC is even checked.
582
405
  var raw = await subtle.unwrapKey("raw", wrapped, kekKey, { name: "AES-KW" }, { name: "HMAC", hash: "SHA-256" }, true, ["sign"]);
583
- // exportKey allocates an ArrayBuffer holding the unwrapped key; the Buffer below is a copy of it,
584
- // and the original would otherwise stay readable with nothing referencing it. That is a
585
- // controllable allocation, not one of the runtime-internal copies the best-effort caveat covers.
586
- // Only the intermediate is wiped: the returned Buffer is the key the caller must use to open
587
- // the content, so wiping it here would destroy the result instead of protecting it.
588
406
  var rawAb = await subtle.exportKey("raw", raw);
589
407
  try {
590
- // An explicit copy, not Buffer.from(arrayBuffer): that form returns a view sharing the
591
- // ArrayBuffer's memory, so wiping the intermediate below would zero the key being returned.
592
408
  var view = new Uint8Array(rawAb);
593
409
  var out = Buffer.alloc(view.length);
594
410
  out.set(view);
@@ -609,20 +425,10 @@ function _kemOtherInfo(wrapName, kekBytes, ukm) {
609
425
  if (ukm) kids.push(b.explicit(0, b.octetString(ukm)));
610
426
  return b.sequence(kids);
611
427
  }
612
- // RFC 3211 double-CBC unwrap (M26): decrypt pass 2 (IV = last block of pass 1), then pass 1
613
- // (IV = first block), validate the count + complement check bytes, with uniform failure on mismatch.
614
428
  function _pwriUnwrap(kek, wrapped, iv, keyBits) {
615
429
  var blk = 16, alg = "aes-" + keyBits + "-cbc";
616
430
  if (wrapped.length < 2 * blk || wrapped.length % blk !== 0) throw _fail();
617
431
  var n = wrapped.length;
618
- // pass2 = CBC(kek, iv2 = pass1[last block], pass1). Recover pass1's last block by decrypting the
619
- // last ciphertext block (ECB) and XORing the previous ciphertext block, a standard CBC last-
620
- // block decrypt that needs no IV, then CBC-decrypt pass2 under that recovered iv2 to get pass1.
621
- // Every intermediate below is derived from the KEK and reveals key material, so all three are
622
- // named before the try and cleared in one finally. pass1 and body hold the wrapped key in the
623
- // clear; lastDec is pass1's final block, recovered ahead of it. The two validation rejects are
624
- // inside the same scope because they are the paths an attacker induces by tampering with the
625
- // wrapped key, and they are where an unswept intermediate would otherwise be abandoned.
626
432
  var lastDec = null, pass1 = null, body = null;
627
433
  try {
628
434
  var ecb = nodeCrypto.createDecipheriv("aes-" + keyBits + "-ecb", kek, Buffer.alloc(0)); ecb.setAutoPadding(false);
@@ -637,34 +443,27 @@ function _pwriUnwrap(kek, wrapped, iv, keyBits) {
637
443
  if (count < 1 || count + 4 > body.length) throw _fail();
638
444
  var cek = body.subarray(4, 4 + count);
639
445
  var bad = 0;
640
- for (var j = 0; j < 3; j++) bad |= (body[1 + j] ^ 0xff) ^ cek[j]; // complement check bytes
446
+ for (var j = 0; j < 3; j++) bad |= (body[1 + j] ^ 0xff) ^ cek[j];
641
447
  if (bad !== 0) throw _fail();
642
- return Buffer.from(cek); // the caller's copy, independent of the block cleared below
448
+ return Buffer.from(cek);
643
449
  } finally {
644
450
  guard.secret.zeroizeAll([body, pass1, lastDec], CmsError, "cms/bad-input", "the PWRI plaintext block");
645
451
  }
646
452
  }
647
- // Coverage residual (the unsupported-algorithm throw arm of each lookup below: _hashW3c,
648
- // _x963Hash, and _originatorSpki that follow): reachable only from a fully well-formed recipient that
649
- // names an inner algorithm we do not implement (a non-registry OAEP hash / kari scheme / originator
650
- // form). The producer never emits one; the fuzz harness (fuzz/cms-decrypt.fuzz.js) drives these arms
651
- // behaviorally under the PkiError-only contract.
652
453
 
653
454
  function _oaepHashFromParams(paramsBytes) {
654
- if (paramsBytes == null) return "SHA-1"; // absent = the RFC 4055 defaults (accept floor)
455
+ if (paramsBytes == null) return "SHA-1";
655
456
  var node = asn1.decode(paramsBytes);
656
457
  var hashName = "SHA-1", mgfHash = null, label = null;
657
458
  (node.children || []).forEach(function (ch) {
658
459
  if (ch.tagClass !== "context") return;
659
- if (ch.tagNumber === 0) { hashName = _hashW3c(asn1.read.oid(ch.children[0].children[0])); } // hashAlgorithm [0]
660
- else if (ch.tagNumber === 1) { // maskGenAlgorithm [1]
460
+ if (ch.tagNumber === 0) { hashName = _hashW3c(asn1.read.oid(ch.children[0].children[0])); }
461
+ else if (ch.tagNumber === 1) {
661
462
  var mg = ch.children[0];
662
463
  if (asn1.read.oid(mg.children[0]) !== O("mgf1")) throw _err("cms/unsupported-algorithm", "unsupported OAEP mask generation function");
663
464
  mgfHash = _hashW3c(asn1.read.oid(mg.children[1].children[0]));
664
- } else if (ch.tagNumber === 2) { label = asn1.read.octetString(ch.children[0].children[1]); } // pSourceAlgorithm [2]
465
+ } else if (ch.tagNumber === 2) { label = asn1.read.octetString(ch.children[0].children[1]); }
665
466
  });
666
- // WebCrypto RSA-OAEP ties the MGF1 hash to the OAEP hash and supports only an empty label, so any
667
- // parameter set we cannot faithfully honor is rejected, never silently ignored.
668
467
  if (mgfHash != null && mgfHash !== hashName) throw _err("cms/unsupported-algorithm", "the OAEP MGF1 hash must equal the OAEP hash");
669
468
  if (label != null && label.length > 0) throw _err("cms/unsupported-algorithm", "a non-empty OAEP label is not supported");
670
469
  return hashName;
@@ -672,7 +471,7 @@ function _oaepHashFromParams(paramsBytes) {
672
471
  var HASH_W3C = {}; HASH_W3C[O("sha1")] = "SHA-1"; HASH_W3C[O("sha256")] = "SHA-256"; HASH_W3C[O("sha384")] = "SHA-384"; HASH_W3C[O("sha512")] = "SHA-512";
673
472
  function _hashW3c(o) { if (!HASH_W3C[o]) throw _err("cms/unsupported-algorithm", "unsupported OAEP hash " + o); return HASH_W3C[o]; }
674
473
 
675
- var X963_HASH = {}; // kari stdDH scheme OID -> W3C hash
474
+ var X963_HASH = {};
676
475
  [["dhSinglePass-stdDH-sha1kdf-scheme", "SHA-1"], ["dhSinglePass-stdDH-sha224kdf-scheme", "SHA-224"], ["dhSinglePass-stdDH-sha256kdf-scheme", "SHA-256"], ["dhSinglePass-stdDH-sha384kdf-scheme", "SHA-384"], ["dhSinglePass-stdDH-sha512kdf-scheme", "SHA-512"],
677
476
  ["dhSinglePass-cofactorDH-sha1kdf-scheme", "SHA-1"], ["dhSinglePass-cofactorDH-sha224kdf-scheme", "SHA-224"], ["dhSinglePass-cofactorDH-sha256kdf-scheme", "SHA-256"], ["dhSinglePass-cofactorDH-sha384kdf-scheme", "SHA-384"], ["dhSinglePass-cofactorDH-sha512kdf-scheme", "SHA-512"]
678
477
  ].forEach(function (r) { X963_HASH[O(r[0])] = r[1]; });
@@ -683,9 +482,6 @@ function _kariWrap(kea) {
683
482
  return { oid: wrapOid, name: oid.name(wrapOid) };
684
483
  }
685
484
  function _originatorSpki(originator) {
686
- // originator OriginatorIdentifierOrKey CHOICE; we require originatorKey [1] (RFC 5753 MUST for
687
- // ephemeral-static). The parser surfaces { form, value:{ algorithm, publicKey } }, so reconstruct
688
- // the SPKI (SEQUENCE { algorithm, subjectPublicKey BIT STRING }) for importKey.
689
485
  if (!originator || originator.form !== "originatorKey") throw _err("cms/unsupported-algorithm", "kari requires an originatorKey (ephemeral-static ECDH)");
690
486
  var v = originator.value;
691
487
  var algKids = [b.oid(v.algorithm.oid)];
@@ -696,13 +492,11 @@ var MONT = {}; MONT[O("X25519")] = { name: "X25519", hkdf: "SHA-256" }; MONT[O("
696
492
  function _isMont(spki) { var o = asn1.read.oid(asn1.decode(spki).children[0].children[0]); return !!MONT[o]; }
697
493
  function _montName(spki) { return MONT[asn1.read.oid(asn1.decode(spki).children[0].children[0])]; }
698
494
  var CURVE = {}; CURVE[O("prime256v1")] = { curve: "P-256" }; CURVE[O("secp384r1")] = { curve: "P-384" }; CURVE[O("secp521r1")] = { curve: "P-521" };
699
- // The recipient certificate's EC curve OID -- authoritative for the kari agreement (both keys share it).
700
495
  function _ecCurveFromCert(cert) {
701
496
  var spki = x509.parse(_normCertDer(cert)).subjectPublicKeyInfo;
702
497
  if (spki.algorithm.oid !== O("ecPublicKey") || spki.algorithm.parameters == null) return null;
703
498
  return asn1.read.oid(asn1.decode(spki.algorithm.parameters));
704
499
  }
705
- // Rebuild an EC SPKI with explicit namedCurve parameters (for an originator key that omitted them).
706
500
  function _withEcCurveParams(spki, curveOid) {
707
501
  var node = asn1.decode(spki);
708
502
  var alg = node.children[0];
@@ -712,30 +506,16 @@ var MLKEM = {}; MLKEM[O("id-ml-kem-512")] = "ML-KEM-512"; MLKEM[O("id-ml-kem-768
712
506
  function _mlkemName(o) { return MLKEM[o]; }
713
507
 
714
508
  function _explicitSetOf(implicitBytes) {
715
- // The AAD is the authAttrs re-encoded under the EXPLICIT SET OF tag (RFC 5083 sec. 2.2). The
716
- // parser surfaces authAttrsBytes as the transmitted [1] IMPLICIT form; retag 0xA1 -> 0x31.
717
- // AuthenticatedData authAttrs are transmitted [2] IMPLICIT (0xA2); the same out[0]=0x31 handles it.
718
509
  var out = Buffer.from(implicitBytes);
719
510
  out[0] = 0x31;
720
511
  return out;
721
512
  }
722
513
 
723
- // ---- AuthenticatedData verification (RFC 5652 sec. 9.3) ---------------------
724
- // The macAlgorithm OID name -> WebCrypto HMAC hash, and the digestAlgorithm name -> hash (data-driven).
725
514
  var MAC_HASH = { hmacWithSHA256: "SHA-256", hmacWithSHA384: "SHA-384", hmacWithSHA512: "SHA-512" };
726
515
  var DIGEST_WC = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
727
- // The minimum conveyed MAC-key length (128-bit security floor). A ktri/pwri recipient can wrap a
728
- // key of any length (unlike AES-KW's 16-octet minimum), so a hostile message could convey a tiny or
729
- // empty MAC key that weakens the HMAC (or makes subtle.importKey throw); reject it fail-closed.
730
516
  var MAC_KEY_MIN_OCTETS = 16;
731
517
  function _isDerNull(p) { return Buffer.isBuffer(p) && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
732
518
 
733
- // Verify a CMS AuthenticatedData (RFC 5652 sec. 9): recover the MAC key through the same RecipientInfo
734
- // unwrap decrypt uses, recompute the HMAC over the sec. 9.2 preimage (the re-tagged [2] authAttrs SET
735
- // OF when present, else the eContent value octets), constant-time-compare to the transmitted mac, and,
736
- // when authAttrs are present, independently confirm message-digest == digest(content) (sec. 9.3
737
- // do-not-trust-originator). The content is returned only after every check passes. Every
738
- // secret-dependent failure collapses to the uniform cms/decrypt-failed (no unwrap-success oracle).
739
519
  async function _verifyAuthenticatedData(parsed, km, opts) {
740
520
  var macAlg = parsed.macAlgorithm;
741
521
  var hash = MAC_HASH[macAlg.name];
@@ -744,15 +524,11 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
744
524
  var content = parsed.encapContentInfo.eContent;
745
525
  if (content == null) throw _err("cms/bad-input", "a detached AuthenticatedData (absent eContent) is not supported");
746
526
 
747
- // Determine the sec. 9.2 preimage + (present mode) the message-digest claim, both structural.
748
527
  var preimage, mdCheck = null;
749
528
  if (parsed.authAttrsBytes) {
750
529
  var dHash = DIGEST_WC[parsed.digestAlgorithm && parsed.digestAlgorithm.name];
751
530
  if (!dHash) throw _err("cms/unsupported-algorithm", "unsupported digestAlgorithm " + JSON.stringify(parsed.digestAlgorithm && parsed.digestAlgorithm.name));
752
531
  preimage = _explicitSetOf(parsed.authAttrsBytes);
753
- // The parser already enforced content-type == eContentType (cms/content-type-mismatch) and the
754
- // single-valued message-digest attribute; read the claimed digest from the parsed authAttrs (which
755
- // decrypt re-parses from the same bytes the MAC covers).
756
532
  var mdAttr = (parsed.authAttrs || []).filter(function (a) { return a.type === O("messageDigest"); })[0];
757
533
  mdCheck = { hash: dHash, declared: asn1.read.octetString(asn1.decode(mdAttr.values[0])) };
758
534
  } else {
@@ -763,29 +539,16 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
763
539
  for (var ci = 0; ci < candidates.length; ci++) {
764
540
  try {
765
541
  _assertSupported(candidates[ci].ri, km);
766
- var macKey = await _acquireCek(candidates[ci].ri, km, opts); // recover the MAC key (uniform failure)
767
- // RFC 3218 implicit rejection: _ktriCek returns null on an RSA v1.5 unwrap fault, and a hostile
768
- // ktri/pwri could convey a below-floor (128-bit) key; in either case substitute a fresh random
769
- // key so the MAC verify still runs and fails uniformly, never a fast-path that distinguishes an
770
- // invalid/short unwrap from a MAC mismatch (Bleichenbacher / weak-key oracle freedom).
771
- // The substitute is held separately, never overwriting macKey: a hostile recipient can
772
- // unwrap to a non-null key that is merely too short, and assigning over it would drop the only
773
- // reference to that recovered secret, leaving it readable while the cleanup cleared the random
774
- // replacement instead. Both are this module's allocations and both are cleared below.
542
+ var macKey = await _acquireCek(candidates[ci].ri, km, opts);
775
543
  var macSubstitute = null;
776
544
  if (macKey == null || macKey.length < MAC_KEY_MIN_OCTETS) macSubstitute = nodeCrypto.randomBytes(MAC_KEY_MIN_OCTETS);
777
- // The MAC key is this path's equivalent of the content-encryption key: whether recovered from
778
- // the recipient entry or substituted, it is cleared once the verify and digest checks are done
779
- // -- on the failing path too, and the returned object holds a copy of the content, never the key.
780
545
  try {
781
546
  var key = await subtle.importKey("raw", macSubstitute || macKey, { name: "HMAC", hash: hash }, false, ["verify"]);
782
547
  if (!(await subtle.verify({ name: "HMAC" }, key, Buffer.from(parsed.mac), preimage))) throw _fail();
783
548
  if (mdCheck) {
784
549
  var actual = Buffer.from(await subtle.digest(mdCheck.hash, content));
785
- if (!actual.equals(mdCheck.declared)) throw _fail(); // sec. 9.3: recompute, never trust the originator's digest
550
+ if (!actual.equals(mdCheck.declared)) throw _fail();
786
551
  }
787
- // Like GCM, the MAC is verified above before anything is yielded, so the stream is a chunker
788
- // over the already-authenticated plaintext (uniform `content` shape, no memory win).
789
552
  var authedContent = Buffer.from(content);
790
553
  return _originFields({
791
554
  content: opts.stream === true ? _bufferChunks(authedContent) : authedContent,
@@ -799,38 +562,11 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
799
562
  guard.secret.zeroizeAll([macKey, macSubstitute], CmsError, "cms/bad-input", "the message-authentication key");
800
563
  }
801
564
  } catch (e) {
802
- if (candidates.length === 1) throw e; // one candidate: preserve its exact (distinct or uniform) verdict
565
+ if (candidates.length === 1) throw e;
803
566
  }
804
567
  }
805
- throw _fail(); // every ambiguous candidate failed -> uniform
806
- }
807
- // Coverage residual in _verifyAuthenticatedData -- defensive branches reachable only via a hand-built
808
- // malformed message (the producer never emits them) or unreachable behind an upstream guarantee:
809
- // * the macAlgorithm-parameters-not-NULL reject and the unsupported-digestAlgorithm reject fire only
810
- // on a hand-crafted message (this producer emits absent params + SHA-2); the behavioral guard is
811
- // the frozen cms/unsupported-algorithm code, not a round-trip.
812
- // * the detached (absent eContent) reject is the deferred-with-condition path (this producer always
813
- // attaches the content); re-open when a detached AuthenticatedData caller appears.
814
- // * `parsed.authAttrs || []` / `parsed.recipientInfos || []`: the parser always surfaces these as
815
- // arrays (or rejects), so the `|| []` fallbacks never fire.
816
- // * `oid.name(eContentType) || eContentType` fires only for an unregistered content type; `macAlg.name
817
- // || macAlg.oid` and `digestAlgorithm.name || .oid` never reach the `.oid` fallback because the
818
- // algorithm name was already validated against MAC_HASH / DIGEST_WC above.
819
- // * the `macKey == null` half of the random-key substitution fires only for a hand-crafted RSA v1.5
820
- // ktri (this producer emits OAEP); its behavior is identical to the tested below-floor path (a
821
- // random key -> the MAC verify fails uniformly), so the < 16 vector covers the substitution.
822
- // _normKeyDer(key) -> { der, owned }: the recipient private key as PKCS#8 DER, and whether the
823
- // buffer is one this module made.
824
- //
825
- // The distinction decides who may wipe it. A caller handing in their own Buffer keeps a live
826
- // reference and will use it again; wiping that would destroy the key out from under them. The other
827
- // two forms produce a new buffer here (a Uint8Array is copied, a PEM string is decoded), and that
828
- // buffer is a second copy of a private key which nothing else can reach, so it lives until the
829
- // garbage collector happens to reuse the page unless this module clears it.
830
- //
831
- // Returning the flag, in place of always copying, keeps the caller's buffer un-duplicated: making our
832
- // own copy of every key so we could uniformly wipe it would add a copy of the secret to solve the
833
- // problem of having one.
568
+ throw _fail();
569
+ }
834
570
  function _normKeyDer(key) {
835
571
  if (Buffer.isBuffer(key)) return { der: key, owned: false };
836
572
  if (key instanceof Uint8Array) return { der: Buffer.from(key), owned: true };
@@ -842,7 +578,6 @@ function _normKeyDer(key) {
842
578
  }
843
579
  throw _err("cms/bad-input", "the recipient private key must be a PKCS#8 DER Buffer or PEM string");
844
580
  }
845
- // Wipe a key buffer this module owns. A no-op for the caller's own Buffer, which is theirs.
846
581
  function _releaseKeyDer(k) {
847
582
  if (k && k.owned) guard.secret.zeroize(k.der, CmsError, "cms/bad-input", "the recipient private-key copy");
848
583
  }