#! @PERL@
# Copyright (c) 2010-2012 Zmanda Inc. All Rights Reserved.
#
# 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 the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Contact information: Carbonite Inc., 756 N Pastoria Ave
# Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
use lib '@amperldir@';
use strict;
use warnings;
use Getopt::Long;
use POSIX qw(WIFEXITED WEXITSTATUS strftime);
use File::Glob qw( :glob );
use File::Compare;
use File::Copy;
use Socket; # This defines PF_INET and SOCK_STREAM
use IO::Socket::SSL;
use Amanda::Config qw( :init :getconf );
use Amanda::Util qw( :constants );
use Amanda::Logfile qw( :logtype_t log_add );
use Amanda::Debug qw( debug );
use Amanda::Paths;
use Amanda::Amdump;
##
# Main
sub usage {
my ($msg) = @_;
print STDERR <<EOF;
Usage: amssl <conf> [--client] [--init | -create-ca | --create-server-cert NAME | --create-client-cert NAME] [--confirm] [-o configoption]*
[--cacert CA-CERT-FILE] [--cakey CA-KEY-FILE]
EOF
print STDERR "$msg\n" if $msg;
exit 1;
}
Amanda::Util::setup_application("amssl", "server", $CONTEXT_DAEMON, "amanda", "amanda");
my $config_overrides = new_config_overrides($#ARGV+1);
my @config_overrides_opts;
my $opt_client = 0;
my $opt_init = 0;
my $opt_ca = 0;
my $opt_server_cert = 0;
my $opt_client_cert = 0;
my $opt_country;
my $opt_state;
my $opt_locality;
my $opt_organisation;
my $opt_organisation_unit;
my $opt_common;
my $opt_email;
my $opt_batch;
my $opt_server;
my $opt_port;
my $opt_cacert;
my $opt_cakey;
my $opt_config;
debug("Arguments: " . join(' ', @ARGV));
Getopt::Long::Configure(qw(bundling));
GetOptions(
'version' => \&Amanda::Util::version_opt,
'help|usage|?' => \&usage,
'client' => \$opt_client,
'init' => \$opt_init,
'create-ca' => \$opt_ca,
'create-server-cert:s' => \$opt_server_cert,
'create-client-cert:s' => \$opt_client_cert,
'country:s' => \$opt_country,
'state:s' => \$opt_state,
'locality:s' => \$opt_locality,
'organisation:s' => \$opt_organisation,
'organisation_unit:s' => \$opt_organisation_unit,
'common:s' => \$opt_common,
'email:s' => \$opt_email,
'batch' => \$opt_batch,
'server:s' => \$opt_server,
'config:s' => \$opt_config,
'port:s' => \$opt_port,
'cacert:s' => \$opt_cacert,
'cakey:s' => \$opt_cakey,
) or usage();
set_config_overrides($config_overrides);
if ($opt_client) {
config_init_with_global($CONFIG_INIT_EXPLICIT_NAME | $CONFIG_INIT_CLIENT, $opt_config);
} else {
config_init_with_global($CONFIG_INIT_EXPLICIT_NAME, $opt_config);
}
my ($cfgerr_level, @cfgerr_errors) = config_errors();
if ($cfgerr_level >= $CFGERR_WARNINGS) {
config_print_errors();
if ($cfgerr_level >= $CFGERR_ERRORS) {
die("errors processing config file");
}
}
Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
$opt_port = 10081 if !defined $opt_port;
# never change these values, that will create incompatibility between version
my $MSG_SERVER_FINGERPRINT = 1;
my $MSG_SERVER_CRT = 2;
my $MSG_CSR = 3;
my $MSG_CRT = 4;
my $MSG_CA_CRT = 5;
my $ssl_config;
my $ca_cert_file;
my $ca_key_file;
my $batch = " -batch " if $opt_batch;
my $SSL_DIR = getconf($CNF_SSL_DIR);
if ($opt_init) {
mkdir $SSL_DIR;
write_openssl_cnf_template();
my $data = { 'config' => { 'COUNTRY_NAME_VALUE' => $opt_country,
'STATE_VALUE' => $opt_state,
'LOCALITY_NAME_VALUE' => $opt_locality,
'ORGANIZATION_NAME_VALUE' => $opt_organisation,
'ORGANIZATION_UNIT_NAME_VALUE' => $opt_organisation_unit,
'COMMON_NAME_VALUE' => $opt_common,
'EMAIL_ADDRESS_VALUE' => $opt_email,
},
'CA_CERT_FILE' => $opt_cacert,
'CA_KEY_FILE' => $opt_cakey,
};
open SSL_DATA, ">$SSL_DIR/openssl.data";
my $dumper = Data::Dumper->new([ $data ], ["ssl_config"]);
$dumper->Purity(1);
print SSL_DATA $dumper->Dump;
close SSL_DATA;
if ($opt_cacert && -f $opt_cacert) {
copy($opt_cacert, "$SSL_DIR/CA/crt.pem")
}
} elsif ($opt_client && $opt_client_cert) {
if (-l "$SSL_DIR/remote/$opt_client_cert") {
die("$opt_client_cert is a server");
}
my $hostdir = "$SSL_DIR/me";
mkdir $hostdir;
mkdir "$hostdir/private";
$opt_common = $opt_client_cert if !defined $opt_common;
my $openssl_cnf_file = "$hostdir/openssl.cnf";
load_data_config();
write_openssl_cnf($openssl_cnf_file);
# generate private key
system("openssl genrsa -out $hostdir/private/key.pem 1024");
# generate csr
system("openssl req -config $openssl_cnf_file $batch -new -key $hostdir/private/key.pem -out $hostdir/csr.pem");
select( ( select(STDOUT), $| = 1 )[0] );
print "\nConnecting to server... ";
# connect to client
my $fd = IO::Socket::SSL->new(
# where to connect
PeerHost => $opt_server,
PeerPort => "$opt_port",
SSL_verify_mode => 0,
) or die "failed connect or ssl handshake: $!,$SSL_ERROR";
print " connected!\n";
my $tmp_server_dir = "$SSL_DIR/remote/tmp";
mkdir "$SSL_DIR/remote";
mkdir $tmp_server_dir;
# get server fingerprint
get_message_file($fd, $MSG_SERVER_FINGERPRINT, "$tmp_server_dir/fingerprint.new");
# find certificate name
my $server_hostdir = "$SSL_DIR/remote/$opt_server";
mkdir "$server_hostdir";
# compare server fingerprint with the current one
if (-f "$server_hostdir/fingerprint") {
if (compare("$tmp_server_dir/fingerprint.new","$server_hostdir/fingerprint") != 0) {
die("ERROR: fingerprint from server differ from what we have ($server_hostdir/fingerprint)");
}
unlink "$tmp_server_dir/fingerprint.new";
} else {
# save server fingerprint
rename "$tmp_server_dir/fingerprint.new", "$server_hostdir/fingerprint";
}
# send $hostdir/csr.pem to server
send_message_file($fd, $MSG_CSR, "$hostdir/csr.pem");
unlink "$hostdir/csr.pem";
# get $hostdir/crt.pem from server
get_message_file($fd, $MSG_CRT, "$hostdir/crt.pem");
# get server CA cert
mkdir "$hostdir/CA";
get_message_file($fd, $MSG_CA_CRT, "$hostdir/CA/crt.pem");
# compute my fingerprint
system("openssl x509 -in $hostdir/crt.pem -fingerprint -noout > $hostdir/fingerprint");
symlink "../me", "$SSL_DIR/remote/$opt_client_cert";
} elsif ($opt_ca) {
mkdir "$SSL_DIR/CA";
mkdir "$SSL_DIR/CA/private";
my $openssl_cnf_file = "$SSL_DIR/CA/openssl.cnf";
load_data_config();
write_openssl_cnf($openssl_cnf_file);
system("openssl genrsa -des3 -out $SSL_DIR/CA/private/key.pem 1024");
system("openssl req -config $openssl_cnf_file $batch -new -key $SSL_DIR/CA/private/key.pem -out $SSL_DIR/CA/csr.pem");
system("openssl x509 -days 3650 -signkey $SSL_DIR/CA/private/key.pem -in $SSL_DIR/CA/csr.pem -req -out $SSL_DIR/CA/crt.pem");
} elsif ($opt_server_cert) {
my $hostdir = "$SSL_DIR/me";
mkdir $hostdir;
mkdir "$hostdir/private";
$opt_common = $opt_server_cert if !defined $opt_common;
my $openssl_cnf_file = "$hostdir/openssl.cnf";
load_data_config();
write_openssl_cnf($openssl_cnf_file);
system("openssl genrsa -out $hostdir/private/key.pem 1024");
system("openssl req -config $openssl_cnf_file $batch -new -key $hostdir/private/key.pem -out $hostdir/csr.pem");
system("openssl x509 -days 3650 -CA $ca_cert_file -CAkey $ca_key_file -set_serial 01 -in $hostdir/csr.pem -req -out $hostdir/crt.pem");
system("openssl x509 -in $hostdir/crt.pem -fingerprint -noout > $hostdir/fingerprint");
my $server_hostdir = "$SSL_DIR/remote/$opt_server_cert";
mkdir "$SSL_DIR/remote";
symlink "../me", $server_hostdir;
} elsif ($opt_client_cert) {
if (-l "$SSL_DIR/remote/$opt_client_cert") {
die("$opt_client_cert is a server");
}
load_data_config();
my $hostdir = "$SSL_DIR/remote/$opt_client_cert";
mkdir "$SSL_DIR/remote";
mkdir $hostdir;
select( ( select(STDOUT), $| = 1 )[0] );
print "\nWaiting for client to connect... ";
# wait connection from server
my $srv = IO::Socket::SSL->new(
LocalAddr => "0.0.0.0:$opt_port",
Listen => 10,
SSL_cert_file => "$SSL_DIR/me/crt.pem",
SSL_key_file => "$SSL_DIR/me/private/key.pem",
SSL_verify_mode => 0,
) or die "failed connect or ssl handshake: $!,$SSL_ERROR";
my $fd = $srv->accept or
die "failed to accept or ssl handshake: $!,$SSL_ERROR";
# Check peer is $opt_client_cert
#
print " connected!\n";
# send server fingerprint to client
send_message_file($fd, $MSG_SERVER_FINGERPRINT, "$SSL_DIR/me/fingerprint");
# get $hostdir/csr.pem from client
get_message_file($fd, $MSG_CSR, "$hostdir/csr.pem");
# check certificate common name is $opt_client_cert
my $subject = `openssl req -noout -subject -in $hostdir/csr.pem`;
chomp $subject;
$subject =~ s/\*//g; # Remove wildcard astricks
my ($CN) = $subject =~ /CN=([^\/]*)/;
if(!$CN) {
die "Unable to read SSL certificate\n";
}
elsif($opt_client_cert !~ /$CN/i) {
die "SSL Certificate Common Name mismatch. Retrieved common name '$CN' does not match hostname '$opt_client_cert'";
}
# sign csr
system("openssl x509 -days 3650 -CA $ca_cert_file -CAkey $ca_key_file -set_serial 01 -in $hostdir/csr.pem -req -out $hostdir/crt.pem");
# send $hostdir/crt.pem to client
send_message_file($fd, $MSG_CRT, "$hostdir/crt.pem");
# send CA CERT to client
send_message_file($fd, $MSG_CA_CRT, "$ca_cert_file");
system("openssl x509 -in $hostdir/crt.pem -fingerprint -noout > $hostdir/fingerprint");
unlink "$hostdir/csr.pem";
unlink "$hostdir/crt.pem";
} else {
usage('no command specified');
}
exit;
sub send_message_file {
my $fd = shift;
my $msg = shift;
my $filename = shift;
my $data;
{
local $/ = undef;
my $DATA;
open $DATA, "<$filename" || die("$filename: $!");
$data = <$DATA>;
close $DATA;
}
my $size = length($data);
print $fd pack("NN", $size, $msg), $data;
}
sub get_message_file {
my $fd = shift;
my $msg = shift;
my $filename = shift;
my $desc;
sysread $fd, $desc, 8;
my ($size, $gmsg) = unpack("NN", $desc);
my $data;
sysread $fd, $data, $size;
die("MSG doesn't match: $msg != $gmsg") if $msg != $gmsg;
my $DATA;
open $DATA, ">$filename" || die("$filename: $!");
print $DATA $data;
close $DATA;
}
sub load_data_config {
my $contents;
{
local $/ = undef;
my $SSL_DATA;
open $SSL_DATA, "<$SSL_DIR/openssl.data" || die("$SSL_DIR/openssl.data: $!");
$contents = <$SSL_DATA>;
close $SSL_DATA;
}
eval $contents;
if ($@) {
die;
}
if (!defined $ssl_config or ref($ssl_config) ne 'HASH') {
die;
}
$ssl_config->{'config'}->{'COUNTRY_NAME_VALUE'} = $opt_country if defined $opt_country;
$ssl_config->{'config'}->{'STATE_VALUE'} = $opt_state if defined $opt_state;
$ssl_config->{'config'}->{'LOCALITY_NAME_VALUE'} = $opt_locality if defined $opt_locality;
$ssl_config->{'config'}->{'ORGANIZATION_NAME_VALUE'} = $opt_organisation if defined $opt_organisation;
$ssl_config->{'config'}->{'ORGANIZATION_UNIT_NAME_VALUE'} = $opt_organisation_unit if defined $opt_organisation_unit;
$ssl_config->{'config'}->{'COMMON_NAME_VALUE'} = $opt_common if defined $opt_common;
$ssl_config->{'config'}->{'EMAIL_ADDRESS_VALUE'} = $opt_email if defined $opt_email;
$ssl_config->{'CA_CERT_FILE'} = $opt_cacert if defined $opt_cacert;
$ssl_config->{'CA_KEY_FILE'} = $opt_cakey if defined $opt_cakey;
$ca_cert_file = $ssl_config->{'CA_CERT_FILE'} || "$SSL_DIR/CA/crt.pem";
$ca_key_file = $ssl_config->{'CA_KEY_FILE'} || "$SSL_DIR/CA/private/key.pem";
}
sub write_openssl_cnf {
my $openssl_cnf_file = shift;
my $CNF_TEMPLATE;
my $template;
{
local $/ = undef;
open $CNF_TEMPLATE, "<$SSL_DIR/openssl.cnf.template";
$template = <$CNF_TEMPLATE>;
close $CNF_TEMPLATE;
}
if (!$ssl_config->{'config'}->{'COUNTRY_NAME_VALUE'}) {
$ssl_config->{'config'}->{'COUNTRY_NAME_VALUE'} = "XX";
$batch = "";
}
if (!$ssl_config->{'config'}->{'STATE_VALUE'}) {
$ssl_config->{'config'}->{'STATE_VALUE'} = "Default State/Province";
$batch = "";
}
if (!$ssl_config->{'config'}->{'LOCALITY_NAME_VALUE'}) {
$ssl_config->{'config'}->{'LOCALITY_NAME_VALUE'} = "Default City";
$batch = "";
}
if (!$ssl_config->{'config'}->{'ORGANIZATION_NAME_VALUE'}) {
$ssl_config->{'config'}->{'ORGANIZATION_NAME_VALUE'} = "Default Company Ltd";
$batch = "";
}
if (!$ssl_config->{'config'}->{'ORGANIZATION_UNIT_NAME_VALUE'}) {
$ssl_config->{'config'}->{'ORGANIZATION_UNIT_NAME_VALUE'} = "Organizational Unit Name (eg, section)";
$batch = "";
}
if (!$ssl_config->{'config'}->{'COMMON_NAME_VALUE'}) {
$batch = "";
}
if (!$ssl_config->{'config'}->{'EMAIL_ADDRESS_VALUE'}) {
$batch = "";
}
my $key;
my $value;
while (($key,$value) = each %{$ssl_config->{'config'}}) {
$template =~ s/$key/$value/;
}
my $CNF;
open $CNF, ">$openssl_cnf_file";
print $CNF $template;
close $CNF;
}
sub write_openssl_cnf_template {
return if -f "$SSL_DIR/openssl.cnf.template";
my $CNF_TEMPLATE;
open $CNF_TEMPLATE, ">$SSL_DIR/openssl.cnf.template";
print $CNF_TEMPLATE <<'CNF_TEMPLATE_FILE';
#
# OpenSSL configuration file for amanda.
# This is used for generation of certificate requests.
#
# This definition stops the following lines choking if HOME isn't
# defined.
HOME = .
RANDFILE = $ENV::HOME/.rnd
[ req ]
default_bits = 2048
default_md = sha256
default_keyfile = privkey.pem
distinguished_name = req_distinguished_name
attributes = req_attributes
x509_extensions = v3_ca # The extentions to add to the self signed cert
# Passwords for private keys if not present they will be prompted for
# input_password = secret
# output_password = secret
# This sets a mask for permitted string types. There are several options.
# default: PrintableString, T61String, BMPString.
# pkix : PrintableString, BMPString (PKIX recommendation before 2004)
# utf8only: only UTF8Strings (PKIX recommendation after 2004).
# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings).
# MASK:XXXX a literal mask value.
# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings.
string_mask = utf8only
# req_extensions = v3_req # The extensions to add to a certificate request
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = COUNTRY_NAME_VALUE
countryName_min = 2
countryName_max = 2
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = STATE_VALUE
localityName = Locality Name (eg, city)
localityName_default = LOCALITY_NAME_VALUE
0.organizationName = Organization Name (eg, company)
0.organizationName_default = ORGANIZATION_NAME_VALUE
# we can do this but it is not needed normally :-)
#1.organizationName = Second Organization Name (eg, company)
#1.organizationName_default = World Wide Web Pty Ltd
organizationalUnitName = Organizational Unit Name (eg, section)
organizationalUnitName_default = ORGANIZATION_UNIT_NAME_VALUE
commonName = Common Name (eg, your name or your server\'s hostname)
commonName_default = COMMON_NAME_VALUE
commonName_max = 64
emailAddress = Email Address
emailAddress_default = EMAIL_ADDRESS_VALUE
emailAddress_max = 64
# SET-ex3 = SET extension number 3
[ req_attributes ]
#challengePassword = A challenge password
#challengePassword_min = 4
#challengePassword_max = 20
#
#unstructuredName = An optional company name
[ v3_ca ]
# Extensions for a typical CA
# PKIX recommendation.
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
# This is what PKIX recommends but some broken software chokes on critical
# extensions.
#basicConstraints = critical,CA:true
# So we do this instead.
basicConstraints = CA:true
# Key usage: this is typical for a CA certificate. However since it will
# prevent it being used as an test self-signed certificate it is best
# left out by default.
# keyUsage = cRLSign, keyCertSign
# Some might want this also
# nsCertType = sslCA, emailCA
# Include email address in subject alt name: another PKIX recommendation
# subjectAltName=email:copy
# Copy issuer details
# issuerAltName=issuer:copy
# DER hex encoding of an extension: beware experts only!
# obj=DER:02:03
# Where 'obj' is a standard or added object
# You can even override a supported extension:
# basicConstraints= critical, DER:30:03:01:01:FF
CNF_TEMPLATE_FILE
close $CNF_TEMPLATE;
}