Home | History | Annotate | Line # | Download | only in smtp
      1 /*	$NetBSD: smtp_reqtls_policy.c,v 1.2 2026/05/09 18:49:20 christos Exp $	*/
      2 
      3 /*++
      4 /* NAME
      5 /*	smtp_reqtls_policy 3
      6 /* SUMMARY
      7 /*	requiretls next-hop policy
      8 /* SYNOPSIS
      9 /*	#include <smtp_reqtls_policy.h>
     10 /*
     11 /*	SMTP_REQTLS_POLICY *smtp_reqtls_policy_parse(
     12 /*	const char *origin,
     13 /*	const char *extern_policy)
     14 /*
     15 /*	int	smtp_reqtls_policy_eval(
     16 /*	SMTP_REQTLS_POLICY *intern_policy,
     17 /*	const char *nexthop_name)
     18 /*
     19 /*	void	smtp_reqtls_policy_free(
     20 /*	SMTP_REQTLS_POLICY *intern_policy)
     21 /* DESCRIPTION
     22 /*	This module determines the REQUIRETLS enforcement level for
     23 /*	connections to a next-hop destination.
     24 /*
     25 /*	smtp_reqtls_policy_parse() converts a policy from human-readable
     26 /*	form to internal form. It should be called as part of
     27 /*	before-chroot initialization. A policy is a list of elements that
     28 /*	will be matched in the specified order. A policy element must be
     29 /*	an atom ("enforce", "opportunistic+starttls", "opportunistic",
     30 /*	"disable", "error") or a type:table. A table lookup result must
     31 /*	be an atom, not a type:table. To match a parent domain name
     32 /*	with a table that wants and exact match, specify an explicit
     33 /*	ASCII '.' before the parent domain name. In a policy lookup
     34 /*	table, an internationalized domain name must be specified in
     35 /*	A-label form.
     36 /*
     37 /*	smtp_reqtls_policy_eval() evaluates an internal-form
     38 /*	policy for the specified next-hop destination. An
     39 /*	internationalized next-hop name is converted to A-label form.
     40 /*	The result is SMTP_REQTLS_POLICY_ACT_ENFORCE (enforce),
     41 /*	SMTP_REQTLS_POLICY_ACT_OPP_TLS (opportunistic+starttls),
     42 /*	SMTP_REQTLS_POLICY_ACT_OPPORTUNISTIC (opportunistic),
     43 /*	SMTP_REQTLS_POLICY_ACT_DISABLE, or SMTP_REQTLS_POLICY_ACT_ERROR
     44 /*	(unknown command or database access error).
     45 /*
     46 /*	smtp_reqtls_policy_free() destroys an internal-form policy.
     47 /*
     48 /*	Arguments:
     49 /* .IP origin
     50 /*	The configuration parameter name for the policy from main.cf.
     51 /*	This is used for error reporting only.
     52 /* .IP extern_policy
     53 /*	External policy representation.
     54 /* .IP intern_policy
     55 /*	Internal policy representation.
     56 /* .IP nexthop_name
     57 /*	Next-hop destination without [ ], :service,  or :port.
     58 /* HISTORY
     59 /*	This as derived from the Postfix server_policy(3) module.
     60 /* LICENSE
     61 /* .ad
     62 /* .fi
     63 /*	The Secure Mailer license must be distributed with this software.
     64 /* AUTHOR(S)
     65 /*	Wietse Venema
     66 /*	porcupine.org
     67 /*--*/
     68 
     69  /*
     70   * System library.
     71   */
     72 #include <sys_defs.h>
     73 #include <string.h>
     74 
     75 #ifdef STRCASECMP_IN_STRINGS_H
     76 #include <strings.h>
     77 #endif
     78 
     79  /*
     80   * Utility library.
     81   */
     82 #include <msg.h>
     83 #include <midna_domain.h>
     84 #include <mymalloc.h>
     85 #include <stringops.h>
     86 #include <dict.h>
     87 #include <valid_hostname.h>
     88 
     89  /*
     90   * Global library.
     91   */
     92 #include <mail_params.h>
     93 
     94  /*
     95   * Application-specific.
     96   */
     97 #include <smtp_reqtls_policy.h>
     98 
     99 struct SMTP_REQTLS_POLICY {
    100     char   *origin;			/* parameter name or lookup table */
    101     ARGV   *items;			/* parsed policy */
    102 };
    103 
    104 /* smtp_reqtls_policy_parse - parse policy */
    105 
    106 SMTP_REQTLS_POLICY *smtp_reqtls_policy_parse(const char *origin,
    107 					          const char *extern_policy)
    108 {
    109     char   *saved_policy = mystrdup(extern_policy);
    110     SMTP_REQTLS_POLICY *intern_policy;
    111     char   *bp = saved_policy;
    112     char   *item;
    113 
    114     intern_policy = (SMTP_REQTLS_POLICY *) mymalloc(sizeof(*intern_policy));
    115     intern_policy->origin = mystrdup(origin);
    116     intern_policy->items = argv_alloc(1);
    117 
    118     while ((item = mystrtokq(&bp, CHARS_COMMA_SP, CHARS_BRACE)) != 0) {
    119 	if (strchr(item, ':') != 0) {
    120 	    item = dict_open(item, O_RDONLY, DICT_FLAG_LOCK
    121 			     | DICT_FLAG_FOLD_FIX
    122 			     | DICT_FLAG_UTF8_REQUEST)->reg_name;
    123 	}
    124 	argv_add(intern_policy->items, item, (char *) 0);
    125     }
    126     argv_terminate(intern_policy->items);
    127 
    128     /*
    129      * Cleanup.
    130      */
    131     myfree(saved_policy);
    132     return (intern_policy);
    133 }
    134 
    135 /* smtp_reqtls_policy_eval - evaluate policy */
    136 
    137 int     smtp_reqtls_policy_eval(SMTP_REQTLS_POLICY *intern_policy,
    138 				        const char *nexthop_name)
    139 {
    140     char  **cpp;
    141     DICT   *dict;
    142     char   *item;
    143     const char *dict_val;
    144     int     ret;
    145     const char *name;
    146     const char *next;
    147     const char *origin;
    148     const char *aname;
    149 
    150 #define STREQ(x,y) (strcasecmp((x), (y)) == 0)
    151 
    152     origin = intern_policy->origin;
    153     for (cpp = intern_policy->items->argv; (item = *cpp) != 0; cpp++) {
    154 	if (msg_verbose)
    155 	    msg_info("origin=%s name=%s item=%s", origin, nexthop_name, item);
    156 	if (STREQ(item, SMTP_REQTLS_POLICY_NAME_OPP_TLS)) {
    157 	    return (SMTP_REQTLS_POLICY_ACT_OPP_TLS);
    158 	} else if (STREQ(item, SMTP_REQTLS_POLICY_NAME_ENFORCE)) {
    159 	    return (SMTP_REQTLS_POLICY_ACT_ENFORCE);
    160 	} else if (STREQ(item, SMTP_REQTLS_POLICY_NAME_OPPORTUNISTIC)) {
    161 	    return (SMTP_REQTLS_POLICY_ACT_OPPORTUNISTIC);
    162 	} else if (STREQ(item, SMTP_REQTLS_POLICY_NAME_DISABLE)) {
    163 	    return (SMTP_REQTLS_POLICY_ACT_DISABLE);
    164 	} else if (strchr(item, ':') != 0) {
    165 
    166 	    /*
    167 	     * To avoid ambiguity (insecurity!) with unnormalized U-label
    168 	     * forms and unnormalized label separators, the policy contains
    169 	     * A-label forms, and the evaluator converts queries from U-label
    170 	     * form to A-label form. Determine the conversion result once, so
    171 	     * that it can be reused when a policy contains more than one
    172 	     * lookup table. The alternative requires additional logic that
    173 	     * normalizes domain names before updating or matching a policy.
    174 	     * For consistency across Postfix, such logic would also be
    175 	     * needed for all other configuration and policy mechanisms.
    176 	     */
    177 #ifndef NO_EAI
    178 	    if (!valid_hostaddr(nexthop_name, DONT_GRIPE)
    179 		&& !allascii(nexthop_name)) {
    180 		if ((aname = midna_domain_to_ascii(nexthop_name)) == 0) {
    181 		    msg_warn("%s: malformed next-hop destination: '%s' -- "
    182 			     "using default policy '%s'",
    183 			     VAR_SMTP_REQTLS_POLICY, nexthop_name,
    184 			     SMTP_REQTLS_POLICY_NAME_DEFAULT);
    185 		    return (SMTP_REQTLS_POLICY_ACT_DEFAULT);
    186 		}
    187 	    } else
    188 #endif
    189 		aname = nexthop_name;
    190 	    if ((dict = dict_handle(item)) == 0)
    191 		msg_panic("%s: unexpected dictionary: %s", __func__, item);
    192 	    for (name = aname; name != 0 && name[0] != 0; name = next) {
    193 		if ((dict_val = dict_get(dict, name)) != 0) {
    194 		    /* Disallow nested table. */
    195 		    if (dict_val[strcspn(dict_val, ":")]) {
    196 			msg_warn("table %s: nested lookup result \"%s\" "
    197 			   "is not allowed -- ignoring remainder of policy",
    198 				 item, dict_val);
    199 			return (SMTP_REQTLS_POLICY_ACT_ERROR);
    200 		    }
    201 		    /* Disallow composite lookup result. */
    202 		    else if (dict_val[strcspn(dict_val, CHARS_COMMA_SP)]) {
    203 			msg_warn("table %s: composite lookup result \"%s\" "
    204 			   "is not allowed -- ignoring remainder of policy",
    205 				 item, dict_val);
    206 			return (SMTP_REQTLS_POLICY_ACT_ERROR);
    207 		    }
    208 		    /* Simple atom. Avoid six malloc() and free() calls. */
    209 		    else {
    210 			SMTP_REQTLS_POLICY fake_policy;
    211 
    212 			fake_policy.origin = item;
    213 			ARGV_FAKE_BEGIN(fake_argv, dict_val);
    214 			fake_policy.items = &fake_argv;
    215 			ret = smtp_reqtls_policy_eval(&fake_policy, name);
    216 			ARGV_FAKE_END;
    217 			return (ret);
    218 		    }
    219 		} else if (dict->error != 0) {
    220 		    msg_warn("%s: %s:%s: table lookup error -- ignoring "
    221 			     "the remainder of this policy", origin,
    222 			     dict->type, dict->name);
    223 		    return (SMTP_REQTLS_POLICY_ACT_ERROR);
    224 		}
    225 		/* Look up .parent if the table wants an exact match. */
    226 		if ((dict->flags & DICT_FLAG_FIXED) == 0
    227 		    || valid_hostaddr(name, DONT_GRIPE))
    228 		    break;
    229 		next = strchr(name + 1, '.');
    230 	    }
    231 	} else if (STREQ(item, SMTP_REQTLS_POLICY_NAME_ERROR)) {
    232 	    return (SMTP_REQTLS_POLICY_ACT_ERROR);
    233 	} else {
    234 	    msg_warn("%s: unknown policy action: '%s' -- ignoring the "
    235 		     "remainder of this policy", origin, item);
    236 	    return (SMTP_REQTLS_POLICY_ACT_ERROR);
    237 	}
    238     }
    239     if (msg_verbose)
    240 	msg_info("origin=%s name=%s - no match", origin, nexthop_name);
    241     return (SMTP_REQTLS_POLICY_ACT_ENFORCE);
    242 }
    243 
    244 /* smtp_reqtls_policy_free - release storage for policy and lookup tables */
    245 
    246 void    smtp_reqtls_policy_free(SMTP_REQTLS_POLICY *intern_policy)
    247 {
    248     char  **cpp;
    249     DICT   *dict;
    250     const char *item;
    251 
    252     myfree(intern_policy->origin);
    253     for (cpp = intern_policy->items->argv; (item = *cpp) != 0; cpp++) {
    254 	if (strchr(item, ':') != 0) {
    255 	    if ((dict = dict_handle(item)) == 0)
    256 		msg_panic("%s: unexpected dictionary: %s", __func__, item);
    257 	    dict_unregister(item);
    258 	}
    259     }
    260     argv_free(intern_policy->items);
    261     myfree((void *) intern_policy);
    262 }
    263