--- loncom/CrGenerate.pl 2004/06/29 11:13:08 1.2 +++ loncom/CrGenerate.pl 2004/07/05 11:36:52 1.8 @@ -2,7 +2,7 @@ # The LearningOnline Network # CrGenerate - Generate a loncapa certificate request. # -# $Id: CrGenerate.pl,v 1.2 2004/06/29 11:13:08 foxr Exp $ +# $Id: CrGenerate.pl,v 1.8 2004/07/05 11:36:52 foxr Exp $ # # Copyright Michigan State University Board of Trustees # @@ -48,21 +48,30 @@ # Import section: use strict; +use lib '/home/httpd/lib/perl'; use MIME::Entity; -use Mail::Mailer; use LONCAPA::Configuration; +use File::Copy; -# Global variable declarations: +# Global variable declarations:4 my $SSLCommand; # Full path to openssl command. my $CertificateDirectory; # LONCAPA Certificate directory. my $KeyFilename; # Key filename (within CertificateDirectory). -my $Passphrase="loncapawhatever"; # Initial passphrase for keyfile 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 = 1; # 1 for on, 0 for off. +my $DEBUG = 0; # 1 for on, 0 for off. # Send debugging to stderr. # Parameters: @@ -71,18 +80,353 @@ my $DEBUG = 1; # 1 for on, 0 for off. # $DEBUG - message is only written if this is true. # sub Debug { - my $msg = shift; + 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"); + -sub ReadConfig {} -sub GenerateRequest {} -sub InstallKey {} -sub MailRequest {} -sub Cleanup {} + 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!"); +} @@ -96,3 +440,124 @@ MailRequest; # Mail certificate reques 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 + + +