From 4ed6f0982cc0553e31e7beadf441beb8573a07d4 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 10 Dec 2020 21:52:44 +0100 Subject: libinterimap: add support for the TLS SNI (Server Name Indication) extension. This is controlled by the new 'SSL_hostname' option. The default value of that option is the value of the 'host' option when it is hostname, and the empty string (which disables SNI) when it is an IP literal. --- Changelog | 5 ++++ doc/interimap.1.md | 7 +++++ doc/pullimap.1.md | 7 +++++ lib/Net/IMAP/InterIMAP.pm | 15 ++++++++-- tests/certs/generate | 3 ++ tests/list | 3 +- tests/tls-sni/interimap.remote | 3 ++ tests/tls-sni/remote.conf | 7 +++++ tests/tls-sni/t | 66 ++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 tests/tls-sni/interimap.remote create mode 100644 tests/tls-sni/remote.conf create mode 100644 tests/tls-sni/t diff --git a/Changelog b/Changelog index d227efb..87ce9fd 100644 --- a/Changelog +++ b/Changelog @@ -5,6 +5,11 @@ interimap (0.5.4) upstream; hostname or IP literal specified by the 'host' option. Previously it was only checking the chain of trust. This bumps the minimum Net::SSLeay version to 1.83 and OpenSSL version 1.0.2. + * libinterimap: add support for the TLS SNI (Server Name Indication) + extension, controlled by the new 'SSL_hostname' option. The default + value of that option is the value of the 'host' option when it is + hostname, and the empty string (which disables SNI) when it is an IP + literal. + libinterimap: show the matching pinned SPKI in --debug mode. + test suite: always generate new certificates on `make test`. Hence running `make test` now requires OpenSSL 1.1.1 or later. diff --git a/doc/interimap.1.md b/doc/interimap.1.md index d21424b..54c3dcf 100644 --- a/doc/interimap.1.md +++ b/doc/interimap.1.md @@ -442,6 +442,13 @@ Valid options are: : File containing trusted certificates to use during server certificate verification when `SSL_verify=YES`. +*SSL_hostname* + +: Name to use for the TLS SNI (Server Name Indication) extension. The + default value is taken from the *host* option when it is a hostname, + and to the empty string when it is an IP literal. + Setting *SSL_hostname* to the empty string explicitly disables SNI. + Supported extensions {#supported-extensions} ==================== diff --git a/doc/pullimap.1.md b/doc/pullimap.1.md index bcf5ade..fb3a73b 100644 --- a/doc/pullimap.1.md +++ b/doc/pullimap.1.md @@ -261,6 +261,13 @@ Valid options are: : File containing trusted certificates to use during server certificate verification when `SSL_verify=YES`. +*SSL_hostname* + +: Name to use for the TLS SNI (Server Name Indication) extension. The + default value is taken from the *host* option when it is a hostname, + and to the empty string when it is an IP literal. + Setting *SSL_hostname* to the empty string explicitly disables SNI. + Control flow {#control-flow} ============ diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm index a0efcc1..e3a5d31 100644 --- a/lib/Net/IMAP/InterIMAP.pm +++ b/lib/Net/IMAP/InterIMAP.pm @@ -65,6 +65,7 @@ my %OPTIONS = ( SSL_protocols => qr/\A(!?$RE_SSL_PROTO(?: !?$RE_SSL_PROTO)*)\z/, SSL_fingerprint => qr/\A((?:[A-Za-z0-9]+\$)?\p{AHex}+(?: (?:[A-Za-z0-9]+\$)?\p{AHex}+)*)\z/, SSL_cipherlist => qr/\A(\P{Control}+)\z/, + SSL_hostname => qr/\A(\P{Control}*)\z/, SSL_verify => qr/\A(YES|NO)\z/i, SSL_CApath => qr/\A(\P{Control}+)\z/, SSL_CAfile => qr/\A(\P{Control}+)\z/, @@ -1739,6 +1740,8 @@ sub _start_ssl($$) { $self->_ssl_error("X509_VERIFY_PARAM_set_purpose()") unless Net::SSLeay::X509_VERIFY_PARAM_set_purpose($vpm, $purpose) == 1; $self->_ssl_error("CTX_set_purpose()") unless Net::SSLeay::CTX_set_purpose($ctx, $purpose) == 1; + my $host = $self->{host} // $self->panic(); + my ($hostip, $hostipfam) = _parse_hostip($host); if ($self->{SSL_verify} // 1) { # for X509_VERIFY_PARAM_set1_{ip,host}() $self->panic("Failed requirement libssl >=1.0.2") if $openssl_version < 0x1000200f; @@ -1751,8 +1754,6 @@ sub _start_ssl($$) { } # verify DNS hostname or IP literal - my $host = $self->{host} // $self->panic(); - my ($hostip, $hostipfam) = _parse_hostip($host); if (defined $hostipfam) { my $addr = Socket::inet_pton($hostipfam, $hostip) // $self->panic(); $self->_ssl_error("X509_VERIFY_PARAM_set1_ip()") @@ -1770,6 +1771,16 @@ sub _start_ssl($$) { my $ssl = Net::SSLeay::new($ctx) or $self->fail("Can't create new SSL structure"); Net::SSLeay::set_fd($ssl, fileno $socket) or $self->fail("SSL filehandle association failed"); + + # always use 'SSL_hostname' when set, otherwise use 'host' (unless it's an IP) on OpenSSL >=0.9.8f + my $servername = $self->{SSL_hostname} // (defined $hostipfam ? "" : $host); + if ($servername ne "") { + $self->panic("Failed requirement libssl >=0.9.8f") if $openssl_version < 0x00908070; + $self->_ssl_error("Can't set TLS servername extension (value $servername)") + unless Net::SSLeay::set_tlsext_host_name($ssl, $servername) == 1; + $self->log("Using SNI with name $servername") if $self->{debug}; + } + $self->_ssl_error("Can't initiate TLS/SSL handshake") unless Net::SSLeay::connect($ssl) == 1; $self->panic() unless $self->{_SSL_PEER_VERIFIED}; # sanity check $self->panic() if ($self->{SSL_verify} // 1) and Net::SSLeay::get_verify_result($ssl) != Net::SSLeay::X509_V_OK(); diff --git a/tests/certs/generate b/tests/certs/generate index 6457765..de379a0 100755 --- a/tests/certs/generate +++ b/tests/certs/generate @@ -39,3 +39,6 @@ new ./dovecot.rsa.key "localhost" "DNS:localhost,DNS:ip6-localhost,IP:127.0.0.1, openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -pkeyopt ec_param_enc:named_curve -out ./dovecot.ecdsa.key new ./dovecot.ecdsa.key "localhost" >./dovecot.ecdsa.crt + +openssl genpkey -algorithm RSA -out ./dovecot.rsa2.key +new ./dovecot.rsa2.key "imap.example.net" "DNS:imap.example.net,DNS:localhost" >./dovecot.rsa2.crt diff --git a/tests/list b/tests/list index 0ad86e4..cb31a73 100644 --- a/tests/list +++ b/tests/list @@ -51,7 +51,8 @@ split-set Split large sets to avoid extra-long command lines tls SSL/TLS handshake ... tls-verify-peer tls-pin-fingerprint pubkey fingerprint pinning - tls-rsa+ecdsa pubkey fingerprint pinning for hybrid RSA+ECDSA + tls-rsa+ecdsa pubkey fingerprint pinning for dual-cert RSA+ECDSA + tls-sni TLS servername extension (SNI) tls-protocols force TLS protocol versions . Live synchronization (60s) diff --git a/tests/tls-sni/interimap.remote b/tests/tls-sni/interimap.remote new file mode 100644 index 0000000..9f0d521 --- /dev/null +++ b/tests/tls-sni/interimap.remote @@ -0,0 +1,3 @@ +type = imaps +port = 10993 +SSL_verify = no diff --git a/tests/tls-sni/remote.conf b/tests/tls-sni/remote.conf new file mode 100644 index 0000000..4ccfb44 --- /dev/null +++ b/tests/tls-sni/remote.conf @@ -0,0 +1,7 @@ +!include conf.d/imapd.conf +!include conf.d/ssl.conf + +local_name imap.example.net { + ssl_cert = >"$XDG_CONFIG_HOME/interimap/config" +for ip in "127.0.0.1" "[::1]"; do + sed -i "s/^host\\s*=.*/host = $ip/" -- "$XDG_CONFIG_HOME/interimap/config" + interimap --debug || error + ! grep "^remote: Using SNI with name " <"$STDERR" || error "Using SNI with IP $ip" + grep -Fx "remote: Peer certificate fingerprint: sha256\$$X509_SHA256" <"$STDERR" || error +done + +# verify that SNI actually works (ie we're served the right cert) +sni_ok() { + grep -Fx "remote: Using SNI with name $SERVERNAME" <"$STDERR" || error + grep -Fx "remote: Peer certificate fingerprint: sha256\$$X509_2_SHA256" <"$STDERR" || error +} +echo "SSL_hostname = $SERVERNAME" >>"$XDG_CONFIG_HOME/interimap/config" +interimap --debug || error +sni_ok + + +## make sure SSL_hostname doesn't affect certificate verification ## + +# bad CA, bad host +sed -i "s/^host\\s*=.*/host = 127.0.0.1/" -- "$XDG_CONFIG_HOME/interimap/config" +sed -i "s/^SSL_verify\\s*=.*/SSL_verify = YES/" -- "$XDG_CONFIG_HOME/interimap/config" +! interimap --debug || error +sni_ok +grep -Fx "remote: ERROR: Can't initiate TLS/SSL handshake" <"$STDERR" || error + +# good CA, bad host +echo "SSL_CAfile = $HOME/.dovecot/conf.d/ca.crt" >>"$XDG_CONFIG_HOME/interimap/config" +! interimap --debug || error +sni_ok +grep -Fx "remote: ERROR: Can't initiate TLS/SSL handshake" <"$STDERR" || error + +# bad CA, good host +sed -i "/^SSL_CAfile\\s*=/d" -- "$XDG_CONFIG_HOME/interimap/config" +sed -i "s/^host\\s*=.*/host = localhost/" -- "$XDG_CONFIG_HOME/interimap/config" +! interimap --debug || error +sni_ok +grep -Fx "remote: ERROR: Can't initiate TLS/SSL handshake" <"$STDERR" || error + +# good CA, good host +echo "SSL_CAfile = $HOME/.dovecot/conf.d/ca.crt" >>"$XDG_CONFIG_HOME/interimap/config" +interimap --debug || error +sni_ok + +# vim: set filetype=sh : -- cgit v1.2.3