Skip to content
Snippets Groups Projects
mkusercert.in 17 KiB
Newer Older
#!/usr/bin/perl -wT
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2012 University of Utah and the Flux Group.
# All rights reserved.
#
use English;
use Getopt::Std;

#
# Load the Testbed support stuff.
#
use lib "@prefix@/lib";
use libaudit;
use libdb;
use libtestbed;
#
# Create user SSL certificates.
# 
sub usage()
{
    print("Usage: mkusercert [-d] [-o] [-r] [-g] [-p password] <user>\n");
my $optlist  = "dp:ogrc:";
my $debug    = 0;
my $output   = 0;
my $password = "";
my $geniflag = 0;

#
# Configure variables
#
my $TB		= "@prefix@";
my $TBOPS	= "@TBOPSEMAIL@";
my $TBLOGS	= "@TBLOGSEMAIL@";
my $OURDOMAIN   = "@OURDOMAIN@";
my $PGENIDOMAIN = "@PROTOGENI_DOMAIN@";
my $PGENISUPPORT= @PROTOGENI_SUPPORT@;
my $CONTROL	= "@USERNODE@";
my $BOSSNODE	= "@BOSSNODE@";

# Locals
my $USERDIR	= USERROOT();
my $SSLDIR      = "$TB/lib/ssl";
my $TEMPLATE    = "$SSLDIR/usercert.cnf";
my $CACONFIG    = "$SSLDIR/ca.cnf";
my $EMULAB_CERT = "$TB/etc/emulab.pem";
my $EMULAB_KEY  = "$TB/etc/emulab.key";
my $OPENSSL     = "/usr/bin/openssl";
my $KEYGEN	= "/usr/bin/ssh-keygen";
my $ADDKEY	= "$TB/sbin/addpubkey";
my $WORKDIR     = "$TB/ssl";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
my $SAVEUID	= $UID;
# Locals
my $encrypted   = 0;
my $db_password = "''";
my $sh_password = "";
#
# We don't want to run this script unless its the real version.
#
if ($EUID != 0) {
    die("*** $0:\n".
	"    Must be setuid! Maybe its a development version?\n");
}

#
# This script is setuid, so please do not run it as root. Hard to track
# what has happened.
#
if ($UID == 0) {
    die("*** $0:\n".
	"    Please do not run this as root! Its already setuid!\n");
}

#
# Untaint the path
#
$ENV{'PATH'} = "$TB/bin:$TB/sbin:/bin:/usr/bin:/usr/bin:/usr/sbin";
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

#
# Turn off line buffering on output
#
$| = 1;

#
sub CreateNewCert();
sub ChangePassPhrase();

#
# Rewrite audit version of ARGV to prevent password in mail logs.
#
my @NEWARGV = @ARGV;
for (my $i = 0; $i < scalar(@NEWARGV); $i++) {
    if ($NEWARGV[$i] eq "-p" || $NEWARGV[$i] eq "-c") {
	$NEWARGV[$i + 1] = "**********";
    }
}
AuditSetARGV(@NEWARGV);

#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
if (! getopts($optlist, \%options)) {
    usage();
}
if (defined($options{"d"})) {
    $debug = 1;
}
if (defined($options{"r"})) {
    $reusekey = 1;
}
if (defined($options{"g"})) {
    $geniflag = 1;
}
if (defined($options{"p"})) {
    $password = $options{"p"};

    #
    # Make sure its all escaped since any printable char is allowed.
    #
    if ($password =~ /^([\040-\176]*)$/) {
	$password = $1;
    }
    else {
	die("Tainted argument: $password\n");
    }
    $db_password = DBQuoteSpecial($password);
    $sh_password = $password;
    $sh_password =~ s/\'/\'\\\'\'/g;
    $sh_password = "$sh_password";
    $encrypted = 1;
if (defined($options{"c"})) {
    $old_password = $options{"c"};

    #
    # Make sure its all escaped since any printable char is allowed.
    #
    if ($old_password =~ /^([\040-\176]*)$/) {
	$old_password = $1;
    }
    else {
	die("Tainted argument: $old_password\n");
    }
    $old_password =~ s/\'/\'\\\'\'/g;
}
if (@ARGV != 1) {
    usage();
}
if ($geniflag && !$encrypted) {
    fatal("GENI certs must be encrypted (use -p password).");
}
if ($reusekey && !$encrypted) {
    fatal("Cannot reuse the key for an unencrypted cert (use -p password).");
}
my $user = $ARGV[0];

#
# Untaint the arguments.
#
if ($user =~ /^([-\w]+)$/i) {
    $user = $1;
}
else {
    die("Tainted argument: $user\n");
}

# Map target user to object.
my $target_user = User->Lookup($user);
if (! defined($target_user)) {
    fatal("$user does not exist!");
}

# Map invoking user to object.
my $this_user = User->LookupByUnixId($UID);
if (! defined($this_user)) {
    fatal("You ($UID) do not exist!");
}

#
# CD to the workdir, and then serialize on the lock file since there is
# some shared goop that the ssl tools muck with (serial number, index, etc.).
# 
chdir("$WORKDIR") or
    fatal("Could not chdir to $WORKDIR: $!");

TBScriptLock("mkusercert") == 0 or
    fatal("Could not get the lock!");
#
# Create a client side cert. Reuse the original key if are told to,
# and it actually exists, and the password is valid.
#
# Do this before the AuditStart() so that user error goes back to web.
#
my $reqargs = "";

if ($reusekey) {
    my $privkey;
    my $cert;
    if ($target_user->SSLCert(1, \$cert, \$privkey)) {
	$reusekey = 0;
	goto newkey;
    }

    open(KEYF, "> usercert_key.pem") or
	fatal("Could not create file to store existing private key");
    print KEYF "-----BEGIN RSA PRIVATE KEY-----\n";
    print KEYF $privkey;
    print KEYF "-----END RSA PRIVATE KEY-----\n";
    close(KEYF);

    #
    # Make sure the user provided the proper passphrase. 
    #
    my $output =
	emutil::ExecQuiet("$OPENSSL rsa -check -in usercert_key.pem ".
			  "  -passin 'pass:${sh_password}'");
    if ($?) {
	print STDERR $output;
	UserFatal("Cannot decrypt private key. Correct pass phrase?");
    }
    $reqargs  = "-key usercert_key.pem -passin 'pass:${sh_password}' ";
}
else {
  newkey:
    $reqargs  = "-keyout usercert_key.pem";
    $reqargs .= ($encrypted ? " -passout 'pass:${sh_password}' " : " -nodes ")
}

#
# This script is always audited. Mail is sent automatically upon exit.
#
if (AuditStart(0)) {
    #
    # Parent exits normally
    #
    exit(0);
}

#
# Get the user info (the user being operated on).
#
my $user_uuid   = $target_user->uuid();
my $user_number = $target_user->unix_uid();
my $user_uid    = $target_user->uid();
my $user_dbid   = $target_user->dbid();

#
# Get the users earliest project membership to use as the default group
# for the case that the account is being (re)created. We convert that to
# the unix info.
#
my $default_groupgid;

if ($target_user->FirstApprovedProject(\$default_project) < 0) {
    fatal("Could not locate default project for $target_user");
}
if (defined($default_project)) {
    $default_groupgid = $default_project->unix_gid();
    print "No group membership for $target_user; using the guest group!\n";
    (undef,undef,$default_groupgid,undef) = getgrnam("guest");
sub CreateNewCert() {
    #
    # Need an index file, which is the openssl version of the DB.
    #
    if (! -e "index.txt") {
	open(IND, ">index.txt")
	    or fatal("Could not create index.txt");
	close(IND);
    }
    #
    # We have to figure out what the next serial number will be and write
    # that into the file. We could let "ca' keep track, but with devel
    # trees, we might end up with duplicate serial numbers.
    #
    $serial = TBGetUniqueIndex("user_sslcerts");
    open(SER, ">serial")
	or fatal("Could not create new serial file");
    printf SER "%08x\n", $serial;
    close(SER);
    #
    # Create a template conf file. We tack on the DN record based on the
    # user particulars.
    #
    system("cp -f $TEMPLATE usercert.cnf") == 0
	or fatal("Could not copy $TEMPLATE to current dir");
    open(TEMP, ">>usercert.cnf")
	or fatal("Could not open $TEMPLATE for append: $!");
    if ($PGENISUPPORT) {
	my $url = "@PROTOGENI_URL@/sa";
	# unregistered OID 2.25.305821105408246119474742976030998643995
	# (corresponding to UUID e61300a0-c4c5-11de-b14e-0002a5d5c51b)
	# is used to indicate generic ProtoGENI XMLRPC servers.
	print TEMP
	    "authorityInfoAccess=2.25.305821105408246119474742976030998643995;URI:$url\n";
    }
    print TEMP "\n";
    print TEMP "[ req_distinguished_name ]\n";
    print TEMP "C\t\t=@SSLCERT_COUNTRY@\n";
    print TEMP "ST\t\t=@SSLCERT_STATE@\n";
    print TEMP "L\t\t=@SSLCERT_LOCALITY@\n";
    print TEMP "O\t\t=@SSLCERT_ORGNAME@\n";
    if ($PGENISUPPORT && $encrypted) {
	print TEMP "OU\t\t= $PGENIDOMAIN.$user_uid\n";
    }
    else {
	print TEMP "OU\t\t= $OU\n";
    }
    print TEMP "CN\t\t= $user_uuid\n";
    print TEMP "emailAddress\t= $user_uid" . "\@" . "$OURDOMAIN\n";
    print TEMP "\n[ req_altname ]\nURI.1=urn:publicid:IDN+$OURDOMAIN" .
	"+user+$user_uid\n" .
	"email=$user_uid" . "\@" . "$OURDOMAIN\n" .
	"URI.2=urn:uuid:$user_uuid\n\n";
    close(TEMP)
	or fatal("Could not close usercert.cnf: $!");
    #
    # Create a client side private key and certificate request.
    #
    system("$OPENSSL req -new -config usercert.cnf ".
	   "$reqargs -out usercert_req.pem") == 0
	   or fatal("Could not create certificate request");
    
    #
    # Sign the client cert request, creating a client certificate.
    #
    $UID = 0;
    system("$OPENSSL ca -batch -policy policy_sslxmlrpc -days $days ".
	   " -name CA_usercerts -config $CACONFIG ".
	   " -out usercert_cert.pem -cert $EMULAB_CERT -keyfile $EMULAB_KEY ".
	   " -infiles usercert_req.pem") == 0
	   or fatal("Could not sign certificate request");
    $UID = $SAVEUID;
    #
    # We store the DN in the DB too, for creating the crl index file without
    # having to reparse all the certs.
    #
    my $DN = `$OPENSSL x509 -subject -noout -in usercert_cert.pem`;
    chomp($DN);
    if ($DN =~ /^subject=\s*(\/[-\/\=\w\@\.\s]+)$/) {
	$DN = $1;
    }
    else {
	fatal("Could not parse DN from certificate");
    }
    #
    # Grab the cert path and strip off the header goo, then insert into
    # the DB.
    #
    my $certstring = "";

    open(CERT, "$OPENSSL x509 -in usercert_cert.pem |")
	or fatal("Could not start x509 on usercert_cert.pem");

    while (<CERT>) {
	next
	    if ($_ =~ /^--.*--$/);
	$certstring .= $_;
    }
    close(CERT);

    #
    # Now suck in the priv key.
    # 
    my $pkeystring = "";
    open(PKEY, "usercert_key.pem")
	or fatal("Could open usercert_key.pem");

    while (<PKEY>) {
	next
	    if ($_ =~ /^--.*--$/);
	$pkeystring .= $_;
    }
    close(PKEY);

    $pkeystring  = DBQuoteSpecial($pkeystring);
    $certstring  = DBQuoteSpecial($certstring);
    my $dnstring = DBQuoteSpecial($DN);
    # Ensure we keep it past revocation.
    $days++;

    #
    # We save all of the encrypted certs in the DB since we are going to issue
    # CRLs for protogeni. We do not bother to save old unencrypted certs since
    # they have a different OU and so protogeni will not accept them, they
    # do not need to be revoked. The sslxmlrpc server checks the table directly
    # so only the most recent is needed.
    #
    DBQueryFatal("insert into user_sslcerts ".
		 "(uid,uid_idx,idx,created,expires,encrypted,password, ".
		 " cert,privkey,DN) ".
		 "values ('$user_uid', '$user_dbid', $serial, now(), ".
		 "        DATE_ADD(now(), INTERVAL $days DAY), ".
		 "        $encrypted, $db_password, ".
		 "        $certstring, $pkeystring, $dnstring)");

    if ($encrypted) {
	DBQueryFatal("update user_sslcerts set ".
		     "  revoked=now() ".
		     "where uid_idx='$user_dbid' and idx!=$serial and ".
		     "      encrypted=1 and revoked is null");

	#
	# We also want to get rid of the associated ssh pub key that
	# we add below. We use the comment to get rid of anything
	# that looks like "sslcert:" since up to now we have been leaving
	# old ones behind.
	#
	DBQueryFatal("delete from user_pubkeys ".
		     "where uid_idx='$user_dbid' and comment like 'sslcert:%'");
    }
    else {
	DBQueryFatal("delete from user_sslcerts ".
		     "where uid_idx='$user_dbid' and idx!=$serial and ".
		     "      encrypted=0");
    }

    #
    # Combine the key and the certificate into one file which is
    # installed in the users home directory.
    #
    system("cat usercert_key.pem usercert_cert.pem > usercert.pem") == 0
	or fatal("Could not combine cert and key into one file");
# Change passphrase on the key, and create new pem file.
sub ChangePassPhrase()
{
    my $privkey;
    my $cert;
    if ($target_user->SSLCert(1, \$cert, \$privkey)) {
	UserFatal("No encrypted key for pass phrase change!");
    }
    open(KEYF, "> old_key.pem") or
	fatal("Could not create file to store existing private key");
    print KEYF "-----BEGIN RSA PRIVATE KEY-----\n";
    print KEYF $privkey;
    print KEYF "-----END RSA PRIVATE KEY-----\n";
    close(KEYF);
    #
    # Make sure the user provided the proper passphrase. 
    #
    system("$OPENSSL rsa -des3 -in old_key.pem -out usercert_key.pem ".
	   "  -passout 'pass:${sh_password}' ".
	   "  -passin 'pass:${old_password}' >/dev/null 2>&1") == 0
	   or UserFatal("Cannot decrypt private key. Correct pass phrase?");
	   
    #
    # Stick the cert into a file too, for changing the pass phrase.
    # See below.
    #
    open(CERTF, "> usercert_cert.pem") or
	fatal("Could not create file to store existing certificate");
    print CERTF "-----BEGIN CERTIFICATE-----\n";
    print CERTF $cert;
    print CERTF "-----END CERTIFICATE-----\n";
    close(CERTF);
    
    #
    # Need to figure which row for update.
    #
    my $query_result =
	DBQueryFatal("select idx from user_sslcerts ".
		     "where uid_idx='$user_dbid' and ".
		     "      cert=". DBQuoteSpecial($cert));
    if (!$query_result->numrows) {
	fatal("Could not find idx for certificate");
    }
    ($serial) = $query_result->fetchrow_array();
    
    #
    # Now suck in the priv key.
    # 
    my $pkeystring = "";
    open(PKEY, "usercert_key.pem")
	or fatal("Could open usercert_key.pem");

    while (<PKEY>) {
	next
	    if ($_ =~ /^--.*--$/);
	$pkeystring .= $_;
    }
    close(PKEY);

    $pkeystring  = DBQuoteSpecial($pkeystring);

    DBQueryFatal("update user_sslcerts set ".
		 "  password=$db_password,privkey=$pkeystring  ".
		 "where uid_idx='$user_dbid' and idx=$serial");

    #
    # Combine the key and the certificate into one file which is
    # installed in the users home directory.
    #
    system("cat usercert_key.pem usercert_cert.pem > usercert.pem") == 0
	or fatal("Could not combine cert and key into one file");
}

if (defined($old_password)) {
    ChangePassPhrase();

#
# Copy the certificate to the users .ssl directory.
#
my $ssldir = "$USERDIR/$user_uid/.ssl";
if (! -d $ssldir) {
    mkdir($ssldir, 0700) or
	fatal("Could not mkdir $ssldir: $!");

    chown($user_number, $default_groupgid, $ssldir)
	or fatal("Could not chown $ssldir: $!");
}

my $target;

    $target = "$ssldir/encrypted.pem";
}
else {
    $target = "$ssldir/emulab.pem";
}

system("cp -f usercert.pem $target") == 0
    or fatal("Could not copy usercert.pem to $target");

chown($user_number, $default_groupgid, "$target")
    or fatal("Could not chown $target: $!");

if ($encrypted) {
    #
    # Convert to pkcs12 format, strictly for the geni xmlrpc code, whichs
    # does not provide a way to give the passphrase for encrypted x509 keys.
    #
    system("$OPENSSL pkcs12 -export -in usercert.pem -des3 ".
	   "-passin 'pass:${sh_password}' -passout 'pass:${sh_password}' ".
	   "-out usercert.p12 -rand ./.rnd")
	== 0 or fatal("Could not create usercert.p12");

    $target = "$ssldir/encrypted.p12";
    
    system("cp -f usercert.p12 $target") == 0
	or fatal("Could not copy usercert.p12 to $target");

    chown($user_number, $default_groupgid, "$target")
	or fatal("Could not chown $target: $!");

    chmod(0600, $target)
	or fatal("Could not chmod $target: $!");

    #
    # Create an SSH key from the private key. Mostly for geni users,
    # who tend not to know how to do such things.
    #
    my $pemfile = "$ssldir/encrypted.pem";
    my $sshdir  = "$USERDIR/$user_uid/.ssh";
    my $pphrase = User::escapeshellarg($password);
    # This comment is special. It functions as a cross table reference
    # between pubkeys and sslcerts. I might do this differently later.
    my $comment = User::escapeshellarg("sslcert:${serial}");

    # ssh-keygen whines and refuses to extract unless the mode is 600.
    chmod(0600, $pemfile)
	or fatal("Could not chmod $pemfile: $!");

    #
    # The key format is identical to openssh, so just copy it over.
    #
    system("/bin/cp usercert_key.pem $sshdir/encrypted.key") == 0
	or fatal("Could not copy private key to $sshdir/encrypted.key: $!");
    chmod(0600, "$sshdir/encrypted.key")
	or fatal("Could not chmod $sshdir/encrypted.key: $!");

    #
    # No need to do this when just changing the passphrase. 
    if (!defined($old_password)) {
	#
	# Extract a public key.
	#
	system("$KEYGEN -P $pphrase -y -f $pemfile > $sshdir/encrypted.pub")
	    == 0
	    or fatal("Could not extract ssh pubkey from $pemfile");

	#
	# And add the pubkey to the DB. Mark it as nodelete and that it should
	# remove existing key with same comment. 
	#
	$EUID = $UID;
	system("$ADDKEY -s -N -R -C $comment -u $user_uid ".
	       "      -f $sshdir/encrypted.pub")
	    == 0 or fatal("Could not add pubkey $sshdir/encrypted.pub");
    }
exit(0);

sub fatal($) {
    my($mesg) = $_[0];

    die("*** $0:\n".
	"    $mesg\n");
}
sub UserFatal($) {
    my($mesg) = $_[0];

    TBScriptUnlock();
    print STDERR $mesg;
    # Need to do this so that the web interface sees the message.
    LogEnd(1);
    # And again since the above print went to the the log.
    print STDERR $mesg;
    # Tell web interface to tell user. 
    exit(1);
}