Apollon Oikonomopoulos
2017-Sep-13 20:51 UTC
[RFC master-2.2 0/1] Support OpenSSL 1.1 API for setting allowed TLS versions
Hi, I came up with the following patch while trying to figure out a good solution for the situation described in Debian bug #871987[1]. In short, OpenSSL in Debian unstable has disabled TLSv1.0 and TLSv1.1 *by default*. That means that unless an application requests otherwise, only TLSv1.2 is supported. In the world of e-mail this is seemingly an issue, as there are still way too many old clients out there supporting only TLSv1 or TLSv1.1. Now, traditionally OpenSSL 0.9.8/1.0 used SSL_CTX_set_options() to allow *disabling* specific protocols, without offering a way to enable previously disabled protocols. OpenSSL 1.1 introduced a dedicated API[2] to set allowed protocol versions, taking a linear version approach: the application may request a minimum and a maximum allowed version (inclusive), allowing all versions inbetween as well. Dovecot's existing ssl_protocols option is probably not ideal to use with this new "linear" model. Instead, I introduced two new options, ssl_min_proto_version and ssl_max_proto_version, that map directly to OpenSSL 1.1 concepts. I have tested the patch with both OpenSSL 1.0 and OpenSSL 1.1. With OpenSSL 1.1 it works as expected; with OpenSSL 1.0 it doesn't seem to break anything. Other than that, this is a first version; I'm sure there are still things to improve, so comments are welcome :) Regards, Apollon [1] https://bugs.debian.org/871987 [2] https://www.openssl.org/docs/man1.1.0/ssl/SSL_CTX_set_min_proto_version.html Apollon Oikonomopoulos (1): Support setting min/max SSL protocol version doc/example-config/conf.d/10-ssl.conf | 4 ++++ src/config/config-parser.c | 25 +++++++++++++++++++++ src/lib-master/master-service-ssl-settings.c | 4 ++++ src/lib-master/master-service-ssl-settings.h | 2 ++ src/lib-master/master-service-ssl.c | 2 ++ src/lib-ssl-iostream/iostream-openssl-common.c | 12 +++++++++++ src/lib-ssl-iostream/iostream-openssl.h | 1 + src/lib-ssl-iostream/iostream-ssl.h | 2 ++ src/login-common/ssl-proxy-openssl.c | 30 ++++++++++++++++++++++++++ 9 files changed, 82 insertions(+) -- 2.14.1
Apollon Oikonomopoulos
2017-Sep-13 20:51 UTC
[RFC master-2.2 1/1] Support setting min/max SSL protocol version
OpenSSL 1.1 exposes a new API for setting the minimum and maximum supported SSL protocol version, using SSL_CTX_set_min_proto_version and SSL_CTX_set_max_proto_version respectively. The main difference with the old SSL_CTX_set_options API is that the new API can either restrict or relax the library defaults; the old API could only be used to selectively disable protocols (but not enable what might have been disabled by default). The new API allows distributions and vendors to ship OpenSSL versions with stricter run-time defaults (e.g. TLSv1.2-only), while still allowing applications to enable older protocols (e.g. TLSv1) when dealing with legacy clients. To support the new API, we add two new config file options, ssl_min_proto_version and ssl_max_proto_version. These settings are only effective when built against OpenSSL 1.1. Also, dovecot will issue a warning if the old-style ssl_options config file option is encountered while running on OpenSSL 1.1 (although it will not ignore the option at this point). Signed-off-by: Apollon Oikonomopoulos <apoikos at debian.org> --- doc/example-config/conf.d/10-ssl.conf | 4 ++++ src/config/config-parser.c | 25 +++++++++++++++++++++ src/lib-master/master-service-ssl-settings.c | 4 ++++ src/lib-master/master-service-ssl-settings.h | 2 ++ src/lib-master/master-service-ssl.c | 2 ++ src/lib-ssl-iostream/iostream-openssl-common.c | 12 +++++++++++ src/lib-ssl-iostream/iostream-openssl.h | 1 + src/lib-ssl-iostream/iostream-ssl.h | 2 ++ src/login-common/ssl-proxy-openssl.c | 30 ++++++++++++++++++++++++++ 9 files changed, 82 insertions(+) diff --git a/doc/example-config/conf.d/10-ssl.conf b/doc/example-config/conf.d/10-ssl.conf index cf651c252..aceae233a 100644 --- a/doc/example-config/conf.d/10-ssl.conf +++ b/doc/example-config/conf.d/10-ssl.conf @@ -47,6 +47,10 @@ ssl_key = </etc/ssl/private/dovecot.pem # SSL protocols to use #ssl_protocols = !SSLv3 +# For OpenSSL 1.1.0 and later, use ssl_min_proto_version and +# ssl_max_proto_version to specify supported protocols. +# ssl_min_proto_version = TLSv1 +# ssl_max_proto_version = TLSv1.2 # SSL ciphers to use #ssl_cipher_list = ALL:!LOW:!SSLv2:!EXP:!aNULL diff --git a/src/config/config-parser.c b/src/config/config-parser.c index b7c569339..ebcf2dd60 100644 --- a/src/config/config-parser.c +++ b/src/config/config-parser.c @@ -20,6 +20,7 @@ #include <unistd.h> #include <fcntl.h> #include <time.h> +#include <openssl/opensslv.h> #ifdef HAVE_GLOB_H # include <glob.h> #endif @@ -419,6 +420,11 @@ config_all_parsers_check(struct config_parser_context *ctx, struct master_service_settings_output output; unsigned int i, count; const char *ssl_set, *global_ssl_set; +#if OPENSSL_VERSION_NUMBER >= 0x10100000 + const char *ssl_protocols; +#else + const char *ssl_min_proto_version, *ssl_max_proto_version; +#endif pool_t tmp_pool; bool ssl_warned = FALSE; int ret = 0; @@ -454,6 +460,25 @@ config_all_parsers_check(struct config_parser_context *ctx, ssl_warned = TRUE; } +#if OPENSSL_VERSION_NUMBER >= 0x10100000 + ssl_protocols = get_str_setting(parsers[i], "ssl_protocols", ""); + if (*ssl_protocols != '\0') + i_warning("ssl_protocols is deprecated and will be " + "ignored in future versions when running " + "with OpenSSL 1.1. Please use " + "ssl_min_proto_version and " + "ssl_max_proto_version instead."); +#else + ssl_min_proto_version = get_str_setting(parsers[i], + "ssl_min_proto_version", ""); + ssl_max_proto_version = get_str_setting(parsers[i], + "ssl_max_proto_version", ""); + if ((*ssl_min_proto_version != '\0') || + (*ssl_max_proto_version != '\0')) + i_warning("ssl_*_proto_version ignored, " + "not supported by OpenSSL"); +#endif + ret = config_filter_parser_check(ctx, tmp_parsers, error_r); config_filter_parsers_free(tmp_parsers); p_clear(tmp_pool); diff --git a/src/lib-master/master-service-ssl-settings.c b/src/lib-master/master-service-ssl-settings.c index 2487c8369..484022618 100644 --- a/src/lib-master/master-service-ssl-settings.c +++ b/src/lib-master/master-service-ssl-settings.c @@ -24,6 +24,8 @@ static const struct setting_define master_service_ssl_setting_defines[] = { DEF(SET_STR, ssl_key_password), DEF(SET_STR, ssl_cipher_list), DEF(SET_STR, ssl_protocols), + DEF(SET_STR, ssl_min_proto_version), + DEF(SET_STR, ssl_max_proto_version), DEF(SET_STR, ssl_cert_username_field), DEF(SET_STR, ssl_crypto_device), DEF(SET_BOOL, ssl_verify_client_cert), @@ -53,6 +55,8 @@ static const struct master_service_ssl_settings master_service_ssl_default_setti #else .ssl_protocols = "!SSLv3", #endif + .ssl_min_proto_version = "", + .ssl_max_proto_version = "", .ssl_cert_username_field = "commonName", .ssl_crypto_device = "", .ssl_verify_client_cert = FALSE, diff --git a/src/lib-master/master-service-ssl-settings.h b/src/lib-master/master-service-ssl-settings.h index a4157d3ef..0fc9aa9ca 100644 --- a/src/lib-master/master-service-ssl-settings.h +++ b/src/lib-master/master-service-ssl-settings.h @@ -13,6 +13,8 @@ struct master_service_ssl_settings { const char *ssl_key_password; const char *ssl_cipher_list; const char *ssl_protocols; + const char *ssl_min_proto_version; + const char *ssl_max_proto_version; const char *ssl_cert_username_field; const char *ssl_crypto_device; const char *ssl_options; diff --git a/src/lib-master/master-service-ssl.c b/src/lib-master/master-service-ssl.c index f8966bec8..54a22b6ba 100644 --- a/src/lib-master/master-service-ssl.c +++ b/src/lib-master/master-service-ssl.c @@ -113,6 +113,8 @@ void master_service_ssl_ctx_init(struct master_service *service) i_zero(&ssl_set); ssl_set.protocols = set->ssl_protocols; + ssl_set.min_proto_version = set->ssl_min_proto_version; + ssl_set.max_proto_version = set->ssl_max_proto_version; ssl_set.cipher_list = set->ssl_cipher_list; ssl_set.ca = set->ssl_ca; ssl_set.cert = set->ssl_cert; diff --git a/src/lib-ssl-iostream/iostream-openssl-common.c b/src/lib-ssl-iostream/iostream-openssl-common.c index bc0180367..8cbb99085 100644 --- a/src/lib-ssl-iostream/iostream-openssl-common.c +++ b/src/lib-ssl-iostream/iostream-openssl-common.c @@ -80,6 +80,18 @@ int openssl_get_protocol_options(const char *protocols) return op; } +int openssl_get_protocol_version(const char *protocol) +{ + if (!strcmp(protocol, "TLSv1")) + return TLS1_VERSION; + else if (!strcmp(protocol, "TLSv1.1")) + return TLS1_1_VERSION; + else if (!strcmp(protocol, "TLSv1.2")) + return TLS1_2_VERSION; + + return 0; +} + static const char *asn1_string_to_c(ASN1_STRING *asn_str) { const char *cstr; diff --git a/src/lib-ssl-iostream/iostream-openssl.h b/src/lib-ssl-iostream/iostream-openssl.h index d46d608d1..b6e5e100d 100644 --- a/src/lib-ssl-iostream/iostream-openssl.h +++ b/src/lib-ssl-iostream/iostream-openssl.h @@ -76,6 +76,7 @@ int openssl_cert_match_name(SSL *ssl, const char *verify_name); int openssl_get_protocol_options(const char *protocols); #define OPENSSL_ALL_PROTOCOL_OPTIONS \ (SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1) +int openssl_get_protocol_version(const char *protocol); /* Sync plain_input/plain_output streams with BIOs. Returns TRUE if at least one byte was read/written. */ diff --git a/src/lib-ssl-iostream/iostream-ssl.h b/src/lib-ssl-iostream/iostream-ssl.h index 3969f74df..948878d26 100644 --- a/src/lib-ssl-iostream/iostream-ssl.h +++ b/src/lib-ssl-iostream/iostream-ssl.h @@ -6,6 +6,8 @@ struct ssl_iostream_context; struct ssl_iostream_settings { const char *protocols; + const char *min_proto_version; + const char *max_proto_version; const char *cipher_list; const char *ca, *ca_file, *ca_dir; /* context-only */ const char *cert; diff --git a/src/login-common/ssl-proxy-openssl.c b/src/login-common/ssl-proxy-openssl.c index 8c7c3d531..dd1c2e67b 100644 --- a/src/login-common/ssl-proxy-openssl.c +++ b/src/login-common/ssl-proxy-openssl.c @@ -105,6 +105,8 @@ struct ssl_server_context { const char *ca; const char *cipher_list; const char *protocols; + const char *min_proto_version; + const char *max_proto_version; bool verify_client_cert; bool prefer_server_ciphers; bool compression; @@ -186,6 +188,10 @@ static int ssl_server_context_cmp(const struct ssl_server_context *ctx1, return 1; if (null_strcmp(ctx1->protocols, ctx2->protocols) != 0) return 1; + if (null_strcmp(ctx1->min_proto_version, ctx2->min_proto_version) != 0) + return 1; + if (null_strcmp(ctx1->max_proto_version, ctx2->max_proto_version) != 0) + return 1; return ctx1->verify_client_cert == ctx2->verify_client_cert ? 0 : 1; } @@ -631,6 +637,8 @@ ssl_server_context_get(const struct login_settings *login_set, lookup_ctx.ca = set->ssl_ca; lookup_ctx.cipher_list = set->ssl_cipher_list; lookup_ctx.protocols = set->ssl_protocols; + lookup_ctx.min_proto_version = set->ssl_min_proto_version; + lookup_ctx.max_proto_version = set->ssl_max_proto_version; lookup_ctx.verify_client_cert = set->ssl_verify_client_cert || login_set->auth_ssl_require_client_cert || login_set->auth_ssl_username_from_cert; @@ -1272,6 +1280,9 @@ ssl_server_context_init(const struct login_settings *login_set, SSL_CTX *ssl_ctx; pool_t pool; STACK_OF(X509_NAME) *xnames; +#if OPENSSL_VERSION_NUMBER >= 0x10100000 + int proto_version; +#endif pool = pool_alloconly_create("ssl server context", 4096); ctx = p_new(pool, struct ssl_server_context, 1); @@ -1283,6 +1294,8 @@ ssl_server_context_init(const struct login_settings *login_set, ctx->ca = p_strdup(pool, ssl_set->ssl_ca); ctx->cipher_list = p_strdup(pool, ssl_set->ssl_cipher_list); ctx->protocols = p_strdup(pool, ssl_set->ssl_protocols); + ctx->min_proto_version = p_strdup(pool, ssl_set->ssl_min_proto_version); + ctx->max_proto_version = p_strdup(pool, ssl_set->ssl_max_proto_version); ctx->verify_client_cert = ssl_set->ssl_verify_client_cert || login_set->auth_ssl_require_client_cert || login_set->auth_ssl_username_from_cert; @@ -1301,6 +1314,23 @@ ssl_server_context_init(const struct login_settings *login_set, } if (ctx->prefer_server_ciphers) SSL_CTX_set_options(ssl_ctx, SSL_OP_CIPHER_SERVER_PREFERENCE); +#if OPENSSL_VERSION_NUMBER >= 0x10100000 + if (*ctx->min_proto_version != '\0') { + proto_version = openssl_get_protocol_version(ctx->min_proto_version); + if (!proto_version) + i_fatal("Invalid minimum TLS version '%s'", + ctx->min_proto_version); + SSL_CTX_set_min_proto_version(ssl_ctx, proto_version); + } + if (*ctx->max_proto_version != '\0') { + proto_version = openssl_get_protocol_version(ctx->max_proto_version); + if (!proto_version) + i_fatal("Invalid maximum TLS version '%s'", + ctx->max_proto_version); + SSL_CTX_set_max_proto_version(ssl_ctx, proto_version); + } +#endif + /* TODO: stop setting the protocol options for OpenSSL >= 1.1 */ SSL_CTX_set_options(ssl_ctx, openssl_get_protocol_options(ctx->protocols)); if (ctx->pri.cert != NULL && *ctx->pri.cert != '\0' && -- 2.14.1
Brad Smith
2017-Sep-14 16:15 UTC
[RFC master-2.2 0/1] Support OpenSSL 1.1 API for setting allowed TLS versions
On 9/13/2017 4:51 PM, Apollon Oikonomopoulos wrote:> Hi, > > I came up with the following patch while trying to figure out a good solution > for the situation described in Debian bug #871987[1]. In short, OpenSSL in > Debian unstable has disabled TLSv1.0 and TLSv1.1 *by default*. That means that > unless an application requests otherwise, only TLSv1.2 is supported. In the > world of e-mail this is seemingly an issue, as there are still way too many old > clients out there supporting only TLSv1 or TLSv1.1. > > Now, traditionally OpenSSL 0.9.8/1.0 used SSL_CTX_set_options() to allow > *disabling* specific protocols, without offering a way to enable previously > disabled protocols. OpenSSL 1.1 introduced a dedicated API[2] to set allowed > protocol versions, taking a linear version approach: the application may > request a minimum and a maximum allowed version (inclusive), allowing all > versions inbetween as well. > > Dovecot's existing ssl_protocols option is probably not ideal to use with this > new "linear" model. Instead, I introduced two new options, > ssl_min_proto_version and ssl_max_proto_version, that map directly to OpenSSL > 1.1 concepts. > > I have tested the patch with both OpenSSL 1.0 and OpenSSL 1.1. With OpenSSL 1.1 > it works as expected; with OpenSSL 1.0 it doesn't seem to break anything. Other > than that, this is a first version; I'm sure there are still things to improve, > so comments are welcome :)Just FYI LibreSSL 2.6.0 and newer has picked up this API.