*************************************************/
/* Copyright (c) University of Cambridge 1995 - 2019 */
-/* Copyright (c) The Exim Maintainers 2020 */
+/* Copyright (c) The Exim Maintainers 2020 - 2021 */
/* See the file NOTICE for conditions of use and distribution. */
/* Portions Copyright (c) The OpenSSL Project 1999 */
# ifndef DISABLE_OCSP
# define EXIM_HAVE_OCSP
# endif
+# define EXIM_HAVE_ALPN /* fail ret from hshake-cb is ignored by LibreSSL */
# else
# define EXIM_NEED_OPENSSL_INIT
# endif
# endif
#endif
+#if LIBRESSL_VERSION_NUMBER >= 0x3040000fL
+# define EXIM_HAVE_OPENSSL_CIPHER_GET_ID
+#endif
+
#if !defined(LIBRESSL_VERSION_NUMBER) \
|| LIBRESSL_VERSION_NUMBER >= 0x20010000L
# if !defined(OPENSSL_NO_ECDH)
{ US"no_tlsv1", SSL_OP_NO_TLSv1 },
#endif
#ifdef SSL_OP_NO_TLSv1_1
-#if SSL_OP_NO_TLSv1_1 == 0x00000400L
+# if SSL_OP_NO_TLSv1_1 == 0x00000400L
/* Error in chosen value in 1.0.1a; see first item in CHANGES for 1.0.1b */
-#warning OpenSSL 1.0.1a uses a bad value for SSL_OP_NO_TLSv1_1, ignoring
-#else
+# warning OpenSSL 1.0.1a uses a bad value for SSL_OP_NO_TLSv1_1, ignoring
+# else
{ US"no_tlsv1_1", SSL_OP_NO_TLSv1_1 },
-#endif
+# endif
#endif
#ifdef SSL_OP_NO_TLSv1_2
{ US"no_tlsv1_2", SSL_OP_NO_TLSv1_2 },
builtin_macro_create(US"_HAVE_TLS_OCSP");
builtin_macro_create(US"_HAVE_TLS_OCSP_LIST");
# endif
+# ifdef EXIM_HAVE_ALPN
+builtin_macro_create(US"_HAVE_TLS_ALPN");
+# endif
}
#else
#ifdef EXIM_HAVE_OPENSSL_TLSEXT
static SSL_CTX *server_sni = NULL;
#endif
+#ifdef EXIM_HAVE_ALPN
+static BOOL server_seen_alpn = FALSE;
+#endif
static char ssl_errstring[256];
uschar ** errstr );
/* Callbacks */
-#ifdef EXIM_HAVE_OPENSSL_TLSEXT
-static int tls_servername_cb(SSL *s, int *ad ARG_UNUSED, void *arg);
-#endif
#ifndef DISABLE_OCSP
static int tls_server_stapling_cb(SSL *s, void *arg);
#endif
+#ifdef EXIM_HAVE_ALPN
+/*************************************************
+* Callback to handle ALPN *
+*************************************************/
+
+/* Called on server if tls_alpn nonblank after expansion,
+when client offers ALPN, after the SNI callback.
+If set and not matching the list then we dump the connection */
+
+static int
+tls_server_alpn_cb(SSL *ssl, const uschar ** out, uschar * outlen,
+ const uschar * in, unsigned int inlen, void * arg)
+{
+server_seen_alpn = TRUE;
+DEBUG(D_tls)
+ {
+ debug_printf("Received TLS ALPN offer:");
+ for (int pos = 0, siz; pos < inlen; pos += siz+1)
+ {
+ siz = in[pos];
+ if (pos + 1 + siz > inlen) siz = inlen - pos - 1;
+ debug_printf(" '%.*s'", siz, in + pos + 1);
+ }
+ debug_printf(". Our list: '%s'\n", tls_alpn);
+ }
+
+/* Look for an acceptable ALPN */
+
+if ( inlen > 1 /* at least one name */
+ && in[0]+1 == inlen /* filling the vector, so exactly one name */
+ )
+ {
+ const uschar * list = tls_alpn;
+ int sep = 0;
+ for (uschar * name; name = string_nextinlist(&list, &sep, NULL, 0); )
+ if (Ustrncmp(in+1, name, in[0]) == 0)
+ {
+ *out = in+1; /* we checked for exactly one, so can just point to it */
+ *outlen = inlen;
+ return SSL_TLSEXT_ERR_OK; /* use ALPN */
+ }
+ }
+
+/* More than one name from clilent, or name did not match our list. */
+
+/* This will be fatal to the TLS conn; would be nice to kill TCP also.
+Maybe as an option in future; for now leave control to the config (must-tls). */
+
+DEBUG(D_tls) debug_printf("TLS ALPN rejected\n");
+return SSL_TLSEXT_ERR_ALERT_FATAL;
+}
+#endif /* EXIM_HAVE_ALPN */
+
+
+
#ifndef DISABLE_OCSP
/*************************************************
tls_certificate */
SSL_CTX_set_tlsext_servername_callback(ctx, tls_servername_cb);
SSL_CTX_set_tlsext_servername_arg(ctx, state);
+
+# ifdef EXIM_HAVE_ALPN
+ if (tls_alpn && *tls_alpn)
+ {
+ uschar * exp_alpn;
+ if ( expand_check(tls_alpn, US"tls_alpn", &exp_alpn, errstr)
+ && *exp_alpn && !isblank(*exp_alpn))
+ {
+ tls_alpn = exp_alpn; /* subprocess so ok to overwrite */
+ SSL_CTX_set_alpn_select_cb(ctx, tls_server_alpn_cb, state);
+ }
+ else
+ tls_alpn = NULL;
+ }
+# endif
}
# ifndef DISABLE_OCSP
else /* client */
/* Load certs from file, return TRUE on success */
static BOOL
-chain_from_pem_file(const uschar * file, STACK_OF(X509) * verify_stack)
+chain_from_pem_file(const uschar * file, STACK_OF(X509) ** vp)
{
BIO * bp;
-X509 * x;
+STACK_OF(X509) * verify_stack = *vp;
-while (sk_X509_num(verify_stack) > 0)
- X509_free(sk_X509_pop(verify_stack));
+if (verify_stack)
+ while (sk_X509_num(verify_stack) > 0)
+ X509_free(sk_X509_pop(verify_stack));
+else
+ verify_stack = sk_X509_new_null();
if (!(bp = BIO_new_file(CS file, "r"))) return FALSE;
-while ((x = PEM_read_bio_X509(bp, NULL, 0, NULL)))
+for (X509 * x; x = PEM_read_bio_X509(bp, NULL, 0, NULL); )
sk_X509_push(verify_stack, x);
BIO_free(bp);
+*vp = verify_stack;
return TRUE;
}
#endif
{ file = NULL; dir = expcerts; }
else
{
+ STACK_OF(X509) * verify_stack =
+#ifndef DISABLE_OCSP
+ !host ? state_server.verify_stack :
+#endif
+ NULL;
+ STACK_OF(X509) ** vp = &verify_stack;
+
file = expcerts; dir = NULL;
#ifndef DISABLE_OCSP
/* In the server if we will be offering an OCSP proof, load chain from
/*XXX Glitch! The file here is tls_verify_certs: the chain for verifying the client cert.
This is inconsistent with the need to verify the OCSP proof of the server cert.
*/
-
if ( !host
&& statbuf.st_size > 0
&& state_server.u_ocsp.server.file
- && !chain_from_pem_file(file, state_server.verify_stack)
+ && !chain_from_pem_file(file, vp)
)
{
log_write(0, LOG_MAIN|LOG_PANIC,
}
#endif
+#ifdef EXIM_HAVE_ALPN
+/* If require-alpn, check server_seen_alpn here. Else abort TLS */
+if (!tls_alpn || !*tls_alpn)
+ { DEBUG(D_tls) debug_printf("TLS: was not watching for ALPN\n"); }
+else if (!server_seen_alpn)
+ if (verify_check_host(&hosts_require_alpn) == OK)
+ {
+ /* We'd like to send a definitive Alert but OpenSSL provides no facility */
+ SSL_shutdown(ssl);
+ tls_error(US"handshake", NULL, US"ALPN required but not negotiated", errstr);
+ return FAIL;
+ }
+ else
+ { DEBUG(D_tls) debug_printf("TLS: no ALPN presented in handshake\n"); }
+else DEBUG(D_tls)
+ {
+ const uschar * name;
+ unsigned len;
+ SSL_get0_alpn_selected(ssl, &name, &len);
+ if (len && name)
+ debug_printf("ALPN negotiated: '%.*s'\n", (int)*name, name+1);
+ else
+ debug_printf("ALPN: no protocol negotiated\n");
+ }
+#endif
+
+
/* TLS has been set up. Record data for the connection,
adjust the input functions to read via TLS, and initialize things. */
receive_getc = tls_getc;
receive_getbuf = tls_getbuf;
receive_get_cache = tls_get_cache;
+receive_hasc = tls_hasc;
receive_ungetc = tls_ungetc;
receive_feof = tls_feof;
receive_ferror = tls_ferror;
debug_printf("decoding session: %s\n", ssl_errstring);
}
}
-#ifdef EXIM_HAVE_SESSION_TICKET
- else if ( SSL_SESSION_get_ticket_lifetime_hint(ss) + dt->time_stamp
- < time(NULL))
+ else
{
- DEBUG(D_tls) debug_printf("session expired\n");
- dbfn_delete(dbm_file, key);
- }
+ unsigned long lifetime =
+#ifdef EXIM_HAVE_SESSION_TICKET
+ SSL_SESSION_get_ticket_lifetime_hint(ss);
+#else /* Use, fairly arbitrilarily, what we as server would */
+ f.running_in_test_harness ? 6 : ssl_session_timeout;
#endif
- else if (!SSL_set_session(ssl, ss))
- {
- DEBUG(D_tls)
+ if (lifetime + dt->time_stamp < time(NULL))
{
- ERR_error_string_n(ERR_get_error(),
- ssl_errstring, sizeof(ssl_errstring));
- debug_printf("applying session to ssl: %s\n", ssl_errstring);
+ DEBUG(D_tls) debug_printf("session expired\n");
+ dbfn_delete(dbm_file, key);
+ }
+ else if (!SSL_set_session(ssl, ss))
+ {
+ DEBUG(D_tls)
+ {
+ ERR_error_string_n(ERR_get_error(),
+ ssl_errstring, sizeof(ssl_errstring));
+ debug_printf("applying session to ssl: %s\n", ssl_errstring);
+ }
+ }
+ else
+ {
+ DEBUG(D_tls) debug_printf("good session\n");
+ tlsp->resumption |= RESUME_CLIENT_SUGGESTED;
+ tlsp->verify_override = dt->verify_override;
+ tlsp->ocsp = dt->ocsp;
}
- }
- else
- {
- DEBUG(D_tls) debug_printf("good session\n");
- tlsp->resumption |= RESUME_CLIENT_SUGGESTED;
- tlsp->verify_override = dt->verify_override;
- tlsp->ocsp = dt->ocsp;
}
}
else
#endif /* !DISABLE_TLS_RESUME */
+#ifdef EXIM_HAVE_ALPN
+/* Expand and convert an Exim list to an ALPN list. False return for fail.
+NULL plist return for silent no-ALPN.
+*/
+
+static BOOL
+tls_alpn_plist(const uschar * tls_alpn, const uschar ** plist, unsigned * plen,
+ uschar ** errstr)
+{
+uschar * exp_alpn;
+
+if (!expand_check(tls_alpn, US"tls_alpn", &exp_alpn, errstr))
+ return FALSE;
+
+if (!exp_alpn)
+ {
+ DEBUG(D_tls) debug_printf("Setting TLS ALPN forced to fail, not sending\n");
+ *plist = NULL;
+ }
+else
+ {
+ /* The server implementation only accepts exactly one protocol name
+ but it's little extra code complexity in the client. */
+
+ const uschar * list = exp_alpn;
+ uschar * p = store_get(Ustrlen(exp_alpn), is_tainted(exp_alpn)), * s, * t;
+ int sep = 0;
+ uschar len;
+
+ for (t = p; s = string_nextinlist(&list, &sep, NULL, 0); t += len)
+ {
+ *t++ = len = (uschar) Ustrlen(s);
+ memcpy(t, s, len);
+ }
+ *plist = (*plen = t - p) ? p : NULL;
+ }
+return TRUE;
+}
+#endif /* EXIM_HAVE_ALPN */
+
+
/*************************************************
* Start a TLS session in a client *
*************************************************/
}
}
+if (ob->tls_alpn)
+#ifdef EXIM_HAVE_ALPN
+ {
+ const uschar * plist;
+ unsigned plen;
+
+ if (!tls_alpn_plist(ob->tls_alpn, &plist, &plen, errstr))
+ return FALSE;
+ if (plist)
+ if (SSL_set_alpn_protos(exim_client_ctx->ssl, plist, plen) != 0)
+ {
+ tls_error(US"alpn init", host, NULL, errstr);
+ return FALSE;
+ }
+ else
+ DEBUG(D_tls) debug_printf("Setting TLS ALPN '%s'\n", ob->tls_alpn);
+ }
+#else
+ log_write(0, LOG_MAIN, "ALPN unusable with this OpenSSL library version; ignoring \"%s\"\n",
+ ob->tls_alpn);
+#endif
+
#ifdef SUPPORT_DANE
if (conn_args->dane)
if (dane_tlsa_load(exim_client_ctx->ssl, host, &conn_args->tlsa_dnsa, errstr) != OK)
tls_client_resume_posthandshake(exim_client_ctx, tlsp);
#endif
+#ifdef EXIM_HAVE_ALPN
+if (ob->tls_alpn) /* We requested. See what was negotiated. */
+ {
+ const uschar * name;
+ unsigned len;
+
+ SSL_get0_alpn_selected(exim_client_ctx->ssl, &name, &len);
+ if (len > 0)
+ { DEBUG(D_tls) debug_printf("ALPN negotiated %u: '%.*s'\n", len, (int)*name, name+1); }
+ else if (verify_check_given_host(CUSS &ob->hosts_require_alpn, host) == OK)
+ {
+ /* Would like to send a relevant fatal Alert, but OpenSSL has no API */
+ tls_error(US"handshake", host, US"ALPN required but not negotiated", errstr);
+ return FALSE;
+ }
+ }
+#endif
+
#ifdef SSL_get_extms_support
tlsp->ext_master_secret = SSL_get_extms_support(exim_client_ctx->ssl) == 1;
#endif
return ssl_xfer_buffer[ssl_xfer_buffer_lwm++];
}
+BOOL
+tls_hasc(void)
+{
+return ssl_xfer_buffer_lwm < ssl_xfer_buffer_hwm;
+}
+
uschar *
tls_getbuf(unsigned * len)
{
void
-tls_get_cache(void)
+tls_get_cache(unsigned lim)
{
#ifndef DISABLE_DKIM
int n = ssl_xfer_buffer_hwm - ssl_xfer_buffer_lwm;
+debug_printf("tls_get_cache\n");
+if (n > lim)
+ n = lim;
if (n > 0)
dkim_exim_verify_feed(ssl_xfer_buffer+ssl_xfer_buffer_lwm, n);
#endif
receive_getc = smtp_getc;
receive_getbuf = smtp_getbuf;
receive_get_cache = smtp_get_cache;
+ receive_hasc = smtp_hasc;
receive_ungetc = smtp_ungetc;
receive_feof = smtp_feof;
receive_ferror = smtp_ferror;
{
long result, item;
uschar * exp, * end;
-uschar keep_c;
BOOL adding, item_parsed;
/* Server: send no (<= TLS1.2) session tickets */
return FALSE;
}
adding = *s++ == '+';
- for (end = s; (*end != '\0') && !isspace(*end); ++end) /**/ ;
- keep_c = *end;
- *end = '\0';
- item_parsed = tls_openssl_one_option_parse(s, &item);
- *end = keep_c;
+ for (end = s; *end && !isspace(*end); ) end++;
+ item_parsed = tls_openssl_one_option_parse(string_copyn(s, end-s), &item);
if (!item_parsed)
{
DEBUG(D_tls) debug_printf("openssl option setting unrecognised: \"%s\"\n", s);