Annotation of loncom/CrCA.pl, revision 1.3
1.1 raeburn 1: #!/usr/bin/perl
1.2 raeburn 2: # The LearningOnline Network with CAPA
3: # Script to create a Certificate Authority (CA) for a LON-CAPA cluster.
4: #
1.3 ! raeburn 5: # $Id: CrCA.pl,v 1.2 2019/01/01 04:55:00 raeburn Exp $
1.2 raeburn 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:
1.1 raeburn 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:
1.3 ! raeburn 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;
1.1 raeburn 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:
1.3 ! raeburn 160: Country, State/Province, City, Cluster Name, Organizational Name, E-mail address, Default certificate lifetime (days), CRL re-creation interval (days)
1.1 raeburn 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: );
1.3 ! raeburn 179: my ($clustername,$organization,$country,$state,$city,$email,$clusterhostname,$days,$crldays);
1.1 raeburn 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".
1.3 ! raeburn 191: 'This name will be included as the Organization for the CA certificate.'."\n";
1.1 raeburn 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
1.3 ! raeburn 230: crlDistributionPoints = URI:http://$clusterhostname/adm/dns/loncapaCRL
1.1 raeburn 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: }
1.3 ! raeburn 319: my $makecacert;
1.1 raeburn 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);
1.3 ! raeburn 330: my $defsel = 0;
1.1 raeburn 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";
1.3 ! raeburn 359: print 'Create a new certificate? [Y/n]';
! 360: $defsel = 1;
1.1 raeburn 361: } elsif ($starttime > $now) {
362: $status = 'future';
1.3 ! raeburn 363: print "Current CA certificate will be valid after $cert{'start'}\n";
! 364: print 'Create a new certificate? [y/N]';
1.1 raeburn 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";
1.3 ! raeburn 369: print 'Create a new certificate? [y/N]';
1.1 raeburn 370: }
371: } else {
372: print "Could not determine validity of current CA certificate\n";
1.3 ! raeburn 373: print 'Create a new certificate? [Y/n]';
! 374: $defsel = 1;
1.1 raeburn 375: }
1.3 ! raeburn 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;
1.1 raeburn 383: }
384: } else {
1.3 ! raeburn 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)) {
1.1 raeburn 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 = '';
1.3 ! raeburn 630: Term::ReadKey::ReadMode('raw');
1.1 raeburn 631: my $key;
1.3 ! raeburn 632: while(ord($key = Term::ReadKey::ReadKey(0)) != 10) {
1.1 raeburn 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: }
1.3 ! raeburn 641: Term::ReadKey::ReadMode('normal');
1.1 raeburn 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 {
1.3 ! raeburn 677: my ($keydir,$certdir,$sslkeypass,$cadays) = @_;
1.1 raeburn 678: # generate SSL cert for CA
679: my $created;
1.3 ! raeburn 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 |");
1.1 raeburn 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 {
1.3 ! raeburn 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";
1.1 raeburn 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) {
1.3 ! raeburn 744: my $countrydesc = Locale::Country::code2country($posscountry);
1.1 raeburn 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 '') {
1.3 ! raeburn 761: if (Locale::Country::code2country(lc($choice))) {
1.1 raeburn 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'}
1.3 ! raeburn 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
1.1 raeburn 832:
1.3 ! raeburn 833: Enter a choice of 1-8 to change, otherwise enter 9:
1.1 raeburn 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);
1.3 ! raeburn 881: 7) Default certificate lifetime: $data{'days'}
1.1 raeburn 882: Enter new value:
883: END
884: my $choice2=<>;
885: chomp($choice2);
886: $choice2 =~ s/\D//g;
1.3 ! raeburn 887: $data{'days'}=$choice2;
1.1 raeburn 888: } elsif ($choice == 8) {
889: print(<<END);
1.3 ! raeburn 890: 8) CRL re-creation interval: $data{'crldays'}
1.1 raeburn 891: Enter new value:
892: END
893: my $choice2=<>;
894: chomp($choice2);
895: $choice2 =~ s/\D//g;
1.3 ! raeburn 896: $data{'crldays'}=$choice2;
1.1 raeburn 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>