diff --git a/parseconf.c b/parseconf.c index 3729818..ee1b8b4 100644 --- a/parseconf.c +++ b/parseconf.c @@ -188,6 +188,7 @@ parseconf_str_array[] = { "rsa_private_key_file", &tunable_rsa_private_key_file }, { "dsa_private_key_file", &tunable_dsa_private_key_file }, { "ca_certs_file", &tunable_ca_certs_file }, + { "ssl_sni_hostname", &tunable_ssl_sni_hostname }, { "cmds_denied", &tunable_cmds_denied }, { 0, 0 } }; diff --git a/ssl.c b/ssl.c index 09ec96a..b622347 100644 --- a/ssl.c +++ b/ssl.c @@ -41,6 +41,13 @@ static long bio_callback( BIO* p_bio, int oper, const char* p_arg, int argi, long argl, long retval); static int ssl_verify_callback(int verify_ok, X509_STORE_CTX* p_ctx); static DH *ssl_tmp_dh_callback(SSL *ssl, int is_export, int keylength); +static int ssl_alpn_callback(SSL* p_ssl, + const unsigned char** p_out, + unsigned char* outlen, + const unsigned char* p_in, + unsigned int inlen, + void* p_arg); +static long ssl_sni_callback(SSL* p_ssl, int* p_al, void* p_arg); static int ssl_cert_digest( SSL* p_ssl, struct vsf_session* p_sess, struct mystr* p_str); static void maybe_log_shutdown_state(struct vsf_session* p_sess); @@ -285,6 +292,11 @@ ssl_init(struct vsf_session* p_sess) SSL_CTX_set_timeout(p_ctx, INT_MAX); } + /* Set up ALPN to check for FTP protocol intention of client. */ + SSL_CTX_set_alpn_select_cb(p_ctx, ssl_alpn_callback, p_sess); + /* Set up SNI callback for an optional hostname check. */ + SSL_CTX_set_tlsext_servername_callback(p_ctx, ssl_sni_callback); + SSL_CTX_set_tlsext_servername_arg(p_ctx, p_sess); SSL_CTX_set_tmp_dh_callback(p_ctx, ssl_tmp_dh_callback); if (tunable_ecdh_param_file) @@ -871,6 +883,133 @@ ssl_tmp_dh_callback(SSL *ssl, int is_export, int keylength) return DH_get_dh(keylength); } +static int +ssl_alpn_callback(SSL* p_ssl, + const unsigned char** p_out, + unsigned char* outlen, + const unsigned char* p_in, + unsigned int inlen, + void* p_arg) { + unsigned int i; + struct vsf_session* p_sess = (struct vsf_session*) p_arg; + int is_ok = 0; + + (void) p_ssl; + + /* Initialize just in case. */ + *p_out = p_in; + *outlen = 0; + + for (i = 0; i < inlen; ++i) { + unsigned int left = (inlen - i); + if (left < 4) { + continue; + } + if (p_in[i] == 3 && p_in[i + 1] == 'f' && p_in[i + 2] == 't' && + p_in[i + 3] == 'p') + { + is_ok = 1; + *p_out = &p_in[i + 1]; + *outlen = 3; + break; + } + } + + if (!is_ok) + { + str_alloc_text(&debug_str, "ALPN rejection"); + vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str); + } + if (!is_ok || tunable_debug_ssl) + { + str_alloc_text(&debug_str, "ALPN data: "); + for (i = 0; i < inlen; ++i) { + str_append_char(&debug_str, p_in[i]); + } + vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str); + } + + if (is_ok) + { + return SSL_TLSEXT_ERR_OK; + } + else + { + return SSL_TLSEXT_ERR_ALERT_FATAL; + } +} + +static long +ssl_sni_callback(SSL* p_ssl, int* p_al, void* p_arg) +{ + static struct mystr s_sni_expected_hostname; + static struct mystr s_sni_received_hostname; + + int servername_type; + const char* p_sni_servername; + struct vsf_session* p_sess = (struct vsf_session*) p_arg; + int is_ok = 0; + + (void) p_ssl; + (void) p_arg; + + if (tunable_ssl_sni_hostname) + { + str_alloc_text(&s_sni_expected_hostname, tunable_ssl_sni_hostname); + } + + /* The OpenSSL documentation says it is pre-initialized like this, but set + * it just in case. + */ + *p_al = SSL_AD_UNRECOGNIZED_NAME; + + servername_type = SSL_get_servername_type(p_ssl); + p_sni_servername = SSL_get_servername(p_ssl, TLSEXT_NAMETYPE_host_name); + if (p_sni_servername != NULL) { + str_alloc_text(&s_sni_received_hostname, p_sni_servername); + } + + if (str_isempty(&s_sni_expected_hostname)) + { + is_ok = 1; + } + else if (servername_type != TLSEXT_NAMETYPE_host_name) + { + /* Fail. */ + str_alloc_text(&debug_str, "SNI bad type: "); + str_append_ulong(&debug_str, servername_type); + vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str); + } + else + { + if (!str_strcmp(&s_sni_expected_hostname, &s_sni_received_hostname)) + { + is_ok = 1; + } + else + { + str_alloc_text(&debug_str, "SNI rejection"); + vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str); + } + } + + if (!is_ok || tunable_debug_ssl) + { + str_alloc_text(&debug_str, "SNI hostname: "); + str_append_str(&debug_str, &s_sni_received_hostname); + vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str); + } + + if (is_ok) + { + return SSL_TLSEXT_ERR_OK; + } + else + { + return SSL_TLSEXT_ERR_ALERT_FATAL; + } +} + void ssl_add_entropy(struct vsf_session* p_sess) { diff --git a/tunables.c b/tunables.c index c96c1ac..d8dfcde 100644 --- a/tunables.c +++ b/tunables.c @@ -152,6 +152,7 @@ const char* tunable_ssl_ciphers; const char* tunable_rsa_private_key_file; const char* tunable_dsa_private_key_file; const char* tunable_ca_certs_file; +const char* tunable_ssl_sni_hostname; static void install_str_setting(const char* p_value, const char** p_storage); @@ -309,6 +310,7 @@ tunables_load_defaults() install_str_setting(0, &tunable_rsa_private_key_file); install_str_setting(0, &tunable_dsa_private_key_file); install_str_setting(0, &tunable_ca_certs_file); + install_str_setting(0, &tunable_ssl_sni_hostname); } void diff --git a/tunables.h b/tunables.h index 8d50150..de6cab0 100644 --- a/tunables.h +++ b/tunables.h @@ -157,6 +157,7 @@ extern const char* tunable_ssl_ciphers; extern const char* tunable_rsa_private_key_file; extern const char* tunable_dsa_private_key_file; extern const char* tunable_ca_certs_file; +extern const char* tunable_ssl_sni_hostname; extern const char* tunable_cmds_denied; #endif /* VSF_TUNABLES_H */ diff --git a/vsftpd.conf.5 b/vsftpd.conf.5 index 815773f..7006287 100644 --- a/vsftpd.conf.5 +++ b/vsftpd.conf.5 @@ -1128,6 +1128,12 @@ for further details. Default: PROFILE=SYSTEM .TP +.B ssl_sni_hostname +If set, SSL connections will be rejected unless the SNI hostname in the +incoming handshakes matches this value. + +Default: (none) +.TP .B user_config_dir This powerful option allows the override of any config option specified in the manual page, on a per-user basis. Usage is simple, and is best illustrated