/* * zdkimfilter - written by ale in milano on Thu 11 Feb 2010 03:48:15 PM CET * Sign outgoing, verify incoming mail messages Copyright (C) 2010-2023 Alessandro Vesely This file is part of zdkimfilter zdkimfilter is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. zdkimfilter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License version 3 along with zdkimfilter. If not, see . Additional permission under GNU GPLv3 section 7: If you modify zdkimfilter, or any covered part of it, by linking or combining it with OpenSSL, OpenDKIM, Sendmail, or any software developed by The Trusted Domain Project or Sendmail Inc., containing parts covered by the applicable licence, the licensor of zdkimfilter grants you additional permission to convey the resulting work. */ #include #if !ZDKIMFILTER_DEBUG #define NDEBUG #endif #include #include #include #include #include #include #include #include #include // for LOG_DEBUG,... constants #include #include // name conflict with older opendkim versions //#define dkim_policy unsupported_dkim_policy #include //undef dkim_policy #include #include #include #include #include #include #include "filterlib.h" #include "filedefs.h" #include "myadsp.h" #include "redact.h" #include "vb_fgets.h" #include "parm.h" #include "database.h" #include "filecopy.h" #include "util.h" #include "publicsuffix.h" #include "spf_result_string.h" #include #include #include "transform.h" #include "cstring.h" #include #if !PATH_MAX #define PATH_MAX 1024 #endif #if !HAVE_RANDOM static inline long random(void) {return rand();} static inline void srandom(unsigned int seed) {srand(seed);} #endif static const char content_type[] = "Content-Type"; static const char content_transfer_encoding[] = "Content-Transfer-Encoding"; static inline char *my_basename(char const *name) // neither GNU nor POSIX... { char *b = strrchr(name, '/'); if (b) return b + 1; return (char*)name; } static char *strdup_normalize(char const *s, int stop_at) { char *copy; // trim left int ch; while ((ch = *(unsigned char const*)s) != 0 && isspace(ch)) ++s; if (ch) { // find terminator char const *t = s; while ((ch = *(unsigned char const*)t) != 0 && ch != stop_at) ++t; // trim right while (t > s && isspace(*(unsigned char const*)(t - 1))) --t; assert(t > s); // duplicate normalizing spaces char *d = copy = malloc(t - s + 1); if (d) { int spaces = 0; while (s < t) { ch = *(unsigned char const *)s++; if (isspace(ch)) { if (spaces++ == 0) *d++ = ' '; } else { spaces = 0; *d++ = ch; } } *d = 0; } } else copy = strdup(""); return copy; } static char* strdup_add(char *augend, char *addend) { unsigned sz = strlen(augend) + strlen(addend) + 3; char *copy = realloc(augend, sz); if (copy) { strcat(strcat(copy, "\r\n"), addend); char *t = ©[sz - 1]; // trim right while (t > copy && isspace(*(unsigned char*)(t - 1))) *--t = 0; } return copy; } #if 0 // until version 3.16 this was called from domain_flag_from() static char* normalize_utf8(char *domain) /* * idn2_to_unicode_8z8z */ { if (domain) { char *out = NULL; if (idn2_to_unicode_8z8z(domain, &out, 0) == IDN2_OK) return out; } return NULL; } #endif static char* normalize_utf8_twice(char *domain) /* * Converting twice seems to also convert to lowercase. */ { if (domain) { char *out = NULL, *out2 = NULL; int rtc = idn2_to_ascii_8z(domain, &out, 0); if (rtc == 0) { int rtc2 = idn2_to_unicode_8z8z(out, &out2, 0); if (rtc2 == 0) { free(out); return out2; } free(out2); } free(out); } return NULL; } static int domain_is_valid(char *domain) /* * return 1 if valid, 0 if invalid */ { int rtc = 0; if (domain) { if (domain[0] == '[') // address-literal return 0; char *out = NULL; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpointer-sign" #pragma GCC diagnostic ignored "-Wincompatible-pointer-types" rtc = idn2_lookup_u8(domain, &out, 0) == IDN2_OK; #pragma GCC diagnostic pop if (rtc) // conversion succeeded { for (char *p = out;;) { int ch = *(unsigned char*)p++; if (isalnum(ch) || ch == '-' || ch == '.') continue; if (ch != 0) // invalid char, non l-d-h rtc = 0; break; } } free(out); } return rtc; } static void clean_stats_info_content(stats_info *stats) // only called by clean_stats { if (stats) { free(stats->content_type); free(stats->content_encoding); free(stats->date); free(stats->message_id); free(stats->from); free(stats->subject); free(stats->envelope_sender); // don't free(stats->ino_mtime_pid); it is in dyn.info stats->ino_mtime_pid = NULL; } } typedef struct blocked_user_list { char *data; size_t size; time_t mtime; } blocked_user_list; static int search_list(blocked_user_list *bul, char const *u) { assert(bul); if (bul->data && bul->size && u) { size_t const ulen = strlen(u); char *p = bul->data; while (p) { while (isspace(*(unsigned char*)p)) ++p; if (*p != '#') { if (strincmp(p, u, ulen) == 0) return 1; // found } p = strchr(p, '\n'); } } return 0; } typedef struct key_choice { dkim_sigkey_t key; char *selector; char *domain; } key_choice; typedef struct per_message_parm { key_choice k; char *authserv_id; char *action_header; char *auth_or_relay; // either info.authsender or info.relayclient char *save_file; stats_info *stats; var_buf vb; fl_msg_info info; int rtc; char is_verifying; // if 0 is signing. char db_connected; char special; // never block outgoing messages to postmaster@domain only. char undo_percent_relay; char no_write; char do_seal; // called by zarcseal char wrapped; // test_mode == fl_wrapped (-t1,z*) } per_message_parm; char dummy_authserv_id[] = "zdkimverify"; typedef enum split_filter { split_do_both, split_verify_only, split_sign_only } split_filter; typedef struct dkimfl_parm { DKIM_LIB *dklib; fl_parm *fl; db_work_area *dwa; publicsuffix_trie *pst, *pst2; char const *config_fname; // possibly allocated in main() char const *prog_name; //static, from argv[0] parm_t z; per_message_parm dyn; blocked_user_list blocklist; // other unsigned int dkim_init_flag; split_filter split; char pid_created; char use_dwa_after_sign, use_dwa_verifying; char user_blocked; } dkimfl_parm; static inline const u_char ** cast_const_u_char_parm_array(const char **a) {return (const u_char **)a;} static inline u_char ** cast_u_char_parm_array(char **a) {return (u_char **)a;} static char const parm_z_domain_keys[] = COURIER_SYSCONF_INSTALL "/filters/keys"; static char const *const parm_z_trusted_dnswl[] = {"list.dnswl.org", NULL}; #define MAX_SIGNATURES 128 static void config_default(dkimfl_parm *parm) // only non-zero... { parm->z.domain_keys = (char*)parm_z_domain_keys; parm->z.verbose = 3; parm->z.max_signatures = MAX_SIGNATURES; parm->z.trusted_dnswl = (char const**)parm_z_trusted_dnswl; // 2 = medium – make sure to avoid false positives but allow override for clear cases (-10.0) parm->z.dnswl_worthiness_pass = 2; parm->z.dnswl_invalid_ip = DNSWL_ORG_INVALID_IP_ENDIAN; parm->z.dnswl_octet_index = 3; parm->z.whitelisted_pass = 3; parm->z.honored_report_interval = DEFAULT_REPORT_INTERVAL; } static void config_cleanup_default(dkimfl_parm *parm) { if (parm->z.domain_keys == parm_z_domain_keys) parm->z.domain_keys = NULL; if (parm->z.trusted_dnswl == parm_z_trusted_dnswl) parm->z.trusted_dnswl = NULL; } static void no_trailing_char(char *s, int slash) { if (s) { size_t l = strlen(s); while (l > 0 && s[l-1] == slash) s[--l] = 0; } } static void config_wrapup(dkimfl_parm *parm) { if (parm->z.dns_timeout < 0) { fl_report(LOG_WARNING, "dns_timeout cannot be negative (%d)", parm->z.dns_timeout); parm->z.dns_timeout = 0; } if (parm->z.dnswl_octet_index > 3) parm->z.dnswl_octet_index = 3; if (parm->z.verbose < 0) parm->z.verbose = 0; if (parm->z.dnswl_worthiness_pass > UINT8_MAX) parm->z.dnswl_worthiness_pass = UINT8_MAX; no_trailing_char(parm->z.action_header, ':'); no_trailing_char(parm->z.domain_keys, '/'); no_trailing_char(parm->z.tmp, '/'); if (parm->z.tmp && strncmp(parm->z.tmp, "/tmp/", 5) == 0) { struct stat st; int rtc = stat(parm->z.tmp, &st); if (rtc && errno == ENOENT) { int rtc_mk = mkdir(parm->z.tmp, 0770); /* * don't report race conditions when running multiple copies */ int errno_mk = errno; rtc = stat(parm->z.tmp, &st); if (rtc && rtc_mk) fl_report(LOG_CRIT, "mkdir %s failed: %s", parm->z.tmp, strerror(errno_mk)); } if (rtc || !S_ISDIR(st.st_mode) || euidaccess(parm->z.tmp, R_OK|W_OK|X_OK)) { fl_report(LOG_WARNING, "disabling tmp = %s", parm->z.tmp); free(parm->z.tmp); parm->z.tmp = NULL; } } int period = adjust_period(parm->z.honored_report_interval); if (period != parm->z.honored_report_interval) { fl_report(LOG_WARNING, "bad honored_report_interval %d, adjusted to %d. CHECK CRON!", parm->z.honored_report_interval, period); parm->z.honored_report_interval = period; } if (parm->z.max_signatures > MAX_SIGNATURES) { fl_report(LOG_WARNING, "max_signatures set to %d is too hight: setting it to max value accepted %d.", parm->z.max_signatures, MAX_SIGNATURES); parm->z.max_signatures = MAX_SIGNATURES; } } static inline void some_dwa_cleanup(dkimfl_parm *parm) { assert(parm); if (parm->dwa) { void *parm_target[PARM_TARGET_SIZE]; parm_target[parm_t_id] = NULL; parm_target[db_parm_t_id] = db_parm_addr(parm->dwa); clear_parm(parm_target); db_clear(parm->dwa); parm->dwa = NULL; } } static void some_cleanup(dkimfl_parm *parm) // parent { assert(parm); void *parm_target[PARM_TARGET_SIZE]; parm_target[parm_t_id] = &parm->z; parm_target[db_parm_t_id] = parm->dwa? db_parm_addr(parm->dwa): NULL; fl_clear_msg_info(&parm->dyn.info); config_cleanup_default(parm); clear_parm(parm_target); if (parm->dwa) { db_clear(parm->dwa); parm->dwa = NULL; } free(parm->blocklist.data); publicsuffix_done(parm->pst); publicsuffix_done(parm->pst2); } static int parm_config(dkimfl_parm *parm, char const *fname, int no_db) // initialization, 0 on success { set_parm_logfun(&fl_report); int errs = 0; config_default(parm); if (!no_db && (parm->dwa = db_init()) == NULL) errs = 1; if (fname == NULL) { struct stat st; if (stat(default_config_file, &st)) { if (errno == ENOENT) return 0; // can do without it fl_report(LOG_ALERT, "Cannot stat %s: %s", default_config_file, strerror(errno)); return -1; } fname = default_config_file; } else if (*fname == 0) // invoked with -f "" return 0; void *parm_target[PARM_TARGET_SIZE]; parm_target[parm_t_id] = &parm->z; parm_target[db_parm_t_id] = parm->dwa? db_parm_addr(parm->dwa): NULL; errs += read_all_values(parm_target, fname); if (errs == 0) { config_wrapup(parm); if (parm->dwa) { int in = 0, out = 0; int rtc = db_config_wrapup(parm->dwa, &in, &out); if (rtc < 0) errs = 1; else if (in <= 0 && out <= 0) // no statements compiled: reset { some_dwa_cleanup(parm); } else { parm->use_dwa_after_sign = out > 0; parm->use_dwa_verifying = in > 0; } } } if (parm->z.verbose >= 4 && parm->z.redact_received_auth && !redact_is_fully_featured()) fl_report(LOG_WARNING, "Option redact_received_header is set in %s," " but it is not fully featured.", fname); parm->config_fname = fname; return errs; } static void check_split(dkimfl_parm *parm) { if (parm->z.split_verify) { if (strcmp(my_basename(parm->z.split_verify), parm->prog_name) == 0) { parm->split = split_verify_only; if (parm->dwa && parm->use_dwa_verifying == 0) some_dwa_cleanup(parm); } else { parm->split = split_sign_only; if (parm->dwa && parm->use_dwa_after_sign == 0) some_dwa_cleanup(parm); } } else parm->split = split_do_both; // 0 if (parm->split) { if (parm->z.verbose >= 3) fl_report(LOG_INFO, "%s configured to %s only", parm->prog_name, parm->split == split_sign_only? "sign": "verify"); } } // functions common for both incoming and outgoing msgs static void clear_prescreen(domain_prescreen* dps) { while (dps != NULL) { domain_prescreen* const next = dps->next; if (dps->sig) { for (int n = 0; n < dps->nsigs; ++n) free(dps->sig[n]); free(dps->sig); } free(dps->dmarc_record); free(dps->dmarc_rua); free(dps); dps = next; } } static domain_prescreen* do_get_prescreen(domain_prescreen** dps_head, char const *c_domain, int create) { assert(dps_head); assert(c_domain); char norm[256*4]; size_t ulen = sizeof norm - 1; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpointer-sign" #pragma GCC diagnostic ignored "-Wincompatible-pointer-types" char* n = u8_tolower(c_domain, strlen(c_domain), NULL, UNINORM_NFC, norm, &ulen); #pragma GCC diagnostic pop if (n != &norm[0] || ulen >= sizeof norm) return NULL; norm[ulen] = 0; char *domain = NULL; if (idn2_to_unicode_8z8z(norm, &domain, 0) != IDN2_OK) return NULL; domain_prescreen**dps = dps_head; while (*dps != NULL) { int const cmp = strcmp(domain, (*dps)->name); if (cmp < 0) { dps = &(*dps)->next; continue; } if (cmp == 0) { free(domain); return *dps; } break; } if (create == 0) { free(domain); return NULL; } size_t const len = sizeof(domain_prescreen); size_t const len2 = strlen(domain) + 1; domain_prescreen *new_dps = malloc(len + len2); if (new_dps) { memset(new_dps, 0, len); new_dps->next = *dps; *dps = new_dps; memcpy(&new_dps->name[0], domain, len2); } else fl_report(LOG_ALERT, "MEMORY FAULT"); free(domain); return new_dps; } static domain_prescreen* find_prescreen(domain_prescreen* dps_head, char const *c_domain) { if (dps_head == NULL) return NULL; return do_get_prescreen(&dps_head, c_domain, 0); } static domain_prescreen* get_prescreen(domain_prescreen** dps_head, char const *c_domain) { return do_get_prescreen(dps_head, c_domain, 1); } static void clean_stats(dkimfl_parm *parm) { assert(parm); if (parm->dyn.stats) { clear_prescreen(parm->dyn.stats->domain_head); parm->dyn.stats->domain_head = NULL; clean_stats_info_content(parm->dyn.stats); free(parm->dyn.stats); parm->dyn.stats = NULL; } } static void collect_stats(dkimfl_parm *parm, char const *start) { assert(parm); assert(parm->dyn.stats); char **target = NULL; char const *s; int stop_at = 0; if ((s = hdrval(start, content_type)) != NULL) { target = &parm->dyn.stats->content_type; stop_at = ';'; } else if ((s = hdrval(start, content_transfer_encoding)) != NULL) target = &parm->dyn.stats->content_encoding; else if ((s = hdrval(start, "Date")) != NULL) target = &parm->dyn.stats->date; else if ((s = hdrval(start, "Message-Id")) != NULL) target = &parm->dyn.stats->message_id; else if ((s = hdrval(start, "From")) != NULL) target = &parm->dyn.stats->from; else if ((s = hdrval(start, "Subject")) != NULL) target = &parm->dyn.stats->subject; if (target && *target == NULL && (*target = strdup_normalize(s, stop_at)) == NULL) // memory faults are silently ignored for stats clean_stats(parm); else if ((s = hdrval(start, "Precedence")) != NULL) { while (isspace(*(unsigned char const*)s)) ++s; size_t len; int ch; if (strincmp(s, "list", 4) == 0 && ((len = strlen(s)) <= 4 || (ch = ((unsigned char const*)s)[5]) == ';' || isspace(ch))) parm->dyn.stats->mailing_list = 1; } else if (strincmp(start, "List-", 5) == 0) { if (hdrval(start, "List-Id") || hdrval(start, "List-Post") || hdrval(start, "List-Unsubscribe")) parm->dyn.stats->mailing_list = 1; } else if (hdrval(start, "Mailing-List")) parm->dyn.stats->mailing_list = 1; } // outgoing static int read_key(dkimfl_parm *parm, char *domain, key_choice *k) // read private key and selector from disk, return 0 or parm->dyn.rtc = -1; // when returning 0, k->key and k->selector are set so as to // reflect results, they are assumed to be NULL on entry. { assert(parm); assert(domain); assert(k); assert(k->key == NULL); assert(k->selector == NULL); char buf[PATH_MAX], buf2[PATH_MAX], *key = NULL, *selector = NULL; FILE *fp = NULL; struct stat st; size_t dkl, fl; char *fname = NULL; if ((dkl = strlen(parm->z.domain_keys)) + (fl = strlen(domain)) + 2 >= PATH_MAX) { errno = ENAMETOOLONG; goto error_exit; } memcpy(buf, parm->z.domain_keys, dkl); buf[dkl] = '/'; fname = normalize_utf8_twice(domain); if (fname == NULL) { errno = EINVAL; goto error_exit; } strcpy(&buf[dkl+1], fname); if (stat(buf, &st)) { if (errno == ENOENT) { free(fname); return 0; // OK, domain not configured } goto error_exit; } if ((key = malloc(st.st_size + 1)) == NULL || (fp = fopen(buf, "r")) == NULL || fread(key, st.st_size, 1, fp) != 1) goto error_exit; fclose(fp); fp = NULL; key[st.st_size] = 0; /* * readlink fails with EINVAL if the domain is not a symbolic link. * It is not an error to omit selector specification. */ ssize_t lsz = readlink(buf, buf2, sizeof buf2); if (lsz < 0 || (size_t)lsz >= sizeof buf2) { if ((errno != EINVAL && parm->z.verbose) || parm->z.verbose >= 8) fl_report(errno == EINVAL? LOG_INFO: LOG_ALERT, "id=%s: cannot readlink for %s: readlink returns %zd: %s", parm->dyn.info.id, fname, lsz, strerror(errno)); if (errno != EINVAL) goto error_exit_no_msg; } else /* * get selector from symbolic link base name, e.g. * * example.com -> ../somewhere/my-selector * or * example.com -> example.com.my-selector.private */ { buf2[lsz] = 0; char *name = my_basename(buf2); if (strincmp(name, fname, fl) == 0) { name += fl; if (*name == '.') ++name; } char *ext = strrchr(name, '.'); if (ext && (strcmp(ext, ".private") == 0 || strcmp(ext, ".pem") == 0)) *ext = 0; if ((selector = strdup(name)) == NULL) goto error_exit_no_msg; } k->key = (dkim_sigkey_t) key; k->selector = selector; free(fname); return 0; error_exit: if (parm->z.verbose) fl_report(LOG_ALERT, "id=%s: error reading key %s: %s", parm->dyn.info.id, fname? fname: domain, strerror(errno)); error_exit_no_msg: if (fp) fclose(fp); free(key); free(selector); free(fname); return parm->dyn.rtc = -1; } static char *default_domain_choice(dkimfl_parm *parm, int type) /* * Determine default domain. * Return NULL for fatal error. */ { assert(parm); char *domain; if (type == '*' && parm->dyn.auth_or_relay && (domain = strchr(parm->dyn.auth_or_relay, '@')) != NULL) ++domain; else // is that how local domains work? if ((domain = parm->z.default_domain) == NULL) { fl_report(type == '*'? LOG_CRIT: LOG_ERR, "id=%s: no '%c' domain name for %s: configure default_domain", parm->dyn.info.id, type, parm->dyn.auth_or_relay); } return domain; } static int default_key_choice(dkimfl_parm *parm, int type, key_choice *k) /* * Determine signing domain and try to read the key. Set k->domain * whether or not key and selector are set. * Return 0 or -1 for fatal error. */ { assert(parm); assert(k->key == NULL); assert(k->selector == NULL); assert(k->domain == NULL); char *domain = default_domain_choice(parm, type); if (domain && read_key(parm, domain, k) == 0 && (k->domain = strdup(domain)) != NULL) return 0; free(k->key); k->key = NULL; free(k->selector); k->selector = NULL; return -1; } typedef struct key_finder { size_t choice_max, count; dkimfl_parm *parm; struct choice_element { key_choice k; char const* header; // alias, not malloced } choice[]; } key_finder; static void key_finder_clean(key_finder *kf) { if (kf) { for (size_t i = 0; i < kf->choice_max; ++i) { free(kf->choice[i].k.key); free(kf->choice[i].k.selector); free(kf->choice[i].k.domain); } free(kf); } } static void key_finder_set_dyn(key_finder *kf) /* * After all header fields are processed, keep the 1st choice * key (or 1st choice domain for not signing) */ { assert(kf); if (kf->parm) { for (size_t i = 0; i < kf->choice_max; ++i) if (kf->choice[i].k.key) { kf->parm->dyn.k = kf->choice[i].k; memset(&kf->choice[i], 0, sizeof kf->choice[0]); break; } if (kf->parm->dyn.k.key == NULL) for (size_t i = 0; i < kf->choice_max; ++i) if (kf->choice[i].k.domain) { assert(kf->choice[i].k.selector == NULL); kf->parm->dyn.k.domain = kf->choice[i].k.domain; memset(&kf->choice[i], 0, sizeof kf->choice[0]); break; } kf->parm = NULL; } } static key_finder *key_finder_init(dkimfl_parm *parm) /* * Initialize key finder if needed. * * Return a structure to track headers or NULL. * On hard failure return NULL and set parm->dyn.rtc */ { assert(parm); size_t choice_max = 0; if (parm->z.key_choice_header != NULL && parm->dyn.do_seal == 0) while (parm->z.key_choice_header[choice_max] != NULL) ++choice_max; else { if (default_key_choice(parm, '*', &parm->dyn.k)) parm->dyn.rtc = -1; return NULL; } size_t i = choice_max * sizeof (struct choice_element) + sizeof (key_finder); key_finder *kf = (key_finder*)malloc(i); if (kf == NULL) { parm->dyn.rtc = -1; return NULL; } memset(kf, 0, i); kf->parm = parm; /* * key_choice_header may have duplicates which become active in case * the relevant field appears multiple times. However, the default * domain ("-", or "*") cannot be duplicated. * * count "-", or "*" tokens */ size_t keep = 0, count = choice_max; for (i = 0; i < choice_max; ++i) { char const* const h = parm->z.key_choice_header[i]; if (h[1] == 0 && strchr("-*", h[0])) ++keep; kf->choice[i].header = h; } assert(count >= keep); /* * process each "-", or "*" tokens */ if (keep) { char seen[3] = {0, 0, 0}; count -= keep; for (i = 0; i < choice_max; ++i) { char const *const h = parm->z.key_choice_header[i]; if (h[1] == 0 && strchr("-*", h[0])) { if (strchr(seen, h[0]) == NULL) { seen[strlen(seen)] = h[0]; assert(strlen(seen) < sizeof seen); if (default_key_choice(parm, h[0], &kf->choice[i].k)) { key_finder_clean(kf); parm->dyn.rtc = -1; return NULL; } } kf->choice[i].header = NULL; if (--keep <= 0) break; } } } assert(keep == 0); kf->choice_max = choice_max; kf->count = count; if (count == 0) // There are no fields to track { key_finder_set_dyn(kf); key_finder_clean(kf); return NULL; } return kf; } static int key_finder_track(key_finder *kf, char *start) /* * Called with kf and a pointer to a full 0-terminated header field, * including \r\n. On choice field, check if it leads to a signing key. * * Return 0 if ok, -1 on hard fail. * * Return 1 if all fields have been found and no further tracking is * needed. */ { assert(kf); assert(start); assert(kf->parm); for (size_t i = 0; i < kf->choice_max; ++i) { char const *const h = kf->choice[i].header; if (h) { char *const val = hdrval(start, h); if (val) { char *domain, *user, *scan = strdup(val); if (scan == NULL) return kf->parm->dyn.rtc = -1; if (my_mail_parse(scan, &user, &domain) == 0 && domain_is_valid(domain)) { assert(kf->choice[i].k.key == NULL); assert(kf->choice[i].k.selector == NULL); assert(kf->choice[i].k.domain == NULL); int rtc = read_key(kf->parm, domain, &kf->choice[i].k); if (rtc) { free(scan); return kf->parm->dyn.rtc = -1; } if ((kf->choice[i].k.domain = strdup(domain)) == NULL) { free(scan); return kf->parm->dyn.rtc = -1; } } free(scan); kf->choice[i].header = NULL; // don't reuse it kf->count -= 1; if (kf->parm->z.verbose >= 8) fl_report(LOG_DEBUG, "id=%s: matched %sheader \"%s\" at choice %zd: " "domain=%s, key=%s, selector=%s", kf->parm->dyn.info.id, kf->count == 0? "last ": "", h, i, kf->choice[i].k.domain? kf->choice[i].k.domain: "NONE", kf->choice[i].k.key? "yes": "no", kf->choice[i].k.selector? kf->choice[i].k.selector: "NONE"); if (kf->count == 0) { key_finder_set_dyn(kf); return 1; } break; } } } return 0; } #if OLD_PREVIEW_FUNCTION static int header_preview(dkimfl_parm *parm, cte_transform *cte) /* * Do the original function of detemining domain/keys. In addition, * check Content-Type: and Content-Transfer-Encoding: and determine if * (i) Content-Transfer-Encoding: must be saved as Original-, or * (ii) Autoconversion to base64 is needed. */ { assert(parm); int rtc = 0; size_t i, keep, count, choice_max = 0; struct choice_element { key_choice k; char const* header; // alias, not malloced } *choice = NULL; if (parm->z.key_choice_header != NULL) while (parm->z.key_choice_header[choice_max] != NULL) ++choice_max; else rtc = default_key_choice(parm, '*', &parm->dyn.k); if (choice_max > 0 && (choice = (struct choice_element*) calloc(choice_max, sizeof *choice)) == NULL) return parm->dyn.rtc = -1; /* * key_choice_header may have duplicates which become active in case * the relevant field appears multiple times. However, the default * domain ("-", or "*") cannot be duplicated. * * count "-", or "*" tokens */ keep = 0; count = choice_max; for (i = 0; i < choice_max; ++i) { char const* const h = parm->z.key_choice_header[i]; if (h[1] == 0 && strchr("-*", h[0])) ++keep; choice[i].header = h; } assert(count >= keep); /* * process each "-", or "*" tokens */ if (keep) { char seen[3] = {0, 0, 0}; count -= keep; for (i = 0; i < choice_max; ++i) { char const *const h = parm->z.key_choice_header[i]; if (h[1] == 0 && strchr("-*", h[0])) { if (strchr(seen, h[0]) == NULL) { seen[strlen(seen)] = h[0]; assert(strlen(seen) < sizeof seen); rtc = default_key_choice(parm, h[0], &choice[i].k); if (rtc) break; } choice[i].header = NULL; if (--keep <= 0) break; } } } assert(keep == 0 || rtc != 0); int seen_ct_cte = 0; if (parm->z.disable_experimental) seen_ct_cte = 3; /* * Read the header to 1) find all choice[] values and 2) CTE */ if (rtc == 0 && (count > 0 || seen_ct_cte != 3)) { FILE* fp = fl_get_file(parm->fl); assert(fp); var_buf *vb = &parm->dyn.vb; while (rtc == 0 || seen_ct_cte != 3) { char *p = vb_fgets(vb, keep, fp); char *eol = p? strchr(p, '\n'): NULL; if (eol == NULL) { if (parm->z.verbose) fl_report(LOG_ALERT, "id=%s: header field is bad (%.20s...)", parm->dyn.info.id, vb_what(vb, fp)); rtc = parm->dyn.rtc = -1; break; } int const next = eol >= p? fgetc(fp): '\n'; int const cont = next != EOF && next != '\n'; char *const start = vb->buf; if (cont && isspace(next)) // wrapped { *++eol = next; keep = eol + 1 - start; continue; } /* * full 0-terminated header field, including trailing \n, is in buffer; * if it is a choice header, check if it leads to a signing key. */ for (i = 0; i < choice_max; ++i) { char const *const h = choice[i].header; if (h) { char *const val = hdrval(start, h); if (val) { char *domain, *user; if (my_mail_parse(val, &user, &domain) == 0 && domain_is_valid(domain)) { assert(choice[i].k.key == NULL); assert(choice[i].k.selector == NULL); assert(choice[i].k.domain == NULL); rtc = read_key(parm, domain, &choice[i].k); if (rtc) break; if ((choice[i].k.domain = strdup(domain)) == NULL) { rtc = parm->dyn.rtc = -1; break; } } choice[i].header = NULL; // don't reuse it count -= 1; if (parm->z.verbose >= 8) fl_report(LOG_DEBUG, "id=%s: matched header \"%s\" at choice %zd: " "domain=%s, key=%s, selector=%s", parm->dyn.info.id, h, i, choice[i].k.domain? choice[i].k.domain: "NONE", choice[i].k.key? "yes": "no", choice[i].k.selector? choice[i].k.selector: "NONE"); break; } } } if (seen_ct_cte != 3) { char *s; if ((seen_ct_cte & 1) == 0 && (s = hdrval(start, content_type)) != NULL) { if (content_type_is_multipart(s)) { *cte = cte_transform_identity_multipart; seen_ct_cte |= 3; // override (unrealistic) cte } else seen_ct_cte |= 1; } else if ((seen_ct_cte & 2) == 0 && (s = hdrval(start, content_transfer_encoding)) != NULL) { *cte = parse_content_transfer_encoding_simple(s); seen_ct_cte |= 2; } } if (!cont || (count <= 0 && seen_ct_cte == 3)) // eoh or found all break; start[0] = next; keep = 1; } rewind(fp); } /* * all header fields processed; * keep 1st choice key or 1st choice domain, and free the rest. */ if (rtc == 0) { for (i = 0; i < choice_max; ++i) if (choice[i].k.key) { parm->dyn.k = choice[i].k; memset(&choice[i], 0, sizeof choice[0]); break; } if (parm->dyn.k.key == NULL) for (i = 0; i < choice_max; ++i) if (choice[i].k.domain) { assert(choice[i].k.selector == NULL); parm->dyn.k.domain = choice[i].k.domain; memset(&choice[i], 0, sizeof choice[0]); break; } } for (i = 0; i < choice_max; ++i) { free(choice[i].k.key); free(choice[i].k.selector); free(choice[i].k.domain); } free(choice); return parm->dyn.rtc; } #endif // OLD_PREVIEW_FUNCTION static inline int my_dkim_header(dkimfl_parm *parm, DKIM *dkim, char *field, size_t len) /* * Wrapper used for signing. */ { assert(len > 0); assert(dkim); DKIM_STAT status = dkim_header(dkim, field, len); if (status == DKIM_STAT_OK_NOTUSED) return 0; if (status != DKIM_STAT_OK) { if (parm->z.verbose) { char const *err = dkim_getresultstr(status); fl_report(LOG_CRIT, "id=%s: signing dkim_header failed on %zu bytes: %s (%d)", parm->dyn.info.id, len, err? err: "unknown", (int)status); } if (status == DKIM_STAT_NORESOURCE || status == DKIM_STAT_INVALID) return parm->dyn.rtc = -1; // DKIM_STAT_SYNTAX and DKIM_STAT_INTERNAL (bad ARC?) pass } return 0; } static inline int my_add_to_original_signed(dkimfl_parm *parm, original_header **oh_base, char *field, original_h_flag original_h) { if (add_to_original_signed(oh_base, field, original_h) < 0) { fl_report(LOG_CRIT, "id=%s: MEMORY FAULT", parm->dyn.info.id); return parm->dyn.rtc = -1; } return 0; } static int replace_received_auth(dkimfl_parm *parm, char **new_text, char *start, char *s, size_t s_len) { char *p2, *eol2, *authuserbuf, *addr; if ((p2 = strchr(s, '\n')) != NULL && (p2 = strchr(p2 + 1, '(')) != NULL && (eol2 = strchr(p2, '\n')) != NULL && (authuserbuf = strstr(p2, "AUTH: ")) != NULL && // 123456 (authuserbuf += 6) < eol2 && (addr = strchr(authuserbuf, ' ')) != NULL) { char *eaddr = ++addr; int ch; size_t const len = strlen(parm->dyn.info.authsender); while ((ch = *(unsigned char*)eaddr) != 0 && strchr("),", ch) == NULL) ++eaddr; if (eaddr < eol2 && addr + len == eaddr && strincmp(addr, parm->dyn.info.authsender, len) == 0) /* * found: compose the replacement with the redacted field */ { char *red = redacted(parm->z.redact_received_auth, parm->dyn.info.authsender); size_t redlen = red? strlen(red): 0; size_t r_len = s_len - len + redlen; char *p = *new_text = malloc(r_len + 1); if (p) { memcpy(p, start, addr - start); p += addr - start; if (red) { memcpy(p, red, redlen); p += redlen; } assert(strlen(eaddr) + redlen + (size_t)(addr - start) == r_len); strcpy(p, eaddr); } free(red); return p? 1: -1; } } return 0; } #include "strdup_crsp.h" #include "rfc822.h" static inline size_t chomp_cr(char *hdr) { char *s = hdr, *d = hdr; if (d) { int ch; while ((ch = *(unsigned char*)s++) != 0) if (ch != '\r') *d++ = ch; *d = 0; } return d - hdr; } typedef struct replacement { struct replacement *next; uint64_t offset; // offset (in file) where replacement starts char *new_text; // possibly NULL, no trailing \n size_t length; // length of old text plus a trailing \n } replacement; replacement *add_replacement(replacement **r) /* * append a zeroed structure to the linked list. */ { replacement *new_r = malloc(sizeof (replacement)); if (new_r) { memset(new_r, 0, sizeof *new_r); while (*r) r = &(*r)->next; *r = new_r; } return new_r; } typedef struct sign_parms { replacement *repl; original_header *oh_base; transform_stack *vc; DKIM *dkim; dkimfl_parm *parm; char *auth_pass; cstring *ar; // Authentication-Results when sealing char qp_to_base64; char original_header_fields_exist; } sign_parms; static void clear_sign_parms(sign_parms *sh) { assert(sh); if (sh->parm) free(sh->parm->dyn.authserv_id); // either strdup in ar_gather or from k.domain free(sh->auth_pass); if (sh->dkim) dkim_free(sh->dkim); while (sh->repl) { replacement *r = sh->repl->next; free(sh->repl->new_text); free(sh->repl); sh->repl = r; } clear_original_header(sh->oh_base); clear_transform_stack(sh->vc); } static int insert_auth_pass(dkimfl_parm *parm, replacement **repl, DKIM *dkim, uint64_t offset) /* * A-R with auth=pass; if signed, must get hashed at the lowest place * in the header, w.r.t. other A-Rs. * * Return 0 on success, -1 on failure. */ { static char const auth_pass_fmt[] = "Authentication-Results: %s;%s auth=pass (details omitted)"; if (parm->z.add_auth_pass && parm->dyn.k.domain && parm->dyn.do_seal == 0) { char const *nl = ""; size_t l = strlen(parm->dyn.k.domain) + sizeof auth_pass_fmt; if (l > 77) { nl = "\r\n"; l += 2; } char *nt = malloc(l); if (nt == NULL) { fl_report(LOG_ALERT, "MEMORY FAULT"); return parm->dyn.rtc = -1; } replacement *new_r = add_replacement(repl); if (new_r == NULL) { free(nt); fl_report(LOG_ALERT, "MEMORY FAULT"); return parm->dyn.rtc = -1; } l = sprintf(nt, auth_pass_fmt, parm->dyn.k.domain, nl); new_r->offset = offset; new_r->new_text = nt; new_r->length = 1; // replace just \n return my_dkim_header(parm, dkim, nt, l); } return 0; } static int ar_gather(void* void_sh, int seq, name_val *nv, size_t n) /* * Copy A-R into sh->ar */ { assert(void_sh); if (nv == NULL) // last call return seq; sign_parms *sh = void_sh; assert(nv->name); if (seq < 0) // authserv-id: assume the topmost one was locally generated { if (sh->parm->dyn.authserv_id == NULL) { sh->parm->dyn.authserv_id = strdup(nv->name); return 0; } else return stricmp(sh->parm->dyn.authserv_id, nv->name) == 0? 0: 1; } cstring *ar = sh->ar; if (ar) ar = cstr_addstr(ar, ";\r\n "); int col = 1; for (unsigned int i = 0; i < n; ++i) { if (ar) { size_t pos = cstr_length(ar); int addcol; ar = cstr_printfrc(ar, &addcol, " %s=%s", nv[i].name, nv[i].value); col += addcol; if (col > 78 && ar) { ar = cstr_insstr(ar, pos, "\r\n "); col = 3; } } } sh->ar = ar; return 0; } static int sign_headers(sign_parms *sh) // return parm->dyn.rtc = -1 for unrecoverable error, // parm->dyn.rtc (0) otherwise { assert(sh); dkimfl_parm *parm = sh->parm; assert(parm); size_t keep = 0; var_buf *vb = &parm->dyn.vb; FILE* fp = fl_get_file(parm->fl); assert(fp); DKIM *dkim = sh->dkim; key_finder *kf = NULL; if (dkim) { kf = key_finder_init(parm); if (kf == NULL) { if (parm->dyn.rtc < 0 || insert_auth_pass(parm, &sh->repl, dkim, 0)) return -1; } } uint64_t offset = 0; size_t newlines = 0; bool search_received = parm->z.redact_received_auth; bool seen_anything = 0; bool seen_ct_cte = 0; if (parm->z.disable_experimental) seen_ct_cte = 1; // shortcut C-T-E processing for (;;) { char *p = vb_fgets(vb, keep, fp); char *eol = p? strchr(p, '\n'): NULL; if (eol == NULL) { if (vb->buf == NULL || ferror(fp)) parm->dyn.rtc = -1; else if (seen_anything == 0 || p) /* * A message with no body can be valid. If no field is found, * the message is not valid. If a non-terminated field is * found, the message is not valid. */ parm->dyn.rtc = 3; if (parm->dyn.rtc != 0) { if (parm->z.verbose) fl_report(parm->dyn.rtc < 0? LOG_ALERT: LOG_ERR, "id=%s: bad header (%.20s...)", parm->dyn.info.id, vb_what(vb, fp)); goto error_exit; } break; } int const next = eol >= p? fgetc(fp): '\n'; int const cont = next != EOF && next != '\n'; char *const start = vb->buf; seen_anything = 1; keep = eol - start; if (cont && isspace(next)) // wrapped { *eol++ = '\r'; *eol++ = '\n'; *eol = next; keep += 3; ++newlines; continue; } /* * full field is in buffer, keep bytes excluding trailing \n * (neither dkim_header nor replacements want trailing \n) */ if (keep) { *eol = 0; if (parm->dyn.stats) collect_stats(parm, start); if (dkim) { int rc = 0; if (kf) { int rtc = key_finder_track(kf, start); if (rtc == 1) { key_finder_clean(kf); kf = NULL; /* * Right before the authenticating field. */ rc = insert_auth_pass(parm, &sh->repl, dkim, offset); if (rc < 0) goto error_exit; } } char *nt = NULL, *s; original_h_flag original_h = 0; int oh = 0, naddrs; if (search_received && (s = hdrval(start, "Received")) != NULL) { if ((rc = replace_received_auth(parm, &nt, start, s, keep)) > 0) search_received = false; } else if (seen_ct_cte == 0 && (s = hdrval(start, content_type)) != NULL) { if (content_type_is_multipart(s)) seen_ct_cte = 1; // override (unrealistic) cte, if any } else if (seen_ct_cte == 0 && (s = hdrval(start, content_transfer_encoding)) != NULL) /* * C-T-E is only meaningful for single-part body. Signing it * is discouraged as that can hinder MLM transforms; however, * it must be signed if l= is given. If it is signed, save * the original. * Save as Original- if it is base64, to avoid erroneous * decoding after MLM transformation. * Convert it to base64 if it is quoted printable. */ { cte_transform cte = parse_content_transfer_encoding_simple(s); if (cte == cte_transform_base64) /* * TODO: check column width and cr of existing encoding?? * or is it easier to decode + re-encode? */ { rc = add_to_original_signed(&sh->oh_base, start, 0); } else if (cte == cte_transform_quoted_printable) { if (parm->dyn.do_seal == 0 && parm->z.no_qp_conversion == 0) { static char const *replace_cte = "Content-Transfer-Encoding: base64"; if ((nt = strdup_crsp(replace_cte)) == NULL || add_to_original_signed(&sh->oh_base, nt, 0) < 0) rc = -1; else { rc = 1; sh->qp_to_base64 = 1; } } } else original_h |= original_h_only_if_signed; seen_ct_cte = 1; } else if ((oh = 0, s = hdrval(start, "To")) != NULL || (oh = 2, s = hdrval(start, "Reply-To")) != NULL || (oh = 3, s = hdrval(start, "From")) != NULL || (oh = 1, s = hdrval(start, "Cc")) != NULL) { if (parm->z.noaddrrewrite == 0) rc = replace_courier_wrap(&nt, &naddrs, start); else { rc = 0; // TODO: proper mail parse? naddrs = strchr(start, ',')? 2: 1; } /* * Save the Original- value if there are multiple * addresses, since MLM likely swap them. */ if ((oh && naddrs > 1) || oh > 1) { original_h |= original_h_only_if_signed; if (oh == 3) original_h |= original_h_is_author; } } else if ((s = hdrval(start, "Subject")) != NULL) /* * Save Subject: anyway, see note at check_original_subject(). */ { original_h |= original_h_only_if_signed; } else if (strincmp(start, "Original-", 9) == 0 || hdrval(start, "Author") != NULL) sh->original_header_fields_exist = 1; else if (sh->ar && (s = hdrval(start, "Authentication-Results")) != NULL) { int rtc = a_r_parse(s, &ar_gather, sh); if (rtc == 0 && sh->ar != NULL) rc = 1; // all ok, remove current field else if (sh->ar == NULL) rc = -1; else if (parm->z.verbose >= 6) { char const *ar_err = rtc > 0? "Extraneous ID": a_r_error(rtc); fl_report(rtc > 0? LOG_INFO: LOG_ERR, "id=%s: A-R ignored on sealing: %s: \"%.20s%s\"", parm->dyn.info.id, ar_err, s, strlen(s) > 20? "[...]": ""); } } if (rc > 0) { replacement *new_r = add_replacement(&sh->repl); if (new_r) { new_r->offset = offset; new_r->new_text = nt; new_r->length = keep - newlines + 1; if (nt) { rc = my_dkim_header(parm, dkim, nt, strlen(nt)); if (rc == 0 && original_h) rc = my_add_to_original_signed(parm, &sh->oh_base, nt, original_h); } else // delete the newline as well new_r->length += 1; } else { free(nt); rc = -1; } } else if (rc == 0) { rc = my_dkim_header(parm, dkim, start, keep); if (rc == 0 && original_h) rc = my_add_to_original_signed(parm, &sh->oh_base, start, original_h); } if (rc < 0) goto error_exit; } } offset += keep - newlines + 1; if (!cont) break; newlines = 0; start[0] = next; keep = 1; } /* * Finalize key finder, in case. */ if (kf) { key_finder_set_dyn(kf); if (parm->z.verbose >= 8) fl_report(LOG_DEBUG, "id=%s: key finder still has %zd choice headers", parm->dyn.info.id, kf->count); key_finder_clean(kf); kf = NULL; if (insert_auth_pass(parm, &sh->repl, dkim, offset)) return parm->dyn.rtc = -1; } /* * Add sentinel if using replacements. That way, copy_replacement() * will copy exactly the header. */ if (sh->repl) { replacement *new_r = add_replacement(&sh->repl); if (new_r) new_r->offset = offset + 1; else parm->dyn.rtc = -1; } return parm->dyn.rtc; error_exit: if (kf) key_finder_clean(kf); return parm->dyn.rtc = -1; } static char *strchr_del_cr(char const *cp, int nl) { char *eol = strchr(cp, nl); if (eol && eol > cp && *(eol - 1) == '\r') *--eol = nl; return eol; } static int copy_body(dkimfl_parm *parm, DKIM *dkim, transform_stack *vc) // Used for both signing and verifying. // // return parm->dyn.rtc = -1 for unrecoverable error, // parm->dyn.rtc (0) otherwise { assert(parm); FILE* fp = fl_get_file(parm->fl); assert(fp); char buf[8192]; int rtc = 0; char const *id_or_ip = "id"; char const *info = parm->dyn.info.id; if (parm->dyn.is_verifying) { id_or_ip = "ip"; info = parm->dyn.info.tcpremoteip; } char* (*const my_strchr)(char const*, int) = parm->dyn.no_write? &strchr_del_cr: &strchr; while (fgets(buf, sizeof buf - 1, fp)) { char *eol = (*my_strchr)(buf, '\n'); if (eol) { *eol++ = '\r'; *eol++ = '\n'; *eol = 0; } else eol = &buf[sizeof buf - 1]; size_t const len = eol - &buf[0]; if (dkim) { DKIM_STAT status = dkim_body(dkim, buf, len); if (status != DKIM_STAT_OK) { if (parm->z.verbose) { char const *err = dkim_geterror(dkim); if (err == NULL) err = dkim_getresultstr(status); fl_report(LOG_CRIT, "%s=%s: dkim_body failed on %zu bytes: %s (%d)", id_or_ip, info, len, err? err: "unknown", (int)status); } return parm->dyn.rtc = -1; } } if (vc && rtc == 0) { int rc = (*vc->fn)(buf, len, vc); if (rc) { rtc = parm->dyn.rtc = -1; if (parm->z.verbose) fl_report(LOG_ERR, "%s=%s: transform_stack failed on %zu bytes: %d", id_or_ip, info, len, rc); if (dkim == NULL) break; } } else if (dkim && dkim_minbody(dkim) == 0) break; } if (vc) // flush { int rc = (*vc->fn)(NULL, 0, vc); if (rc && parm->z.verbose) fl_report(LOG_ERR, "%s=%s: transform_stack failed on flushing: %d", id_or_ip, info, rc); } return parm->dyn.rtc; } typedef struct copy_body_to_dkim_parm { DKIM *dkim; FILE *save_fp; char *dyn_info; char const *id_or_ip; int verbose; } copy_body_to_dkim_parm; static int copy_body_to_dkim(char *in, unsigned len, transform_stack *vc) /* * Called on first pass, by copy_body() */ { assert(vc); assert(vc->next == NULL); // is terminal copy_body_to_dkim_parm *cbvp = (copy_body_to_dkim_parm*)vc->fn_parm; assert(cbvp); DKIM *dkim = cbvp->dkim; assert(dkim); if (in && len) { if (cbvp->save_fp) fwrite(in, len, 1, cbvp->save_fp); DKIM_STAT status = dkim_body(dkim, in, len); if (status != DKIM_STAT_OK) { if (cbvp->verbose) { char const *err = dkim_geterror(dkim); if (err == NULL) err = dkim_getresultstr(status); fl_report(LOG_CRIT, "%s=%s: dkim_body trans failed on %u bytes: %s (%d)", cbvp->id_or_ip, cbvp->dyn_info, len, err? err: "unknown", (int)status); } return 1; } } return 0; } typedef struct copy_body_to_file_parm { FILE *fp; char *dyn_info; char const *id_or_ip; int verbose; } copy_body_to_file_parm; static int copy_body_to_file(char *in, unsigned len, transform_stack *vc) /* * Called on second signing pass, by copy_body() */ { assert(vc); assert(vc->next == NULL); // is terminal copy_body_to_file_parm *cbtf = (copy_body_to_file_parm*)vc->fn_parm; assert(cbtf); FILE *fp= cbtf->fp; assert(fp); if (in && len) { if (fwrite(in, len, 1, fp) != 1) { if (cbtf->verbose) { fl_report(LOG_CRIT, "%s=%s: copy_body_to_file failed on %u bytes: %s (%d)", cbtf->id_or_ip, cbtf->dyn_info, len, strerror(errno), (int)errno); } return 1; } } return 0; } static void copy_replacement(dkimfl_parm *parm, FILE *fp, FILE *fp_out, replacement *repl) { uint64_t offset = 0; while (repl) { char buf[4096]; size_t in = sizeof buf; bool last = false; assert(offset <= repl->offset); if (offset + in >= repl->offset) { in = repl->offset - offset; last = true; } if (in && (in = fread(buf, 1, in, fp)) > 0 && fwrite(buf, in, 1, fp_out) != 1) break; offset += in; if (last) { size_t l = chomp_cr(repl->new_text); if (l && fwrite(repl->new_text, l, 1, fp_out) != 1) break; size_t length = repl->length; if (length == 0) NULL; else if (length == 1) { if (fputc('\n', fp_out) == EOF) break; } else /* * read and discard the original header (except the trailing \n) */ { length -= 1; offset += length; while (length > 0) { in = sizeof buf < length? sizeof buf: length; length -= in; if (fread(buf, 1, in, fp) != in) fl_report(LOG_ERR, "cannot advance %zu in mail file: %s", in, strerror(errno)); } } repl = repl->next; } } if (repl) parm->dyn.rtc = -1; } static void recipient_s_domains(dkimfl_parm *parm, char *user_domain) // count recipients of outgoing messages and build domain list for database, // flag parm->dyn.special if the postmaster is the only recipient. { assert(parm); assert(parm->fl); unsigned rcpt_count = 0; int special_candidate = 0; domain_prescreen *dps_head = NULL; fl_rcpt_enum *fre = fl_rcpt_start(parm->fl); if (fre) { char *rcpt; while ((rcpt = fl_rcpt_next(fre)) != NULL) { char *user, *domain; if (my_mail_parse(rcpt, &user, &domain) == 0 && domain_is_valid(domain)) { if (++rcpt_count == 1 && user_domain) special_candidate = stricmp(domain, user_domain) == 0 && stricmp(user, "postmaster") == 0; domain_prescreen* dps = get_prescreen(&dps_head, domain); if (dps == NULL) // memory fault { clear_prescreen(dps_head); return; } } } fl_rcpt_clear(fre); } if (dps_head == NULL) { /* * No recipients, this message won't be sent, so skip db part. */ clean_stats(parm); fl_report(LOG_ERR, "id=%s: unable to collect recipients from ctl file", parm->dyn.info.id); } else { if (parm->dyn.stats) { parm->dyn.stats->domain_head = dps_head; parm->dyn.stats->rcpt_count = rcpt_count? rcpt_count: 1; // how come? } else clear_prescreen(dps_head); parm->dyn.special = rcpt_count == 1 && special_candidate; } } static inline int user_is_blocked(dkimfl_parm *parm) { return parm->user_blocked = search_list(&parm->blocklist, parm->dyn.auth_or_relay); } static inline int is_common_address(char const *from) { char *addr; if (from == NULL || (addr = strdup(from)) == NULL) return 0; static const char *rfc2142[] = { "postmaster", "abuse", "info", "sales", "list", "support", "noc", "marketing", "security", "hostmaster", "webmaster", NULL}; char *domain, *user; int rtc = 0; if (my_mail_parse(addr, &user, &domain) == 0 && user) for (char const **s = &rfc2142[0]; *s; ++s) if (stricmp(user, *s) == 0) { rtc = 1; break; } free(addr); return rtc; } static inline void stats_outgoing(dkimfl_parm *parm) { if (parm->dyn.stats) { parm->dyn.stats->outgoing = 1; char *s = parm->dyn.stats->envelope_sender = fl_get_sender(parm->fl); if (s && *s == 0) parm->dyn.stats->complaint_flag |= 1; if (is_common_address(parm->dyn.stats->from)) parm->dyn.stats->complaint_flag |= 2; if (parm->dyn.stats->rcpt_count == 0) recipient_s_domains(parm, NULL); } } static inline int hex_value(int ch) { assert(isxdigit(ch)); assert(isdigit(ch) || isupper(ch)); return ch - (ch <= '9'? '0' - 0: 'A' - 0xA); } static int alte_hex(char const *x2) { int val = 0; int ch1 = toupper(*(unsigned char *)x2++); if (isxdigit(ch1)) { val = hex_value(ch1); int ch2 = toupper(*(unsigned char *)x2); if (isxdigit(ch2)) val = 16*val + hex_value(ch2); } return val; } static void transfigure_dkim_hdr(char *s) /* * The string is going to contain just the header field names, encoded * as follows: * * TAB mark string as transfigured * number of entries number of field following, can be 0 * *(IA5String) 1 byte length + length bytes string */ { assert(s); char *nentries = s; *nentries++ = '\t'; // overwrite v- *nentries = 0; int ch; char *t = strchr(s + 2, ';'); while (t) { while (isspace(ch = *(unsigned char*)++t)) continue; if (ch == 0) return; if (ch == 'h') break; t = strchr(t + 1, ';'); } if (t == 0) // unlikely return; while (isspace(ch = *(unsigned char*)++t)) continue; if (ch != '=') return; /* * Copy each header field name */ char *length = nentries + 1; char *out = length + 1; for (;;) { while (isspace(ch = *(unsigned char*)++t)) continue; unsigned len = 0; while (len < UCHAR_MAX) { if (ch == '=') // hex-octet { ch = alte_hex(++t); if (*t) ++t; } else if (ch == ':' || ch == ';' || isspace(ch)) break; *out++ = ch; len += 1; ch = *(unsigned char*)++t; } ++*nentries; *length = len; length = out++; while (isspace(ch)) ch = *(unsigned char*)++t; if (ch != ':') break; } } static int field_is_signed(original_header *oh, char *dkim_hdr) { assert(oh); assert(dkim_hdr); if (*dkim_hdr != '\t') transfigure_dkim_hdr(dkim_hdr); assert(*dkim_hdr == '\t'); int nentries = *(unsigned char*)(dkim_hdr+1); char *p = dkim_hdr + 2; for (int i = 0; i < nentries; ++i) { unsigned length = *(unsigned char*)p++; if (length == oh->name_length && strincmp(p, oh->field, length) == 0) return 1; p += length; } return 0; } static void free_parm_dyn_k(dkimfl_parm *parm) { if (parm->dyn.k.key) { memset(parm->dyn.k.key, 0, strlen(parm->dyn.k.key)); free(parm->dyn.k.key); parm->dyn.k.key = NULL; } if (parm->dyn.k.selector) { free(parm->dyn.k.selector); parm->dyn.k.selector = NULL; } if (parm->dyn.k.domain) { free(parm->dyn.k.domain); parm->dyn.k.domain = NULL; } } struct verify_parms; typedef struct context { struct verify_parms *vh; sign_parms *sh; } context; static void sign_message(dkimfl_parm *parm) /* * Possibly sign the message, set rtc 1 if signed, -1 if failed, * leave rtc as-is (0) if there is no need to rewrite. * * The message is read twice. On the first pass header and body are read * and canonicalized. On the second pass, the signed message is written. */ { assert(parm); assert(parm->dyn.k.key == NULL); assert(parm->dyn.k.selector == NULL); assert(parm->dyn.k.domain == NULL); sign_parms sh; memset(&sh, 0, sizeof sh); sh.parm = parm; if (parm->dyn.do_seal) { sh.ar = cstr_init(1024); if (sh.ar == NULL) { parm->dyn.rtc = -1; return; } } context ctx; ctx.sh = &sh; ctx.vh = NULL; if (vb_init(&parm->dyn.vb)) { parm->dyn.rtc = -1; return; } /* * Reject the message if the user is banned from sending, * but allow (emergency?) messages --special-- that is, the * only recipient is the postmaster at the signing domain. */ if (user_is_blocked(parm)) { char *user_domain = default_domain_choice(parm, '*'); recipient_s_domains(parm, user_domain); if (parm->dyn.special) { if (parm->z.verbose >= 3) fl_report(LOG_INFO, "id=%s: allowing blocked user %s to send to postmaster@%s", parm->dyn.info.id, parm->dyn.auth_or_relay, user_domain); } else { static const char null_domain[] = "--domain misconfigured--"; static const char templ[] = "550 BLOCKED: can send to only.\n"; clean_stats(parm); char const *print_domain = user_domain? user_domain: null_domain; char *smtp_reason = malloc(sizeof templ + strlen(print_domain)); if (smtp_reason) { sprintf(smtp_reason, templ, print_domain); fl_pass_message(parm->fl, smtp_reason); fl_free_on_exit(parm->fl, smtp_reason); parm->dyn.rtc = 2; } else parm->dyn.rtc = -1; if (parm->z.verbose >= 3 || parm->dyn.rtc < 0) fl_report(parm->dyn.rtc < 0? LOG_CRIT: LOG_INFO, "id=%s: %s user %s from sending", parm->dyn.info.id, parm->dyn.rtc == 2? "blocked": "MEMORY FAULT trying to block", parm->dyn.auth_or_relay); } } if (parm->dyn.rtc == 0) /* * Create dkim signing context. */ { DKIM_STAT status; dkim_alg_t signalg = /* DKIM_SIGN_DEFAULT is also valid for ed25519 */ parm->z.sign_rsa_sha1? DKIM_SIGN_RSASHA1: DKIM_SIGN_DEFAULT; sh.dkim = dkim_sign(parm->dklib, parm->dyn.info.id, NULL, // parm->dyn.k.key, selector, parm->dyn.k.domain, parm->z.header_canon_relaxed? DKIM_CANON_RELAXED: DKIM_CANON_SIMPLE, parm->z.body_canon_relaxed? DKIM_CANON_RELAXED: DKIM_CANON_SIMPLE, signalg, ULONG_MAX /* signbytes */, &status); if (sh.dkim == NULL || status != DKIM_STAT_OK) { if (parm->z.verbose) { char const *err = dkim_getresultstr(status); fl_report(LOG_ERR, "id=%s: dkim_sign failed (%d, %sNULL): %s", parm->dyn.info.id, (int)status, sh.dkim? "non-": "", err? err: "unknown"); } parm->dyn.rtc = -1; } else dkim_set_user_context(sh.dkim, &ctx); } /* * First pass. */ if (parm->dyn.rtc == 0 && sign_headers(&sh) == 0) { if (parm->dyn.k.key == NULL || parm->dyn.k.domain == NULL) { if (parm->z.verbose >= 2) fl_report(LOG_INFO, "id=%s: not s%sing for %s: no %s", parm->dyn.info.id, parm->dyn.do_seal? "eal": "ign", parm->dyn.auth_or_relay, parm->dyn.k.domain? "key": "domain"); // add to db even if not signed if (parm->dyn.stats) stats_outgoing(parm); free_parm_dyn_k(parm); vb_clean(&parm->dyn.vb); clear_sign_parms(&sh); return; } char *selector = parm->dyn.k.selector? parm->dyn.k.selector: parm->z.selector? parm->z.selector: "s"; DKIM_STAT status = dkim_signkeys(sh.dkim, parm->dyn.k.key, selector, parm->dyn.k.domain); if (parm->z.verbose >= 6 && sh.dkim && status == DKIM_STAT_OK) { char const *signalg_name = dkim_getsignalg(sh.dkim); fl_report(LOG_INFO, "id=%s: s%sing for %s with domain %s, selector %s, %s", parm->dyn.info.id, parm->dyn.do_seal? "eal": "ign", parm->dyn.auth_or_relay, parm->dyn.k.domain, selector, signalg_name? signalg_name: "Unknown key type"); } if (parm->dyn.do_seal && parm->dyn.authserv_id == NULL) /* * If found no A-R headers, use domain instead. */ { parm->dyn.authserv_id = parm->dyn.k.domain; parm->dyn.k.domain = NULL; } free_parm_dyn_k(parm); if (status != DKIM_STAT_OK) { if (parm->z.verbose) { char const *err = dkim_getresultstr(status); fl_report(LOG_ERR, "id=%s: dkim_signkeys failed (%d): %s", parm->dyn.info.id, (int)status, err? err: "unknown"); } parm->dyn.rtc = -1; vb_clean(&parm->dyn.vb); clear_sign_parms(&sh); return; } status = dkim_eoh(sh.dkim); if (status != DKIM_STAT_OK) { if (parm->z.verbose >= 3) { char const *err = dkim_getresultstr(status); fl_report(LOG_INFO, "id=%s: signing dkim_eoh: %s (stat=%d)", parm->dyn.info.id, err? err: "(NULL)", (int)status); } } if (parm->dyn.do_seal) { struct arc_info arc_info; if (dkim_getarcinfo(sh.dkim, &arc_info) == DKIM_STAT_OK) { switch (arc_info.arc_state) { case ARC_CHAIN_SEAL_OK: if (parm->z.verbose >= 6 && arc_info.arc_data_available) fl_report(LOG_INFO, "id=%s: ARC sealing after i=%d %s", parm->dyn.info.id, arc_info.dkim_arccount, dkim_getsealdomain(sh.dkim, arc_info.dkim_arccount - 1)); break; case ARC_CHAIN_FAIL: case ARC_CHAIN_TEMPERROR: case ARC_CHAIN_PERMERROR: if (parm->z.verbose >= 3) fl_report(LOG_ERR, "id=%s: ARC failure: %s: setting cv=fail", parm->dyn.info.id, dkim_getsealerror(sh.dkim)); break; case ARC_CHAIN_FAIL_CV: // log returning from arc_do_seal case ARC_CHAIN_NONE: // nothing to say break; case ARC_CHAIN_UNKNOWN: case ARC_CHAIN_PASS: default: assert(0); break; } } } } if (parm->dyn.rtc == 0 && sh.qp_to_base64) { copy_body_to_dkim_parm cbvp; memset(&cbvp, 0, sizeof cbvp); cbvp.dkim = sh.dkim; cbvp.dyn_info = parm->dyn.info.id; cbvp.id_or_ip = "id"; cbvp.verbose = parm->z.verbose; if ((sh.vc = append_transform_stack(NULL, &decode_quoted_printable, NULL)) == NULL || (sh.vc = append_transform_stack(sh.vc, &encode_base64, NULL)) == NULL || (sh.vc = append_transform_stack(sh.vc, ©_body_to_dkim, &cbvp)) == NULL) { parm->dyn.rtc = -1; } else if (parm->z.verbose >= 5) { fl_report(LOG_INFO, "id=%s: re-encode qp message to base64", parm->dyn.info.id); } } if (parm->dyn.rtc == 0 && copy_body(parm, sh.vc? NULL: sh.dkim, sh.vc) == 0) { vb_clean(&parm->dyn.vb); clear_transform_stack(sh.vc); sh.vc = NULL; DKIM_STAT status = dkim_eom(sh.dkim, NULL); if (status != DKIM_STAT_OK) { if (parm->z.verbose) { char const *err = dkim_geterror(sh.dkim); if (err == NULL) err = dkim_getresultstr(status); fl_report(LOG_ERR, "id=%s: dkim_eom failed (%d): %s", parm->dyn.info.id, (int)status, err? err: "unknown"); } parm->dyn.rtc = -1; } } stats_outgoing(parm); // after sign_headers to check From: /* * Second pass. */ FILE *fp = NULL; char *dkim_hdr = NULL; if (parm->dyn.rtc == 0) { fp = fl_get_write_file(parm->fl); if (parm->dyn.do_seal == 0) { size_t len; DKIM_STAT status = dkim_getsighdr_a(sh.dkim, &dkim_hdr, &len, sizeof DKIM_SIGNHEADER + 1); if (fp == NULL || status != DKIM_STAT_OK) { parm->dyn.rtc = -1; dkim_free(sh.dkim); sh.dkim = NULL; } else // Write signature as first field { chomp_cr(dkim_hdr); fprintf(fp, DKIM_SIGNHEADER ": %s\n", dkim_hdr); } } else { unsigned int ar_len = cstr_length(sh.ar); if (sh.ar) sh.ar = cstr_insstr(sh.ar, 0, parm->dyn.authserv_id? parm->dyn.authserv_id: "default"); if (sh.ar && ar_len == 0) sh.ar = cstr_addstr(sh.ar, "; none"); char *rhd[3]; DKIM_STAT status = sh.ar? arc_do_seal(sh.dkim, cstr_get(sh.ar), rhd): DKIM_STAT_NORESOURCE; free(sh.ar); sh.ar = NULL; if (status == ARC_CV_FAIL_ALREADY) { if (parm->z.verbose >= 1) fl_report(LOG_ERR, "id=%s: found ARC-Seal with cv=fail, no need to add more.", parm->dyn.info.id); } else if (fp == NULL || status != DKIM_STAT_OK) { parm->dyn.rtc = -1; dkim_free(sh.dkim); sh.dkim = NULL; } else // Write ARC set as first fields { for (int i = 0; i < 3; ++i) { chomp_cr(rhd[i]); fprintf(fp, "%s\n", rhd[i]); free(rhd[i]); } } } } if (parm->dyn.rtc == 0 && parm->dyn.wrapped == 0) { // A-R, possibly signed, is second field if (sh.auth_pass) { chomp_cr(sh.auth_pass); fputs(sh.auth_pass, fp); fputc('\n', fp); free(sh.auth_pass); sh.auth_pass = NULL; } } if (parm->dyn.rtc == 0 && parm->dyn.do_seal == 0 && sh.original_header_fields_exist == 0 && parm->z.disable_experimental == 0) { // Original- fields while (sh.oh_base) { original_header *oh = sh.oh_base; if (oh->only_if_signed == 0 || field_is_signed(oh, dkim_hdr)) { char *const field = oh->field; chomp_cr(field); if (oh->is_author) fprintf(fp, "Author%s\n", field + oh->colon_ndx); else fprintf(fp, "Original-%s\n", field); } sh.oh_base = oh->next; free(oh); } // this doesn't frees the possibly transfigured dkim_hdr dkim_free(sh.dkim); sh.dkim = NULL; } FILE *in = NULL; if (parm->dyn.rtc == 0 && fp) /* * Write the message header if there are replacements. * (Assert there are replacements if sh.qp_to_base64). */ { in = fl_get_file(parm->fl); assert(in); rewind(in); assert(sh.qp_to_base64 == 0 || sh.repl != NULL); copy_replacement(parm, in, fp, sh.repl); } if (parm->dyn.rtc == 0 && fp && in) /* * Write the message body. If signed a base64, write base64 again. */ { assert(sh.vc == NULL); // otherwise rtc should be -1 if (sh.qp_to_base64) { base64_encode_parm b64; memset(&b64, 0, sizeof b64); b64.nooutcr = 1; copy_body_to_file_parm cbtf; memset(&cbtf, 0, sizeof cbtf); cbtf.fp = fp; cbtf.dyn_info = parm->dyn.info.id; cbtf.id_or_ip = "id"; cbtf.verbose = parm->z.verbose; if ((sh.vc = append_transform_stack(NULL, &decode_quoted_printable, NULL)) == NULL || (sh.vc = append_transform_stack(sh.vc, &encode_base64, &b64)) == NULL || (sh.vc = append_transform_stack(sh.vc, ©_body_to_file, &cbtf)) == NULL || copy_body(parm, NULL, sh.vc) != 0) parm->dyn.rtc = -1; else parm->dyn.rtc = 1; } else if (filecopy(in, fp) == 0) parm->dyn.rtc = 1; else parm->dyn.rtc = -1; } /* * Cleanup */ free(dkim_hdr); vb_clean(&parm->dyn.vb); clear_sign_parms(&sh); } // verify typedef struct verify_parms { domain_prescreen *domain_head, **domain_ptr; // not malloc'd or maintained elsewhere dkimfl_parm *parm; char const *policy_type, *policy_result, *policy_comment; // dps for relevant methods/policies domain_prescreen *author_dps, *whitelisted_dps, *dnswl_dps; DKIM *dkim; FILE *outfile; dkim_transform dt; dmarc_domains dd; dmarc_rec dmarc; size_t received_spf; int dnswl_count; // total number of signatures, 0 on error int nsigs; // number of DKIM signing domains, elements of domain_ptr int ndoms; int policy; int presult; int do_adsp, do_dmarc; enum vh_step {vh_step_verify, vh_step_retry, vh_step_copy} step; // unsigned int org_domain_in_dwa: 1; unsigned int aligned_spf_pass: 1; unsigned int domain_flags:1; unsigned int have_spf_pass:1; unsigned int have_trusted_voucher:1; unsigned int shoot_on_sight:1; unsigned int trans_did_pass:1; unsigned int forced_authentication_policy:1; } verify_parms; static int is_trusted_voucher(char const **const tv, char const *const voucher) // return non-zero if voucher is in the trusted_voucher list // the returned value is 1 + the index of trust, for sorting and mv2tv { if (tv && voucher) for (int i = 0; tv[i] != NULL; ++i) if (stricmp(voucher, tv[i]) == 0) return i + 1; return 0; } static inline char* mv2tv(char *mv, char const **const tv) { int i = is_trusted_voucher(tv, mv) - 1; return (char*) (i >= 0? tv[i]: NULL); } static void clean_vh(verify_parms *vh) { clear_prescreen(vh->domain_head); clear_dkim_transform(&vh->dt); free(vh->domain_ptr); free(vh->dmarc.rua); // these may be allocated by pst's org_domain() // called from domain_flag_from() free(vh->dd.domain); free(vh->dd.super_org); free(vh->dd.org_domain); free(vh->dt.dd.domain); free(vh->dt.dd.super_org); free(vh->dt.dd.org_domain); dkim_free(vh->dkim); } static int check_db_connected(dkimfl_parm *parm) /* * Track db_connected. Connection is only attempted if dwa was inited. * On connection, pass the authenticated user and the client IP, if any. * * This function must be called before attempting any query. * * Return -1 on hardfail, 0 otherwise. */ { assert(parm); assert(parm->fl); db_work_area *const dwa = parm->dwa; if (dwa == NULL || parm->dyn.db_connected) return 0; if (db_connect(dwa) != 0) return -1; parm->dyn.db_connected = 1; char *s = NULL; int got_it = 0; if ((s = parm->dyn.auth_or_relay) != NULL) // outgoing { char *dom = strchr(s, '@'); if (dom) *dom = 0; if (*s == 0) // no local part s = "postmaster"; db_set_authenticated_user(dwa, s, dom? dom + 1: NULL); if (dom) *dom = '@'; } if ((s = parm->dyn.info.frommta) != NULL && strincmp(s, "dns;", 4) == 0) /* for esmtp, this is done by courieresmtpd.c as: if (!host) host=""; argv[n]=buf=courier_malloc(strlen(host)+strlen(tcpremoteip)+strlen( helobuf)+sizeof("dns; ( [])")); strcat(strcat(strcpy(buf, "dns; "), helobuf), " ("); if (*host) strcat(strcat(buf, host), " "); strcat(strcat(strcat(buf, "["), tcpremoteip), "])"); and then conveyed to ctlfile 'f' (COMCTLFILE_FROMMTA). E.g. fdns; helobuf (rdns.host.example [192.0.2.1]) where host is the reverse lookup (iprev), if enabled. */ { s = strrchr(s + 4, '('); if (s && *++s) { char *iprev = s, *en = NULL; if (*iprev == '[') { s = iprev; iprev = NULL; } else { en = strchr(iprev, ' '); if (en) { s = en + 1; *en = 0; } } char *e = strchr(s, ']'); if (e) { *e = 0; if (*s == '[') ++s; if (strncmp(s, "::ffff:", 7) == 0) s += 7; db_set_client_ip(dwa, s, iprev); *e = ']'; got_it = 1; } if (en) *en = ' '; } } if (!got_it) { if (parm->dyn.info.tcpremoteip) db_set_client_ip(dwa, parm->dyn.info.tcpremoteip, NULL); else if (fl_whence(parm->fl) == fl_whence_other && // working stdalone (s = getenv("REMOTE_ADDR")) != NULL) { db_set_client_ip(dwa, s, NULL); } } return 0; } static inline int change_sign(int old, int newval) { return (old <= 0 && newval > 0) || (old > 0 && newval <= 0); } static int domain_flag_from(verify_parms *vh, int is_original) /* * Set up domain_prescreen for From: or the best Original-From:, * looking up org and super domains as needed, and setting flags. */ { assert(vh); assert(is_original >= 0 && is_original <= 1); assert(vh->dt.cf[is_original] && vh->dt.cf[is_original]->domain); publicsuffix_trie const *const pst = vh->parm->pst; publicsuffix_trie const *const pst2 = vh->parm->pst2; dmarc_domains *dd = is_original? &vh->dt.dd: &vh->dd; char *from = dd->domain; if (from == NULL) { /* * This was dkim_getdomain(dkim), set by dkim_eoh_verify(), * before calling the prescreen function. */ dd->domain = from = normalize_utf8_twice(vh->dt.cf[is_original]->domain); if (from == NULL) return 0; } domain_prescreen *dps = get_prescreen(&vh->domain_head, from); if (dps == NULL) return vh->parm->dyn.rtc = -1; vh->dt.cf[is_original]->dps = dps; if (is_original) dps->u.f.is_o_from = dps->u.f.is_o_aligned = 1; else dps->u.f.is_from = dps->u.f.is_aligned = 1; if (pst) { char *od = dd->org_domain; if (od == NULL) od = dd->org_domain = org_domain(pst, from); if (od != NULL) { domain_prescreen *dps_org; if (strcmp(from, od) != 0) { dps_org = get_prescreen(&vh->domain_head, od); if (dps_org == NULL) return vh->parm->dyn.rtc = -1; dps->org = dps_org; } else dps_org = dps; if (is_original) dps_org->u.f.is_o_org_domain = dps_org->u.f.is_o_aligned = 1; else dps_org->u.f.is_org_domain = dps_org->u.f.is_aligned = 1; if (pst2) { char *sod = dd->super_org; if (sod == NULL) sod = dd->super_org = org_domain(pst2, od); if (sod != NULL) { domain_prescreen *dps_org_org; if (strcmp(od, sod) != 0) { dps_org_org = get_prescreen(&vh->domain_head, sod); if (dps_org_org == NULL) return vh->parm->dyn.rtc = -1; dps_org->org = dps_org_org; } else dps_org_org = dps_org; // need a dps to get domain flags, e.g. do_dmarc if (is_original) dps_org_org->u.f.is_o_super_org_domain = 1; else dps_org_org->u.f.is_super_org_domain = 1; } } } } return 0; } static int compare_candidate_from(candidate_from *cf1, candidate_from *cf2) { if (cf1 == NULL) return cf2 == NULL? 0: -1; if (cf2 == NULL) return 1; if (cf1->dps == NULL) return cf2->dps == NULL? 0: -1; if (cf2->dps == NULL) return 1; return cf1->substring_length - cf2->substring_length; } static int domain_flags(verify_parms *vh) /* * Called either before or after checking signatures; that is, either * at end-of-header or at end-of-message. * Set domain_val to sort domains. Check organizational domain and alignment * assuming relaxed policy --to be undone if a non-relaxed policy is discovered. * Retrieve per-domain whitelisting and adsp/dmarc settings. */ { assert(vh && vh->parm); assert(vh->step < vh_step_copy); if (vh->domain_flags) return 0; if (vh->dt.cf[0] && vh->dt.cf[0]->domain) { if (domain_flag_from(vh, 0) < 0) return vh->parm->dyn.rtc = -1; if (vh->dt.cf[0]->mailbox) { candidate_from **cf = vh->dt.cf; cf[0]->substring_length = strlen(cf[0]->mailbox); // best char *at = strrchr(cf[0]->mailbox, '@'); char *less = strrchr(cf[0]->mailbox, '<'); // remove domain, to avoid spurious copies to self // we're looking at a matching display name if (at) *at = 0; if (less) *less = 0; for (unsigned int i = 1; i < MAX_FROM_CANDIDATES; ++i) { if (cf[i] && cf[i]->domain) { /* * Only consider signing domains, which have been seen * already before calling this function. * TODO: what if there is a signature by a sub/super domain? */ if (cf[i]->dps == NULL) { domain_prescreen *dps = find_prescreen(vh->domain_head, cf[i]->domain); if (dps == NULL) { free(cf[i]->mailbox); cf[i]->mailbox = NULL; continue; } cf[i]->dps = dps; } if (cf[i]->mailbox) { char *at_i = strrchr(cf[i]->mailbox, '@'); char *less_i = strrchr(cf[i]->mailbox, '<'); if (at_i) *at_i = 0; if (less_i) *less_i = 0; cf[i]->substring_length = longest_substring(cf[0]->mailbox, cf[i]->mailbox); if (at_i) *at_i = '@'; if (less_i) *less_i = '<'; } } } if (at) *at = '@'; if (less) *less = '<'; /* * Sort cf[i]'s, 1 <= i < MAX_FROM_CANDIDATES */ for (int i = 1; i < MAX_FROM_CANDIDATES;) { if (i == 1 || compare_candidate_from(cf[i], cf[i-1]) <= 0) ++i; else { candidate_from *tmp = cf[i -1]; cf[i-1] = cf[i]; cf[i] = tmp; --i; } } } } if (vh->dt.cf[1] && vh->dt.cf[1]->dps) /* * Add a candidate From: if it seems plausible */ { if (vh->dt.cf[1]->substring_length >= MIN_SUBSTRING_LENGTH && vh->dt.cf[0] && vh->dt.cf[1]->mailbox && vh->dt.cf[0]->mailbox && strcmp(vh->dt.cf[1]->mailbox, vh->dt.cf[0]->mailbox) != 0) { char buf[strlen(vh->dt.cf[1]->mailbox) + 10]; strcat(strcpy(buf, "From: "), vh->dt.cf[1]->mailbox); if (add_to_original(&vh->dt, buf) < 0) { vh->parm->dyn.rtc = -1; return -1; } char *replaceable = NULL; switch (vh->dt.cf[1]->is) { case candidate_is_author: vh->dt.author_is_candidate = 1; break; case candidate_is_original_from: vh->dt.original_from_is_candidate = 1; break; case candidate_is_reply_to: replaceable = "Reply-To"; break; case candidate_is_cc: replaceable = "Cc"; break; default: break; } /* * If the original From: was copied to Reply-To: or Cc:, * probably that value was not there originally. */ if (replaceable && peek_original_header(&vh->dt, replaceable) == NULL) { if (vh->dt.cf[1]->part == 0) // whole header { char empty_field[16]; strncat(strncpy(empty_field, replaceable, sizeof empty_field), ":", sizeof empty_field); add_to_original(&vh->dt, empty_field); } else { char *start; char const *h = dkim_get_processed_header(vh->dkim, replaceable, 0); if (h && (start = strdup(h)) != NULL) { char *s = hdrval(start, replaceable); char *t = &s[vh->dt.cf[1]->part]; *t = 0; // trim right while (t > s && isspace(*(unsigned char*)(t - 1))) *--t = 0; add_to_original(&vh->dt, start); free(start); } } } if (vh->dt.cf[0]->dps && vh->dt.cf[0]->dps != vh->dt.cf[1]->dps) vh->dt.cf[0]->dps->u.f.is_not_original = 1; } } if (check_original_subject(&vh->dt) < 0) // possibly add Original-Subject: return -1; if (vh->dt.cf[1] && vh->dt.cf[1]->dps && vh->dt.cf[1]->substring_length >= MIN_SUBSTRING_LENGTH) { assert(vh->dt.cf[1]->domain); // precondition for dps if ((vh->dt.cf[0] == NULL || vh->dt.cf[0]->dps != vh->dt.cf[1]->dps) && domain_flag_from(vh, 1) < 0) return vh->parm->dyn.rtc = -1; assert(vh->dt.cf[1]->dps->u.f.is_o_from || vh->dt.cf[0]->dps == vh->dt.cf[1]->dps); } if (vh->dt.sender_domain) { domain_prescreen *dps = get_prescreen(&vh->domain_head, vh->dt.sender_domain); if (dps == NULL) return vh->parm->dyn.rtc = -1; dps->u.f.is_sender = 1; } if (vh->dt.list_domain) { domain_prescreen *dps = get_prescreen(&vh->domain_head, vh->dt.list_domain); if (dps == NULL) return vh->parm->dyn.rtc = -1; dps->u.f.is_list = 1; } db_work_area *const dwa = vh->parm->dwa; if (dwa && check_db_connected(vh->parm) < 0) return -1; for (domain_prescreen *dps = vh->domain_head; dps; dps = dps->next) { dps->domain_val = 0; if (dps->u.f.is_o_from) { dps->domain_val += 4500; // original author's domain } else if (dps->u.f.is_o_org_domain || dps->u.f.is_o_super_org_domain) { dps->domain_val += 4000; // original author's organizational domain } else if (vh->dt.dd.org_domain && is_subdomain(dps->name, vh->dt.dd.org_domain)) { dps->u.f.is_o_aligned = 1; dps->domain_val += 3500; // domain aligned to original } else if (dps->u.f.is_from) { dps->domain_val += 2500; // author's domain } else if (dps->u.f.is_org_domain || dps->u.f.is_super_org_domain) { dps->domain_val += 2000; // author's organizational domain } else if (vh->dd.org_domain && is_subdomain(dps->name, vh->dd.org_domain)) { dps->u.f.is_aligned = 1; dps->domain_val += 1500; // aligned domain } if (dwa) { int dmarc = 0, adsp = 0, dcount = 0, c = db_get_domain_flags(dwa, dps->name, &dps->whitelisted, &dmarc, &adsp, &dcount); if (vh->parm->z.verbose >= 8) fl_report(LOG_DEBUG, "ip=%s: %d db_sql_domain_flags: " "%d, %d, %d, %d for %s", vh->parm->dyn.info.tcpremoteip, c, dps->whitelisted, dmarc, adsp, dcount, dps->name); if (c > 0) { if (dps->whitelisted >= 0) { if (dps->whitelisted > 0) { dps->u.f.is_known = 1; if (dps->whitelisted > 1) { dps->u.f.is_whitelisted = 1; if (dps->whitelisted > 2) dps->u.f.is_trusted = 1; } dps->domain_val += dps->whitelisted * 500; } } else { vh->shoot_on_sight = 1; dps->u.f.shoot_on_sight = 1; if (vh->parm->z.verbose >= 5) fl_report(LOG_NOTICE, "ip=%s: shoot on sight for %s", vh->parm->dyn.info.tcpremoteip, dps->name); } if (c > 1 && (dps->u.f.is_aligned || dps->u.f.is_super_org_domain)) /* * DMARC is enabled/disabled based on "official" From: only. * (From: rewriting is meant to skip exactly this.) */ { if (dmarc > 1) vh->forced_authentication_policy = 1; int old = vh->do_dmarc; dmarc += old; vh->do_dmarc = dmarc; if (change_sign(old, dmarc) && vh->parm->z.verbose >= 8) fl_report(LOG_WARNING, "ip=%s: %sabling DMARC for %s: %d -> %d", vh->parm->dyn.info.tcpremoteip, dmarc > 0? "en": "dis", dps->name, old, dmarc); if (c > 2 && dps->u.f.is_from) { old = vh->do_adsp; adsp += old; vh->do_adsp = adsp; if (change_sign(old, adsp) && vh->parm->z.verbose >= 8) fl_report(LOG_WARNING, "ip=%s: %sabling ADSP for %s: %d -> %d", vh->parm->dyn.info.tcpremoteip, adsp > 0? "en": "dis", dps->name, old, adsp); } } } else dps->whitelisted = 0; } if (dps->u.f.is_dnswl) dps->domain_val += 200; // dnswl relay's signature if (dps->u.f.is_mfrom && dps->spf >= spf_neutral) dps->domain_val += 100; // sender's domain signature if (dps->u.f.is_helo && dps->spf >= spf_neutral) dps->domain_val += 15; // relay's signature } vh->domain_flags = 1; return 0; } #if 0 static inline int sig_ignored(DKIM_SIGINFO *const sig) { unsigned int const sig_flags = dkim_sig_getflags(sig); return (sig_flags & DKIM_SIGFLAG_IGNORE) != 0; } #endif static int domain_sort(verify_parms *vh, DKIM_SIGINFO** sigs, int nsigs) /* * Setup the domain prescreen that will eventually be passed to the database. * Domains are drawn from the signature array; domains from other authentication * methods yielding a domain name (from, mfrom, helo, and dnswl) are already in * the linked list beginning at vh->domain_head = dps_head. * * domain_ptr is an array of sorted pointers to dps structures. Only signing * domains are in there. * * Two temporary arrays are used, sigs_mirror is * a signature-to-domain map, and sigs_copy is a swap area. * * Signatures are not yet verified at this time (sort affects verify order). * * return -1 for fatal error, number of domains otherwise */ { assert(vh && vh->parm); int ndoms = nsigs; // max number of signing domains int good = 1; domain_prescreen** domain_ptr = calloc(ndoms+1, sizeof(domain_prescreen*)); domain_prescreen** sigs_mirror = calloc(nsigs+1, sizeof(domain_prescreen*)); DKIM_SIGINFO** sigs_copy = calloc(nsigs+1, sizeof(DKIM_SIGINFO*)); domain_prescreen **dps_head = &vh->domain_head; if (!(domain_ptr && sigs_mirror && sigs_copy)) good = 0; // size_t const helolen = helo? strlen(helo): 0; ndoms = 0; /* * 1st pass: Create prescreens with name-based domain evaluation. * Fill in sigs_mirror and domain_ptr. */ if (good) for (int c = 0; c < nsigs; ++c) { DKIM_SIGERROR const sigerr = dkim_sig_geterror(sigs[c]); char *domain = dkim_sig_getdomain(sigs[c]); if (domain && *domain && (sigerr == DKIM_SIGERROR_UNKNOWN || sigerr == DKIM_SIGERROR_OK)) { domain_prescreen *dps = get_prescreen(dps_head, domain); if (dps == NULL) { good = 0; break; } sigs_mirror[c] = dps; if (dps->nsigs++ == 0) // first time domain seen in this loop { domain_ptr[ndoms++] = dps; } } else /* * Discard signatures with formal errors */ { if (vh->parm->z.verbose >= 5) { if (domain == NULL) domain = ""; else if (*domain == 0) domain = ""; char const *sigerrstr = dkim_sig_geterrorstr(sigerr); fl_report(LOG_INFO, "ip=%s: ignoring signature by %s: %s", vh->parm->dyn.info.tcpremoteip, domain, sigerrstr? sigerrstr: ""); } dkim_sig_ignore(sigs[c]); sigs_mirror[c] = NULL; } } if (good && domain_flags(vh)) good = 0; if (!good) { free(domain_ptr); free(sigs_mirror); free(sigs_copy); return -1; } /* * Sort domain_ptr, based on domain flags. Use gnome sort, as we * expect 2 ~ 4 elements. (It starts getting sensibly slow with * 1000 elements --1ms on nocona xeon.) */ for (int c = 0; c < ndoms;) { if (c == 0 || domain_ptr[c]->domain_val <= domain_ptr[c-1]->domain_val) ++c; else { domain_prescreen *const dps = domain_ptr[c]; domain_ptr[c] = domain_ptr[c-1]; domain_ptr[c-1] = dps; --c; } } /* * Allocate indexes in the sorted sig array. Reuse sigval as next_index. */ int next_ndx = 0; for (int c = 0; c < ndoms; ++c) { domain_prescreen *const dps = domain_ptr[c]; dps->sigval = dps->start_ndx = next_ndx; next_ndx += dps->nsigs; assert(dps->nsigs > 0); } /* * Make a copy of sigs, then * 2nd pass: Rewrite sigs array based on allocated indexes. * That way, domains are ordered by preference, while signatures for each * domain are gathered in their original order. */ assert(nsigs <= MAX_SIGNATURES); if (nsigs) { unsigned const nsigs_len = nsigs * sizeof(DKIM_SIGINFO*); memcpy(sigs_copy, sigs, nsigs_len); for (int c = 0; c < nsigs; ++c) { DKIM_SIGINFO *const sig = sigs_copy[c]; domain_prescreen *const dps = sigs_mirror[c]; if (dps) { sigs[dps->sigval++] = sig; } else if (next_ndx < nsigs) /* * Put signatures without domain at the end. */ { sigs[next_ndx++] = sig; } } } free(sigs_mirror); free(sigs_copy); vh->ndoms = ndoms; if (ndoms) { vh->domain_ptr = realloc(domain_ptr, ndoms * sizeof(domain_prescreen*)); if (vh->parm->dyn.stats) { vh->parm->dyn.stats->signatures_count = nsigs; if (vh->parm->z.log_dkim_order_above > 0 && vh->parm->z.log_dkim_order_above < ndoms && vh->parm->z.verbose >= 3) fl_report(LOG_WARNING, "id=%s: %d DKIM signing domains, current dkim order is %d.", vh->parm->dyn.info.id, ndoms, vh->parm->z.log_dkim_order_above); } } else { free(domain_ptr); clean_stats(vh->parm); } return ndoms; } typedef enum message_disposition { dispo_deliver, dispo_quarantine, dispo_drop, dispo_reject } message_disposition; static char const *explain_dispo(message_disposition dispo) { switch (dispo) { case dispo_deliver: return "deliver"; case dispo_quarantine: return "quarantine"; case dispo_drop: return "drop"; case dispo_reject: return "reject"; default: return "undefined disposition"; } } typedef struct found_footer { dkim_transform *dt; char *boundary; char *first_content_type; long length, footer, entity_start, entity_end, empty_entity_start; unsigned boundary_length; unsigned lines_after_footer, non_empty_lines_before_footer; unsigned entity_count; unsigned long_lines; unsigned found_content_type: 3; // see fff_ct enum below unsigned found_content_transfer_encoding: 1; unsigned long_line: 1; unsigned footer_found: 1; unsigned entity_header: 1; unsigned partial_cleanup: 1; unsigned is_text_plain: 1; unsigned body_seen: 1; unsigned decoded_entity: 1; unsigned fatal: 1; cte_transform cte; } found_footer; typedef enum fff_ct // found footer: found content type { fff_ct_not_seen, fff_ct_current, fff_ct_done } fff_ct; static inline int ff_check_footer(found_footer *ff, char *in, unsigned len) { assert(ff); assert(in); if (ff->footer_found) { ++ff->lines_after_footer; return 0; } int rtc = strspn(in, "_-") >= len - 2 && len > 6; if (!rtc && len == 5 && in[0] == '-' && in[1] == '-' && in[2] == ' ') rtc = 1; // Usenet Signature Convention "-- \r\n" if (rtc) { ff->footer_found = 1; ff->lines_after_footer = 0; } return rtc; } static inline int ff_check_footer_single(found_footer *ff, char *in, unsigned len) /* * Different reasoning. You can have several footers in the same entity. */ { assert(ff); assert(in); if (ff->footer_found) ++ff->lines_after_footer; int rtc = strspn(in, "_-") >= len - 2 && len > 6; if (!rtc && len == 5 && in[0] == '-' && in[1] == '-' && in[2] == ' ') rtc = 1; // Usenet Signature Convention "-- \r\n" if (rtc) { ff->footer_found = 1; ff->lines_after_footer = 0; } return rtc; } static int find_footers_single(char *in, unsigned len, transform_stack *vc) /* * Parse the body, one line at a time. * * Decoding functions are set up as needed. * * A valid footer is in a text/plain entity. * * Set footer where the footer line was found. */ { assert(vc); assert(vc->next == NULL); found_footer *ff = (found_footer*)vc->fn_parm; assert(ff && ff->boundary == NULL); if (in == NULL) { if (len == 1) // cleanup { dkim_transform *dt = ff->dt; if (ff->footer_found && ff->lines_after_footer < 10) { dt->footer_found = 1; dt->footer = 1; dt->length[0] = ff->footer; dt->length[1] = 0; } free(ff->first_content_type); free(ff); vc->fn_parm = NULL; } return 0; } if (in[len-1] == '\n') { if (len <= 2) { if (ff->footer_found) ++ff->lines_after_footer; } else if (!ff->long_line) { if (ff_check_footer_single(ff, in, len)) { ff->footer = ff->length; } } ff->long_line = 0; } else if (ff->long_line == 0) { ff->long_line = 1; ++ff->long_lines; } ff->length += len; return 0; } // forward declaration needed static inline transform_stack * set_decode_functions(cte_transform cte, cte_transform original_cte, found_footer *ff); static inline int ff_is_boundary(found_footer *ff, char *in, unsigned len) { assert(ff); assert(in); return len - 3 >= ff->boundary_length && in[0] == '-' && in[1] == '-' && strncmp(&in[2], ff->boundary, ff->boundary_length) == 0; } static inline void ff_check_header(found_footer *ff, char *in) /* * Track ct and cte. */ { assert(ff && ff->entity_header); assert(in); switch (ff->found_content_type) { case fff_ct_not_seen: { char *s = hdrval(in, content_type); if (s) { if (ff->first_content_type == NULL && ff->fatal == 0) { ff->first_content_type = strdup_normalize(in, 0); if (ff->first_content_type == NULL) { ff->fatal = 1; ff->found_content_type = fff_ct_done; } else ff->found_content_type = fff_ct_current; } else ff->found_content_type = fff_ct_done; ff->is_text_plain = content_type_is_text_plain(s) > 0; return; // don't check content_transfer_encoding... } break; } case fff_ct_current: { if (isspace(*(unsigned char*)in)) { ff->first_content_type = strdup_add(ff->first_content_type, in); if (ff->first_content_type == NULL) ff->fatal = 1; return; } ff->found_content_type = fff_ct_done; break; } default: break; } if (!ff->found_content_transfer_encoding) { char *s = hdrval(in, content_transfer_encoding); if (s) { ff->found_content_transfer_encoding = 1; ff->cte = parse_content_transfer_encoding_simple(s); } } } static int find_footers_multipart(char *in, unsigned len, transform_stack *vc) /* * Parse the body, one line at a time. * * For multipart bodies, count the children of the outermost entity, * so we can determine whether a footer was on the 2nd one (mime-wrap) * or on an added part (add-part). * * A valid footer is in a text/plain entity. * * Set footer where the footer boundary was found. */ { assert(vc); assert(vc->fn_parm); found_footer *ff = (found_footer*)vc->fn_parm; assert(ff && ff->boundary); unsigned decoded_entity = 0; /* * If there is a decode function, run it, unless boundary reached. */ if (vc->next) { if (in && !ff_is_boundary(ff, in, len)) { vc = vc->next; ff->decoded_entity = 1; int rtc = (*vc->fn)(in, len, vc); ff->length += len; return rtc; } /* * We used a decoder. Flush and cleanup its tail. * This is a partial cleanup of the tail only. */ ff->partial_cleanup = 1; (*vc->next->fn)(NULL, 0, vc->next); clear_transform_stack(vc->next); vc->next = NULL; ff->partial_cleanup = 0; ff->decoded_entity = 0; } else decoded_entity = ff->decoded_entity; assert(vc->next == NULL); if (in == NULL) { if (len == 1) // cleanup { if (ff->partial_cleanup == 0) { dkim_transform *dt = ff->dt; if (ff->footer && ff->entity_count > 1) { dt->footer_found = 1; if (ff->entity_count == 2) { dt->mime_wrap = 1; dt->first_content_type = ff->first_content_type; ff->first_content_type = NULL; } else { dt->length[0] = ff->footer; dt->length[1] = ff->entity_end; dt->add_part = 1; } } free(ff->first_content_type); free(ff); vc->fn_parm = NULL; } } return 0; } if (in[len-1] == '\n') { if (len <= 2) // CRLF { if (decoded_entity == 0 && ff->entity_header) { ff->entity_header = 0; if (ff->found_content_type == fff_ct_not_seen) ff->is_text_plain = 1; if (ff->is_text_plain && ff->found_content_transfer_encoding && (ff->cte == cte_transform_base64 || ff->cte == cte_transform_quoted_printable)) /* * The footer can be base64 encoded. Need to decode it * in order to recognize it. */ { vc->next = set_decode_functions(ff->cte, 0, ff); if (vc->next == NULL) return 1; // fail } } else { if (ff->footer_found) ++ff->lines_after_footer; } } else if (!ff->long_line) { if (decoded_entity == 0 && ff_is_boundary(ff, in, len)) { if (in[ff->boundary_length + 2] == '-') /* * End of outermost entity. Was the last one a footer? */ { if (ff->footer_found && ff->non_empty_lines_before_footer < 2 && ff->lines_after_footer < 10 && ff->long_lines == 0) { /* * Heuristic: * An empty entity just before the one containing * the footer is more likely an erroneous insertion * by the MLM than an original empty attachment. */ if (ff->empty_entity_start) { ff->footer = ff->empty_entity_start; --ff->entity_count; } else ff->footer = ff->entity_start; } ff->entity_end = ff->length; } else // next entity { if (!ff->body_seen && ff->is_text_plain) ff->empty_entity_start = ff->entity_start; else ff->empty_entity_start = 0; ++ff->entity_count; ff->entity_start = ff->length; ff->non_empty_lines_before_footer = 0; ff->found_content_type = fff_ct_not_seen; ff->found_content_transfer_encoding = 0; ff->long_lines = 0; ff->entity_header = 1; ff->is_text_plain = 0; ff->footer_found = 0; ff->cte = 0; ff->body_seen = 0; } } else if (decoded_entity == 0 && ff->entity_header) { ff_check_header(ff, in); } else // entity's body or MIME preamble { ff->body_seen = 1; if (ff->is_text_plain) // candidate footer { ff_check_footer(ff, in, len); if (ff->footer_found == 0) ++ff->non_empty_lines_before_footer; } } } ff->long_line = 0; } else if (ff->long_line == 0) { ff->body_seen = 1; ff->long_line = 1; ++ff->long_lines; } if (decoded_entity == 0) ff->length += len; return 0; } static inline transform_stack * set_decode_functions(cte_transform cte, cte_transform original_cte, found_footer *ff) /* * Prepare a transform stack for decoding. If ff is given, also add * rectify function and find_footers. */ { transform_stack *vc = NULL; if (cte == cte_transform_base64) vc = append_transform_stack(NULL, &decode_base64, NULL); else if (cte == cte_transform_quoted_printable && original_cte != cte_transform_quoted_printable) vc = append_transform_stack(NULL, &decode_quoted_printable, NULL); if (ff) { transform_fn fn = ff->boundary? &find_footers_multipart: &find_footers_single; if (vc) vc = append_transform_stack(vc, rectify_lines, NULL); vc = append_transform_stack(vc, fn, ff); } return vc; } static inline int verify_all_sigs(verify_parms *const vh) /* * verify all sigs if any of the following is true: * - all of them are to be reported on the message * - we use a database and report them there * - we have trusted vouchers to check */ { return vh->parm->z.report_all_sigs || (!vh->parm->z.verify_one_domain && (vh->parm->dwa != NULL || vh->have_trusted_voucher)); } static inline int domain_is_transform_candidate(domain_prescreen const *dps) /* * Try and categorize indirect mail flows. */ { return (dps->u.f.is_from || dps->u.f.is_spf_from || dps->u.f.is_org_domain || dps->u.f.is_super_org_domain || dps->u.f.is_aligned || dps->u.f.is_o_from || dps->u.f.is_o_org_domain || dps->u.f.is_o_super_org_domain || dps->u.f.is_o_aligned) && !(dps->u.f.is_mfrom || dps->u.f.is_helo || dps->u.f.is_sender || dps->u.f.is_not_original || dps->u.f.is_list); } static inline DKIM_SIGINFO * find_sig2(dkimfl_parm *const parm, DKIM *dkim, DKIM_SIGINFO *sig, int hint) { DKIM_SIGINFO *s = dkim_get_samesig(dkim, sig, hint); // only available in 0x020B030A if (s == NULL && parm->z.verbose >= 1) { unsigned int flag = dkim_sig_getflags(sig); if ((flag & DKIM_SIGFLAG_PASSED) != 0) { char *dom = dkim_sig_getdomain(sig), *sel = dkim_sig_getselector(sig); fl_report(LOG_CRIT, "ip=%s: good signature not found in trans, d=%s, s=%s", parm->dyn.info.tcpremoteip, dom? dom: "NULL", sel? sel: "NULL"); } } return s; } static inline dkim_result sigerror2result(DKIM_SIGERROR rc, unsigned int sig_flags) { // idea: if it's wrong in the DNS it is an error, otherwise a failure. switch (rc) { case DKIM_SIGERROR_KEYFAIL: return dkim_temperror; case DKIM_SIGERROR_BADSIG: case DKIM_SIGERROR_BODYHASH: return (sig_flags & DKIM_SIGFLAG_TESTKEY)? dkim_neutral: dkim_fail; case DKIM_SIGERROR_KEYTOOSMALL: return dkim_policy; default: return dkim_permerror; } } static inline dkim_result sig_is_good(DKIM_SIGINFO *const sig) { assert(sig); unsigned int const sig_flags = dkim_sig_getflags(sig); int const bh = dkim_sig_getbh(sig); DKIM_SIGERROR const rc = dkim_sig_geterror(sig); if (sig_flags & DKIM_SIGFLAG_IGNORE) return dkim_policy; if ((sig_flags & DKIM_SIGFLAG_PASSED) != 0 && bh == DKIM_SIGBH_MATCH && rc == DKIM_SIGERROR_OK) return dkim_pass; // we didn't process this sig if ((sig_flags & DKIM_SIGFLAG_PROCESSED) == 0 && rc == DKIM_SIGERROR_UNKNOWN && bh == DKIM_SIGBH_UNTESTED) return dkim_none; return sigerror2result(rc, sig_flags); } static inline dkim_result sig2_is_good(DKIM_SIGINFO *sig2, DKIM_SIGINFO *sig, transform_retry_mode rm) { assert(sig); assert(sig2); if (rm != transform_retry_header) return sig_is_good(sig2); /* * transform_retry_header did not feed the body, presumably because * the body hash was already good in the first attempt. So here we * consider that bh. DKIM_SIGERROR_BODYHASH is set after verifying * that the signature validate, so its presence (due to having passed * an empty body) implies that the header verified. */ unsigned int const sig_flags = dkim_sig_getflags(sig2); int const bh = dkim_sig_getbh(sig); DKIM_SIGERROR rc = dkim_sig_geterror(sig2); if (rc == DKIM_SIGERROR_BODYHASH) dkim_sig_seterror(sig2, rc = DKIM_SIGERROR_OK); if (sig_flags & DKIM_SIGFLAG_IGNORE) return dkim_policy; if ((sig_flags & DKIM_SIGFLAG_PASSED) != 0 && bh == DKIM_SIGBH_MATCH && rc == DKIM_SIGERROR_OK) return dkim_pass; // we didn't process this sig if ((sig_flags & DKIM_SIGFLAG_PROCESSED) == 0 || (rc == DKIM_SIGERROR_UNKNOWN && bh == DKIM_SIGBH_UNTESTED)) return dkim_none; return sigerror2result(rc, sig_flags); } static int post_eoh_retry(verify_parms *vh) /* * Called after dkim_eoh(). */ { assert(vh); assert(vh->step == vh_step_retry); DKIM_SIGINFO **sigs; int nsigs, rtc = -1; DKIM *dkim = vh->dkim, *dkim2 = vh->dt.dkim; if (dkim && dkim2 && dkim_getsiglist(dkim2, &sigs, &nsigs) == DKIM_STAT_OK) { rtc = 0; for (int n = 0; n < nsigs; ++n) { DKIM_SIGINFO *sig2 = sigs[n], *sig = find_sig2(vh->parm, dkim, sig2, n); if (sig) { dkim_result dr = sig_is_good(sig); if (dr == dkim_policy || dr == dkim_pass) dkim_sig_ignore(sig2); else { DKIM_SIGERROR rc = dkim_sig_geterror(sig); if (rc != DKIM_SIGERROR_OK && rc != DKIM_SIGERROR_BADSIG && rc != DKIM_SIGERROR_BODYHASH) dkim_sig_seterror(sig2, rc); } } } } return rtc; } static signature_prescreen *new_dps_sig(char const *selector) { assert(selector); size_t const len = sizeof(signature_prescreen); size_t const len2 = strlen(selector) + 1; signature_prescreen *new_sig = malloc(len + len2); if (new_sig) { memset(new_sig, 0, len); memcpy(&new_sig->name[0], selector, len2); } else fl_report(LOG_ALERT, "MEMORY FAULT"); return new_sig; } static DKIM_STAT dkim_sig_sort(DKIM *dkim, DKIM_SIGINFO** sigs, int nsigs) /* * Callback from dkim_eoh(). */ { context *const ctx = dkim_get_user_context(dkim); verify_parms *const vh = ctx->vh; assert(dkim && sigs && vh); if (vh->step == vh_step_retry) return DKIM_CBSTAT_CONTINUE; if (nsigs > vh->parm->z.max_signatures) { fl_pass_message(vh->parm->fl, "550 Too many DKIM signatures\n"); vh->parm->dyn.rtc = 2; if (vh->parm->z.verbose >= 3) fl_report(LOG_ERR, "ip=%s: %d DKIM signatures, max is %d, message rejected.", vh->parm->dyn.info.tcpremoteip, nsigs, vh->parm->z.max_signatures); return DKIM_CBSTAT_REJECT; } vh->nsigs = nsigs; int rtc = domain_sort(vh, sigs, nsigs); if (rtc < 0) { vh->parm->dyn.rtc = -1; return DKIM_CBSTAT_TRYAGAIN; } return DKIM_CBSTAT_CONTINUE; } static void dkim_key_retrieved(DKIM *dkim, DKIM_STAT status, DKIM_SIGINFO *sig) { context *const ctx = dkim_get_user_context(dkim); assert(dkim && sig && ctx); assert(ctx->vh != NULL || ctx->sh != NULL); int const verbose = ctx->vh? ctx->vh->parm->z.verbose: ctx->sh->parm->z.verbose; if (verbose >= 8) { char prologue[80]; if (ctx->vh) snprintf(prologue, sizeof prologue, "ip=%s", ctx->vh->parm->dyn.info.tcpremoteip); else snprintf(prologue, sizeof prologue, "id=%s", ctx->sh->parm->dyn.info.id); char const *domain = dkim_sig_getdomain(sig); if (domain == NULL) domain = "NULL domain"; char const *selector = dkim_sig_getselector(sig); if (selector == NULL) selector = "NULL selector"; if (status == DKIM_STAT_OK) { char const *algo = dkim_sig_getalgorithm(sig); unsigned int bits = 0; dkim_sig_getkeysize(sig, &bits); // dkim_sig_getkeysize --- Not yet set keybits fl_report(LOG_DEBUG, "%s: retrieved %s->%s: %s, %u bits", prologue, domain, selector, algo? algo: "NULL", bits); } else { DKIM_SIGERROR const rc = dkim_sig_geterror(sig); char const *sigerr = dkim_sig_geterrorstr(rc); char const *err = dkim_geterror(dkim); fl_report(LOG_DEBUG, "%s: %s NOT retrieved %s->%s key: %s", prologue, err? err: "Unknown error", domain, selector, sigerr? sigerr: "NULL"); } } } static int post_eoh_process(verify_parms *const vh) /* * Build structure and start processing signatures. * This was done in dkim_sig_final() until v.2.2. * * Has to be done after dkim_eoh() completion, otherwise dkim_sig_process() * returns INTERNAL_ERROR, with dkim_error = "dkim_canon_getfinal() failed". */ { assert(vh); assert(vh->step == vh_step_verify); int const ndoms = vh->ndoms; domain_prescreen **const domain_ptr = vh->domain_ptr; int did_pass = 0; // signatures valid given the current header int could_pass = 0; // not verifiable, but could try changing header. DKIM_SIGINFO **sigs; int nsigs; if (dkim_getsiglist(vh->dkim, &sigs, &nsigs) != DKIM_STAT_OK) return -1; assert(nsigs == vh->nsigs); for (int c = 0; c < ndoms; ++c) { domain_prescreen *const dps = domain_ptr[c]; int const transform_candidate = domain_is_transform_candidate(dps); int bad_sigs = 0; if (dps->nsigs > 0) { if ((dps->sig = calloc(dps->nsigs, sizeof dps->sig[0])) == NULL) return -1; for (int n = 0; n < dps->nsigs; ++n) { int const ndx = n + dps->start_ndx; assert(ndx >= 0 && ndx < nsigs); DKIM_SIGINFO *const sig = sigs[ndx]; DKIM_SIGERROR rc = dkim_sig_geterror(sig); if (rc > 0) { ++bad_sigs; continue; } if ((dps->sig[n] = new_dps_sig(dkim_sig_getselector(sig))) == NULL) return DKIM_CBSTAT_ERROR; dps->sig[n]->sig = sig; DKIM_STAT status = dkim_sig_process(vh->dkim, sig); if (status != DKIM_STAT_OK) continue; if (transform_candidate) { unsigned int const sig_flags = dkim_sig_getflags(sig); rc = dkim_sig_geterror(sig); if ((sig_flags & DKIM_SIGFLAG_PASSED) != 0) ++did_pass; else if (rc == DKIM_SIGERROR_BADSIG) { /* * See if we have an alternative header field. */ if (is_any_original_signed(vh->dt.oh_base, sig)) ++could_pass; else ++bad_sigs; } else ++bad_sigs; } } } if (transform_candidate && bad_sigs && vh->parm->z.verbose >= 8 && vh->parm->z.disable_experimental == 0) { fl_report(LOG_DEBUG, "ip=%s: %d of %d sig(s) by candidate %s cannot verify", vh->parm->dyn.info.tcpremoteip, bad_sigs, dps->nsigs, dps->name); } } if (could_pass) vh->dt.could_pass = 1; if ((did_pass || could_pass) && vh->dt.seen_list_post == 1 && vh->parm->z.disable_experimental == 0) /* * Enable body parsing, in case the body hash won't match. * (Signature with other kind of errors don't play.) */ { found_footer *ff = malloc(sizeof *ff); if (ff) { memset(ff, 0, sizeof *ff); ff->boundary = vh->dt.boundary; if (ff->boundary) ff->boundary_length = strlen(ff->boundary); ff->dt = &vh->dt; } transform_stack *vc = set_decode_functions(vh->dt.cte, vh->dt.original_cte, ff); if (ff == NULL || (vh->dt.vc = vc) == NULL) { vh->parm->dyn.rtc = -1; return DKIM_CBSTAT_TRYAGAIN; } if (could_pass == 0) // no need to redo the header { FILE* fp = fl_get_file(vh->parm->fl); assert(fp); vh->dt.body_offset = ftell(fp); } if (vh->parm->z.verbose >= 5) { fl_report(LOG_INFO, "ip=%s: enabling body parse (sigs pass: %d, could pass: %d)", vh->parm->dyn.info.tcpremoteip, did_pass, could_pass); } } return 0; } static inline void check_dkim_status_eoh( dkimfl_parm *const parm, DKIM_STAT status, char const *trans) { if (status != DKIM_STAT_OK) { if (parm->z.verbose >= 7 || (parm->z.verbose >= 5 && status != DKIM_STAT_NOSIG)) { char const *err = dkim_getresultstr(status); fl_report(LOG_INFO, "ip=%s: verifying dkim_eoh%s: %s (stat=%d)", parm->dyn.info.tcpremoteip, trans, err? err: "(NULL)", (int)status); } } } static inline int check_dkim_status_sig( dkimfl_parm *const parm, DKIM_STAT status, DKIM_SIGINFO* sig, int prev_rtc, char const *trans) { char *dom = dkim_sig_getdomain(sig), *sel = dkim_sig_getselector(sig); int rtc = -1; if ((status != DKIM_STAT_OK && parm->z.verbose >= 1) || (status == DKIM_STAT_OK && parm->z.verbose >= 7)) { int sigerr = dkim_sig_geterror(sig); char const * err; if (status == DKIM_STAT_OK) err = sigerr? dkim_sig_geterrorstr(sigerr): ""; else err = dkim_getresultstr(status); rtc = status == DKIM_STAT_OK && sigerr == DKIM_SIGERROR_OK? 0: 1; char const *result = rtc == 0? "success": "failure"; if (*trans == 0 || prev_rtc != 0 || rtc != 1 || parm->z.verbose >= 10) /* * Useless to report transformed verification failure * of signatures that succeeded without transformation. */ { char buf[80]; if (err == NULL) { if (status == DKIM_STAT_OK) sprintf(buf, "sigerr=%d", sigerr); else sprintf(buf, "status=%d", (int)status); err = buf; } fl_report(LOG_INFO, "ip=%s: sig %s%s d=%s, s=%s%s%s", parm->dyn.info.tcpremoteip, result, trans, dom? dom: "NULL", sel? sel: "NULL", *err? ": ": "", err); } } return rtc; } static int dkim_sig_wrapup(verify_parms *vh) /* * Called on final eom. Check author domain, whitelisted. */ { int const ndoms = vh->ndoms; domain_prescreen **const domain_ptr = vh->domain_ptr; int const verify_all = verify_all_sigs(vh); DKIM *dkim = vh->dkim, *dkim2 = vh->dt.dkim; int dkim_order = 0; for (int c = 0; c < ndoms; ++c) { domain_prescreen *const dps = domain_ptr[c]; dps->sigval = 0; // reuse for number of verified signatures if (dps->nsigs > 0) { for (int n = 0; n < dps->nsigs; ++n) { signature_prescreen *sp = dps->sig[n]; DKIM_SIGINFO *const sig = sp->sig; DKIM_STAT status = dkim_sig_process(dkim, sig); unsigned int const sig_flags = dkim_sig_getflags(sig); int prev = check_dkim_status_sig(vh->parm, status, sig, -1, ""); if (status != DKIM_STAT_OK) // internal error continue; if ((sig_flags & DKIM_SIGFLAG_IGNORE) != 0) { sp->dk = sp->dk2 = dkim_none; continue; } dkim_result dk = sig_is_good(sig), dk2 = dkim_none; DKIM_SIGINFO *sig2 = NULL; if (dkim2) sig2 = find_sig2(vh->parm, dkim2, sig, 0); if (sig2) { sp->sig2 = sig2; status = dkim_sig_process(dkim2, sig2); dk2 = sig2_is_good(sig2, sig, vh->dt.retry_mode); check_dkim_status_sig(vh->parm, status, sig2, prev, " (trans)"); if (dk2 == dkim_pass && dk > dkim_pass) /* * We need to say: * 1) whether the From: was rewritten and restored, * 2) whether a dkim_result was after undoing some transformation. * For (1), a new entry is needed in IdentifierType. * (Another one would be needed for Sender:) * (2) is done by setting dkim_trans here (human_result * "through MLM transformation") */ { sp->dkim_trans = 1; vh->trans_did_pass = 1; if (vh->parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: signature by %s, s=%s recovered by transformation", vh->parm->dyn.info.tcpremoteip, dps->name, dps->sig[n]->name); } } sp->dk = dk; sp->dk2 = dk2; if (dk == dkim_pass || dk2 == dkim_pass) { sp->dkim_order = ++dkim_order; dps->u.f.sig_is_ok = 1; if (dps->sigval++ == 0) { dps->first_good = n; } if (!verify_all) break; } } if (dps->sigval == 0) /* * We assumed signatures were valid and we erred. This domain is not * DKIM-authenticated, so undo flagging. An alternative approach is to * check whitelisting / trusted VBR after signature validation. We * check in advance, and then attempt signature validation in the * resulting order: Signature validation costs more than local lookup. */ { if (dps->u.f.is_dnswl == 0 && dps->u.f.spf_pass == 0) { dps->whitelisted = 0; dps->u.f.is_trusted = 0; dps->u.f.is_whitelisted = 0; dps->u.f.is_known = 0; } } } } /* * Repeat to assign dkim_order to failed signatures */ for (int c = 0; c < ndoms; ++c) { domain_prescreen *const dps = domain_ptr[c]; for (int n = 0; n < dps->nsigs; ++n) if (dps->sig[n] && dps->sig[n]->dkim_order == 0) dps->sig[n]->dkim_order = ++dkim_order; } return DKIM_CBSTAT_CONTINUE; } static DKIM_STAT dkim_sig_final(DKIM *dkim, DKIM_SIGINFO** sigs, int nsigs) /* * Called by dkim_eom_verify(). */ { context *const ctx = dkim_get_user_context(dkim); verify_parms *const vh = ctx->vh; assert(dkim && sigs && vh); if (vh->step == vh_step_retry) return dkim_sig_wrapup(vh); assert(vh->step == vh_step_verify); assert(vh->dt.retry_mode == transform_retry_none); if (vh->parm->z.disable_experimental) return dkim_sig_wrapup(vh); /* * Decide whether there is a possible MLM transformation to undo. */ int can_redo_body = 0; int can_redo_header = 0; if (vh->dt.could_pass) can_redo_header = 1; if (vh->dt.vc) // get result of find_footers() { clear_transform_stack(vh->dt.vc); vh->dt.vc = NULL; if (vh->dt.footer_found) can_redo_body = 1; } if (vh->parm->z.verbose >= 8) fl_report(LOG_DEBUG, "ip=%s: retry check: " "can%s redo header, can%s redo body", vh->parm->dyn.info.tcpremoteip, can_redo_header? "": "not", can_redo_body? "": "not"); if (can_redo_body == 0 && can_redo_header == 0) return dkim_sig_wrapup(vh); int const ndoms = vh->ndoms; domain_prescreen **const domain_ptr = vh->domain_ptr; int const verify_all = verify_all_sigs(vh); int retry_header = 0; // badsig, bh ok int retry_body = 0; // passed, bh bad int retry_both = 0; // badsig, bh bad for (int c = 0; c < ndoms; ++c) { domain_prescreen *const dps = domain_ptr[c]; if (domain_is_transform_candidate(dps)) { for (int n = 0; n < dps->nsigs; ++n) { signature_prescreen *sp = dps->sig[n]; DKIM_SIGINFO *const sig = sp->sig; DKIM_SIGERROR rc = dkim_sig_geterror(sig); /* * dkim_sig_process() won't do the body hash if the * signature failed. Let's have the body hash anyway. */ if (rc == DKIM_SIGERROR_BADSIG) dkim_sig_seterror(sig, DKIM_SIGERROR_UNKNOWN); else if (rc > 0) continue; // this signature cannot be verified anyway. DKIM_STAT status = dkim_sig_process(dkim, sig); if (rc == DKIM_SIGERROR_BADSIG) dkim_sig_seterror(sig, DKIM_SIGERROR_BADSIG); if (status == DKIM_STAT_OK) { unsigned int const sig_flags = dkim_sig_getflags(sig); int bh = dkim_sig_getbh(sig); rc = dkim_sig_geterror(sig); if (bh == DKIM_SIGBH_UNTESTED) { if (vh->parm->z.verbose >= 8) { char const *err =dkim_sig_geterrorstr(rc); char *dom = dkim_sig_getdomain(sig), *sel = dkim_sig_getselector(sig); fl_report(LOG_DEBUG, "ip=%s: sig d=%s, s=%s already failed: %s", vh->parm->dyn.info.tcpremoteip, dom? dom: "NULL", sel? sel: "NULL", err? err: "NULL"); } continue; } // this relies on changed bh check in dkim_sig_process() if ((sig_flags & DKIM_SIGFLAG_PASSED) != 0) { if (bh == DKIM_SIGBH_MISMATCH && can_redo_body) ++retry_body; } else if (rc == DKIM_SIGERROR_BADSIG || rc == DKIM_SIGERROR_BODYHASH) { if (bh == DKIM_SIGBH_MISMATCH) { if (can_redo_header && can_redo_body) ++retry_both; } else if (can_redo_header) ++retry_header; } } } if (!verify_all) break; } } if (retry_header == 0 && retry_body == 0 && retry_both == 0) { if (vh->parm->z.verbose >= 8) fl_report(LOG_DEBUG, "ip=%s: no retry", vh->parm->dyn.info.tcpremoteip); return dkim_sig_wrapup(vh); } if (retry_both || (retry_header && retry_body)) vh->dt.retry_mode = transform_retry_all; else if (retry_header) vh->dt.retry_mode = transform_retry_header; else vh->dt.retry_mode = transform_retry_body; assert(vh->dt.retry_mode != transform_retry_none); if (vh->parm->z.verbose >= 5) fl_report(LOG_INFO, "ip=%s: enabling retry " "(badsig, bh ok: %d; badsig, bh bad: %d; pass, bh bad: %d)", vh->parm->dyn.info.tcpremoteip, retry_header, retry_both, retry_body); return DKIM_CBSTAT_CONTINUE; (void)nsigs; // unused (void)sigs; // unused } typedef struct a_r_verify_parm { char const *my_id; // foreign A-R must not be similar to this char const *info_ip; char const *authserv_id; // only valid during the call int zrename, maybe_attack, log, verbose; } a_r_verify_parm; static int a_r_verify(void *v, int step, name_val* nv, size_t nv_count) { a_r_verify_parm *arp = v; assert(arp); int rtc = 0; if (nv == NULL) // last call { rtc = step; if (arp->authserv_id == NULL) { arp->zrename = 1; if (arp->log) fl_report(LOG_NOTICE, "ip=%s: renaming invalid Authentication-Results", arp->info_ip); } else { if (arp->my_id && stricmp(arp->my_id, arp->authserv_id) == 0) arp->maybe_attack = arp->zrename = 1; if (arp->log) { if (arp->maybe_attack) fl_report(LOG_NOTICE, "ip=%s: renaming Authentication-Results from %s", arp->info_ip, arp->authserv_id); else if (arp->verbose >= 6) fl_report(LOG_INFO, "ip=%s: found Authentication-Results by %s", arp->info_ip, arp->authserv_id); } // TODO: check a list of trusted/untrusted id's } } else if (step < 0) { arp->authserv_id = nv[0].name; } return rtc; (void)nv_count; } typedef struct a_r_reader_parm { char const *authserv_id, *discarded; // only valid during the call domain_prescreen *dnswl_dps; // output (was dnswl_domain) dkimfl_parm *parm; // input param domain_prescreen **domain_head; // input for get_prescreen int resinfo_count, dnswl_count; union ip_number { int32_t ip; uint8_t ip_c[4]; } u; } a_r_reader_parm; static int a_r_reader(void *v, int step, name_val* nv, size_t nv_count) { a_r_reader_parm *arp = v; assert(arp); assert(arp->parm); assert(arp->domain_head); int rtc = 0; if (nv == NULL) // last call { rtc = step; if (arp->dnswl_dps || arp->u.ip) // found at least one { if (arp->discarded && arp->parm->z.verbose >= 6) fl_report(LOG_INFO, "ip=%s: discarded %s and other %d trusted zone(s) in " "Authentication-Results by %s", arp->parm->dyn.info.tcpremoteip, arp->discarded, arp->dnswl_count - 2, arp->authserv_id? arp->authserv_id: "(null authserv-id)"); } else if (arp->parm->z.verbose >= 2 && arp->parm->dyn.no_write == 0) /* * ALLOW_EXCLUSIVE and trust_a_r should mirror each other. * ALLOW_EXCLUSIVE is configured in Courier's sysconfdir/esmtpd. * (dyn.no_write: skip if called by zdkimverify) */ fl_report(rtc? LOG_ERR: LOG_NOTICE, "ip=%s: Authentication-Results by %s: %s", arp->parm->dyn.info.tcpremoteip, arp->authserv_id? arp->authserv_id: "(null authserv-id)", rtc != 0 ? "unparseable data": arp->resinfo_count <= 0? "empty": arp->dnswl_count > 0? "no dnswl domain found": "please check ALLOW_EXCLUSIVE is set"); } else if (step < 0) { arp->authserv_id = nv[0].name; } else { arp->resinfo_count += 1; if (nv_count > 0 && stricmp(nv[0].name, "dnswl") == 0 && stricmp(nv[0].value, "pass") == 0) { char const *dns_zone = NULL, *policy_txt = NULL, *policy_ip = NULL; for (size_t i = 1; i < nv_count; ++i) { if (stricmp(nv[i].name, "dns.zone") == 0) dns_zone = nv[i].value; else if (stricmp(nv[i].name, "policy.txt") == 0) policy_txt = nv[i].value; else if (stricmp(nv[i].name, "policy.ip") == 0) policy_ip = nv[i].value; } char const **const zone = arp->parm->z.trusted_dnswl; if (zone && dns_zone) { int trusted = 0; for (size_t i = 0; zone[i] != NULL; ++i) if (stricmp(zone[i], dns_zone) == 0) { trusted = 1; break; } if (trusted) { arp->dnswl_count += 1; if (policy_txt && arp->dnswl_dps == NULL) { char cp[64]; // domain length /* * extract a domain name from policy_txt */ size_t o = 0; for (size_t i = 0; i < sizeof cp; ++i) { int const ch = *(unsigned char*)&policy_txt[i]; if (ch == 0 || isspace(ch)) { if (i > 0) { cp[o] = 0; if ((arp->dnswl_dps = get_prescreen(arp->domain_head, cp)) != NULL) arp->dnswl_dps->u.f.is_dnswl = 1; else arp->parm->dyn.rtc = -1; if (arp->parm->z.verbose >= 6) fl_report(LOG_INFO, "ip=%s: domain %s whitelisted by %s", arp->parm->dyn.info.tcpremoteip, cp, dns_zone); } break; } if (isalnum(ch) || strchr(".-_", ch) != NULL) cp[o++] = ch; else if (ch != '"') break; } } else if (arp->dnswl_dps && arp->discarded == NULL) arp->discarded = dns_zone; if (policy_ip && arp->u.ip == 0) { size_t n = 0; char const *p = policy_ip; unsigned bad = 0, u = 0; for (;;) { unsigned const ch = *(unsigned char *)p++; if (ch == '.' || ch == 0) { if (u < 256 && n < 4) arp->u.ip_c[n++] = u; else bad = 1; u = 0; if (ch == 0) break; } else if (isalnum(ch)) u = 10 * u + ch - '0'; } if (bad || arp->u.ip == arp->parm->z.dnswl_invalid_ip) { if (arp->parm->z.verbose >= 1) fl_report(LOG_CRIT, "Zone %s lookup has invalid IP %s", dns_zone, policy_ip); arp->u.ip = 0; } } else if (arp->dnswl_dps && arp->discarded == NULL) arp->discarded = dns_zone; } } } } return rtc; } static domain_prescreen * get_sender_prescreen2(domain_prescreen** dps_head, char *sender) { /* * If it contains spaces, most likely it's an error message */ if (strchr(sender, ' ') != NULL) return NULL; return get_prescreen(dps_head, sender); } static domain_prescreen * get_sender_prescreen(domain_prescreen** dps_head, char *sender) /* * Parse sender properly and get a prescreen for the domain. */ { assert(sender); char *eol = strchr(sender, '\n'); if (eol == NULL) eol = sender + strlen(sender) - 1; while (eol > sender && isspace(*(unsigned char*)eol)) --eol; if (*eol != ';') return NULL; *eol = 0; domain_prescreen *dps; if (strchr(sender, '@') == NULL) dps = get_sender_prescreen2(dps_head, sender); else { char *user, *domain, *cont; char *str = strdup(sender); if (str == NULL || my_mail_parse_c(str, &user, &domain, &cont) != 0 || domain == NULL) dps = NULL; else dps = get_sender_prescreen2(dps_head, domain); free(str); } *eol = ';'; return dps; } static void report_dkim_header_syntax(dkimfl_parm *parm, DKIM* dkim, char *start) { if (parm->z.verbose >= 5) { char *bad_eol = strchr(start, '\r'); if (bad_eol) *bad_eol = 0; char const *err = dkim_geterror(dkim); fl_report(LOG_ERR, "ip=%s: bad header field \"%s%s\": %s", parm->dyn.info.tcpremoteip, start, bad_eol? "...": "", err? err: "Syntax error (5)"); if (bad_eol) *bad_eol = '\r'; if (err) dkim_clearerror(dkim); } } static int fgetc_skip_cr(FILE *fp) { int ch = fgetc(fp); if (ch == '\r') ch = fgetc(fp); return ch; } static int verify_headers(verify_parms *vh) // return parm->dyn.rtc = -1 for unrecoverable error, // parm->dyn.rtc (0) otherwise { assert(vh && vh->parm); dkimfl_parm *const parm = vh->parm; size_t keep = 0; var_buf *vb = &parm->dyn.vb; FILE* fp = fl_get_file(parm->fl); assert(fp); enum vh_step const step = vh->step; DKIM *const dkim = step == vh_step_verify? vh->dkim: step == vh_step_retry? vh->dt.dkim: NULL; FILE *const out = step == vh_step_copy? vh->outfile: NULL; FILE *const save_fp = vh->dt.save_fp; // for zdkimverify discard CRs int (*const my_fgetc)(FILE*) = parm->dyn.no_write? &fgetc_skip_cr: &fgetc; char* (*const my_strchr)(char const*, int) = parm->dyn.no_write? &strchr_del_cr: &strchr; int seen_received = 0, seen_anything = 0; int from_header_field = 0; for (;;) { char *p = vb_fgets(vb, keep, fp); char *eol = p? (*my_strchr)(p, '\n'): NULL; if (eol == NULL) { if (vb->buf == NULL) parm->dyn.rtc = -1; else if (seen_anything == 0 || p) /* * A message with no body can be valid. If no field is found, * the message is not valid. If a non-terminated field is * found, the message is not valid. */ parm->dyn.rtc = 3; if (parm->dyn.rtc != 0) { if (parm->z.verbose) fl_report(parm->dyn.rtc < 0? LOG_ALERT: LOG_ERR, "ip=%s: bad header field (%.20s...)", parm->dyn.info.tcpremoteip, vb_what(vb, fp)); clean_stats(parm); } return parm->dyn.rtc; } seen_anything += 1; /* * Reading algorithm: the next char is read ahead, * thus the value to keep in the buffer is positive * except after reading the very first line. * * Assuming lines arrive without \r, for dkim, insert \r\n in * the middle of the line and pass header field without * end-of-line terminator; for fp, keep \n and write it to file. */ int const next = eol >= p? (*my_fgetc)(fp): '\n'; int const cont = next != EOF && next != '\n'; char *const start = vb->buf; if (cont && isspace(next)) // wrapped { if (dkim) { *eol++ = '\r'; *eol = '\n'; } *++eol = next; keep = eol + 1 - start; continue; } /* * full header, including trailing \n, is in buffer * process it. */ if (dkim) *eol = 0; int i, zap = 0, zrename = 0; size_t dkim_unrename = 0; char *start2 = start; // Transformed header char *s; // malformed headers can go away... if (!isalpha(*(unsigned char*)start)) zap = 1; // A-R fields else if ((s = hdrval(start, "Authentication-Results")) != NULL) { if (!parm->z.trust_a_r) { a_r_verify_parm arp; memset(&arp, 0, sizeof arp); arp.my_id = parm->dyn.authserv_id; arp.info_ip = parm->dyn.info.tcpremoteip; arp.verbose = parm->z.verbose; arp.log = dkim == NULL && parm->z.verbose >= 2; if (a_r_parse(s, &a_r_verify, &arp) < 0) { zrename = 1; if (arp.log) fl_report(LOG_NOTICE, "ip=%s: renaming unparseable Authentication-Results", parm->dyn.info.tcpremoteip); } else /* * Courier puts A-R after "Received" (but before Received-SPF). * After "Received", a matching authserv_id might be malicious. * For the time being, we discard it. * * A further possibility is to check the Received-SPF, assuming * that it is configured and thus always present: If Courier's * A-R is before that, then it is authentic. The advantage to * do so would be to keep trusted A-R fields. */ zrename = arp.zrename; } // acquire trusted results on 1st pass else if (step == vh_step_verify) { a_r_reader_parm arp; memset(&arp, 0, sizeof arp); arp.parm = parm; arp.domain_head = &vh->domain_head; if (a_r_parse(s, &a_r_reader, &arp) == 0) { if (arp.dnswl_dps && arp.u.ip) arp.dnswl_dps->dnswl_value = arp.u.ip_c[parm->z.dnswl_octet_index]; vh->dnswl_count += arp.dnswl_count; } } } /* * unrename Authentication-Results: There are different * opinions on signing such fields, or the "X-Original-" * variant thereof. To the opposite, the "Old-" variant * seems to be specific of Courier or servers acting in * a similar fashion. Thus, the probability of breaking * a signature by unrenaming seems to be lower than that * of breaking it by not doing so. */ else if ((s = hdrval(start, "Old-Authentication-Results")) != NULL) { if (dkim) dkim_unrename = 4; } /* * Masked out by MLM */ else if ((s = hdrval(start, "X-Mailman-Original-DKIM-Signature")) != NULL) { if (dkim) dkim_unrename = 19; } // Only on first step, acquire relevant header info else if (step == vh_step_verify) { /* ** cache courier's SPF results, get authserv_id, count Received ** ** Note: relevant remote host data (IP, name) is retrieved ** from ctlfile, see check_db_connected(). */ if (strincmp(start, "Received", 8) == 0) { if ((s = hdrval(start, "Received")) != NULL) { if (parm->dyn.stats) parm->dyn.stats->received_count += 1; if (parm->dyn.authserv_id == NULL && seen_received == 0) { seen_received = 1; while (s && parm->dyn.authserv_id == NULL) { s = strstr(s, " by "); if (s) { s += 4; while (isspace(*(unsigned char*)s)) ++s; char *const authserv_id = s; int ch; while (isalnum(ch = *(unsigned char*)s) || strchr(".-_", ch) != NULL) ++s; char *ea = s; while (isspace(*(unsigned char*)s)) ++s; *ea = 0; if (strincmp(s, "with ", 5) == 0 && s > ea && (parm->dyn.authserv_id = strdup(authserv_id)) == NULL) { clean_stats(parm); return parm->dyn.rtc = -1; } *ea = ch; } } } } else if (!parm->z.no_spf && vh->received_spf < 3 && (s = hdrval(start, "Received-SPF")) != NULL) { ++vh->received_spf; while (isspace(*(unsigned char*)s)) ++s; spf_result spf = spf_result_string(s); s = strstr(s, "SPF="); if (s) { s += 4; // 1234567 char *sender = strstr(s, "sender="); if (sender) { sender += 7; domain_prescreen *dps = get_sender_prescreen(&vh->domain_head, sender); if (dps) { // multiple spf result can race, choose the higher if (spf > dps->spf) dps->spf = spf; if (spf == spf_pass) dps->u.f.spf_pass = 1; if (strincmp(s, "HELO", 4) == 0) dps->u.f.is_helo = 1; else if (strincmp(s, "MAILFROM", 8) == 0) dps->u.f.is_mfrom = 1; else if (strincmp(s, "FROM", 4) == 0) dps->u.f.is_spf_from = 1; } } } } } // DKIM-Transform // definition of candidate from indexes: else if ((i = candidate_is_from, s = hdrval(start, "From")) != NULL || (i = candidate_is_author, s = hdrval(start, "Author")) != NULL || (i = candidate_is_original_from, s = hdrval(start, "Original-From")) != NULL || (i = candidate_is_x_original_from, s = hdrval(start, "X-Original-From")) != NULL || (i = candidate_is_reply_to, s = hdrval(start, "Reply-To")) != NULL || (i = candidate_is_cc, s = hdrval(start, "Cc")) != NULL) /* * Author: should be used according to RFC 9057. * * Original-From: is used on the local server to indicate * the replacement value if anyone was verified. * * Reply-To: or Cc: set by Mailman: * https://wiki.list.org/DEV/DMARC * X-Original-From: by IETF: * https://trac.ietf.org/trac/dmarc/wiki/XOriginalFrom */ { if (i == candidate_is_from) ++from_header_field; else if (i == candidate_is_author) vh->dt.author_in_header = 1; else if (i == candidate_is_original_from) vh->dt.original_from_in_header = 1; if (vh->dt.cf[i] == NULL && add_candidate_from(&vh->dt.cf[i], s, i) < 0) return parm->dyn.rtc = -1; } else if (strincmp(start, "Original-", 9) == 0) // 0123456789 { if (add_to_original(&vh->dt, start + 9) < 0) return parm->dyn.rtc = -1; } else if ((s = hdrval(start, content_transfer_encoding)) != NULL) { parse_content_transfer_encoding(&vh->dt, s, 0); } else if ((s = hdrval(start, content_type)) != NULL) { parse_content_type(&vh->dt, s, 0); } else if ((s = hdrval(start, "Sender")) != NULL) { if (vh->dt.sender_domain == NULL) { char *user, *domain, *scrap = strdup(s); if (scrap != NULL && my_mail_parse(scrap, &user, &domain) == 0 && domain != NULL) vh->dt.sender_domain = strdup(domain); free(scrap); } } else if (i = 0, strincmp(start, "List-", 5) == 0 && ((s = hdrval(start, "List-Subscribe")) != NULL || (s = hdrval(start, "List-Unsubscribe")) != NULL || (i = 1, s = hdrval(start, "List-Post")) != NULL)) { if (vh->dt.list_domain == NULL) { int rtc = mailto_domain(s, &vh->dt.list_domain); if (rtc == -1) { fl_report(LOG_ALERT, "MEMORY FAULT"); return parm->dyn.rtc = -1; } } if (i == 1) vh->dt.seen_list_post = 1; } else if ((i = 0, s = hdrval(start, "Subject")) != NULL || (i = 1, s = hdrval(start, "Thread-Topic")) != NULL || (i = 2, s = hdrval(start, "X-ASG-Orig-Subj")) != NULL) { if (vh->dt.save_subject[i] == NULL) vh->dt.save_subject[i] = strdup(s); } else if ((s = hdrval(start, "References")) != NULL || (s = hdrval(start, "In-Reply-To")) != NULL) { vh->dt.is_reply = 1; } // (only if stats enabled) // save stats' content_type and content_encoding, check mailing_list if (parm->dyn.stats) collect_stats(parm, start); } else if (vh->step == vh_step_copy && vh->dt.original_from_remove && hdrval(start, "Original-From") != NULL) { zap = 1; if (parm->z.verbose >= 8) { char *nl = strchr(start, '\n'); unsigned len = nl? (unsigned)(nl - start): strlen(start); fl_report(LOG_DEBUG, "ip=%s: remove header %.*s", parm->dyn.info.tcpremoteip, len, start); } } // action header if (parm->z.action_header && parm->dyn.action_header == NULL && (s = hdrval(start, parm->z.action_header)) != NULL && (parm->dyn.action_header = strdup_normalize(s, 0)) == NULL) { fl_report(LOG_ALERT, "MEMORY FAULT"); return parm->dyn.rtc = -1; } if (!zap) { int err = 0; DKIM_STAT status; size_t len = eol - start - dkim_unrename; if (dkim) { // DKIM-Transform if (step == vh_step_retry) { start2 = replace_original_header(&vh->dt, start); if (start2) { if (*start2) { len = strlen(start2); dkim_unrename = 0; } else // empty field interpreted as not present start2 = NULL; } else { start2 = start; } if (start2 && save_fp) { fwrite(start2 + dkim_unrename, len, 1, save_fp); fwrite("\r\n", 2, 1, save_fp); } } if (start2) { status = dkim_header(dkim, start2 + dkim_unrename, len); err = status != DKIM_STAT_OK; if (err && status == DKIM_STAT_SYNTAX) { report_dkim_header_syntax(parm, dkim, start2); err = 0; } } } else { if (zrename) { static const char zrenamed[] = "Z-Renamed-"; err = fwrite(zrenamed, sizeof zrenamed - 1, 1, out) != 1; } err |= fwrite(start, len + 1, 1, out) != 1; status = DKIM_STAT_OK; // happy compiler } if (err) { if (parm->z.verbose) { char const *errs, *what, *when = ""; if (dkim) { what = "dkim_header"; if (step == vh_step_retry) when = " trans"; errs = dkim_getresultstr(status); err = (int)status; } else { what = "fwrite"; errs = strerror(errno); err = errno; } fl_report(LOG_ALERT, "ip=%s: %s%s failed: %s (%d)", parm->dyn.info.tcpremoteip, what, when, errs? errs: "unknown", err); } return parm->dyn.rtc = -1; } } if (!cont) break; start[0] = next; keep = 1; } /* * All headers processed. Header ending newline ('\n') read. * Check results thus far. */ if (dkim) { if (step == vh_step_verify) { char const *ct = peek_original_header(&vh->dt, content_type); if (ct) parse_content_type(&vh->dt, ct, 1); char const *cte = peek_original_header(&vh->dt, content_transfer_encoding); if (cte) parse_content_transfer_encoding(&vh->dt, cte, 1); /* * Except for base64, assume the original Content-Type was * the same as the current one. In practice, this avoids * spurious quoted-printable re-coding. */ if (ct == NULL && cte == NULL && vh->dt.cte != cte_transform_base64) vh->dt.original_cte = vh->dt.cte; } else // vh_step_retry { for (original_header*oh = vh->dt.oh_base; oh != NULL; oh = oh->next) if (!oh->is_empty && !oh->is_replaced) { size_t len = strlen(oh->field); DKIM_STAT status = dkim_header(dkim, oh->field, len); if (status == DKIM_STAT_SYNTAX) report_dkim_header_syntax(parm, dkim, oh->field); else if (status != DKIM_STAT_OK) { char const *errs = dkim_getresultstr(status); fl_report(LOG_ALERT, "ip=%s: trans dkim_header() failed: %s (%d)", parm->dyn.info.tcpremoteip, errs? errs: "unknown", status); } } } // dkim_sig_sort may return TRYAGAIN, and parm->dyn.rtc = -1; DKIM_STAT status = dkim_eoh(dkim); check_dkim_status_eoh(parm, status, step == vh_step_retry? "trans": ""); if (vh->step == vh_step_verify) { if (parm->z.still_allow_no_from == 0 && from_header_field != 1) /* Reject messages with missing or multiple From: lines. Set on 19 Apr 2024 for v.3.18, zdkimfilter behaved like so: Mar 14 18:50:15 22 north courierfilter: zdkimfilter[8078]:ip=40.107.244.119: verifying dkim_eoh: Syntax error (stat=5) Mar 14 18:50:15 18 north courierfilter: zdkimfilter[8078]:ip=40.107.244.119: dkim_body failed on 8 bytes: can't determine sender address (9) Mar 14 18:50:15 19 north courierfilter: zdkimfilter[8078]:ip=40.107.244.119: permanent verification failure: can't determine sender address Mar 14 18:50:15 22 north courierfilter: zdkimfilter[8078]:ip=40.107.244.119: different From: is it NULL (OpenDKIM) or "" (Courier SPF)? Mar 14 18:50:15 22 north courierfilter: zdkimfilter[8078]:ip=40.107.244.119: can't determine sender address Mar 14 18:50:15 22 north courierfilter: zdkimfilter[8078]:ip=40.107.244.119: arc=? (id=microsoft.com, No error) Mar 14 18:50:15 22 north courierfilter: zdkimfilter[8078]:ip=40.107.244.119: response: 432 Mail filter momentarily unavailable. Should check vh->dd.domain == NULL || *vh->dd.domain == 0 ? */ { if (parm->z.verbose >= 3) fl_report(LOG_ERR, "ip=%s: %d From: header fields. Reject message.", parm->dyn.info.tcpremoteip, from_header_field); static const char reject_bad_from[] = "550 Messages must have one From: header field.\n"; fl_pass_message(parm->fl, reject_bad_from); return parm->dyn.rtc = 2; } if (status == DKIM_STAT_OK && post_eoh_process(vh) < 0) { fl_report(LOG_CRIT, "ip=%s: post_eoh() failed", parm->dyn.info.tcpremoteip); return parm->dyn.rtc = -1; } } } else if (ferror(out)) { if (parm->z.verbose) fl_report(LOG_ALERT, "ip=%s: frwite failed with %s", parm->dyn.info.tcpremoteip, strerror(errno)); return parm->dyn.rtc = -1; } return parm->dyn.rtc; } typedef struct dkim_result_summary { char *id; //malloc'd char const *result, *err; domain_prescreen *original_from_dps; char only_want_success; } dkim_result_summary; /* * u.f.is_o_from is set right after reading the header for the first time. * The function call order is: * dkim_eoh() -> * dkim_sig_sort() -> * domain_sort() -> * domain_flags() -- asserts u.f.is_o_from after calling -> * domain_flag_from() * * At that point, vh->dt.cf[1]->domain is the candidate original From:. * * After retry, writing the A-R header field in write_file(), if the * ORIGINAL_FROM_CONDITION is satisfied, the special reason= tag is * set, and the subsequent code makes sure that a suitable Original-From: * is present in the rewritten header. Up to vh->dt.cleared == 1. */ #define ORIGINAL_FROM_CONDITION(dps) \ (dps->u.f.is_from == 0 && dps->u.f.is_o_from != 0) static void assert_is_unique(verify_parms *vh, domain_prescreen *original_from_dps) { assert(vh); assert(original_from_dps); assert((vh->dt.cf[1] && vh->dt.cf[1]->domain) || vh->dt.cleared); #if !defined NDEBUG if (vh->dt.cf[1]) { domain_prescreen *same_dps = find_prescreen(vh->domain_head, vh->dt.cf[1]->domain); assert(original_from_dps == same_dps); for (domain_prescreen *dps = vh->domain_head; dps; dps = dps->next) assert(!ORIGINAL_FROM_CONDITION(dps) || dps == original_from_dps); } #else (void)original_from_dps; (void)vh; #endif } static int print_signature_resinfo(FILE *fp, domain_prescreen *dps, int nsig, dkim_result_summary *drs, DKIM *dkim) // Last argument, dkim, to get sig in order to use header.b, if dps->nsigs > 1. // Start printing the semicolon+newline that terminate either the previous // resinfo or the authserv-id, then print the signature details. // No print for ignored signatures. // Return 1 or 0, the number of resinfo's written. { assert(dps); assert(drs); if (dps->sig == NULL) return 0; DKIM_SIGINFO *sig = dps->sig[nsig]->sig, *sig2 = dps->sig[nsig]->sig2; unsigned int sig_flags; if (sig == NULL || ((sig_flags = dkim_sig_getflags(sig)) & DKIM_SIGFLAG_IGNORE) != 0) return 0; char buf[80], *id = NULL, htype = 0; memset(buf, 0, sizeof buf); // changed in 2.3: if it's only domain, report header.d if (dkim_sig_getidentity(NULL, sig, buf, sizeof buf) == DKIM_STAT_OK && buf[0] != '@') { id = buf; htype = 'i'; } else if ((id = dkim_sig_getdomain(sig)) != NULL) htype = 'd'; char buf2[80], *id2 = NULL; memset(buf2, 0, sizeof buf2); size_t sz2 = sizeof buf2; if (dkim && dps && (dps->nsigs > 1 || id == NULL)) { if (dkim_get_sigsubstring(dkim, sig, buf2, &sz2) == DKIM_STAT_OK && sz2 < sizeof buf2) id2 = &buf2[0]; } if (id == NULL) // if no domain, output header.b only { id = id2; htype = 'b'; id2 = NULL; } if (id == NULL || htype == 0) //useless to report an unidentifiable signature return 0; int const is_test = (sig_flags & DKIM_SIGFLAG_TESTKEY) != 0; dkim_result dk = dps->sig[nsig]->dk; dkim_result dk2 = dps->sig[nsig]->dk2; dkim_result dr; DKIM_SIGINFO *sigr; char const *reason = NULL; int is_sig2, is_success = dk == dkim_pass; if (sig2 && dk2 > dkim_none && (dk2 < dk || dk2 == dkim_pass)) // if both passed print "transformed" too { dr = dk2; is_sig2 = 1; sigr = sig2; is_success = 1; if (ORIGINAL_FROM_CONDITION(dps)) { reason = "Original-From: transformed"; drs->original_from_dps = dps; } else reason = "transformed"; } else { dr = dk; is_sig2 = 0; sigr = sig; } char const *result = get_dkim_result(dr), *err = NULL; if (dr == dkim_neutral || dr == dkim_fail) { DKIM_SIGERROR const rc = dkim_sig_geterror(sigr); err = dkim_sig_geterrorstr(rc); } fprintf(fp, ";\n dkim=%s ", result); if (reason) fprintf(fp, "reason=\"%s\" ", reason); union flags_as_an_int_or_bitfields u; u.all = 0; u.f.is_whitelisted = dps->u.f.is_whitelisted; if (err || is_test || u.all) { int cont = 0; fputc('(', fp); if (is_test) { fputs("test key", fp); cont = 1; } if (err) { if (cont) fputs(", ", fp); fputs(err, fp); if (is_sig2) fputs(" (trans)", fp); cont = 1; } if (u.f.is_whitelisted) { if (cont) fputs(", ", fp); fputs("whitelisted", fp); cont = 1; } fputs(") ", fp); } if (must_be_quoted(id)) fprintf(fp, "header.%c=\"%s\"", htype, id); else fprintf(fp, "header.%c=%s", htype, id); if (id2) { char const * const sel = dkim_sig_getselector(sig); if (must_be_quoted(id2)) fprintf(fp, "\n header.b=\"%s\" (%s)", id2, sel); else fprintf(fp, "\n header.b=%s (%s)", id2, sel); } if (drs->id == NULL) { drs->id = strdup(id); drs->result = result; if (drs->only_want_success == 0 || is_success) drs->err = err; } return 1; } static char *log_arc_info(struct arc_info *arc_info, DKIM *dkim) { char const *arc_error = "", *arc_comma = ""; char arc_buf[160]; if (arc_info->arc_state != ARC_CHAIN_PASS) { arc_error = dkim_getsealerror(dkim); arc_comma = ", "; } if (arc_info->arc_data_available) snprintf(arc_buf, sizeof arc_buf, ", arc=%s (id=%s%s%s)", arc_info->arc_state_desc, dkim_getsealdomain(dkim, arc_info->dkim_arccount - 1), arc_comma, arc_error); else snprintf(arc_buf, sizeof arc_buf, ", arc=%s (%d set(s)%s%s)", arc_info->arc_state_desc, arc_info->dkim_arccount, arc_comma, arc_error); return strdup(arc_buf); } static int write_file(verify_parms *vh, FILE *fp, DKIM_STAT status) /* * Used to rewrite mail file as well as to save dropped file. * fp is open for writing */ { assert(vh); assert(vh->step < vh_step_copy); assert(fp); assert(vh->policy_type); assert(vh->policy_result); assert(vh->policy_comment); dkimfl_parm *const parm = vh->parm; DKIM *dkim = vh->dkim; if (parm->dyn.no_write && isatty(1)) /* * zdkimverify reading stderr, fp == stdout. * To avoid mixing log lines on stderr, sleep while all * log lines are printed out, and expect SIGUSR1 at that time. * SIGUSRs are blocked in fl_runchild(). */ { sigset_t usr; sigemptyset(&usr); sigaddset(&usr, SIGUSR1); fputs("FILTER-RESPONSE:\n", stderr); fflush(stderr); struct timespec ts = {0, 250000000}; // 1/4sec sigtimedwait(&usr, NULL, &ts); } /* * according to RFC 5451, Section 7.1, point 5, the A-R field * should always appear above the corresponding Received field. */ fprintf(fp, "Authentication-Results: %s", parm->dyn.authserv_id); int auth_given = 0; int log_written = 0; char *spf_domain[2] = {NULL, NULL}; if (vh->have_spf_pass) for (domain_prescreen *dps = vh->domain_head; dps; dps = dps->next) if (dps->spf == spf_pass) { if (dps->u.f.is_helo) spf_domain[0] = dps->name; if (dps->u.f.is_mfrom) spf_domain[1] = dps->name; } /* * The only authentication may happen to be BOFHSPFFROM, not written: * will get "Authentication-Results: authserv.id; none" in that case. */ if (spf_domain[0] || spf_domain[1]) { fprintf(fp, ";\n spf=pass smtp.%s=%s", spf_domain[1]? "mailfrom": "helo", spf_domain[1]? spf_domain[1]: spf_domain[0]); ++auth_given; } struct arc_info arc_info; if (dkim_getarcinfo(dkim, &arc_info) != DKIM_STAT_OK) arc_info.arc_state = ARC_CHAIN_NONE; dkim_result_summary drs; memset(&drs, 0, sizeof drs); // don't report DKIM failures if ARC passes if (arc_info.arc_state == ARC_CHAIN_PASS) drs.only_want_success = 1; if (vh->ndoms) { int d_auth = 0; domain_prescreen *print_dps = NULL; if (parm->z.report_all_sigs) { if (vh->domain_ptr && vh->ndoms) { for (int c = 0; c < vh->ndoms; ++c) { domain_prescreen *dps = vh->domain_ptr[c]; for (int ndx = 0; ndx < dps->nsigs; ++ndx) d_auth += print_signature_resinfo(fp, dps, ndx, &drs, dkim); } } if (d_auth == 0) { fprintf(fp, ";\n dkim=%s", drs.result = "none"); d_auth = 1; } } else { for (int c = 0; c < vh->ndoms; ++c) if (vh->domain_ptr[c]->u.f.sig_is_ok) { print_dps = vh->domain_ptr[c]; break; } if (print_dps == NULL) { print_dps = vh->domain_ptr[0]; print_dps->first_good = 0; } d_auth = print_signature_resinfo(fp, print_dps, print_dps->first_good, &drs, dkim); } if (d_auth > 0 && parm->z.verbose >= 3) { char *arc_buf; if (arc_info.arc_state != ARC_CHAIN_NONE) arc_buf = log_arc_info(&arc_info, dkim); else arc_buf = NULL; fl_report(LOG_INFO, "ip=%s: verified:%s dkim=%s (id=%s, %s%sstat=%d)%s%s%s", parm->dyn.info.tcpremoteip, (spf_domain[0] || spf_domain[1])? " spf=pass,": "", drs.result, drs.id? drs.id: "-", drs.err? drs.err: "", drs.err? ", ": "", (int)status, vh->policy_type, vh->policy_result, arc_buf? arc_buf: ""); free(arc_buf); log_written += 1; } free(drs.id); drs.id = NULL; auth_given += d_auth; } if (*vh->policy_result) { char const *method = NULL; int printed = 0; if (POLICY_IS_DMARC(vh->policy)) /* * TODO: polrec.p=quarantine polrec.domain=example.com * (after policy_comment) */ { method = "dmarc"; if (vh->dd.domain) printed = fprintf(fp, ";\n dmarc=%s%s header.from=%s", vh->policy_result, vh->policy_comment, vh->dd.domain); } else if (POLICY_IS_ADSP(vh->policy)) /* * RFC 5617 just says "contents of the From: header field, * with comments removed." * * Note: This method can say nxdomain, DMARC cannot. */ { method = "dkim-adsp"; char const *const user = dkim_getuser(dkim); if (user && vh->dd.domain) printed = fprintf(fp, ";\n dkim-adsp=%s header.from=\"%s@%s\"", vh->policy_result, user, vh->dd.domain); } if (printed == 0 && method) { printed = fprintf(fp, ";\n %s=%s%s", method, vh->policy_result, vh->policy_comment); if (parm->z.verbose >= 4) { fl_report(LOG_INFO, "ip=%s: policy:%s %s%s", parm->dyn.info.tcpremoteip, method, vh->policy_result, vh->policy_comment); log_written += 1; } } if (printed) ++auth_given; } if (arc_info.arc_state != ARC_CHAIN_NONE) { fprintf(fp, ";\n arc=%s", arc_info.arc_state_desc); if (arc_info.arc_state == ARC_CHAIN_PASS) fprintf(fp, " header.oldest-pass=%d", arc_info.oldest_pass); fprintf(fp, " (%d set(s))", arc_info.dkim_arccount); if (*parm->dyn.info.tcpremoteip != 'N') fprintf(fp, " smtp.remote-ip=%s", parm->dyn.info.tcpremoteip); ++auth_given; } if (auth_given <= 0) fputs("; none", fp); fputc('\n', fp); if (log_written == 0 && parm->z.verbose >= 7) { fl_report(LOG_INFO, "ip=%s: verified: %d auth method(s) written", parm->dyn.info.tcpremoteip, auth_given); } if (drs.original_from_dps) /* * Add Original-From: if it wasn't there already. MDA can restore * From: with this value (after any external forwarding). */ { assert_is_unique(vh, drs.original_from_dps); if (!vh->dt.original_from_is_candidate && vh->dt.cf[1] && vh->dt.cf[1]->mailbox) { fprintf(fp, "Original-From: %s\n", vh->dt.cf[1]->mailbox); if (parm->z.verbose >= 8) fl_report(LOG_DEBUG, "ip=%s: add header Original-From: %s", parm->dyn.info.tcpremoteip, vh->dt.cf[1]->mailbox); if (vh->dt.original_from_in_header) vh->dt.original_from_remove = 1; } } if (parm->dyn.no_write) return parm->dyn.rtc = 1; /* * now for the rest of the header, and body */ vh->step = vh_step_copy; vh->outfile = fp; rewind(fl_get_file(parm->fl)); int rtc = verify_headers(vh); if (rtc == 0 && fputc('\n', fp) != EOF && filecopy(fl_get_file(parm->fl), fp) == 0) return parm->dyn.rtc = 1; // good return /* * Either verify_header() or file writing error. * verify_headers() logged any error already. */ if (rtc == 0) fl_report(LOG_CRIT, "ip=%s: file I/O error: %s", parm->dyn.info.tcpremoteip, strerror(errno)); return parm->dyn.rtc = -1; } static FILE* save_file(dkimfl_parm *parm, char const *envelope_sender, char **fname_ptr) // NULL = logged error, otherwise preheader written { assert(parm); assert(parm->fl); assert(parm->z.save_drop); assert(parm->dyn.action_header); static const char templ[] = "/zdrop-"; char const *const dir = parm->z.save_drop; char const *const name = parm->dyn.action_header; static const size_t name_len_min = 10, name_len_max = 80; size_t const dir_len = strlen(dir); size_t const name_len = strlen(name); size_t const namesize = dir_len + sizeof templ + 10 + (name_len > name_len_max? name_len_max: name_len); char *const fname = (char*)malloc(namesize); if (fname == NULL) return NULL; memcpy(fname, dir, dir_len); memcpy(fname + dir_len, templ, sizeof templ); char *p = fname + dir_len + sizeof templ - 1, // max space available for name; 6 for XXXXXX *const end = fname + namesize - 7; char const *s = name; size_t cp_name = 0; while (p < end) { int ch = *(unsigned char*)s++; if (ch == 0 || (isspace(ch) && cp_name > name_len_min)) break; if (!isalnum(ch) && ch != '.' && ch != '-') ch = '_'; *p++ = ch; ++cp_name; } if (p < end) *p++ = '-'; // 123456 strcpy(p, "XXXXXX"); assert(strlen(fname) < namesize); int fno = mkstemp(fname); if (fno == -1) fl_report(LOG_ERR, "mkstemp fails on %s: %s", fname, strerror(errno)); else { FILE *fp = fdopen(fno, "w+"); if (fp) { fprintf(fp, "%s\n", envelope_sender); fl_rcpt_enum *fre = fl_rcpt_start(parm->fl); if (fre) { char *rcpt; while ((rcpt = fl_rcpt_next(fre)) != NULL) fprintf(fp, "%s\n", rcpt); fl_rcpt_clear(fre); fputc('\n', fp); // empty line ends pre-header *fname_ptr = fname; return fp; } fclose(fp); } else { fl_report(LOG_ERR, "fdopen fails: %s", strerror(errno)); close(fno); } unlink(fname); } free(fname); return NULL; } static int check_dkim_status_eom(DKIM_STAT status, DKIM *dkim, dkimfl_parm *parm, char const *trans) { int rtc = 0; switch (status) { case DKIM_STAT_OK: // pass break; case DKIM_STAT_NOSIG: // none case DKIM_STAT_BADSIG: // should be treated as NOSIG... break; case DKIM_STAT_NORESOURCE: case DKIM_STAT_INTERNAL: case DKIM_STAT_CBTRYAGAIN: case DKIM_STAT_CBERROR: { if (parm->z.verbose >= 3) { char const * err = dkim_geterror(dkim); if (err == NULL) err = dkim_getresultstr(status); fl_report(LOG_ERR, "ip=%s: %stemporary verification failure: %s", parm->dyn.info.tcpremoteip, trans, err? err: "NULL"); } // temperror except for missing CNAME (which is permerror) rtc = parm->dyn.rtc = -1; break; } case DKIM_STAT_CBREJECT: // Returned by dkim_sig_sort(). Too many signatures. clean_stats(parm); break; case DKIM_STAT_SYNTAX: case DKIM_STAT_NOKEY: case DKIM_STAT_CANTVRFY: case DKIM_STAT_REVOKED: case DKIM_STAT_KEYFAIL: /* was temporary until 09 Feb 2021 */ default: { if (parm->z.verbose >= 4) { char const *err = dkim_geterror(dkim); if (err == NULL) err = dkim_getresultstr(status); fl_report(LOG_ERR, "ip=%s: %spermanent verification failure: %s", parm->dyn.info.tcpremoteip, trans, err? err: "NULL"); } rtc = 2; break; } } return rtc; } static void set_dmarc(dmarc_domains const *dd, dmarc_rec *dmarc, domain_prescreen **domain_head, dkimfl_parm *parm) /* * Prepare domain record for zaggregate */ { char const *dom; switch (dmarc->found_at_org) { case 0: dom = dd->domain; break; case 1: dom = dd->org_domain; break; case 2: dom = dd->super_org; break; default: dom = NULL; break; } if (dom) { domain_prescreen *dps = get_prescreen(domain_head, dom); if (dps) { dps->u.f.is_dmarc = 1; dps->dmarc_record = write_dmarc_rec(dmarc, 0); if (dmarc->rua) { uint32_t ri = adjust_ri(dmarc->ri, parm->z.honored_report_interval); if (dmarc->ri != 0 && ri != dmarc->ri && parm->z.verbose >= 5) fl_report(LOG_INFO, "ri of %s adjusted from %u to %u " "(honored_report_interval = %d)", dps->name, dmarc->ri, ri, parm->z.honored_report_interval); dps->dmarc_ri = ri; dps->original_ri = dmarc->ri; char *bad = NULL, *rua = dmarc->rua? adjust_rua(&dmarc->rua, &bad): NULL; if (bad && parm->z.verbose >= 5) fl_report(LOG_INFO, "rua URI of %s not supported: %s (supported: %s)", dps->name, bad, rua? rua: "none"); free(bad); dps->dmarc_rua = rua; } } } } static void verify_message(dkimfl_parm *parm) /* * add/remove A-R records, set rtc 1 if ok, 2 if rejected, -1 if failed, * leave rtc as-is (0) if there is no need to rewrite. */ { parm->dyn.is_verifying = 1; if (parm->z.verbose >= 3) fl_report(LOG_INFO, "id=%s: verify msg from %s -- %s", parm->dyn.info.id, parm->dyn.info.tcpremoteip, parm->dyn.info.frommta); verify_parms vh; memset(&vh, 0, sizeof vh); vh.presult = PRESULT_NOT_DONE; // vh.policy = POLICY_NOT_DEFINED; vh.do_adsp = parm->z.honor_author_domain != 0; vh.do_dmarc = parm->z.honor_dmarc != 0; DKIM_STAT status; vh.dkim = dkim_verify(parm->dklib, parm->dyn.info.id, NULL, &status); if (vh.dkim == NULL || status != DKIM_STAT_OK) { char const *err = vh.dkim? dkim_geterror(vh.dkim): NULL; fl_report(LOG_CRIT, "ip=%s: cannot init OpenDKIM: %s", parm->dyn.info.tcpremoteip, err? err: "NULL"); parm->dyn.rtc = -1; clean_stats(parm); return; } context ctx; ctx.sh = NULL; ctx.vh = &vh; dkim_set_user_context(vh.dkim, &ctx); vh.parm = parm; verify_headers(&vh); if (parm->dyn.authserv_id == NULL || parm->dyn.rtc != 0) { if (parm->dyn.no_write == 0 /* not zdkimverify */ || parm->dyn.rtc != 0) { if (parm->dyn.authserv_id) free(parm->dyn.authserv_id); clean_stats(parm); clean_vh(&vh); if (parm->dyn.rtc == 0) fl_report(LOG_ERR, "ip=%s: missing Courier Received: ignoring message", parm->dyn.info.tcpremoteip); return; } // set dummy value, in case if (parm->dyn.authserv_id == NULL) parm->dyn.authserv_id = dummy_authserv_id; } if (vh.dt.vc || dkim_minbody(vh.dkim) > 0) copy_body(parm, vh.dkim, vh.dt.vc); status = dkim_eom(vh.dkim, NULL); int rtc = check_dkim_status_eom(status, vh.dkim, parm, ""); if (rtc) { if (rtc == 2) // permerror /* If there are no domains in domain_head * and parm->z.reject_on_nxdomain is set, * the message should also be rejected. * (The latter condition implies DKIM_STAT_SYNTAX * or DKIM_STAT_INVALID for empty or missing From:) * * However, domain_head != NULL doesn't imply From: is good. */ if (vh.domain_head == NULL) { if (parm->z.reject_on_nxdomain) { fl_pass_message(parm->fl, "550 Unacceptable From: header field.\n"); parm->dyn.rtc = 2; if (parm->z.verbose >= 3) fl_report(LOG_ERR, "ip=%s: no From: domain, message rejected.", parm->dyn.info.tcpremoteip); } /* * Mail with no mailfrom and no From: triggers * "Internal error, invalid stats" in db_set_stats_info. * Why that kind of message has no Received-SPF:? */ clean_stats(parm); /* * On spurious signer, don't even attempt transformation. */ clear_dkim_transform(&vh.dt); } } // transform body if (vh.dt.retry_mode && parm->dyn.rtc == 0) { DKIM_STAT status; vh.dt.dkim = dkim_verify(parm->dklib, parm->dyn.save_file? parm->dyn.save_file: parm->dyn.info.id, NULL, &status); if (vh.dt.dkim == NULL || status != DKIM_STAT_OK) { char const *err = vh.dt.dkim? dkim_geterror(vh.dt.dkim): NULL; fl_report(LOG_CRIT, "ip=%s: cannot init OpenDKIM (trans): %s", parm->dyn.info.tcpremoteip, err? err: "NULL"); clear_dkim_transform(&vh.dt); parm->dyn.rtc = -1; } else if (parm->z.verbose >= 3) { int ndx = peek_original_header(&vh.dt, "From")? 1: 0; fl_report(LOG_INFO, "ip=%s: transformation enabled for \"%s\" %s", parm->dyn.info.tcpremoteip, vh.dt.cf[ndx] && vh.dt.cf[ndx]->mailbox? vh.dt.cf[ndx]->mailbox: "???", retry_mode_string(vh.dt.retry_mode)); } } if (vh.dt.retry_mode && parm->dyn.rtc == 0) { vh.step = vh_step_retry; FILE* fp = fl_get_file(parm->fl); assert(fp); context *ctx = dkim_get_user_context(vh.dkim); dkim_set_user_context(vh.dt.dkim, ctx); if (parm->dyn.save_file) { char const *tmp = parm->z.tmp? parm->z.tmp: "/tmp"; char save_file[MAXPATHLEN]; if (snprintf(save_file, MAXPATHLEN, "%s/%s", tmp, parm->dyn.save_file) < MAXPATHLEN) vh.dt.save_file = strdup(save_file); if (vh.dt.save_file && (vh.dt.save_fp = fopen(vh.dt.save_file, "w")) == NULL) fl_report(LOG_ERR, "ip=%s: cannot open %s: %s", parm->dyn.info.tcpremoteip, vh.dt.save_file, strerror(errno)); } if (vh.dt.retry_mode == transform_retry_header || vh.dt.retry_mode == transform_retry_all) { // set original Content-Type if one was found (mime_wrapped) if (vh.dt.first_content_type && peek_original_header(&vh.dt, content_type) == NULL) add_to_original(&vh.dt, vh.dt.first_content_type); rewind(fp); verify_headers(&vh); if (vh.dt.save_fp) fwrite("\r\n", 2, 1, vh.dt.save_fp); } else // redo just the body { assert(vh.dt.body_offset); fseek(fp, vh.dt.body_offset, SEEK_SET); status = dkim_borrow_header_verify(vh.dt.dkim, vh.dkim); check_dkim_status_eoh(parm, status, "trans body"); if (parm->dyn.rtc == 0) post_eoh_retry(&vh); } } if (vh.dt.retry_mode && parm->dyn.rtc == 0) { if (vh.dt.retry_mode == transform_retry_body || vh.dt.retry_mode == transform_retry_all) /* * Set auto-connvert so as to obtain the same Type/Encoding * and skip the footer (if any) */ { if (vh.dt.mime_wrap && vh.dt.cte == cte_transform_identity_multipart) vh.dt.vc = append_transform_stack(NULL, &skip_one_entity, NULL); else if (vh.dt.add_part && vh.dt.cte == cte_transform_identity_multipart) vh.dt.vc = append_transform_stack(NULL, &skip_last_entity, vh.dt.length); else if (vh.dt.cte < cte_transform_identity_multipart && vh.dt.original_cte < cte_transform_identity_multipart && vh.dt.footer) { vh.dt.vc = set_decode_functions(vh.dt.cte, vh.dt.original_cte, NULL); vh.dt.vc = append_transform_stack(vh.dt.vc, &limit_size, &vh.dt.length[0]); if (vh.dt.original_cte == cte_transform_base64) vh.dt.vc = append_transform_stack(vh.dt.vc, &encode_base64, &vh.dt.b64); else if (vh.dt.original_cte == cte_transform_quoted_printable && vh.dt.cte != cte_transform_quoted_printable) { vh.dt.vc = append_transform_stack(vh.dt.vc, &encode_quoted_printable, &vh.dt.b64); } } copy_body_to_dkim_parm cbvp; memset(&cbvp, 0, sizeof cbvp); cbvp.save_fp = vh.dt.save_fp; cbvp.dkim = vh.dt.dkim; cbvp.dyn_info = parm->dyn.info.tcpremoteip; cbvp.id_or_ip = "ip"; cbvp.verbose = parm->z.verbose; vh.dt.vc = append_transform_stack(vh.dt.vc, ©_body_to_dkim, &cbvp); if (parm->z.verbose >= 5) { char buf[80]; if (vh.dt.cte == vh.dt.original_cte) buf[0] = 0; else snprintf(buf, sizeof buf, " from %s back to %s ", cte_transform_string(vh.dt.cte), cte_transform_string(vh.dt.original_cte)); fl_report(LOG_INFO, "ip=%s: transform message%s with%s%s%s.", parm->dyn.info.tcpremoteip, buf, vh.dt.footer? " footer": "", vh.dt.add_part? " add-part": "", vh.dt.mime_wrap? " mime-wrap": ""); } if (dkim_minbody(vh.dt.dkim) > 0) copy_body(parm, NULL, vh.dt.vc); } else // re-done just the header { status = dkim_borrow_body_verify(vh.dt.dkim, vh.dkim); if (status != DKIM_STAT_OK) { if (parm->z.verbose) { char const *err = dkim_geterror(vh.dt.dkim); if (err == NULL) err = dkim_getresultstr(status); fl_report(LOG_CRIT, "ip=%s: dkim_borrow_body_verify failed: %s (%d)", parm->dyn.info.tcpremoteip, err? err: "unknown", (int)status); } clear_dkim_transform(&vh.dt); parm->dyn.rtc = -1; } } if (parm->dyn.rtc == 0) { status = dkim_eom(vh.dt.dkim, NULL); int rtc = check_dkim_status_eom(status, vh.dt.dkim, parm, " trans: "); /* * Why would it fail on the second call? * Abort transformation in this case (per-domain dps clearing * is done below.) */ if (rtc) clear_dkim_transform(&vh.dt); } } if (vh.dt.retry_mode && parm->dyn.rtc == 0) { /* * TODO: If there is an authenticated original domain, then policies * are to be applied to it, not the rewritten sender. * However, aggregate reports should be sent to both the "official" * From: domain and to the Original-From:/Author:. Note that the * latter is only considered "effective" if the Original-From: * field is originally present. */ if (vh.dt.dkim) { } } /* * Policy results and whitelisting */ vh.policy_type = vh.policy_result = vh.policy_comment = ""; if (vh.domain_flags == 0 && parm->dyn.rtc == 0) domain_flags(&vh); /* * pass unauthenticated From: to database */ if (parm->dyn.rtc == 0 && vh.dd.domain && parm->dyn.stats) { if (parm->z.publicsuffix) parm->dyn.stats->scope = save_unauthenticated_dmarc; else if (parm->z.save_from_anyway) parm->dyn.stats->scope = save_unauthenticated_from; } /* * DMARC/ADSP policy check */ if (parm->dyn.rtc == 0) { if (vh.dd.domain == NULL && vh.dt.dd.domain == NULL) vh.presult = PRESULT_NXDOMAIN; else if (vh.do_dmarc >= vh.do_adsp) { vh.presult = get_dmarc(&vh.dd, &vh.dmarc); if (vh.presult == PRESULT_FOUND) { vh.policy = vh.dmarc.effective_p; if (parm->dwa) set_dmarc(&vh.dd, &vh.dmarc, &vh.domain_head, parm); } if (vh.dt.dd.domain && parm->dwa && ((vh.dt.author_in_header && vh.dt.author_is_candidate) || (vh.dt.original_from_in_header && vh.dt.original_from_is_candidate)) && get_dmarc(&vh.dt.dd, &vh.dt.dmarc) == PRESULT_FOUND) /* * If there was an Author: or Original-From: which happened * to be the candidate, then we assume the header field was * put in place by the author's domain. That can be * interpreted as a request to receive aggregate reports. */ { set_dmarc(&vh.dt.dd, &vh.dt.dmarc, &vh.domain_head, parm); } if (parm->z.verbose >= 7) { char *disp = vh.dd.domain; if (vh.dmarc.found_at_org > 0) { assert(vh.dd.org_domain); assert(vh.dd.super_org || vh.dmarc.found_at_org < 2); char * parent = vh.dmarc.found_at_org == 1? vh.dd.org_domain: vh.dd.super_org; size_t len = strlen(vh.dd.domain), le = strlen(parent); char *p = len > le? malloc(len + 3): NULL; if (p) { size_t diff = len - le; disp = p; *p++ = '['; memcpy(p, vh.dd.domain, diff); p += diff; *p++ = ']'; memcpy(p, parent, le + 1); } } fl_report(LOG_INFO, "DMARC %sabled (%d), policy %s for %s", vh.do_dmarc > 0? "en": "dis", vh.do_dmarc, presult_explain(vh.presult), disp); if (disp != vh.dd.domain) free(disp); } } if (vh.presult != PRESULT_FOUND && vh.presult != PRESULT_NXDOMAIN && vh.do_dmarc <= vh.do_adsp) { vh.presult = my_get_adsp(vh.dd.domain, &vh.policy); if (parm->z.verbose >= 7) fl_report(LOG_INFO, "ADSP %sabled (%d), policy %s for %s", vh.do_adsp > 0? "en": "dis", vh.do_adsp, presult_explain(vh.presult), vh.dd.domain); } if (vh.presult <= PRESULT_DNS_ERROR && (vh.do_dmarc > 0 || vh.do_adsp > 0 || parm->z.reject_on_nxdomain)) { if (parm->z.verbose >= 3) fl_report(LOG_ERR, "ip=%s: temporary author domain verification failure: %s", parm->dyn.info.tcpremoteip, vh.presult == PRESULT_DNS_ERROR? "DNS server": "garbled data"); parm->dyn.rtc = -1; } } /* * Review alignment, whitelisting, dnswl. * Set vh.*_dps according to value/ preference order * (domain_val is the domain preference order). * If transformation failed, do per-domain dps clearing. */ int any_auth_is_ok = 0, from_sig_is_ok = 0, aligned_sig_is_ok = 0, aligned_spf_is_ok = 0; for (domain_prescreen *dps = vh.domain_head; dps; dps = dps->next) { // database whitelisted > 1, SPF or DKIM authenticated if (dps->u.f.is_whitelisted) { if (dps->u.f.sig_is_ok || dps->u.f.spf_pass) { if (vh.whitelisted_dps == NULL || vh.whitelisted_dps->whitelisted < dps->whitelisted || vh.whitelisted_dps->domain_val < dps->domain_val) vh.whitelisted_dps = dps; if (dps->whitelisted < parm->z.whitelisted_pass) dps->u.f.is_whitelisted = 0; } else dps->u.f.is_whitelisted = 0; } if (dps->u.f.is_dnswl && !dps->u.f.shoot_on_sight && (vh.dnswl_dps == NULL || vh.dnswl_dps->dnswl_value < dps->dnswl_value || vh.dnswl_dps->domain_val < dps->domain_val)) vh.dnswl_dps = dps; if (dps->u.f.is_from) { from_sig_is_ok |= dps->u.f.sig_is_ok; if (vh.author_dps == NULL) vh.author_dps = dps; } if (dps->u.f.is_spf_from && !dps->u.f.is_from && parm->z.verbose >= 5) fl_report(LOG_INFO, "ip=%s: different From: is it %s (OpenDKIM) or %s (Courier SPF)?", parm->dyn.info.tcpremoteip, vh.dd.domain? vh.dd.domain: "NULL", dps->name); if (dps->u.f.is_aligned) { aligned_sig_is_ok |= (dps->u.f.is_from || vh.dmarc.adkim != 's') && dps->u.f.sig_is_ok; // spf_pass on From: domain is not officially valid int aligned_spf = (dps->u.f.is_from || vh.dmarc.aspf != 's') && dps->u.f.spf_pass; if (!(dps->u.f.is_mfrom || dps->u.f.is_helo)) aligned_spf <<= 1; aligned_spf_is_ok |= aligned_spf; } if (dps->u.f.spf_pass) { any_auth_is_ok = 1; vh.have_spf_pass = 1; } if (dps->u.f.sig_is_ok) any_auth_is_ok = 1; if (vh.dt.cleared) // per-domain dps clearing { for (int n = 0; n < dps->nsigs; ++n) { signature_prescreen *sp = dps->sig[n]; sp->sig2 = NULL; sp->dk2 = dkim_none; sp->dkim_trans = 0; } } } int aligned_auth_is_ok = aligned_sig_is_ok | aligned_spf_is_ok; /* * Apply policy if it implies to reject or drop message */ if (parm->dyn.rtc == 0) { // !!if (vh.dd.domain != NULL && vh.presult >= 0) // !!{ /* * If NXDOMAIN, pretend to have a strict ADSP policy, so as to * detect adsp-nxdomain --this includes vh.dd.domain == NULL */ if (vh.presult == PRESULT_NXDOMAIN) vh.policy = ADSP_POLICY_ALL; /* * Forced authentication policy. If a policy is defined but not strict, * or if there's no policy at all, force it to reject. */ if (!POLICY_IS_STRICT(vh.policy) && vh.forced_authentication_policy) vh.policy |= FORCED_POLICY_REJECT; bool policy_fail = POLICY_IS_STRICT(vh.policy) && ((POLICY_IS_DMARC(vh.policy) && aligned_auth_is_ok == 0) || (POLICY_IS_FORCED(vh.policy) && any_auth_is_ok == 0) || (POLICY_IS_ADSP(vh.policy) && from_sig_is_ok == 0)); // TODO: Add option to enable reject on bad header. If enabled, // set bad_message also if dkim_headercheck() returns FALSE. bool bad_message = parm->dyn.action_header || vh.shoot_on_sight; if (parm->dyn.stats) { if (vh.presult == PRESULT_NXDOMAIN) { parm->dyn.stats->nxdomain = 1; } else if (POLICY_IS_DMARC(vh.policy)) { parm->dyn.stats->dmarc_found = vh.presult == PRESULT_FOUND; parm->dyn.stats->dmarc_dkim = aligned_sig_is_ok; parm->dyn.stats->dmarc_spf = aligned_spf_is_ok & 1; parm->dyn.stats->dkim_any = vh.ndoms > 0; parm->dyn.stats->spf_any = vh.received_spf > 0; // default values to be filled below assert(parm->dyn.stats->dmarc_reason == dmarc_reason_none); assert(parm->dyn.stats->dmarc_dispo == dmarc_dispo_none); } else if (POLICY_IS_ADSP(vh.policy)) { parm->dyn.stats->adsp_any = 1; parm->dyn.stats->adsp_found = vh.presult == PRESULT_FOUND; parm->dyn.stats->adsp_unknown = vh.policy == ADSP_POLICY_UNKNOWN; parm->dyn.stats->adsp_all = vh.policy == ADSP_POLICY_ALL; parm->dyn.stats->adsp_discardable = vh.policy == ADSP_POLICY_DISCARDABLE; parm->dyn.stats->adsp_fail = policy_fail; } } /* * Determine message disposition: * unless disabled by parameter or whitelisted, do action: * reject if dd.domain is not valid, ADSP == all, or DMARC == reject, * discard if ADSP == discardable; */ if (policy_fail || bad_message) { message_disposition dispo; if (policy_fail && !bad_message && POLICY_IS_DMARC(vh.policy) && vh.dmarc.pct != 100 && random() % 100 >= vh.dmarc.pct) { dispo = dispo_deliver; if (parm->dyn.stats) parm->dyn.stats->dmarc_reason = dmarc_reason_sampled_out; if (parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: %s dmarc=fail, but toss >= %d%%", parm->dyn.info.tcpremoteip, vh.dd.domain, vh.dmarc.pct); } else if (bad_message || (POLICY_IS_DMARC(vh.policy) && vh.do_dmarc > 0) || (POLICY_IS_FORCED(vh.policy) && vh.do_dmarc > 0) || (POLICY_IS_ADSP(vh.policy) && vh.do_adsp > 0) || (parm->z.reject_on_nxdomain && vh.presult == PRESULT_NXDOMAIN)) // candidate for reject/ quarantine/ drop { dispo = dispo_reject; // most likely char const *smtp_reason = NULL, *drop_reason = NULL; char *free_reason = NULL; if (vh.presult == PRESULT_NXDOMAIN && parm->z.reject_on_nxdomain) { smtp_reason = "550 Invalid author domain\n"; if (parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: reject! invalid domain %s", parm->dyn.info.tcpremoteip, vh.dd.domain); } else if (policy_fail && vh.policy == ADSP_POLICY_ALL && vh.do_adsp > 0) { smtp_reason = "550 DKIM signature required by ADSP\n"; if (parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: reject! ADSP policy=all fail for %s", parm->dyn.info.tcpremoteip, vh.dd.domain); } else if (policy_fail && vh.policy == DMARC_POLICY_REJECT && vh.do_dmarc > 0) { smtp_reason = "550 Reject after DMARC policy.\n"; if (parm->dyn.stats) parm->dyn.stats->dmarc_dispo = dmarc_dispo_reject; if (parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: reject! DMARC policy=reject for %s", parm->dyn.info.tcpremoteip, vh.dd.domain); } else if (policy_fail && vh.policy == ADSP_POLICY_DISCARDABLE && vh.do_adsp > 0) { dispo = dispo_drop; drop_reason = "ADSP discardable"; if (parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: drop! ADSP policy=discardable for %s", parm->dyn.info.tcpremoteip, vh.dd.domain); } else if (policy_fail && POLICY_IS_FORCED(vh.policy) && vh.do_dmarc > 1) { smtp_reason = "550 Reject for authentication policy reasons.\n"; if (parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: reject! Forced authentication policy for %s", parm->dyn.info.tcpremoteip, vh.dd.domain); } else if (parm->dyn.action_header) { assert(parm->z.action_header); if (parm->z.header_action_is_reject) { size_t size = strlen(parm->z.action_header) + 10; free_reason = malloc(size); if (free_reason) { snprintf(free_reason, size, "550 %s.\n", parm->z.action_header); smtp_reason = free_reason; } } else { dispo = dispo_drop; drop_reason = parm->z.action_header; } if (parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: %s! %s: %s", parm->dyn.info.tcpremoteip, dispo == dispo_reject? "reject": "drop", parm->z.action_header, parm->dyn.action_header); } else if (vh.shoot_on_sight) { smtp_reason = "550 Rejected for policy reasons (blacklisted).\n"; if (parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: reject! shoot on sight", parm->dyn.info.tcpremoteip); } else if (policy_fail && vh.policy == DMARC_POLICY_QUARANTINE && vh.do_dmarc > 0) /* * Here, we assume that quarantine is going to be * honored downstream based on A-R, if do_dmarc is set. */ { vh.policy_comment = " (QUARANTINE)"; dispo = dispo_quarantine; if (parm->dyn.stats) parm->dyn.stats->dmarc_dispo = dmarc_dispo_quarantine; if (parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: quarantine! DMARC policy=quarantine for %s", parm->dyn.info.tcpremoteip, vh.dd.domain); } else // should never happen { assert(false); dispo = dispo_deliver; fl_report(LOG_CRIT, "INTERNAL ERROR!!: %spolicy_fail, %sbad_message, " "policy=%d, presult=%d, do_dmarc=%d, do_adsp=%d", policy_fail? "": "!", bad_message? "": "!", vh.policy, vh.presult, vh.do_dmarc, vh.do_adsp); } // whitelisting bool override_dispo = false; if (vh.whitelisted_dps && vh.whitelisted_dps->whitelisted >= parm->z.whitelisted_pass) { override_dispo = true; if (parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: %s->deliver!! %s is whitelisted (%d) (auth: %s)", parm->dyn.info.tcpremoteip, explain_dispo(dispo), vh.whitelisted_dps->name, vh.whitelisted_dps->whitelisted, vh.whitelisted_dps->u.f.sig_is_ok? "DKIM": "SPF"); } else if (vh.dnswl_dps && vh.dnswl_dps->dnswl_value >= (uint8_t)parm->z.dnswl_worthiness_pass) { override_dispo = true; if (parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: %s->deliver!! %s is in dnswl (%u)", parm->dyn.info.tcpremoteip, explain_dispo(dispo), vh.dnswl_dps->name, vh.dnswl_dps->dnswl_value); } if (override_dispo) { if (policy_fail && parm->dyn.stats) { parm->dyn.stats->dmarc_dispo = dmarc_dispo_none; parm->dyn.stats->dmarc_reason = dmarc_reason_trusted_forwarder; } dispo = dispo_deliver; } if (dispo != dispo_deliver) { if (parm->dyn.stats) /* * Until 27 Jun 2022 dmarc_dispo was set only if * POLICY_IS_DMARC(vh.policy). Now they are set * anyway. */ { if (dispo == dispo_reject) { parm->dyn.stats->reject = 1; parm->dyn.stats->dmarc_dispo = dmarc_dispo_reject; if (aligned_auth_is_ok) // reject even if pass parm->dyn.stats->dmarc_reason = dmarc_reason_local_policy; } else // quarantine or drop { parm->dyn.stats->dmarc_dispo = dmarc_dispo_quarantine; if (aligned_auth_is_ok) // presumably drop albeit pass parm->dyn.stats->dmarc_reason = dmarc_reason_local_policy; if (dispo == dispo_drop) parm->dyn.stats->drop = 1; } } if (dispo == dispo_reject) { fl_pass_message(parm->fl, smtp_reason? smtp_reason: "550 bad message.\n"); parm->dyn.rtc = 2; } else if (dispo == dispo_drop) // drop, and stop filtering { int droperr = 0; if (parm->z.save_drop) { char *envelope_sender = fl_get_sender(parm->fl), *fname = NULL; FILE *fp = save_file(parm, envelope_sender, &fname); droperr = 1; if (fp) { if (write_file(&vh, fp, status) >= 0) droperr = ferror(fp); droperr |= fclose(fp); if (droperr == 0 && parm->z.verbose >= 4 && fname) fl_report(LOG_INFO, "dropped message saved in %s", my_basename(fname)); } if (droperr) fl_report(LOG_CRIT, "error on %s: %s", fname? fname: "drop file", strerror(errno)); if (parm->dyn.stats) parm->dyn.stats->envelope_sender = envelope_sender; else free(envelope_sender); free(fname); } fl_pass_message(parm->fl, smtp_reason? smtp_reason: "050 Message dropped.\n"); fl_drop_message(parm->fl, drop_reason); parm->dyn.rtc = 2; } } if (free_reason) fl_free_on_exit(parm->fl, free_reason); } else if (policy_fail && POLICY_IS_DMARC(vh.policy) && parm->dyn.stats) // not applying DMARC policy, why? do_dmarc <= 0 { parm->dyn.stats->dmarc_reason = dmarc_reason_local_policy; } } if (POLICY_IS_DMARC(vh.policy)) { /* * If transformation succeeded, it is considered a mailing list. */ if (vh.trans_did_pass && parm->dyn.stats && parm->dyn.stats->dmarc_reason == dmarc_reason_none) parm->dyn.stats->dmarc_reason = dmarc_reason_mailing_list; /* * Policy did not fail only because of BOFHSPFFROM. * This is non-standard, so log it. */ if (!policy_fail && aligned_sig_is_ok == 0 && aligned_spf_is_ok == 2) { if (parm->z.verbose >= 3) fl_report(LOG_INFO, "ip=%s: %s pass only because BOFHSPFFROM", parm->dyn.info.tcpremoteip, vh.dd.domain); if (parm->dyn.stats && parm->dyn.stats->dmarc_reason == dmarc_reason_none) parm->dyn.stats->dmarc_reason = dmarc_reason_other; } } } /* * prepare DMARC/ADSP results for printing A-R */ if (parm->dyn.rtc == 0) { if (vh.presult == PRESULT_NXDOMAIN) { if (POLICY_IS_ADSP(vh.policy)) { vh.policy_type = " adsp="; vh.policy_result = "nxdomain"; } else vh.policy_type = " policy=nxdomain"; } else if (POLICY_IS_DMARC(vh.policy)) { if (vh.policy != DMARC_POLICY_NONE) { vh.policy_result = aligned_auth_is_ok? "pass": "fail"; if (vh.policy == DMARC_POLICY_QUARANTINE) { vh.policy_type = " dmarc:quarantine="; } else // if (vh.policy == DMARC_POLICY_REJECT) { vh.policy_type = " dmarc:reject="; } } } else if (vh.policy == ADSP_POLICY_ALL) { vh.policy_type = " adsp:all="; vh.policy_result = from_sig_is_ok? "pass": "fail"; } else if (vh.policy == ADSP_POLICY_DISCARDABLE) { vh.policy_type = " adsp:discardable="; vh.policy_result = from_sig_is_ok? "pass": "discard"; } } /* * log errors, if any */ if (parm->z.verbose >= 5) { char const *err = dkim_geterror(vh.dkim); if (err && *err) fl_report(LOG_INFO, "ip=%s: %s", parm->dyn.info.tcpremoteip, err); } /* * write the A-R field if required anyway, spf, or signatures */ if (parm->dyn.rtc == 0 && (parm->z.add_a_r_anyway || vh.ndoms || vh.have_spf_pass || *vh.policy_result)) { FILE *fp; if (parm->dyn.no_write) // zdkimverify fp = stdout; else fp = fl_get_write_file(parm->fl); if (fp == NULL) { parm->dyn.rtc = -1; clean_stats(parm); clean_vh(&vh); return; } write_file(&vh, fp, status); } else if (parm->z.verbose >= 3) // log ARC errors, if any { struct arc_info arc_info; if (dkim_getarcinfo(vh.dkim, &arc_info) != DKIM_STAT_OK) arc_info.arc_state = ARC_CHAIN_NONE; if (arc_info.arc_state != ARC_CHAIN_NONE) { char *logstr = log_arc_info(&arc_info, vh.dkim); if (logstr) { fl_report(LOG_INFO, "ip=%s: %s", parm->dyn.info.tcpremoteip, logstr + 2 /* skip comma */); free(logstr); } } } if (parm->dyn.authserv_id != dummy_authserv_id) free(parm->dyn.authserv_id); parm->dyn.authserv_id = NULL; if (parm->dyn.action_header) { free(parm->dyn.action_header); parm->dyn.action_header = NULL; } if (parm->dyn.rtc < 0) clean_stats(parm); else if (parm->dyn.stats) { if (parm->dyn.stats->envelope_sender == NULL) parm->dyn.stats->envelope_sender = fl_get_sender(parm->fl); parm->dyn.stats->domain_head = vh.domain_head; vh.domain_head = NULL; } clean_vh(&vh); } // after filter functions static int update_blocked_user_list(dkimfl_parm *parm) /* * (Re)load list from disk (also run in parent). * return -1 on error, +1 on update, 0 otherwise; */ { char const *const fname = parm->z.blocked_user_list; int updated = 0, rtc = -1; if (fname) { char const *failed_action = NULL; struct stat st; if (stat(fname, &st)) { if (errno == ENOENT) // no file, no blocked users { updated = rtc = parm->blocklist.data || parm->blocklist.size || parm->blocklist.mtime; free(parm->blocklist.data); parm->blocklist.data = NULL; parm->blocklist.size = 0; parm->blocklist.mtime = 0; } else failed_action = "stat"; } else if (st.st_mtime != parm->blocklist.mtime || (size_t)st.st_size != parm->blocklist.size) { if (st.st_size == 0) { free(parm->blocklist.data); parm->blocklist.data = NULL; parm->blocklist.size = 0; parm->blocklist.mtime = st.st_mtime; updated = rtc = 1; } else if ((uint64_t)st.st_size >= SIZE_MAX) { fl_report(LOG_ALERT, "file %s: size %ldu too large: max = %lu\n", fname, st.st_size, SIZE_MAX); } else { char *data = malloc(st.st_size + 1); if (data == NULL) failed_action = "malloc"; else { FILE *fp = fopen(fname, "r"); if (fp == NULL) failed_action = "fopen"; else { size_t in = fread(data, 1, st.st_size, fp); if ((ferror(fp) | fclose(fp)) != 0) failed_action = "fread"; else if (in != (size_t)st.st_size) { if (parm->z.verbose >= 2) fl_report(LOG_NOTICE, "race condition reading %s (size from %zu to %zu)", fname, st.st_size, in); } else { free(parm->blocklist.data); data[in] = 0; parm->blocklist.data = data; parm->blocklist.size = st.st_size; parm->blocklist.mtime = st.st_mtime; updated = rtc = 1; } } } } } else rtc = 0; if (failed_action) fl_report(LOG_ALERT, "cannot %s %s: %s", failed_action, fname, strerror(errno)); else if ((updated && parm->z.verbose >= 2) || parm->z.verbose >= 8) { struct tm tm; localtime_r(&parm->blocklist.mtime, &tm); fl_report(updated? LOG_INFO: LOG_DEBUG, "%s %s version of %04d-%02d-%02dT%02d:%02d:%02d (%zu bytes) on %s", fname, updated? "updated to": "still at", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, parm->blocklist.size, fl_whence_string(parm->fl)); } } return rtc; } static void block_user(dkimfl_parm *parm, char *reason) { assert(parm); assert(reason); assert(!parm->user_blocked); assert(parm->dyn.auth_or_relay); time_t now = time(0); // approx. query time, for list entry /* * check there is an on-disk copy of blocked_user_list, * refresh the in-memory copy of the list, and * ensure the user is still not blocked, */ char const *const fname = parm->z.blocked_user_list; int rtc; if (fname == NULL || (rtc = update_blocked_user_list(parm)) < 0 || (rtc > 0 && search_list(&parm->blocklist, parm->dyn.auth_or_relay) != 0)) return; /* * write to disk a temp copy of the list, * add the user to it, and * move it back to blocked_user_list. */ char const *failed_action = NULL; int failed_errno = 0; size_t l = strlen(fname); char *fname_tmp = malloc(l + 20); if (fname_tmp) { memcpy(fname_tmp, fname, l); fname_tmp[l] = 0; strcat(&fname_tmp[l], ".XXXXXX"); int fd = mkstemp(fname_tmp); if (fd >= 0) { FILE *fp = fdopen(fd, "w"); if (fp) { if (parm->blocklist.data && fwrite(parm->blocklist.data, parm->blocklist.size, 1, fp) != 1) { failed_action = "fwrite"; failed_errno = errno; } else { char *t = strchr(reason, '\n'); if (t) *t = 0; struct tm tm; localtime_r(&now, &tm); fprintf(fp, "%s on %04d-%02d-%02dT%02d:%02d:%02d %s\n", parm->dyn.auth_or_relay, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, reason); } if ((ferror(fp) | fclose(fp)) && failed_action == NULL) { failed_action = "fprintf"; failed_errno = errno; } if (failed_action == NULL) { if (rename(fname_tmp, fname) == 0) { // make this noticeable anyway if (parm->z.verbose >= 1) fl_report(LOG_CRIT, "ip=%s: user %s added to %s: %s", parm->dyn.info.tcpremoteip, parm->dyn.auth_or_relay, fname, reason); } else { failed_action = "rename"; failed_errno = errno; } } } else { failed_action = "fdopen"; failed_errno = errno; } } else { failed_action = "mkstemp"; failed_errno = errno; } free(fname_tmp); } else { failed_action = "malloc"; if ((failed_errno = errno) == 0) failed_errno = ENOMEM; } if (failed_action) fl_report(LOG_CRIT, "cannot %s %s: %s", failed_action, fname, strerror(failed_errno)); } static inline dkimfl_parm *get_parm(fl_parm *fl) { assert(fl); dkimfl_parm **parm = (dkimfl_parm**)fl_get_parm(fl); assert(parm && *parm); return *parm; } static void after_filter_stats(fl_parm *fl) { dkimfl_parm *parm = get_parm(fl); if (parm && parm->dwa && parm->dyn.stats) { if (check_db_connected(parm) == 0) { parm->dyn.stats->pst = parm->pst; if (db_set_stats_info(parm->dwa, parm->dyn.stats) == 0 && parm->dyn.stats->outgoing && !parm->user_blocked) { char *block = db_check_user(parm->dwa); /* * If block is not null and not zero, write block */ if (block) { char *p = block, *t = NULL; while (isspace(*(unsigned char*)p)) ++p; long l = strtol(p, &t, 0); if (l || (t == p && *p)) block_user(parm, p); free(block); } } } some_dwa_cleanup(parm); } clean_stats(parm); if (fl_is_nofork(fl) == 0) { if (parm->dklib) dkim_close(parm->dklib); some_cleanup(parm); free(parm); query_done(); } } static inline void enable_dwa(dkimfl_parm *parm) { if (parm->dwa && (parm->dyn.stats = malloc(sizeof *parm->dyn.stats)) != NULL) { memset(parm->dyn.stats, 0, sizeof *parm->dyn.stats); parm->dyn.stats->ino_mtime_pid = parm->dyn.info.id; } } static void dkimfilter(fl_parm *fl) { static char null_value[] = "NULL"; dkimfl_parm *parm = get_parm(fl); parm->fl = fl; int signing = 1; fl_get_msg_info(fl, &parm->dyn.info); if (parm->dyn.info.id == NULL) parm->dyn.info.id = null_value; if (parm->dyn.info.tcpremoteip == NULL) parm->dyn.info.tcpremoteip = null_value; if (parm->dyn.info.is_relayclient) { if (parm->split != split_verify_only) { /* * We only consider RELAYCLIENT=@signer.example.com * After SMTP Authentication, RELAYCLIENT is set to an empty * value, so we don't need to undo percent relay in that case. */ if (parm->dyn.info.authsender) parm->dyn.auth_or_relay = parm->dyn.info.authsender; else if (!parm->z.let_relayclient_alone && parm->dyn.info.relayclient && // courier < 0.73? parm->dyn.info.relayclient[0] == '@' && parm->dyn.info.relayclient[1] != 0) { parm->dyn.auth_or_relay = parm->dyn.info.relayclient; parm->dyn.undo_percent_relay = 1; } else if (parm->z.default_domain) { size_t n = strlen(parm->z.default_domain) + 2; char *a_or_r = malloc(n); if (a_or_r) { snprintf(a_or_r, n, "@%s", parm->z.default_domain); fl_free_on_exit(fl, a_or_r); parm->dyn.auth_or_relay = a_or_r; } } else if (parm->z.verbose >= 3) { fl_report(LOG_WARNING, "Cannot sign message: " "no authenticated sender, " "empty or disabled RELAYCLIENT, " "and no default_domain"); } if (parm->dyn.auth_or_relay) { if (parm->use_dwa_after_sign) enable_dwa(parm); sign_message(parm); if (parm->dyn.undo_percent_relay) fl_undo_percent_relay(fl, parm->dyn.info.relayclient); } } } else if (parm->split != split_sign_only) { if (vb_init(&parm->dyn.vb)) parm->dyn.rtc = -1; else { if (parm->use_dwa_verifying) enable_dwa(parm); verify_message(parm); signing = 0; } } vb_clean(&parm->dyn.vb); static char const resp_tempfail[] = "432 Mail filter momentarily unavailable.\n"; int verbose_threshold = 4; switch (parm->dyn.rtc) { case -1: // unrecoverable error if (parm->z.tempfail_on_error) { fl_pass_message(fl, resp_tempfail); verbose_threshold = 3; } else fl_pass_message(fl, "250 Failed.\n"); clean_stats(parm); break; case 0: // not rewritten fl_pass_message(fl, "250 not filtered.\n"); break; case 1: // rewritten fl_pass_message(fl, "250 Ok.\n"); break; case 2: // rejected, message already given to fl_pass_message, or dropped; // available info already logged if verbose >= 3 break; case 3: // invalid message, e.g. empty fl_pass_message(fl, "250 Invalid.\n"); break; default: assert(0); break; } if (parm->dyn.stats) fl_set_after_filter(parm->fl, after_filter_stats); else if (parm->dwa) some_dwa_cleanup(parm); assert(fl_get_passed_message(fl) != NULL); if (parm->z.verbose >= verbose_threshold) { char const *msg = fl_get_passed_message(fl); int l = strlen(msg) - 1; assert(l > 0 && msg[l] == '\n'); if (signing) fl_report(LOG_INFO, "id=%s: response: %.*s", parm->dyn.info.id, l, msg); else fl_report(LOG_INFO, "ip=%s: response: %.*s", parm->dyn.info.tcpremoteip, l, msg); } if (parm->dyn.info.id == null_value) parm->dyn.info.id = NULL; if (parm->dyn.info.tcpremoteip == null_value) parm->dyn.info.tcpremoteip = NULL; /* * Keep stuff for after_filter_stats, in case. */ if (fl_is_nofork(fl) == 0 && parm->dyn.stats == NULL) { if (parm->dklib) dkim_close(parm->dklib); some_cleanup(parm); free(parm); query_done(); } } static void return_432(fl_parm *fl) { fl_pass_message(fl, "432 Mail filter not loaded.\n"); } /* * print parm */ static void report_config(fl_parm *fl) { dkimfl_parm *parm = get_parm(fl); void *parm_target[PARM_TARGET_SIZE]; parm_target[parm_t_id] = &parm->z; parm_target[db_parm_t_id] = db_parm_addr(parm->dwa); print_parm(parm_target); } /* * faked DNS lookups by libopendkim, test2 function * (for gdb debugging: use --batch-run and then exit+) */ static void set_keyfile(fl_parm *fl) { assert(fl); dkim_query_t qtype = DKIM_QUERY_FILE; dkimfl_parm *parm = get_parm(fl); static char keyfile[] = "KEYFILE"; assert(parm); int nok = dkim_options(parm->dklib, DKIM_OP_SETOPT, DKIM_OPTS_QUERYMETHOD, &qtype, sizeof qtype) | dkim_options(parm->dklib, DKIM_OP_SETOPT, DKIM_OPTS_QUERYINFO, keyfile, strlen(keyfile)); set_adsp_query_faked('k'); if (nok || parm->z.verbose >= 8) fl_report(nok? LOG_ERR: LOG_INFO, "DKIM query method%s set to file \"%s\"", nok? " not": "", keyfile); } /* * test3 can be used to set an invalid domain * in case no policyfile is found, or the policy specified therein. */ static void set_policyfile(fl_parm *fl) { set_adsp_query_faked('p'); (void)fl; } static void set_fixedtime(fl_parm *fl) { dkimfl_parm *parm = get_parm(fl); uint64_t fixed_time = 1671190000; int rtc = dkim_options(parm->dklib, DKIM_OP_SETOPT, DKIM_OPTS_FIXEDTIME, &fixed_time, sizeof fixed_time); assert(rtc == 0); (void)rtc; // not used under NDEBUG } static const char pid_dir[] = ZDKIMFILTER_PID_DIR; static int pid_file_name(dkimfl_parm *parm, char *fname) { assert(parm && parm->prog_name); if (strlen(parm->prog_name) + sizeof pid_dir + 5 >= PATH_MAX) { errno = ENAMETOOLONG; return 1; } strcpy(fname, pid_dir); fname[sizeof pid_dir - 1] = '/'; strcat(strcpy(fname + sizeof pid_dir, parm->prog_name), ".pid"); return 0; } static int write_pid_file_and_check_split_and_init_pst(fl_parm *fl) // this is init_complete, called once before fl_main loop { assert(fl); dkimfl_parm *parm = get_parm(fl); fl_test_mode test_mode = fl_get_test_mode(fl); assert(parm); // random is used for DMARC pct= srandom((unsigned int)time(NULL)); if (test_mode == fl_no_test) // write pid { char const *failed_action = NULL; char pid_file[PATH_MAX]; if (pid_file_name(parm, pid_file)) failed_action = "name"; else { FILE *fp = fopen(pid_file, "w"); if (fp) { fprintf(fp, "%lu\n", (unsigned long) getpid()); if ((ferror(fp) | fclose(fp)) != 0) failed_action = "write"; parm->pid_created = 1; } else failed_action = "open"; } if (failed_action) fl_report(LOG_ALERT, "cannot %s %s/%s.pid: %s", failed_action, pid_dir, parm->prog_name, strerror(errno)); } /* * fl_wrapped is used by zdkimsign/ verify, so do whatever requested */ if (test_mode != fl_wrapped) check_split(parm); else { parm->dyn.wrapped = 1; parm->z.noaddrrewrite = 1; } if (parm->dkim_init_flag == 0) { if (query_init() < 0) { fl_report(LOG_CRIT, "cannot init query: %s", strerror(errno)); return 1; } } if (parm->split != split_sign_only) { if (parm->z.publicsuffix) { parm->pst = publicsuffix_init(parm->z.publicsuffix, NULL); if (parm->z.psddmarc) parm->pst2 = publicsuffix_init(parm->z.psddmarc, NULL); } } return 0; } static void delete_pid_file(dkimfl_parm *parm) { if (parm->pid_created) { char pid_file[PATH_MAX]; if (pid_file_name(parm, pid_file) != 0 || unlink(pid_file) != 0) fprintf(stderr, "ERR: avfilter: cannot delete %s/%s.pid: %s\n", pid_dir, parm->prog_name, strerror(errno)); } } static void check_blocked_user_list(fl_parm *fl) /* * this gets called once on init and thereafter on every message */ { assert(fl); dkimfl_parm *parm = get_parm(fl); assert(parm); parm->fl = fl; update_blocked_user_list(parm); } static int init_dkim(dkimfl_parm *parm, int savefiles) { parm->dklib = dkim_init(parm->dkim_init_flag, NULL, NULL); if (parm->dklib == NULL) { fl_report(LOG_ERR, "dkim_init fault"); return 1; } int nok = 0; unsigned int options = 0; // TODO: set DKIM_OPTS_SIGNATURETTL (uint64_t) if have expire parameter nok |= dkim_set_prescreen(parm->dklib, dkim_sig_sort) != DKIM_STAT_OK; nok |= dkim_set_key_retrieved(parm->dklib, dkim_key_retrieved) != DKIM_STAT_OK; nok |= dkim_set_final(parm->dklib, dkim_sig_final) != DKIM_STAT_OK; nok |= dkim_options(parm->dklib, DKIM_OP_SETOPT, DKIM_OPTS_SIGNHDRS, parm->z.sign_hfields? parm->z.sign_hfields: dkim_should_signhdrs, sizeof parm->z.sign_hfields) != DKIM_STAT_OK; if (parm->z.oversign_hfields) nok |= dkim_options(parm->dklib, DKIM_OP_SETOPT, DKIM_OPTS_OVERSIGNHDRS, parm->z.oversign_hfields, sizeof parm->z.oversign_hfields) != DKIM_STAT_OK; nok |= dkim_options(parm->dklib, DKIM_OP_SETOPT, DKIM_OPTS_SKIPHDRS, parm->z.skip_hfields? parm->z.skip_hfields: dkim_should_not_signhdrs, sizeof parm->z.skip_hfields) != DKIM_STAT_OK; nok |= dkim_options(parm->dklib, DKIM_OP_GETOPT, DKIM_OPTS_FLAGS, &options, sizeof options) != DKIM_STAT_OK; /* * Set DKIM_LIBFLAGS_DELAYSIGPROC to skip processing sigs at eoh, * since that is done in post_eoh_process(). */ options |= DKIM_LIBFLAGS_DELAYSIGPROC; if (parm->dyn.no_write) // wrapped by zdkimverify { // didn't work, so commented out // options |= DKIM_LIBFLAGS_FIXCRLF; } /* * Until 27 Jun 2022 zdkimfilter puts l= by default. As that is * dangerous, force no_signlen unless Content-Type is to be signed. */ else if (!parm->z.no_signlen) { if (dkim_tobesigned(parm->dklib, "Content-Type") == 1) options |= DKIM_LIBFLAGS_SIGNLEN; else { check_split(parm); if (parm->split != split_verify_only) fl_report(LOG_CRIT, "Dangerous configuration in %s. %s.", parm->config_fname, "Please set no_signlen or sign Content-Type:"); } } if (!parm->z.report_all_sigs) options |= DKIM_LIBFLAGS_VERIFYONE; if (parm->z.add_ztags) options |= DKIM_LIBFLAGS_ZTAGS; #if !defined NDEBUG if (savefiles) options |= DKIM_LIBFLAGS_TMPFILES | DKIM_LIBFLAGS_KEEPFILES; #else (void)savefiles; #endif nok |= dkim_options(parm->dklib, DKIM_OP_SETOPT, DKIM_OPTS_FLAGS, &options, sizeof options) != DKIM_STAT_OK; if (parm->z.dns_timeout > 0) // DEFTIMEOUT is 10 secs { nok |= dkim_options(parm->dklib, DKIM_OP_SETOPT, DKIM_OPTS_TIMEOUT, &parm->z.dns_timeout, sizeof parm->z.dns_timeout) != DKIM_STAT_OK; } if (parm->z.tmp) { nok |= dkim_options(parm->dklib, DKIM_OP_SETOPT, DKIM_OPTS_TMPDIR, parm->z.tmp, sizeof parm->z.tmp) != DKIM_STAT_OK; } if (parm->z.min_key_bits) { nok |= dkim_options(parm->dklib, DKIM_OP_SETOPT, DKIM_OPTS_MINKEYBITS, &parm->z.min_key_bits, sizeof parm->z.min_key_bits) != DKIM_STAT_OK; } if (nok) { fl_report(LOG_ERR, "Unable to set lib options"); dkim_close(parm->dklib); parm->dklib = NULL; return 1; } return 0; } #if HAVE_OPENDBX #define DEFAULT_NO_DB 0 #else #define DEFAULT_NO_DB 1 #endif static void reload_config(fl_parm *fl) /* * on sighup, assume installed filter --no arguments. * config_fname is retained for testing, though. */ { assert(fl); dkimfl_parm **parm = (dkimfl_parm**)fl_get_parm(fl); assert(parm && *parm); int rtc = 1; dkimfl_parm *new_parm = calloc(1, sizeof *new_parm); if (new_parm == NULL) fl_report(LOG_ALERT, "MEMORY FAULT"); else if (parm_config(new_parm, (*parm)->config_fname, DEFAULT_NO_DB)) fl_report(LOG_ERR, "Unable to read new config file"); else { new_parm->dkim_init_flag = (*parm)->dkim_init_flag; rtc = init_dkim(new_parm, 0); } if (rtc == 0) { rtc = 0; new_parm->pid_created = (*parm)->pid_created; if (new_parm->z.verbose >= 2) fl_report(LOG_INFO, "New config file read from %s", (*parm)->config_fname); new_parm->prog_name = (*parm)->prog_name; check_split(new_parm); if (new_parm->dkim_init_flag == 0) { /* * re-read resolv.conf, in case it has changed */ query_done(); if (query_init() < 0) { fl_report(LOG_CRIT, "cannot init query: %s", strerror(errno)); rtc = 1; } } if (new_parm->split != split_sign_only) { /* * publicsuffix_init returns the old pointer if file not changed, * in any case, it manages cleanup. */ if (rtc == 0 && new_parm->z.publicsuffix) { new_parm->pst = publicsuffix_init(new_parm->z.publicsuffix, (*parm)->pst); (*parm)->pst = NULL; if (new_parm->z.psddmarc) { new_parm->pst2 = publicsuffix_init(new_parm->z.psddmarc, (*parm)->pst2); (*parm)->pst2 = NULL; } } } } if (rtc) { some_cleanup(new_parm); free(new_parm); } else { dkimfl_parm *old_parm = *parm; if (old_parm->dklib) dkim_close(old_parm->dklib); some_cleanup(old_parm); free(old_parm); *parm = new_parm; } } static fl_init_parm functions = { dkimfilter, write_pid_file_and_check_split_and_init_pst, check_blocked_user_list, reload_config, NULL, NULL, report_config, set_keyfile, set_policyfile, set_fixedtime }; int main(int argc, char *argv[]) { int rtc = 0, i, no_db = DEFAULT_NO_DB, no_write = 0, do_seal = 0, savefiles = 0; char *config_file = NULL, *save_file = NULL; unsigned int dkim_init_flag = 0; for (i = 1; i < argc; ++i) { char const *const arg = argv[i]; if (strcmp(arg, "-f") == 0) { config_file = ++i < argc ? argv[i] : NULL; } else if (strcmp(arg, "--save-files") == 0) { savefiles = 1; save_file = ++i < argc ? argv[i] : NULL; } else if (strcmp(arg, "--no-db") == 0) { no_db = 1; } else if (strcmp(arg, "--no-write") == 0) { no_write = 1; } else if (strcmp(arg, "--do-seal") == 0) { do_seal = 1; } else if (strcmp(arg, "--version") == 0) { char const *version_string; unsigned long version_number = dkim_ssl_version(&version_string); char buf[16]; int bufok = snprintf(buf, sizeof buf, "%ld.%ld.%ld", #ifdef USE_GNUTLS version_number >> 16, (version_number & 0xff00L) >> 8, version_number & 0xff #else version_number >> 28, (version_number & 0xff00000L) >> 20, (version_number & 0xff000L) >> 12 #endif ) < (int) sizeof buf; printf(PACKAGE_NAME ", version " PACKAGE_VERSION "\n" "Compiled with" #if defined NDEBUG "out" #endif " debugging support\n" "Compiled with " #ifdef USE_GNUTLS "GnuTLS" #else "OpenSSL" #endif " library version %#lX\n" "Linked with " #ifdef USE_GNUTLS "GnuTLS" #else "OpenSSL" #endif " library version %s (%s)\n" "and with libunistring version: %#lX (%smatch)\n" "and with IDN2 version: %#lX (%smatch)\n", version_number, version_string, bufok? strstr(version_string, buf)? "match": "DOESN'T MATCH": "??", (unsigned long)_libunistring_version, (unsigned long)_libunistring_version == (unsigned long)(_LIBUNISTRING_VERSION)? "": "DO NOT ", (unsigned long)IDN2_VERSION, idn2_check_version(IDN2_VERSION)? "": "DO NOT "); return 0; } else if (strcmp(arg, "--help") == 0) { printf("zdkimfilter command line args:\n" /* 12345678901234567890123456 */ " -f config-filename override %s\n" " --save-files [filename] save intermediate debug files\n" " --no-db omit database processing\n" " --no-write don't rewrite mail, output some info\n" " --do-seal don't sign DKIM, seal ARC\n" " --help print this stuff and exit\n" " --version print version string and exit\n", default_config_file); fl_main(NULL, NULL, argc - i + 1, argv + i - 1, 0, 0); return 0; } else if (strcmp(arg, "--batch-test") == 0) { fl_log_no_pid = 1; dkim_init_flag = 1; } } char *prog_name = my_basename(argv[0]), *emb; if (config_file == NULL && (emb = strstr(prog_name, "-f_")) != NULL) { unsigned int len = strlen(prog_name) + sizeof default_config_file; if ((config_file = malloc(len)) == NULL) rtc = 2; else { i = -1; // can free config_file strcpy(config_file, default_config_file); char *trail = strrchr(config_file, '/'); if (trail == NULL) rtc = 2; else { if (emb[3] == 0) // use whole name if -f_ is at end emb = prog_name; else emb += 3; // append extension if default has it and this misses it strcpy(trail + 1, emb); char const *ext = strrchr(default_config_file, '.'); trail = strrchr(trail, '.'); if (ext && !(trail && strcmp(trail, ext) == 0)) strcat(config_file, ext); } } } set_argv0_slash(prog_name); set_program_name(prog_name); dkimfl_parm *parm = rtc == 0? calloc(1, sizeof *parm): NULL; if (parm == NULL || parm_config(parm, config_file, no_db)) { rtc = 2; fl_report(LOG_ERR, parm? "Error reading config file": "MEMORY FAULT"); } parm->prog_name = prog_name; parm->dyn.no_write = no_write; parm->dyn.save_file = save_file; parm->dyn.do_seal = do_seal; parm->dkim_init_flag = dkim_init_flag; if (rtc == 0) { if (init_dkim(parm, savefiles)) rtc = 2; } if (rtc) functions.filter_fn = &return_432; if (rtc == 0 || argc == 1) rtc = fl_main(&functions, &parm, argc, argv, parm->z.all_mode, parm->z.verbose); if (parm) { delete_pid_file(parm); if (parm->dklib) dkim_close(parm->dklib); some_cleanup(parm); free(parm); query_done(); } if (i < 0) { free(config_file); config_file = NULL; } return rtc; }