@blamejs/core 0.15.12 → 0.15.14

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 (365) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +45 -29
  4. package/lib/acme.js +6 -5
  5. package/lib/agent-event-bus.js +18 -4
  6. package/lib/agent-idempotency.js +7 -6
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-snapshot.js +32 -2
  10. package/lib/agent-tenant.js +2 -5
  11. package/lib/ai-adverse-decision.js +2 -15
  12. package/lib/ai-aedt-bias-audit.js +2 -1
  13. package/lib/ai-capability.js +1 -6
  14. package/lib/ai-content-detect.js +1 -3
  15. package/lib/ai-dp.js +1 -5
  16. package/lib/ai-frontier-protocol.js +1 -1
  17. package/lib/ai-input.js +2 -2
  18. package/lib/ai-model-manifest.js +1 -1
  19. package/lib/ai-output.js +16 -7
  20. package/lib/ai-pref.js +3 -8
  21. package/lib/ai-quota.js +3 -14
  22. package/lib/api-key.js +37 -28
  23. package/lib/api-snapshot.js +4 -1
  24. package/lib/app-shutdown.js +7 -1
  25. package/lib/archive-adapters.js +2 -4
  26. package/lib/archive-entry-policy.js +32 -0
  27. package/lib/archive-gz.js +9 -0
  28. package/lib/archive-read.js +14 -24
  29. package/lib/archive-tar-read.js +56 -24
  30. package/lib/archive.js +6 -12
  31. package/lib/arg-parser.js +7 -6
  32. package/lib/asn1-der.js +70 -22
  33. package/lib/asyncapi-traits.js +2 -6
  34. package/lib/atomic-file.js +312 -33
  35. package/lib/audit-chain.js +183 -54
  36. package/lib/audit-daily-review.js +36 -16
  37. package/lib/audit-emit.js +82 -0
  38. package/lib/audit-sign.js +258 -0
  39. package/lib/audit-tools.js +108 -23
  40. package/lib/audit.js +91 -2
  41. package/lib/auth/access-lock.js +7 -29
  42. package/lib/auth/bot-challenge.js +1 -1
  43. package/lib/auth/ciba.js +43 -4
  44. package/lib/auth/dpop.js +29 -36
  45. package/lib/auth/fido-mds3.js +14 -16
  46. package/lib/auth/jwt-external.js +26 -8
  47. package/lib/auth/jwt.js +15 -17
  48. package/lib/auth/lockout.js +24 -27
  49. package/lib/auth/oauth.js +67 -45
  50. package/lib/auth/oid4vci.js +55 -32
  51. package/lib/auth/oid4vp.js +3 -2
  52. package/lib/auth/openid-federation.js +6 -6
  53. package/lib/auth/passkey.js +4 -4
  54. package/lib/auth/password.js +8 -2
  55. package/lib/auth/saml.js +70 -40
  56. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  57. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  58. package/lib/auth/sd-jwt-vc.js +25 -5
  59. package/lib/auth/status-list.js +21 -9
  60. package/lib/auth/step-up.js +15 -13
  61. package/lib/auth-bot-challenge.js +27 -39
  62. package/lib/backup/bundle.js +18 -20
  63. package/lib/backup/crypto.js +23 -8
  64. package/lib/backup/index.js +55 -67
  65. package/lib/backup/manifest.js +7 -1
  66. package/lib/bounded-map.js +112 -1
  67. package/lib/breach-deadline.js +1 -11
  68. package/lib/break-glass.js +41 -22
  69. package/lib/cache.js +7 -18
  70. package/lib/cbor.js +36 -12
  71. package/lib/cdn-cache-control.js +15 -12
  72. package/lib/cert.js +8 -13
  73. package/lib/chain-writer.js +162 -47
  74. package/lib/cli.js +10 -2
  75. package/lib/client-hints.js +7 -9
  76. package/lib/cloud-events.js +43 -33
  77. package/lib/cluster-storage.js +9 -41
  78. package/lib/cms-codec.js +2 -1
  79. package/lib/codepoint-class.js +84 -1
  80. package/lib/compliance-ai-act-logging.js +1 -6
  81. package/lib/compliance-ai-act-transparency.js +2 -4
  82. package/lib/compliance-eaa.js +2 -12
  83. package/lib/compliance-sanctions-fetcher.js +21 -28
  84. package/lib/compliance-sanctions.js +11 -21
  85. package/lib/compliance.js +3 -10
  86. package/lib/config-drift.js +24 -20
  87. package/lib/content-credentials.js +11 -8
  88. package/lib/content-digest.js +21 -12
  89. package/lib/cookies.js +15 -13
  90. package/lib/cose.js +39 -11
  91. package/lib/cra-report.js +1 -11
  92. package/lib/crdt.js +2 -1
  93. package/lib/crypto-field.js +34 -33
  94. package/lib/crypto.js +138 -7
  95. package/lib/csp.js +239 -3
  96. package/lib/daemon.js +46 -42
  97. package/lib/data-act.js +46 -13
  98. package/lib/db-file-lifecycle.js +14 -6
  99. package/lib/db-query.js +75 -49
  100. package/lib/db.js +66 -27
  101. package/lib/dbsc.js +5 -6
  102. package/lib/ddl-change-control.js +18 -14
  103. package/lib/deprecate.js +4 -5
  104. package/lib/did.js +6 -2
  105. package/lib/dora.js +43 -13
  106. package/lib/dr-runbook.js +1 -1
  107. package/lib/dsa.js +2 -1
  108. package/lib/dsr.js +30 -20
  109. package/lib/early-hints.js +19 -0
  110. package/lib/eat.js +5 -1
  111. package/lib/external-db-migrate.js +21 -22
  112. package/lib/external-db.js +74 -30
  113. package/lib/fda-21cfr11.js +42 -13
  114. package/lib/fdx.js +22 -18
  115. package/lib/file-upload.js +80 -66
  116. package/lib/flag-evaluation-context.js +5 -8
  117. package/lib/flag-providers.js +6 -2
  118. package/lib/forms.js +1 -1
  119. package/lib/framework-error.js +12 -0
  120. package/lib/framework-schema.js +19 -0
  121. package/lib/fsm.js +7 -12
  122. package/lib/gate-contract.js +911 -39
  123. package/lib/gdpr-ropa.js +22 -22
  124. package/lib/graphql-federation.js +18 -5
  125. package/lib/guard-agent-registry.js +2 -1
  126. package/lib/guard-all.js +3 -2
  127. package/lib/guard-archive.js +9 -30
  128. package/lib/guard-auth.js +23 -80
  129. package/lib/guard-cidr.js +20 -96
  130. package/lib/guard-csv.js +135 -196
  131. package/lib/guard-domain.js +23 -106
  132. package/lib/guard-dsn.js +17 -14
  133. package/lib/guard-email.js +46 -53
  134. package/lib/guard-envelope.js +2 -2
  135. package/lib/guard-event-bus-topic.js +2 -1
  136. package/lib/guard-filename.js +12 -60
  137. package/lib/guard-graphql.js +28 -75
  138. package/lib/guard-html-wcag.js +15 -2
  139. package/lib/guard-html.js +74 -128
  140. package/lib/guard-idempotency-key.js +2 -1
  141. package/lib/guard-image.js +280 -77
  142. package/lib/guard-imap-command.js +9 -10
  143. package/lib/guard-jmap.js +1 -1
  144. package/lib/guard-json.js +101 -109
  145. package/lib/guard-jsonpath.js +20 -88
  146. package/lib/guard-jwt.js +32 -114
  147. package/lib/guard-list-id.js +2 -7
  148. package/lib/guard-list-unsubscribe.js +2 -7
  149. package/lib/guard-mail-compose.js +5 -6
  150. package/lib/guard-mail-move.js +3 -2
  151. package/lib/guard-mail-query.js +5 -3
  152. package/lib/guard-mail-sieve.js +6 -7
  153. package/lib/guard-managesieve-command.js +6 -5
  154. package/lib/guard-markdown.js +76 -110
  155. package/lib/guard-message-id.js +5 -6
  156. package/lib/guard-mime.js +20 -99
  157. package/lib/guard-oauth.js +19 -73
  158. package/lib/guard-pdf.js +65 -72
  159. package/lib/guard-pop3-command.js +13 -14
  160. package/lib/guard-posture-chain.js +2 -1
  161. package/lib/guard-regex.js +24 -99
  162. package/lib/guard-saga-config.js +2 -1
  163. package/lib/guard-shell.js +22 -87
  164. package/lib/guard-smtp-command.js +9 -12
  165. package/lib/guard-sql.js +15 -13
  166. package/lib/guard-stream-args.js +2 -1
  167. package/lib/guard-svg.js +103 -149
  168. package/lib/guard-template.js +23 -80
  169. package/lib/guard-tenant-id.js +2 -1
  170. package/lib/guard-text.js +592 -0
  171. package/lib/guard-time.js +27 -95
  172. package/lib/guard-uuid.js +21 -93
  173. package/lib/guard-xml.js +76 -106
  174. package/lib/guard-yaml.js +24 -60
  175. package/lib/html-balance.js +7 -3
  176. package/lib/http-client-cache.js +5 -12
  177. package/lib/http-client-cookie-jar.js +35 -16
  178. package/lib/http-client.js +233 -74
  179. package/lib/http-message-signature.js +3 -2
  180. package/lib/i18n-messageformat.js +5 -7
  181. package/lib/i18n.js +83 -26
  182. package/lib/iab-tcf.js +3 -2
  183. package/lib/importmap-integrity.js +41 -1
  184. package/lib/inbox.js +8 -8
  185. package/lib/incident-report.js +12 -27
  186. package/lib/ip-utils.js +49 -6
  187. package/lib/jobs.js +3 -2
  188. package/lib/json-patch.js +1 -1
  189. package/lib/json-path.js +24 -3
  190. package/lib/jtd.js +2 -2
  191. package/lib/keychain.js +6 -18
  192. package/lib/legal-hold.js +30 -23
  193. package/lib/log-stream-cloudwatch.js +44 -112
  194. package/lib/log-stream-otlp-grpc.js +17 -14
  195. package/lib/log-stream-otlp.js +16 -80
  196. package/lib/log-stream-webhook.js +20 -92
  197. package/lib/log.js +2 -2
  198. package/lib/mail-agent.js +2 -2
  199. package/lib/mail-arc-sign.js +8 -3
  200. package/lib/mail-arf.js +4 -3
  201. package/lib/mail-auth.js +43 -69
  202. package/lib/mail-bimi.js +35 -55
  203. package/lib/mail-bounce.js +3 -3
  204. package/lib/mail-crypto-pgp.js +3 -2
  205. package/lib/mail-crypto-smime.js +71 -6
  206. package/lib/mail-dav.js +8 -35
  207. package/lib/mail-deploy.js +15 -14
  208. package/lib/mail-dkim.js +19 -38
  209. package/lib/mail-greylist.js +17 -30
  210. package/lib/mail-helo.js +4 -7
  211. package/lib/mail-journal.js +13 -9
  212. package/lib/mail-mdn.js +12 -7
  213. package/lib/mail-rbl.js +7 -12
  214. package/lib/mail-scan.js +5 -7
  215. package/lib/mail-send-deliver.js +33 -20
  216. package/lib/mail-server-imap.js +32 -87
  217. package/lib/mail-server-jmap.js +39 -52
  218. package/lib/mail-server-managesieve.js +29 -83
  219. package/lib/mail-server-mx.js +33 -74
  220. package/lib/mail-server-net.js +177 -0
  221. package/lib/mail-server-pop3.js +30 -83
  222. package/lib/mail-server-rate-limit.js +30 -6
  223. package/lib/mail-server-registry.js +1 -1
  224. package/lib/mail-server-submission.js +34 -73
  225. package/lib/mail-server-tls.js +98 -2
  226. package/lib/mail-spam-score.js +9 -12
  227. package/lib/mail-store-fts.js +1 -1
  228. package/lib/mail-store.js +3 -2
  229. package/lib/mail.js +28 -13
  230. package/lib/markup-escape.js +31 -0
  231. package/lib/markup-tokenizer.js +78 -0
  232. package/lib/mcp-tool-registry.js +26 -23
  233. package/lib/mcp.js +7 -5
  234. package/lib/mdoc.js +36 -14
  235. package/lib/metrics.js +46 -33
  236. package/lib/middleware/age-gate.js +3 -23
  237. package/lib/middleware/api-encrypt.js +13 -24
  238. package/lib/middleware/assetlinks.js +2 -7
  239. package/lib/middleware/bearer-auth.js +2 -1
  240. package/lib/middleware/body-parser.js +41 -52
  241. package/lib/middleware/bot-disclose.js +7 -10
  242. package/lib/middleware/bot-guard.js +28 -28
  243. package/lib/middleware/clear-site-data.js +5 -1
  244. package/lib/middleware/compression.js +9 -0
  245. package/lib/middleware/cors.js +32 -23
  246. package/lib/middleware/csrf-protect.js +73 -23
  247. package/lib/middleware/daily-byte-quota.js +12 -30
  248. package/lib/middleware/deny-response.js +19 -1
  249. package/lib/middleware/fetch-metadata.js +35 -4
  250. package/lib/middleware/idempotency-key.js +4 -20
  251. package/lib/middleware/network-allowlist.js +61 -30
  252. package/lib/middleware/rate-limit.js +45 -44
  253. package/lib/middleware/scim-server.js +5 -3
  254. package/lib/middleware/security-headers.js +33 -7
  255. package/lib/middleware/security-txt.js +2 -6
  256. package/lib/middleware/speculation-rules.js +6 -3
  257. package/lib/middleware/tus-upload.js +14 -29
  258. package/lib/middleware/web-app-manifest.js +2 -7
  259. package/lib/migrations.js +4 -13
  260. package/lib/mime-parse.js +34 -17
  261. package/lib/module-loader.js +44 -0
  262. package/lib/money.js +105 -0
  263. package/lib/mtls-ca.js +125 -41
  264. package/lib/network-byte-quota.js +4 -18
  265. package/lib/network-dane.js +8 -6
  266. package/lib/network-dns-resolver.js +98 -7
  267. package/lib/network-dns.js +9 -2
  268. package/lib/network-dnssec.js +18 -17
  269. package/lib/network-heartbeat.js +3 -2
  270. package/lib/network-smtp-policy.js +52 -46
  271. package/lib/network-tls.js +67 -47
  272. package/lib/network-tsig.js +2 -2
  273. package/lib/nis2-report.js +2 -12
  274. package/lib/nist-crosswalk.js +1 -1
  275. package/lib/nonce-store.js +81 -3
  276. package/lib/ntp-check.js +28 -0
  277. package/lib/numeric-bounds.js +31 -8
  278. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  279. package/lib/object-store/azure-blob.js +6 -47
  280. package/lib/object-store/gcs-bucket-ops.js +4 -2
  281. package/lib/object-store/gcs.js +14 -75
  282. package/lib/object-store/http-put.js +2 -7
  283. package/lib/object-store/http-request.js +157 -0
  284. package/lib/object-store/local.js +37 -17
  285. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  286. package/lib/object-store/sigv4.js +10 -79
  287. package/lib/observability-otlp-exporter.js +29 -29
  288. package/lib/observability.js +95 -0
  289. package/lib/openapi-paths-builder.js +7 -3
  290. package/lib/otel-export.js +7 -10
  291. package/lib/outbox.js +54 -41
  292. package/lib/pagination.js +11 -15
  293. package/lib/parsers/safe-env.js +5 -4
  294. package/lib/parsers/safe-ini.js +10 -4
  295. package/lib/parsers/safe-toml.js +11 -9
  296. package/lib/parsers/safe-xml.js +3 -3
  297. package/lib/parsers/safe-yaml.js +28 -9
  298. package/lib/pick.js +63 -5
  299. package/lib/pipl-cn.js +69 -59
  300. package/lib/privacy-pass.js +8 -6
  301. package/lib/problem-details.js +2 -5
  302. package/lib/pubsub.js +4 -8
  303. package/lib/queue-local.js +10 -3
  304. package/lib/redact.js +9 -4
  305. package/lib/render.js +4 -2
  306. package/lib/request-helpers.js +334 -29
  307. package/lib/resource-access-lock.js +3 -3
  308. package/lib/restore-bundle.js +46 -18
  309. package/lib/restore-rollback.js +10 -4
  310. package/lib/restore.js +24 -22
  311. package/lib/retention.js +23 -9
  312. package/lib/router.js +17 -4
  313. package/lib/safe-async.js +457 -0
  314. package/lib/safe-buffer.js +137 -6
  315. package/lib/safe-decompress.js +2 -1
  316. package/lib/safe-dns.js +2 -1
  317. package/lib/safe-ical.js +14 -28
  318. package/lib/safe-icap.js +1 -1
  319. package/lib/safe-json.js +51 -8
  320. package/lib/safe-jsonpath.js +6 -5
  321. package/lib/safe-mime.js +25 -29
  322. package/lib/safe-redirect.js +2 -5
  323. package/lib/safe-schema.js +7 -6
  324. package/lib/safe-sieve.js +1 -1
  325. package/lib/safe-sql.js +126 -0
  326. package/lib/safe-vcard.js +13 -27
  327. package/lib/sandbox-worker.js +15 -2
  328. package/lib/sandbox.js +4 -3
  329. package/lib/scheduler.js +22 -13
  330. package/lib/sec-cyber.js +82 -37
  331. package/lib/seeders.js +4 -11
  332. package/lib/self-update-standalone-verifier.js +16 -0
  333. package/lib/self-update.js +92 -69
  334. package/lib/session-device-binding.js +112 -66
  335. package/lib/session.js +219 -7
  336. package/lib/sql.js +228 -81
  337. package/lib/sse.js +6 -10
  338. package/lib/static.js +201 -111
  339. package/lib/storage.js +4 -2
  340. package/lib/structured-fields.js +275 -16
  341. package/lib/template.js +7 -5
  342. package/lib/tenant-quota.js +53 -38
  343. package/lib/tsa.js +16 -17
  344. package/lib/validate-opts.js +154 -8
  345. package/lib/vault/index.js +5 -0
  346. package/lib/vault/passphrase-ops.js +22 -26
  347. package/lib/vault/passphrase-source.js +8 -3
  348. package/lib/vault/rotate.js +13 -18
  349. package/lib/vault/seal-pem-file.js +34 -49
  350. package/lib/vault-aad.js +3 -2
  351. package/lib/vc.js +3 -8
  352. package/lib/vendor/MANIFEST.json +10 -10
  353. package/lib/vendor/public-suffix-list.dat +23 -10
  354. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  355. package/lib/vex.js +35 -13
  356. package/lib/web-push-vapid.js +23 -20
  357. package/lib/webhook-dispatcher.js +706 -0
  358. package/lib/webhook.js +59 -19
  359. package/lib/websocket-channels.js +9 -7
  360. package/lib/websocket.js +8 -4
  361. package/lib/worm.js +3 -4
  362. package/lib/ws-client.js +65 -56
  363. package/lib/x509-chain.js +44 -0
  364. package/package.json +1 -1
  365. package/sbom.cdx.json +6 -6
package/lib/mail-auth.js CHANGED
@@ -46,7 +46,10 @@ var zlib = require("node:zlib");
46
46
  var net = require("node:net");
47
47
  var nodeCrypto = require("node:crypto");
48
48
  var lazyRequire = require("./lazy-require");
49
+ var safeBuffer = require("./safe-buffer");
49
50
  var validateOpts = require("./validate-opts");
51
+ var structuredFields = require("./structured-fields");
52
+ var markupEscape = require("./markup-escape").markupEscape;
50
53
  var bCrypto = require("./crypto");
51
54
  var C = require("./constants");
52
55
  var dkim = require("./mail-dkim");
@@ -109,22 +112,11 @@ function _getDefaultResolver() {
109
112
  return _defaultResolver;
110
113
  }
111
114
 
115
+ // TXT resolution reuses the shared reshape in networkDnsResolver.resolveTxt,
116
+ // passing this module's own resolver so TXT shares the resolver (and cache)
117
+ // used for A / MX / PTR below.
112
118
  async function _safeResolveTxt(qname, operatorLookup) {
113
- if (operatorLookup) return operatorLookup(qname, "TXT");
114
- var r = await _getDefaultResolver().queryTxt(qname);
115
- var out = [];
116
- for (var i = 0; i < r.rrs.length; i += 1) {
117
- var rr = r.rrs[i];
118
- if (rr && rr.type === 16) { // IANA DNS qtype TXT
119
- out.push(Array.isArray(rr.decoded) ? rr.decoded : [String(rr.decoded)]);
120
- }
121
- }
122
- if (out.length === 0) {
123
- var err = new Error("no TXT records for " + qname);
124
- err.code = "ENODATA";
125
- throw err;
126
- }
127
- return out;
119
+ return networkDnsResolver().resolveTxt(qname, operatorLookup, _getDefaultResolver());
128
120
  }
129
121
 
130
122
  async function _safeResolveA(qname, family /* 4|6 */, operatorLookup) {
@@ -1082,15 +1074,11 @@ async function _spfEvaluateDomain(domain, ip, dnsLookup, lookups, ctx) {
1082
1074
 
1083
1075
  async function _fetchDmarcRecord(domain, dnsLookup) {
1084
1076
  var qname = "_dmarc." + domain.toLowerCase();
1085
- var records;
1086
- try {
1087
- records = await _safeResolveTxt(qname, dnsLookup);
1088
- } catch (e) {
1089
- if (e && (e.code === "ENOTFOUND" || e.code === "ENODATA")) return null;
1090
- throw new MailAuthError("mail-auth/dmarc-lookup-failed",
1091
- "DMARC TXT lookup for " + qname + " failed: " +
1092
- ((e && e.message) || String(e)));
1093
- }
1077
+ var records = await networkDnsResolver().safeResolveTxt(qname, {
1078
+ dnsLookup: dnsLookup,
1079
+ errorFactory: function (code, msg) { return new MailAuthError(code, msg); },
1080
+ code: "mail-auth/dmarc-lookup-failed",
1081
+ });
1094
1082
  if (!Array.isArray(records)) return null;
1095
1083
  var matches = [];
1096
1084
  for (var i = 0; i < records.length; i += 1) {
@@ -1120,14 +1108,12 @@ var DMARCBIS_VALID_PSD = { y: 1, n: 1, u: 1 };
1120
1108
  function _parseDmarcRecord(text) {
1121
1109
  var policy = { v: null, p: null, sp: null, np: null, psd: null,
1122
1110
  pct: 100, adkim: "r", aspf: "r" }; // RFC 7489 default pct
1123
- var pairs = text.split(";"); // allow:bare-split-on-quoted-header — RFC 7489 §6.4 DMARC tag-list grammar: `tag-spec *( ";" tag-spec )` with tag-value = 0*( tval *( WSP / FWS ) ); NO quoted-string allowed
1111
+ // RFC 7489 §6.4 DMARC tag-list grammar: `tag-spec *( ";" tag-spec )`,
1112
+ // tag-value carries NO quoted-string — the naive split is correct.
1113
+ var pairs = structuredFields.parseTagList(text);
1124
1114
  for (var i = 0; i < pairs.length; i += 1) {
1125
- var kv = pairs[i].trim();
1126
- if (kv.length === 0) continue;
1127
- var eq = kv.indexOf("=");
1128
- if (eq === -1) continue;
1129
- var key = kv.slice(0, eq).trim().toLowerCase();
1130
- var val = kv.slice(eq + 1).trim();
1115
+ var key = pairs[i][0];
1116
+ var val = pairs[i][1];
1131
1117
  if (key === "v") policy.v = val;
1132
1118
  else if (key === "p") policy.p = val.toLowerCase();
1133
1119
  else if (key === "sp") policy.sp = val.toLowerCase();
@@ -1136,7 +1122,7 @@ function _parseDmarcRecord(text) {
1136
1122
  else if (key === "aspf") policy.aspf = val.toLowerCase();
1137
1123
  else if (key === "np") {
1138
1124
  var npVal = val.toLowerCase();
1139
- if (!DMARCBIS_VALID_NP[npVal]) {
1125
+ if (!Object.prototype.hasOwnProperty.call(DMARCBIS_VALID_NP, npVal)) {
1140
1126
  throw new MailAuthError("mail-auth/dmarcbis-bad-tag",
1141
1127
  "DMARC np= must be one of none|quarantine|reject, got " + JSON.stringify(val));
1142
1128
  }
@@ -1144,7 +1130,7 @@ function _parseDmarcRecord(text) {
1144
1130
  }
1145
1131
  else if (key === "psd") {
1146
1132
  var psdVal = val.toLowerCase();
1147
- if (!DMARCBIS_VALID_PSD[psdVal]) {
1133
+ if (!Object.prototype.hasOwnProperty.call(DMARCBIS_VALID_PSD, psdVal)) {
1148
1134
  throw new MailAuthError("mail-auth/dmarcbis-bad-tag",
1149
1135
  "DMARC psd= must be one of y|n|u, got " + JSON.stringify(val));
1150
1136
  }
@@ -1417,10 +1403,10 @@ async function arcVerify(rfc822, opts) {
1417
1403
  var orderTrail = []; // [{ inst, name, idx }]
1418
1404
  for (var i = 0; i < headers.length; i += 1) {
1419
1405
  var line = headers[i];
1420
- var colonAt = line.indexOf(":");
1421
- if (colonAt === -1) continue;
1422
- var name = line.slice(0, colonAt).trim().toLowerCase();
1423
- var value = line.slice(colonAt + 1).trim();
1406
+ var khv = structuredFields.parseKeyValuePiece(line, ":");
1407
+ if (khv.value === null) continue;
1408
+ var name = khv.key;
1409
+ var value = khv.value.trim();
1424
1410
  if (name !== "arc-seal" && name !== "arc-message-signature" &&
1425
1411
  name !== "arc-authentication-results") continue;
1426
1412
  // ARC hop instance per RFC 8617 §4.2.1 — bounded to 3 digits; the
@@ -1734,9 +1720,9 @@ async function _verifyAmsViaDkim(rfc822, hop, sigValue, tags, dkim, dnsLookup) {
1734
1720
  var rebuilt = [];
1735
1721
  for (var i = 0; i < headerLines.length; i += 1) {
1736
1722
  var line = headerLines[i];
1737
- var colonAt = line.indexOf(":");
1738
- if (colonAt === -1) { rebuilt.push(line); continue; }
1739
- var name = line.slice(0, colonAt).trim().toLowerCase();
1723
+ var khv = structuredFields.parseKeyValuePiece(line, ":");
1724
+ if (khv.value === null) { rebuilt.push(line); continue; }
1725
+ var name = khv.key;
1740
1726
  if (name === "arc-message-signature" ||
1741
1727
  name === "arc-seal" ||
1742
1728
  name === "dkim-signature") {
@@ -1747,7 +1733,7 @@ async function _verifyAmsViaDkim(rfc822, hop, sigValue, tags, dkim, dnsLookup) {
1747
1733
  // canonicalizes it via h=). Pre-v0.8.17 stripped every AAR
1748
1734
  // unconditionally, breaking verification on chains that
1749
1735
  // included AAR in h= (Microsoft + Google interop).
1750
- var instMatch = /\bi\s*=\s*(\d+)/.exec(line.slice(colonAt + 1));
1736
+ var instMatch = /\bi\s*=\s*(\d+)/.exec(khv.value);
1751
1737
  if (!instMatch || parseInt(instMatch[1], 10) !== hop.instance) continue;
1752
1738
  }
1753
1739
  rebuilt.push(line);
@@ -1763,16 +1749,11 @@ async function _verifyAmsViaDkim(rfc822, hop, sigValue, tags, dkim, dnsLookup) {
1763
1749
  }
1764
1750
 
1765
1751
  function _parseArcTagList(value) {
1752
+ // RFC 8617 §4 ARC tag-list grammar (same as DKIM's): `tag-spec *( ";"
1753
+ // tag-spec )`, tag-value contains no DQUOTE, FWS inside a value ignored.
1754
+ var pairs = structuredFields.parseTagList(value, { stripValueWs: true });
1766
1755
  var tags = {};
1767
- var parts = String(value).split(";"); // allow:bare-split-on-quoted-header RFC 8617 §4 ARC tag-list grammar (same as the DKIM RFC's): `tag-spec *( ";" tag-spec )`, tag-value contains no DQUOTE
1768
-
1769
- for (var i = 0; i < parts.length; i += 1) {
1770
- var p = parts[i].trim();
1771
- if (p.length === 0) continue;
1772
- var eq = p.indexOf("=");
1773
- if (eq === -1) continue;
1774
- tags[p.slice(0, eq).trim().toLowerCase()] = p.slice(eq + 1).trim().replace(/\s+/g, "");
1775
- }
1756
+ for (var i = 0; i < pairs.length; i += 1) tags[pairs[i][0]] = pairs[i][1];
1776
1757
  return tags;
1777
1758
  }
1778
1759
 
@@ -1791,11 +1772,9 @@ function _parseDkimKeyRecord(records) {
1791
1772
  }
1792
1773
 
1793
1774
  function _canonRelaxedHeader(name, value) {
1794
- // RFC 6376 §3.4.2 relaxed header canon: lowercase name, unfold,
1795
- // collapse internal WSP runs, strip trailing WSP.
1796
- var unfolded = String(value).replace(/\r?\n[ \t]+/g, " ");
1797
- var trimmed = unfolded.replace(/[ \t]+/g, " ").replace(/^[ \t]+|[ \t]+$/g, "");
1798
- return name.toLowerCase() + ":" + trimmed + "\r\n";
1775
+ // RFC 6376 §3.4.2 relaxed header canon shared with the DKIM signer/verifier
1776
+ // so the DMARC/ARC paths reach a byte-identical canon (RFC 8617 §5.1.1).
1777
+ return dkim.canonHeaderRelaxed(name, value);
1799
1778
  }
1800
1779
 
1801
1780
  function _pemFromB64KeyMaterial(b64) {
@@ -1897,10 +1876,10 @@ async function arcEvaluate(rfc822, opts) {
1897
1876
  var hopAr = {};
1898
1877
  for (var hi = 0; hi < headers.length; hi += 1) {
1899
1878
  var line = headers[hi];
1900
- var colonAt = line.indexOf(":");
1901
- if (colonAt === -1) continue;
1902
- var name = line.slice(0, colonAt).trim().toLowerCase();
1903
- var value = line.slice(colonAt + 1).trim();
1879
+ var khv = structuredFields.parseKeyValuePiece(line, ":");
1880
+ if (khv.value === null) continue;
1881
+ var name = khv.key;
1882
+ var value = khv.value.trim();
1904
1883
  if (name === "arc-seal") {
1905
1884
  var iMatch = value.match(/(?:^|[;,\s])i=(\d+)/); // allow:regex-no-length-cap — header bounded by RFC 5322 998
1906
1885
  var dMatch = value.match(/(?:^|[;,\s])d=([^\s;]+)/); // allow:regex-no-length-cap — header bounded by RFC 5322 998
@@ -2038,7 +2017,7 @@ function authResultsEmit(opts) {
2038
2017
  }
2039
2018
  var method = String(r.method || "").toLowerCase();
2040
2019
  var result = String(r.result || "").toLowerCase();
2041
- if (!AR_VALID_METHODS[method]) {
2020
+ if (!Object.prototype.hasOwnProperty.call(AR_VALID_METHODS, method)) {
2042
2021
  throw new MailAuthError("mail-auth/ar-bad-method",
2043
2022
  "authResults.emit: unknown method '" + r.method + "'");
2044
2023
  }
@@ -2167,7 +2146,7 @@ function _countFromAuthors(value) {
2167
2146
  // (RFC 7489 §6.6.1 — a multi-author From is the header-duplication
2168
2147
  // spoofing shape and must not have "one" author picked from it).
2169
2148
  function _extractFromHeaders(headerBlock) {
2170
- var unfolded = headerBlock.replace(/\r?\n[ \t]+/g, " ");
2149
+ var unfolded = structuredFields.unfoldHeaderContinuations(headerBlock);
2171
2150
  var lines = unfolded.split(/\r?\n/);
2172
2151
  var fromValues = [];
2173
2152
  for (var i = 0; i < lines.length; i += 1) {
@@ -2552,12 +2531,7 @@ function _shapeAggregateReport(parsed) {
2552
2531
  // before they reach here, but escaping is applied uniformly so a future
2553
2532
  // caller can't bypass it.
2554
2533
  function _xmlEscapeText(value) {
2555
- return String(value)
2556
- .replace(/&/g, "&amp;")
2557
- .replace(/</g, "&lt;")
2558
- .replace(/>/g, "&gt;")
2559
- .replace(/"/g, "&quot;")
2560
- .replace(/'/g, "&apos;");
2534
+ return markupEscape(value, { apos: "&apos;" });
2561
2535
  }
2562
2536
 
2563
2537
  // Emit `<tag>escaped-text</tag>` when value is non-null/defined; emit
@@ -2984,9 +2958,9 @@ function dmarcParseForensicReport(input, opts) {
2984
2958
  var maxBytes = (typeof opts.maxBytes === "number" && isFinite(opts.maxBytes) && opts.maxBytes > 0)
2985
2959
  ? opts.maxBytes
2986
2960
  : DMARC_RUF_MAX_REPORT_BYTES;
2987
- if (asString.length > maxBytes) {
2961
+ if (safeBuffer.byteLengthOf(asString) > maxBytes) {
2988
2962
  return _rufError("mail-auth/dmarc-ruf-too-large",
2989
- "dmarc.parseForensicReport: report exceeds " + maxBytes + " bytes (got " + asString.length + ")");
2963
+ "dmarc.parseForensicReport: report exceeds " + maxBytes + " bytes (got " + safeBuffer.byteLengthOf(asString) + ")");
2990
2964
  }
2991
2965
 
2992
2966
  // ---- Bisect top-level headers / body; require multipart/report ----
package/lib/mail-bimi.js CHANGED
@@ -48,15 +48,17 @@
48
48
  * RFC 9091 BIMI policy lookup, VMC + CMC fetch + chain validation, and Tiny-PS SVG profile enforcement for inbox brand-mark rendering.
49
49
  */
50
50
 
51
- var dns = require("node:dns");
52
51
  var nodeCrypto = require("node:crypto");
53
- var dnsPromises = dns.promises;
54
52
 
55
53
  var asn1 = require("./asn1-der");
56
54
  var C = require("./constants");
57
55
  var httpClient = require("./http-client");
58
56
  var lazyRequire = require("./lazy-require");
57
+ var networkDnsResolver = lazyRequire(function () { return require("./network-dns-resolver"); });
59
58
  var safeBuffer = require("./safe-buffer");
59
+ var markupTokenizer = require("./markup-tokenizer");
60
+ var x509Chain = require("./x509-chain");
61
+ var structuredFields = require("./structured-fields");
60
62
  var safeUrl = require("./safe-url");
61
63
  var validateOpts = require("./validate-opts");
62
64
  var { defineClass, MailBimiError } = require("./framework-error");
@@ -211,17 +213,12 @@ function recordShape(opts) {
211
213
  function parseRecord(text) {
212
214
  if (typeof text !== "string" || text.length === 0) return null;
213
215
  if (text.length > BIMI_RECORD_MAX_BYTES) return null;
214
- // RFC 9091 4 - semicolon-separated, key=value, leading "v=BIMI1".
215
- var parts = text.split(";");
216
+ // RFC 9091 §4 semicolon-separated key=value (no DQUOTE), leading "v=BIMI1".
217
+ var pairs = structuredFields.parseTagList(text);
216
218
  var rv = { v: null, l: null, a: null };
217
- for (var i = 0; i < parts.length; i += 1) {
218
- var p = parts[i].trim();
219
- if (p.length === 0) continue;
220
- var eq = p.indexOf("=");
221
- if (eq === -1) continue;
222
- var k = p.slice(0, eq).trim().toLowerCase();
223
- var v = p.slice(eq + 1).trim();
224
- if (k === "v" || k === "l" || k === "a") rv[k] = v;
219
+ for (var i = 0; i < pairs.length; i += 1) {
220
+ var k = pairs[i][0];
221
+ if (k === "v" || k === "l" || k === "a") rv[k] = pairs[i][1];
225
222
  }
226
223
  if (rv.v !== BIMI_VERSION || !rv.l) return null;
227
224
  return rv;
@@ -264,16 +261,13 @@ async function fetchPolicy(domain, opts) {
264
261
  opts = opts || {};
265
262
  var selector = opts.selector || BIMI_DEFAULT_SELECTOR;
266
263
  var qname = selector + "._bimi." + domain;
267
- var records;
268
- try {
269
- if (opts.dnsLookup) records = await opts.dnsLookup(qname, "TXT");
270
- else records = await dnsPromises.resolveTxt(qname);
271
- } catch (e) {
272
- if (e && (e.code === "ENOTFOUND" || e.code === "ENODATA")) return null;
273
- throw new BimiError("mail-bimi/lookup-failed",
274
- "bimi.fetchPolicy: TXT lookup for " + qname + " failed: " +
275
- ((e && e.message) || String(e)));
276
- }
264
+ // Secure DNS path (DoH/DNSSEC via the framework resolver) — not plaintext
265
+ // dnsPromises, which a spoofed BIMI TXT could exploit to mislead VMC discovery.
266
+ var records = await networkDnsResolver().safeResolveTxt(qname, {
267
+ dnsLookup: opts.dnsLookup,
268
+ errorFactory: function (code, msg) { return new BimiError(code, msg); },
269
+ code: "mail-bimi/lookup-failed",
270
+ });
277
271
  // RFC 9091 4.1 - a TXT lookup may return multiple chunks; pick
278
272
  // the first record that begins with v=BIMI1.
279
273
  for (var i = 0; i < (records || []).length; i += 1) {
@@ -406,7 +400,7 @@ function validateTinyPsSvg(svgBytes) {
406
400
  }
407
401
  // Any element name starting with "animate" is animation (covers
408
402
  // future SMIL extensions not in the static list above).
409
- if (name.indexOf("animate") === 0 && !TINY_PS_FORBIDDEN_TAGS[name]) {
403
+ if (name.indexOf("animate") === 0 && !Object.prototype.hasOwnProperty.call(TINY_PS_FORBIDDEN_TAGS, name)) {
410
404
  _vio("element-forbidden",
411
405
  "<" + name + "> animation element is forbidden in Tiny-PS");
412
406
  }
@@ -499,10 +493,14 @@ function _tokenizeTinyPsSvg(s) {
499
493
  if (lt > pos) tokens.push({ type: "text", raw: s.slice(pos, lt) });
500
494
 
501
495
  if (s.startsWith("<!--", lt)) {
502
- var endC = s.indexOf("-->", lt + 4);
496
+ // WHATWG comment end ("--!>" + abrupt "<!-->" / "<!--->"), matching a
497
+ // browser parsing inline SVG — so an element smuggled past an early
498
+ // terminator can't slip the Tiny-PS validation (mXSS differential).
499
+ // Fails closed on an unterminated comment.
500
+ var endC = markupTokenizer.htmlCommentEnd(s, lt);
503
501
  if (endC === -1) throw new Error("unterminated comment"); // allow:bare-error-throw — caught by outer try/catch and re-thrown as MailBimiError("bimi/svg-tiny-ps-violation")
504
- tokens.push({ type: "comment", raw: s.slice(lt, endC + 3) });
505
- pos = endC + 3;
502
+ tokens.push({ type: "comment", raw: s.slice(lt, endC) });
503
+ pos = endC;
506
504
  continue;
507
505
  }
508
506
  if (s.startsWith("<![CDATA[", lt)) {
@@ -542,32 +540,19 @@ function _tokenizeTinyPsSvg(s) {
542
540
  continue;
543
541
  }
544
542
 
545
- var pp = lt + 1;
546
- var inQuote = "";
547
- while (pp < len) {
548
- var ch = s.charAt(pp);
549
- if (inQuote) {
550
- if (ch === inQuote) inQuote = "";
551
- } else {
552
- if (ch === '"' || ch === "'") inQuote = ch;
553
- else if (ch === ">") break;
554
- }
555
- pp += 1;
556
- }
543
+ var pp = markupTokenizer.scanToTagEnd(s, lt + 1, len);
557
544
  if (pp >= len) throw new Error("unterminated start tag"); // allow:bare-error-throw — caught by outer try/catch and re-thrown as MailBimiError("bimi/svg-tiny-ps-violation")
558
545
  var raw = s.slice(lt, pp + 1);
559
546
  var inner = raw.slice(1, raw.length - 1);
560
547
  var selfClosing = inner.endsWith("/");
561
548
  if (selfClosing) inner = inner.slice(0, inner.length - 1);
562
549
 
563
- var nameMatch = inner.match(/^([A-Za-z][A-Za-z0-9:_-]*)/);
564
- var tagName = nameMatch ? nameMatch[1].toLowerCase() : "";
565
- var attrSrc = nameMatch ? inner.slice(nameMatch[0].length) : "";
550
+ var bimiParts = markupTokenizer.splitTagNameAttrs(inner, /^([A-Za-z][A-Za-z0-9:_-]*)/);
566
551
 
567
552
  tokens.push({
568
553
  type: "tag",
569
- name: tagName,
570
- attrs: _parseTinyPsAttrs(attrSrc),
554
+ name: bimiParts.tagName,
555
+ attrs: _parseTinyPsAttrs(bimiParts.attrSrc),
571
556
  raw: raw,
572
557
  selfClosing: selfClosing,
573
558
  });
@@ -843,11 +828,10 @@ function _verifyCertChain(leaf, intermediates, anchors) {
843
828
 
844
829
  for (var ai = 0; ai < anchors.length; ai += 1) {
845
830
  var anchor = anchors[ai];
846
- if (current.checkIssued(anchor)) {
847
- try {
848
- if (current.verify(anchor.publicKey)) return { ok: true };
849
- } catch (_e) { /* fall through to next anchor */ }
850
- }
831
+ // issuerValidlyIssued enforces basicConstraints cA:TRUE on the anchor
832
+ // in addition to the issuance + signature checks, so a non-CA cert
833
+ // cannot mint a VMC (basicConstraints bypass, CVE-2002-0862 class).
834
+ if (x509Chain.issuerValidlyIssued(anchor, current)) return { ok: true };
851
835
  }
852
836
  if (current.checkIssued(current)) {
853
837
  return { ok: false, reason: "self-signed root not in trust-anchor bundle" };
@@ -857,13 +841,9 @@ function _verifyCertChain(leaf, intermediates, anchors) {
857
841
  for (var ii = 0; ii < intermediates.length; ii += 1) {
858
842
  var cand = intermediates[ii];
859
843
  if (cand === current) continue;
860
- if (current.checkIssued(cand)) {
861
- try {
862
- if (current.verify(cand.publicKey)) {
863
- nextIssuer = cand;
864
- break;
865
- }
866
- } catch (_e) { /* fall through */ }
844
+ if (x509Chain.issuerValidlyIssued(cand, current)) {
845
+ nextIssuer = cand;
846
+ break;
867
847
  }
868
848
  }
869
849
  if (nextIssuer === null) {
@@ -448,7 +448,7 @@ function handler(opts) {
448
448
  opts = opts || {};
449
449
  var vendor = opts.vendor;
450
450
  var customParser = typeof opts.parser === "function" ? opts.parser : null;
451
- if (!customParser && (typeof vendor !== "string" || !VENDORS[vendor])) {
451
+ if (!customParser && (typeof vendor !== "string" || !Object.prototype.hasOwnProperty.call(VENDORS, vendor))) {
452
452
  throw _err("handler/bad-config",
453
453
  "mailBounce.handler requires { vendor } (one of " +
454
454
  Object.keys(VENDORS).join(", ") + ") or { parser }");
@@ -761,7 +761,7 @@ function _parseDsn(rawMessage) {
761
761
  throw _err("bounce/dsn-malformed",
762
762
  "mailBounce.dsn.parse: per-recipient group missing Final-Recipient");
763
763
  }
764
- if (action && !DSN_ACTIONS[action]) {
764
+ if (action && !Object.prototype.hasOwnProperty.call(DSN_ACTIONS, action)) {
765
765
  throw _err("bounce/dsn-malformed",
766
766
  "mailBounce.dsn.parse: Action token '" + action + "' is not RFC 3464 §2.3.3");
767
767
  }
@@ -825,7 +825,7 @@ function _buildDsn(opts) {
825
825
  validateOpts.requireNonEmptyString(opts.finalRecipient,
826
826
  "mailBounce.dsn.build: opts.finalRecipient", MailBounceError, "bounce/dsn-malformed");
827
827
  var action = String(opts.action || "failed").toLowerCase();
828
- if (!DSN_ACTIONS[action]) {
828
+ if (!Object.prototype.hasOwnProperty.call(DSN_ACTIONS, action)) {
829
829
  throw _err("bounce/dsn-malformed",
830
830
  "mailBounce.dsn.build: opts.action must be one of " +
831
831
  Object.keys(DSN_ACTIONS).join(" / ") + "; got '" + action + "'");
@@ -113,6 +113,7 @@
113
113
  * band fingerprint pinning is the operator's responsibility)
114
114
  */
115
115
  var lazyRequire = require("./lazy-require");
116
+ var safeBuffer = require("./safe-buffer");
116
117
  var audit = lazyRequire(function () { return require("./audit"); });
117
118
  var nodeCrypto = require("node:crypto");
118
119
  var validateOpts = require("./validate-opts");
@@ -1168,7 +1169,7 @@ function wkdFetch(email, opts) {
1168
1169
  var urls = wkdComputeUrl(email, { advancedHost: opts.advancedHost });
1169
1170
  return Promise.resolve(opts.httpsGet(urls.direct)).then(function (resp) {
1170
1171
  if (resp && resp.status === 200 && Buffer.isBuffer(resp.body) && resp.body.length > 0) { // HTTP 200
1171
- if (resp.body.length > maxBytes) {
1172
+ if (safeBuffer.byteLengthOf(resp.body) > maxBytes) {
1172
1173
  throw new MailCryptoError("mail-crypto/pgp/wkd-too-large",
1173
1174
  "wkd.fetch: key bytes " + resp.body.length + " exceed maxKeyBytes=" + maxBytes);
1174
1175
  }
@@ -1176,7 +1177,7 @@ function wkdFetch(email, opts) {
1176
1177
  }
1177
1178
  return Promise.resolve(opts.httpsGet(urls.advanced)).then(function (resp2) {
1178
1179
  if (resp2 && resp2.status === 200 && Buffer.isBuffer(resp2.body) && resp2.body.length > 0) { // HTTP 200
1179
- if (resp2.body.length > maxBytes) {
1180
+ if (safeBuffer.byteLengthOf(resp2.body) > maxBytes) {
1180
1181
  throw new MailCryptoError("mail-crypto/pgp/wkd-too-large",
1181
1182
  "wkd.fetch: key bytes " + resp2.body.length + " exceed maxKeyBytes=" + maxBytes);
1182
1183
  }
@@ -98,6 +98,7 @@ var lazyRequire = require("./lazy-require");
98
98
  var audit = lazyRequire(function () { return require("./audit"); });
99
99
  var nodeCrypto = require("node:crypto");
100
100
  var validateOpts = require("./validate-opts");
101
+ var x509Chain = require("./x509-chain");
101
102
  var cms = require("./cms-codec");
102
103
  var asn1 = require("./asn1-der");
103
104
  var pqcSoftware = require("./pqc-software");
@@ -297,7 +298,8 @@ function verify(opts) {
297
298
  // verification failure (bad alg, missing signed-attrs, message-
298
299
  // digest mismatch, signature mismatch); reaching the next line
299
300
  // means the per-signer verify succeeded.
300
- var siResult = _verifySignerInfo(sd.signerInfos[0], msgBytes, opts.signerPublicKey, opts.audit);
301
+ var siResult = _verifySignerInfo(sd.signerInfos[0], msgBytes, opts.signerPublicKey, opts.audit,
302
+ sd.encapContent && sd.encapContent.eContentType);
301
303
  // Trust-anchor chain validation when operator supplies roots. The
302
304
  // signer cert is in sd.certificates (its serialNumber matches the
303
305
  // sid's serialNumber); intermediate certs are also in
@@ -336,7 +338,7 @@ function verify(opts) {
336
338
  // signer's signature, masking real multi-signer envelopes as a single
337
339
  // false-failure). verifyAll now iterates `sd.signerInfos` directly and
338
340
  // calls this helper per index with the matching per-signer key.
339
- function _verifySignerInfo(si, msgBytes, signerPublicKey, auditHandle) {
341
+ function _verifySignerInfo(si, msgBytes, signerPublicKey, auditHandle, eContentType) {
340
342
  var sigAlg = _oidToSigAlg(si.sigAlgOid);
341
343
  if (!sigAlg) {
342
344
  throw new MailCryptoError("mail-crypto/smime/bad-sig-alg",
@@ -365,6 +367,26 @@ function _verifySignerInfo(si, msgBytes, signerPublicKey, auditHandle) {
365
367
  "smime.verify: recomputed message digest does not match signedAttrs.messageDigest " +
366
368
  "(message was tampered or signed-attrs were swapped)");
367
369
  }
370
+ // RFC 5652 §11.1: when signedAttrs are present the contentType attribute
371
+ // MUST be present and MUST equal the SignedData eContentType. The TSA
372
+ // sibling enforces this; S/MIME omitted it, so a SignerInfo with a missing
373
+ // or mismatched contentType still verified — a cross-protocol content-type
374
+ // binding gap. The check runs over signedAttrs, which the signature below
375
+ // covers, so a tampered attribute also fails the signature.
376
+ if (eContentType) {
377
+ var ctOid = _extractContentTypeOid(si.signedAttrsRaw);
378
+ if (!ctOid) {
379
+ _audit(auditHandle, "mail.crypto.smime.verify_fail", "denied", { reason: "no-content-type-attr" });
380
+ throw new MailCryptoError("mail-crypto/smime/no-content-type-attr",
381
+ "smime.verify: signedAttrs missing the contentType attribute (RFC 5652 §11.1)");
382
+ }
383
+ if (ctOid !== eContentType) {
384
+ _audit(auditHandle, "mail.crypto.smime.verify_fail", "denied", { reason: "content-type-mismatch" });
385
+ throw new MailCryptoError("mail-crypto/smime/content-type-mismatch",
386
+ "smime.verify: signedAttrs.contentType (" + ctOid + ") does not match eContentType (" +
387
+ eContentType + ")");
388
+ }
389
+ }
368
390
  var ok;
369
391
  try {
370
392
  ok = sigAlg.pqc.verify(
@@ -471,7 +493,10 @@ function _verifyTrustChain(sd, trustAnchorCertsPem, signerPublicKey, auditHandle
471
493
  // Stop when we've reached a trust anchor.
472
494
  for (var ri = 0; ri < roots.length; ri += 1) {
473
495
  var r = roots[ri];
474
- if (current.issuer === r.subject) {
496
+ // Enforce basicConstraints cA:TRUE on the issuing root in addition to
497
+ // the DN match + signature — a non-CA cert can never be a chain issuer
498
+ // (basicConstraints bypass, CVE-2002-0862 class).
499
+ if (current.issuer === r.subject && x509Chain.isCaCert(r)) {
475
500
  try {
476
501
  if (current.verify(r.publicKey)) {
477
502
  void auditHandle;
@@ -480,12 +505,12 @@ function _verifyTrustChain(sd, trustAnchorCertsPem, signerPublicKey, auditHandle
480
505
  } catch (_e) { /* fall through to next root */ }
481
506
  }
482
507
  }
483
- // Find an intermediate whose subject == current.issuer.
508
+ // Find an intermediate whose subject == current.issuer (and that is a CA).
484
509
  var found = null;
485
510
  for (var hi = 0; hi < chain.length; hi += 1) {
486
511
  var h = chain[hi];
487
512
  if (h === current) continue;
488
- if (h.subject === current.issuer) {
513
+ if (h.subject === current.issuer && x509Chain.isCaCert(h)) {
489
514
  try {
490
515
  if (current.verify(h.publicKey)) { found = h; break; }
491
516
  } catch (_e) { /* try next */ }
@@ -574,7 +599,8 @@ function verifyAll(opts) {
574
599
  throw new MailCryptoError("mail-crypto/smime/missing-key",
575
600
  "verifyAll: no public key supplied for SignerInfo serial " + serialHex);
576
601
  }
577
- var siRes = _verifySignerInfo(si, msgBytes, pub, opts.audit);
602
+ var siRes = _verifySignerInfo(si, msgBytes, pub, opts.audit,
603
+ sd.encapContent && sd.encapContent.eContentType);
578
604
  results.push({
579
605
  serialHex: serialHex,
580
606
  sigAlgOid: si.sigAlgOid,
@@ -621,6 +647,42 @@ function _extractSerialHex(sidBytes) {
621
647
 
622
648
  // RFC 5652 §11.2 messageDigest OID.
623
649
  var OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4";
650
+ // RFC 5652 §11.1 contentType signed-attribute OID.
651
+ var OID_CONTENT_TYPE_ATTR = "1.2.840.113549.1.9.3";
652
+
653
+ // Extract the contentType signed-attribute's OID value from a signedAttrs
654
+ // SET, or null if absent / malformed. Mirrors _extractMessageDigest's walk
655
+ // but the attribute value is an OID (the eContentType), not an OCTET STRING.
656
+ function _extractContentTypeOid(signedAttrsRaw) {
657
+ var node;
658
+ try { node = asn1.readNode(signedAttrsRaw); }
659
+ catch (_e) { return null; }
660
+ if (node.tag !== asn1.TAG.SET) return null;
661
+ var attrs;
662
+ try { attrs = asn1.readSequence(node.value); }
663
+ catch (_e) { return null; }
664
+ for (var i = 0; i < attrs.length; i += 1) {
665
+ var attr = attrs[i];
666
+ if (attr.tag !== asn1.TAG.SEQUENCE) continue;
667
+ var children;
668
+ try { children = asn1.readSequence(attr.value); }
669
+ catch (_e) { continue; }
670
+ if (children.length < 2) continue;
671
+ var oid;
672
+ try { oid = asn1.readOid(children[0]); }
673
+ catch (_e) { continue; }
674
+ if (oid !== OID_CONTENT_TYPE_ATTR) continue;
675
+ var valuesSet = children[1];
676
+ if (valuesSet.tag !== asn1.TAG.SET) continue;
677
+ var valueChildren;
678
+ try { valueChildren = asn1.readSequence(valuesSet.value); }
679
+ catch (_e) { continue; }
680
+ if (valueChildren.length === 0) continue;
681
+ try { return asn1.readOid(valueChildren[0]); }
682
+ catch (_e) { return null; }
683
+ }
684
+ return null;
685
+ }
624
686
 
625
687
  function _extractMessageDigest(signedAttrsRaw) {
626
688
  // signedAttrsRaw is `31 LL VV...` — the universal SET-tagged blob
@@ -821,4 +883,7 @@ module.exports = {
821
883
  // validation (a cert matches the verified signer key iff its SPKI public
822
884
  // key equals, or has as a suffix, the raw signer key bytes).
823
885
  _certKeyMatches: _certKeyMatches,
886
+ // Exposed for tests — RFC 5652 §11.1 contentType signed-attr extraction
887
+ // (verify binds it to the eContentType).
888
+ _extractContentTypeOid: _extractContentTypeOid,
824
889
  };