#!/usr/bin/perl $|=1; # Displays status of LON-CAPA SSL certs, and allows new certificate # signing requests to be created and e-mailed to CA for cluster to # which server/VM belongs. # $Id: manage_ssl_certs.pl,v 1.1 2018/08/18 23:33:30 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # # This file is part of the LearningOnline Network with CAPA (LON-CAPA). # # LON-CAPA 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. # # LON-CAPA 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 LON-CAPA; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # /home/httpd/html/adm/gpl.txt # # http://www.lon-capa.org/ # use strict; use lib '/home/httpd/lib/perl/'; use LONCAPA::Configuration; use LONCAPA::Lond; use LONCAPA::SSL; use LONCAPA; use Term::ReadKey; use Locale::Country; my ($lonCluster,$domainDescription,$hostname,$certmail, $certsdir,$privkey,$connectcsr,$replicatecsr); print(<; chomp($choice); if ($choice==1) { $lonCluster='production'; $flag=1; } elsif ($choice==2) { $lonCluster='standalone'; $flag=1; } elsif ($choice==3) { $lonCluster='development'; $flag=1; } elsif ($choice==4) { $lonCluster='existing'; $flag=1; my $earlyout; foreach my $file ('hosts.tab','dns_hosts.tab', 'domain.tab','dns_domain.tab') { unless (-e '/home/httpd/lonTabs/'.$file) { print <) { if ($configline =~ /^[^\#]*PerlSetVar/) { my ($unused,$varname,$varvalue)=split(/\s+/,$configline); chomp($varvalue); $perlvar{$varname}=$varvalue if $varvalue!~/^\{\[\[\[\[/; } } close(CONFIG); } } my $perlstaticref = &get_static_config($confdir); if (ref($perlstaticref) ne 'HASH') { exit; } if (open(IN,'<','/home/httpd/lonTabs/domain.tab')) { while(my $line = ) { if ($line =~ /^\Q$perlvar{'lonDefDomain'}\E\:/) { (undef,$domainDescription)=split(/:/,$line); chomp($domainDescription); last; } } close(IN); } if (open(IN,'<','/home/httpd/lonTabs/hosts.tab')) { while(my $line = ) { if ($line =~ /^\Q$perlvar{'lonHostID'}\E\:/) { (undef,undef,undef,$hostname)=split(/:/,$line); last; } } close(IN); } $certsdir = $perlstaticref->{'lonCertificateDirectory'}; $privkey = $perlstaticref->{'lonnetPrivateKey'}; $connectcsr = $perlstaticref->{'lonnetCertificate'}; $connectcsr =~ s/\.pem$/.csr/; $replicatecsr = $perlstaticref->{'lonnetHostnameCertificate'}; $replicatecsr =~ s/\.pem$/.csr/; $certmail = &get_mail(); print "\nRetrieving status information for SSL key and certificates ...\n\n"; my ($certinfo,$lonkeystatus,$lonhostcertstatus,$lonhostnamecertstatus,$sslref) = &get_cert_status($perlvar{'lonHostID'},$hostname,$perlstaticref); print $certinfo; my %sslstatus; if (ref($sslref) eq 'HASH') { %sslstatus = %{$sslref}; } while (!$flag) { print(<; chomp($choice); if ($choice==1) { if ($sslstatus{'key'} == 1) { print(<; chomp($choice2); if ($choice2 eq '1') { my $sslkeypass = &get_new_sslkeypass(); &make_key($certsdir,$privkey,$sslkeypass); } } elsif ($sslstatus{'key'} == 0) { print(<; chomp($choice2); if (($choice2 eq '1') || ($choice2 eq '2')) { &ssl_info(); my $country = &get_country($hostname); my $state = &get_state(); my $city = &get_city(); my $connectsubj = "/C=$country/ST=$state/O=$domainDescription/L=$city/CN=$perlvar{'lonHostID'}/OU=LONCAPA/emailAddress=$certmail"; ($domainDescription,$country,$state,$city) = &confirm_locality($domainDescription,$country,$state,$city); my $sslkeypass; if ($choice2 eq '1') { $sslkeypass = &get_new_sslkeypass(); &make_key($certsdir,$privkey,$sslkeypass); } elsif ($choice2 eq '2') { $sslkeypass = &get_password('Enter existing password for SSL key'); &encrypt_key($certsdir,$privkey,$sslkeypass); } &make_host_csr($certsdir,$sslkeypass,$connectcsr,$connectsubj); &mail_csr('host',$lonCluster,$perlvar{'lonHostID'},$hostname,$certsdir,$connectcsr,$replicatecsr,$perlstaticref); print "\nRetrieving status information for SSL key and certificates ...\n\n"; ($certinfo,$lonkeystatus,$lonhostcertstatus,$lonhostnamecertstatus,$sslref) = &get_cert_status($perlvar{'lonHostID'},$hostname,$perlstaticref); if (ref($sslref) eq 'HASH') { %sslstatus = %{$sslref}; } } elsif ($choice2 eq '3') { if (-e "$certsdir/$connectcsr") { &mail_csr('host',$lonCluster,$perlvar{'lonHostID'},$hostname,$certsdir,$connectcsr,$replicatecsr,$perlstaticref); } } } elsif (($sslstatus{'host'} == 0) || ($sslstatus{'host'} == 4) || ($sslstatus{'host'} == 5)) { my $sslkeypass; if ($sslstatus{'key'} == 1) { print(<; chomp($choice2); if ($choice2 eq '1') { $sslkeypass = &get_new_sslkeypass(); &make_key($certsdir,$privkey,$sslkeypass); } elsif ($choice2 eq '2') { $sslkeypass = &get_password('Enter existing password for SSL key'); &encrypt_key($certsdir,$privkey,$sslkeypass); } } else { print(<; chomp($choice2); if (($choice2 eq '1') || ($choice2 eq '2')) { &ssl_info(); my $country = &get_country($hostname); my $state = &get_state(); my $city = &get_city(); my $replicatesubj = "/C=$country/ST=$state/O=$domainDescription/L=$city/CN=internal-$hostname/OU=LONCAPA/emailAddress=$certmail"; my $sslkeypass; if ($choice2 eq '1') { $sslkeypass = &get_new_sslkeypass(); &make_key($certsdir,$privkey,$sslkeypass); } elsif ($choice2 eq '2') { $sslkeypass = &get_password('Enter existing password for SSL key'); &encrypt_key($certsdir,$privkey,$sslkeypass); } &make_hostname_csr($certsdir,$sslkeypass,$replicatecsr,$replicatesubj); &mail_csr('hostname',$lonCluster,$perlvar{'lonHostID'},$hostname,$certsdir,$connectcsr,$replicatecsr,$perlstaticref); print "\nRetrieving status information for SSL key and certificates ...\n\n"; ($certinfo,$lonkeystatus,$lonhostcertstatus,$lonhostnamecertstatus,$sslref) = &get_cert_status($perlvar{'lonHostID'},$hostname,$perlstaticref); if (ref($sslref) eq 'HASH') { %sslstatus = %{$sslref}; } } elsif ($choice2 eq '3') { if (-e "$certsdir/$replicatecsr") { &mail_csr('hostname',$lonCluster,$perlvar{'lonHostID'},$hostname,$certsdir,$connectcsr,$replicatecsr,$perlstaticref); } } } elsif (($sslstatus{'hostname'} == 0) || ($sslstatus{'hostname'} == 4) || ($sslstatus{'hostname'} == 5)) { my $sslkeypass; if ($sslstatus{'key'} == 1) { print(<; chomp($choice2); if ($choice2 eq '1') { $sslkeypass = &get_new_sslkeypass(); &make_key($certsdir,$privkey,$sslkeypass); } elsif ($choice2 eq '2') { $sslkeypass = &get_password('Enter existing password for SSL key'); &encrypt_key($certsdir,$privkey,$sslkeypass); } } else { print(<) { if ($configline =~ /^[^\#]?PerlSetVar/) { my ($unused,$varname,$varvalue)=split(/\s+/,$configline); chomp($varvalue); $LCperlvar{$varname}=$varvalue; } } close(CONFIG); } return \%LCperlvar; } sub get_sslnames { my %sslnames = ( key => 'lonnetPrivateKey', host => 'lonnetCertificate', hostname => 'lonnetHostnameCertificate', ca => 'lonnetCertificateAuthority', ); return %sslnames; } sub get_ssldesc { my %ssldesc = ( key => 'Private Key', host => 'Connections Certificate', hostname => 'Replication Certificate', ca => 'LON-CAPA CA Certificate', ); return %ssldesc; } sub get_cert_status { my ($lonHostID,$hostname,$perlvarstatic) = @_; my $currcerts = &LONCAPA::SSL::print_certstatus({$lonHostID => $hostname,},'text','cgi'); my ($lonkeystatus,$lonhostcertstatus,$lonhostnamecertstatus,%sslstatus); my $output = ''; if ($currcerts eq "$lonHostID:error") { $output .= "No information available for SSL certificates\n"; $sslstatus{'key'} = -1; $sslstatus{'host'} = -1; $sslstatus{'hostname'} = -1; $sslstatus{'ca'} = -1; $lonkeystatus = 'unknown status'; $lonhostcertstatus = 'unknown status'; $lonhostnamecertstatus = 'unknown status'; } else { my %sslnames = &get_sslnames(); my %ssldesc = &get_ssldesc(); my %csr; my ($lonhost,$info) = split(/\:/,$currcerts,2); if ($lonhost eq $lonHostID) { my @items = split(/\&/,$info); foreach my $item (@items) { my ($key,$value) = split(/=/,$item,2); if ($key =~ /^(host(?:|name))\-csr$/) { $csr{$1} = $value; } my @data = split(/,/,$value); if (grep(/^\Q$key\E$/,keys(%sslnames))) { my ($checkcsr,$comparecsr); if (lc($data[0]) eq 'yes') { $output .= "$ssldesc{$key} ".$perlvarstatic->{$sslnames{$key}}." available with status = $data[1]\n"; if ($key eq 'key') { $lonkeystatus = "status: $data[1]"; if ($data[1] =~ /ok$/) { $sslstatus{$key} = 1; } } else { my $setstatus; if (($key eq 'host') || ($key eq 'hostname')) { if ($data[1] eq 'otherkey') { $sslstatus{$key} = 4; $setstatus = 1; if ($key eq 'host') { $lonhostcertstatus = "status: created with different key"; } elsif ($key eq 'hostname') { $lonhostnamecertstatus = "status: created with different key"; } } elsif ($data[1] eq 'nokey') { $sslstatus{$key} = 5; $setstatus = 1; if ($key eq 'host') { $lonhostcertstatus = "status: created with missing key"; } elsif ($key eq 'hostname') { $lonhostnamecertstatus = "status: created with missing key"; } } if ($setstatus) { $comparecsr = 1; } } unless ($setstatus) { if ($data[1] eq 'expired') { $sslstatus{$key} = 2; if (($key eq 'host') || ($key eq 'hostname')) { $comparecsr = 1; } } elsif ($data[1] eq 'future') { $sslstatus{$key} = 3; $sslstatus{$key} = 3; } else { $sslstatus{$key} = 1; } if ($key eq 'host') { $lonhostcertstatus = "status: $data[1]"; } elsif ($key eq 'hostname') { $lonhostnamecertstatus = "status: $data[1]"; } } } } else { $sslstatus{$key} = 0; $output .= "$ssldesc{$key} ".$perlvarstatic->{$sslnames{$key}}." not available\n"; if ($key eq 'key') { $lonkeystatus = 'still needed'; } elsif (($key eq 'host') || ($key eq 'hostname')) { $checkcsr = 1; } } if (($checkcsr) || ($comparecsr)) { my $csrfile = $perlvarstatic->{$sslnames{$key}}; $csrfile =~s /\.pem$/.csr/; my $csrstatus; if (-e $perlvarstatic->{'lonCertificateDirectory'}."/$csrfile") { if (open(PIPE,"openssl req -text -noout -verify -in ".$perlvarstatic->{'lonCertificateDirectory'}."/$csrfile 2>&1 |")) { while() { chomp(); $csrstatus = $_; last; } close(PIPE); if ($comparecsr) { my $csrhash; if (open(PIPE,"openssl x509 -in certificate.crt -pubkey -noout -outform pem | sha256sum")) { $csrhash = ; close(PIPE); } } } $output .= "Certificate signing request for $ssldesc{$key} available with status = $csrstatus\n\n"; if ($key eq 'host') { $lonhostcertstatus = 'awaiting signature'; } else { $lonhostnamecertstatus = 'awaiting signature'; } $sslstatus{$key} = 3; } elsif ($checkcsr) { $output .= "No certificate signing request available for $ssldesc{$key}\n\n"; if ($key eq 'host') { $lonhostcertstatus = 'still needed'; } else { $lonhostnamecertstatus = 'still needed'; } } } } } # FIXME If different key, or missing key, or expired, check if there is a csr that does not match the cert, and that may be awaiting signature. } } return ($output,$lonkeystatus,$lonhostcertstatus,$lonhostnamecertstatus,\%sslstatus); } 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 SSL 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 SSL 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; } sub send_mail { my ($hostname,$recipient,$subj,$file) = @_; my $from = 'www@'.$hostname; my $certmail = "To: $recipient\n". "From: $from\n". "Subject: ".$subj."\n". "Content-type: text/plain\; charset=UTF-8\n". "MIME-Version: 1.0\n\n"; if (open(my $fh,"<$file")) { while (<$fh>) { $certmail .= $_; } close($fh); $certmail .= "\n\n"; if (open(my $mailh, "|/usr/lib/sendmail -oi -t -odb")) { print $mailh $certmail; close($mailh); print "Mail sent ($subj) to $recipient\n"; } else { print "Sending mail ($subj) to $recipient failed.\n"; } } return; } sub mail_csr { my ($types,$lonCluster,$lonHostID,$hostname,$certsdir,$connectcsr,$replicatecsr,$perlvarref) = @_; my ($camail,$flag); if ($lonCluster eq 'production' || $lonCluster eq 'development') { $camail = $perlvarref->{'SSLEmail'}; } else { $flag=0; # get Certificate Authority E-mail while (!$flag) { print(<; chomp($choice); if ($choice ne '') { open(OUT,'>>/tmp/loncapa_updatequery.out'); print(OUT 'Certificate Authority Email Address'."\t".$choice."\n"); close(OUT); $camail=$choice; $flag=1; } else { print "Invalid input (an email address is required).\n"; } } } if ($camail) { my $subj; if (($types eq 'both') || ($types = 'host')) { if (-e "$certsdir/$connectcsr") { $subj = "Certificate Request ($lonHostID)"; print(&send_mail($hostname,$camail,$subj,"$certsdir/$connectcsr")); } } if (($types eq 'both') || ($types = 'hostname')) { if (-e "$certsdir/$replicatecsr") { $subj = "Certificate Request (internal-$hostname)"; print(&send_mail($hostname,$camail,$subj,"$certsdir/$replicatecsr")); } } } } sub ssl_info { print(<; chomp($choice); if ($choice ne '') { if (&Locale::Country::code2country(lc($choice))) { open(OUT,'>>/tmp/loncapa_updatequery.out'); print(OUT 'country'."\t".uc($choice)."\n"); close(OUT); $country=uc($choice); $flag=1; } else { print "Invalid input -- a valid two letter country code is required\n"; } } elsif (($choice eq '') && ($posscountry ne '')) { open(OUT,'>>/tmp/loncapa_updatequery.out'); print(OUT 'country'."\t".$posscountry."\n"); close(OUT); $country = $posscountry; $flag = 1; } else { print "Invalid input -- a country code is required\n"; } } return $country; } sub get_state { # get State or Province my $flag=0; my $state = ''; while (!$flag) { print(<; chomp($choice); if ($choice ne '') { open(OUT,'>>/tmp/loncapa_updatequery.out'); print(OUT 'state'."\t".$choice."\n"); close(OUT); $state=$choice; $flag=1; } else { print "Invalid input (a state or province name is required).\n"; } } return $state; } sub get_city { # get City my $flag=0; my $city = ''; while (!$flag) { print(<; chomp($choice); if ($choice ne '') { open(OUT,'>>/tmp/loncapa_updatequery.out'); print(OUT 'city'."\t".$choice."\n"); close(OUT); $city=$choice; $flag=1; } else { print "Invalid input (a city is required).\n"; } } return $city; } sub confirm_locality { my ($domainDescription,$country,$state,$city) = @_; my $flag = 0; while (!$flag) { print(<; chomp($choice); if ($choice == 1) { print(<; chomp($choice2); $domainDescription=$choice2; } elsif ($choice == 2) { print(<; chomp($choice2); $country = uc($choice2); } elsif ($choice == 3) { print(<; chomp($choice2); $state=$choice2; } elsif ($choice == 4) { print(<; chomp($choice2); $city=$choice2; } elsif ($choice == 5) { $flag=1; $state =~ s{/}{ }g; $city =~ s{/}{ }g; $domainDescription =~ s{/}{ }g; } else { print "Invalid input.\n"; } } return ($domainDescription,$country,$state,$city); } sub make_key { my ($certsdir,$privkey,$sslkeypass) = @_; # generate SSL key if ($certsdir && $privkey) { if (-f "$certsdir/lonKey.enc") { my $mode = 0600; chmod $mode, "$certsdir/lonKey.enc"; } open(PIPE,"openssl genrsa -des3 -passout pass:$sslkeypass -out $certsdir/lonKey.enc 2048 2>&1 |"); close(PIPE); if (-f "$certsdir/$privkey") { my $mode = 0600; chmod $mode, "$certsdir/$privkey"; } open(PIPE,"openssl rsa -in $certsdir/lonKey.enc -passin pass:$sslkeypass -out $certsdir/$privkey -outform PEM |"); close(PIPE); if (-f "$certsdir/lonKey.enc") { my $mode = 0400; chmod $mode, "$certsdir/lonKey.enc"; } if (-f "$certsdir/$privkey") { my $mode = 0400; chmod $mode, "$certsdir/$privkey"; } } else { print "Key creation failed. Missing one or more of: certificates directory, key name\n"; } } sub encrypt_key { my ($certsdir,$privkey,$sslkeypass) = @_; if ($certsdir && $privkey) { if ((-f "$certsdir/$privkey") && (!-f "$certsdir/lonKey.enc")) { open(PIPE,"openssl rsa -des3 -in $certsdir/$privkey -out $certsdir/lonKey.enc |"); } } return; } sub make_host_csr { my ($certsdir,$sslkeypass,$connectcsr,$connectsubj) = @_; # generate SSL csr for hostID if ($certsdir && $connectcsr && $connectsubj) { open(PIPE,"openssl req -key $certsdir/lonKey.enc -passin pass:$sslkeypass -new -batch -subj \"$connectsubj\" -out $certsdir/$connectcsr |"); close(PIPE); } else { print "Creation of certificate signing request failed. Missing one or more of: certificates directory, CSR name, or locality information.\n"; } } sub make_hostname_csr { my ($certsdir,$sslkeypass,$replicatecsr,$replicatesubj) = @_; # generate SSL csr for internal hostname if ($certsdir && $replicatecsr && $replicatesubj) { open(PIPE,"openssl req -key $certsdir/lonKey.enc -passin pass:$sslkeypass -new -batch -subj \"$replicatesubj\" -out $certsdir/$replicatecsr |"); close(PIPE); } else { print "Creation of certificate signing request failed. Missing one or more of: certificates directory, CSR name, or locality information.\n"; } } sub get_mail { my $email; my $flag=0; # get E-mail Address while (!$flag) { print(<; chomp($choice); if (($choice ne '') && ($choice =~ /^[^\@]+\@[^\@]+$/)) { $email=$choice; $flag=1; } else { print "Invalid input (a valid email address is required).\n"; } } return $email; }