@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.
- package/CHANGELOG.md +4 -0
- package/index.js +2 -0
- package/lib/a2a-tasks.js +45 -29
- package/lib/acme.js +6 -5
- package/lib/agent-event-bus.js +18 -4
- package/lib/agent-idempotency.js +7 -6
- package/lib/agent-orchestrator.js +2 -5
- package/lib/agent-saga.js +3 -5
- package/lib/agent-snapshot.js +32 -2
- package/lib/agent-tenant.js +2 -5
- package/lib/ai-adverse-decision.js +2 -15
- package/lib/ai-aedt-bias-audit.js +2 -1
- package/lib/ai-capability.js +1 -6
- package/lib/ai-content-detect.js +1 -3
- package/lib/ai-dp.js +1 -5
- package/lib/ai-frontier-protocol.js +1 -1
- package/lib/ai-input.js +2 -2
- package/lib/ai-model-manifest.js +1 -1
- package/lib/ai-output.js +16 -7
- package/lib/ai-pref.js +3 -8
- package/lib/ai-quota.js +3 -14
- package/lib/api-key.js +37 -28
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +7 -1
- package/lib/archive-adapters.js +2 -4
- package/lib/archive-entry-policy.js +32 -0
- package/lib/archive-gz.js +9 -0
- package/lib/archive-read.js +14 -24
- package/lib/archive-tar-read.js +56 -24
- package/lib/archive.js +6 -12
- package/lib/arg-parser.js +7 -6
- package/lib/asn1-der.js +70 -22
- package/lib/asyncapi-traits.js +2 -6
- package/lib/atomic-file.js +312 -33
- package/lib/audit-chain.js +183 -54
- package/lib/audit-daily-review.js +36 -16
- package/lib/audit-emit.js +82 -0
- package/lib/audit-sign.js +258 -0
- package/lib/audit-tools.js +108 -23
- package/lib/audit.js +91 -2
- package/lib/auth/access-lock.js +7 -29
- package/lib/auth/bot-challenge.js +1 -1
- package/lib/auth/ciba.js +43 -4
- package/lib/auth/dpop.js +29 -36
- package/lib/auth/fido-mds3.js +14 -16
- package/lib/auth/jwt-external.js +26 -8
- package/lib/auth/jwt.js +15 -17
- package/lib/auth/lockout.js +24 -27
- package/lib/auth/oauth.js +67 -45
- package/lib/auth/oid4vci.js +55 -32
- package/lib/auth/oid4vp.js +3 -2
- package/lib/auth/openid-federation.js +6 -6
- package/lib/auth/passkey.js +4 -4
- package/lib/auth/password.js +8 -2
- package/lib/auth/saml.js +70 -40
- package/lib/auth/sd-jwt-vc-holder.js +2 -14
- package/lib/auth/sd-jwt-vc-issuer.js +2 -14
- package/lib/auth/sd-jwt-vc.js +25 -5
- package/lib/auth/status-list.js +21 -9
- package/lib/auth/step-up.js +15 -13
- package/lib/auth-bot-challenge.js +27 -39
- package/lib/backup/bundle.js +18 -20
- package/lib/backup/crypto.js +23 -8
- package/lib/backup/index.js +55 -67
- package/lib/backup/manifest.js +7 -1
- package/lib/bounded-map.js +112 -1
- package/lib/breach-deadline.js +1 -11
- package/lib/break-glass.js +41 -22
- package/lib/cache.js +7 -18
- package/lib/cbor.js +36 -12
- package/lib/cdn-cache-control.js +15 -12
- package/lib/cert.js +8 -13
- package/lib/chain-writer.js +162 -47
- package/lib/cli.js +10 -2
- package/lib/client-hints.js +7 -9
- package/lib/cloud-events.js +43 -33
- package/lib/cluster-storage.js +9 -41
- package/lib/cms-codec.js +2 -1
- package/lib/codepoint-class.js +84 -1
- package/lib/compliance-ai-act-logging.js +1 -6
- package/lib/compliance-ai-act-transparency.js +2 -4
- package/lib/compliance-eaa.js +2 -12
- package/lib/compliance-sanctions-fetcher.js +21 -28
- package/lib/compliance-sanctions.js +11 -21
- package/lib/compliance.js +3 -10
- package/lib/config-drift.js +24 -20
- package/lib/content-credentials.js +11 -8
- package/lib/content-digest.js +21 -12
- package/lib/cookies.js +15 -13
- package/lib/cose.js +39 -11
- package/lib/cra-report.js +1 -11
- package/lib/crdt.js +2 -1
- package/lib/crypto-field.js +34 -33
- package/lib/crypto.js +138 -7
- package/lib/csp.js +239 -3
- package/lib/daemon.js +46 -42
- package/lib/data-act.js +46 -13
- package/lib/db-file-lifecycle.js +14 -6
- package/lib/db-query.js +75 -49
- package/lib/db.js +66 -27
- package/lib/dbsc.js +5 -6
- package/lib/ddl-change-control.js +18 -14
- package/lib/deprecate.js +4 -5
- package/lib/did.js +6 -2
- package/lib/dora.js +43 -13
- package/lib/dr-runbook.js +1 -1
- package/lib/dsa.js +2 -1
- package/lib/dsr.js +30 -20
- package/lib/early-hints.js +19 -0
- package/lib/eat.js +5 -1
- package/lib/external-db-migrate.js +21 -22
- package/lib/external-db.js +74 -30
- package/lib/fda-21cfr11.js +42 -13
- package/lib/fdx.js +22 -18
- package/lib/file-upload.js +80 -66
- package/lib/flag-evaluation-context.js +5 -8
- package/lib/flag-providers.js +6 -2
- package/lib/forms.js +1 -1
- package/lib/framework-error.js +12 -0
- package/lib/framework-schema.js +19 -0
- package/lib/fsm.js +7 -12
- package/lib/gate-contract.js +911 -39
- package/lib/gdpr-ropa.js +22 -22
- package/lib/graphql-federation.js +18 -5
- package/lib/guard-agent-registry.js +2 -1
- package/lib/guard-all.js +3 -2
- package/lib/guard-archive.js +9 -30
- package/lib/guard-auth.js +23 -80
- package/lib/guard-cidr.js +20 -96
- package/lib/guard-csv.js +135 -196
- package/lib/guard-domain.js +23 -106
- package/lib/guard-dsn.js +17 -14
- package/lib/guard-email.js +46 -53
- package/lib/guard-envelope.js +2 -2
- package/lib/guard-event-bus-topic.js +2 -1
- package/lib/guard-filename.js +12 -60
- package/lib/guard-graphql.js +28 -75
- package/lib/guard-html-wcag.js +15 -2
- package/lib/guard-html.js +74 -128
- package/lib/guard-idempotency-key.js +2 -1
- package/lib/guard-image.js +280 -77
- package/lib/guard-imap-command.js +9 -10
- package/lib/guard-jmap.js +1 -1
- package/lib/guard-json.js +101 -109
- package/lib/guard-jsonpath.js +20 -88
- package/lib/guard-jwt.js +32 -114
- package/lib/guard-list-id.js +2 -7
- package/lib/guard-list-unsubscribe.js +2 -7
- package/lib/guard-mail-compose.js +5 -6
- package/lib/guard-mail-move.js +3 -2
- package/lib/guard-mail-query.js +5 -3
- package/lib/guard-mail-sieve.js +6 -7
- package/lib/guard-managesieve-command.js +6 -5
- package/lib/guard-markdown.js +76 -110
- package/lib/guard-message-id.js +5 -6
- package/lib/guard-mime.js +20 -99
- package/lib/guard-oauth.js +19 -73
- package/lib/guard-pdf.js +65 -72
- package/lib/guard-pop3-command.js +13 -14
- package/lib/guard-posture-chain.js +2 -1
- package/lib/guard-regex.js +24 -99
- package/lib/guard-saga-config.js +2 -1
- package/lib/guard-shell.js +22 -87
- package/lib/guard-smtp-command.js +9 -12
- package/lib/guard-sql.js +15 -13
- package/lib/guard-stream-args.js +2 -1
- package/lib/guard-svg.js +103 -149
- package/lib/guard-template.js +23 -80
- package/lib/guard-tenant-id.js +2 -1
- package/lib/guard-text.js +592 -0
- package/lib/guard-time.js +27 -95
- package/lib/guard-uuid.js +21 -93
- package/lib/guard-xml.js +76 -106
- package/lib/guard-yaml.js +24 -60
- package/lib/html-balance.js +7 -3
- package/lib/http-client-cache.js +5 -12
- package/lib/http-client-cookie-jar.js +35 -16
- package/lib/http-client.js +233 -74
- package/lib/http-message-signature.js +3 -2
- package/lib/i18n-messageformat.js +5 -7
- package/lib/i18n.js +83 -26
- package/lib/iab-tcf.js +3 -2
- package/lib/importmap-integrity.js +41 -1
- package/lib/inbox.js +8 -8
- package/lib/incident-report.js +12 -27
- package/lib/ip-utils.js +49 -6
- package/lib/jobs.js +3 -2
- package/lib/json-patch.js +1 -1
- package/lib/json-path.js +24 -3
- package/lib/jtd.js +2 -2
- package/lib/keychain.js +6 -18
- package/lib/legal-hold.js +30 -23
- package/lib/log-stream-cloudwatch.js +44 -112
- package/lib/log-stream-otlp-grpc.js +17 -14
- package/lib/log-stream-otlp.js +16 -80
- package/lib/log-stream-webhook.js +20 -92
- package/lib/log.js +2 -2
- package/lib/mail-agent.js +2 -2
- package/lib/mail-arc-sign.js +8 -3
- package/lib/mail-arf.js +4 -3
- package/lib/mail-auth.js +43 -69
- package/lib/mail-bimi.js +35 -55
- package/lib/mail-bounce.js +3 -3
- package/lib/mail-crypto-pgp.js +3 -2
- package/lib/mail-crypto-smime.js +71 -6
- package/lib/mail-dav.js +8 -35
- package/lib/mail-deploy.js +15 -14
- package/lib/mail-dkim.js +19 -38
- package/lib/mail-greylist.js +17 -30
- package/lib/mail-helo.js +4 -7
- package/lib/mail-journal.js +13 -9
- package/lib/mail-mdn.js +12 -7
- package/lib/mail-rbl.js +7 -12
- package/lib/mail-scan.js +5 -7
- package/lib/mail-send-deliver.js +33 -20
- package/lib/mail-server-imap.js +32 -87
- package/lib/mail-server-jmap.js +39 -52
- package/lib/mail-server-managesieve.js +29 -83
- package/lib/mail-server-mx.js +33 -74
- package/lib/mail-server-net.js +177 -0
- package/lib/mail-server-pop3.js +30 -83
- package/lib/mail-server-rate-limit.js +30 -6
- package/lib/mail-server-registry.js +1 -1
- package/lib/mail-server-submission.js +34 -73
- package/lib/mail-server-tls.js +98 -2
- package/lib/mail-spam-score.js +9 -12
- package/lib/mail-store-fts.js +1 -1
- package/lib/mail-store.js +3 -2
- package/lib/mail.js +28 -13
- package/lib/markup-escape.js +31 -0
- package/lib/markup-tokenizer.js +78 -0
- package/lib/mcp-tool-registry.js +26 -23
- package/lib/mcp.js +7 -5
- package/lib/mdoc.js +36 -14
- package/lib/metrics.js +46 -33
- package/lib/middleware/age-gate.js +3 -23
- package/lib/middleware/api-encrypt.js +13 -24
- package/lib/middleware/assetlinks.js +2 -7
- package/lib/middleware/bearer-auth.js +2 -1
- package/lib/middleware/body-parser.js +41 -52
- package/lib/middleware/bot-disclose.js +7 -10
- package/lib/middleware/bot-guard.js +28 -28
- package/lib/middleware/clear-site-data.js +5 -1
- package/lib/middleware/compression.js +9 -0
- package/lib/middleware/cors.js +32 -23
- package/lib/middleware/csrf-protect.js +73 -23
- package/lib/middleware/daily-byte-quota.js +12 -30
- package/lib/middleware/deny-response.js +19 -1
- package/lib/middleware/fetch-metadata.js +35 -4
- package/lib/middleware/idempotency-key.js +4 -20
- package/lib/middleware/network-allowlist.js +61 -30
- package/lib/middleware/rate-limit.js +45 -44
- package/lib/middleware/scim-server.js +5 -3
- package/lib/middleware/security-headers.js +33 -7
- package/lib/middleware/security-txt.js +2 -6
- package/lib/middleware/speculation-rules.js +6 -3
- package/lib/middleware/tus-upload.js +14 -29
- package/lib/middleware/web-app-manifest.js +2 -7
- package/lib/migrations.js +4 -13
- package/lib/mime-parse.js +34 -17
- package/lib/module-loader.js +44 -0
- package/lib/money.js +105 -0
- package/lib/mtls-ca.js +125 -41
- package/lib/network-byte-quota.js +4 -18
- package/lib/network-dane.js +8 -6
- package/lib/network-dns-resolver.js +98 -7
- package/lib/network-dns.js +9 -2
- package/lib/network-dnssec.js +18 -17
- package/lib/network-heartbeat.js +3 -2
- package/lib/network-smtp-policy.js +52 -46
- package/lib/network-tls.js +67 -47
- package/lib/network-tsig.js +2 -2
- package/lib/nis2-report.js +2 -12
- package/lib/nist-crosswalk.js +1 -1
- package/lib/nonce-store.js +81 -3
- package/lib/ntp-check.js +28 -0
- package/lib/numeric-bounds.js +31 -8
- package/lib/object-store/azure-blob-bucket-ops.js +2 -6
- package/lib/object-store/azure-blob.js +6 -47
- package/lib/object-store/gcs-bucket-ops.js +4 -2
- package/lib/object-store/gcs.js +14 -75
- package/lib/object-store/http-put.js +2 -7
- package/lib/object-store/http-request.js +157 -0
- package/lib/object-store/local.js +37 -17
- package/lib/object-store/sigv4-bucket-ops.js +2 -1
- package/lib/object-store/sigv4.js +10 -79
- package/lib/observability-otlp-exporter.js +29 -29
- package/lib/observability.js +95 -0
- package/lib/openapi-paths-builder.js +7 -3
- package/lib/otel-export.js +7 -10
- package/lib/outbox.js +54 -41
- package/lib/pagination.js +11 -15
- package/lib/parsers/safe-env.js +5 -4
- package/lib/parsers/safe-ini.js +10 -4
- package/lib/parsers/safe-toml.js +11 -9
- package/lib/parsers/safe-xml.js +3 -3
- package/lib/parsers/safe-yaml.js +28 -9
- package/lib/pick.js +63 -5
- package/lib/pipl-cn.js +69 -59
- package/lib/privacy-pass.js +8 -6
- package/lib/problem-details.js +2 -5
- package/lib/pubsub.js +4 -8
- package/lib/queue-local.js +10 -3
- package/lib/redact.js +9 -4
- package/lib/render.js +4 -2
- package/lib/request-helpers.js +334 -29
- package/lib/resource-access-lock.js +3 -3
- package/lib/restore-bundle.js +46 -18
- package/lib/restore-rollback.js +10 -4
- package/lib/restore.js +24 -22
- package/lib/retention.js +23 -9
- package/lib/router.js +17 -4
- package/lib/safe-async.js +457 -0
- package/lib/safe-buffer.js +137 -6
- package/lib/safe-decompress.js +2 -1
- package/lib/safe-dns.js +2 -1
- package/lib/safe-ical.js +14 -28
- package/lib/safe-icap.js +1 -1
- package/lib/safe-json.js +51 -8
- package/lib/safe-jsonpath.js +6 -5
- package/lib/safe-mime.js +25 -29
- package/lib/safe-redirect.js +2 -5
- package/lib/safe-schema.js +7 -6
- package/lib/safe-sieve.js +1 -1
- package/lib/safe-sql.js +126 -0
- package/lib/safe-vcard.js +13 -27
- package/lib/sandbox-worker.js +15 -2
- package/lib/sandbox.js +4 -3
- package/lib/scheduler.js +22 -13
- package/lib/sec-cyber.js +82 -37
- package/lib/seeders.js +4 -11
- package/lib/self-update-standalone-verifier.js +16 -0
- package/lib/self-update.js +92 -69
- package/lib/session-device-binding.js +112 -66
- package/lib/session.js +219 -7
- package/lib/sql.js +228 -81
- package/lib/sse.js +6 -10
- package/lib/static.js +201 -111
- package/lib/storage.js +4 -2
- package/lib/structured-fields.js +275 -16
- package/lib/template.js +7 -5
- package/lib/tenant-quota.js +53 -38
- package/lib/tsa.js +16 -17
- package/lib/validate-opts.js +154 -8
- package/lib/vault/index.js +5 -0
- package/lib/vault/passphrase-ops.js +22 -26
- package/lib/vault/passphrase-source.js +8 -3
- package/lib/vault/rotate.js +13 -18
- package/lib/vault/seal-pem-file.js +34 -49
- package/lib/vault-aad.js +3 -2
- package/lib/vc.js +3 -8
- package/lib/vendor/MANIFEST.json +10 -10
- package/lib/vendor/public-suffix-list.dat +23 -10
- package/lib/vendor/public-suffix-list.data.js +5498 -5494
- package/lib/vex.js +35 -13
- package/lib/web-push-vapid.js +23 -20
- package/lib/webhook-dispatcher.js +706 -0
- package/lib/webhook.js +59 -19
- package/lib/websocket-channels.js +9 -7
- package/lib/websocket.js +8 -4
- package/lib/worm.js +3 -4
- package/lib/ws-client.js +65 -56
- package/lib/x509-chain.js +44 -0
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
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
|
|
1126
|
-
|
|
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
|
|
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
|
|
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
|
|
1421
|
-
if (
|
|
1422
|
-
var name =
|
|
1423
|
-
var value =
|
|
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
|
|
1738
|
-
if (
|
|
1739
|
-
var name =
|
|
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(
|
|
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
|
|
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
|
|
1795
|
-
//
|
|
1796
|
-
|
|
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
|
|
1901
|
-
if (
|
|
1902
|
-
var name =
|
|
1903
|
-
var value =
|
|
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
|
|
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 =
|
|
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
|
|
2556
|
-
.replace(/&/g, "&")
|
|
2557
|
-
.replace(/</g, "<")
|
|
2558
|
-
.replace(/>/g, ">")
|
|
2559
|
-
.replace(/"/g, """)
|
|
2560
|
-
.replace(/'/g, "'");
|
|
2534
|
+
return markupEscape(value, { 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
|
|
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
|
|
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
|
|
215
|
-
var
|
|
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 <
|
|
218
|
-
var
|
|
219
|
-
if (
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
505
|
-
pos = endC
|
|
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
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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 (
|
|
861
|
-
|
|
862
|
-
|
|
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) {
|
package/lib/mail-bounce.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 + "'");
|
package/lib/mail-crypto-pgp.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|
package/lib/mail-crypto-smime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|