mkusercert.in 9.17 KB
Newer Older
1 2 3
#!/usr/bin/perl -wT
#
# EMULAB-COPYRIGHT
4
# Copyright (c) 2000-2008 University of Utah and the Flux Group.
5 6
# All rights reserved.
#
7
use strict;
8 9 10
use English;
use Getopt::Std;

11 12 13 14 15 16 17
#
# Load the Testbed support stuff.
#
use lib "@prefix@/lib";
use libaudit;
use libdb;
use libtestbed;
18
use User;
19

20 21 22 23 24
#
# Create user SSL certificates.
# 
sub usage()
{
25
    print("Usage: mkusercert [-d] [-o] [-g] [-p password] <user>\n");
26 27
    exit(-1);
}
28 29 30 31 32
my $optlist  = "dp:og";
my $debug    = 0;
my $output   = 0;
my $password = "";
my $geniflag = 0;
33 34 35 36 37 38 39 40

#
# Configure variables
#
my $TB		= "@prefix@";
my $TBOPS	= "@TBOPSEMAIL@";
my $TBLOGS	= "@TBLOGSEMAIL@";
my $OURDOMAIN   = "@OURDOMAIN@";
41 42
my $PGENIDOMAIN = "@PROTOGENI_DOMAIN@";
my $PGENISUPPORT= @PROTOGENI_SUPPORT@;
43 44
my $CONTROL	= "@USERNODE@";
my $BOSSNODE	= "@BOSSNODE@";
45
my $OU          = "sslxmlrpc";	# orgunit
46 47 48 49 50 51 52 53 54 55

# 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 $WORKDIR     = "$TB/ssl";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
56
my $SAVEUID	= $UID;
57

58 59 60 61 62
# Locals
my $encrypted   = 0;
my $db_password = "''";
my $sh_password = "";

63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
#
# 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;

#
92
# Function prototypes
93
#
94
sub fatal($);
95 96 97 98 99

#
# Rewrite audit version of ARGV to prevent password in mail logs.
#
my @NEWARGV = @ARGV;
100
for (my $i = 0; $i < scalar(@NEWARGV); $i++) {
101 102 103 104 105 106 107 108 109 110
    if ($NEWARGV[$i] eq "-p") {
	$NEWARGV[$i + 1] = "**********";
    }
}
AuditSetARGV(@NEWARGV);

#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
111
my %options = ();
112 113 114 115 116 117
if (! getopts($optlist, \%options)) {
    usage();
}
if (defined($options{"d"})) {
    $debug = 1;
}
118 119 120 121
if (defined($options{"g"})) {
    $OU = "geni";
    $geniflag = 1;
}
122 123 124 125 126 127 128 129 130 131 132 133
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");
    }
134 135 136 137 138
    $db_password = DBQuoteSpecial($password);
    $sh_password = $password;
    $sh_password =~ s/\'/\'\\\'\'/g;
    $sh_password = "$sh_password";
    $encrypted = 1;
139 140 141 142
}
if (@ARGV != 1) {
    usage();
}
143 144 145
if ($geniflag && !$encrypted) {
    fatal("GENI certs must be encrypted (use -p password).");
}
146 147 148 149 150 151 152 153 154 155 156 157
my $user = $ARGV[0];

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

158 159 160 161 162 163 164 165 166 167 168 169
# 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!");
}

170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
#
# This script is always audited. Mail is sent automatically upon exit.
#
if (AuditStart(0)) {
    #
    # Parent exits normally
    #
    exit(0);
}

#
# 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: $!");

187 188
TBScriptLock("mkusercert") == 0 or
    fatal("Could not get the lock!");
189 190 191 192

#
# Get the user info (the user being operated on).
#
193
my $user_uuid   = $target_user->uuid();
194
my $user_number = $target_user->unix_uid();
195 196
my $user_uid    = $target_user->uid();
my $user_dbid   = $target_user->dbid();
197 198 199 200 201 202

#
# 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.
#
203
my $default_project;
204 205
my $default_groupgid;

206 207 208
if ($target_user->FirstApprovedProject(\$default_project) < 0) {
    fatal("Could not locate default project for $target_user");
}
209

210 211
if (defined($default_project)) {
    $default_groupgid = $default_project->unix_gid();
212 213
}
else {
214
    print "No group membership for $target_user; using the guest group!\n";
215

216
    (undef,undef,$default_groupgid,undef) = getgrnam("guest");
217 218
}

219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
#
# 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.
#
my $serial = TBGetUniqueIndex("user_sslcerts");

open(SER, ">serial")
    or fatal("Could not create new serial file");
printf SER "%08x\n", $serial;
close(SER);

240 241 242 243 244 245 246 247 248 249
#
# 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: $!");

250 251 252 253 254 255
if ($PGENISUPPORT) {
    print TEMP "OU\t\t= $PGENIDOMAIN.$user_uid\n";
}
else {
    print TEMP "OU\t\t= $user_uid\n";
}
256
print TEMP "CN\t\t= $user_uuid\n";
257
print TEMP "emailAddress\t= $user_uid" . "\@" . "$OURDOMAIN\n";
258 259 260 261 262 263 264
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 ".
265
       ($encrypted ? " -passout 'pass:${sh_password}' " : " -nodes ") .
266 267 268 269 270 271
       " -keyout usercert_key.pem -out usercert_req.pem") == 0
    or fatal("Could not create certificate request");

#
# Sign the client cert request, creating a client certificate.
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
272
$UID = 0;
273 274
system("$OPENSSL ca -batch -policy policy_sslxmlrpc ".
       " -name CA_usercerts -config $CACONFIG ".
275 276 277
       " -out usercert_cert.pem -cert $EMULAB_CERT -keyfile $EMULAB_KEY ".
       " -infiles usercert_req.pem") == 0
    or fatal("Could not sign certificate request");
Leigh B. Stoller's avatar
Leigh B. Stoller committed
278
$UID = $SAVEUID;
279 280

#
281 282 283 284
# We save all of the certs in the DB, but we are not worrying about
# revocation yet. By saving them, we can eventually do that. But we do
# have to set the status bit to revoked though or else we will not know
# later (okay, we can probably figure it out if we had to).
285
#
286 287 288
DBQueryFatal("update user_sslcerts set ".
	     "  status='revoked',revoked=now() ".
	     "where uid_idx='$user_dbid' and encrypted=$encrypted");
289

290
DBQueryFatal("insert into user_sslcerts ".
291 292 293
	     "(uid,uid_idx,idx,created,encrypted,orgunit,status,password) ".
	     "values ('$user_uid', '$user_dbid', $serial, now(), ".
	     "        $encrypted, '$OU', 'valid', $db_password)");
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327

#
# 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);
DBQueryFatal("update user_sslcerts set cert=$certstring,privkey=$pkeystring ".
328
	     "where uid_idx='$user_dbid' and idx=$serial");
329 330

#
331 332
# Combine the key and the certificate into one file which is
# installed in the users home directory.
333 334 335 336 337 338 339
#
system("cat usercert_key.pem usercert_cert.pem > usercert.pem") == 0
    or fatal("Could not combine cert and key into one file");

#
# Copy the certificate to the users .ssl directory.
#
340
my $ssldir = "$USERDIR/$user_uid/.ssl";
341 342 343 344 345 346 347 348 349 350
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;

351
if ($encrypted) {
352 353 354 355 356 357 358 359 360 361 362 363
    $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: $!");

364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
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 -des ".
	   "-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: $!");
}

383
TBScriptUnlock();
384 385 386 387 388
exit(0);

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

389
    TBScriptUnlock();
390 391 392
    die("*** $0:\n".
	"    $mesg\n");
}