From dadbd59070b840ed9cf997928adc93752b483c10 Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Tue, 4 Oct 2016 10:22:29 +0100 Subject: [PATCH] Calculate MTU for PSK-NEGOTIATE Signed-off-by: David Woodhouse --- gnutls-dtls.c | 80 ++++++++++++++++++--------- openconnect-internal.h | 3 - openssl-dtls.c | 122 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 168 insertions(+), 37 deletions(-) diff --git a/gnutls-dtls.c b/gnutls-dtls.c index 3017cefb..4b515a7b 100644 --- a/gnutls-dtls.c +++ b/gnutls-dtls.c @@ -316,37 +316,63 @@ int dtls_try_handshake(struct openconnect_info *vpninfo) char *str; if (!err) { - if (strcmp(vpninfo->dtls_cipher, "PSK-NEGOTIATE") && - !gnutls_session_is_resumed(vpninfo->dtls_ssl)) { - /* Someone attempting to hijack the DTLS session? - * A real server would never allow a full session - * establishment instead of the agreed resume. */ - vpn_progress(vpninfo, PRG_ERR, - _("DTLS session resume failed; possible MITM attack. Disabling DTLS.\n")); - dtls_close(vpninfo); - vpninfo->dtls_attempt_period = 0; - vpninfo->dtls_state = DTLS_DISABLED; - return -EIO; - } + if (!strcmp(vpninfo->dtls_cipher, "PSK-NEGOTIATE")) { + /* For PSK-NEGOTIATE, we have to determine the tunnel MTU + * for ourselves based on the base MTU */ + int data_mtu = vpninfo->cstp_basemtu; + if (vpninfo->peer_addr->sa_family == IPPROTO_IPV6) + data_mtu -= 40; /* IPv6 header */ + else + data_mtu -= 20; /* Legacy IP header */ + data_mtu -= 8; /* UDP header */ + if (data_mtu < 0) { + vpn_progress(vpninfo, PRG_ERR, + _("Peer MTU %d too small to allow DTLS\n"), + vpninfo->cstp_basemtu); + goto nodtls; + } + /* Reduce it by one because that's the payload header *inside* + * the encryption */ + data_mtu = dtls_set_mtu(vpninfo, data_mtu) - 1; + if (data_mtu < vpninfo->ip_info.mtu) { + vpn_progress(vpninfo, PRG_INFO, + _("DTLS MTU reduced to %d\n"), + data_mtu); + vpninfo->ip_info.mtu = data_mtu; + } + } else { + if (!gnutls_session_is_resumed(vpninfo->dtls_ssl)) { + /* Someone attempting to hijack the DTLS session? + * A real server would never allow a full session + * establishment instead of the agreed resume. */ + vpn_progress(vpninfo, PRG_ERR, + _("DTLS session resume failed; possible MITM attack. Disabling DTLS.\n")); + nodtls: + dtls_close(vpninfo); + vpninfo->dtls_attempt_period = 0; + vpninfo->dtls_state = DTLS_DISABLED; + return -EIO; + } #ifdef HAVE_GNUTLS_DTLS_SET_DATA_MTU - /* Make sure GnuTLS's idea of the MTU is sufficient to take - a full VPN MTU (with 1-byte header) in a data record. */ - err = gnutls_dtls_set_data_mtu(vpninfo->dtls_ssl, vpninfo->ip_info.mtu + 1); - if (err) { - vpn_progress(vpninfo, PRG_ERR, - _("Failed to set DTLS MTU: %s\n"), - gnutls_strerror(err)); - goto error; - } + /* Make sure GnuTLS's idea of the MTU is sufficient to take + a full VPN MTU (with 1-byte header) in a data record. */ + err = gnutls_dtls_set_data_mtu(vpninfo->dtls_ssl, vpninfo->ip_info.mtu + 1); + if (err) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to set DTLS MTU: %s\n"), + gnutls_strerror(err)); + goto error; + } #else - /* If we don't have gnutls_dtls_set_data_mtu() then make sure - we leave enough headroom by adding the worst-case overhead. - We only support AES128-CBC and DES-CBC3-SHA anyway, so - working out the worst case isn't hard. */ - gnutls_dtls_set_mtu(vpninfo->dtls_ssl, - vpninfo->ip_info.mtu + DTLS_OVERHEAD); + /* If we don't have gnutls_dtls_set_data_mtu() then make sure + we leave enough headroom by adding the worst-case overhead. + We only support AES128-CBC and DES-CBC3-SHA anyway, so + working out the worst case isn't hard. */ + gnutls_dtls_set_mtu(vpninfo->dtls_ssl, + vpninfo->ip_info.mtu + DTLS_OVERHEAD); #endif + } vpninfo->dtls_state = DTLS_CONNECTED; str = get_gnutls_cipher(vpninfo->dtls_ssl); diff --git a/openconnect-internal.h b/openconnect-internal.h index e44a549c..6c299004 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -323,9 +323,6 @@ static inline void init_pkt_queue(struct pkt_q *q) 20 /* biggest supported MAC (SHA1) */ + 16 /* biggest supported IV (AES-128) */ + \ 16 /* max padding */) -#define LINK_TO_TUNNEL_MTU(linkmtu) \ - (linkmtu - DTLS_OVERHEAD) - struct esp { #if defined(OPENCONNECT_GNUTLS) gnutls_cipher_hd_t cipher; diff --git a/openssl-dtls.c b/openssl-dtls.c index 326428a1..2e5aa707 100644 --- a/openssl-dtls.c +++ b/openssl-dtls.c @@ -46,16 +46,98 @@ extern void dtls1_stop_timer(SSL *); #endif +#ifndef DTLS_get_data_mtu +/* This equivalent functionality was submitted for OpenSSL 1.1.1+ in + * https://github.com/openssl/openssl/pull/1666 */ +static int dtls_get_data_mtu(struct openconnect_info *vpninfo, int mtu) +{ + int ivlen, maclen, blocksize = 0, pad = 0; +#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER) + const SSL_CIPHER *s_ciph = SSL_get_current_cipher(vpninfo->dtls_ssl); + int cipher_nid; + const EVP_CIPHER *e_ciph; + const EVP_MD *e_md; + char wtf[128]; + + cipher_nid = SSL_CIPHER_get_cipher_nid(s_ciph); + if (cipher_nid == NID_chacha20_poly1305) { + ivlen = 0; /* Automatically derived from handshake and seqno */ + maclen = 16; /* Poly1305 */ + } else { + e_ciph = EVP_get_cipherbynid(cipher_nid); + switch (EVP_CIPHER_mode(e_ciph)) { + case EVP_CIPH_GCM_MODE: + ivlen = EVP_GCM_TLS_EXPLICIT_IV_LEN; + maclen = EVP_GCM_TLS_TAG_LEN; + break; + + case EVP_CIPH_CCM_MODE: + ivlen = EVP_CCM_TLS_EXPLICIT_IV_LEN; + SSL_CIPHER_description(s_ciph, wtf, sizeof(wtf)); + if (strstr(wtf, "CCM8")) + maclen = 8; + else + maclen = 16; + break; + + case EVP_CIPH_CBC_MODE: + blocksize = EVP_CIPHER_block_size(e_ciph); + ivlen = EVP_CIPHER_iv_length(e_ciph); + pad = 1; + + e_md = EVP_get_digestbynid(SSL_CIPHER_get_digest_nid(s_ciph)); + maclen = EVP_MD_size(e_md); + break; + + default: + vpn_progress(vpninfo, PRG_ERR, + _("Unable to calculate DTLS overhead for %s\n"), + SSL_CIPHER_get_name(s_ciph)); + ivlen = 0; + maclen = DTLS_OVERHEAD; + break; + } + } +#else + /* OpenSSL <= 1.0.2 only supports CBC ciphers with PSK */ + ivlen = EVP_CIPHER_iv_length(EVP_CIPHER_CTX_cipher(vpninfo->dtls_ssl->enc_write_ctx)); + maclen = EVP_MD_CTX_size(vpninfo->dtls_ssl->write_hash); + blocksize = ivlen; + pad = 1; +#endif + + /* Even when it pretended to, OpenSSL never did encrypt-then-mac. + * So the MAC is *inside* the encryption, unconditionally. + * https://github.com/openssl/openssl/pull/1705 */ + if (mtu < DTLS1_RT_HEADER_LENGTH + ivlen) + return 0; + mtu -= DTLS1_RT_HEADER_LENGTH + ivlen; + + /* For CBC mode round down to blocksize */ + if (blocksize) + mtu -= mtu % blocksize; + + /* Finally, CBC modes require at least one byte to indicate + * padding length, as well as the MAC. */ + if (mtu < pad + maclen) + return 0; + mtu -= pad + maclen; + return mtu; +} +#endif /* !DTLS_get_data_mtu */ + /* sets the DTLS MTU and returns the actual tunnel MTU */ unsigned dtls_set_mtu(struct openconnect_info *vpninfo, unsigned mtu) { -#ifdef DTLS_set_link_mtu - DTLS_set_link_mtu(vpninfo->dtls_ssl, mtu); + /* This is the record MTU (not the link MTU, which includes + * IP+UDP headers, and not the payload MTU */ + SSL_set_mtu(vpninfo->dtls_ssl, mtu); + +#ifdef DTLS_get_data_mtu + return DTLS_get_data_mtu(vpninfo->dtls_ssl); #else - /* not sure if this is equivalent */ - SSL_set_mtu(vpninfo->dtls_ssl, LINK_TO_TUNNEL_MTU(mtu)); + return dtls_get_data_mtu(vpninfo, mtu); #endif - return LINK_TO_TUNNEL_MTU(mtu); } #if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER) @@ -388,13 +470,39 @@ int dtls_try_handshake(struct openconnect_info *vpninfo) if (ret == 1) { const char *c; - if (strcmp(vpninfo->dtls_cipher, "PSK-NEGOTIATE") && - !SSL_session_reused(vpninfo->dtls_ssl)) { + if (!strcmp(vpninfo->dtls_cipher, "PSK-NEGOTIATE")) { + /* For PSK-NEGOTIATE, we have to determine the tunnel MTU + * for ourselves based on the base MTU */ + int data_mtu = vpninfo->cstp_basemtu; + if (vpninfo->peer_addr->sa_family == IPPROTO_IPV6) + data_mtu -= 40; /* IPv6 header */ + else + data_mtu -= 20; /* Legacy IP header */ + data_mtu -= 8; /* UDP header */ + if (data_mtu < 0) { + vpn_progress(vpninfo, PRG_ERR, + _("Peer MTU %d too small to allow DTLS\n"), + vpninfo->cstp_basemtu); + goto nodtls; + } + /* Reduce it by one because that's the payload header *inside* + * the encryption */ + data_mtu = dtls_set_mtu(vpninfo, data_mtu) - 1; + if (data_mtu < 0) + goto nodtls; + if (data_mtu < vpninfo->ip_info.mtu) { + vpn_progress(vpninfo, PRG_INFO, + _("DTLS MTU reduced to %d\n"), + data_mtu); + vpninfo->ip_info.mtu = data_mtu; + } + } else if (!SSL_session_reused(vpninfo->dtls_ssl)) { /* Someone attempting to hijack the DTLS session? * A real server would never allow a full session * establishment instead of the agreed resume. */ vpn_progress(vpninfo, PRG_ERR, _("DTLS session resume failed; possible MITM attack. Disabling DTLS.\n")); + nodtls: dtls_close(vpninfo); SSL_CTX_free(vpninfo->dtls_ctx); vpninfo->dtls_ctx = NULL; -- 2.49.0