--- loncom/CrCA.pl 2019/01/01 04:55:00 1.2 +++ loncom/CrCA.pl 2019/07/18 00:28:04 1.4 @@ -2,7 +2,7 @@ # The LearningOnline Network with CAPA # Script to create a Certificate Authority (CA) for a LON-CAPA cluster. # -# $Id: CrCA.pl,v 1.2 2019/01/01 04:55:00 raeburn Exp $ +# $Id: CrCA.pl,v 1.4 2019/07/18 00:28:04 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -26,13 +26,6 @@ # http://www.lon-capa.org/ 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 @@ -79,6 +72,8 @@ Term::ReadKey Sys::Hostname::FQDN Locale::Country Crypt::OpenSSL::X509 +Crypt::X509::CRL +MIME::Base64 DateTime::Format::x509 File::Slurp @@ -97,7 +92,57 @@ for use in the cluster. END -#Proceed? + print ('Continue? [Y/n]'); + my $go_on = &get_user_selection(1); + if (!$go_on) { + exit; + } + + eval { require Sys::Hostname::FQDN; }; + if ($@) { + print "Could not find required perl module: Sys::Hostname::FQDN. Exiting.\n"; + exit; + } + eval { require Term::ReadKey; }; + if ($@) { + print "Could not find required perl module: Term::ReadKey. Exiting\n"; + exit; + } + eval { require Locale::Country; }; + if ($@) { + print "Could not find required perl module: Locale::Country. Exiting\n"; + exit; + } + eval { require Crypt::OpenSSL::X509; }; + if ($@) { + print "Could not find required perl module: Crypt::OpenSSL::X509. Exiting\n"; + exit; + } + eval { require Crypt::X509::CRL; }; + if ($@) { + print "Could not find required perl module: Crypt::X509::CRL. Exiting\n"; + exit; + } + eval { require DateTime::Format::x509; }; + if ($@) { + print "Could not find required perl module: DateTime::Format::x509. Exiting\n"; + exit; + } + eval { require File::Slurp; }; + if ($@) { + print "Could not find required perl module: File::Slurp. Exiting\n"; + exit; + } + eval { require MIME::Base64; }; + if ($@) { + print "Could not find required perl core module: MIME::Base64\n"; + exit; + } + eval { require Cwd; }; + if ($@) { + print "Could not find required perl core module: Cwd\n"; + exit; + } my ($dir,$hostname,%data); @@ -152,7 +197,7 @@ END A configuration file: $dir/lonca/opensslca.conf will be created. The following information will be included: -Country, State/Province, City, Cluster Name, Organizational Name, E-mail address, CA certificate lifetime (days), Default certificate lifetime (days), CRL re-creation interval (days) +Country, State/Province, City, Cluster Name, Organizational Name, E-mail address, Default certificate lifetime (days), CRL re-creation interval (days) END $hostname = Sys::Hostname::FQDN::fqdn(); @@ -171,7 +216,7 @@ END clustername => 'Cluster name', organization => 'Organization name', ); - my ($clustername,$organization,$country,$state,$city,$email,$cadays,$clusterhostname,$days,$crldays); + my ($clustername,$organization,$country,$state,$city,$email,$clusterhostname,$days,$crldays); $clusterhostname = $hostname; $country = &get_country($hostname); print "Enter state or province name\n"; @@ -183,10 +228,8 @@ END '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"; + 'This name will be included as the Organization 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"; @@ -224,7 +267,7 @@ organizationalUnitName = optional [ certificate_extensions ] basicConstraints = CA:false -crlDistributionPoints = URI:http://$clusterhostname/adm/dns/loncapaCAcrl +crlDistributionPoints = URI:http://$clusterhostname/adm/dns/loncapaCRL [ req ] @@ -313,6 +356,7 @@ END exit; } } + my $makecacert; 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 |"); @@ -323,6 +367,7 @@ END my $hashfromcert = ; close(PIPE); chomp($hashfromcert); + my $defsel = 0; if ($hashfromkey eq $hashfromcert) { my ($now,$starttime,$endtime,$status,%cert); my $x509 = Crypt::OpenSSL::X509->new_from_file("$dir/lonca/cacert.pem"); @@ -351,31 +396,39 @@ END if ($endtime <= $now) { $status = 'previous'; print "Current CA certificate expired $cert{'end'}\n"; + print 'Create a new certificate? [Y/n]'; + $defsel = 1; } elsif ($starttime > $now) { $status = 'future'; - print "Current CA certificate will be valid after $cert{'start'}\n"; + print "Current CA certificate will be valid after $cert{'start'}\n"; + print 'Create a new certificate? [y/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; - } - } + print 'Create a new certificate? [y/N]'; } } else { print "Could not determine validity of current CA certificate\n"; - exit; + print 'Create a new certificate? [Y/n]'; + $defsel = 1; } + } else { + print "Current CA certificate does not match key.\n"; + print 'Create a new certificate? [Y/n]'; + $defsel = 1; + } + if (&get_user_selection($defsel)) { + $makecacert = 1; } } else { - unless (&make_ca_cert("$dir/lonca/private","$dir/lonca",$sslkeypass)) { - print "Failed to create CA cert\n"; + $makecacert = 1; + } + if ($makecacert) { + print "Enter the lifetime (in days) for the CA root certificate distributed to all nodes, e.g., 3650\n"; + my $cadays = &get_days(); + unless (&make_ca_cert("$dir/lonca/private","$dir/lonca",$sslkeypass,$cadays)) { + print "Failed to create CA certificate\n"; exit; } } @@ -390,25 +443,98 @@ END 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"; + my $defcrlsel = 1; + if (!-e "$dir/lonca/crl/loncapaCAcrl.pem") { + print "No Revocation Certificate List found.\n"; + print 'Create Certificate Revocation List [Y/n]'; + } else { + if (open(PIPE,"openssl crl -in $dir/lonca/crl/loncapaCAcrl.pem -inform pem -CAfile $dir/lonca/cacert.pem -noout 2>&1 |")) { + my $crlstatus = ; + close(PIPE); + chomp($crlstatus); + my $failmsg = "Could not determine 'valid from' and 'valid to' dates for Certificate Revocation List.\n"; + if ($crlstatus =~ /OK/) { + print "Current Certficate Revocation List is consistent with current CA certificate.\n"; + if (open(my $fh,'<',"$dir/lonca/crl/loncapaCAcrl.pem")) { + my $pem_crl = ''; + while (my $line=<$fh>) { + chomp($line); + next if ($line eq '-----BEGIN X509 CRL-----'); + next if ($line eq '-----END X509 CRL-----'); + $pem_crl .= $line; + } + close($fh); + my $der_crl = MIME::Base64::decode_base64($pem_crl); + if ($der_crl ne '') { + my $decoded = Crypt::X509::CRL->new( crl => $der_crl ); + if (ref($decoded)) { + if ($decoded->error) { + print $failmsg; + } else { + my $starttime = $decoded->this_update; + my $endtime = $decoded->next_update; + if (($endtime ne '') && ($endtime < time)) { + print "Certificate Revocation List is no longer valid.\n"; + } elsif ($starttime > time) { + print "Certificate Revocation List will become valid in the future.\n"; + } elsif (($starttime ne '') && ($endtime ne '')) { + my $showstart = localtime($starttime); + my $showend = localtime($endtime); + print "Certificate Revocation List valid from: $showstart to: $showend\n"; + $defcrlsel = 0; + } else { + print $failmsg; + } + } + } else { + print $failmsg; + } + } else { + print $failmsg; + } + } else { + print $failmsg; + } + } else { + print "Current Certificate Revocation List is not consistent with current CA certificate.\n"; + } + if ($defcrlsel) { + print 'Create Certificate Revocation List [Y/n]'; + } else { + print 'Create Certificate Revocation List [y/N]'; + } + } else { + print "Could not check Certificate Revocation List status.\n"; + print 'Create Certificate Revocation List [Y/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? + if (&get_user_selection($defcrlsel)) { + if (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") { + if (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); + if ($revoked eq 'verify OK') { + print "Certificate Revocation List created\n"; + } else { + print "Certificate Revocation List status: $revoked\n"; + } + } else { + print "Could not check Certificate Revocation List status\n"; + } + } else { + print "Failed to create Certificate Revocation List\n"; + } + } else { + print "Failed to create Certificate Revocation List\n"; + } } + exit(0); + sub cafield_to_key { my %mapping = ( @@ -614,9 +740,9 @@ sub get_password { local $| = 1; print $prompt.': '; my $newpasswd = ''; - ReadMode 'raw'; + Term::ReadKey::ReadMode('raw'); my $key; - while(ord($key = ReadKey(0)) != 10) { + while(ord($key = Term::ReadKey::ReadKey(0)) != 10) { if(ord($key) == 127 || ord($key) == 8) { chop($newpasswd); print "\b \b"; @@ -625,7 +751,7 @@ sub get_password { print '*'; } } - ReadMode 'normal'; + Term::ReadKey::ReadMode('normal'); print "\n"; return $newpasswd; } @@ -661,28 +787,19 @@ sub make_key { # sub make_ca_cert { - my ($keydir,$certdir,$sslkeypass) = @_; + my ($keydir,$certdir,$sslkeypass,$cadays) = @_; # 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 |"); + if ((-d $keydir) && (-d $certdir) && ($sslkeypass ne '') && ($cadays =~ /^\d+$/) && ($cadays > 0)) { + open(PIPE,"openssl req -x509 -key $keydir/cakey.pem -passin pass:$sslkeypass -new -days $cadays -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"; + print "Creation of CA root certificate failed. Missing one or more of: CA directory, CA key directory, CA passphrase, or certificate lifetime (number of days).\n"; } return $created; } @@ -737,7 +854,7 @@ sub get_country { ($posscountry) = ($desiredhostname =~ /\.(a-z){2}$/); } if ($posscountry) { - my $countrydesc = &Locale::Country::code2country($posscountry); + my $countrydesc = Locale::Country::code2country($posscountry); if ($countrydesc eq '') { undef($posscountry); } @@ -754,7 +871,7 @@ sub get_country { my $choice=; chomp($choice); if ($choice ne '') { - if (&Locale::Country::code2country(lc($choice))) { + if (Locale::Country::code2country(lc($choice))) { $country=uc($choice); $flag=1; } else { @@ -814,7 +931,8 @@ sub confirm_config { print(<; chomp($choice); @@ -875,32 +992,23 @@ END $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) { + } elsif ($choice == 8) { print(<; chomp($choice2); $choice2 =~ s/\D//g; $data{'crldays'}=$choice2; - } elsif ($choice == 10) { + } elsif ($choice == 9) { $flag=1; foreach my $key (keys(%data)) { $data{$key} =~ s{/}{ }g;