1 /* $NetBSD: smtp_tlsrpt.c,v 1.2 2025/02/25 19:15:49 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
smtp_tlsrpt_post_jail(const char * sockname_pname,const char * sockname_pval)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
smtp_tlsrpt_find_policy(const char * adomain)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
smtp_tlsrpt_create_wrapper(SMTP_STATE * state,const char * domain)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 state->tlsrpt = trw_create(
256 /* rpt_socket_name= */ var_smtp_tlsrpt_sockname,
257 /* rpt_policy_domain= */ adomain,
258 /* rpt_policy_string= */ rr->data,
259 /* skip_reused_hs = */ var_smtp_tlsrpt_skip_reused_hs);
260 dns_rr_free(rr);
261 } else {
262 if (msg_verbose)
263 msg_info("%s: no policy for domain %s",
264 smtp_tlsrpt_support, domain);
265 }
266 }
267
268 /* smtp_tlsrpt_set_no_policy - no policy found */
269
smtp_tlsrpt_set_no_policy(SMTP_STATE * state)270 static void smtp_tlsrpt_set_no_policy(SMTP_STATE *state)
271 {
272 trw_set_tls_policy(state->tlsrpt, TLSRPT_NO_POLICY_FOUND,
273 /* tls_policy_strings= */ (const char *const *) 0,
274 /* tls_policy_domain= */ (char *) 0,
275 /* mx_host_patterns= */ (const char *const *) 0);
276 }
277
278 /* smtp_tlsrpt_set_dane_policy - add DANE policy properties */
279
smtp_tlsrpt_set_dane_policy(SMTP_STATE * state)280 static void smtp_tlsrpt_set_dane_policy(SMTP_STATE *state)
281 {
282 VSTRING *buf = vstring_alloc(200);
283 ARGV *argv = argv_alloc(10);
284 TLS_DANE *dane = state->tls->dane;
285 TLS_TLSA *tlsa;
286
287 for (tlsa = dane->tlsa; tlsa != 0; tlsa = tlsa->next) {
288 vstring_sprintf(buf, "%d %d %d ", tlsa->usage,
289 tlsa->selector, tlsa->mtype);
290 hex_encode_opt(buf, (char *) tlsa->data, tlsa->length,
291 HEX_ENCODE_FLAG_APPEND);
292 argv_add(argv, STR(buf), (char *) 0);
293 }
294 trw_set_tls_policy(state->tlsrpt, TLSRPT_POLICY_TLSA,
295 (const char *const *) argv->argv, dane->base_domain,
296 /* mx_host_patterns= */ (const char *const *) 0);
297 argv_free(argv);
298 vstring_free(buf);
299 }
300
301 /* smtp_tlsrpt_set_ext_policy - add external policy from smtp_tls_policy_maps */
302
smtp_tlsrpt_set_ext_policy(SMTP_STATE * state)303 static void smtp_tlsrpt_set_ext_policy(SMTP_STATE *state)
304 {
305 SMTP_TLS_POLICY *tls = state->tls;
306 tlsrpt_policy_type_t policy_type_val;
307
308 if (tls->ext_policy_type == 0)
309 msg_panic("smtp_tlsrpt_set_ext_policy: no policy type");
310
311 switch (policy_type_val =
312 convert_tlsrpt_policy_type(tls->ext_policy_type)) {
313 case TLSRPT_POLICY_STS:
314 trw_set_tls_policy(state->tlsrpt, policy_type_val,
315 (const char *const *) tls->ext_policy_strings->argv,
316 tls->ext_policy_domain,
317 (const char *const *) tls->ext_mx_host_patterns->argv);
318 break;
319 case TLSRPT_NO_POLICY_FOUND:
320 smtp_tlsrpt_set_no_policy(state);
321 break;
322 default:
323 /* Policy type must be validated in smtp_tls_policy_maps parser. */
324 msg_panic("unexpected policy type: \"%s\"",
325 tls->ext_policy_type);
326 }
327
328 /*
329 * TODO(wietse) propagate tls->policy_failure to force policy enforcement
330 * to fail with the indicated error, and prevent a false positive match
331 * when a certificate would satisfy conventional PKI constraints.
332 */
333 }
334
335 /* smtp_tlsrpt_set_tls_policy - set built-in or external policy */
336
smtp_tlsrpt_set_tls_policy(SMTP_STATE * state)337 void smtp_tlsrpt_set_tls_policy(SMTP_STATE *state)
338 {
339 SMTP_TLS_POLICY *tls = state->tls;
340
341 if (TLS_DANE_BASED(tls->level)) { /* Desired by local policy */
342 if (tls->dane != 0) /* Actual policy */
343 smtp_tlsrpt_set_dane_policy(state);
344 else /* No policy */
345 smtp_tlsrpt_set_no_policy(state);
346 } else if (tls->ext_policy_type) {
347 smtp_tlsrpt_set_ext_policy(state);
348 } else {
349 smtp_tlsrpt_set_no_policy(state);
350 }
351 }
352
353 /* smtp_tlsrpt_set_tcp_connection - set TCP connection info from SMTP_STATE */
354
smtp_tlsrpt_set_tcp_connection(SMTP_STATE * state)355 void smtp_tlsrpt_set_tcp_connection(SMTP_STATE *state)
356 {
357 SMTP_ITERATOR *iter = state->iterator;
358 SMTP_SESSION *session = state->session;
359 MAI_HOSTADDR_STR client_addr;
360 struct sockaddr_storage addr_storage;
361 SOCKADDR_SIZE addr_storage_len = sizeof(addr_storage);
362 int aierr;
363
364 /*
365 * Get the IP client address string. The Postfix SMTP_ITERATOR already
366 * contains strings with server-side connection information.
367 */
368 if (getsockname(vstream_fileno(session->stream),
369 (struct sockaddr *) &addr_storage,
370 &addr_storage_len) < 0) {
371 msg_warn("%s: getsockname() failed (%m)"
372 " skipping the ignoring client-side IP address",
373 smtp_tlsrpt_support);
374 client_addr.buf[0] = 0;
375 } else if ((aierr = sane_sockaddr_to_hostaddr(
376 (struct sockaddr *) &addr_storage,
377 addr_storage_len, &client_addr,
378 (MAI_SERVPORT_STR *) 0,
379 SOCK_STREAM)) != 0) {
380 msg_warn("%s: cannot convert IP address to string (%s)"
381 " -- skipping the client-side IP address",
382 smtp_tlsrpt_support, MAI_STRERROR(aierr));
383 client_addr.buf[0] = 0;
384 }
385 trw_set_tcp_connection(state->tlsrpt, client_addr.buf, STR(iter->host),
386 STR(iter->addr));
387 }
388
389 /* smtp_tlsrpt_set_ehlo_resp - format and set EHLO response */
390
smtp_tlsrpt_set_ehlo_resp(SMTP_STATE * state,const char * reply)391 void smtp_tlsrpt_set_ehlo_resp(SMTP_STATE *state, const char *reply)
392 {
393 ARGV *argv;
394 VSTRING *buf;
395 char **cpp;
396
397 /*
398 * Generate SMTP-style line breaks ("\r\n") for a multiline response.
399 * Internally, smtp_chat_resp() returns a multiline response as text
400 * separated with "\n". This is because Postfix by design removes
401 * protocol-specific line endings on input, uses its own internal form to
402 * represent text lines, and generates protocol-specific line endings on
403 * output. The conversion to "\r\n" below is such an output conversion.
404 */
405 buf = vstring_alloc(100);
406 argv = argv_split(reply, "\n");
407 for (cpp = argv->argv; *cpp; cpp++) {
408 vstring_strcat(buf, *cpp);
409 if (cpp[1])
410 vstring_strcat(buf, "\r\n");
411 }
412 argv_free(argv);
413 trw_set_ehlo_resp(state->tlsrpt, STR(buf));
414 vstring_free(buf);
415 }
416
417 #endif /* USE_TLSRPT && USE_TLS */
418