@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
package/lib/smime.js CHANGED
@@ -48,20 +48,6 @@ var SmimeError = frameworkError.SmimeError;
48
48
 
49
49
  function _err(code, msg, cause) { return new SmimeError(code, msg, cause); }
50
50
 
51
- // ---- the option surface each verb accepts -----------------------------------
52
- //
53
- // A misspelled option is the one input that reads as an omission, never as a value: nothing is
54
- // out of range, nothing fails to parse, and the caller who asked for something stricter silently
55
- // gets the looser default. `protectHeaders` misspelled sends the headers a caller meant to protect
56
- // as ordinary display copies; `strictMicalg` misspelled accepts the mismatch it was set to reject;
57
- // `entity` misspelled wraps a caller's complete MIME entity inside another one.
58
- //
59
- // The tables are per verb, not per module, because the surfaces genuinely differ (`form` means
60
- // something on sign and nothing on encrypt) and a merged table would accept each verb's options at
61
- // every other one, which is the same silence in a wider form. Each is the keys that verb's body and
62
- // the helpers it hands `opts` to actually read: the value goes through _entityBytes, _cmsSignOpts or
63
- // _cmsEncryptOpts as readily as it is read here, so a table built from the verb's own lines alone
64
- // would refuse options that work.
65
51
  var SIGN_OPTS = {
66
52
  form: 1, entity: 1, contentType: 1, signingTime: 1, protectHeaders: 1, headers: 1, hcp: 1,
67
53
  sid: 1, signedAttributes: 1, additionalSignedAttributes: 1,
@@ -85,58 +71,32 @@ function _knownOpts(opts, known, verb) {
85
71
  });
86
72
  }
87
73
 
88
- // RFC 8551 uses application/pkcs7-<kind>; OpenSSL's legacy `smime` command emits the PKCS#7
89
- // application/x-pkcs7-<kind>. Accept both on the RECEIVE side (we always EMIT the RFC 8551 form).
90
74
  function _isPkcs7(type, kind) {
91
75
  var t = (type || "").toLowerCase();
92
76
  return t === "application/pkcs7-" + kind || t === "application/x-pkcs7-" + kind;
93
77
  }
94
78
 
95
- // The RFC 8551 sec. 3.4.3.2 micalg name for a CMS digest (schema-cms surfaces "sha256" etc.).
96
- // RFC 8551 sec. 3.4.3.2 micalg names, extended with the SHAKE names FIPS 204/205 CMS signers digest
97
- // with (RFC 8702). An unknown digest passes through verbatim and is never regex-mangled (a blind
98
- // `sha`->`sha-` would corrupt "shake256" into "sha-ke256").
99
79
  var MICALG = { sha1: "sha-1", sha224: "sha-224", sha256: "sha-256", sha384: "sha-384", sha512: "sha-512", md5: "md5", shake128: "shake128", shake256: "shake256" };
100
80
 
101
- // The exact bytes to sign: the caller's content wrapped as a MIME entity (text/plain by default) in
102
- // its canonical form, or, when opts.entity is set, the caller's own complete MIME entity, canonical.
103
81
  function _entityBytes(content, opts) {
104
82
  var raw = guard.bytes.view(content, SmimeError, "smime/bad-input", "content");
105
83
  if (opts.entity) return mime.canonicalize(raw, SmimeError, "smime/bad-mime");
106
- // Declare the honest Content-Transfer-Encoding: "7bit" requires every byte <= 127 (RFC 2045); a body
107
- // with any 8-bit byte is "8bit". Declaring 7bit for 8-bit content is a false claim a transport could act on.
108
84
  var cte = _is7bit(raw) ? "7bit" : "8bit";
109
- // Build through the guarded serializer so a CRLF / NUL in opts.contentType cannot inject a header field.
110
85
  return mime.buildEntity([{ name: "Content-Type", value: opts.contentType || "text/plain; charset=utf-8" }, { name: "Content-Transfer-Encoding", value: cte }], raw, SmimeError, "smime/bad-header");
111
86
  }
112
87
  function _is7bit(buf) { for (var i = 0; i < buf.length; i++) if (buf[i] > 0x7f) return false; return true; }
113
88
 
114
- // ---- RFC 9788 header protection (an opts.protectHeaders-gated MIME transform over the shipped CMS path) ----
115
- // The protected header fields are inlined on the Cryptographic Payload root (its Content-Type gains an `hp`
116
- // parameter: "clear" for signed-only, "cipher" for encrypted); the outer display headers are copied as-is
117
- // (signed) or passed through a Header Confidentiality Policy (encrypted). The CMS crypto is UNCHANGED.
118
89
 
119
- // Is this (lowercased) header field name Structural? RFC 9787 sec. 1.1.1 defines a Structural Header Field as
120
- // MIME-Version OR any field whose name begins with "Content-" (Content-Type / -Transfer-Encoding /
121
- // -Disposition / -ID / -Description / -Language / -Location / ...). These describe the specific MIME part --
122
- // the library sets them -- so they are never protected as Non-Structural fields (rejected at the producer) nor
123
- // surfaced in the authenticated protectedHeaders set on verify. The prefix rule is the RFC definition itself,
124
- // so it matches every Content-* field (not just an enumerated subset) and is not a prototype-key lookup (a
125
- // field named "constructor" / "toString" is simply not Structural).
126
90
  function _isStructural(lname) {
127
91
  return lname === "mime-version" || lname.indexOf("content-") === 0;
128
92
  }
129
93
 
130
- // Normalize opts.headers (an object { Name: value } or an array [{ name, value }]) to an ordered list. A
131
- // malformed shape is a config-time smime/bad-input (the value's byte integrity is guarded at emission).
132
94
  function _hpHeaderList(headers) {
133
95
  if (headers == null) return [];
134
96
  var out = [];
135
97
  if (Array.isArray(headers)) {
136
98
  headers.forEach(function (h) {
137
99
  if (!h || typeof h !== "object") throw _err("smime/bad-input", "each opts.headers entry must be { name, value }");
138
- // Read name + value once each: an accessor / Proxy could return a different value on a second read, so
139
- // the snapshot (not the live property) is what both the signed inner header and the outer display copy use.
140
100
  var name = h.name, v = h.value;
141
101
  if (typeof name !== "string") throw _err("smime/bad-input", "each opts.headers entry must be { name, value }");
142
102
  out.push({ name: name, value: v == null ? "" : String(v) });
@@ -146,17 +106,10 @@ function _hpHeaderList(headers) {
146
106
  } else {
147
107
  throw _err("smime/bad-input", "opts.headers must be an object or an array of { name, value }");
148
108
  }
149
- // Only Non-Structural fields are protected (RFC 9787): a Structural header (Content-Type, MIME-Version, ...)
150
- // describes the MIME structure -- the library sets it -- so a caller passing one would duplicate it on the
151
- // payload root. And a REPEATED field name is rejected here at the PRODUCER: a repeated protected field is an
152
- // unsupported shape (verify fails closed on the ambiguity), so the library never emits a message it cannot
153
- // re-consume. Reject both; never emit a malformed / self-inconsistent entity.
154
109
  var seen = Object.create(null);
155
110
  out.forEach(function (h) {
156
111
  var ln = h.name.toLowerCase();
157
112
  if (_isStructural(ln)) throw _err("smime/bad-input", "opts.headers must not include the Structural header " + JSON.stringify(h.name) + " (RFC 9787); only Non-Structural fields (Subject / From / To / ...) are protected");
158
- // HP-Outer is a reserved field the library emits itself (RFC 9788 sec. 2.2) to document outer-header
159
- // confidentiality; a caller-supplied one would be mistaken for that machinery, so reject it.
160
113
  if (ln === "hp-outer") throw _err("smime/bad-input", "opts.headers must not include the reserved HP-Outer field (RFC 9788 sec. 2.2); the library emits it");
161
114
  if (seen[ln]) throw _err("smime/bad-input", "opts.headers must not repeat a protected field name (" + JSON.stringify(h.name) + "); a repeated protected field is not supported");
162
115
  seen[ln] = 1;
@@ -164,12 +117,6 @@ function _hpHeaderList(headers) {
164
117
  return out;
165
118
  }
166
119
 
167
- // The Header Confidentiality Policy for an OUTER header of an encrypted-HP message. hcp_baseline (RFC 9788
168
- // sec. 3.2.1, the default) obscures Subject to "[...]" and REMOVES Comments / Keywords (null);
169
- // hcp_no_confidentiality leaves every field visible (opt-in only). This is the sec. 3.2.1 pseudocode
170
- // Verbatim: Bcc is intentionally not stripped. RFC 9788 sec. 11.4 explains that removing Bcc during
171
- // encryption can break deliverability to a Bcc'd recipient, so the choice is left to the caller (omit Bcc
172
- // from opts.headers to keep it out of the plaintext outer headers).
173
120
  function _applyHcp(name, value, hcp) {
174
121
  if (hcp === "hcp_no_confidentiality") return value;
175
122
  var ln = name.toLowerCase();
@@ -178,8 +125,6 @@ function _applyHcp(name, value, hcp) {
178
125
  return value;
179
126
  }
180
127
 
181
- // The outer display header list: for "clear" (signed) the fields are copied verbatim; for "cipher"
182
- // (encrypted) each passes through the HCP, where a null result removes the field (it lives only in the ciphertext).
183
128
  function _outerHeaderList(list, mode, hcp) {
184
129
  var policy = hcp == null ? "hcp_baseline" : hcp;
185
130
  if (policy !== "hcp_baseline" && policy !== "hcp_no_confidentiality") throw _err("smime/bad-input", "unknown opts.hcp policy " + JSON.stringify(hcp) + " (only \"hcp_baseline\" or \"hcp_no_confidentiality\")");
@@ -195,39 +140,19 @@ function _outerHeaderList(list, mode, hcp) {
195
140
  return out;
196
141
  }
197
142
 
198
- // Build the inner Cryptographic Payload for a header-protected message: the caller's body wrapped with its
199
- // Content-Type (gaining ; hp="<mode>") + Content-Transfer-Encoding + the protected fields inlined, canonical.
200
- // hpList is the single normalized snapshot of opts.headers (see the caller), and that same list feeds the signed/
201
- // encrypted inner header set here and the outer display copy, so an accessor / Proxy cannot make them diverge.
202
- // outerList is the HCP-processed outer set (_outerHeaderList) -- for a cipher payload it also drives the
203
- // HP-Outer records embedded here.
204
143
  function _protectedInnerEntity(content, opts, mode, hpList, outerList) {
205
- // Header protection inlines the protected fields on the payload ROOT it builds, so a caller-supplied
206
- // complete entity (opts.entity) is an unsupported combination: reject it, and never silently bury the
207
- // caller's entity (its own Content-Type + headers) beneath a new default text/plain wrapper.
208
144
  if (opts.entity) throw _err("smime/bad-input", "opts.entity is not supported with protectHeaders -- pass raw content + opts.contentType");
209
145
  var raw = guard.bytes.view(content, SmimeError, "smime/bad-input", "content");
210
- // Header protection sets the hp parameter itself; a caller hp in opts.contentType (with OR without a value)
211
- // would emit two hp attributes (Content-Type: ...; hp; hp="clear"), an unconsumable non-interoperable
212
- // message -- reject it here (hasParam catches a bare "hp" that paramCount would miss).
213
146
  if (opts.contentType != null && mime.hasParam(String(opts.contentType), "hp")) throw _err("smime/bad-input", "opts.contentType must not set the hp parameter -- header protection sets it (hp=" + JSON.stringify(mode) + ")");
214
147
  var ct = (opts.contentType || "text/plain; charset=utf-8") + "; hp=\"" + mode + "\"";
215
148
  var fields = [{ name: "Content-Type", value: ct }, { name: "Content-Transfer-Encoding", value: _is7bit(raw) ? "7bit" : "8bit" }];
216
149
  hpList.forEach(function (h) { fields.push(h); });
217
- // RFC 9788 sec. 2.2 / sec. 5.2.1 step 5.v: an ENCRYPTED header-protected payload documents inside the
218
- // ciphertext, as HP-Outer records, every Non-Structural field it deliberately left visible in the outer
219
- // (unprotected) header section, as "HP-Outer: <name>: <outer-value>". A field the HCP removed has no HP-Outer
220
- // record; its absence authenticates that it was made confidential by removal. Signed-only (clear) payloads
221
- // carry no HP-Outer (sec. 2.2: "not relevant for signed-only messages").
222
150
  if (mode === "cipher") {
223
151
  outerList.forEach(function (h) { fields.push({ name: "HP-Outer", value: h.name + ": " + h.value }); });
224
152
  }
225
153
  return mime.buildEntity(fields, raw, SmimeError, "smime/bad-header");
226
154
  }
227
155
 
228
- // The outer header prefix (display copies + MIME-Version) that precedes the Cryptographic Envelope's
229
- // Content-Type in a header-protected message. Each field is guard-validated (no CR/LF/NUL injection). list is
230
- // the same HCP-processed outer set the inner entity's HP-Outer records were built from.
231
156
  function _outerPrefix(list) {
232
157
  var s = "";
233
158
  for (var i = 0; i < list.length; i++) {
@@ -237,21 +162,11 @@ function _outerPrefix(list) {
237
162
  return s + "MIME-Version: 1.0\r\n";
238
163
  }
239
164
 
240
- // Does the payload header region declare an hp Content-Type parameter? A cheap raw scan (unfold the header
241
- // block, then look for a Content-Type line carrying an hp= parameter) so a NON-HP payload is never parsed --
242
- // no swallow -- and only a payload that CLAIMS hp is validated (a malformed such payload then fails closed,
243
- // never a silent downgrade to unprotected).
244
- // Re-decode a header value that mime.parse latin1-decoded (byte-preserving) as UTF-8 (RFC 6532), so a
245
- // non-ASCII protected value round-trips intact -- a latin1 string maps 1:1 to bytes, recovering the emitted UTF-8.
246
165
  function _utf8Header(s) { return Buffer.from(s, "latin1").toString("utf8"); }
247
166
 
248
- // ---- header parsing by character scan (no regex) ----------------------------------------------
249
- // ASCII lowercase of a code point (A-Z -> a-z), for the case-insensitive "content-type" literal match.
250
167
  function _asciiLower(c) { return (c >= 65 && c <= 90) ? c + 32 : c; }
251
168
  var _CONTENT_TYPE = "content-type";
252
169
 
253
- // RFC 5322 header unfolding: a CRLF followed by one-or-more SP/HTAB (a folded continuation) collapses
254
- // to a single space (matching /\r\n[ \t]+/g -> " ").
255
170
  function _unfoldHeaderFolds(s) {
256
171
  var out = "", i = 0, n = s.length;
257
172
  while (i < n) {
@@ -268,9 +183,6 @@ function _unfoldHeaderFolds(s) {
268
183
  return out;
269
184
  }
270
185
 
271
- // If the line beginning at `start` is a Content-Type field -- ^[ \t]*content-type[ \t]*: -- return the
272
- // whole line up to (not including) the next CR or LF (the [^\r\n]* tail); else null. Case-insensitive
273
- // on the literal, matching the /i flag over the ASCII field name.
274
186
  function _contentTypeLineAt(s, start) {
275
187
  var n = s.length, p = start;
276
188
  while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
@@ -280,14 +192,12 @@ function _contentTypeLineAt(s, start) {
280
192
  }
281
193
  p += _CONTENT_TYPE.length;
282
194
  while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
283
- if (p >= n || s.charCodeAt(p) !== 0x3a) return null; // ":"
195
+ if (p >= n || s.charCodeAt(p) !== 0x3a) return null;
284
196
  p += 1;
285
197
  while (p < n && s.charCodeAt(p) !== 0x0d && s.charCodeAt(p) !== 0x0a) p += 1;
286
198
  return s.slice(start, p);
287
199
  }
288
200
 
289
- // Every Content-Type field in the header block (the /gim match, /m making ^ match each line start):
290
- // scan the block start and each position after an LF. Returns the matched lines, or null when none.
291
201
  function _contentTypeLines(s) {
292
202
  var lines = [], lineStart = 0;
293
203
  while (lineStart <= s.length) {
@@ -300,7 +210,6 @@ function _contentTypeLines(s) {
300
210
  return lines.length ? lines : null;
301
211
  }
302
212
 
303
- // The ^[ \t]*content-type[ \t]*: prefix removed from a matched Content-Type line (the value remains).
304
213
  function _contentTypeValue(s) {
305
214
  var n = s.length, p = 0;
306
215
  while (p < n && (s.charCodeAt(p) === 0x20 || s.charCodeAt(p) === 0x09)) p += 1;
@@ -315,105 +224,45 @@ function _contentTypeValue(s) {
315
224
  }
316
225
 
317
226
  function _declaresHp(content) {
318
- // content is the CANONICAL entity bytes -- the caller (_hpSurface) runs mime.canonicalizeText ONCE so
319
- // detection here and the subsequent mime.parse see the identical normalized form (bare CR / LF -> CRLF).
320
- // Detecting hp on the canonical form -- the bytes the signature covers -- is what stops a fold/line-ending
321
- // transport rewrite from stripping the hp signal from a still-valid signed message.
322
227
  var s = content.toString("latin1");
323
228
  var sep = s.indexOf("\r\n\r\n");
324
229
  var head = _unfoldHeaderFolds(sep !== -1 ? s.slice(0, sep) : s);
325
- // Detect hp by composing mime.paramCount, the same comment- + quoted-string-aware tokenizer the parser
326
- // uses -- over each Content-Type field, so the probe never diverges from mime.parse: a leading-whitespace
327
- // or before-colon field name is matched (the parser trims it), while an hp= inside a MIME comment or a
328
- // quoted value is not a parameter (so an ordinary Content-Type cannot opt a message into HP processing). A
329
- // real hp on ANY Content-Type line is detected; a duplicate Content-Type is rejected downstream.
330
230
  var cts = _contentTypeLines(head);
331
231
  if (!cts) return false;
332
232
  for (var i = 0; i < cts.length; i++) {
333
- // hasParam (not paramCount): a payload that names the hp attribute without a value ("...; hp") still
334
- // CLAIMS header protection -- detect it so _hpSurface fails closed on the malformed declaration rather
335
- // than silently downgrading it to unprotected.
336
233
  if (mime.hasParam(_contentTypeValue(cts[i]), "hp")) return true;
337
234
  }
338
235
  return false;
339
236
  }
340
237
 
341
- // A fresh not-protected surface (returned when no header protection is present or detection fails soft). A NEW
342
- // object each call -- the caller Object.assign()s it onto the verify/decrypt result, so it must not be shared.
343
- // `present` + `protectedHeaders` describe only cryptographically-declared (hp=) protection; `legacy` is null
344
- // unless an opt-in legacy RFC8551HP inference succeeded (then it carries its own headers/mode/etc.; see below).
345
- // No header protection means no protected From to compare the outer one against, so the
346
- // mismatch question is unanswered rather than answered "no" -- null, matching every other
347
- // field here that reports an absent property.
348
238
  function _noneSurface() { return { protectedHeaders: null, headerProtection: { present: false, mode: null, fromMismatch: null, confidential: [], legacy: null } }; }
349
239
 
350
- // Count the Content-Type fields in a parsed header list. A structure with more than one is AMBIGUOUS: mime.parse
351
- // surfaces only the first via contentType, so an hp= parameter or a Cryptographic-Layer media type on a later
352
- // field would be invisible to the classification, and both the standard HP path and the legacy path fail closed on it.
353
240
  function _contentTypeCount(headers) { var n = 0; headers.forEach(function (h) { if (h.lname === "content-type") n++; }); return n; }
354
241
 
355
- // Header fields that must not sensibly repeat: the RFC 5322 sec. 3.6 max-1 originator/destination/identification
356
- // fields, plus Return-Path (the envelope sender; RFC 5322 sec. 3.6 groups it under trace, but RFC 5321 sec. 4.4
357
- // restricts a delivered message to a single one). A duplicate of one of these is a malformed / ambiguous set (a
358
- // consumer could pick a different sender / subject); a duplicate of any OTHER field (Received and the other
359
- // trace fields, Resent-*, Comments, Keywords, and optional / X-* fields) is legitimately repeatable in a real
360
- // message. A Set (not a plain object) so an attacker-chosen field name like "constructor" / "__proto__" cannot
361
- // match an inherited Object.prototype member and false-trip the singleton reject.
362
242
  var RFC5322_SINGLETON = new Set(["date", "from", "sender", "reply-to", "to", "cc", "bcc", "message-id", "in-reply-to", "references", "subject", "return-path"]);
363
243
 
364
- // Extract the Non-Structural protected fields from a recovered inner header list. Shared by the standard HP path
365
- // (over the Cryptographic Payload root) and the legacy path (over the message/rfc822 inner message, sec. 4.10).
366
- // Returns { protectedHeaders (UTF-8 map, last-wins, the ergonomic single-value view surfaced by the standard
367
- // path), entries (an ordered array of every { name, value, raw } occurrence, since a real message repeats trace
368
- // fields, and a last-wins collapse would drop all but one), innerFrom, refouter (HP-Outer records), dup (any
369
- // duplicate field name), dupSingleton (a duplicate of an RFC 5322 singleton field) }.
370
244
  function _extractProtected(headerList) {
371
- // mime.parse decodes the header block as latin1 (byte-preserving), so a protected value emitted as UTF-8
372
- // (RFC 6532, the guard permits it) is re-decoded latin1->UTF-8 here to round-trip intact. protectedHeaders
373
- // surfaces the exact authenticated field body (rawValue: leading/trailing whitespace preserved), and not a
374
- // trimmed value that would diverge from the signed octets.
375
245
  var protectedHeaders = Object.create(null), innerFrom = null, seen = Object.create(null), dup = null, dupSingleton = null;
376
- var entries = []; // every Non-Structural { name, value } occurrence, in order -- retains legally-repeated fields
377
- var refouter = []; // RFC 9788 sec. 4.2.1: the HP-Outer records (name + the value the field had in the outer section)
246
+ var entries = [];
247
+ var refouter = [];
378
248
  headerList.forEach(function (h) {
379
249
  if (_isStructural(h.lname)) return;
380
250
  if (h.lname === "hp-outer") {
381
- // RFC 9788 sec. 4.2.1 step 4.i: split the HP-Outer value on the first colon into (name, outer-value)
382
- // -> refouter. The value is kept BYTE-PRESERVING (latin1), never UTF-8-decoded, because the sec. 4.3.1
383
- // confidentiality comparison must be octet-exact (a lossy decode maps distinct invalid octets 0x80/0x81
384
- // to one replacement char, which would mis-classify an obscured field as exposed and drop it from the
385
- // confidential set). HP-Outer is never surfaced as a protected header nor counted toward the duplicate
386
- // check (sec. 2.2: it "can appear multiple times"). A valueless HP-Outer (no inner colon) is ignored.
387
251
  var ci = h.rawValue.indexOf(":");
388
252
  if (ci >= 0) refouter.push({ name: h.rawValue.slice(0, ci).trim().toLowerCase(), value: h.rawValue.slice(ci + 1).trim() });
389
253
  return;
390
254
  }
391
- var val = _utf8Header(h.rawValue); // the exact authenticated field body (RFC 6532 UTF-8)
392
- entries.push({ name: h.name, value: val, raw: h.rawValue }); // EVERY occurrence: `value` (UTF-8) for surfacing, `raw` (latin1) for the octet-exact confidentiality match
255
+ var val = _utf8Header(h.rawValue);
256
+ entries.push({ name: h.name, value: val, raw: h.rawValue });
393
257
  if (seen[h.lname]) { dup = h.name; if (RFC5322_SINGLETON.has(h.lname)) dupSingleton = h.name; }
394
258
  seen[h.lname] = 1;
395
- protectedHeaders[h.name] = val; // SURFACE (map, last-wins -- the ergonomic single-value view; `entries` keeps every occurrence + the octet-exact `raw`)
396
- // innerFrom uses the byte-preserving value: a lossy UTF-8 decode maps distinct invalid 8-bit sequences (0x80
397
- // vs 0x81) to the same replacement char, which would let an attacker alter the displayed From without
398
- // tripping fromMismatch. Compare the raw octets instead.
259
+ protectedHeaders[h.name] = val;
399
260
  if (h.lname === "from") innerFrom = h.value;
400
261
  });
401
262
  return { protectedHeaders: protectedHeaders, entries: entries, innerFrom: innerFrom, refouter: refouter, dup: dup, dupSingleton: dupSingleton };
402
263
  }
403
264
 
404
- // RFC 9788 sec. 4.3.1: a protected field occurrence is end-to-end confidential (encrypted-only) unless a
405
- // `refouter` record carries its EXACT name + value, meaning that value was copied verbatim to the visible outer
406
- // section. The match is per-OCCURRENCE and MULTISET (each outer record accounts for at most one inside
407
- // occurrence), so a legally-repeated field with one obscured value and one exposed value (Keywords: secret then
408
- // Keywords: public, outer exposing only "public") still reports the name confidential, because a last-wins collapse
409
- // would drop the hidden "secret" and let a caller leak it. A field name is confidential if ANY of its
410
- // occurrences is unexposed. The comparison is octet-exact (latin1 raw), never lossy-decoded. For the standard
411
- // path `refouter` is the HP-Outer records; for the legacy path it is the actual outer Header Section (part A).
412
265
  function _computeConfidential(entries, refouter) {
413
- // Index the outer occurrences by (normalized name -> value -> available count) so the multiset comparison is
414
- // O(N + M), not a per-occurrence rescan of the outer array: matching N identical exposed occurrences would
415
- // otherwise be N(N+1)/2 comparisons: a DoS an attacker could drive toward the 16 MiB section cap. A two-level
416
- // Map (never a plain object) so an attacker-chosen name/value like "__proto__" cannot confuse the lookup.
417
266
  var byName = new Map();
418
267
  refouter.forEach(function (r) {
419
268
  var m = byName.get(r.name); if (!m) { m = new Map(); byName.set(r.name, m); }
@@ -422,110 +271,48 @@ function _computeConfidential(entries, refouter) {
422
271
  var confidential = [], seen = Object.create(null);
423
272
  entries.forEach(function (e) {
424
273
  var m = byName.get(e.name.toLowerCase()), val = e.raw.trim(), n = (m && m.get(val)) || 0;
425
- if (n > 0) { m.set(val, n - 1); return; } // an outer occurrence accounts for (exposes) this one
274
+ if (n > 0) { m.set(val, n - 1); return; }
426
275
  if (!seen[e.name]) { seen[e.name] = 1; confidential.push(e.name); }
427
276
  });
428
277
  return confidential;
429
278
  }
430
279
 
431
- // Inspect every outer From (outerEnt.header returns only the first; an attacker may append a second, forged From
432
- // that a MUA displays). A mismatch is ANYTHING but exactly one outer From equal to the protected one: a removed
433
- // outer From, a duplicate, or a differing value all flag tampering of the displayed sender.
434
- // null when there was nothing to compare, true/false only when a comparison ran. A bare
435
- // false cannot distinguish "the outer From agrees with the protected one" from "no inner
436
- // From was protected, so no comparison happened" -- and the second is the common case,
437
- // which is how `!fromMismatch` came to read as a passed check on a message where nothing
438
- // was checked at all.
439
280
  function _computeFromMismatch(innerFrom, outerEnt) {
440
281
  if (innerFrom == null) return null;
441
282
  var outerFroms = [];
442
- outerEnt.headers.forEach(function (h) { if (h.lname === "from") outerFroms.push(h.value.trim()); }); // byte-preserving (latin1)
283
+ outerEnt.headers.forEach(function (h) { if (h.lname === "from") outerFroms.push(h.value.trim()); });
443
284
  return outerFroms.length !== 1 || outerFroms[0] !== innerFrom.trim();
444
285
  }
445
286
 
446
- // Is this (lowercased) media type a Cryptographic Layer (RFC 9788 sec. 4.10.1)? Conservative: any
447
- // application/pkcs7-mime (incl. an OpenSSL x-pkcs7-mime, or a compressed-data layer) plus multipart/signed and
448
- // multipart/encrypted. Mis-classifying part D as a layer only degrades legacy detection to none, the safe direction.
449
287
  function _isCryptoLayer(type) {
450
- // `type` is the already-lowercased media type from mime.parse's _parseStructured; _isPkcs7 guards the
451
- // null/undefined case internally, so no local `|| ""` fallback is needed (and no dead branch is introduced).
452
288
  return _isPkcs7(type, "mime") || type === "multipart/signed" || type === "multipart/encrypted";
453
289
  }
454
290
 
455
- // The legacy `refouter`: the actual visible outer Header Section (part A) as name+value records, so the sec.
456
- // 4.3.1 confidentiality comparison runs against the real outer values (a legacy message carries no HP-Outer).
457
291
  function _outerRefouter(outerEnt) {
458
292
  var r = [];
459
293
  outerEnt.headers.forEach(function (h) { if (!_isStructural(h.lname) && h.lname !== "hp-outer") r.push({ name: h.lname, value: h.rawValue.trim() }); });
460
294
  return r;
461
295
  }
462
296
 
463
- // RFC 9788 sec. 4.10 backward compatibility: identify + surface a legacy RFC8551HP message, a Cryptographic
464
- // Envelope whose payload is a bare message/rfc822 object carrying the real headers, with NO hp= parameter. This
465
- // is detect-only (sec. 4.10: "An MUA MUST NOT generate an RFC8551HP message ... MAY try to render") and opt-in
466
- // (`enabled`). Identification (sec. 4.10.1) is the four conjunctive conditions C1-C4; every failure (an
467
- // unparseable inner message, a non-message/rfc822 payload, a nested Cryptographic Layer, an hp= on part D, or an
468
- // ambiguous/empty protected set -- fails SOFT to none (never throws), because the message is not PRECISELY
469
- // identified. On identification (sec. 4.10.2) the inner message's (part D's) Non-Structural fields are surfaced,
470
- // the mode is inferred from the envelope (clear for signed, cipher for encrypted), and the confidential set is
471
- // derived from the actual outer section (part A). CRUCIAL: a legacy RFC8551HP message is structurally
472
- // INDISTINGUISHABLE from an ordinary signed/forwarded message/rfc822 (sec. 4.10.2: the inference is "not based
473
- // on any strong end-to-end guarantees"), so the inferred set is never placed in `protectedHeaders` and never
474
- // sets `present:true`; those describe only cryptographically-declared hp= protection a caller may trust. The
475
- // inference is surfaced only under `headerProtection.legacy` (its own { headers, mode, fromMismatch, confidential }
476
- // object), so a caller keying trust off `present` / `protectedHeaders` can never mistake an opted-in heuristic --
477
- // possibly a forwarded attachment -- for this message's authenticated headers; consuming it is an explicit choice.
478
297
  function _legacyHpSurface(canon, outerEnt, mode, enabled) {
479
298
  if (!enabled) return _noneSurface();
480
299
  var partC, partD;
481
300
  try {
482
- // C1 (outermost object is a Cryptographic Layer) holds by construction: _hpSurface only reaches here after a
483
- // successful, AUTHENTICATED verify/decrypt. C2: the Cryptographic Payload must be a SINGLE message/rfc822 object.
484
301
  partC = mime.parse(canon, SmimeError, "smime/bad-header-protection");
485
302
  if (partC.contentType.type !== "message/rfc822") return _noneSurface();
486
303
  partD = mime.parse(partC.body, SmimeError, "smime/bad-header-protection");
487
- } catch (_e) { return _noneSurface(); } // an unparseable legacy candidate is not precisely identified -- fail soft
488
- // The C2-C4 classification reads only the first Content-Type of each part (mime.parse surfaces that one). A
489
- // duplicate Content-Type on part C or part D is ambiguous, since a later field could carry hp= or a
490
- // Cryptographic Layer media type the checks below would miss. Fail soft there, matching the standard path's
491
- // duplicate reject.
304
+ } catch (_e) { return _noneSurface(); }
492
305
  if (_contentTypeCount(partC.headers) > 1 || _contentTypeCount(partD.headers) > 1) return _noneSurface();
493
- // C4: an hp= on part D means the message is not legacy (sec. 4.1: a consumer MUST ignore hp outside the payload
494
- // root). C3: part D must not itself be a Cryptographic Layer (a genuinely nested signed/encrypted message).
495
306
  if (mime.hasParam(partD.contentType.value, "hp")) return _noneSurface();
496
307
  if (_isCryptoLayer(partD.contentType.type)) return _noneSurface();
497
308
  var ex = _extractProtected(partD.headers);
498
- // Only a duplicate SINGLETON field (RFC 5322 sec. 3.6) is the ambiguous case. Part D is an ordinary received
499
- // message whose trace fields (Received, ...) and other repeatable fields legitimately recur, so a repeatable
500
- // duplicate must not reject the inference (else legacy detection fails on essentially all delivered mail).
501
309
  if (ex.dupSingleton) return _noneSurface();
502
- if (!ex.entries.length) return _noneSurface(); // no Non-Structural fields -- nothing to surface
310
+ if (!ex.entries.length) return _noneSurface();
503
311
  var confidential = mode === "cipher" ? _computeConfidential(ex.entries, _outerRefouter(outerEnt)) : [];
504
- // present stays false + protectedHeaders stays null: the inferred set is surfaced only under `legacy` so a
505
- // caller cannot mistake an opt-in heuristic (indistinguishable from a forwarded message/rfc822) for authenticated
506
- // headers. `headers` is the ORDERED [{ name, value }] list (every occurrence; a real message repeats trace
507
- // fields); the internal `raw` per occurrence is dropped from the public shape.
508
312
  var headers = ex.entries.map(function (e) { return { name: e.name, value: e.value }; });
509
- // The OUTER fromMismatch stays unanswered here: a legacy wrap is an inference, never
510
- // declared protection, so the top-level field must not report a comparison the message
511
- // did not authenticate. The legacy block carries its own, clearly opt-in.
512
313
  return { protectedHeaders: null, headerProtection: { present: false, mode: null, fromMismatch: null, confidential: [], legacy: { headers: headers, mode: mode, fromMismatch: _computeFromMismatch(ex.innerFrom, outerEnt), confidential: confidential } } };
513
314
  }
514
315
 
515
- // Detect + surface RFC 9788 header protection on a recovered inner entity, FAIL-CLOSED. A payload that does
516
- // not declare hp surfaces protectedHeaders:null, unless opts.legacyHeaderProtection is set and it is a legacy
517
- // RFC8551HP message/rfc822 wrap (sec. 4.10), which _legacyHpSurface then surfaces (opt-in, fail-soft). A payload
518
- // that DECLARES hp is validated: a malformed block, an invalid hp value, or an hp mode that CONTRADICTS the
519
- // cryptographic envelope (expectedMode "clear" for a signed message, "cipher" for a decrypted one) throws
520
- // smime/bad-header-protection -- never a silent downgrade. Otherwise the inline Non-Structural fields ARE the
521
- // authenticated set; a From that differs from the untrusted OUTER From is flagged fromMismatch.
522
- // The email identities a signer certificate actually asserts. RFC 8550 sec. 4.4.3 makes
523
- // subjectAltName the carrier: an ASCII local-part rides the rfc822Name CHOICE, and a
524
- // certificate MAY assert several addresses, so this collects every one rather than the
525
- // first. A non-ASCII local-part rides otherName / SmtpUTF8Mailbox (RFC 8398 sec. 3); those
526
- // are counted as PRESENT but not returned as comparable strings, so a certificate that
527
- // carries only internationalized identities reports "there is an identity here that this
528
- // comparison cannot read" instead of the indistinguishable "no identities".
529
316
  var SAN_OID = oid.byName("subjectAltName");
530
317
  var SMTP_UTF8_MAILBOX = oid.byName("smtpUtf8Mailbox");
531
318
  var EMAIL_ADDRESS_ATTR = oid.byName("emailAddress");
@@ -535,11 +322,6 @@ var _extCtx = { E: function (c, m, cause) { return new SmimeError(c, m, cause);
535
322
  function _signerEmails(certDer) {
536
323
  var out = { addresses: [], unreadable: 0 };
537
324
  var parsed = null;
538
- // A failure here is COUNTED, never absorbed: it raises `unreadable`, which drives the
539
- // sender verdict to null (undecidable) rather than to a clean "no identities". Returning
540
- // early on the error would make an unreadable certificate indistinguishable from one
541
- // that genuinely asserts no address, and the caller would read "nothing to match" as a
542
- // finished comparison instead of one that never happened.
543
325
  try { parsed = schemaX509.parse(certDer); }
544
326
  catch (parseFailed) { out.unreadable++; out.reason = parseFailed.code || "unparseable"; }
545
327
  if (parsed === null) return out;
@@ -550,39 +332,13 @@ function _signerEmails(certDer) {
550
332
  catch (sanFailed) { out.unreadable++; out.reason = sanFailed.code || "bad-san"; }
551
333
  if (dec === null) return;
552
334
  (dec.names || []).forEach(function (n) {
553
- if (n.tagNumber === 1) { out.addresses.push(String(n.value)); return; } // rfc822Name
554
- // An otherName only bears on the EMAIL question when it is an SmtpUTF8Mailbox
555
- // (RFC 8398 sec. 3). A certificate may legitimately carry unrelated otherNames -- a
556
- // Microsoft UPN is routine in enterprise issuance -- and counting those as an
557
- // unreadable identity would drive a certificate whose rfc822Name matches perfectly
558
- // to an undecidable verdict, making expectedSender unusable for that whole class.
335
+ if (n.tagNumber === 1) { out.addresses.push(String(n.value)); return; }
559
336
  if (n.tagNumber !== 0) return;
560
- // The decoder builds otherName as value: { typeId, valueBytes } (schema-pkix's
561
- // GeneralName rule), so the type-id lives one level in. Reading it off the node
562
- // itself silently matched nothing, which made every SmtpUTF8Mailbox invisible and
563
- // let a legacy subject value speak for a certificate whose SAN was authoritative.
564
- // An otherName whose type-id cannot be read at all counts as unreadable: unknown
565
- // shape is a reason to withhold a verdict, not to ignore the entry.
566
337
  var typeId = n.value && typeof n.value === "object" ? n.value.typeId : undefined;
567
338
  if (typeId === undefined) { out.unreadable++; return; }
568
339
  if (String(typeId) === SMTP_UTF8_MAILBOX) out.unreadable++;
569
340
  });
570
341
  });
571
- // RFC 8550 sec. 3: the address SHOULD live in subjectAltName, but a receiving agent
572
- // "MUST recognize email addresses in the distinguished name field in the PKCS #9
573
- // emailAddress attribute". Reading only the extension would leave every legacy
574
- // certificate of that shape permanently undecidable, so the subject DN is consulted too.
575
- // It is a fallback, not a peer: a SAN address is the conformant carrier.
576
- // A fallback, not a peer: consulted ONLY when subjectAltName yielded no address. RFC 8550
577
- // sec. 3 says the address SHOULD be in the extension and SHOULD NOT be in the subject, so
578
- // where both exist the extension is authoritative. Merging them would let a stale or
579
- // conflicting subject value satisfy expectedSender while the SAN names a different
580
- // mailbox -- a positive binding drawn from the weaker of two disagreeing identities.
581
- // "No SAN email identity" means none of EITHER kind. An SmtpUTF8Mailbox raises
582
- // `unreadable` without adding a comparable address, so testing `addresses` alone would
583
- // fall through to the subject for exactly the certificates whose SAN does carry an
584
- // authoritative identity -- letting a stale subject value override an internationalized
585
- // SAN this toolkit simply cannot read.
586
342
  var sanCarriedIdentity = out.addresses.length > 0 || out.unreadable > 0;
587
343
  var rdns = !sanCarriedIdentity && parsed.subject ? parsed.subject.rdns : null;
588
344
  (rdns || []).forEach(function (rdn) {
@@ -594,25 +350,8 @@ function _signerEmails(certDer) {
594
350
  return out;
595
351
  }
596
352
 
597
- // What the signer's certificate says about who sent this, and whether anyone asked.
598
- //
599
- // `match` is THREE-valued and never collapses: true only when an identity in the signer's
600
- // certificate matches under RFC 5280 sec. 7.5; false when every identity was comparable and
601
- // none matched; null when the question was not asked, or was asked and could not be decided
602
- // (no readable identity, or an address needing an IDNA transform this toolkit will not do).
603
- // A boolean here would make "nobody checked" indistinguishable from "checked and agreed",
604
- // which is the defect this field exists to remove -- so a caller enforcing sender binding
605
- // tests `match === true`, and null fails that test exactly as false does.
606
- //
607
- // `expectedSender` is the authoritative input. The outer From is attacker-controlled on an
608
- // unprotected message, so comparing against it is reported as advisory (`source: "from"`)
609
- // and never presented as a verified binding.
610
353
  function _senderSurface(signers, outerEnt, expectedSender) {
611
354
  var expected = null, source = null;
612
- // Entry-tier: a mistyped option is a caller bug and throws, never a coercion. String()
613
- // would accept any object with a toString, so `{ toString: () => "alice@example.com" }`
614
- // would drive a sender binding the caller never actually stated -- the authoritative
615
- // input to this comparison must be the string it claims to be.
616
355
  if (expectedSender != null) {
617
356
  if (typeof expectedSender !== "string") {
618
357
  throw _err("smime/bad-input", "expectedSender must be a string email address, got " + typeof expectedSender);
@@ -622,53 +361,24 @@ function _senderSurface(signers, outerEnt, expectedSender) {
622
361
  else {
623
362
  var froms = [];
624
363
  outerEnt.headers.forEach(function (h) { if (h.lname === "from") froms.push(h.value.trim()); });
625
- // Exactly one From, or there is no unambiguous address to compare against.
626
364
  if (froms.length === 1) { expected = _addrSpec(froms[0]); source = expected === null ? null : "from"; }
627
365
  }
628
366
  var identities = [], undecidable = false, sawIdentity = false;
629
367
  (signers || []).forEach(function (s) {
630
- // Only a signer whose signature actually verified may contribute an identity.
631
- // cms.verify reports every matched signer, including ones that failed, as ok:false --
632
- // harvesting their subjectAltName would let a tampered message report valid:false
633
- // alongside sender.match:true, and this verb's own guidance is to enforce
634
- // sender.match === true. A failed signer makes the answer undecidable, never a clean
635
- // no-match.
636
- // Only a signer whose signature actually verified may contribute an identity.
637
- // cms.verify reports every matched signer, including ones that failed, as ok:false --
638
- // harvesting their subjectAltName would let a tampered message report valid:false
639
- // alongside sender.match:true, and this verb's own guidance is to enforce
640
- // sender.match === true. A failed signer makes the answer undecidable, never a clean
641
- // no-match.
642
- // A verified signature is not enough: anyone can self-sign a certificate carrying the
643
- // victim's address, and cms.verify reports that signer ok:true / trusted:false. Reading
644
- // its subjectAltName would let an attacker-minted certificate produce a positive
645
- // binding, which is the same defect one layer up from the ok:false case. Name your
646
- // roots in opts.trustAnchors; with none supplied nothing is trusted, so the binding
647
- // question stays unanswered rather than answered from an unvetted certificate.
648
368
  if (!s || s.ok !== true || s.trusted !== true) { undecidable = true; return; }
649
369
  var e = s.cert ? _signerEmails(s.cert) : null;
650
- if (e === null) { undecidable = true; return; } // no certificate to read at all
370
+ if (e === null) { undecidable = true; return; }
651
371
  if (e.unreadable > 0) { undecidable = true; sawIdentity = true; }
652
372
  e.addresses.forEach(function (a) { sawIdentity = true; if (identities.indexOf(a) === -1) identities.push(a); });
653
373
  });
654
374
  if (expected === null) return { checked: false, expected: null, source: null, identities: identities, match: null };
655
375
  if (!sawIdentity) return { checked: true, expected: expected, source: source, identities: identities, match: null };
656
- // No early return on the first match. A match is only the answer once nothing else has
657
- // made the result undecidable: a failed co-signer, an unreadable certificate or an
658
- // address this toolkit cannot canonicalize all mean the verdict is unsettled, and
659
- // reporting true from inside the loop would let that be read as a clean binding.
660
- // Undecidable always wins over a positive.
661
376
  var sawMatch = false, sawNotComparable = false;
662
377
  for (var i = 0; i < identities.length; i++) {
663
378
  var v = guard.name.emailEqual(identities[i], expected);
664
379
  if (v === "match") sawMatch = true;
665
380
  else if (v === "not-comparable") sawNotComparable = true;
666
381
  }
667
- // The question is existential: does a verified signer's certificate assert this address?
668
- // An exact match answers it, and a second identity that could not be compared does not
669
- // un-answer it -- a certificate legitimately carrying several addresses would otherwise
670
- // fail the binding because of one it was never asked about. Undecidability only decides
671
- // the verdict when nothing matched.
672
382
  var verdict;
673
383
  if (sawMatch) verdict = true;
674
384
  else if (undecidable || sawNotComparable) verdict = null;
@@ -676,75 +386,34 @@ function _senderSurface(signers, outerEnt, expectedSender) {
676
386
  return { checked: true, expected: expected, source: source, identities: identities, match: verdict };
677
387
  }
678
388
 
679
- // The addr-spec inside an RFC 5322 From. A display-name form ("Bob <bob@x>") yields the
680
- // angle-addr; a bare address yields itself. Anything else (a group, a second address, an
681
- // unclosed angle bracket) returns null so the caller reports "not compared" rather than
682
- // comparing against a fragment.
683
389
  function _addrSpec(v) {
684
390
  var lt = v.indexOf("<"), gt = v.lastIndexOf(">");
685
391
  if (lt === -1 && gt === -1) return v.indexOf(",") === -1 ? v.trim() : null;
686
392
  if (lt === -1 || gt < lt) return null;
687
- // Nothing but whitespace may follow the angle address. `Alice <alice@x>, mallory@y` is two
688
- // mailboxes, and returning the first would report a single compared sender over a field
689
- // that named more than one -- the ambiguity this function exists to refuse.
690
393
  if (v.slice(gt + 1).trim().length) return null;
691
394
  var inner = v.slice(lt + 1, gt).trim();
692
395
  return inner.length && inner.indexOf(",") === -1 && inner.indexOf("<") === -1 ? inner : null;
693
396
  }
694
397
 
695
398
  function _hpSurface(content, outerEnt, expectedMode, authenticated, legacyEnabled) {
696
- // Header protection is an AUTHENTICATED property: surface the inner headers as protected only when the
697
- // cryptographic verdict succeeded (a valid signature, or an authenticated-encryption decrypt). An invalid
698
- // signature or an unauthenticated (AES-CBC, no integrity) decrypt yields attacker-influenced bytes, and those
699
- // are never marked or exposed as protected (a caller must not treat protectedHeaders as a trust signal
700
- // unless integrity held).
701
399
  if (!authenticated) return _noneSurface();
702
- // Detect + parse the CANONICAL entity: the exact bytes the signature covers. A transport may rewrite a
703
- // CRLF fold OR the header/body separator to bare CR/LF; canonicalize repairs them (so verification still
704
- // succeeds), and both the hp detection and the parse must run on the repaired bytes, or they diverge from
705
- // the signed content -- stripping the signal, or false-rejecting a valid message as an unparseable block.
706
- // The returned `content` stays the raw recovered bytes; only this HP inspection uses the canonical copy.
707
400
  var canon = mime.canonicalizeText(content);
708
- // A payload that does not declare hp is either non-protected (protectedHeaders:null) or a legacy RFC8551HP
709
- // message/rfc822 wrap the caller opted into detecting (sec. 4.10). Both are mutually exclusive with the
710
- // standard path below by construction: it runs only when a real hp= is present on the payload root.
711
401
  if (!_declaresHp(canon)) return _legacyHpSurface(canon, outerEnt, expectedMode, legacyEnabled);
712
- var inner = mime.parse(canon, SmimeError, "smime/bad-header-protection"); // a malformed HP block fails closed
713
- // A duplicate Content-Type makes the hp declaration ambiguous (a parser reads params from the first field,
714
- // but an hp= may sit on a later one), so the wrap is malformed and fails closed, never a silent downgrade.
402
+ var inner = mime.parse(canon, SmimeError, "smime/bad-header-protection");
715
403
  var ctCount = _contentTypeCount(inner.headers);
716
404
  if (ctCount > 1) throw _err("smime/bad-header-protection", "a header-protected payload must carry exactly one Content-Type field (found " + ctCount + ")");
717
- // A duplicate hp attribute is ambiguous (mime.parse keeps the last value, but a recipient honoring the
718
- // first would see a different mode); fail closed, like the duplicate Content-Type. Counted by attribute
719
- // name (bare OR valued) so a bare+valued pair ("hp; hp=x") is caught too, and quote-/comment-aware (an hp=
720
- // inside a quoted value or comment is not the hp parameter, so a message we emit is never self-rejected).
721
405
  if (mime.paramNameCount(inner.contentType.value, "hp") > 1) throw _err("smime/bad-header-protection", "a header-protected payload declares the hp parameter more than once");
722
- // _declaresHp composes the same comment/quoted-string-aware tokenizer (mime.paramCount) mime.parse uses, so
723
- // reaching here means the single Content-Type carries a real hp parameter (a duplicate is rejected above) --
724
- // hp is defined. A malformed value fails CLOSED at the mode checks below, never a silent downgrade.
725
- // The hp keyword values clear / cipher are an enumerated Content-Type parameter. Per RFC 2045 sec. 5.1 a
726
- // value of this kind is compared case-insensitively for its intended use, so a peer that emits hp="Clear"
727
- // or hp=CIPHER is still recognized. (We always EMIT lowercase; we ACCEPT any case. The value is inside the
728
- // signed/encrypted payload, so normalizing it enables no attack; the mode-contradiction check still runs.)
729
- // _declaresHp detected the hp attribute (hasParam); a bare "hp" with no value is a malformed HP declaration
730
- // and fails closed (mime.parse skips a valueless parameter, so params.hp is undefined here only for that
731
- // malformed case).
732
406
  var raw = inner.contentType.params.hp;
733
407
  if (raw === undefined) throw _err("smime/bad-header-protection", "a header-protected payload declares a bare hp parameter with no value (expected hp=\"clear\" or hp=\"cipher\")");
734
408
  var hp = raw.toLowerCase();
735
409
  if (hp !== "clear" && hp !== "cipher") throw _err("smime/bad-header-protection", "the header-protected payload declares an invalid hp value " + JSON.stringify(raw) + " (only clear / cipher)");
736
410
  if (hp !== expectedMode) throw _err("smime/bad-header-protection", "the payload hp=" + JSON.stringify(hp) + " contradicts the cryptographic envelope (a " + (expectedMode === "cipher" ? "decrypted" : "signed") + " message requires hp=" + JSON.stringify(expectedMode) + ")");
737
411
  var ex = _extractProtected(inner.headers);
738
- // A duplicate protected field is ambiguous (the last-wins overwrite hides an earlier value a different
739
- // parser might select), so the payload fails closed.
740
412
  if (ex.dup) throw _err("smime/bad-header-protection", "a header-protected payload has a duplicate protected header field " + JSON.stringify(ex.dup));
741
- // RFC 9788 sec. 4.3.1: for an ENCRYPTED payload (hp="cipher") the confidential set is the fields not copied
742
- // verbatim to the outer section via an HP-Outer record. Signed-only (clear) payloads carry no HP-Outer.
743
413
  var confidential = hp === "cipher" ? _computeConfidential(ex.entries, ex.refouter) : [];
744
414
  return { protectedHeaders: ex.protectedHeaders, headerProtection: { present: true, mode: hp, fromMismatch: _computeFromMismatch(ex.innerFrom, outerEnt), confidential: confidential, legacy: null } };
745
415
  }
746
416
 
747
- // Map smime opts to cms.sign opts (the S/MIME layer is algorithm-agnostic and forwards any signer).
748
417
  function _cmsSignOpts(opts, detached) {
749
418
  var o = { detached: detached };
750
419
  if (opts.signingTime !== undefined) o.signingTime = opts.signingTime;
@@ -754,20 +423,12 @@ function _cmsSignOpts(opts, detached) {
754
423
  return o;
755
424
  }
756
425
 
757
- // Coverage residual: the defensive `|| default` fallbacks in the helpers below (a null media type, a
758
- // non-standard SignerInfo digest name -> a derived micalg, an absent micalg -> "sha-256"/null, a
759
- // non-Buffer cms.sign result) and the two catch arms (a CMS body that passed cms.verify yet fails a
760
- // second schema-cms.parse) are belt-and-braces around the primary path the round-trips exercise; the
761
- // producer never emits any of them.
762
426
  function _micName(name) { return name ? (MICALG[name] || name) : null; }
763
- // Normalize a micalg header value into the same sorted-distinct comma-joined form _micalgOf emits.
764
427
  function _micalgSet(value) {
765
428
  var seen = [];
766
429
  String(value).split(",").forEach(function (m) { var t = m.trim().toLowerCase(); if (t && seen.indexOf(t) < 0) seen.push(t); });
767
430
  return seen.sort().join(",");
768
431
  }
769
- // The micalg for the SignedData -- RFC 8551 sec. 3.4.3.2: EVERY signer's message-digest algorithm,
770
- // distinct, sorted, comma-separated (a multi-signer message with mixed digests lists them all).
771
432
  function _micalgOf(p7Der) {
772
433
  var parsed;
773
434
  try { parsed = schemaCms.parse(guard.bytes.view(p7Der, SmimeError, "smime/bad-mime", "the CMS body")); }
@@ -780,11 +441,8 @@ function _micalgOf(p7Der) {
780
441
  return out.length ? out.sort().join(",") : null;
781
442
  }
782
443
 
783
- // A fresh multipart boundary that cannot collide with the content (random, dashed, unique per call).
784
444
  function _boundary() { return "----=_pki_smime_" + nodeCrypto.randomBytes(18).toString("hex"); }
785
445
 
786
- // Base64 wrapped to 64-column lines joined by CRLF, no trailing CRLF (matching (.{64})->"$1\r\n"
787
- // then a trailing-CRLF strip; base64 output carries no CRLF of its own).
788
446
  function _wrap64Crlf(s) {
789
447
  var out = "";
790
448
  for (var i = 0; i < s.length; i += 64) {
@@ -841,11 +499,8 @@ function _base64Body(der) {
841
499
  async function sign(content, signers, opts) {
842
500
  opts = opts || {};
843
501
  _knownOpts(opts, SIGN_OPTS, "sign");
844
- // RFC 9788 header protection (opts.protectHeaders): the inner Cryptographic Payload gains hp="clear" +
845
- // the inlined protected fields, and the outer frame carries the display copies. Off by default => the
846
- // shipped path, byte-for-byte.
847
502
  var hp = opts.protectHeaders === true;
848
- var hpList = hp ? _hpHeaderList(opts.headers) : null; // ONE snapshot for both the signed inner + outer display copies
503
+ var hpList = hp ? _hpHeaderList(opts.headers) : null;
849
504
  var outerList = hp ? _outerHeaderList(hpList, "clear", opts.hcp) : null;
850
505
  var entity = hp ? _protectedInnerEntity(content, opts, "clear", hpList, outerList) : _entityBytes(content, opts);
851
506
  var outer = hp ? _outerPrefix(outerList) : "";
@@ -868,10 +523,6 @@ async function sign(content, signers, opts) {
868
523
  return _capped(Buffer.concat([head2, dl, entity, mime.CRLF, dl, sigPart, mime.CRLF, close]));
869
524
  }
870
525
 
871
- // A produced message MUST stay within the same size ceiling verify enforces on receive, so sign never
872
- // emits an S/MIME message our own verify would reject as too large (producer/consumer symmetry).
873
- // Coverage residual: the throw only fires on a >16 MiB assembled message -- a real cap, exercised by no
874
- // unit vector (driving it would sign 16 MiB).
875
526
  function _capped(msg) {
876
527
  guard.limits.byteCap(msg, C.LIMITS.MIME_MAX_BYTES, _err, "smime/too-large", "the assembled S/MIME message");
877
528
  return msg;
@@ -962,25 +613,11 @@ async function verify(message, opts) {
962
613
  _knownOpts(opts, VERIFY_OPTS, "verify");
963
614
  var ent = mime.parse(message, SmimeError, "smime/bad-mime");
964
615
  var ct = ent.contentType;
965
- // Forwarded, not re-decided here. This verb documents itself as pki.cms.verify's verdict plus the
966
- // MIME surface, so the trust seam that verb offers has to reach it: building the options from
967
- // scratch and passing only `certs` would leave a caller naming trust anchors with no way to have
968
- // them applied, and a `trusted` that read false for want of ever being asked.
969
616
  var vOpts = {};
970
617
  if (opts.certs) vOpts.certs = opts.certs;
971
618
  if (opts.trustAnchors != null) {
972
619
  vOpts.trustAnchors = opts.trustAnchors;
973
- // Trusted FOR THIS PURPOSE. A chain alone does not make a signer right for email: a
974
- // certificate restricted to serverAuth chains to its root perfectly well and is still the
975
- // wrong key to have signed a message. RFC 8551 sec. 4.4.4 names emailProtection as the purpose
976
- // an S/MIME signer's certificate must carry, so this verb asks for it rather than accepting the
977
- // purpose-neutral answer. A caller who means something else says so with `requiredEku`.
978
620
  vOpts.requiredEku = opts.requiredEku != null ? opts.requiredEku : ["emailProtection"];
979
- // Both ends of the chain. The EKU above constrains the LEAF; this selects the anchor's own
980
- // trust metadata, which pki.path consults only when a purpose is named. A root distributed
981
- // with NSS trust bits can be marked untrusted for email while remaining a good TLS root, so
982
- // asking only the leaf would let a root explicitly distrusted for email still answer
983
- // "trusted" for an email message.
984
621
  vOpts.checkPurpose = opts.checkPurpose != null ? opts.checkPurpose : "emailProtection";
985
622
  }
986
623
  if (opts.time !== undefined) vOpts.time = opts.time;
@@ -1000,10 +637,6 @@ async function verify(message, opts) {
1000
637
  var sigEnt = mime.parse(parts[1], SmimeError, "smime/bad-mime");
1001
638
  if (!_isPkcs7(sigEnt.contentType.type, "signature")) throw _err("smime/bad-multipart", "the second part must be application/pkcs7-signature");
1002
639
  var p7s = _decodeCms(sigEnt);
1003
- // RFC 8551 sec. 3.4 / RFC 1847: the multipart/signed signature is DETACHED -- the SignedData carries
1004
- // NO encapsulated content, the first MIME part IS the signed unit. Reject an ATTACHED SignedData:
1005
- // otherwise cms.verify would verify over the embedded eContent and IGNORE the first part, so an
1006
- // attacker could pair any validly-signed attached blob with an arbitrary first part (a forgery).
1007
640
  var sd;
1008
641
  try { sd = schemaCms.parse(p7s); }
1009
642
  catch (e) { throw _err("smime/bad-mime", "the pkcs7-signature part is not a CMS SignedData", e); }
@@ -1013,8 +646,6 @@ async function verify(message, opts) {
1013
646
  var canon = mime.canonicalize(parts[0], SmimeError, "smime/bad-mime");
1014
647
  var res2 = await cms.verify(p7s, Object.assign({}, vOpts, { content: canon }));
1015
648
  var micalg = ct.params.micalg || null;
1016
- // Compare micalg as an ORDER-INDEPENDENT set of digest names (RFC 8551 sec. 3.4.3.2 lists them
1017
- // comma-separated, in no required order, possibly with whitespace).
1018
649
  if (opts.strictMicalg && micalg && _micalgSet(micalg) !== (_micalgOf(p7s) || "")) {
1019
650
  throw _err("smime/micalg-mismatch", "the multipart/signed micalg " + JSON.stringify(micalg) + " disagrees with the SignerInfo digests");
1020
651
  }
@@ -1023,17 +654,10 @@ async function verify(message, opts) {
1023
654
  throw _err("smime/unsupported-type", "not a signed S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
1024
655
  }
1025
656
 
1026
- // The application/pkcs7-mime header for an opaque S/MIME message (RFC 8551 sec. 3.2 / sec. 3.2.2). `name`
1027
- // is the smime.p7m (enveloped) / smime.p7z (compressed) file name for the media type + Content-Disposition.
1028
657
  function _pkcs7MimeHead(smimeType, name) {
1029
658
  return Buffer.from("Content-Type: application/pkcs7-mime; smime-type=" + smimeType + "; name=" + name + "\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=" + name + "\r\n\r\n", "latin1");
1030
659
  }
1031
660
 
1032
- // The smime-type for a CMS enveloped body, DERIVED from its OUTER content type (RFC 8551 sec. 3.2.2) --
1033
- // the single source of truth, never an opts guess: AuthEnvelopedData -> "authEnveloped-data" (AEAD,
1034
- // integrity), EnvelopedData -> "enveloped-data" (no integrity). Anything else is not an S/MIME enveloped
1035
- // message. Returns { smimeType, name } (name is the parsed contentTypeName). Throws smime/unsupported-type
1036
- // for a non-enveloped body (EncryptedData / SignedData carries no smime-type mapping here).
1037
661
  function _envelopedTypeOf(der, badParseCode) {
1038
662
  var name;
1039
663
  try { name = schemaCms.parse(guard.bytes.view(der, SmimeError, "smime/bad-mime", "the CMS body")).contentTypeName; }
@@ -1043,10 +667,6 @@ function _envelopedTypeOf(der, badParseCode) {
1043
667
  throw _err(badParseCode, "the pkcs7-mime body is a " + name + ", not an EnvelopedData / AuthEnvelopedData");
1044
668
  }
1045
669
 
1046
- // Map smime opts to cms.encrypt opts. The S/MIME layer is recipient-agnostic: it forwards the content
1047
- // -encryption choice + every recipient-shaping opt unchanged, and never emits PEM (the CMS body is base64
1048
- // inside a MIME frame). opts.contentType is the MIME media type of the INNER entity (handled by
1049
- // _entityBytes), NOT the CMS encapsulated content type -- it is deliberately not forwarded here.
1050
670
  function _cmsEncryptOpts(opts) {
1051
671
  var o = {};
1052
672
  if (opts.contentEncryptionAlgorithm !== undefined) o.contentEncryptionAlgorithm = opts.contentEncryptionAlgorithm;
@@ -1099,18 +719,11 @@ function _cmsEncryptOpts(opts) {
1099
719
  async function encrypt(content, recipients, opts) {
1100
720
  opts = opts || {};
1101
721
  _knownOpts(opts, ENCRYPT_OPTS, "encrypt");
1102
- // RFC 9788 header protection (opts.protectHeaders): the inner payload carries hp="cipher" + the REAL
1103
- // headers (inside the ciphertext); the outer frame carries only the Header-Confidentiality-Policy-processed
1104
- // display copies (hcp_baseline obscures Subject to [...], removes Comments/Keywords). Off => shipped path.
1105
722
  var hp = opts.protectHeaders === true;
1106
- var hpList = hp ? _hpHeaderList(opts.headers) : null; // ONE snapshot for both the encrypted inner + outer display copies
723
+ var hpList = hp ? _hpHeaderList(opts.headers) : null;
1107
724
  var outerList = hp ? _outerHeaderList(hpList, "cipher", opts.hcp) : null;
1108
725
  var entity = hp ? _protectedInnerEntity(content, opts, "cipher", hpList, outerList) : _entityBytes(content, opts);
1109
- // Build the outer prefix (opts.headers + opts.hcp were validated when outerList was computed) BEFORE the
1110
- // expensive cms.encrypt, so a bad policy / header fails closed without doing the crypto.
1111
726
  var outer = hp ? Buffer.from(_outerPrefix(outerList), "utf8") : Buffer.alloc(0);
1112
- // Normalize a single descriptor to an array so cms.encrypt always takes the ENVELOPED path (an array of
1113
- // RecipientInfos), never bare EncryptedData -- which is not an S/MIME construct (no smime-type maps to it).
1114
727
  var recips = Array.isArray(recipients) ? recipients : [recipients];
1115
728
  var der = _toBuf(await cms.encrypt(entity, recips, _cmsEncryptOpts(opts)));
1116
729
  var head = _pkcs7MimeHead(_envelopedTypeOf(der, "smime/bad-mime"), "smime.p7m");
@@ -1165,8 +778,6 @@ async function decrypt(message, keyMaterial, opts) {
1165
778
  var ent = mime.parse(message, SmimeError, "smime/bad-mime");
1166
779
  var ct = ent.contentType;
1167
780
  if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not an encrypted S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
1168
- // The smime-type header is advisory (the CMS body is authoritative), so compare it case-insensitively --
1169
- // liberal on accept, strict on emit -- rather than rejecting a legitimate message over its parameter casing.
1170
781
  var st = ct.params["smime-type"];
1171
782
  var stLower = st ? st.toLowerCase() : st;
1172
783
  if (st && stLower !== "enveloped-data" && stLower !== "authenveloped-data") throw _err("smime/unsupported-type", "unsupported smime-type " + JSON.stringify(st) + " (only enveloped-data / authEnveloped-data)");
@@ -1246,7 +857,6 @@ async function decompress(message, opts) {
1246
857
  var ent = mime.parse(message, SmimeError, "smime/bad-mime");
1247
858
  var ct = ent.contentType;
1248
859
  if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not a compressed S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
1249
- // The smime-type header is advisory (the CMS body is authoritative) -- compare case-insensitively.
1250
860
  var st = ct.params["smime-type"];
1251
861
  if (st && st.toLowerCase() !== "compressed-data") throw _err("smime/unsupported-type", "unsupported smime-type " + JSON.stringify(st) + " (only compressed-data)");
1252
862
  var der = _decodeCms(ent);
@@ -1256,10 +866,7 @@ async function decompress(message, opts) {
1256
866
  return { content: res.content, contentType: res.contentType, contentTypeName: res.contentTypeName, compressionAlgorithm: res.compressionAlgorithm };
1257
867
  }
1258
868
 
1259
- // Decode a CMS body per its Content-Transfer-Encoding (base64 is the S/MIME norm; 7bit/8bit/binary pass through).
1260
869
  function _decodeCms(ent) {
1261
- // guard.encoding.base64 is strict (no whitespace) and rejects via a (code,msg) FACTORY -- strip the
1262
- // MIME line-wrapping whitespace first, and pass _err (not the SmimeError class).
1263
870
  if (ent.cte === "base64") return guard.encoding.base64(pkix.stripBase64Whitespace(ent.body.toString("latin1")), C.LIMITS.MIME_MAX_BYTES, _err, "smime/bad-mime", "the CMS body");
1264
871
  return ent.body;
1265
872
  }