File:  [LON-CAPA] / loncom / CrCA.pl
Revision 1.3: download - view: text, annotated - select for diffs
Mon Jul 8 23:00:16 2019 UTC (3 years, 1 month ago) by raeburn
Branches: MAIN
CVS tags: HEAD
- Script to create a Certificate Authority (CA) for a LON-CAPA cluster.
  - Can display preamble then exit.
  - Prompt to create a new CA cert, even if an unexpired one exists.
  - Prompt for CA certificate lifetime (days) whenever new CA cert is made.
  - Remove some debug code.

    1: #!/usr/bin/perl
    2: # The LearningOnline Network with CAPA
    3: # Script to create a Certificate Authority (CA) for a LON-CAPA cluster.
    4: #
    5: # $Id: CrCA.pl,v 1.3 2019/07/08 23:00:16 raeburn Exp $
    6: #
    7: # Copyright Michigan State University Board of Trustees
    8: #
    9: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
   10: # LON-CAPA is free software; you can redistribute it and/or modify
   11: # it under the terms of the GNU General Public License as published by
   12: # the Free Software Foundation; either version 2 of the License, or
   13: # (at your option) any later version.
   14: #
   15: # LON-CAPA is distributed in the hope that it will be useful,
   16: # but WITHOUT ANY WARRANTY; without even the implied warranty of
   17: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   18: # GNU General Public License for more details.
   19: #
   20: # You should have received a copy of the GNU General Public License
   21: # along with LON-CAPA; if not, write to the Free Software
   22: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   23: #
   24: # /home/httpd/html/adm/gpl.txt
   25: #
   26: # http://www.lon-capa.org/
   27: 
   28: use strict;
   29: 
   30: #
   31: # Expected structure
   32: #
   33: #  /lonca
   34: #          opensslca.cnf
   35: #          cacert.pem
   36: #          index.txt
   37: #          /certs
   38: #          /crl
   39: #          /private
   40: #          /requests
   41: #
   42: 
   43:   print(<<END);
   44: 
   45: ****** SSL Certificate Authority *****
   46: 
   47: If you are running your own cluster of LON-CAPA nodes you will need to
   48: create a Certificate Authority (CA) for your cluster. You will then use
   49: the CA to sign LON-CAPA SSL certificate signing requests generated by
   50: the nodes in your cluster.
   51: 
   52: LON-CAPA SSL Certificates can be used in two different contexts:
   53: (a) if you configure LON-CAPA to use a secure channel for exchange of
   54: the shared encryption key when establishing an "internal" LON-CAPA
   55: connection between nodes in your cluster, and (b) if you configure
   56: LON-CAPA to use client SSL certificate validation when one node replicates
   57: content from library node(s) in your cluster.
   58: 
   59: Although a LON-CAPA cluster may contain multiple domains and/or multiple
   60: library nodes, there will only be one LON-CAPA certificate authority (CA)
   61: for the cluster.  The CA certificate signing infrastructure need not be 
   62: housed on a LON-CAPA node; it can instead be installed on a separate
   63: Linux instance.  The instance housing the CA infrastructure needs to
   64: have the following Linux packages installed:
   65: 
   66: openssl
   67: perl
   68: 
   69: and the following perl modules from CPAN installed:
   70: 
   71: Term::ReadKey
   72: Sys::Hostname::FQDN
   73: Locale::Country
   74: Crypt::OpenSSL::X509
   75: DateTime::Format::x509
   76: File::Slurp
   77: 
   78: You need to decide on a directory you wish to use to hold the
   79: CA infrastructure. If necessary you should create a new directory.
   80: Then move this script (CrCA.pl) to that directory, and run it with
   81: the command: perl CrCA.pl
   82: 
   83: The script will create any required subdirectories (and files) 
   84: within that directory, if they do not already exist.
   85: 
   86: You will need to provide a password to be used for the openssl CA key 
   87: which will be stored in the /private subdirectory, and will be used
   88: when signing certificate signing requests to create LON-CAPA certificates 
   89: for use in the cluster.
   90: 
   91: END
   92: 
   93:   print ('Continue? [Y/n]');
   94:   my $go_on = &get_user_selection(1);
   95:   if (!$go_on) {
   96:       exit;
   97:   }
   98: 
   99:   require Sys::Hostname::FQDN;
  100:   require Term::ReadKey;
  101:   require Locale::Country;
  102:   require Crypt::OpenSSL::X509;
  103:   require DateTime::Format::x509;
  104:   require File::Slurp;
  105:   require Cwd;
  106: 
  107:   my ($dir,$hostname,%data);
  108: 
  109: # Check if required subdirectories exist in current directory.
  110:   $dir = Cwd::getcwd();
  111: 
  112:   if (-e "$dir/lonca") {
  113:       if ((!-d "$dir/lonca") && (-f "$dir/lonca")) {
  114:           print "A lonca directory is required, but there is an existing file of that name.\n".
  115:                 "Please either delete the lonca file, or change to a different directory, and ".
  116:                 "create the CA infrastructure there.\n";
  117:           exit;
  118:       }
  119:   } else {
  120:       mkdir("$dir/lonca",0700);
  121:       system('chown root:root '."$dir/lonca");
  122:   }
  123:   if (-d "$dir/lonca") {
  124:       foreach my $subdir ('certs','crl','private','requests') {
  125:           if (!-d "$dir/lonca/$subdir") {
  126:               if (-f "$dir/lonca/$subdir") {
  127:                   print "A $subdir sub-directory is required, but there is an existing file of that name.\n".
  128:                         "Please either delete or move the $subdir file, then run this script again.\n";
  129:                   exit;
  130:               } else {
  131:                   mkdir("$dir/lonca/$subdir",0700);
  132:                   system('chown root:root '."$dir/lonca/$subdir");
  133:               }
  134:           }
  135:       }
  136:   } else {
  137:       print "A lonca directory is required, but no directory exists\n";
  138:       exit;
  139:   }  
  140:   if (-e "$dir/lonca/opensslca.conf") {
  141:       # retrieve existing config file and verify that if contains the required fields.
  142:       %data = &parse_config("$dir/lonca/opensslca.conf");
  143:       my %update = &confirm_config(%data);
  144:       my %changes;
  145:       foreach my $field ('clustername','organization','email','country','state','city','days','crldays') {
  146:           if ($data{$field} ne $update{$field}) {
  147:               $changes{$field} = $update{$field};
  148:           }
  149:       }
  150:       if (keys(%changes)) {
  151:           &save_config_changes("$dir/lonca/opensslca.conf",\%changes);
  152:       }
  153:   } else {
  154:       print(<<END);
  155: ****** Certificate Authority Configuration File *****
  156: 
  157: A configuration file: $dir/lonca/opensslca.conf will be created.
  158: 
  159: The following information will be included: 
  160: Country, State/Province, City, Cluster Name, Organizational Name, E-mail address, Default certificate lifetime (days), CRL re-creation interval (days)
  161: 
  162: END
  163:       $hostname = Sys::Hostname::FQDN::fqdn();
  164:       if ($hostname eq '') {
  165:           $hostname =&get_hostname();
  166:       } else {
  167:           print "Hostname detected: $hostname. Is that correct? [Y/n]";
  168:           if (!&get_user_selection(1)) {
  169:               $hostname =&get_hostname();
  170:           }
  171:       }
  172: 
  173:       my %fieldname = (
  174:                         city => 'City',
  175:                         state => 'State or Province',
  176:                         clustername => 'Cluster name',
  177:                         organization => 'Organization name',
  178:                       );
  179:       my ($clustername,$organization,$country,$state,$city,$email,$clusterhostname,$days,$crldays);
  180:       $clusterhostname =  $hostname;
  181:       $country = &get_country($hostname);
  182:       print "Enter state or province name\n";
  183:       $state = &get_info($fieldname{'state'});
  184:       print "Enter city name\n";
  185:       $city = &get_info($fieldname{'city'});
  186:       $email = &get_camail();
  187:       print 'Enter a name for this LON-CAPA cluster, e.g., "Lon-CAPA learning network"'."\n".
  188:             'This name will be included as the Common Name for the CA certificate.'."\n";
  189:       $clustername = &get_info($fieldname{'clustername'});
  190:       print 'Enter the organization name for this LON-CAPA cluster, e.g., "Lon CAPA certification authority"'."\n".
  191:             'This name will be included as the Organization for the CA certificate.'."\n";    
  192:       $organization = &get_info($fieldname{'organization'});
  193:       print "Enter the default lifetime (in days) for each certificate created/signed by the CA for individual nodes, e.g., 3650\n";
  194:       $days = &get_days();
  195:       print "Enter the re-creation interval (in days) for the CA's certificate revocation list (CRL), e.g., 180\n";
  196:       $crldays = &get_days();
  197: 
  198:       if (open(my $fh,'>',"$dir/lonca/opensslca.conf")) {
  199:           print $fh <<"END";
  200: [ ca ]
  201: default_ca       =  loncapa
  202: 
  203: [ loncapa ]
  204: dir              = $dir/lonca
  205: certificate      = $dir/lonca/cacert.pem
  206: database         = $dir/lonca/index.txt
  207: new_certs_dir    = $dir/lonca/certs
  208: private_key      = $dir/lonca/private/cakey.pem
  209: serial           = $dir/lonca/serial
  210: 
  211: default_crl_days = $crldays
  212: default_days     = $days
  213: default_md       = sha256
  214: 
  215: policy           = loncapa_policy
  216: x509_extensions  = certificate_extensions
  217: 
  218: [ loncapa_policy ]
  219: 
  220: commonName           = supplied
  221: stateOrProvinceName  = supplied
  222: countryName          = supplied
  223: emailAddress         = supplied
  224: organizationName     = supplied
  225: organizationalUnitName = optional
  226: 
  227: [ certificate_extensions ]
  228: 
  229: basicConstraints   = CA:false
  230: crlDistributionPoints = URI:http://$clusterhostname/adm/dns/loncapaCRL
  231: 
  232: [ req ]
  233: 
  234: default_bits       = 2048
  235: distinguished_name = loncapa_ca
  236: 
  237: x509_extensions    = loncapa_ca_extensions
  238: 
  239: [ loncapa_ca ]
  240: 
  241: commonName           = $clustername
  242: localityName         = $city
  243: stateOrProvinceName  = $state
  244: countryName          = $country
  245: emailAddress         = $email
  246: organizationName     = $organization
  247: 
  248: [ loncapa_ca_extensions ]
  249: basicConstraints  = CA:true
  250: 
  251: [ crl_ext ]
  252: 
  253: authorityKeyIdentifier=keyid:always,issuer:always
  254: 
  255: 
  256: END
  257: 
  258:       } else {
  259:           print 'Error: failed to wtite to '."$dir/lonca/opensslca.conf. Exiting.\n";
  260:           exit;
  261:       }
  262:       %data = &parse_config("$dir/lonca/opensslca.conf");
  263:       my %update = &confirm_config(%data);
  264:       my %changes;
  265:       foreach my $field ('clustername','organization','email','country','state','city','days','crldays') {
  266:           if ($data{$field} ne $update{$field}) {
  267:               $changes{$field} = $update{$field};
  268:           }
  269:       }
  270:       if (keys(%changes)) {
  271:           &save_config_changes("$dir/lonca/opensslca.conf",\%changes);
  272:       }
  273:   }
  274: 
  275:   my $sslkeypass;
  276:   if (-e "$dir/lonca/private/cakey.pem") {
  277:       my ($keyok,$try);
  278:       print "CA key aleady exists\n";
  279:       $try = 1;
  280:       while (!$keyok && $try) {
  281:           $sslkeypass = &get_password('Enter the password for the CA key');
  282:           if ($sslkeypass ne '') {
  283:               open(PIPE,"openssl rsa -noout -in lonca/private/cakey.pem -passin pass:$sslkeypass -check |");
  284:               my $check = <PIPE>;
  285:               close(PIPE);
  286:               chomp($check);
  287:               if ($check eq 'RSA key ok') {
  288:                   $keyok = 1;
  289:                   last;
  290:               } else {
  291:                   print "CA key check failed. Try again? [Y/n]";
  292:                   if (!&get_user_selection(1)) {
  293:                       $try = 0;
  294:                   }
  295:               }
  296:           }
  297:       }
  298:       unless ($keyok) {
  299:           print "CA key check failed. Create a new key? [Y/n]";
  300:           if (&get_user_selection(1)) {
  301:               $sslkeypass = &get_new_sslkeypass();
  302:               # generate SSL key
  303:               unless (&make_key("$dir/lonca/private",$sslkeypass)) {
  304:                   print "Failed to create CA key\n";
  305:                   exit;
  306:               }
  307:           } else {
  308:               exit;
  309:           }
  310:       } 
  311:   } else {
  312:       $sslkeypass = &get_new_sslkeypass();
  313:       # generate SSL key
  314:       unless (&make_key("$dir/lonca/private",$sslkeypass)) {
  315:           print "Failed to create CA key\n";
  316:           exit;
  317:       }
  318:   }
  319:   my $makecacert;
  320:   if (-e "$dir/lonca/cacert.pem") {
  321:       print "A CA certificate exists\n";
  322:       open(PIPE,"openssl pkey -in $dir/lonca/private/cakey.pem -passin pass:$sslkeypass -pubout -outform der | sha256sum |");
  323:       my $hashfromkey = <PIPE>;
  324:       close(PIPE);
  325:       chomp($hashfromkey);
  326:       open(PIPE,"openssl x509 -in $dir/lonca/cacert.pem -pubkey | openssl pkey -pubin -pubout -outform der | sha256sum |");
  327:       my $hashfromcert = <PIPE>;
  328:       close(PIPE);
  329:       chomp($hashfromcert);
  330:       my $defsel = 0;
  331:       if ($hashfromkey eq $hashfromcert) {
  332:           my ($now,$starttime,$endtime,$status,%cert);
  333:           my $x509 = Crypt::OpenSSL::X509->new_from_file("$dir/lonca/cacert.pem");
  334:           my @items = split(/,\s+/,$x509->subject());
  335:           foreach my $item (@items) {
  336:               my ($name,$value) = split(/=/,$item);
  337:               if ($name eq 'CN') {
  338:                   $cert{'cn'} = $value;
  339:               }
  340:           }
  341:           $cert{'start'} = $x509->notBefore();
  342:           $cert{'end'} = $x509->notAfter();
  343:           $cert{'alg'} = $x509->sig_alg_name();
  344:           $cert{'size'} = $x509->bit_length();
  345:           $cert{'email'} = $x509->email();
  346:           my $dt = DateTime::Format::x509->parse_datetime($cert{'start'});
  347:           if (ref($dt)) {
  348:               $starttime = $dt->epoch;
  349:           }
  350:           $dt =  DateTime::Format::x509->parse_datetime($cert{'end'});
  351:           if (ref($dt)) {
  352:               $endtime = $dt->epoch;
  353:           }
  354:           $now = time;
  355:           if (($starttime ne '') && ($endtime ne '')) {
  356:               if ($endtime <= $now) {
  357:                   $status = 'previous';
  358:                   print "Current CA certificate expired $cert{'end'}\n"; 
  359:                   print 'Create a new certificate? [Y/n]';
  360:                   $defsel = 1;
  361:               } elsif ($starttime > $now) {
  362:                   $status = 'future';
  363:                   print "Current CA certificate will be valid after $cert{'start'}\n";
  364:                   print 'Create a new certificate? [y/N]';
  365:               } else {
  366:                   $status eq 'active';
  367:                   print "Current CA certificate valid until $cert{'end'}".' '.
  368:                         "Signature Algorithm: $cert{'alg'}; Public Key size: $cert{'size'}\n"; 
  369:                   print 'Create a new certificate? [y/N]';
  370:               }
  371:           } else {
  372:               print "Could not determine validity of current CA certificate\n";
  373:               print 'Create a new certificate? [Y/n]';
  374:               $defsel = 1;
  375:           }
  376:       } else {
  377:           print "Current CA certificate does not match key.\n";
  378:           print 'Create a new certificate? [Y/n]';
  379:           $defsel = 1;
  380:       }
  381:       if (&get_user_selection($defsel)) {
  382:           $makecacert = 1;
  383:       }
  384:   } else {
  385:       $makecacert = 1;
  386:   }
  387:   if ($makecacert) {
  388:       print "Enter the lifetime (in days) for the CA root certificate distributed to all nodes, e.g., 3650\n";
  389:       my $cadays = &get_days();
  390:       unless (&make_ca_cert("$dir/lonca/private","$dir/lonca",$sslkeypass,$cadays)) {
  391:           print "Failed to create CA cert\n";
  392:           exit;
  393:       }
  394:   }
  395: 
  396:   if (!-e "$dir/lonca/index.txt") {
  397:       File::Slurp::write_file("$dir/lonca/index.txt");
  398:   }
  399:   if (-e "$dir/lonca/index.txt") {
  400:       my $mode = 0600;
  401:       chmod $mode, "$dir/lonca/index.txt";
  402:   } else {
  403:       print "lonca/index.txt file is missing\n";
  404:       exit; 
  405:   }    
  406: # echo 1000 > serial
  407: 
  408: 
  409:   unless (-e "$dir/lonca/crl/loncapaCAcrl.pem") {
  410:       open(PIPE,"openssl ca -gencrl -keyfile $dir/lonca/private/cakey.pem -cert $dir/lonca/cacert.pem -out $dir".
  411:                 "/lonca/crl/loncapaCAcrl.pem -config $dir/lonca/opensslca.conf -passin pass:$sslkeypass |");
  412:       close(PIPE);
  413:       if (-e "$dir/lonca/crl/loncapaCAcrl.pem") {
  414:           print "Certificate Revocation List created\n";
  415:       }
  416:   }
  417:   if (-e "$dir/lonca/crl/loncapaCAcrl.pem") {
  418:       open(PIPE,"openssl crl -in $dir/lonca/crl/loncapaCAcrl.pem -inform pem -CAfile $dir/lonca/cacert.pem  -noout 2>&1 |");
  419:       my $revoked = <PIPE>;
  420:       close(PIPE);
  421:       chomp($revoked);
  422:       print "Revocation certificate status: $revoked\n";
  423: # Create a new one? 
  424:   }
  425: 
  426: sub cafield_to_key {
  427:     my %mapping = (
  428:                     city         => 'localityName',
  429:                     state        => 'stateOrProvinceName',
  430:                     country      => 'countryName',
  431:                     email        => 'emailAddress',
  432:                     organization => 'organizationName',
  433:                     clustername  => 'commonName',
  434:                   );
  435:     return %mapping;
  436: }
  437: 
  438: sub field_to_key {
  439:     my %mapping = (
  440:                     days    => 'default_days',
  441:                     crldays => 'default_crl_days',
  442:                   );
  443: }
  444: 
  445: sub parse_config {
  446:     my ($filepath) = @_;
  447:     my (%fields,%data);
  448:     if (open(my $fh,'<',$filepath)) {
  449:         my $currsection;
  450:         while(<$fh>) {
  451:             chomp();
  452:             s/(^\s+|\s+$)//g;
  453:             if (/^\[\s*([^\s]+)\s*\]/) {
  454:                 $currsection = $1;
  455:             } elsif (/^([^=]+)=([^=]+)$/) {
  456:                 my ($key,$value) = ($1,$2);
  457:                 $key =~ s/\s+$//;
  458:                 $value =~ s/^\s+//;
  459:                 if ($currsection ne '') {
  460:                     $fields{$currsection}{$key} = $value;
  461:                 }
  462:             }
  463:         }
  464:         close($fh); 
  465:     }
  466:     if (ref($fields{'loncapa_ca'}) eq 'HASH') {
  467:         my %ca_mapping = &cafield_to_key();
  468:         foreach my $key (keys(%ca_mapping)) {
  469:             $data{$key} = $fields{'loncapa_ca'}{$ca_mapping{$key}};
  470:         }
  471:     }
  472:     if (ref($fields{'loncapa'}) eq 'HASH') {
  473:         my %mapping = &field_to_key();
  474:         foreach my $key (keys(%mapping)) {
  475:             $data{$key} = $fields{'loncapa'}{$mapping{$key}};
  476:         }
  477:     }
  478:     return %data; 
  479: }
  480: 
  481: sub save_config_changes {
  482:     my ($filepath,$updated) = @_;
  483:     return unless (ref($updated) eq 'HASH');
  484:     my %mapping = &field_to_key();
  485:     my %ca_mapping = &cafield_to_key();
  486:     my %revmapping = reverse(%mapping);
  487:     my %rev_ca_mapping = reverse(%ca_mapping);
  488:     my $lines;
  489:     if (open(my $fh,'<',$filepath)) {
  490:         my $currsection;
  491:         while(<$fh>) {
  492:             my $line = $_;
  493:             chomp();
  494:             s/(^\s+|\s+$)//g;
  495:             my $newline;
  496:             if (/^\[\s*([^\s]+)\s*\]/) {
  497:                 $currsection = $1;
  498:             } elsif (/^([^=]+)=([^=]*)$/) {
  499:                 my ($origkey,$origvalue) = ($1,$2);
  500:                 my ($key,$value) = ($origkey,$origvalue);
  501:                 $key =~ s/\s+$//;
  502:                 $value =~ s/^\s+//;
  503:                 if ($currsection eq 'loncapa_ca') {
  504:                     if ((exists($rev_ca_mapping{$key})) && (exists($updated->{$rev_ca_mapping{$key}}))) {
  505:                         if ($value eq '') {
  506:                             if ($origvalue eq '') {
  507:                                 $origvalue = ' ';
  508:                             }
  509:                             $origvalue .= $updated->{$rev_ca_mapping{$key}};
  510:                         } else {
  511:                             $origvalue =~ s/\Q$value\E/$updated->{$rev_ca_mapping{$key}}/;
  512:                         }
  513:                         $newline = $origkey.'='.$origvalue."\n";
  514:                     }
  515:                 } elsif ($currsection eq 'loncapa') {
  516:                     if ((exists($revmapping{$key})) && (exists($updated->{$revmapping{$key}}))) {
  517:                         if ($value eq '') {
  518:                             if ($origvalue eq '') {
  519:                                 $origvalue = ' ';
  520:                             }
  521:                             $origvalue .= $updated->{$revmapping{$key}};
  522:                         } else {
  523:                             $origvalue =~ s/\Q$value\E/$updated->{$revmapping{$key}}/;
  524:                         }
  525:                         $newline = $origkey.'='.$origvalue."\n";
  526:                     }
  527:                 }
  528:             }
  529:             if ($newline) {
  530:                 $lines .= $newline;
  531:             } else {
  532:                 $lines .= $line;
  533:             }
  534:         }
  535:         close($fh);
  536:         if (open(my $fout,'>',$filepath)) {
  537:             print $fout $lines;
  538:             close($fout);
  539:         } else {
  540:             print "Error: failed to open '$filepath' for writing\n"; 
  541:         }
  542:     }
  543:     return;
  544: }
  545: 
  546: #
  547: # get_hostname() prompts the user to provide the server's hostname.
  548: #
  549: # If invalid input is provided, the routine is called recursively
  550: # until, a valid hostname is provided.
  551: #
  552: 
  553: sub get_hostname {
  554:     my $hostname;
  555:     print 'Enter the hostname of this server, e.g., loncapa.somewhere.edu'."\n";
  556:     my $choice = <STDIN>;
  557:     chomp($choice);
  558:     $choice =~ s/(^\s+|\s+$)//g;
  559:     if ($choice eq '') {
  560:         print "Hostname you entered was either blank or contanied only white space.\n";
  561:     } elsif ($choice =~ /^[\w\.\-]+$/) {
  562:         $hostname = $choice;
  563:     } else {
  564:         print "Hostname you entered was invalid --  a hostname may only contain letters, numbers, - and .\n";
  565:     }
  566:     while ($hostname eq '') {
  567:         $hostname = &get_hostname();
  568:     }
  569:     print "\n";
  570:     return $hostname;
  571: }
  572: 
  573: sub get_new_sslkeypass {
  574:     my $sslkeypass;
  575:     my $flag=0;
  576: # get password for SSL key
  577:     while (!$flag) {
  578:         $sslkeypass = &make_passphrase();
  579:         if ($sslkeypass) {
  580:             $flag = 1;
  581:         } else {
  582:             print "Invalid input (a password is required for the CA key).\n";
  583:         }
  584:     }
  585:     return $sslkeypass;
  586: }
  587: 
  588: sub make_passphrase {
  589:     my ($got_passwd,$firstpass,$secondpass,$passwd);
  590:     my $maxtries = 10;
  591:     my $trial = 0;
  592:     while ((!$got_passwd) && ($trial < $maxtries)) {
  593:         $firstpass = &get_password('Enter a password for the CA key (at least 6 characters long)');
  594:         if (length($firstpass) < 6) {
  595:             print('Password too short.'."\n".
  596:               'Please choose a password with at least six characters.'."\n".
  597:               'Please try again.'."\n");
  598:         } elsif (length($firstpass) > 30) {
  599:             print('Password too long.'."\n".
  600:                   'Please choose a password with no more than thirty characters.'."\n".
  601:                   'Please try again.'."\n");
  602:         } else {
  603:             my $pbad=0;
  604:             foreach (split(//,$firstpass)) {if ((ord($_)<32)||(ord($_)>126)){$pbad=1;}}
  605:             if ($pbad) {
  606:                 print('Password contains invalid characters.'."\n".
  607:                       'Password must consist of standard ASCII characters.'."\n".
  608:                       'Please try again.'."\n");
  609:             } else {
  610:                 $secondpass = &get_password('Enter password a second time');
  611:                 if ($firstpass eq $secondpass) {
  612:                     $got_passwd = 1;
  613:                     $passwd = $firstpass;
  614:                 } else {
  615:                     print('Passwords did not match.'."\n".
  616:                           'Please try again.'."\n");
  617:                 }
  618:             }
  619:         }
  620:         $trial ++;
  621:     }
  622:     return $passwd;
  623: }
  624: 
  625: sub get_password {
  626:     my ($prompt) = @_;
  627:     local $| = 1;
  628:     print $prompt.': ';
  629:     my $newpasswd = '';
  630:     Term::ReadKey::ReadMode('raw');
  631:     my $key;
  632:     while(ord($key = Term::ReadKey::ReadKey(0)) != 10) {
  633:         if(ord($key) == 127 || ord($key) == 8) {
  634:             chop($newpasswd);
  635:             print "\b \b";
  636:         } elsif(!ord($key) < 32) {
  637:             $newpasswd .= $key;
  638:             print '*';
  639:         }
  640:     }
  641:     Term::ReadKey::ReadMode('normal');
  642:     print "\n";
  643:     return $newpasswd;
  644: }
  645: 
  646: #
  647: # make_key() generates CA root key
  648: #
  649: 
  650: sub make_key {
  651:     my ($keydir,$sslkeypass) = @_;
  652: # generate SSL key
  653:     my $created;
  654:     if (($keydir ne '') && ($sslkeypass ne '')) {
  655:         if (-f "$keydir/cakey.pem") {
  656:             my $mode = 0600;
  657:             chmod $mode, "$keydir/cakey.pem";
  658:         }
  659:         open(PIPE,"openssl genrsa -aes256 -passout pass:$sslkeypass -out $keydir/cakey.pem 2048 2>&1 |");
  660:         close(PIPE);
  661:         if (-f "$keydir/cakey.pem") {
  662:             my $mode = 0400;
  663:             chmod $mode, "$keydir/cakey.pem";
  664:             $created= 1;
  665:         }
  666:     } else {
  667:         print "Key creation failed.  Missing one or more of: certificates directory, key name\n";
  668:     }
  669:     return $created;
  670: }
  671: 
  672: #
  673: # make_ca_cert() generates CA root certificate
  674: #
  675: 
  676: sub make_ca_cert {
  677:     my ($keydir,$certdir,$sslkeypass,$cadays) = @_;
  678: # generate SSL cert for CA
  679:     my $created;
  680:     if ((-d $keydir) && (-d $certdir) && ($sslkeypass ne '') && ($cadays =~ /^\d+$/) && ($cadays > 0))  {
  681:         open(PIPE,"openssl req -x509 -key $keydir/cakey.pem -passin pass:$sslkeypass -new -days $cadays -batch -config $certdir/opensslca.conf -out $certdir/cacert.pem |");
  682:         close(PIPE);
  683:         if (-f "$certdir/cacert.pem") {
  684:             my $mode = 0600;
  685:             chmod $mode, "$certdir/cacert.pem";
  686:             $created= 1;
  687:         }
  688:     } else {
  689:         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";
  690:     }
  691:     return $created;
  692: }
  693: 
  694: sub get_camail {
  695:     my $camail;
  696:     my $flag=0;
  697: # get Certificate Authority E-mail
  698:     while (!$flag) {
  699:         print(<<END);
  700: 
  701: Enter e-mail address of Certificate Authority. 
  702: END
  703: 
  704:         my $choice=<>;
  705:         chomp($choice);
  706:         if (($choice ne '') && ($choice =~ /^[^\@]+\@[^\@]+$/)) {
  707:             $camail=$choice;
  708:             $flag=1;
  709:         } else {
  710:             print "Invalid input (a valid email address is required).\n";
  711:         }
  712:     }
  713:     return $camail;
  714: }
  715: 
  716: sub ssl_info {
  717:     print(<<END);
  718: 
  719: ****** Information about Country, State or Province and City *****
  720: 
  721: A two-letter country code, e.g., US, CA, DE etc. as defined by ISO 3166,
  722: is required. A state or province, and a city are also required.
  723: This locality information is included in two SSL certificates used internally
  724: by LON-CAPA, unless you are running standalone.
  725: 
  726: If your server will be part of either the production or development
  727: clusters, then the certificate will need to be signed by the official
  728: LON-CAPA Certificate Authority (CA).  If you will be running your own
  729: cluster then the cluster will need to create its own CA.
  730: 
  731: END
  732: }
  733: 
  734: sub get_country {
  735:     my ($desiredhostname) = @_;
  736: # get Country
  737:     my ($posscountry,$country);
  738:     if ($desiredhostname =~ /\.(edu|com|org)$/) {
  739:         $posscountry = 'us';
  740:     } else {
  741:         ($posscountry) = ($desiredhostname =~ /\.(a-z){2}$/);
  742:     }
  743:     if ($posscountry) {
  744:         my $countrydesc = Locale::Country::code2country($posscountry);
  745:         if ($countrydesc eq '') {
  746:             undef($posscountry);
  747:         }
  748:     }
  749: 
  750:     my $flag=0;
  751:     while (!$flag) {
  752:         if ($posscountry) {
  753:             $posscountry = uc($posscountry);
  754:             print "Enter Two-Letter Country Code [$posscountry]:\n";
  755:         } else {
  756:             print "Enter the Two-Letter Country Code:\n";
  757:         }
  758:         my $choice=<STDIN>;
  759:         chomp($choice);
  760:         if ($choice ne '') {
  761:             if (Locale::Country::code2country(lc($choice))) {
  762:                 $country=uc($choice);
  763:                 $flag=1;
  764:             } else {
  765:                 print "Invalid input -- a valid two letter country code is required\n";
  766:             }
  767:         } elsif (($choice eq '') && ($posscountry ne '')) {
  768:             $country = $posscountry;
  769:             $flag = 1;
  770:         } else {
  771:             print "Invalid input -- a country code is required\n";
  772:         }
  773:     }
  774:     return $country;
  775: }
  776: 
  777: sub get_info {
  778:     my ($typename) = @_;
  779:     my $value;
  780:     my $choice = <STDIN>;
  781:     chomp($choice);
  782:     $choice =~ s/(^\s+|\s+$)//g;
  783:     if ($choice eq '') {
  784:         print "$typename you entered was either blank or contained only white space.\n";
  785:     } else {
  786:         $value = $choice;
  787:     }
  788:     while ($value eq '') {
  789:         $value = &get_info($typename);
  790:     }
  791:     print "\n";
  792:     return $value;
  793: }
  794: 
  795: sub get_days {
  796:     my $value;
  797:     my $choice = <STDIN>;
  798:     chomp($choice);
  799:     $choice =~ s/(^\s+|\s+$)//g;
  800:     if ($choice eq '') {
  801:         print "The value you entered was either blank or contained only white space.\n";
  802:     } elsif ($choice !~ /^\d+$/) {
  803:         print "The value you entered contained invalid characters -- you must enter just an integer.\n";
  804:     } else {
  805:         $value = $choice;
  806:     }
  807:     while ($value eq '') {
  808:         $value = &get_days();
  809:     }
  810:     print "\n";
  811:     return $value;
  812: }
  813: 
  814: sub confirm_config {
  815:     my (%data) = @_;
  816:     my $flag = 0;
  817:     while (!$flag) {
  818:         print(<<END);
  819: 
  820: The cluster name, organization name, country, state and city will be 
  821: included in the CA certificate
  822: 
  823: 1) Cluster Name: $data{'clustername'}
  824: 2) Organization Name: $data{'organization'}
  825: 3) Country: $data{'country'}
  826: 4) State or Province: $data{'state'}
  827: 5) City: $data{'city'}
  828: 6) E-mail: $data{'email'}
  829: 7) Default certificate lifetime for issued certs (days): $data{'days'}
  830: 8) CRL recreation interval (days): $data{'crldays'}
  831: 9) Everything is correct up above
  832: 
  833: Enter a choice of 1-8 to change, otherwise enter 9: 
  834: END
  835:         my $choice=<STDIN>;
  836:         chomp($choice);
  837:         if ($choice == 1) {
  838:             print(<<END);
  839: 1) Cluster Name: $data{'clustername'}
  840: Enter new value:
  841: END
  842:             my $choice2=<STDIN>;
  843:             chomp($choice2);
  844:             $data{'clustername'}=$choice2;
  845:             chomp($choice2);
  846:             $data{'organization'}=$choice2;
  847:         } elsif ($choice == 3) {
  848:             print(<<END);
  849: 3) Country: $data{'country'}
  850: Enter new value (this should be a two-character code, e,g, US, CA, DE):
  851: END
  852:             my $choice2=<STDIN>;
  853:             chomp($choice2);
  854:             $data{'country'} = uc($choice2);
  855:         } elsif ($choice == 4) {
  856:             print(<<END);
  857: 4) State or Province: $data{'state'}
  858: Enter new value:
  859: END
  860:             my $choice2=<>;
  861:             chomp($choice2);
  862:             $data{'state'}=$choice2;
  863:         } elsif ($choice == 5) {
  864:             print(<<END);
  865: 5) City: $data{'city'}
  866: Enter new value:
  867: END
  868:             my $choice2=<>;
  869:             chomp($choice2);
  870:             $data{'city'}=$choice2;
  871:         } elsif ($choice == 6) {
  872:             print(<<END);
  873: 6) E-mail: $data{'email'}
  874: Enter new value:
  875: END
  876:             my $choice2=<>;
  877:             chomp($choice2);
  878:             $data{'email'}=$choice2;
  879:         } elsif ($choice == 7) {
  880: print(<<END);
  881: 7) Default certificate lifetime: $data{'days'}
  882: Enter new value:
  883: END
  884:             my $choice2=<>;
  885:             chomp($choice2);
  886:             $choice2 =~ s/\D//g;
  887:             $data{'days'}=$choice2;
  888:         } elsif ($choice == 8) {
  889: print(<<END);
  890: 8) CRL re-creation interval: $data{'crldays'}
  891: Enter new value:
  892: END
  893:             my $choice2=<>;
  894:             chomp($choice2);
  895:             $choice2 =~ s/\D//g;
  896:             $data{'crldays'}=$choice2;
  897:         } elsif ($choice == 9) {
  898:             $flag=1;
  899:             foreach my $key (keys(%data)) { 
  900:                 $data{$key} =~ s{/}{ }g;
  901:             }  
  902:         } else {
  903:             print "Invalid input.\n";
  904:         }
  905:     }
  906:     return %data; 
  907: }
  908: 
  909: sub get_user_selection {
  910:     my ($defaultrun) = @_;
  911:     my $do_action = 0;
  912:     my $choice = <STDIN>;
  913:     chomp($choice);
  914:     $choice =~ s/(^\s+|\s+$)//g;
  915:     my $yes = 'y';
  916:     if ($defaultrun) {
  917:         if (($choice eq '') || ($choice =~ /^\Q$yes\E/i)) {
  918:             $do_action = 1;
  919:         }
  920:     } else {
  921:         if ($choice =~ /^\Q$yes\E/i) {
  922:             $do_action = 1;
  923:         }
  924:     }
  925:     return $do_action;
  926: }
  927: 

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>