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