aboutsummaryrefslogtreecommitdiffstats
path: root/lib/Net/IMAP/InterIMAP.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Net/IMAP/InterIMAP.pm')
-rw-r--r--lib/Net/IMAP/InterIMAP.pm289
1 files changed, 192 insertions, 97 deletions
diff --git a/lib/Net/IMAP/InterIMAP.pm b/lib/Net/IMAP/InterIMAP.pm
index 8b69e12..55a18a0 100644
--- a/lib/Net/IMAP/InterIMAP.pm
+++ b/lib/Net/IMAP/InterIMAP.pm
@@ -1,6 +1,6 @@
#----------------------------------------------------------------------
# A minimal IMAP4 client for QRESYNC-capable servers
-# Copyright © 2015-2019 Guilhem Moulin <guilhem@fripost.org>
+# Copyright © 2015-2022 Guilhem Moulin <guilhem@fripost.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -16,7 +16,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#----------------------------------------------------------------------
-package Net::IMAP::InterIMAP v0.0.5;
+package Net::IMAP::InterIMAP v0.5.7;
use v5.20.0;
use warnings;
use strict;
@@ -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.86_06 ();
use List::Util qw/all first/;
use POSIX ':signal_h';
use Socket qw/SOCK_STREAM SOCK_RAW SOCK_CLOEXEC IPPROTO_TCP SHUT_RDWR
@@ -62,9 +62,13 @@ my %OPTIONS = (
command => qr/\A(\P{Control}+)\z/,
'null-stderr' => qr/\A(YES|NO)\z/i,
compress => qr/\A(YES|NO)\z/i,
- SSL_protocols => qr/\A(!?$RE_SSL_PROTO(?: !?$RE_SSL_PROTO)*)\z/,
- SSL_fingerprint => qr/\A((?:[A-Za-z0-9]+\$)?\p{AHex}+)\z/,
+ SSL_protocols => qr/\A(!?$RE_SSL_PROTO(?: !?$RE_SSL_PROTO)*)\z/, # TODO deprecated, remove in 0.6
+ SSL_protocol_min => qr/\A(\P{Control}+)\z/,
+ SSL_protocol_max => qr/\A(\P{Control}+)\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_ciphersuites => qr/\A(\P{Control}*)\z/, # "an empty list is permissible"
+ 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/,
@@ -324,19 +328,19 @@ sub new($%) {
my $pid = fork // $self->panic("fork: $!");
unless ($pid) {
# children
- close($self->{S}) or $self->panic("Can't close: $!");
- open STDIN, '<&', $s or $self->panic("Can't dup: $!");
- open STDOUT, '>&', $s or $self->panic("Can't dup: $!");
+ close($self->{S}) or $self->panic("close: $!");
+ open STDIN, '<&', $s or $self->panic("dup: $!");
+ open STDOUT, '>&', $s or $self->panic("dup: $!");
my $stderr2;
- if ($self->{'null-stderr'} // 0) {
+ if (($self->{'null-stderr'} // 0) and !($self->{debug} // 0)) {
open $stderr2, '>&', *STDERR;
- open STDERR, '>', '/dev/null' or $self->panic("Can't open /dev/null: $!");
+ open STDERR, '>', '/dev/null' or $self->panic("open(/dev/null): $!");
}
my $sigset = POSIX::SigSet::->new(SIGINT);
my $oldsigset = POSIX::SigSet::->new();
- sigprocmask(SIG_BLOCK, $sigset, $oldsigset) // $self->panic("Can't block SIGINT: $!");
+ sigprocmask(SIG_BLOCK, $sigset, $oldsigset) // $self->panic("sigprocmask: $!");
unless (exec $command) {
my $err = $!;
@@ -344,12 +348,12 @@ sub new($%) {
close STDERR;
open STDERR, '>&', $stderr2;
}
- $self->panic("Can't exec: $err");
+ $self->panic("exec: $err");
}
}
# parent
- close($s) or $self->panic("Can't close: $!");
+ close($s) or $self->panic("close: $!");
}
else {
foreach (qw/host port/) {
@@ -359,9 +363,9 @@ sub new($%) {
: $self->_tcp_connect(@$self{qw/host port/});
if (defined $self->{keepalive}) {
setsockopt($self->{S}, Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
- or $self->fail("Can't setsockopt SO_KEEPALIVE: $!");
+ or $self->fail("setsockopt SO_KEEPALIVE: $!");
setsockopt($self->{S}, Socket::IPPROTO_TCP, Socket::TCP_KEEPIDLE, 60)
- or $self->fail("Can't setsockopt TCP_KEEPIDLE: $!");
+ or $self->fail("setsockopt TCP_KEEPIDLE: $!");
}
}
@@ -1308,7 +1312,8 @@ sub pull_new_messages($$&@) {
# 2^32-1: don't use '*' since the highest UID can be known already
$range .= "$since:4294967295";
- $UIDNEXT = $cache->{UIDNEXT} // $self->panic(); # sanity check
+ $UIDNEXT = $cache->{UIDNEXT} //
+ $self->panic("Unknown UIDNEXT value - non-compliant server?");
$self->fetch($range, "($attrs)", sub($) {
my $mail = shift;
$UIDNEXT = $mail->{UID} + 1 if $UIDNEXT <= $mail->{UID};
@@ -1367,7 +1372,7 @@ sub push_flag_updates($$@) {
$modified->{$uid} //= [ 0, undef ];
} elsif (defined (my $m = $modified->{$uid})) {
# received an untagged FETCH response, remove from the list of pending changes
- # if the flag list was up to date (either implicitely or explicitely)
+ # if the flag list was up to date (either implicitely or explicitly)
if (!defined $m->[1] or $m->[1] eq $flags) {
delete $modified->{$uid};
push @ok, $uid;
@@ -1446,23 +1451,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) {
@@ -1473,9 +1494,9 @@ sub _tcp_connect($$$) {
# https://stackoverflow.com/questions/8284243/how-do-i-set-so-rcvtimeo-on-a-socket-in-perl
my $timeout = pack('l!l!', 30, 0);
setsockopt($s, Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, $timeout)
- or $self->fail("Can't setsockopt SO_RCVTIMEO: $!");
+ or $self->fail("setsockopt SO_RCVTIMEO: $!");
setsockopt($s, Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, $timeout)
- or $self->fail("Can't setsockopt SO_RCVTIMEO: $!");
+ or $self->fail("setsockopt SO_RCVTIMEO: $!");
until (connect($s, $ai->{addr})) {
next if $! == EINTR; # try again if connect(2) was interrupted by a signal
@@ -1492,7 +1513,7 @@ sub _xwrite($$$) {
while ($length > 0) {
my $n = syswrite($_[0], $_[1], $length, $offset);
- $self->fail("Can't write: $!") unless defined $n and $n > 0;
+ $self->fail("write: $!") unless defined $n and $n > 0;
$offset += $n;
$length -= $n;
}
@@ -1504,7 +1525,7 @@ sub _xread($$$) {
my $offset = 0;
my $buf;
while ($length > 0) {
- my $n = sysread($fh, $buf, $length, $offset) // $self->fail("Can't read: $!");
+ my $n = sysread($fh, $buf, $length, $offset) // $self->fail("read: $!");
$self->fail("0 bytes read (got EOF)") unless $n > 0; # EOF
$offset += $n;
$length -= $n;
@@ -1520,7 +1541,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 +1574,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("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,14 +1621,14 @@ sub _proxify($$$$) {
return $socket;
}
else {
- $self->error("Unsupported proxy protocol $proto");
+ $self->fail("Unsupported proxy protocol $proto");
}
}
# $self->_ssl_verify($self, $preverify_ok, $x509_ctx)
# SSL verify callback function, see
-# https://www.openssl.org/docs/manmaster/ssl/SSL_CTX_set_verify.html
+# https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_verify.html
sub _ssl_verify($$$) {
my ($self, $ok, $x509_ctx) = @_;
return 0 unless $x509_ctx; # reject
@@ -1614,7 +1642,7 @@ sub _ssl_verify($$$) {
$self->log(' Subject Name: ', Net::SSLeay::X509_NAME_oneline(Net::SSLeay::X509_get_subject_name($cert)));
}
- $ok = 1 unless $self->{SSL_verify} // 1;
+ $ok = 1 unless $self->{SSL_verify} // die; # safety check, always set
if ($depth == 0 and !exists $self->{_SSL_PEER_VERIFIED}) {
if ($self->{debug}) {
my $algo = 'sha256';
@@ -1624,15 +1652,23 @@ sub _ssl_verify($$$) {
.$algo.'$'.unpack('H*', Net::SSLeay::X509_digest($cert, $type)));
}
- if (defined (my $fpr = $self->{SSL_fingerprint})) {
- (my $algo, $fpr) = $fpr =~ /^([^\$]+)\$(.*)/ ? ($1, $2) : ('sha256', $fpr);
- my $digest = pack 'H*', ($fpr =~ tr/://rd);
+ if (defined (my $fprs = $self->{SSL_fingerprint})) {
+ my $rv = 0;
+ foreach my $fpr (split /\s+/, $fprs) {
+ (my $algo, $fpr) = $fpr =~ /^([^\$]+)\$(.*)/ ? ($1, $2) : ('sha256', $fpr);
+ my $digest = pack 'H*', ($fpr =~ tr/://rd);
- my $type = Net::SSLeay::EVP_get_digestbyname($algo)
- or $self->_ssl_error("Can't find MD value for name '$algo'");
+ my $type = Net::SSLeay::EVP_get_digestbyname($algo)
+ or $self->_ssl_error("Can't find MD value for name '$algo'");
- my $pkey = Net::SSLeay::X509_get_X509_PUBKEY($cert);
- unless (defined $pkey and Net::SSLeay::EVP_Digest($pkey, $type) eq $digest) {
+ 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;
+ }
+ }
+ unless ($rv) {
$self->warn("Fingerprint doesn't match! MiTM in action?");
$ok = 0;
}
@@ -1643,7 +1679,7 @@ sub _ssl_verify($$$) {
}
my %SSL_proto;
-BEGIN {
+BEGIN { # TODO deprecated, remove in 0.6
sub _append_ssl_proto($$) {
my ($k, $v) = @_;
$SSL_proto{$k} = $v if defined $v;
@@ -1656,82 +1692,141 @@ BEGIN {
_append_ssl_proto( "TLSv1.3", eval { Net::SSLeay::OP_NO_TLSv1_3() } );
}
+# see ssl/ssl_conf.c:protocol_from_string() in the OpenSSL source tree
+my %SSL_protocol_versions = (
+ "SSLv3" => eval { Net::SSLeay::SSL3_VERSION() }
+ , "TLSv1" => eval { Net::SSLeay::TLS1_VERSION() }
+ , "TLSv1.1" => eval { Net::SSLeay::TLS1_1_VERSION() }
+ , "TLSv1.2" => eval { Net::SSLeay::TLS1_2_VERSION() }
+ , "TLSv1.3" => eval { Net::SSLeay::TLS1_3_VERSION() }
+);
+
# $self->_start_ssl($socket)
# Upgrade the $socket to SSL/TLS.
sub _start_ssl($$) {
my ($self, $socket) = @_;
- 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();
+ # need OpenSSL 1.1.0 or later for SSL_CTX_set_min_proto_version(3ssl), see
+ # https://www.openssl.org/docs/man1.1.0/man3/SSL_CTX_set_min_proto_version.html
+ $self->panic("SSL/TLS functions require OpenSSL 1.1.0 or later")
+ if Net::SSLeay::OPENSSL_VERSION_NUMBER() < 0x1010000f;
+
+ my $ctx = Net::SSLeay::CTX_new() or $self->panic("SSL_CTX_new(): $!");
+ $self->{SSL_verify} //= 1; # default is to perform certificate verification
if (defined $self->{_OUTBUF} and $self->{_OUTBUF} ne '') {
$self->warn("Truncating non-empty output buffer (unauthenticated response injection?)");
undef $self->{_OUTBUF};
}
- $self->{SSL_protocols} //= q{!SSLv2 !SSLv3 !TLSv1 !TLSv1.1};
- my ($proto_include, $proto_exclude) = (0, 0);
- foreach (split /\s+/, $self->{SSL_protocols}) {
- my $neg = s/^!// ? 1 : 0;
- s/\.0$//;
- ($neg ? $proto_exclude : $proto_include) |= $SSL_proto{$_} // $self->panic("Unknown SSL protocol: $_");
- }
- if ($proto_include != 0) {
- # exclude all protocols except those explictly included
- my $x = 0;
- $x |= $_ foreach values %SSL_proto;
- $x &= ~ $proto_include;
- $proto_exclude |= $x;
- }
- my @proto_exclude = grep { ($proto_exclude & $SSL_proto{$_}) != 0 } keys %SSL_proto;
- $self->log("Disabling SSL protocols: ".join(', ', sort @proto_exclude)) if $self->{debug};
- $ssl_options |= $SSL_proto{$_} foreach @proto_exclude;
+ my $ssl_options = Net::SSLeay::OP_SINGLE_DH_USE() | Net::SSLeay::OP_SINGLE_ECDH_USE();
$ssl_options |= Net::SSLeay::OP_NO_COMPRESSION();
- # https://www.openssl.org/docs/manmaster/ssl/SSL_CTX_set_options.html
+ if (defined $self->{SSL_protocol_min} or defined $self->{SSL_protocol_max}) {
+ my ($min, $max) = @$self{qw/SSL_protocol_min SSL_protocol_max/};
+ if (defined $min) {
+ my $v = $SSL_protocol_versions{$min} // $self->panic("Unknown protocol version: $min");
+ $self->_ssl_error("CTX_set_min_proto_version()") unless Net::SSLeay::CTX_set_min_proto_version($ctx, $v) == 1;
+ $self->log("Minimum SSL/TLS protocol version: ", $min) if $self->{debug};
+ }
+ if (defined $max) {
+ my $v = $SSL_protocol_versions{$max} // $self->panic("Unknown protocol version: $max");
+ $self->_ssl_error("CTX_set_max_proto_version()") unless Net::SSLeay::CTX_set_max_proto_version($ctx, $v) == 1;
+ $self->log("Maximum SSL/TLS protocol version: ", $max) if $self->{debug};
+ }
+ } elsif (defined (my $protos = $self->{SSL_protocols})) { # TODO deprecated, remove in 0.6
+ $self->warn("SSL_protocols is deprecated and will be removed in a future release! " .
+ "Use SSL_protocol_{min,max} instead.");
+ my ($proto_include, $proto_exclude) = (0, 0);
+ foreach (split /\s+/, $protos) {
+ my $neg = s/^!// ? 1 : 0;
+ s/\.0$//;
+ ($neg ? $proto_exclude : $proto_include) |= $SSL_proto{$_} // $self->panic("Unknown SSL protocol: $_");
+ }
+ if ($proto_include != 0) {
+ # exclude all protocols except those explictly included
+ my $x = 0;
+ $x |= $_ foreach values %SSL_proto;
+ $x &= ~ $proto_include;
+ $proto_exclude |= $x;
+ }
+ my @proto_exclude = grep { ($proto_exclude & $SSL_proto{$_}) != 0 } keys %SSL_proto;
+ $self->log("Disabling SSL protocols: ".join(', ', sort @proto_exclude)) if $self->{debug};
+ $ssl_options |= $SSL_proto{$_} foreach @proto_exclude;
+ }
+
+ # https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_options.html
+ # TODO 0.6: move SSL_CTX_set_options() and SSL_CTX_set_mode() before SSL_CTX_set_{min,max}_proto_version()
Net::SSLeay::CTX_set_options($ctx, $ssl_options);
- # https://www.openssl.org/docs/manmaster/ssl/SSL_CTX_set_mode.html
+ # https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_mode.html
Net::SSLeay::CTX_set_mode($ctx,
Net::SSLeay::MODE_ENABLE_PARTIAL_WRITE() |
Net::SSLeay::MODE_ACCEPT_MOVING_WRITE_BUFFER() |
Net::SSLeay::MODE_AUTO_RETRY() | # don't fail SSL_read on renegotiation
Net::SSLeay::MODE_RELEASE_BUFFERS() );
- if (defined (my $ciphers = $self->{SSL_cipherlist})) {
- Net::SSLeay::CTX_set_cipher_list($ctx, $ciphers)
- or $self->_ssl_error("Can't set cipher list");
+ if (defined (my $str = $self->{SSL_cipherlist})) {
+ $self->_ssl_error("SSL_CTX_set_cipher_list()") unless Net::SSLeay::CTX_set_cipher_list($ctx, $str) == 1;
}
+ if (defined (my $str = $self->{SSL_ciphersuites})) {
+ $self->_ssl_error("SSL_CTX_set_ciphersuites()") unless Net::SSLeay::CTX_set_ciphersuites($ctx, $str) == 1;
+ }
+
+ my $vpm = Net::SSLeay::X509_VERIFY_PARAM_new() or $self->_ssl_error("X509_VERIFY_PARAM_new()");
+ my $purpose = Net::SSLeay::X509_PURPOSE_SSL_SERVER();
+ $self->_ssl_error("X509_VERIFY_PARAM_set_purpose()") unless Net::SSLeay::X509_VERIFY_PARAM_set_purpose($vpm, $purpose) == 1;
+ $self->_ssl_error("CTX_set_purpose()") unless Net::SSLeay::CTX_set_purpose($ctx, $purpose) == 1;
- if ($self->{SSL_verify} // 1) {
- # verify the certificate chain
- 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");
+ my $host = $self->{host} // $self->panic();
+ my ($hostip, $hostipfam) = _parse_hostip($host);
+ if ($self->{SSL_verify}) {
+ # verify certificate chain
+ if (defined $self->{SSL_CAfile} or defined $self->{SSL_CApath}) {
+ $self->_ssl_error("SSL_CTX_load_verify_locations()")
+ unless Net::SSLeay::CTX_load_verify_locations($ctx,
+ $self->{SSL_CAfile} // '', $self->{SSL_CApath} // '') == 1;
+ } else {
+ $self->log("Using default locations for trusted CA certificates") if $self->{debug};
+ $self->_ssl_error("SSL_CTX_set_default_verify_paths()")
+ unless Net::SSLeay::CTX_set_default_verify_paths($ctx) == 1;
+ }
+
+ # 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("SSL_new()");
+ $self->fail("SSL_set_fd()") unless Net::SSLeay::set_fd($ssl, fileno($socket)) == 1;
+
+ # always use 'SSL_hostname' when set, otherwise use 'host' (unless it's an IP)
+ my $servername = $self->{SSL_hostname} // (defined $hostipfam ? "" : $host);
+ if ($servername ne "") {
+ $self->_ssl_error("SSL_set_tlsext_host_name($servername)")
+ unless Net::SSLeay::set_tlsext_host_name($ssl, $servername) == 1;
+ $self->log("Using SNI with name $servername") if $self->{debug};
+ }
- my $ssl = Net::SSLeay::new($ctx) or $self->fail("Can't create new SSL structure");
- Net::SSLeay::set_fd($ssl, fileno $socket) or $self->fail("SSL filehandle association failed");
$self->_ssl_error("Can't initiate TLS/SSL handshake") unless Net::SSLeay::connect($ssl) == 1;
- $self->panic("Couldn't verify") unless $self->{_SSL_PEER_VERIFIED}; # sanity check
+ $self->panic() unless $self->{_SSL_PEER_VERIFIED}; # sanity check
+ $self->panic() if $self->{SSL_verify} 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);
- $self->log(sprintf('SSL protocol: %s (0x%x)', ($v == 0x0002 ? 'SSLv2' :
- $v == 0x0300 ? 'SSLv3' :
- $v == 0x0301 ? 'TLSv1' :
- $v == 0x0302 ? 'TLSv1.1' :
- $v == 0x0303 ? 'TLSv1.2' :
- $v == 0x0304 ? 'TLSv1.3' :
- '??'),
- $v));
+ $self->log(sprintf('SSL protocol: %s (0x%x)',
+ , Net::SSLeay::get_version($ssl)
+ , Net::SSLeay::version($ssl)));
$self->log(sprintf('SSL cipher: %s (%d bits)'
, Net::SSLeay::get_cipher($ssl)
, Net::SSLeay::get_cipher_bits($ssl)));
@@ -1766,7 +1861,7 @@ sub _getline($;$) {
$n = sysread($stdout, $buf, $BUFSIZE, 0);
}
- $self->_ssl_error("Can't read: $!") unless defined $n;
+ $self->_ssl_error("read: $!") unless defined $n;
$self->_ssl_error("0 bytes read (got EOF)") unless $n > 0; # EOF
$self->{_OUTRAWCOUNT} += $n;
@@ -1979,7 +2074,7 @@ sub _cmd_flush($;$$) {
my $written = defined $ssl ?
Net::SSLeay::write_partial($ssl, $offset, $length, $self->{_INBUF}) :
syswrite($stdin, $self->{_INBUF}, $length, $offset);
- $self->_ssl_error("Can't write: $!") unless defined $written and $written > 0;
+ $self->_ssl_error("write: $!") unless defined $written and $written > 0;
$offset += $written;
$length -= $written;