aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Changelog26
-rw-r--r--Makefile18
-rw-r--r--doc/build.md8
-rw-r--r--doc/getting-started.md2
-rw-r--r--doc/interimap.1.md33
-rw-r--r--doc/pullimap.1.md33
-rwxr-xr-xinterimap2
-rw-r--r--interimap.sample4
-rw-r--r--lib/Net/IMAP/InterIMAP.pm107
-rwxr-xr-xpullimap2
-rw-r--r--tests/certs/.gitignore4
-rwxr-xr-xtests/certs/generate44
-rw-r--r--tests/list3
-rwxr-xr-xtests/run3
-rw-r--r--tests/snippets/dovecot/dovecot.ecdsa.crt11
-rw-r--r--tests/snippets/dovecot/dovecot.ecdsa.key5
-rw-r--r--tests/snippets/dovecot/dovecot.rsa.crt19
-rw-r--r--tests/snippets/dovecot/dovecot.rsa.key28
-rw-r--r--tests/tls-pin-fingerprint/t9
-rw-r--r--tests/tls-rsa+ecdsa/t7
-rw-r--r--tests/tls-sni/interimap.remote3
-rw-r--r--tests/tls-sni/remote.conf7
-rw-r--r--tests/tls-sni/t66
-rw-r--r--tests/tls-verify-peer/interimap.remote1
-rw-r--r--tests/tls-verify-peer/t67
25 files changed, 376 insertions, 136 deletions
diff --git a/Changelog b/Changelog
index 4d9b9a4..28a1ef4 100644
--- a/Changelog
+++ b/Changelog
@@ -1,3 +1,29 @@
+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: 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.
+ + test suite: sign all test certificates with the same root CA.
+ + libinterimap: factor out hostname/IP parsing.
+ + document that enclosing 'host' value in square brackets forces its
+ interpretation as an IP literal (hence skips name resolution).
+ + Makefile: new 'release' target; also, change the tag format from
+ upstream/$VERSION to v$VERSION.
+ - documentation: replace example.org with example.net for consistency.
+ - rename 'debian' branch to 'debian/latest' for DEP-14 compliance.
+
+ -- Guilhem Moulin <guilhem@fripost.org> Fri, 11 Dec 2020 11:21:17 +0100
+
interimap (0.5.3) upstream;
* libinterimap: SSL_fingerprint now supports a space-separate list of
diff --git a/Makefile b/Makefile
index 3d60dfb..ff62bf1 100644
--- a/Makefile
+++ b/Makefile
@@ -16,7 +16,21 @@ $(MANUAL_FILES): $(BUILD_DOCDIR)/%: ./doc/%.md
pandoc -f markdown -t json -- "$<" | ./pandoc2man.jq | pandoc -s -f json -t man -o "$@"
test:
- @./tests/run-all
+ ./tests/certs/generate
+ ./tests/run-all
+
+release:
+ @if ! git diff HEAD --quiet -- ./interimap ./pullimap ./Changelog; then \
+ echo "Dirty state, refusing to release!" >&2; \
+ exit 1; \
+ fi
+ sed -ri "0,/^( -- .*) .*/ s//\1 $(shell date -R)/" ./Changelog
+ VERS=$$(dpkg-parsechangelog -l Changelog -SVersion 2>/dev/null) && \
+ sed -ri "0,/^(our \\\$$VERSION\\s*=\s*)'[0-9.]+'\\s*;/ s//\1'$$VERS';/" \
+ -- ./interimap ./pullimap && \
+ git commit -m "Prepare new release v$$VERS." \
+ -- ./interimap ./pullimap ./Changelog && \
+ git tag -sm "Release version $$VERS" "v$$VERS"
## make html CSS="https://guilhem.org/static/css/bootstrap.min.css" BUILD_DOCDIR="$XDG_RUNTIME_DIR/Downloads"
$(HTML_FILES): $(BUILD_DOCDIR)/%.html: ./doc/%.md $(HTML_TEMPLATE)
@@ -58,4 +72,4 @@ uninstall:
clean:
rm -vf -- $(MANUAL_FILES) $(HTML_FILES)
-.PHONY: all manual html doc test install uninstall clean
+.PHONY: all manual html doc test release install uninstall clean
diff --git a/doc/build.md b/doc/build.md
index 4a4f80d..b9291f7 100644
--- a/doc/build.md
+++ b/doc/build.md
@@ -1,7 +1,7 @@
% Build instructions
% [Guilhem Moulin](mailto:guilhem@fripost.org)
-On Debian 9 (codename *Stretch*) and later, installing [`interimap`(1)]
+On Debian 10 (codename *Buster*) and later, installing [`interimap`(1)]
is a single command away:
$ sudo apt install interimap
@@ -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
@@ -84,12 +84,12 @@ Debian GNU/Linux users can also use [`gbp`(1)] from
[`git-buildpackage`](https://tracker.debian.org/pkg/git-buildpackage) in
order to build their own packages:
- $ git checkout debian
+ $ git checkout debian/latest
$ gbp buildpackage
Alternatively, for the development version:
- $ git checkout debian
+ $ git checkout debian/latest
$ git merge master
$ gbp buildpackage --git-force-create --git-upstream-tree=BRANCH
diff --git a/doc/getting-started.md b/doc/getting-started.md
index 1d059b4..83d3ba9 100644
--- a/doc/getting-started.md
+++ b/doc/getting-started.md
@@ -198,7 +198,7 @@ for the sake of clarity we start from an empty file here.
shell process doesn't linger around during the IMAP session.)
3. And finally append a `[remote]` section with your account
- information at `imap.example.org` (adapt the values accordingly):
+ information at `imap.example.net` (adapt the values accordingly):
$ cat >>${XDG_CONFIG_HOME:-~/.config}/interimap/config <<-EOF
diff --git a/doc/interimap.1.md b/doc/interimap.1.md
index 7df0100..2d2a637 100644
--- a/doc/interimap.1.md
+++ b/doc/interimap.1.md
@@ -317,7 +317,9 @@ Valid options are:
*host*
-: Server hostname, for `type=imap` and `type=imaps`.
+: Server hostname or IP address, for `type=imap` and `type=imaps`.
+ The value can optionally be enclosed in square brackets to force its
+ interpretation as an IP literal (hence skip name resolution).
(Default: `localhost`.)
*port*
@@ -327,8 +329,8 @@ Valid options are:
*proxy*
-: An optional SOCKS proxy to use for TCP connections to the IMAP
- server (`type=imap` and `type=imaps` only), formatted as
+: Optional SOCKS proxy to use for TCP connections to the IMAP server
+ (`type=imap` and `type=imaps` only), formatted as
`PROTOCOL://[USER:PASSWORD@]PROXYHOST[:PROXYPORT]`.
If `PROXYPORT` is omitted, it is assumed at port 1080.
Only [SOCKSv5][RFC 1928] is supported (with optional
@@ -418,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.
@@ -434,7 +440,14 @@ 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`.
+
+*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}
====================
@@ -568,6 +581,6 @@ A _getting started_ guide is available [there](getting-started.html).
[INI file]: https://en.wikipedia.org/wiki/INI_file
[PCRE]: https://en.wikipedia.org/wiki/Perl_Compatible_Regular_Expressions
-[`ciphers`(1ssl)]: https://www.openssl.org/docs/manmaster/apps/ciphers.html
-[`verify`(1ssl)]: https://www.openssl.org/docs/manmaster/apps/verify.html
+[`ciphers`(1ssl)]: https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html
+[`verify`(1ssl)]: https://www.openssl.org/docs/manmaster/man1/openssl-verify.html
[`doveadm-deduplicate`(1)]: https://wiki.dovecot.org/Tools/Doveadm/Deduplicate
diff --git a/doc/pullimap.1.md b/doc/pullimap.1.md
index 98ec2ef..c9500e0 100644
--- a/doc/pullimap.1.md
+++ b/doc/pullimap.1.md
@@ -139,7 +139,9 @@ Valid options are:
*host*
-: Server hostname, for `type=imap` and `type=imaps`.
+: Server hostname or IP address, for `type=imap` and `type=imaps`.
+ The value can optionally be enclosed in square brackets to force its
+ interpretation as an IP literal (hence skip name resolution).
(Default: `localhost`.)
*port*
@@ -149,8 +151,8 @@ Valid options are:
*proxy*
-: An optional SOCKS proxy to use for TCP connections to the IMAP
- server (`type=imap` and `type=imaps` only), formatted as
+: Optional SOCKS proxy to use for TCP connections to the IMAP server
+ (`type=imap` and `type=imaps` only), formatted as
`PROTOCOL://[USER:PASSWORD@]PROXYHOST[:PROXYPORT]`.
If `PROXYPORT` is omitted, it is assumed at port 1080.
Only [SOCKSv5][RFC 1928] is supported (with optional
@@ -237,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.
@@ -253,7 +259,14 @@ 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`.
+
+*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}
============
@@ -378,5 +391,5 @@ Standards
[`fetchmail`(1)]: https://www.fetchmail.info/
[`getmail`(1)]: http://pyropus.ca/software/getmail/
[`write`(2)]: https://man7.org/linux/man-pages/man2/write.2.html
-[`ciphers`(1ssl)]: https://www.openssl.org/docs/manmaster/apps/ciphers.html
-[`verify`(1ssl)]: https://www.openssl.org/docs/manmaster/apps/verify.html
+[`ciphers`(1ssl)]: https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html
+[`verify`(1ssl)]: https://www.openssl.org/docs/manmaster/man1/openssl-verify.html
diff --git a/interimap b/interimap
index 9dd197b..f8aa768 100755
--- a/interimap
+++ b/interimap
@@ -22,7 +22,7 @@ use v5.14.2;
use strict;
use warnings;
-our $VERSION = '0.5.3';
+our $VERSION = '0.5.4';
my $NAME = 'interimap';
my $DATABASE_VERSION = 1;
use Getopt::Long qw/:config posix_default no_ignore_case gnu_compat
diff --git a/interimap.sample b/interimap.sample
index 2a7b8de..b4d131c 100644
--- a/interimap.sample
+++ b/interimap.sample
@@ -1,4 +1,4 @@
-#database = imap.example.org.db
+#database = imap.example.net.db
# only consider subscribed mailboxes
list-select-opts = SUBSCRIBED
@@ -15,7 +15,7 @@ null-stderr = YES
[remote]
#type = imaps
-host = imap.example.org
+host = imap.example.net
#port = 993
#proxy = socks5h://localhost:9050
username = guilhem
diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm
index 1a71f59..fff1570 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
@@ -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/,
@@ -1446,23 +1447,39 @@ my $RE_IPv6 = do {
| (?: (?: $h16 : ){0,6} $h16 )? ::
/x };
+# Parse an IPv4 or IPv6. In list context, return a pair (IP, family),
+# otherwise only the IP. If the argument is not an IP (for instance if
+# it's a hostname), then return (undef, undef) resp. undef. The input
+# can optionaly be enclosed in square brackets which forces its
+# interpretation as an IP literal: an error is raised if it is not the
+# case.
+my $RE_IPv4_anchored = qr/\A($RE_IPv4)\z/;
+my $RE_IPv6_anchored = qr/\A($RE_IPv6)\z/;
+sub _parse_hostip($) {
+ my $v = shift // return;
+ my $literal = $v =~ s/\A\[(.*)\]\z/$1/ ? 1 : 0;
+ my ($ip, $af) = $v =~ $RE_IPv4_anchored ? ($1, AF_INET)
+ : $v =~ $RE_IPv6_anchored ? ($1, AF_INET6)
+ : (undef, undef);
+ die "Invalid IP literal: $v\n" if $literal and !defined($ip);
+ return wantarray ? ($ip, $af) : $ip;
+}
# Opens a TCP socket to the given $host and $port.
sub _tcp_connect($$$) {
my ($self, $host, $port) = @_;
my %hints = (socktype => SOCK_STREAM, protocol => IPPROTO_TCP);
- if ($host =~ qr/\A$RE_IPv4\z/) {
- $hints{family} = AF_INET;
- $hints{flags} |= AI_NUMERICHOST;
- } elsif ($host =~ qr/\A\[($RE_IPv6)\]\z/) {
- $host = $1;
- $hints{family} = AF_INET6;
+ my ($host2, $family) = _parse_hostip($host);
+ if (defined $family) {
+ $hints{family} = $family;
$hints{flags} |= AI_NUMERICHOST;
+ } else {
+ $host2 = $host;
}
- my ($err, @res) = getaddrinfo($host, $port, \%hints);
- $self->fail("Can't getaddrinfo: $err") if $err ne '';
+ my ($err, @res) = getaddrinfo($host2, $port, \%hints);
+ $self->fail("getaddrinfo($host2): $err") if $err ne '';
SOCKETS:
foreach my $ai (@res) {
@@ -1520,7 +1537,7 @@ sub _proxify($$$$) {
$port = getservbyname($port, 'tcp') // $self->fail("Can't getservbyname $port")
unless $port =~ /\A[0-9]+\z/;
- $proxy =~ /\A([A-Za-z0-9]+):\/\/(\P{Control}*\@)?($RE_IPv4|\[$RE_IPv6\]|[^:]+)(:[A-Za-z0-9]+)?\z/
+ $proxy =~ /\A([A-Za-z0-9]+):\/\/(\P{Control}*\@)?([^:]+|\[[^\]]+\])(:[A-Za-z0-9]+)?\z/
or $self->fail("Invalid proxy URI $proxy");
my ($proto, $userpass, $proxyhost, $proxyport) = ($1, $2, $3, $4);
$userpass =~ s/\@\z// if defined $userpass;
@@ -1553,23 +1570,30 @@ sub _proxify($$$$) {
$self->fail('SOCKSv5', 'No acceptable authentication methods');
}
- if ($host !~ /\A(?:$RE_IPv4|\[$RE_IPv6\])\z/ and !$resolv) {
+ my ($hostip, $fam) = _parse_hostip($host);
+ unless (defined($fam) or $resolv) {
# resove the hostname $host locally
my ($err, @res) = getaddrinfo($host, undef, {socktype => SOCK_RAW});
- $self->fail("Can't getaddrinfo: $err") if $err ne '';
- ($host) = first { defined $_ } map {
+ $self->fail("getaddrinfo($host): $err") if $err ne '';
+ my ($addr) = first { defined($_) } map {
my ($err, $ipaddr) = getnameinfo($_->{addr}, NI_NUMERICHOST, NIx_NOSERV);
- $err eq '' ? $ipaddr : undef
+ $err eq '' ? [$ipaddr,$_->{family}] : undef
} @res;
- $self->fail("Can't getnameinfo") unless defined $host;
+ $self->fail("Can't getnameinfo") unless defined $addr;
+ ($hostip, $fam) = @$addr;
}
# send a CONNECT command (CMD 0x01)
- my ($typ, $addr) =
- $host =~ /\A$RE_IPv4\z/ ? (0x01, Socket::inet_pton(AF_INET, $host))
- : ($host =~ /\A\[($RE_IPv6)\]\z/ or $host =~ /\A($RE_IPv6)\z/) ? (0x04, Socket::inet_pton(AF_INET6, $1))
- : (0x03, pack('C',length($host)).$host);
- $self->_xwrite($socket, pack('C4', $v, 0x01, 0x00, $typ).$addr.pack('n', $port));
+ my ($typ, $addr);
+ if (defined $fam) {
+ $typ = $fam == AF_INET ? 0x01 : $fam == AF_INET6 ? 0x04 : $self->panic();
+ $addr = Socket::inet_pton($fam, $hostip);
+ } else {
+ # let the SOCKS server do the resolution
+ $typ = 0x03;
+ $addr = pack('C',length($host)) . $host;
+ }
+ $self->_xwrite($socket, pack('C4', $v, 0x01, 0x00, $typ) . $addr . pack('n', $port));
($v2, my $r, my $rsv, $typ) = unpack('C4', $self->_xread($socket, 4));
$self->fail('SOCKSv5', 'Invalid protocol') unless $v == $v2 and $rsv == 0x00;
@@ -1593,7 +1617,7 @@ sub _proxify($$$$) {
return $socket;
}
else {
- $self->error("Unsupported proxy protocol $proto");
+ $self->fail("Unsupported proxy protocol $proto");
}
}
@@ -1635,6 +1659,7 @@ sub _ssl_verify($$$) {
my $pkey = Net::SSLeay::X509_get_X509_PUBKEY($cert);
if (defined $pkey and Net::SSLeay::EVP_Digest($pkey, $type) eq $digest) {
+ $self->log('Peer certificate matches pinned SPKI digest ', $algo .'$'. $fpr) if $self->{debug};
$rv = 1;
last;
}
@@ -1667,6 +1692,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();
@@ -1709,25 +1735,56 @@ 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;
+
+ my $host = $self->{host} // $self->panic();
+ my ($hostip, $hostipfam) = _parse_hostip($host);
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
+ 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");
+
+ # 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("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/pullimap b/pullimap
index baee352..86d80e0 100755
--- a/pullimap
+++ b/pullimap
@@ -22,7 +22,7 @@ use v5.20.2;
use strict;
use warnings;
-our $VERSION = '0.5.3';
+our $VERSION = '0.5.4';
my $NAME = 'pullimap';
use Errno 'EINTR';
diff --git a/tests/certs/.gitignore b/tests/certs/.gitignore
new file mode 100644
index 0000000..8b2d0ad
--- /dev/null
+++ b/tests/certs/.gitignore
@@ -0,0 +1,4 @@
+!/generate
+/*.key
+/*.crt
+/*.pem
diff --git a/tests/certs/generate b/tests/certs/generate
new file mode 100755
index 0000000..de379a0
--- /dev/null
+++ b/tests/certs/generate
@@ -0,0 +1,44 @@
+#!/bin/sh
+
+set -ue
+PATH="/usr/bin:/bin"
+export PATH
+
+BASEDIR="$(dirname -- "$0")"
+OU="InterIMAP test suite"
+cd "$BASEDIR"
+
+cadir="$(mktemp --tmpdir --directory)"
+trap 'rm -rf -- "$cadir"' EXIT INT TERM
+
+# generate CA (we intentionally throw away the private key and serial
+# file to avoid reuse)
+openssl genpkey -algorithm RSA -out "$cadir/ca.key"
+openssl req -new -x509 -rand /dev/urandom -subj "/OU=$OU/CN=Fake Root CA" -key "$cadir/ca.key" -out ./ca.crt
+
+SERIAL=1
+new() {
+ local key="$1" cn="$2"
+ openssl req -new -rand /dev/urandom -key "$key" \
+ -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" "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
+
+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/run b/tests/run
index 8164524..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
}
@@ -102,6 +102,7 @@ prepare() {
cat >>"$home/.dovecot/config" <"$TESTDIR/$u.conf"
fi
cp -aT -- "$BASEDIR/snippets/dovecot" "$home/.dovecot/conf.d"
+ cp -at "$home/.dovecot/conf.d" -- "$BASEDIR/certs/ca.crt" "$BASEDIR/certs"/dovecot.*
proto="$(env -i "${ENVIRON[@]}" doveconf -c "$home/.dovecot/config" -h protocols)"
if [ -n "$proto" ]; then
diff --git a/tests/snippets/dovecot/dovecot.ecdsa.crt b/tests/snippets/dovecot/dovecot.ecdsa.crt
deleted file mode 100644
index b928d4d..0000000
--- a/tests/snippets/dovecot/dovecot.ecdsa.crt
+++ /dev/null
@@ -1,11 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIBkjCCATmgAwIBAgIUWyEAqMhQ0uRLtagXgm68bUypQa4wCgYIKoZIzj0EAwIw
-HzEdMBsGA1UEAwwUSW50ZXJJTUFQIHRlc3Qgc3VpdGUwHhcNMjAxMjA5MTQwOTUy
-WhcNMzAxMjA3MTQwOTUyWjAfMR0wGwYDVQQDDBRJbnRlcklNQVAgdGVzdCBzdWl0
-ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFP7A0ivFsHK/WuCQzz+WWh2jBLO
-7uqhWSMh+1cc//jmn2q910XNH3xVFNkIRo7ddg6X8twli3OvC66/YIbxiTyjUzBR
-MB0GA1UdDgQWBBS/p0mJpdBjKpNrQ/t+oJMrehS7wzAfBgNVHSMEGDAWgBS/p0mJ
-pdBjKpNrQ/t+oJMrehS7wzAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0cA
-MEQCIFMlTb7E92tElIueK8TxbllJ3NOaMb1TMjSScM38N8oOAiAiNI4AkESnimPN
-IOsdnydFYjOkDEhzpXbrBEcP3EgJuQ==
------END CERTIFICATE-----
diff --git a/tests/snippets/dovecot/dovecot.ecdsa.key b/tests/snippets/dovecot/dovecot.ecdsa.key
deleted file mode 100644
index dfbd4a7..0000000
--- a/tests/snippets/dovecot/dovecot.ecdsa.key
+++ /dev/null
@@ -1,5 +0,0 @@
------BEGIN PRIVATE KEY-----
-MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgleLchaikcJbUnkps
-4ITR6FGkW2S2+S+w2ISJSsvgt0ehRANCAART+wNIrxbByv1rgkM8/llodowSzu7q
-oVkjIftXHP/45p9qvddFzR98VRTZCEaO3XYOl/LcJYtzrwuuv2CG8Yk8
------END PRIVATE KEY-----
diff --git a/tests/snippets/dovecot/dovecot.rsa.crt b/tests/snippets/dovecot/dovecot.rsa.crt
deleted file mode 100644
index d10204b..0000000
--- a/tests/snippets/dovecot/dovecot.rsa.crt
+++ /dev/null
@@ -1,19 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDHzCCAgegAwIBAgIUKSm5of13M/4NiGfhLMspFl/+YmYwDQYJKoZIhvcNAQEL
-BQAwHzEdMBsGA1UEAwwUSW50ZXJJTUFQIHRlc3Qgc3VpdGUwHhcNMjAxMjA5MTM1
-NjI1WhcNMzAxMjA3MTM1NjI1WjAfMR0wGwYDVQQDDBRJbnRlcklNQVAgdGVzdCBz
-dWl0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM1DTaSl/4UtngRG
-bAHmxHlNFZJxQVK9AM4tcYna1PGrY/JbmS5kKVFLSM6znHD5aBvTaOy0HLpF15wY
-Vj+zbaWmgqtlKGYGSGoXcTzNYFNJNB/WNhOv25q5VHNNFePTX/zOgQS8geza7qrK
-MZDiMlbuGKCQSKtZqKiEGiMWIyXtVi8BkkHXcTrvDggOTCQlk/0v8dWbGFZZA9ly
-f7PIdxtfm6tacw6Fxcz4ukWx2uoEjOIyOYhgd4WYdM7L9Jnabrh9OHYknuiGZv38
-b2GUZZ0h0RtkcdP1zOxaz4ZTaewo+gLm6yTFsL3mhnNsK/xxx00/QE6C9OyU0Nip
-gGmpT9ECAwEAAaNTMFEwHQYDVR0OBBYEFHlctzGj8GhUJ8GrlHb0mT7DR/mEMB8G
-A1UdIwQYMBaAFHlctzGj8GhUJ8GrlHb0mT7DR/mEMA8GA1UdEwEB/wQFMAMBAf8w
-DQYJKoZIhvcNAQELBQADggEBAJ/FGOVrBmYujPk2ZzJHJZE/+7+upZndrUA+27l7
-u/bHxhLnl94gfGmOaflU+Zyy/9eqLzllY40wkMT6d/SQmfv4C6d+fqk/dDPfdLdk
-N3ew/q/sPvLuEyoj1QoHamWqc3dfgV6p5j4ek6kjyWtjBPcQbVOZ02Xes1GSLzVJ
-Yo9kfbZxk4Y2mqiBDHCM+erNkG002D7cWErjj/fqhYlnjOxU+v9FEm0gLc3VqAkE
-BRuYZbmyMJUklH00R39G2Fey34kcpaB1VCMOLsymWLkZEhfgrl2qPRwGyh+Wc8N5
-gR/w97oHDOfJ2oZRzjRUB7MIhGoY0ED42Ma44Ub4al57XbY=
------END CERTIFICATE-----
diff --git a/tests/snippets/dovecot/dovecot.rsa.key b/tests/snippets/dovecot/dovecot.rsa.key
deleted file mode 100644
index ed77230..0000000
--- a/tests/snippets/dovecot/dovecot.rsa.key
+++ /dev/null
@@ -1,28 +0,0 @@
------BEGIN PRIVATE KEY-----
-MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNQ02kpf+FLZ4E
-RmwB5sR5TRWScUFSvQDOLXGJ2tTxq2PyW5kuZClRS0jOs5xw+Wgb02jstBy6Rdec
-GFY/s22lpoKrZShmBkhqF3E8zWBTSTQf1jYTr9uauVRzTRXj01/8zoEEvIHs2u6q
-yjGQ4jJW7higkEirWaiohBojFiMl7VYvAZJB13E67w4IDkwkJZP9L/HVmxhWWQPZ
-cn+zyHcbX5urWnMOhcXM+LpFsdrqBIziMjmIYHeFmHTOy/SZ2m64fTh2JJ7ohmb9
-/G9hlGWdIdEbZHHT9czsWs+GU2nsKPoC5uskxbC95oZzbCv8ccdNP0BOgvTslNDY
-qYBpqU/RAgMBAAECggEANzx5VGlnTYttDnF09z4GeS4JNBNOJNm/sbwA5bwBudcJ
-WlrT6ewCQmIkAZvL6Yr0PSiy/5+oa2gIEXVrIFFEnGMmnsDmEi52pjYQvu/1j/QP
-FtIqUznrusNMuopv7ZMgLYPUrFWeEQMJXuRyWi7EpSgFcI/jPlkuTcrezbpTUw0D
-bNAQjgXiDGNzyJDVmx496CWtTJHE94wwKo2QAiFU7zCZcqM7JlNCnRenws4KGqJ1
-qyFeCJJlgORQDMpiqaJLMreF41WPs++Xsu07RzQdmFKaS/sX4Um/5uyhlApmxepR
-cwx3RYvOtGArQKreNONn5j16O012DSbFXIyrUjJgAQKBgQD48iZmm2iiq4oC1u+/
-kYPXMHjUBHeMj8D3lA2mnCh4W/vcEj+ZBFgmR90KyYkK2eDuFslvfwzxZqU3sqJE
-au4OsITsSrxhJHAz4pVWPlJiWUCrz/7ektxY6Jw2+Jk8lR3UvLMgJbKpLWkd5Od+
-h5xKNU5Xzu198yX703k4+v5rgQKBgQDTFEcIJK9BffQ5hqMiM7NujdbMQ4ldxUy+
-ivyHk4MX4Z/kequE1rMJ1Ap8hypPJz10NhXM2naQWa8APnFYkNygwbtwTSZaiyMY
-Tav8rjYoA0CfEsPfIx2AFPtDtrhWGH5o9LZI9sjhH79ud412IDZxSGdZmAho8Xdj
-ky5sQr9MUQKBgByF+jplcg65Yt3CbMPhU17TkfSQ8nWrfuufDhVZ7RUlTO1BNgI9
-SjBQqZXz03zny+rbt4bL4trB7Qo9sHPwYIhUV1aPlZf3yddYDc5M47mbClrlQQmV
-gCO7uzJdN4mGeF2IpWl4iEj0CAhB0vhfZ1vlUa2j6vg0ZNS+vTP3JjGBAoGAQ+gW
-IgyLRWqcE5W5DdvMMhj3radcngpHclWMgKF4X0p7Aipk28umtda9uOpTNjvNjYGI
-6equkioIHu/3zyJrmFw7TRnE6QQyOjNizVvOmHjTZVnIIhVN/FLDszkpfKlMob94
-lWivn51zHLrhi8s5OKCufyhmLDzix+ol2TZwDMECgYEAwjhuZRXZeIgjKkvkG+FT
-8ThPNcxSplNca+YM9fQuWAuKkCbKCtvl8m5HDWYYIDx1jkKGHvDGtUl7vV4TtCgJ
-OeCQPjT5SLYs9ienMqitbzKfvCGRNsIG/1NsUrerD0Lau+V0YmbqYYk1Pptr3R8x
-bLzY7IMbPzdI+aPyhNF9KSg=
------END PRIVATE KEY-----
diff --git a/tests/tls-pin-fingerprint/t b/tests/tls-pin-fingerprint/t
index d3830e2..6716833 100644
--- a/tests/tls-pin-fingerprint/t
+++ b/tests/tls-pin-fingerprint/t
@@ -28,7 +28,8 @@ check_mailbox_status "INBOX"
with_remote_config <<-EOF
SSL_fingerprint = $INVALID_FPR $PKEY_SHA256
EOF
-interimap || error
+interimap --debug || error
+grep -Fx "remote: Peer certificate matches pinned SPKI digest sha256\$$PKEY_SHA256" <"$STDERR" || error
# and now an invalid one
@@ -60,13 +61,15 @@ grep -Fx "remote: ERROR: Can't initiate TLS/SSL handshake" <"$STDERR" || error
with_remote_config <<-EOF
SSL_fingerprint = sha256\$$PKEY_SHA256 $INVALID_FPR
EOF
-interimap || error
+interimap --debug || error
+grep -Fx "remote: Peer certificate matches pinned SPKI digest sha256\$$PKEY_SHA256" <"$STDERR" || error
# invalid + valid
with_remote_config <<-EOF
SSL_fingerprint = $INVALID_FPR sha256\$$PKEY_SHA256
EOF
-interimap || error
+interimap --debug || error
+grep -Fx "remote: Peer certificate matches pinned SPKI digest sha256\$$PKEY_SHA256" <"$STDERR" || error
# vim: set filetype=sh :
diff --git a/tests/tls-rsa+ecdsa/t b/tests/tls-rsa+ecdsa/t
index 29352e9..2adf930 100644
--- a/tests/tls-rsa+ecdsa/t
+++ b/tests/tls-rsa+ecdsa/t
@@ -32,6 +32,9 @@ interimap --debug || error
grep -Fx -e "remote: Peer certificate fingerprint: sha256\$$X509_SHA256" \
-e "remote: Peer certificate fingerprint: sha256\$$X509_ALT_SHA256" \
<"$STDERR" || error
+grep -Fx -e "remote: Peer certificate matches pinned SPKI digest sha256\$$PKEY_SHA256" \
+ -e "remote: Peer certificate matches pinned SPKI digest sha256\$$PKEY_ALT_SHA256" \
+ <"$STDERR" || error
# force RSA (XXX do we really have to force TLSv1.2 here?)
cat >>"$XDG_CONFIG_HOME/interimap/config" <<-EOF
@@ -40,10 +43,12 @@ cat >>"$XDG_CONFIG_HOME/interimap/config" <<-EOF
EOF
interimap --debug || error
grep -Fx "remote: Peer certificate fingerprint: sha256\$$X509_SHA256" <"$STDERR" || error
+grep -Fx "remote: Peer certificate matches pinned SPKI digest sha256\$$PKEY_SHA256" <"$STDERR" || error
# force ECDSA
-sed -i "s/^SSL_cipherlist\\s*=.*/SSL_cipherlist = EECDH+AESGCM+aECDSA/" "$XDG_CONFIG_HOME/interimap/config"
+sed -i "s/^SSL_cipherlist\\s*=.*/SSL_cipherlist = EECDH+AESGCM+aECDSA/" -- "$XDG_CONFIG_HOME/interimap/config"
interimap --debug || error
grep -Fx "remote: Peer certificate fingerprint: sha256\$$X509_ALT_SHA256" <"$STDERR" || error
+grep -Fx "remote: Peer certificate matches pinned SPKI digest sha256\$$PKEY_ALT_SHA256" <"$STDERR" || error
# vim: set filetype=sh :
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 :
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 9e4d9fa..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
@@ -28,9 +38,9 @@ verified_peer() {
}
# backup config
-install -m0600 "$XDG_CONFIG_HOME/interimap/config" "$XDG_CONFIG_HOME/interimap/config~"
+install -m0600 -- "$XDG_CONFIG_HOME/interimap/config" "$XDG_CONFIG_HOME/interimap/config~"
with_remote_config() {
- install -m0600 "$XDG_CONFIG_HOME/interimap/config~" "$XDG_CONFIG_HOME/interimap/config"
+ install -m0600 -- "$XDG_CONFIG_HOME/interimap/config~" "$XDG_CONFIG_HOME/interimap/config"
cat >>"$XDG_CONFIG_HOME/interimap/config"
}
@@ -39,42 +49,79 @@ 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
unverified_peer
-! grep -Fx "remote: WARNING: Fingerprint doesn't match! MiTM in action?" <"$STDERR" || error
+grep -Fx "remote: Peer certificate matches pinned SPKI digest sha256\$$PKEY_SHA256" <"$STDERR" || error
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 self-signed test cert 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
-doveconf -c "$HOME_remote/.dovecot/config" -hx ssl_cert >"$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 self-signed test cert 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 :