aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Changelog5
-rw-r--r--doc/build.md2
-rw-r--r--doc/interimap.1.md14
-rw-r--r--doc/pullimap.1.md14
-rw-r--r--lib/Net/IMAP/InterIMAP.pm32
-rwxr-xr-xtests/certs/generate7
-rwxr-xr-xtests/run2
-rw-r--r--tests/tls-verify-peer/interimap.remote1
-rw-r--r--tests/tls-verify-peer/t61
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 :