#!/usr/bin/perl use strict; use Sys::Hostname::FQDN(); use Term::ReadKey; use Locale::Country; use Crypt::OpenSSL::X509; use DateTime::Format::x509; use File::Slurp; use Cwd; # # Expected structure # # /lonca # opensslca.cnf # cacert.pem # index.txt # /certs # /crl # /private # /requests # print(< 'City', state => 'State or Province', clustername => 'Cluster name', organization => 'Organization name', ); my ($clustername,$organization,$country,$state,$city,$email,$cadays,$clusterhostname,$days,$crldays); $clusterhostname = $hostname; $country = &get_country($hostname); print "Enter state or province name\n"; $state = &get_info($fieldname{'state'}); print "Enter city name\n"; $city = &get_info($fieldname{'city'}); $email = &get_camail(); print 'Enter a name for this LON-CAPA cluster, e.g., "Lon-CAPA learning network"'."\n". 'This name will be included as the Common Name for the CA certificate.'."\n"; $clustername = &get_info($fieldname{'clustername'}); print 'Enter the organization name for this LON-CAPA cluster, e.g., "Lon CAPA certification authority"'."\n". 'This name will be included as the Oraganization for the CA certificate.'."\n"; $organization = &get_info($fieldname{'organization'}); print "Enter the lifetime (in days) for the CA root certificate distributed to all nodes, e.g., 3650\n"; $cadays = &get_days(); print "Enter the default lifetime (in days) for each certificate created/signed by the CA for individual nodes, e.g., 3650\n"; $days = &get_days(); print "Enter the re-creation interval (in days) for the CA's certificate revocation list (CRL), e.g., 180\n"; $crldays = &get_days(); if (open(my $fh,'>',"$dir/lonca/opensslca.conf")) { print $fh <<"END"; [ ca ] default_ca = loncapa [ loncapa ] dir = $dir/lonca certificate = $dir/lonca/cacert.pem database = $dir/lonca/index.txt new_certs_dir = $dir/lonca/certs private_key = $dir/lonca/private/cakey.pem serial = $dir/lonca/serial default_crl_days = $crldays default_days = $days default_md = sha256 policy = loncapa_policy x509_extensions = certificate_extensions [ loncapa_policy ] commonName = supplied stateOrProvinceName = supplied countryName = supplied emailAddress = supplied organizationName = supplied organizationalUnitName = optional [ certificate_extensions ] basicConstraints = CA:false crlDistributionPoints = URI:http://$clusterhostname/adm/dns/loncapaCAcrl [ req ] default_bits = 2048 distinguished_name = loncapa_ca x509_extensions = loncapa_ca_extensions [ loncapa_ca ] commonName = $clustername localityName = $city stateOrProvinceName = $state countryName = $country emailAddress = $email organizationName = $organization [ loncapa_ca_extensions ] basicConstraints = CA:true [ crl_ext ] authorityKeyIdentifier=keyid:always,issuer:always END } else { print 'Error: failed to wtite to '."$dir/lonca/opensslca.conf. Exiting.\n"; exit; } %data = &parse_config("$dir/lonca/opensslca.conf"); my %update = &confirm_config(%data); my %changes; foreach my $field ('clustername','organization','email','country','state','city','days','crldays') { if ($data{$field} ne $update{$field}) { $changes{$field} = $update{$field}; } } if (keys(%changes)) { &save_config_changes("$dir/lonca/opensslca.conf",\%changes); } } my $sslkeypass; if (-e "$dir/lonca/private/cakey.pem") { my ($keyok,$try); print "CA key aleady exists\n"; $try = 1; while (!$keyok && $try) { $sslkeypass = &get_password('Enter the password for the CA key'); if ($sslkeypass ne '') { open(PIPE,"openssl rsa -noout -in lonca/private/cakey.pem -passin pass:$sslkeypass -check |"); my $check = ; close(PIPE); chomp($check); if ($check eq 'RSA key ok') { $keyok = 1; last; } else { print "CA key check failed. Try again? [Y/n]"; if (!&get_user_selection(1)) { $try = 0; } } } } unless ($keyok) { print "CA key check failed. Create a new key? [Y/n]"; if (&get_user_selection(1)) { $sslkeypass = &get_new_sslkeypass(); # generate SSL key unless (&make_key("$dir/lonca/private",$sslkeypass)) { print "Failed to create CA key\n"; exit; } } else { exit; } } } else { $sslkeypass = &get_new_sslkeypass(); # generate SSL key unless (&make_key("$dir/lonca/private",$sslkeypass)) { print "Failed to create CA key\n"; exit; } } if (-e "$dir/lonca/cacert.pem") { print "A CA certificate exists\n"; open(PIPE,"openssl pkey -in $dir/lonca/private/cakey.pem -passin pass:$sslkeypass -pubout -outform der | sha256sum |"); my $hashfromkey = ; close(PIPE); chomp($hashfromkey); open(PIPE,"openssl x509 -in $dir/lonca/cacert.pem -pubkey | openssl pkey -pubin -pubout -outform der | sha256sum |"); my $hashfromcert = ; close(PIPE); chomp($hashfromcert); if ($hashfromkey eq $hashfromcert) { my ($now,$starttime,$endtime,$status,%cert); my $x509 = Crypt::OpenSSL::X509->new_from_file("$dir/lonca/cacert.pem"); my @items = split(/,\s+/,$x509->subject()); foreach my $item (@items) { my ($name,$value) = split(/=/,$item); if ($name eq 'CN') { $cert{'cn'} = $value; } } $cert{'start'} = $x509->notBefore(); $cert{'end'} = $x509->notAfter(); $cert{'alg'} = $x509->sig_alg_name(); $cert{'size'} = $x509->bit_length(); $cert{'email'} = $x509->email(); my $dt = DateTime::Format::x509->parse_datetime($cert{'start'}); if (ref($dt)) { $starttime = $dt->epoch; } $dt = DateTime::Format::x509->parse_datetime($cert{'end'}); if (ref($dt)) { $endtime = $dt->epoch; } $now = time; if (($starttime ne '') && ($endtime ne '')) { if ($endtime <= $now) { $status = 'previous'; print "Current CA certificate expired $cert{'end'}\n"; } elsif ($starttime > $now) { $status = 'future'; print "Current CA certificate will be valid after $cert{'start'}\n"; } else { $status eq 'active'; print "Current CA certificate valid until $cert{'end'}".' '. "Signature Algorithm: $cert{'alg'}; Public Key size: $cert{'size'}\n"; } if ($status eq 'previous') { print 'Create a new certificate? [Y/n]'; if (&get_user_selection(1)) { unless (&make_ca_cert("$dir/lonca/private","$dir/lonca",$sslkeypass)) { print "Failed to create CA cert\n"; exit; } } } } else { print "Could not determine validity of current CA certificate\n"; exit; } } } else { unless (&make_ca_cert("$dir/lonca/private","$dir/lonca",$sslkeypass)) { print "Failed to create CA cert\n"; exit; } } if (!-e "$dir/lonca/index.txt") { File::Slurp::write_file("$dir/lonca/index.txt"); } if (-e "$dir/lonca/index.txt") { my $mode = 0600; chmod $mode, "$dir/lonca/index.txt"; } else { print "lonca/index.txt file is missing\n"; exit; } # echo 1000 > serial unless (-e "$dir/lonca/crl/loncapaCAcrl.pem") { open(PIPE,"openssl ca -gencrl -keyfile $dir/lonca/private/cakey.pem -cert $dir/lonca/cacert.pem -out $dir". "/lonca/crl/loncapaCAcrl.pem -config $dir/lonca/opensslca.conf -passin pass:$sslkeypass |"); close(PIPE); if (-e "$dir/lonca/crl/loncapaCAcrl.pem") { print "Certificate Revocation List created\n"; } } if (-e "$dir/lonca/crl/loncapaCAcrl.pem") { open(PIPE,"openssl crl -in $dir/lonca/crl/loncapaCAcrl.pem -inform pem -CAfile $dir/lonca/cacert.pem -noout 2>&1 |"); my $revoked = ; close(PIPE); chomp($revoked); print "Revocation certificate status: $revoked\n"; # Create a new one? } sub cafield_to_key { my %mapping = ( city => 'localityName', state => 'stateOrProvinceName', country => 'countryName', email => 'emailAddress', organization => 'organizationName', clustername => 'commonName', ); return %mapping; } sub field_to_key { my %mapping = ( days => 'default_days', crldays => 'default_crl_days', ); } sub parse_config { my ($filepath) = @_; my (%fields,%data); if (open(my $fh,'<',$filepath)) { my $currsection; while(<$fh>) { chomp(); s/(^\s+|\s+$)//g; if (/^\[\s*([^\s]+)\s*\]/) { $currsection = $1; } elsif (/^([^=]+)=([^=]+)$/) { my ($key,$value) = ($1,$2); $key =~ s/\s+$//; $value =~ s/^\s+//; if ($currsection ne '') { $fields{$currsection}{$key} = $value; } } } close($fh); } if (ref($fields{'loncapa_ca'}) eq 'HASH') { my %ca_mapping = &cafield_to_key(); foreach my $key (keys(%ca_mapping)) { $data{$key} = $fields{'loncapa_ca'}{$ca_mapping{$key}}; } } if (ref($fields{'loncapa'}) eq 'HASH') { my %mapping = &field_to_key(); foreach my $key (keys(%mapping)) { $data{$key} = $fields{'loncapa'}{$mapping{$key}}; } } return %data; } sub save_config_changes { my ($filepath,$updated) = @_; return unless (ref($updated) eq 'HASH'); my %mapping = &field_to_key(); my %ca_mapping = &cafield_to_key(); my %revmapping = reverse(%mapping); my %rev_ca_mapping = reverse(%ca_mapping); my $lines; if (open(my $fh,'<',$filepath)) { my $currsection; while(<$fh>) { my $line = $_; chomp(); s/(^\s+|\s+$)//g; my $newline; if (/^\[\s*([^\s]+)\s*\]/) { $currsection = $1; } elsif (/^([^=]+)=([^=]*)$/) { my ($origkey,$origvalue) = ($1,$2); my ($key,$value) = ($origkey,$origvalue); $key =~ s/\s+$//; $value =~ s/^\s+//; if ($currsection eq 'loncapa_ca') { if ((exists($rev_ca_mapping{$key})) && (exists($updated->{$rev_ca_mapping{$key}}))) { if ($value eq '') { if ($origvalue eq '') { $origvalue = ' '; } $origvalue .= $updated->{$rev_ca_mapping{$key}}; } else { $origvalue =~ s/\Q$value\E/$updated->{$rev_ca_mapping{$key}}/; } $newline = $origkey.'='.$origvalue."\n"; } } elsif ($currsection eq 'loncapa') { if ((exists($revmapping{$key})) && (exists($updated->{$revmapping{$key}}))) { if ($value eq '') { if ($origvalue eq '') { $origvalue = ' '; } $origvalue .= $updated->{$revmapping{$key}}; } else { $origvalue =~ s/\Q$value\E/$updated->{$revmapping{$key}}/; } $newline = $origkey.'='.$origvalue."\n"; } } } if ($newline) { $lines .= $newline; } else { $lines .= $line; } } close($fh); if (open(my $fout,'>',$filepath)) { print $fout $lines; close($fout); } else { print "Error: failed to open '$filepath' for writing\n"; } } return; } # # get_hostname() prompts the user to provide the server's hostname. # # If invalid input is provided, the routine is called recursively # until, a valid hostname is provided. # sub get_hostname { my $hostname; print 'Enter the hostname of this server, e.g., loncapa.somewhere.edu'."\n"; my $choice = ; chomp($choice); $choice =~ s/(^\s+|\s+$)//g; if ($choice eq '') { print "Hostname you entered was either blank or contanied only white space.\n"; } elsif ($choice =~ /^[\w\.\-]+$/) { $hostname = $choice; } else { print "Hostname you entered was invalid -- a hostname may only contain letters, numbers, - and .\n"; } while ($hostname eq '') { $hostname = &get_hostname(); } print "\n"; return $hostname; } sub get_new_sslkeypass { my $sslkeypass; my $flag=0; # get password for SSL key while (!$flag) { $sslkeypass = &make_passphrase(); if ($sslkeypass) { $flag = 1; } else { print "Invalid input (a password is required for the CA key).\n"; } } return $sslkeypass; } sub make_passphrase { my ($got_passwd,$firstpass,$secondpass,$passwd); my $maxtries = 10; my $trial = 0; while ((!$got_passwd) && ($trial < $maxtries)) { $firstpass = &get_password('Enter a password for the CA key (at least 6 characters long)'); if (length($firstpass) < 6) { print('Password too short.'."\n". 'Please choose a password with at least six characters.'."\n". 'Please try again.'."\n"); } elsif (length($firstpass) > 30) { print('Password too long.'."\n". 'Please choose a password with no more than thirty characters.'."\n". 'Please try again.'."\n"); } else { my $pbad=0; foreach (split(//,$firstpass)) {if ((ord($_)<32)||(ord($_)>126)){$pbad=1;}} if ($pbad) { print('Password contains invalid characters.'."\n". 'Password must consist of standard ASCII characters.'."\n". 'Please try again.'."\n"); } else { $secondpass = &get_password('Enter password a second time'); if ($firstpass eq $secondpass) { $got_passwd = 1; $passwd = $firstpass; } else { print('Passwords did not match.'."\n". 'Please try again.'."\n"); } } } $trial ++; } return $passwd; } sub get_password { my ($prompt) = @_; local $| = 1; print $prompt.': '; my $newpasswd = ''; ReadMode 'raw'; my $key; while(ord($key = ReadKey(0)) != 10) { if(ord($key) == 127 || ord($key) == 8) { chop($newpasswd); print "\b \b"; } elsif(!ord($key) < 32) { $newpasswd .= $key; print '*'; } } ReadMode 'normal'; print "\n"; return $newpasswd; } # # make_key() generates CA root key # sub make_key { my ($keydir,$sslkeypass) = @_; # generate SSL key my $created; if (($keydir ne '') && ($sslkeypass ne '')) { if (-f "$keydir/cakey.pem") { my $mode = 0600; chmod $mode, "$keydir/cakey.pem"; } open(PIPE,"openssl genrsa -aes256 -passout pass:$sslkeypass -out $keydir/cakey.pem 2048 2>&1 |"); close(PIPE); if (-f "$keydir/cakey.pem") { my $mode = 0400; chmod $mode, "$keydir/cakey.pem"; $created= 1; } } else { print "Key creation failed. Missing one or more of: certificates directory, key name\n"; } return $created; } # # make_ca_cert() generates CA root certificate # sub make_ca_cert { my ($keydir,$certdir,$sslkeypass) = @_; # generate SSL cert for CA my $created; if ((-d $keydir) && (-d $certdir) && ($sslkeypass ne '')) { my $cmd = "openssl req -x509 -key $keydir/cakey.pem -passin pass:$sslkeypass -new -batch -config $certdir/opensslca.conf -out $certdir/cacert.pem"; print "Calling ||$cmd||\n"; open(PIPE,"openssl req -x509 -key $keydir/cakey.pem -passin pass:$sslkeypass -new -batch -config $certdir/opensslca.conf -out $certdir/cacert.pem |"); close(PIPE); if (-f "$certdir/cacert.pem") { my $mode = 0600; chmod $mode, "$certdir/cacert.pem"; # chmod $mode, "$certdir/careq.pem"; # open(PIPE,"openssl ca -create_serial -out $certdir/cacert.pem -days 3650 -keyfile $keydir/cakey.pem -selfsign -config ./openssl.cnf -infiles $certdir/careq.pem |"); # close(PIPE); # if (-f "$certdir/cacert.pem") { # my $mode = 0600; # chmod $mode, "$certdir/cacert.pem"; # } $created= 1; } } else { print "Creation of CA root certificate failed. Missing one or more of: CA directory, CA key directory, or CA passphrase.\n"; } return $created; } sub get_camail { my $camail; my $flag=0; # get Certificate Authority E-mail while (!$flag) { print(<; chomp($choice); if (($choice ne '') && ($choice =~ /^[^\@]+\@[^\@]+$/)) { $camail=$choice; $flag=1; } else { print "Invalid input (a valid email address is required).\n"; } } return $camail; } sub ssl_info { print(<; chomp($choice); if ($choice ne '') { if (&Locale::Country::code2country(lc($choice))) { $country=uc($choice); $flag=1; } else { print "Invalid input -- a valid two letter country code is required\n"; } } elsif (($choice eq '') && ($posscountry ne '')) { $country = $posscountry; $flag = 1; } else { print "Invalid input -- a country code is required\n"; } } return $country; } sub get_info { my ($typename) = @_; my $value; my $choice = ; chomp($choice); $choice =~ s/(^\s+|\s+$)//g; if ($choice eq '') { print "$typename you entered was either blank or contained only white space.\n"; } else { $value = $choice; } while ($value eq '') { $value = &get_info($typename); } print "\n"; return $value; } sub get_days { my $value; my $choice = ; chomp($choice); $choice =~ s/(^\s+|\s+$)//g; if ($choice eq '') { print "The value you entered was either blank or contained only white space.\n"; } elsif ($choice !~ /^\d+$/) { print "The value you entered contained invalid characters -- you must enter just an integer.\n"; } else { $value = $choice; } while ($value eq '') { $value = &get_days(); } print "\n"; return $value; } sub confirm_config { my (%data) = @_; my $flag = 0; while (!$flag) { print(<; chomp($choice); if ($choice == 1) { print(<; chomp($choice2); $data{'clustername'}=$choice2; chomp($choice2); $data{'organization'}=$choice2; } elsif ($choice == 3) { print(<; chomp($choice2); $data{'country'} = uc($choice2); } elsif ($choice == 4) { print(<; chomp($choice2); $data{'state'}=$choice2; } elsif ($choice == 5) { print(<; chomp($choice2); $data{'city'}=$choice2; } elsif ($choice == 6) { print(<; chomp($choice2); $data{'email'}=$choice2; } elsif ($choice == 7) { print(<; chomp($choice2); $choice2 =~ s/\D//g; $data{'cadays'}=$choice2; } elsif ($choice == 8) { print(<; chomp($choice2); $choice2 =~ s/\D//g; $data{'days'}=$choice2; } elsif ($choice == 9) { print(<; chomp($choice2); $choice2 =~ s/\D//g; $data{'crldays'}=$choice2; } elsif ($choice == 10) { $flag=1; foreach my $key (keys(%data)) { $data{$key} =~ s{/}{ }g; } } else { print "Invalid input.\n"; } } return %data; } sub get_user_selection { my ($defaultrun) = @_; my $do_action = 0; my $choice = ; chomp($choice); $choice =~ s/(^\s+|\s+$)//g; my $yes = 'y'; if ($defaultrun) { if (($choice eq '') || ($choice =~ /^\Q$yes\E/i)) { $do_action = 1; } } else { if ($choice =~ /^\Q$yes\E/i) { $do_action = 1; } } return $do_action; }