#include <sys_defs.h>
#ifdef USE_TLS
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#ifdef STRCASECMP_IN_STRINGS_H
#include <strings.h>
#endif
#include <msg.h>
#include <mymalloc.h>
#include <vstring.h>
#include <stringops.h>
#include <valid_utf8_hostname.h>
#include <ctable.h>
#include <mail_params.h>
#include <maps.h>
#include <dsn_buf.h>
#include <dns.h>
#include "smtp.h"
#define CACHE_SIZE 20
static CTABLE *policy_cache;
static int global_tls_level(void);
static void dane_init(SMTP_TLS_POLICY *, SMTP_ITERATOR *);
static MAPS *tls_policy;
static MAPS *tls_per_site;
void smtp_tls_list_init(void)
{
if (*var_smtp_tls_policy) {
tls_policy = maps_create(VAR_LMTP_SMTP(TLS_POLICY),
var_smtp_tls_policy,
DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX
| DICT_FLAG_UTF8_REQUEST);
if (*var_smtp_tls_per_site)
msg_warn("%s ignored when %s is not empty.",
VAR_LMTP_SMTP(TLS_PER_SITE), VAR_LMTP_SMTP(TLS_POLICY));
return;
}
if (*var_smtp_tls_per_site) {
tls_per_site = maps_create(VAR_LMTP_SMTP(TLS_PER_SITE),
var_smtp_tls_per_site,
DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX
| DICT_FLAG_UTF8_REQUEST);
}
}
static const char *policy_name(int tls_level)
{
const char *name = str_tls_level(tls_level);
if (name == 0)
name = "unknown";
return name;
}
#define MARK_INVALID(why, levelp) do { \
dsb_simple((why), "4.7.5", "client TLS configuration problem"); \
*(levelp) = TLS_LEV_INVALID; } while (0)
static void tls_site_lookup(SMTP_TLS_POLICY *tls, int *site_level,
const char *site_name, const char *site_class)
{
const char *lookup;
if ((lookup = maps_find(tls_per_site, site_name, 0)) != 0) {
if (!strcasecmp(lookup, "NONE")) {
if (*site_level <= TLS_LEV_MAY)
*site_level = TLS_LEV_NONE;
} else if (!strcasecmp(lookup, "MAY")) {
if (*site_level < TLS_LEV_NONE)
*site_level = TLS_LEV_MAY;
} else if (!strcasecmp(lookup, "MUST_NOPEERMATCH")) {
if (*site_level < TLS_LEV_ENCRYPT)
*site_level = TLS_LEV_ENCRYPT;
} else if (!strcasecmp(lookup, "MUST")) {
if (*site_level < TLS_LEV_VERIFY)
*site_level = TLS_LEV_VERIFY;
} else {
msg_warn("%s: unknown TLS policy '%s' for %s %s",
tls_per_site->title, lookup, site_class, site_name);
MARK_INVALID(tls->why, site_level);
return;
}
} else if (tls_per_site->error) {
msg_warn("%s: %s \"%s\": per-site table lookup error",
tls_per_site->title, site_class, site_name);
dsb_simple(tls->why, "4.3.0", "Temporary lookup error");
*site_level = TLS_LEV_INVALID;
return;
}
return;
}
static void tls_policy_lookup_one(SMTP_TLS_POLICY *tls, int *site_level,
const char *site_name,
const char *site_class)
{
const char *lookup;
char *policy;
char *saved_policy;
char *tok;
const char *err;
char *name;
char *val;
static VSTRING *cbuf;
#undef FREE_RETURN
#define FREE_RETURN do { myfree(saved_policy); return; } while (0)
#define INVALID_RETURN(why, levelp) do { \
MARK_INVALID((why), (levelp)); FREE_RETURN; } while (0)
#define WHERE \
STR(vstring_sprintf(cbuf, "%s, %s \"%s\"", \
tls_policy->title, site_class, site_name))
if (cbuf == 0)
cbuf = vstring_alloc(10);
if ((lookup = maps_find(tls_policy, site_name, 0)) == 0) {
if (tls_policy->error) {
msg_warn("%s: policy table lookup error", WHERE);
MARK_INVALID(tls->why, site_level);
}
return;
}
saved_policy = policy = mystrdup(lookup);
if ((tok = mystrtok(&policy, CHARS_COMMA_SP)) == 0) {
msg_warn("%s: invalid empty policy", WHERE);
INVALID_RETURN(tls->why, site_level);
}
*site_level = tls_level_lookup(tok);
if (*site_level == TLS_LEV_INVALID) {
msg_warn("%s: invalid security level \"%s\"", WHERE, tok);
INVALID_RETURN(tls->why, site_level);
}
if (*site_level < TLS_LEV_MAY) {
while ((tok = mystrtok(&policy, CHARS_COMMA_SP)) != 0)
msg_warn("%s: ignoring attribute \"%s\" with TLS disabled",
WHERE, tok);
FREE_RETURN;
}
while ((tok = mystrtok(&policy, CHARS_COMMA_SP)) != 0) {
if ((err = split_nameval(tok, &name, &val)) != 0) {
msg_warn("%s: malformed attribute/value pair \"%s\": %s",
WHERE, tok, err);
INVALID_RETURN(tls->why, site_level);
}
if (!strcasecmp(name, "ciphers")) {
if (*val == 0) {
msg_warn("%s: attribute \"%s\" has empty value", WHERE, name);
INVALID_RETURN(tls->why, site_level);
}
if (tls->grade) {
msg_warn("%s: attribute \"%s\" is specified multiple times",
WHERE, name);
INVALID_RETURN(tls->why, site_level);
}
tls->grade = mystrdup(val);
continue;
}
if (!strcasecmp(name, "protocols")) {
if (tls->protocols) {
msg_warn("%s: attribute \"%s\" is specified multiple times",
WHERE, name);
INVALID_RETURN(tls->why, site_level);
}
tls->protocols = mystrdup(val);
continue;
}
if (!strcasecmp(name, "match")) {
if (*val == 0) {
msg_warn("%s: attribute \"%s\" has empty value", WHERE, name);
INVALID_RETURN(tls->why, site_level);
}
switch (*site_level) {
default:
msg_warn("%s: attribute \"%s\" invalid at security level "
"\"%s\"", WHERE, name, policy_name(*site_level));
INVALID_RETURN(tls->why, site_level);
break;
case TLS_LEV_FPRINT:
if (!tls->dane)
tls->dane = tls_dane_alloc();
tls_dane_add_ee_digests(tls->dane,
var_smtp_tls_fpt_dgst, val, "|");
break;
case TLS_LEV_VERIFY:
case TLS_LEV_SECURE:
if (tls->matchargv == 0)
tls->matchargv = argv_split(val, ":");
else
argv_split_append(tls->matchargv, val, ":");
break;
}
continue;
}
if (!strcasecmp(name, "exclude")) {
if (tls->exclusions) {
msg_warn("%s: attribute \"%s\" is specified multiple times",
WHERE, name);
INVALID_RETURN(tls->why, site_level);
}
tls->exclusions = vstring_strcpy(vstring_alloc(10), val);
continue;
}
if (!strcasecmp(name, "tafile")) {
if (!TLS_MUST_PKIX(*site_level)) {
msg_warn("%s: attribute \"%s\" invalid at security level"
" \"%s\"", WHERE, name, policy_name(*site_level));
INVALID_RETURN(tls->why, site_level);
}
if (*val == 0) {
msg_warn("%s: attribute \"%s\" has empty value", WHERE, name);
INVALID_RETURN(tls->why, site_level);
}
if (!tls->dane)
tls->dane = tls_dane_alloc();
if (!tls_dane_load_trustfile(tls->dane, val)) {
INVALID_RETURN(tls->why, site_level);
}
continue;
}
msg_warn("%s: invalid attribute name: \"%s\"", WHERE, name);
INVALID_RETURN(tls->why, site_level);
}
FREE_RETURN;
}
static void tls_policy_lookup(SMTP_TLS_POLICY *tls, int *site_level,
const char *site_name,
const char *site_class)
{
if (!valid_utf8_hostname(var_smtputf8_enable, site_name, DONT_GRIPE)) {
tls_policy_lookup_one(tls, site_level, site_name, site_class);
return;
}
do {
tls_policy_lookup_one(tls, site_level, site_name, site_class);
} while (*site_level == TLS_LEV_NOTFOUND
&& (site_name = strchr(site_name + 1, '.')) != 0);
}
static int load_tas(TLS_DANE *dane, const char *files)
{
int ret = 0;
char *save = mystrdup(files);
char *buf = save;
char *file;
do {
if ((file = mystrtok(&buf, CHARS_COMMA_SP)) != 0)
ret = tls_dane_load_trustfile(dane, file);
} while (file && ret);
myfree(save);
return (ret);
}
static void set_cipher_grade(SMTP_TLS_POLICY *tls)
{
const char *mand_exclude = "";
const char *also_exclude = "";
switch (tls->level) {
case TLS_LEV_INVALID:
case TLS_LEV_NONE:
return;
case TLS_LEV_MAY:
if (tls->grade == 0)
tls->grade = mystrdup(var_smtp_tls_ciph);
break;
case TLS_LEV_ENCRYPT:
if (tls->grade == 0)
tls->grade = mystrdup(var_smtp_tls_mand_ciph);
mand_exclude = var_smtp_tls_mand_excl;
also_exclude = "eNULL";
break;
case TLS_LEV_HALF_DANE:
case TLS_LEV_DANE:
case TLS_LEV_DANE_ONLY:
case TLS_LEV_FPRINT:
case TLS_LEV_VERIFY:
case TLS_LEV_SECURE:
if (tls->grade == 0)
tls->grade = mystrdup(var_smtp_tls_mand_ciph);
mand_exclude = var_smtp_tls_mand_excl;
also_exclude = "aNULL";
break;
}
#define ADD_EXCLUDE(vstr, str) \
do { \
if (*(str)) \
vstring_sprintf_append((vstr), "%s%s", \
VSTRING_LEN(vstr) ? " " : "", (str)); \
} while (0)
if (tls->exclusions == 0) {
tls->exclusions = vstring_alloc(10);
ADD_EXCLUDE(tls->exclusions, var_smtp_tls_excl_ciph);
ADD_EXCLUDE(tls->exclusions, mand_exclude);
}
ADD_EXCLUDE(tls->exclusions, also_exclude);
}
static void *policy_create(const char *unused_key, void *context)
{
SMTP_ITERATOR *iter = (SMTP_ITERATOR *) context;
int site_level;
const char *dest = STR(iter->dest);
const char *host = STR(iter->host);
SMTP_TLS_POLICY *tls = (SMTP_TLS_POLICY *) mymalloc(sizeof(*tls));
smtp_tls_policy_init(tls, dsb_create());
tls->level = global_tls_level();
site_level = TLS_LEV_NOTFOUND;
if (tls_policy) {
tls_policy_lookup(tls, &site_level, dest, "next-hop destination");
} else if (tls_per_site) {
tls_site_lookup(tls, &site_level, dest, "next-hop destination");
if (site_level != TLS_LEV_INVALID
&& strcasecmp_utf8(dest, host) != 0)
tls_site_lookup(tls, &site_level, host, "server hostname");
if (site_level == TLS_LEV_MAY && tls->level > TLS_LEV_MAY)
site_level = tls->level;
}
switch (site_level) {
default:
tls->level = site_level;
case TLS_LEV_NOTFOUND:
break;
case TLS_LEV_INVALID:
tls->level = site_level;
return ((void *) tls);
}
if (TLS_DANE_BASED(tls->level))
dane_init(tls, iter);
if (tls->level == TLS_LEV_INVALID)
return ((void *) tls);
if (tls->level > TLS_LEV_NONE && tls->protocols == 0)
tls->protocols =
mystrdup((tls->level == TLS_LEV_MAY) ?
var_smtp_tls_proto : var_smtp_tls_mand_proto);
set_cipher_grade(tls);
switch (tls->level) {
case TLS_LEV_INVALID:
case TLS_LEV_NONE:
case TLS_LEV_MAY:
case TLS_LEV_ENCRYPT:
case TLS_LEV_HALF_DANE:
case TLS_LEV_DANE:
case TLS_LEV_DANE_ONLY:
break;
case TLS_LEV_FPRINT:
if (tls->dane == 0)
tls->dane = tls_dane_alloc();
if (!TLS_DANE_HASEE(tls->dane)) {
tls_dane_add_ee_digests(tls->dane, var_smtp_tls_fpt_dgst,
var_smtp_tls_fpt_cmatch, CHARS_COMMA_SP);
if (!TLS_DANE_HASEE(tls->dane)) {
msg_warn("nexthop domain %s: configured at fingerprint "
"security level, but with no fingerprints to match.",
dest);
MARK_INVALID(tls->why, &tls->level);
return ((void *) tls);
}
}
break;
case TLS_LEV_VERIFY:
case TLS_LEV_SECURE:
if (tls->matchargv == 0)
tls->matchargv =
argv_split(tls->level == TLS_LEV_VERIFY ?
var_smtp_tls_vfy_cmatch : var_smtp_tls_sec_cmatch,
CHARS_COMMA_SP ":");
if (*var_smtp_tls_tafile) {
if (tls->dane == 0)
tls->dane = tls_dane_alloc();
if (!TLS_DANE_HASTA(tls->dane)
&& !load_tas(tls->dane, var_smtp_tls_tafile)) {
MARK_INVALID(tls->why, &tls->level);
return ((void *) tls);
}
}
break;
default:
msg_panic("unexpected TLS security level: %d", tls->level);
}
if (msg_verbose && tls->level != global_tls_level())
msg_info("%s TLS level: %s", "effective", policy_name(tls->level));
return ((void *) tls);
}
static void policy_delete(void *item, void *unused_context)
{
SMTP_TLS_POLICY *tls = (SMTP_TLS_POLICY *) item;
if (tls->protocols)
myfree(tls->protocols);
if (tls->grade)
myfree(tls->grade);
if (tls->exclusions)
vstring_free(tls->exclusions);
if (tls->matchargv)
argv_free(tls->matchargv);
if (tls->dane)
tls_dane_free(tls->dane);
dsb_free(tls->why);
myfree((void *) tls);
}
int smtp_tls_policy_cache_query(DSN_BUF *why, SMTP_TLS_POLICY *tls,
SMTP_ITERATOR *iter)
{
VSTRING *key;
if (policy_cache == 0)
policy_cache =
ctable_create(CACHE_SIZE, policy_create, policy_delete, (void *) 0);
key = vstring_alloc(100);
smtp_key_prefix(key, ":", iter, SMTP_KEY_FLAG_NEXTHOP
| SMTP_KEY_FLAG_HOSTNAME
| SMTP_KEY_FLAG_PORT);
ctable_newcontext(policy_cache, (void *) iter);
*tls = *(SMTP_TLS_POLICY *) ctable_locate(policy_cache, STR(key));
vstring_free(key);
if (tls->level == TLS_LEV_INVALID) {
dsb_update(why,
STR(tls->why->status), STR(tls->why->action),
STR(tls->why->mtype), STR(tls->why->mname),
STR(tls->why->dtype), STR(tls->why->dtext),
"%s", STR(tls->why->reason));
return (0);
} else {
return (1);
}
}
void smtp_tls_policy_cache_flush(void)
{
if (policy_cache != 0) {
ctable_free(policy_cache);
policy_cache = 0;
}
}
static int global_tls_level(void)
{
static int l = TLS_LEV_NOTFOUND;
if (l != TLS_LEV_NOTFOUND)
return l;
if (*var_smtp_tls_level) {
if ((l = tls_level_lookup(var_smtp_tls_level)) == TLS_LEV_INVALID)
msg_fatal("invalid tls security level: \"%s\"", var_smtp_tls_level);
} else if (var_smtp_enforce_tls)
l = var_smtp_tls_enforce_peername ? TLS_LEV_VERIFY : TLS_LEV_ENCRYPT;
else
l = var_smtp_use_tls ? TLS_LEV_MAY : TLS_LEV_NONE;
if (msg_verbose)
msg_info("%s TLS level: %s", "global", policy_name(l));
return l;
}
#define NONDANE_CONFIG 0
#define NONDANE_DEST 1
#define DANE_CANTAUTH 2
static void PRINTFLIKE(4, 5) dane_incompat(SMTP_TLS_POLICY *tls,
SMTP_ITERATOR *iter,
int errtype,
const char *fmt,...)
{
va_list ap;
va_start(ap, fmt);
if (tls->level == TLS_LEV_DANE) {
tls->level = (errtype == DANE_CANTAUTH) ? TLS_LEV_ENCRYPT : TLS_LEV_MAY;
if (errtype == NONDANE_CONFIG)
vmsg_warn(fmt, ap);
else if (msg_verbose)
vmsg_info(fmt, ap);
} else {
if (errtype == NONDANE_CONFIG) {
vmsg_warn(fmt, ap);
MARK_INVALID(tls->why, &tls->level);
} else {
tls->level = TLS_LEV_INVALID;
vdsb_simple(tls->why, "4.7.5", fmt, ap);
}
}
va_end(ap);
}
static void dane_init(SMTP_TLS_POLICY *tls, SMTP_ITERATOR *iter)
{
TLS_DANE *dane;
if (!iter->port) {
msg_warn("%s: the \"dane\" security level is invalid for delivery via"
" unix-domain sockets", STR(iter->dest));
MARK_INVALID(tls->why, &tls->level);
return;
}
if (!tls_dane_avail()) {
dane_incompat(tls, iter, NONDANE_CONFIG,
"%s: %s configured, but no requisite library support",
STR(iter->dest), policy_name(tls->level));
return;
}
if (!(smtp_host_lookup_mask & SMTP_HOST_FLAG_DNS)
|| smtp_dns_support != SMTP_DNS_DNSSEC) {
dane_incompat(tls, iter, NONDANE_CONFIG,
"%s: %s configured with dnssec lookups disabled",
STR(iter->dest), policy_name(tls->level));
return;
}
if (smtp_mode && var_ign_mx_lookup_err) {
dane_incompat(tls, iter, NONDANE_CONFIG,
"%s: %s configured with MX lookup errors ignored",
STR(iter->dest), policy_name(tls->level));
return;
}
if (smtp_dns_res_opt & (RES_DEFNAMES | RES_DNSRCH)) {
dane_incompat(tls, iter, NONDANE_CONFIG,
"%s: dns resolver options incompatible with %s TLS",
STR(iter->dest), policy_name(tls->level));
return;
}
if (iter->mx && !iter->mx->dnssec_valid
&& (tls->level == TLS_LEV_DANE_ONLY ||
smtp_tls_insecure_mx_policy <= TLS_LEV_MAY)) {
dane_incompat(tls, iter, NONDANE_DEST, "non DNSSEC destination");
return;
}
if ((dane = tls_dane_resolve(iter->port, "tcp", iter->rr,
var_smtp_tls_force_tlsa)) == 0) {
tls->level = TLS_LEV_INVALID;
dsb_simple(tls->why, "4.7.5", "TLSA lookup error for %s:%u",
STR(iter->host), ntohs(iter->port));
return;
}
if (tls_dane_notfound(dane)) {
dane_incompat(tls, iter, NONDANE_DEST, "no TLSA records found");
tls_dane_free(dane);
return;
}
if (tls_dane_unusable(dane)) {
dane_incompat(tls, iter, DANE_CANTAUTH, "TLSA records unusable");
tls_dane_free(dane);
return;
}
if (iter->mx && !iter->mx->dnssec_valid) {
if (smtp_tls_insecure_mx_policy == TLS_LEV_ENCRYPT) {
dane_incompat(tls, iter, DANE_CANTAUTH,
"Verification not possible, MX RRset is insecure");
tls_dane_free(dane);
return;
}
if (tls->level != TLS_LEV_DANE
|| smtp_tls_insecure_mx_policy != TLS_LEV_DANE)
msg_panic("wrong state for insecure MX host DANE policy");
tls->level = TLS_LEV_HALF_DANE;
}
if (TLS_DANE_HASTA(dane)) {
tls->matchargv = argv_alloc(2);
argv_add(tls->matchargv, dane->base_domain, ARGV_END);
if (iter->mx) {
if (strcmp(iter->mx->qname, iter->mx->rname) == 0)
argv_add(tls->matchargv, iter->mx->qname, ARGV_END);
else
argv_add(tls->matchargv, iter->mx->rname,
iter->mx->qname, ARGV_END);
}
} else if (!TLS_DANE_HASEE(dane))
msg_panic("empty DANE match list");
tls->dane = dane;
return;
}
#endif