addpubkey.in 11.2 KB
Newer Older
1
2
3
#!/usr/bin/perl -wT
#
# EMULAB-COPYRIGHT
4
# Copyright (c) 2000-2005 University of Utah and the Flux Group.
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# All rights reserved.
#
use English;
use Getopt::Std;

#
# Parse ssh public keys and enter into the DB. The default format is
# openssh, but if the key is not in that format, then use ssh-keygen
# to see if it can be converted from either SSH2 or SECSH format into
# openssh format. This gets called from the webpage to parse keys
# uploaded by users.
#
sub usage()
{
19
    print "Usage: addpubkeys [-n] [-k] <user> [<keyfile> | <key>]\n";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
20
    print "       addpubkeys [-i [-f] | -w] <user>\n";
21
22
23
    print "Options:\n";
    print " -k      Indicates that key was passed in on the command line\n";
    print " -n      Verify key format only; do not enter into into DB\n";
24
25
    print " -w      Generate new authkeys (protocol 1 and 2) file for user\n";
    print " -i      Initialize mode; generate initial key for user\n";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
26
    print " -f      Force a generate of initial key for user\n";
27
28
    exit(-1);
}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
29
my $optlist   = "kniwf";
30
31
my $iskey     = 0;
my $verify    = 0;
32
my $initmode  = 0;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
33
my $force     = 0;
34
35
36
my $genmode   = 0;
my $nobody    = 0;
my $noemail   = 0;
37
38
39
40
41
42
43

#
# Configure variables
#
my $TB		= "@prefix@";
my $TBOPS       = "@TBOPSEMAIL@";
my $TBAUDIT     = "@TBAUDITEMAIL@";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
44
my $OURDOMAIN   = "@OURDOMAIN@";
45
46
47
48
49
50
51
52
53
54
55
56
my $HOMEDIR	= "/users";
my $KEYGEN	= "/usr/bin/ssh-keygen";
my $USERUID;

# Locals
my $user;
my $keyfile;
my $keyline;
my $key;
my $comment;
my $user_name;
my $user_email;
57
58
59
60
61

#
# Testbed Support libraries
#
use lib "@prefix@/lib";
62
use libaudit;
63
64
65
use libdb;
use libtestbed;

66
67
68
69
70
#
# Function prototypes
#
sub ParseKey($);
sub InitUser();
Leigh B. Stoller's avatar
Leigh B. Stoller committed
71
sub GenerateKeyFile();
72
73
sub fatal($);

74
75
76
77
78
79
80
81
82
83
84
#
# Turn off line buffering on output
#
$| = 1;

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

85
86
87
88
89
90
91
92
#
# 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");
}

93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#
# 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!\n");
}

#
# Parse command arguments. Once we return from getopts, all that should be
# left are the required arguments.
#
%options = ();
if (! getopts($optlist, \%options)) {
    usage();
}
if (defined($options{"k"})) {
    $iskey = 1;
}
if (defined($options{"n"})) {
    $verify = 1;
}
115
116
117
if (defined($options{"i"})) {
    $initmode = 1;
}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
118
119
120
if (defined($options{"f"})) {
    $force = 1;
}
121
122
if (defined($options{"w"})) {
    $genmode = 1;
123
}
124
if ($verify && $genmode) {
125
126
    usage();
}
127
128
129
130
131
132
133
134
135
136
137
138
if ($initmode || $genmode) {
    if (@ARGV != 1) {
	usage();
    }
}
elsif (@ARGV == 2) {
    $keyfile = $ARGV[1];
}
else {
    usage();
}    
$user = $ARGV[0];
139
140
141
142

#
# Untaint the arguments.
#
Robert Ricci's avatar
Robert Ricci committed
143
if ($user =~ /^([-\w_]+)$/i) {
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
    $user = $1;
}
else {
    fatal("Tainted username: $user");
}

#
# If invoked as "nobody" its for a user with no actual account.
# 
if (getpwuid($UID) eq "nobody") {
    if ($initmode || $genmode) {
	fatal("Bad usage as 'nobody'");
    }
    $nobody = 1;
}
else {
    $USERUID = getpwnam($user);
}

#
# Initmode or genmode, do it and exit.
#
if ($initmode) {
    # Drop root privs, switch to target user.
    $EUID = $USERUID;
    exit InitUser();
}
if ($genmode) {
    # Drop root privs, switch to target user.
    $EUID = $USERUID;
Leigh B. Stoller's avatar
Leigh B. Stoller committed
174
    exit GenerateKeyFile();
175
176
177
}

# Else, key parse mode ...
178
179
180
181
182
183
184
185
186
187
188
189
if ($iskey) {
    if ($keyfile =~ /^([-\w\s\.\@\+\/\=]*)$/) {
	$keyfile = $1;
    }
    else {
	fatal("Tainted key: $keyfile");
    }
    $keyline = $keyfile;
}
else {
    if ($keyfile =~ /^([-\w\.\/]+)$/) {
	$keyfile = $1;
190
     }
191
192
193
194
    else {
	fatal("Tainted filename: $keyfile");
    }
    if (! -e $keyfile) {
195
	fatal("No such file: $keyfile\n");
196
197
198
199
200
    }
    $keyline = `head -1 $keyfile`;
}

#
201
# Check user
202
#
203
204
205
206
if (!$verify) {
    # If its the user himself, then we can generate a new authkeys file. 
    if (!$nobody && getpwuid($UID) ne "$user" && !TBAdmin($UID)) {
	fatal("You are not allowed to set pubkeys for $user.\n");
207
    }
208
209
210
211
212
213
214
215
216
217
218
219
220
221
    if (-d "$HOMEDIR/$user/.ssh") {    
	$genmode = 1;
    }

    #
    # This script is audited when not in verify mode. Since all keys are first
    # checked with verify mode, this should not cause any extra email from bad
    # keys.
    #
    AuditStart(0);

    if (! UserDBInfo($user, \$user_name, \$user_email)) {
	if ($nobody) {
	    $noemail = 1;
222
	}
223
224
	else {
	    fatal("No DB info for $user!");
225
226
	}
    }
227
228
}

229
230
231
# Drop root privs, switching to user.
if (!$nobody) {
    $EUID = $USERUID;
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
}

#
# Grab the first line of the file. Parse it to see if its in the
# format we like (openssh), either protocol 1 or 2.
#
if (ParseKey($keyline)) {
    exit 0;
}
# If the key was entered on the command line, then nothing more to do.
if ($iskey) {
    exit 1;
}

#
# Run ssh-keygen over it and see if it can convert it. 
#
if (! open(KEYGEN, "ssh-keygen -i -f $keyfile 2>/dev/null |")) {
    fatal("*** $0:\n".
	  "    Could not start ssh-keygen\n");
}
$keyline = <KEYGEN>;
if (close(KEYGEN) && ParseKey($keyline)) {
    exit 0;
}
exit 1;

sub ParseKey($) {
    my ($keyline) = @_;
261
262
263
264
265
266
267

    # Enforce a reasonable length on the key.
    if (length($keyline) > 4096) {
	print "Key is too long!\n";
	print "Key: $keyline\n";
	return 0;
    }
268
    
Leigh B. Stoller's avatar
Leigh B. Stoller committed
269
    if ($keyline =~ /^(\d*\s\d*\s[0-9a-zA-Z]*) ([-\w\@\.\ ]*)\s*$/) {
270
271
272
273
274
275
276
277
278
279
280
        # Protocol 1
	$type    = "ssh-rsa1";
	$key     = $1;
	$comment = $2;
    }
    elsif ($keyline =~ /^(\d*\s\d*\s[0-9a-zA-Z]*)\s*$/) {
        # Protocol 1 but no comment field.
	$type    = "ssh-rsa1";
	$key     = $1;
    }
    elsif ($keyline =~
281
	   /^(ssh-rsa|ssh-dss) ([-\w\.\@\+\/\=]*) ([-\w\@\.\ ]*)$/) {
282
283
284
285
286
287
288
289
290
291
292
293
        # Protocol 2
	$type    = $1;
	$key     = "$1 $2";
	$comment = $3;
    }
    elsif ($keyline =~ /^(ssh-rsa|ssh-dss) ([-\w\.\@\+\/\=]*)$/) {
        # Protocol 2 but no comment field
	$type    = $1;
	$key     = "$1 $2";
    }

    if (!defined($key)) {
294
295
	print "Key cannot be parsed!\n";
	print "Key: $keyline\n";
296
297
	return 0;
    }
298

299
300
    # Do not enter into DB if in verify mode.
    if ($verify) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
301
	print "Key was good: $type\n";
302
303
304
305
	return 1;
    }

    #
306
    # Make up a comment field for the DB. 
307
308
309
310
311
312
313
    #
    if (!defined($comment)) {
	$comment = "$type-${user_email}";
    }
    $key = "$key $comment";

    DBQueryFatal("replace into user_pubkeys ".
314
		 "values ('$user', 0, '$key', now(), '$comment')");
315

316
317
318
    #
    # Mark user record as modified so nodes are updated.
    #
319
    TBNodeUpdateAccountsByUID($user);
320

321
322
323
324
325
326
327
328
    my $chunked = "";

    while (length($key)) {
	$chunked .= substr($key, 0, 65, "");
	if (length($key)) {
	    $chunked .= "\n";
	}
    }
329
330
    print "SSH Public Key for '$user' added:\n";
    print "$chunked\n";
331
    
332
333
    # Generate new auth keys file. 
    if ($genmode) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
334
	GenerateKeyFile();
335
336
337
338
339
340
341
342
343
344
345
    }

    if (! $noemail) {
	SENDMAIL("$user_name <$user_email>",
		 "SSH Public Key for '$user' Added",
		 "SSH Public Key for '$user' added:\n".
		 "\n".
		 "$chunked\n",
		 "$TBOPS");
    }
    return 1;
346
347
}

348
349
350
351
352
353
#
# Init function for new users. Generate the first key for the user (which
# is loaded into the DB), and then generate the keyfiles. Note that the
# user might have preloaded personal keys.
#
sub InitUser()
354
{
355
    my $sshdir  = "$HOMEDIR/$user/.ssh";
356
357

    #
358
    # Set up the ssh key, but only if not done so already.
359
    #
360
361
362
363
    if (! -e "$sshdir") {
	mkdir("$sshdir", 0700) or
	    fatal("Could not mkdir $sshdir: $!");
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
364
365
    if (! -e "$sshdir/identity" || $force) {
	print "Creating ssh protocol 1 key for $user.\n";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
366

Leigh B. Stoller's avatar
Leigh B. Stoller committed
367
368
369
370
371
372
373
374
375
376
377
378
	#
	# Want to delete existing key from DB.
	#
	if (-e "$sshdir/identity") {
	    my $ident = `cat $sshdir/identity.pub`;

	    if ($ident =~ /(\d*\s\d*\s[0-9a-zA-Z]*)\s([-\w\@\.]*)/) {
		DBQueryFatal("delete from user_pubkeys ".
			     "where uid='$user' and pubkey='$1 $2'");
	    }
	    unlink("$sshdir/identity");
	}
379

Leigh B. Stoller's avatar
Leigh B. Stoller committed
380
	# Hmm, need to use -C option so comment field makes sense.
381
    
Leigh B. Stoller's avatar
Leigh B. Stoller committed
382
383
384
	if (system("$KEYGEN -t rsa1 -P '' ".
		   "-C '${user}" . "\@" . ${OURDOMAIN} . "' ".
		   "-f $sshdir/identity")) {
385
386
387
388
389
390
391
392
	    fatal("Failure in ssh-keygen!");
	}

	#
	# Grab a copy for the DB.
	# 
	my $ident = `cat $sshdir/identity.pub`;

Leigh B. Stoller's avatar
Leigh B. Stoller committed
393
	if ($ident =~ /(\d*\s\d*\s[0-9a-zA-Z]*)\s([-\w\@\.]*)/) {
394
	    DBQueryFatal("replace into user_pubkeys ".
395
			 "values ('$user', 0, '$1 $2', now(), '$2')");
396
397
	}
	else {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
398
	    fatal("Bad protocol 1 public key: $ident\n");
399
400
	}
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
401
402
403
404
405
    #
    # Moving to V2 keys ...
    #
    if (! -e "$sshdir/id_rsa" || $force) {
	print "Creating ssh protocol 2 key for $user.\n";
406

Leigh B. Stoller's avatar
Leigh B. Stoller committed
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
	#
	# Want to delete existing key from DB.
	#
	if (-e "$sshdir/id_rsa") {
	    my $ident = `cat $sshdir/id_rsa.pub`;

	    if ($ident =~
		/^(ssh-rsa [-\w\.\@\+\/\=]*) ([-\w\@\.\ ]*)$/) {
		DBQueryFatal("delete from user_pubkeys ".
			     "where uid='$user' and pubkey='$1 $2'");
	    }
	    unlink("$sshdir/id_rsa");
	}

	# Hmm, need to use -C option so comment field makes sense.
422
    
Leigh B. Stoller's avatar
Leigh B. Stoller committed
423
424
425
426
427
	if (system("$KEYGEN -t rsa -P '' ".
		   "-C '${user}" . "\@" . ${OURDOMAIN} . "' ".
		   "-f $sshdir/id_rsa")) {
	    fatal("Failure in ssh-keygen!");
	}
428

Leigh B. Stoller's avatar
Leigh B. Stoller committed
429
430
431
432
	#
	# Grab a copy for the DB.
	# 
	my $ident = `cat $sshdir/id_rsa.pub`;
433

Leigh B. Stoller's avatar
Leigh B. Stoller committed
434
435
436
437
	if ($ident =~
	    /^(ssh-rsa [-\w\.\@\+\/\=]*) ([-\w\@\.\ ]*)$/) {
	    DBQueryFatal("replace into user_pubkeys ".
			 "values ('$user', 0, '$1 $2', now(), '$2')");
438

Leigh B. Stoller's avatar
Leigh B. Stoller committed
439
440
441
442
443
	    #
	    # Backwards compat. Remove later.
	    #
	    DBQueryFatal("update users set emulab_pubkey='$1 $2' ".
			 "where uid='$user'");
444
445
	}
	else {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
446
	    fatal("Bad protocol 2 public key: $ident\n");
447
448
	}
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
449
    return GenerateKeyFile();
450
451
452
453
454
455
}

#
# Generate ssh authorized_keys files. Either protocol 1 or 2.
# Returns 0 on success, -1 on failure.
#
Leigh B. Stoller's avatar
Leigh B. Stoller committed
456
sub GenerateKeyFile()
457
{
Leigh B. Stoller's avatar
Leigh B. Stoller committed
458
    my @pkeys   = ();
459
460
461
462
463
464
465
466
467
    my $sshdir  = "$HOMEDIR/$user/.ssh";
    my $keyfile = "$sshdir/authorized_keys";
	
    if (! -e $sshdir) {
	if (! mkdir($sshdir, 0700)) {
	    warn("*** WARNING: Could not mkdir $sshdir: $!\n");
	    return -1;
	}
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
468
469
470
471
472
    my $query_result =
	DBQueryFatal("select pubkey from user_pubkeys where uid='$user'");

    while (my ($key) = $query_result->fetchrow_array()) {
	push(@pkeys, $key);
473
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
474
    
475
476
477
478
479
480
481
482
483
484
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
    print "Generating $keyfile ...\n";

    if (!open(AUTHKEYS, "> ${keyfile}.new")) {
	warn("*** WARNING: Could not open ${keyfile}.new: $!\n");
	return -1;
    }
    print AUTHKEYS "#\n";
    print AUTHKEYS "# DO NOT EDIT! This file auto generated by ".
	"Emulab.Net account software.\n";
    print AUTHKEYS "#\n";
    print AUTHKEYS "# Please use the web interface to edit your ".
	"public key list.\n";
    print AUTHKEYS "#\n";
    
    foreach my $key (@pkeys) {
	print AUTHKEYS "$key\n";
    }
    close(AUTHKEYS);

    if (!chmod(0600, "${keyfile}.new")) {
	warn("*** WARNING: Could not chmod ${keyfile}.new: $!\n");
	return -1;
    }
    if (-e "${keyfile}") {
	if (system("cp -p -f ${keyfile} ${keyfile}.old")) {
	    warn("*** Could not save off ${keyfile}: $!\n");
	    return -1;
	}
	if (!chmod(0600, "${keyfile}.old")) {
	    warn("*** Could not chmod ${keyfile}.old: $!\n");
	}
    }
    if (system("mv -f ${keyfile}.new ${keyfile}")) {
	warn("*** Could not mv ${keyfile} to ${keyfile}.new: $!\n");
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
510
511
512
513
514
515
    elsif (-e "$sshdir/authorized_keys2") {
	#
	# Save to remove deprecated authorized_keys2 file at this point.
	#
	unlink("$sshdir/authorized_keys2");
    }
516
517
518
519
520
521
522
523
524
    return 0;
}

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

    die("*** $0:\n".
	"    $mesg\n");
}