Newer
Older
#!/usr/bin/perl -wT
#
# EMULAB-COPYRIGHT
# Copyright (c) 2000-2012 University of Utah and the Flux Group.
use strict;
Leigh B. Stoller
committed
#
# Load the Testbed support stuff.
#
use lib "@prefix@/lib";
use libaudit;
use libdb;
use libtestbed;
Leigh B. Stoller
committed
use User;
Leigh B. Stoller
committed
#
# 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;
my $reusekey = 0;
my $old_password;
#
# 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@";
my $OU = "sslxmlrpc"; # orgunit
# 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";
# Locals
my $encrypted = 0;
my $db_password = "''";
my $sh_password = "";
my $days = 1000;
#
# 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;
#
Leigh B. Stoller
committed
# Function prototypes
Leigh B. Stoller
committed
sub fatal($);
sub UserFatal($);
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.
#
my %options = ();
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;
$days = 365;
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");
}
Leigh B. Stoller
committed
# 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: $!");
Leigh B. Stoller
committed
TBScriptLock("mkusercert") == 0 or
fatal("Could not get the lock!");
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#
# 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();
Leigh B. Stoller
committed
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.
#
Leigh B. Stoller
committed
my $default_project;
Leigh B. Stoller
committed
if ($target_user->FirstApprovedProject(\$default_project) < 0) {
fatal("Could not locate default project for $target_user");
}
Leigh B. Stoller
committed
if (defined($default_project)) {
$default_groupgid = $default_project->unix_gid();
Leigh B. Stoller
committed
print "No group membership for $target_user; using the guest group!\n";
Leigh B. Stoller
committed
(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");
}
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
#
# 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);
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
#
# 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();
}
else {
CreateNewCert();
}
#
# Copy the certificate to the users .ssl directory.
#
Leigh B. Stoller
committed
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;
if ($encrypted) {
$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: $!");
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
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");
}
}
Leigh B. Stoller
committed
TBScriptUnlock();
exit(0);
sub fatal($) {
my($mesg) = $_[0];
Leigh B. Stoller
committed
TBScriptUnlock();
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);
}