From 77cddf469dcef093f35bd63121ef3b74c4f7434d Mon Sep 17 00:00:00 2001 From: Sawyer X Date: Wed, 1 Apr 2015 22:34:35 +0200 Subject: [PATCH] Add SSL SNI certificates support: A new feature: this adds the ability to support multiple SSL certificates (for multiple domains) under a single IP address. If you provide "ssl_sni_dir", it will read the files inside it, expecting each file to contain both the key and certificate(s) for each time. This means it doesn't matter what the filenames are, as long as they contain both the key and certificate(s) inside them. Then it reads the certificates for the domain name (commonName) and the alternative ones (while removing duplicates) and sets up the proper structure for IO::Socket::SSL. This uses IO::Socket::SSL::Utils (loaded lazily without importing new subroutines), so it doesn't add any new dependencies. If you provide both ssl_cert_file/ssl_key_file and ssl_sni_dir, the latter will win silently. Something to consider: ignoring dotfiles in the directory. --- lib/Perlbal/Service.pm | 43 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/Perlbal/Service.pm b/lib/Perlbal/Service.pm index 3e87dd8..d37a0c9 100644 --- a/lib/Perlbal/Service.pm +++ b/lib/Perlbal/Service.pm @@ -94,6 +94,7 @@ use fields ( 'enable_ssl', # bool: whether this service speaks SSL to the client 'ssl_key_file', # file: path to key pem file 'ssl_cert_file', # file: path to key pem file + 'ssl_sni_dir', # directory: path to certificate PEM directory (used for SNI) 'ssl_cipher_list', # OpenSSL cipher list string 'ssl_ca_path', # directory: path to certificates 'ssl_verify_mode', # int: verification mode, see IO::Socket::SSL documentation @@ -597,6 +598,13 @@ our $tunables = { check_role => "*", }, + 'ssl_sni_dir' => { + des => "Path to the PEM certificate and key files directory SSL SNI.", + default => undef, + check_type => "directory_or_none", + check_role => "*", + }, + 'ssl_cipher_list' => { des => "OpenSSL-style cipher list.", default => "ALL:!LOW:!EXP", @@ -1653,8 +1661,39 @@ sub enable { (defined $self->{ssl_honor_cipher_order} ? (SSL_honor_cipher_order => $self->{ssl_honor_cipher_order}) : ()), }; return $mc->err("IO::Socket:SSL (0.98+) not available. Can't do SSL.") unless eval "use IO::Socket::SSL 0.98 (); 1;"; - return $mc->err("SSL key file ($self->{ssl_key_file}) doesn't exist") unless -f $self->{ssl_key_file}; - return $mc->err("SSL cert file ($self->{ssl_cert_file}) doesn't exist") unless -f $self->{ssl_cert_file}; + + if ( ! $self->{'ssl_sni_dir'} ) { + return $mc->err("SSL key file ($self->{ssl_key_file}) doesn't exist") unless -f $self->{ssl_key_file}; + return $mc->err("SSL cert file ($self->{ssl_cert_file}) doesn't exist") unless -f $self->{ssl_cert_file}; + } else { + IO::Socket::SSL->can_server_sni + or return $mc->err("SSL SNI not supported"); + + require IO::Socket::SSL::Utils; + my $dir = $self->{'ssl_sni_dir'}; + opendir my $dh, $dir + or die "Could not open SSL SNI directory: $!\n"; + + my @files = grep -f File::Spec->catfile( $dir, $_ ), readdir $dh; + + closedir $dh + or die "Could not close SSL SNI directory: $!\n"; + + $opts->{'ssl'}{'SSL_cert_file'} = {}; + $opts->{'ssl'}{'SSL_key_file'} = {}; + + foreach my $file (@files) { + my $path = File::Spec->catfile( $dir, $file ); + my $cert = IO::Socket::SSL::Utils::PEM_file2cert($path); + my $data = IO::Socket::SSL::Utils::CERT_asHash($cert); + IO::Socket::SSL::Utils::CERT_free($cert); + + $opts->{'ssl'}{'SSL_cert_file'}{$_} = + $opts->{'ssl'}{'SSL_key_file'}{$_} = + $path for $data->{'subject'}{'commonName'}, + map $_->[1], @{ $data->{'subjectAltNames'} }; + } + } } my $tl = Perlbal::TCPListener->new($self->{listen}, $self, $opts);