#!/usr/bin/perl # The LearningOnline Network # CrGenerate - Generate a loncapa certificate request. # # $Id: CrGenerate.pl,v 1.8 2004/07/05 11:36:52 foxr Exp $ # # Copyright Michigan State University Board of Trustees # # This file is part of the LearningOnline Network with CAPA (LON-CAPA). # # LON-CAPA is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # LON-CAPA is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with LON-CAPA; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # /home/httpd/html/adm/gpl.txt # # http://www.lon-capa.org/ # # # This script: # 1. Generates a private host key and certificate request/ # 2. Decodes the private host key # 3. Installs the private host key with appropriate permissions # in the appropriate directory (sorry to be vague about this, but # the installation directory is determined by external configuration # info). # 4. Constructs an email to the loncapa cluster administrator # consisting of a generic heading and the certificate request as a MIME # attachment. # 5. Sends the email and # 6. Cleans up after itself by removing any temp files generated. # # # Import section: use strict; use lib '/home/httpd/lib/perl'; use MIME::Entity; use LONCAPA::Configuration; use File::Copy; # Global variable declarations:4 my $SSLCommand; # Full path to openssl command. my $CertificateDirectory; # LONCAPA Certificate directory. my $KeyFilename; # Key filename (within CertificateDirectory). my $RequestEmail; # Email address of loncapa cert admin. my $WebUID; # UID of web user. my $WebGID; # GID of web user. my $Passphrase="loncapawhatever"; # Initial passphrase for keyfile my $RequestFile="loncapaRequest.pem"; # Name of Certificate request file. my $EncodedKey="hostkey.pem"; # Name of encoded key file. my $WebUser="www"; # Username running the web server. my $WebGroup="www"; # Group name running the web server. # Debug/log support: # my $DEBUG = 0; # 1 for on, 0 for off. # Send debugging to stderr. # Parameters: # msg - Message to send to stderr. # Implicit Inputs: # $DEBUG - message is only written if this is true. # sub Debug { my ($msg) = @_; if($DEBUG) { print STDERR "$msg\n"; } } # # Decodes the email address from a textual certificate request # file: # Parameters: # $RequestFile - Name of the file containing the textual # version of the certificate request. # Returns: # Email address contained in the request. # Failure: # If unable to open or unable to fine an email address in the file, # dies with a message. # sub DecodeEmailFromRequest { Debug("DecodeEmailFromRequest"); my ($RequestFile) = @_; Debug("Request file is called $RequestFile"); # We need to look for the line that has a "/Email=" in it. Debug("opening $RequestFile"); open REQUEST, "< $RequestFile" or die "Unable to open $RequestFile to parse return email address"; Debug("Parsing request file"); my $line; my $found = 0; while($line = ) { chomp($line); # Never a bad idea. if($line =~ /\/Email=/) { $found = 1; last; } } if(!$found) { die "There does not appear to be an email address in $RequestFile"; } close REQUEST; Debug("Found /Email in $line"); # $line contains a bunch of comma separated key=value pairs. # The problem is that after these is a /Email= # first we'll split the line up at the commas. # Then we'll look for the entity with the /Email in it. # That line will get split at the / and then the Email= # gets split at the =. I'm sure there's some clever regular expression # substitution that will get it all in a single line, but I think # this approach is gonna be much easier to understand than punctuation # sneezed all over the page: my @commalist = split(/,/, $line); my $item; my $emailequals = ""; foreach $item (@commalist) { if($item =~ /\/Email=/) { # gotcha... $emailequals = $item; last; } } Debug("Pulled out $emailequals from $line"); my ($trash, $addressequals) = split(/\//, $emailequals); Debug("Futher pulled out $addressequals"); my ($junk, $address) = split(/=/, $addressequals); Debug("Parsed final email addresss as $address"); return $address; } # # Read the LonCAPA web config files to get the values of the # configuration global variables we need: # Implicit inputs: # loncapa.conf - configuration file to read (user specific). # Implicit outputs (see global variables section): # SSLCommand, # CertificateDirectory # KeyfileName # RequestEmail # Side-Effects: # Exit with error if cannot complete. # sub ReadConfig { Debug("Reading configuration"); my $perlvarref = LONCAPA::Configuration::read_conf('loncapa.conf'); # Name of the SSL Program if($perlvarref->{SSLProgram}) { $SSLCommand = $perlvarref->{SSLProgram}; Debug("SSL Command: $SSLCommand"); } else { die "Unable to read the SSLCommand configuration option\n"; } # Where the certificates, and host key are installed: if($perlvarref->{lonCertificateDirectory}) { $CertificateDirectory = $perlvarref->{lonCertificateDirectory}; Debug("Local certificate Directory: $CertificateDirectory"); } else { die "Unable to read SSLDirectory configuration option\n"; } # The name of the host key file (to be installed in SSLDirectory). # if($perlvarref->{lonnetPrivateKey}) { $KeyFilename = $perlvarref->{lonnetPrivateKey}; Debug("Private key will be installed as $KeyFilename"); } else { die "Unable to read lonnetPrivateKey conrig paraemter\n"; } # The email address to which the certificate request is sent: if($perlvarref->{SSLEmail}) { $RequestEmail = $perlvarref->{SSLEmail}; Debug("Certificate request will be sent to $RequestEmail"); } else { die "Could not read SSLEmail coniguration key"; } # The UID/GID of the web user: It's possible the web user's # GID is not its primary, so we'll translate that form the # group file separately. my ($login, $pass, $uid, $gid) = getpwnam($WebUser); if($uid) { $WebUID = $uid; Debug("Web user: $WebUser -> UID: $WebUID"); } else { die "Could not translate web user: $WebUser to a uid."; } my $gid = getgrnam($WebGroup); if($gid) { $WebGID = $gid; Debug("Web group: $WebGroup -> GID $WebGID"); } else { die "Unable to translate web group $WebGroup to a gid."; } } # # Generate a certificate request. # The openssl command is issued to create a local host key and # a certificate request. The key is initially encoded. # We will eventually decode this, however, since the key # passphrase is open source we'll protect even the initial # encoded key file too. We'll need to decode the keyfile since # otherwise, openssl will need a passphrase everytime an ssl connection # is created (ouch). # Implicit Inputs: # Passphrase - Initial passphrase for the encoded key. # RequestFile - Filename of the certificate request. # EncodedKey - Filename of the encoded key file. # # Side-Effects: # sub GenerateRequest { Debug("Generating the request and key"); print "We are now going to generate the certificate request\n"; print "You will be prompted by openssl for several pieces of \n"; print "information. Most of this information is for documentation\n"; print "purposes only, so it's not critical if you make a mistake.\n"; print "However: The generated certificate will be sent to the \n"; print "Email address you provide, and you should leave the optional\n"; print "Challenge password blank.\n"; my $requestcmd = $SSLCommand." req -newkey rsa:1024 " ." -keyout hostkey.pem " ." -keyform PEM " ." -out request.pem " ." -outform PEM " ." -passout pass:$Passphrase"; my $status = system($requestcmd); if($status) { die "Certificate request generation failed: $status"; } chmod(0600, "hostkey.pem"); # Protect key since passphrase is opensrc. Debug("Decoding the key"); my $decodecmd = $SSLCommand." rsa -in hostkey.pem" ." -out hostkey.dec" ." -passin pass:$Passphrase"; $status = system($decodecmd); if($status) { die "Host key decode failed"; } chmod(0600, "hostkey.dec"); # Protect the decoded hostkey. # Create the textual version of the request too: Debug("Creating textual version of the request for users."); my $textcmd = $SSLCommand." req -in request.pem -text " ." -out request.txt"; $status = system($textcmd); if($status) { die "Textualization of the certificate request failed"; } Debug("Done"); } # # Installs the decoded host key (hostkey.dec) in the # certificate directory with the correct permissions. # # Implicit Inputs: # hostkey.dec - the name of the host key file. # $CertificateDirectory - where the key file gets installed # $KeyFilename - Final name of the key file. # $WebUser - User who should own the key file. # $WebGroup - Group who should own the key file. # 0400 - Permissions to give to the installed key # file. # 0700 - Permissions given to the certificate # directory if created. # Side-Effects: # If necessary, $CertificateDirectory is created. # $CertificateDirectory/$KeyFilename is ovewritten with the # contents of hostkey.dec in the cwd. # sub InstallKey { Debug("InstallKey"); Debug("Need to create certificate directory?"); if(!(-d $CertificateDirectory)) { Debug("Creating"); mkdir($CertificateDirectory, 0700); chown($WebUID, $WebGID, $CertificateDirectory); } else { Debug("Exists"); } Debug("Installing the key file:"); my $FullKeyPath = $CertificateDirectory."/".$KeyFilename; copy("hostkey.dec", $FullKeyPath); Debug("Setting ownership and permissions"); chmod(0400, $FullKeyPath); chown($WebUID, $WebGID, $FullKeyPath); Debug("Done"); } # # Package up a certificate request and email it to the loncapa # admin. The email sent: # - Has the subject: "LonCAPA certificate request for hostname # - Has, as the body, the text version of the certificate. # This can be inspected by the human issuing the certificate # to decide if they want to really grant it... it will # have the return email and all the documentation fields. # - Has a text attachment that consists of the .pem version of the # request. This is extracted by the human granting the # certificate and used as input to the CrGrant.pl script. # # # Implicit inputs: # request.pem - The certificate request file. # request.txt - Textual version of the request file. # $RequestEmail - Email address to which the key is sent. # sub MailRequest { Debug("Mailing request"); # First we need to pull out the return address from the textual # form of the certificate request: my $FromEmail = DecodeEmailFromRequest("request.txt"); if(!$FromEmail) { die "From email address cannot be decoded from certificate request"; } Debug("Certificate will be sent back to $FromEmail"); # Create the email message headers and all: # Debug("Creating top...level..."); my $top = MIME::Entity->build(Type => "multipart/mixed", From => $FromEmail, To => $RequestEmail, Subject => "LonCAPA certificate request"); if(!$top) { die "Unable to create top level mime document"; } Debug("Attaching Text formatted certificate request"); $top->attach(Path => "request.txt"); Debug("Attaching PEM formatted certificate request..."); $top->attach(Type => "text/plain", Path => "request.pem"); # Now send the email via sendmail this should work as long as # sendmail or postfix are configured properly. Most other mailers # define the sendmail command too for compatibility with what # we're trying to do. I decided to use sendmail directly because # otherwise I'm not sure the mail headers I created in $top # will get properly passed as headers to other mailer thingies. # Debug("Mailing.."); open MAILPIPE, "| /usr/lib/sendmail -t -oi -oem" or die "Failed to open pipe to sendmail: $!"; $top->print(\*MAILPIPE); close MAILPIPE; Debug("Done"); } # # Cleans up the detritus that's been created by this # script (see Implicit inputs below). # Implicit inputs: # request.pem - Name of certificate request file in PEM format # which will be deleted. # request.txt - Name of textual equivalent of request file # which will also be deleted. # hostkey.pem - Encrypted host key which will be deleted. # hostkey.dec - Decoded host key, which will be deleted. # sub Cleanup { Debug("Cleaning up generated, temporary files"); unlink("request.pem", "request.txt", "hostkey.pem", "hostkey.dec"); Debug("done!"); } # Entry point: Debug("Starting program"); ReadConfig; # Read loncapa apache config file. GenerateRequest; # Generate certificate request. InstallKey; # Install the user's key. MailRequest; # Mail certificate request to loncapa Cleanup; # Cleanup temp files created. Debug("Done"); #---------------------- POD documentatio -------------------- =head1 NAME CrGenerate - Generate a loncapa certificate request. =head1 SYNOPSIS Usage: B This should probably be run automatically at system installation time. Root must run this as write access is required to /home/httpd. This is a command line script that: - Generates a hostkey and certificate request. - Installs the protected/decoded host key where secure lond/lonc can find it. - Emails the certificate request to the loncapa certificate manager. In due course if all is legitimate, the loncapa certificate manager will email a certificate installation script to the local loncapa system administrator. =head1 DESCRIPTION Using the default openssl configuration file, a certificate request and local hostkey are created in the current working directory. The local host key is decoded and installed in the loncapa certificate directory. This allows the secure versions of lonc and lond to locate them when attempting to form external connections. The key file is given mode 0400 to secure it from prying eyes. The certificate request in PEM form is attached to an email that contains the textual equivalent of the certificate request and sent to the loncapa certificate manager. All temporary files (certificate request, keys etc.) are removed from the current working directory. It is recommended that the directory this script is run in have permission mask 0700 to ensure that there are no timing holes during which the decoded host key file can be stolen. During certificate generation, the user will receive several prompts. For the default LonCAPA openssl configuration, these prompts, and documentation and sample responses in angle brackets (<>) are shown below: Country Name (2 letter code) [GB]: State or Province Name (full name) [Berkshire]: Locality Name (eg, city) [Newbury]: Organization Name (eg, company) [My Company Ltd]: Organizational Unit Name (eg, section) []: Common Name (eg, your name or your server's host name) [] Email Address []:
Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: =head1 DEPENDENCIES - MIME::Entity Used to create the email message. - LONCAPA::Configuration Used to parse the loncapa configuration files. - File::Copy Used to install the key file. - /usr/lib/sendmail Properly configured sendmail, used to send the certificate request email to the loncapa certificate administrator. - /etc/httpd/conf/* Loncapa configuration files read to locate the certificate directory etc. =head1 FILES The following temporary files are created in the cwd hostkey.pem - PEM formatted version of the encrypted host key. hostkey.dec - PEM formatted decrypted version of the host key. request.pem - PEM formatted certificate request. request.txt - Textual rendering of the certificate request. The following permanent file is created: $CertDir/$Keyfile - The installed decoded host key file. $CertDir is defined by the Perl variable lonCertificateDirectory in /etc/loncapa_apache.conf while $Keyfile is defined by the perl variable lonnetPrivateKey in the same configuration file. =head1 COPYRIGHT: Copyright Michigan State University Board of Trustees This file is part of the LearningOnline Network with CAPA (LON-CAPA). LON-CAPA is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. LON-CAPA is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with LON-CAPA; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA /home/httpd/html/adm/gpl.txt =cut