1 /* $NetBSD: smtp_tlsrpt.c,v 1.3 2026/05/09 18:49:20 christos Exp $ */ 2 3 /*++ 4 /* NAME 5 /* smtp_tlsrpt 3 6 /* SUMMARY 7 /* TLSRPT support for the SMTP protocol engine 8 /* SYNOPSIS 9 /* #include <smtp_tlsrpt.h> 10 /* 11 /* int smtp_tlsrpt_post_jail( 12 /* const char *sockname_pname, 13 /* const char *sockname_pval) 14 /* 15 /* void smtp_tlsrpt_create_wrapper( 16 /* SMTP_STATE *state, 17 /* const char *domain) 18 /* 19 /* void smtp_tlsrpt_set_tls_policy( 20 /* SMTP_STATE *state) 21 /* 22 /* void smtp_tlsrpt_set_tcp_connection( 23 /* SMTP_STATE *state) 24 /* 25 /* void smtp_tlsrpt_set_ehlo_resp( 26 /* SMTP_STATE *state, 27 /* const char *ehlo_resp) 28 /* DESCRIPTION 29 /* This module populates a TLSRPT_WRAPPER object with a) 30 /* remote TLSRPT policy information, b) remote TLSA or STS policy 31 /* information, and c) selected SMTP connection information. This 32 /* object is passed to a TLS protocol engine, which may run in a 33 /* different process than the SMTP protocol engine. The TLS protocol 34 /* engine uses the TLSRPT_WRAPPER object to report a TLS handshake 35 /* error to a TLSRPT library. The SMTP protocol engine uses the 36 /* object to report a TLS handshake error or success. 37 /* 38 /* smtp_tls_post_jail() does configuration sanity checks and returns 39 /* 0 if successful, i.e. TLSRPT support is properly 40 /* configured. Otherwise it returns -1 and logs a warning. Arguments: 41 /* .IP sockname_pname 42 /* The name of a configuration parameter for the endpoint that 43 /* is managed by TLSRPT infrastructure. This name is used in a 44 /* diagnostic message. 45 /* .IP sockname_pval 46 /* The value of said parameter. 47 /* .PP 48 /* smtp_tlsrpt_create_wrapper() destroys a TLSRPT_WRAPPER referenced 49 /* by state->tlsrpt, and looks for a TLSRPT policy for the specified 50 /* domain. If one policy exists, smtp_tlsrpt_create_wrapper() 51 /* attaches a TLSRPT_WRAPPER instance to state->tlsrpt. Otherwise, 52 /* state->tlsrpt will be null, and other smtp_tlsrpt_* calls must not 53 /* be made. The TLSRPT_WRAPPER instance may be reused for different 54 /* SMTP connections for the same TLSRPT policy domain. Arguments: 55 /* .IP domain 56 /* The name of a domain that may publish a TLSRPT policy. An 57 /* internationalized domain name may be in U-label or A-label form 58 /* (the U-label form will be converted to A-label internally). 59 /* .PP 60 /* smtp_tlsrpt_set_tls_policy() updates the TLSRPT_WRAPPER 61 /* object with DANE or STS TLS policy information, and clears 62 /* information that was added with smtp_tlsrpt_set_tcp_connection() 63 /* or smtp_tlsrpt_set_ehlo_resp(). 64 /* .PP 65 /* smtp_tlsrpt_set_tcp_connection() updates the TLSRPT_WRAPPER 66 /* object with TCP connection properties. 67 /* .PP 68 /* smtp_tlsrpt_set_ehlo_resp() updates the TLSRPT_WRAPPER object 69 /* with the SMTP server's EHLO response. 70 /* BUGS 71 /* This module inherits all limitations from tlsrpt_wrapper(3). 72 /* SEE ALSO 73 /* tlsrpt_wrapper(3) TLSRPT support for the TLS protocol engine. 74 /* LICENSE 75 /* .ad 76 /* .fi 77 /* The Secure Mailer license must be distributed with this software. 78 /* AUTHOR(S) 79 /* Wietse Venema 80 /* porcupine.org 81 /*--*/ 82 83 /* 84 * System library. 85 */ 86 #include <sys_defs.h> 87 #include <sys/socket.h> 88 89 /* 90 * Utility library. 91 */ 92 #include <hex_code.h> 93 #include <midna_domain.h> 94 #include <msg.h> 95 #include <myaddrinfo.h> 96 #include <name_code.h> 97 #include <stringops.h> 98 99 /* 100 * Global library. 101 */ 102 #include <mail_params.h> 103 104 /* 105 * TLS library. 106 */ 107 #include <tls.h> 108 #include <tlsrpt_wrapper.h> 109 110 /* 111 * Application-specific. 112 */ 113 #include <smtp.h> 114 115 #if defined(USE_TLS) && defined(USE_TLSRPT) 116 117 static const char smtp_tlsrpt_support[] = "TLSRPT support"; 118 119 /* smtp_tlsrpt_post_jail - post-jail configuration sanity check */ 120 121 int smtp_tlsrpt_post_jail(const char *sockname_pname, 122 const char *sockname_pval) 123 { 124 if (smtp_dns_support == SMTP_DNS_DISABLED) { 125 msg_warn("Cannot enable %s: DNS is disabled", smtp_tlsrpt_support); 126 return (-1); 127 } 128 if (*sockname_pval == 0) { 129 msg_warn("%s: parameter %s has empty value -- %s will be disabled", 130 smtp_tlsrpt_support, sockname_pname, smtp_tlsrpt_support); 131 return (-1); 132 } 133 return (0); 134 } 135 136 /* smtp_tlsrpt_find_policy - look up TLSRPT policy and verify version ID */ 137 138 static DNS_RR *smtp_tlsrpt_find_policy(const char *adomain) 139 { 140 VSTRING *why = vstring_alloc(100); 141 VSTRING *qname = vstring_alloc(100); 142 DNS_RR *rr_list = 0; 143 DNS_RR *rr_result = 0; 144 DNS_RR *rr; 145 DNS_RR *next; 146 int res_opt = 0; 147 int dns_status; 148 149 /* 150 * Preliminaries. 151 */ 152 if (smtp_dns_support == SMTP_DNS_DNSSEC) 153 res_opt |= RES_USE_DNSSEC; 154 155 /* 156 * Lexical features: As specified in RFC 8460, a TLSRPT policy record 157 * must start with a version field ("v=TLSRPTv1") followed by *WSP;*WSP 158 * and at least one other field (we must not assume that the second field 159 * will be "rua"). We leave further validation to the code that actually 160 * needs it. 161 */ 162 #define TLSRPTv1_MAGIC "v=TLSRPTv1" 163 #define TLSRPTv1_MAGIC_LEN (sizeof(TLSRPTv1_MAGIC) - 1) 164 #define RFC5234_WSP " \t" 165 166 /* 167 * Look up TXT records. Ignore records that don't start with the expected 168 * version ID, and require that there is exactly one such DNS record. 169 */ 170 vstring_sprintf(qname, "_smtp._tls.%s", adomain); 171 dns_status = dns_lookup(STR(qname), T_TXT, res_opt, &rr_list, 172 (VSTRING *) 0, why); 173 vstring_free(qname); 174 if (dns_status != DNS_OK) { 175 switch (dns_status) { 176 case DNS_NOTFOUND: 177 case DNS_POLICY: 178 /* Expected results. */ 179 break; 180 default: 181 /* Unexpected results. */ 182 msg_warn("%s: policy lookup failed for %s: %s", 183 smtp_tlsrpt_support, adomain, STR(why)); 184 } 185 } else { 186 for (rr = rr_list; rr; rr = next) { 187 char *cp; 188 189 next = rr->next; 190 if (strncmp(rr->data, TLSRPTv1_MAGIC, TLSRPTv1_MAGIC_LEN) != 0) 191 /* Ignore non-TLSRPTv1 info. */ 192 continue; 193 cp = rr->data + TLSRPTv1_MAGIC_LEN; 194 195 /* 196 * Should the TLSRPT library validate the entire policy for us? 197 */ 198 if (cp[strspn(cp, RFC5234_WSP)] != ';') { 199 msg_warn("%s: ignoring malformed policy for %s:, \"%s\"", 200 smtp_tlsrpt_support, adomain, rr->data); 201 continue; 202 } 203 if (rr_result) { 204 msg_warn("%s: Too many TLSRPT policies for %s", 205 smtp_tlsrpt_support, adomain); 206 dns_rr_free(rr_result); 207 rr_result = 0; 208 break; 209 } 210 rr_result = rr; 211 rr_list = dns_rr_detach(rr_list, rr); 212 } 213 } 214 vstring_free(why); 215 if (rr_list) 216 dns_rr_free(rr_list); 217 return (rr_result); 218 } 219 220 /* smtp_tlsrpt_create_wrapper - look up policy and attach TLSRPT_WRAPPER */ 221 222 void smtp_tlsrpt_create_wrapper(SMTP_STATE *state, const char *domain) 223 { 224 const char *adomain; 225 DNS_RR *rr; 226 227 /* 228 * TODO(wietse): document in a suitable place that state->tlsrpt exists 229 * only if the next-hop domain announces a TLSRPT policy. 230 */ 231 if (state->tlsrpt) { 232 trw_free(state->tlsrpt); 233 state->tlsrpt = 0; 234 } 235 236 /* 237 * IDNA support. An internationalized domain name must be in A-label form 238 * 1) for TLSRPT summaries and 2) for DNS lookups. The A-label lookup 239 * result comes from a limited-size in-process cache, so it does not 240 * matter that the SMTP client requests the same mapping later. 241 */ 242 #ifndef NO_EAI 243 if (!allascii(domain) && (adomain = midna_domain_to_ascii(domain)) != 0) { 244 if (msg_verbose) 245 msg_info("%s: internationalized domain %s asciified to %s", 246 smtp_tlsrpt_support, domain, adomain); 247 } else 248 #endif 249 adomain = domain; 250 251 if ((rr = smtp_tlsrpt_find_policy(adomain)) != 0) { 252 if (msg_verbose) 253 msg_info("%s: domain %s has policy %.100s", 254 smtp_tlsrpt_support, domain, rr->data); 255 if (warn_compat_break_smtp_tlsrpt_skip_reused_hs) { 256 msg_info("using using backwards-compatible default setting " 257 VAR_SMTP_TLSRPT_SKIP_REUSED_HS "=yes"); 258 var_smtp_tlsrpt_skip_reused_hs = 1; 259 warn_compat_break_smtp_tlsrpt_skip_reused_hs = 0; 260 } 261 state->tlsrpt = trw_create( 262 /* rpt_socket_name= */ var_smtp_tlsrpt_sockname, 263 /* rpt_policy_domain= */ adomain, 264 /* rpt_policy_string= */ rr->data, 265 /* skip_reused_hs = */ var_smtp_tlsrpt_skip_reused_hs); 266 dns_rr_free(rr); 267 } else { 268 if (msg_verbose) 269 msg_info("%s: no policy for domain %s", 270 smtp_tlsrpt_support, domain); 271 } 272 } 273 274 /* smtp_tlsrpt_set_no_policy - no policy found */ 275 276 static void smtp_tlsrpt_set_no_policy(SMTP_STATE *state) 277 { 278 trw_set_tls_policy(state->tlsrpt, TLSRPT_NO_POLICY_FOUND, 279 /* tls_policy_strings= */ (const char *const *) 0, 280 /* tls_policy_domain= */ (char *) 0, 281 /* mx_host_patterns= */ (const char *const *) 0); 282 } 283 284 /* smtp_tlsrpt_set_dane_policy - add DANE policy properties */ 285 286 static void smtp_tlsrpt_set_dane_policy(SMTP_STATE *state) 287 { 288 VSTRING *buf = vstring_alloc(200); 289 ARGV *argv = argv_alloc(10); 290 TLS_DANE *dane = state->tls->dane; 291 TLS_TLSA *tlsa; 292 293 for (tlsa = dane->tlsa; tlsa != 0; tlsa = tlsa->next) { 294 vstring_sprintf(buf, "%d %d %d ", tlsa->usage, 295 tlsa->selector, tlsa->mtype); 296 hex_encode_opt(buf, (char *) tlsa->data, tlsa->length, 297 HEX_ENCODE_FLAG_APPEND); 298 argv_add(argv, STR(buf), (char *) 0); 299 } 300 trw_set_tls_policy(state->tlsrpt, TLSRPT_POLICY_TLSA, 301 (const char *const *) argv->argv, dane->base_domain, 302 /* mx_host_patterns= */ (const char *const *) 0); 303 argv_free(argv); 304 vstring_free(buf); 305 } 306 307 /* smtp_tlsrpt_set_ext_policy - add external policy from smtp_tls_policy_maps */ 308 309 static void smtp_tlsrpt_set_ext_policy(SMTP_STATE *state) 310 { 311 SMTP_TLS_POLICY *tls = state->tls; 312 tlsrpt_policy_type_t policy_type_val; 313 314 if (tls->ext_policy_type == 0) 315 msg_panic("smtp_tlsrpt_set_ext_policy: no policy type"); 316 317 #define ARGV_OR_NULL(ap) ((ap) ? (ap)->argv : 0) 318 319 switch (policy_type_val = 320 convert_tlsrpt_policy_type(tls->ext_policy_type)) { 321 case TLSRPT_POLICY_STS: 322 trw_set_tls_policy(state->tlsrpt, policy_type_val, 323 (const char *const *) ARGV_OR_NULL(tls->ext_policy_strings), 324 tls->ext_policy_domain, 325 (const char *const *) ARGV_OR_NULL(tls->ext_mx_host_patterns)); 326 break; 327 case TLSRPT_NO_POLICY_FOUND: 328 smtp_tlsrpt_set_no_policy(state); 329 break; 330 default: 331 /* Policy type must be validated in smtp_tls_policy_maps parser. */ 332 msg_panic("unexpected policy type: \"%s\"", 333 tls->ext_policy_type); 334 } 335 336 /* 337 * TODO(wietse) propagate tls->policy_failure to force policy enforcement 338 * to fail with the indicated error, and prevent a false positive match 339 * when a certificate would satisfy conventional PKI constraints. 340 */ 341 } 342 343 /* smtp_tlsrpt_set_tls_policy - set built-in or external policy */ 344 345 void smtp_tlsrpt_set_tls_policy(SMTP_STATE *state) 346 { 347 SMTP_TLS_POLICY *tls = state->tls; 348 349 if (TLS_DANE_BASED(tls->level)) { /* Desired by local policy */ 350 if (tls->dane != 0) /* Actual policy */ 351 smtp_tlsrpt_set_dane_policy(state); 352 else /* No policy */ 353 smtp_tlsrpt_set_no_policy(state); 354 } else if (tls->ext_policy_type) { 355 smtp_tlsrpt_set_ext_policy(state); 356 } else { 357 smtp_tlsrpt_set_no_policy(state); 358 } 359 } 360 361 /* smtp_tlsrpt_set_tcp_connection - set TCP connection info from SMTP_STATE */ 362 363 void smtp_tlsrpt_set_tcp_connection(SMTP_STATE *state) 364 { 365 SMTP_ITERATOR *iter = state->iterator; 366 SMTP_SESSION *session = state->session; 367 MAI_HOSTADDR_STR client_addr; 368 struct sockaddr_storage addr_storage; 369 SOCKADDR_SIZE addr_storage_len = sizeof(addr_storage); 370 int aierr; 371 372 /* 373 * Get the IP client address string. The Postfix SMTP_ITERATOR already 374 * contains strings with server-side connection information. 375 */ 376 if (getsockname(vstream_fileno(session->stream), 377 (struct sockaddr *) &addr_storage, 378 &addr_storage_len) < 0) { 379 msg_warn("%s: getsockname() failed (%m)" 380 " skipping the ignoring client-side IP address", 381 smtp_tlsrpt_support); 382 client_addr.buf[0] = 0; 383 } else if ((aierr = sane_sockaddr_to_hostaddr( 384 (struct sockaddr *) &addr_storage, 385 &addr_storage_len, &client_addr, 386 (MAI_SERVPORT_STR *) 0, 387 SOCK_STREAM)) != 0) { 388 msg_warn("%s: cannot convert IP address to string (%s)" 389 " -- skipping the client-side IP address", 390 smtp_tlsrpt_support, MAI_STRERROR(aierr)); 391 client_addr.buf[0] = 0; 392 } 393 trw_set_tcp_connection(state->tlsrpt, client_addr.buf, STR(iter->host), 394 STR(iter->addr)); 395 } 396 397 /* smtp_tlsrpt_set_ehlo_resp - format and set EHLO response */ 398 399 void smtp_tlsrpt_set_ehlo_resp(SMTP_STATE *state, const char *reply) 400 { 401 ARGV *argv; 402 VSTRING *buf; 403 char **cpp; 404 405 /* 406 * Generate SMTP-style line breaks ("\r\n") for a multiline response. 407 * Internally, smtp_chat_resp() returns a multiline response as text 408 * separated with "\n". This is because Postfix by design removes 409 * protocol-specific line endings on input, uses its own internal form to 410 * represent text lines, and generates protocol-specific line endings on 411 * output. The conversion to "\r\n" below is such an output conversion. 412 */ 413 buf = vstring_alloc(100); 414 argv = argv_split(reply, "\n"); 415 for (cpp = argv->argv; *cpp; cpp++) { 416 vstring_strcat(buf, *cpp); 417 if (cpp[1]) 418 vstring_strcat(buf, "\r\n"); 419 } 420 argv_free(argv); 421 trw_set_ehlo_resp(state->tlsrpt, STR(buf)); 422 vstring_free(buf); 423 } 424 425 #endif /* USE_TLSRPT && USE_TLS */ 426