aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@fripost.org>2020-12-10 21:52:44 +0100
committerGuilhem Moulin <guilhem@fripost.org>2020-12-11 11:20:41 +0100
commit4ed6f0982cc0553e31e7beadf441beb8573a07d4 (patch)
treeb8ad71b56dc8d9a237e308877922500b46c351e7
parent09376bac4fe99c542223ba0ae23ad6067410b1fa (diff)
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.
-rw-r--r--Changelog5
-rw-r--r--doc/interimap.1.md7
-rw-r--r--doc/pullimap.1.md7
-rw-r--r--lib/Net/IMAP/InterIMAP.pm15
-rwxr-xr-xtests/certs/generate3
-rw-r--r--tests/list3
-rw-r--r--tests/tls-sni/interimap.remote3
-rw-r--r--tests/tls-sni/remote.conf7
-rw-r--r--tests/tls-sni/t66
9 files changed, 113 insertions, 3 deletions
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 = <conf.d/dovecot.rsa2.crt
+ ssl_key = <conf.d/dovecot.rsa2.key
+}
diff --git a/tests/tls-sni/t b/tests/tls-sni/t
new file mode 100644
index 0000000..f18b8b0
--- /dev/null
+++ b/tests/tls-sni/t
@@ -0,0 +1,66 @@
+SERVERNAME="imap.example.net" # cf local_name{} section in the dovecot config
+X509_SHA256="$(doveconf -c "$HOME_remote/.dovecot/config" -hx ssl_cert \
+ | openssl x509 -noout -fingerprint -sha256 \
+ | sed -rn "/^.*=\\s*/ {s///p;q}" | tr -d : | tr "[A-Z]" "[a-z]")"
+X509_2_SHA256="$(doveconf -c "$HOME_remote/.dovecot/config" -f lname="$SERVERNAME" -hx ssl_cert \
+ | openssl x509 -noout -fingerprint -sha256 \
+ | sed -rn "/^.*=\\s*/ {s///p;q}" | tr -d : | tr "[A-Z]" "[a-z]")"
+
+# check that empty SSL_hostname disables SNI
+echo "SSL_hostname =" >>"$XDG_CONFIG_HOME/interimap/config"
+interimap --debug || error
+! grep "^remote: Using SNI with name " <"$STDERR" || error "Empty SSL_hostname didn't disable SNI"
+
+# default servername is the host value
+sed -i "/^SSL_hostname\\s*=/d" -- "$XDG_CONFIG_HOME/interimap/config"
+interimap --debug || error
+grep -Fx "remote: Using SNI with name localhost" <"$STDERR" || error "No default SNI"
+grep -Fx "remote: Peer certificate fingerprint: sha256\$$X509_SHA256" <"$STDERR" || error
+
+# verify that SNI is not used when host is an IP
+echo "host = __INVALID__" >>"$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 :