Subject: | SSL_reuse_ctx is missing certain options |
I am submitting a patch to Net::FTPSSL to correct an issue when using the SSL_Client_Certificate option to trigger both the SSL_reuse_ctx option and the certificate and hostname verification options of IO::Socket::SSL.
A particular vendor that I have to deal with runs an FTPS server of unknown origin. (They've overridden the connect messages, so I can't tell what program their server is running.) I have a Perl script that automatically uploads a file to their server. They recently changed their server settings due to the POODLE vulnerability, and they only support TLS ciphers now. Once their server settings changed, the upload stopped functioning. My script could log in to the site, but as soon as it opened the data channel to upload the file, the connection was dropped by their server immediately.
In the initial connection settings, I am forcing a TLS connection by setting useSSL => 0. When the script first logged in, it did indeed do a TLS handshake and connect to the server just fine, with my client offering 20 TLS ciphers for the server to choose from. When it came time to open the data channel to transfer the file, however, the data channel reverted to an SSL handshake instead of a TLS handshake, and my client offered 26 ciphers instead of the original 20 ciphers, with the extra 6 ciphers being all SSLv2 ciphers for some reason (not even SSLv3). Their server settings are set to reject SSL ciphers now, so as soon as their server saw the extra old SSLv2 ciphers, they dropped the connection.
I eventually found the setting in your module for SSL_Client_Certificate. Even using an empty hash causes your module to use the SSL_reuse_ctx option in IO::Socket::SSL. As soon as I set this value, then the script started to work properly again. The data channel reused the SSL context of the command channel, so it did a TLS handshake and offered the same 20 ciphers that the command channel did, instead of the behavior mentioned above.
By itself, this works great, and it solved my issue. When I discovered that no hostname verification was going on, however, I added the IO::Socket::SSL settings for hostname verification, and that's when things started to break.
Apparently, reusing the SSL context does not reuse all of the initial settings, but only "context-related" settings, whatever those are (according to the IO::Socket::SSL documentation). Also apparently, the settings for SSL_ca_path, SSL_verifycn_name, and SSL_verifycn_scheme (which I am using for hostname verification) are not included in the "context-related" settings. So while the context is indeed being reused for the data channel, by dropping the path to the CA certificate file, the data channel connection fails because it cannot properly verify the certificate anymore.
According to the IO::Socket::SSL documentation, you can send additional settings when reusing an SSL context, but any settings inside the context itself will override any duplicate settings also sent separately, so there should be no danger from sending a setting twice. And since the SSL_ca_path, SSL_verifycn_name, and SSL_verifycn_scheme settings are not inside the SSL context, those need to be re-sent separately when opening the data channel or else they will not be present at all.
Also, when your module prints an error message inside _get_data_channel, it was trying to print the contents of the variable $ssl_opts{SSL_version}, but this variable is not present when reusing an SSL context.
To fix everything, I added a simple patch to your code. If the arguments for SSL_version, SSL_ca_path, SSL_verifycn_name, and SSL_verifycn_scheme are present, they are added into the %ssl_reuse hash used for context reuse, so that they will be present if the context is indeed reused later.
To test everything out, I set up my own FTPS-I server internally, running vsftpd 2.2.2 on Red Hat Enterprise Linux 6. I created my own SSL certificate for the server, using my company's private certificate authority, and I added this private CA certificate to the certificate bundle used by my Perl script so that it would validate successfully. Once I applied the simple patch to your code, then both certificate verification and hostname verification worked properly, and the data channel opened successfully and transferred the file. Without the patch, the command channel would open successfully, but certificate verification would fail when opening the data channel because IO::Socket::SSL had lost the path to the CA certificate file and the settings for hostname verification were also missing.
FYI, the server I was running the script on was Red Hat Enterprise Linux 5.11, with Perl 5.8.8, Net::FTPSSL 0.25, and IO::Socket::SSL 2.008.
Thanks,
Chris Thompson
Subject: | Net-FTPSSL-0.25.patch |
diff -rupN Net-FTPSSL-0.25/FTPSSL.pm Net-FTPSSL-0.25-new/FTPSSL.pm
--- Net-FTPSSL-0.25/FTPSSL.pm 2014-09-05 13:24:16.000000000 -0500
+++ Net-FTPSSL-0.25-new/FTPSSL.pm 2015-01-06 18:52:42.000000000 -0600
@@ -352,6 +352,18 @@ sub new {
(ref ($arg->{SSL_Advanced}) eq "HASH") ) {
# Reuse the command channel context ...
my %ssl_reuse = ( SSL_reuse_ctx => ${*$obj}{_SSL_ctx} );
+ if (defined($ssl_args{SSL_version})) {
+ $ssl_reuse{SSL_version} = $ssl_args{SSL_version};
+ }
+ if (defined($ssl_args{SSL_ca_file})) {
+ $ssl_reuse{SSL_ca_file} = $ssl_args{SSL_ca_file};
+ }
+ if (defined($ssl_args{SSL_verifycn_name})) {
+ $ssl_reuse{SSL_verifycn_name} = $ssl_args{SSL_verifycn_name};
+ }
+ if (defined($ssl_args{SSL_verifycn_scheme})) {
+ $ssl_reuse{SSL_verifycn_scheme} = $ssl_args{SSL_verifycn_scheme};
+ }
${*$obj}{myContext} = \%ssl_reuse;
}