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 :  | 
