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