diff options
-rw-r--r-- | Changelog | 26 | ||||
-rw-r--r-- | Makefile | 18 | ||||
-rw-r--r-- | doc/build.md | 8 | ||||
-rw-r--r-- | doc/getting-started.md | 2 | ||||
-rw-r--r-- | doc/interimap.1.md | 33 | ||||
-rw-r--r-- | doc/pullimap.1.md | 33 | ||||
-rwxr-xr-x | interimap | 2 | ||||
-rw-r--r-- | interimap.sample | 4 | ||||
-rw-r--r-- | lib/Net/IMAP/InterIMAP.pm | 107 | ||||
-rwxr-xr-x | pullimap | 2 | ||||
-rw-r--r-- | tests/certs/.gitignore | 4 | ||||
-rwxr-xr-x | tests/certs/generate | 44 | ||||
-rw-r--r-- | tests/list | 3 | ||||
-rwxr-xr-x | tests/run | 3 | ||||
-rw-r--r-- | tests/snippets/dovecot/dovecot.ecdsa.crt | 11 | ||||
-rw-r--r-- | tests/snippets/dovecot/dovecot.ecdsa.key | 5 | ||||
-rw-r--r-- | tests/snippets/dovecot/dovecot.rsa.crt | 19 | ||||
-rw-r--r-- | tests/snippets/dovecot/dovecot.rsa.key | 28 | ||||
-rw-r--r-- | tests/tls-pin-fingerprint/t | 9 | ||||
-rw-r--r-- | tests/tls-rsa+ecdsa/t | 7 | ||||
-rw-r--r-- | tests/tls-sni/interimap.remote | 3 | ||||
-rw-r--r-- | tests/tls-sni/remote.conf | 7 | ||||
-rw-r--r-- | tests/tls-sni/t | 66 | ||||
-rw-r--r-- | tests/tls-verify-peer/interimap.remote | 1 | ||||
-rw-r--r-- | tests/tls-verify-peer/t | 67 |
25 files changed, 376 insertions, 136 deletions
@@ -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 @@ -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 @@ -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); @@ -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 @@ -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) @@ -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 : |