From 265f133600e9812726a52ea3067409ed3578e882 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 10 Dec 2020 19:39:10 +0100 Subject: libinterimap: make SSL_verify check the hostname as well. More precisely, ensure that the certificate Subject Alternative Name (SAN) or Subject CommonName (CN) matches the hostname or IP literal specified by the 'host' option. Previously it was only verifying the chain of trust. This bumps the minimum Net::SSLeay version to 1.83 and OpenSSL version 1.0.2. --- Changelog | 5 +++ doc/build.md | 2 +- doc/interimap.1.md | 14 +++++--- doc/pullimap.1.md | 14 +++++--- lib/Net/IMAP/InterIMAP.pm | 32 +++++++++++++++--- tests/certs/generate | 7 ++-- tests/run | 2 +- tests/tls-verify-peer/interimap.remote | 1 - tests/tls-verify-peer/t | 61 ++++++++++++++++++++++++++++++---- 9 files changed, 111 insertions(+), 27 deletions(-) diff --git a/Changelog b/Changelog index 71e11f7..d227efb 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,10 @@ interimap (0.5.4) upstream; + * libinterimap: make SSL_verify also checks that the certificate + Subject Alternative Name (SAN) or Subject CommonName (CN) matches the + 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: 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/build.md b/doc/build.md index 4a4f80d..47d1a89 100644 --- a/doc/build.md +++ b/doc/build.md @@ -24,7 +24,7 @@ following Perl modules: * [`Getopt::Long`](https://perldoc.perl.org/Getopt/Long.html) (*core module*) * [`MIME::Base64`](https://perldoc.perl.org/MIME/Base64.html) (*core module*) — if authentication is required * [`List::Util`](https://perldoc.perl.org/List/Util.html) (*core module*) - * [`Net::SSLeay`](https://metacpan.org/pod/Net::SSLeay) ≥1.73 + * [`Net::SSLeay`](https://metacpan.org/pod/Net::SSLeay) ≥1.83 * [`POSIX`](https://perldoc.perl.org/POSIX.html) (*core module*) * [`Socket`](https://perldoc.perl.org/Socket.html) (*core module*) * [`Time::HiRes`](https://perldoc.perl.org/Time/HiRes.html) (*core module*) — if `logfile` is set diff --git a/doc/interimap.1.md b/doc/interimap.1.md index ab35275..d21424b 100644 --- a/doc/interimap.1.md +++ b/doc/interimap.1.md @@ -420,15 +420,19 @@ Valid options are: *SSL_verify* -: Whether to verify the server certificate chain. +: Whether to verify the server certificate chain, and match its + Subject Alternative Name (SAN) or Subject CommonName (CN) against + the value of the *host* option. + (Default: `YES`.) + Note that using *SSL_fingerprint* to specify the fingerprint of the server certificate provides an independent server authentication - measure as it ignores the CA chain. - (Default: `YES`.) + measure as it pins directly its key material and ignore its chain of + trust. *SSL_CApath* -: Directory to use for server certificate verification if +: Directory to use for server certificate verification when `SSL_verify=YES`. This directory must be in “hash format”, see [`verify`(1ssl)] for more information. @@ -436,7 +440,7 @@ Valid options are: *SSL_CAfile* : File containing trusted certificates to use during server - certificate verification if `SSL_verify=YES`. + certificate verification when `SSL_verify=YES`. Supported extensions {#supported-extensions} ==================== diff --git a/doc/pullimap.1.md b/doc/pullimap.1.md index 57790a6..bcf5ade 100644 --- a/doc/pullimap.1.md +++ b/doc/pullimap.1.md @@ -239,15 +239,19 @@ Valid options are: *SSL_verify* -: Whether to verify the server certificate chain. +: Whether to verify the server certificate chain, and match its + Subject Alternative Name (SAN) or Subject CommonName (CN) against + the value of the *host* option. + (Default: `YES`.) + Note that using *SSL_fingerprint* to specify the fingerprint of the server certificate provides an independent server authentication - measure as it ignores the CA chain. - (Default: `YES`.) + measure as it pins directly its key material and ignore its chain of + trust. *SSL_CApath* -: Directory to use for server certificate verification if +: Directory to use for server certificate verification when `SSL_verify=YES`. This directory must be in “hash format”, see [`verify`(1ssl)] for more information. @@ -255,7 +259,7 @@ Valid options are: *SSL_CAfile* : File containing trusted certificates to use during server - certificate verification if `SSL_verify=YES`. + certificate verification when `SSL_verify=YES`. Control flow {#control-flow} ============ diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm index e3a539d..a0efcc1 100644 --- a/lib/Net/IMAP/InterIMAP.pm +++ b/lib/Net/IMAP/InterIMAP.pm @@ -24,7 +24,7 @@ use strict; use Compress::Raw::Zlib qw/Z_OK Z_STREAM_END Z_FULL_FLUSH Z_SYNC_FLUSH MAX_WBITS/; use Config::Tiny (); use Errno qw/EEXIST EINTR/; -use Net::SSLeay 1.73 (); +use Net::SSLeay 1.83 (); use List::Util qw/all first/; use POSIX ':signal_h'; use Socket qw/SOCK_STREAM SOCK_RAW SOCK_CLOEXEC IPPROTO_TCP SHUT_RDWR @@ -1691,6 +1691,7 @@ BEGIN { # Upgrade the $socket to SSL/TLS. sub _start_ssl($$) { my ($self, $socket) = @_; + my $openssl_version = Net::SSLeay::OPENSSL_VERSION_NUMBER(); my $ctx = Net::SSLeay::CTX_new() or $self->panic("Failed to create SSL_CTX $!"); my $ssl_options = Net::SSLeay::OP_SINGLE_DH_USE() | Net::SSLeay::OP_SINGLE_ECDH_USE(); @@ -1733,25 +1734,46 @@ sub _start_ssl($$) { or $self->_ssl_error("Can't set cipher list"); } + my $vpm = Net::SSLeay::X509_VERIFY_PARAM_new() or $self->_ssl_error("X509_VERIFY_PARAM_new()"); + my $purpose = Net::SSLeay::X509_PURPOSE_SSL_SERVER(); + $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; + if ($self->{SSL_verify} // 1) { - # verify the certificate chain + # for X509_VERIFY_PARAM_set1_{ip,host}() + $self->panic("Failed requirement libssl >=1.0.2") if $openssl_version < 0x1000200f; + + # verify certificate chain my ($file, $path) = ($self->{SSL_CAfile} // '', $self->{SSL_CApath} // ''); if ($file ne '' or $path ne '') { Net::SSLeay::CTX_load_verify_locations($ctx, $file, $path) or $self->_ssl_error("Can't load verify locations"); } + + # 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()") + unless Net::SSLeay::X509_VERIFY_PARAM_set1_ip($vpm, $addr) == 1; + } else { + $self->_ssl_error("X509_VERIFY_PARAM_set1_host()") + unless Net::SSLeay::X509_VERIFY_PARAM_set1_host($vpm, $host) == 1; + } } else { Net::SSLeay::CTX_set_verify_depth($ctx, 0); } - Net::SSLeay::CTX_set_purpose($ctx, Net::SSLeay::X509_PURPOSE_SSL_SERVER()) - or $self->_ssl_error("Can't set purpose"); Net::SSLeay::CTX_set_verify($ctx, Net::SSLeay::VERIFY_PEER(), sub($$) {$self->_ssl_verify(@_)}); + $self->_ssl_error("CTX_SSL_set1_param()") unless Net::SSLeay::CTX_set1_param($ctx, $vpm) == 1; 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"); $self->_ssl_error("Can't initiate TLS/SSL handshake") unless Net::SSLeay::connect($ssl) == 1; - $self->panic("Couldn't verify") unless $self->{_SSL_PEER_VERIFIED}; # sanity check + $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(); + Net::SSLeay::X509_VERIFY_PARAM_free($vpm); if ($self->{debug}) { my $v = Net::SSLeay::version($ssl); diff --git a/tests/certs/generate b/tests/certs/generate index 19463d5..6457765 100755 --- a/tests/certs/generate +++ b/tests/certs/generate @@ -20,19 +20,22 @@ SERIAL=1 new() { local key="$1" cn="$2" openssl req -new -rand /dev/urandom -key "$key" \ - -subj "/OU=$OU/CN=$cn" \ + -subj "/OU=$OU/CN=$cn" ${3+-addext subjectAltName="$3"} \ -out "$cadir/new.csr" cat >"$cadir/new-ext.cnf" <<-EOF basicConstraints = critical, CA:FALSE keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = critical, serverAuth EOF + if [ -n "${3+x}" ]; then + printf "subjectAltName = %s\\n" "$3" >>"$cadir/new-ext.cnf" + fi openssl x509 -req -in "$cadir/new.csr" -CA ./ca.crt -CAkey "$cadir/ca.key" \ -CAserial "$cadir/ca.srl" -CAcreateserial -extfile "$cadir/new-ext.cnf" } openssl genpkey -algorithm RSA -out ./dovecot.rsa.key -new ./dovecot.rsa.key "localhost" >./dovecot.rsa.crt +new ./dovecot.rsa.key "localhost" "DNS:localhost,DNS:ip6-localhost,IP:127.0.0.1,IP:::1" >./dovecot.rsa.crt 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 diff --git a/tests/run b/tests/run index 0305812..d216591 100755 --- a/tests/run +++ b/tests/run @@ -93,7 +93,7 @@ prepare() { mail_location = dbox:~/inbox:LAYOUT=index mailbox_list_index = yes ssl = no - listen = 127.0.0.1, ::1 + listen = 127.0.0.1, 127.0.1.1, ::1 namespace inbox { inbox = yes } diff --git a/tests/tls-verify-peer/interimap.remote b/tests/tls-verify-peer/interimap.remote index b02fcd0..263655f 100644 --- a/tests/tls-verify-peer/interimap.remote +++ b/tests/tls-verify-peer/interimap.remote @@ -1,2 +1 @@ -host = ::1 port = 10993 diff --git a/tests/tls-verify-peer/t b/tests/tls-verify-peer/t index 9b676a6..2461a1f 100644 --- a/tests/tls-verify-peer/t +++ b/tests/tls-verify-peer/t @@ -1,6 +1,15 @@ +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]")" +PKEY_SHA256="$(doveconf -c "$HOME_remote/.dovecot/config" -hx ssl_cert \ + | openssl x509 -pubkey | openssl pkey -pubin -outform DER \ + | openssl dgst -sha256 | sed -rn "/^.*=\\s*/ {s///p;q}")" + unverified_peer() { ! interimap --debug || error + # make sure we aborted the handshake immediately after connecting + grep -Fx "remote: Peer certificate fingerprint: sha256\$$X509_SHA256" <"$STDERR" || error grep -Fx "remote: ERROR: Can't initiate TLS/SSL handshake" <"$STDERR" || error sed -nr "s/remote: \[[0-9]+\] (preverify=[0-9]+)$/\1/p" <"$STDERR" >"$TMPDIR/preverify" [ -s "$TMPDIR/preverify" ] || error @@ -11,12 +20,13 @@ unverified_peer() { } verified_peer() { local i u - for ((i = 0; i < 32; i++)); do + for ((i = 0; i < 4; i++)); do u="$(shuf -n1 -e "local" "remote")" sample_message | deliver -u "$u" done interimap --debug || error + grep -Fx "remote: Peer certificate fingerprint: sha256\$$X509_SHA256" <"$STDERR" || error sed -nr "s/remote: \[[0-9]+\] (preverify=[0-9]+)$/\1/p" <"$STDERR" >"$TMPDIR/preverify" [ -s "$TMPDIR/preverify" ] || error ! grep -Fvx "preverify=1" <"$TMPDIR/preverify" || error @@ -39,9 +49,6 @@ unverified_peer step_done step_start "peer verification result honored when pinned pubkey matches" -PKEY_SHA256="$(doveconf -c "$HOME_remote/.dovecot/config" -hx ssl_cert \ - | openssl x509 -pubkey | openssl pkey -pubin -outform DER \ - | openssl dgst -sha256 | sed -rn "/^.*=\\s*/ {s///p;q}")" with_remote_config <<-EOF SSL_fingerprint = sha256\$$PKEY_SHA256 EOF @@ -50,31 +57,71 @@ grep -Fx "remote: Peer certificate matches pinned SPKI digest sha256\$$PKEY_SHA2 step_done capath=$(mktemp --tmpdir="$TMPDIR" --directory capath.XXXXXX) +cp -T -- ~/.dovecot/conf.d/ca.crt "$capath/ca-certificates.crt" step_start "SSL_CAfile" if [ -f "/etc/ssl/certs/ca-certificates.crt" ]; then - # our fake root CA should not be in there + # assume our fake root CA is not there with_remote_config <<<"SSL_CAfile = /etc/ssl/certs/ca-certificates.crt" unverified_peer fi -cp -T -- ~/.dovecot/conf.d/ca.crt "$capath/ca-certificates.crt" +# default host (localhost) is the CN (and also subjectAltName) with_remote_config <<<"SSL_CAfile = $capath/ca-certificates.crt" verified_peer + +# hostnames and IPs included in the subjectAltName should work as well +for host in "ip6-localhost" "127.0.0.1" "::1"; do + with_remote_config <<-EOF + host = $host + SSL_CAfile = $capath/ca-certificates.crt + EOF + verified_peer +done + +# but not for other IPs or hostnames +for host in "ip6-loopback" "127.0.1.1"; do + with_remote_config <<-EOF + host = $host + SSL_CAfile = $capath/ca-certificates.crt + EOF + unverified_peer +done + step_done step_start "SSL_CApath" if [ -d "/etc/ssl/certs" ]; then - # our fake root CA should not be in there + # assume our fake root CA is not there with_remote_config <<<"SSL_CApath = /etc/ssl/certs" unverified_peer fi c_rehash "$capath" +# default host (localhost) is the CN (and also subjectAltName) with_remote_config <<<"SSL_CApath = $capath" verified_peer + +# hostnames and IPs included in the subjectAltName should work as well +for host in "ip6-localhost" "127.0.0.1" "::1"; do + with_remote_config <<-EOF + host = $host + SSL_CApath = $capath + EOF + verified_peer +done + +# but not for other IPs or hostnames +for host in "ip6-loopback" "127.0.1.1"; do + with_remote_config <<-EOF + host = $host + SSL_CApath = $capath + EOF + unverified_peer +done + step_done # vim: set filetype=sh : -- cgit v1.2.3