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