addpubkey.in 10.2 KB
Newer Older
1
2
3
#!/usr/bin/perl -wT
#
# EMULAB-COPYRIGHT
4
# Copyright (c) 2000-2004 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
66
67
68
69
70
71
72
73
74
75
76
use libdb;
use libtestbed;

#
# 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'};

77
78
79
80
81
82
83
84
#
# 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");
}

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#
# 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;
}
107
108
109
if (defined($options{"i"})) {
    $initmode = 1;
}
Leigh B. Stoller's avatar
Leigh B. Stoller committed
110
111
112
if (defined($options{"f"})) {
    $force = 1;
}
113
114
if (defined($options{"w"})) {
    $genmode = 1;
115
}
116
if ($verify && $genmode) {
117
118
    usage();
}
119
120
121
122
123
124
125
126
127
128
129
130
if ($initmode || $genmode) {
    if (@ARGV != 1) {
	usage();
    }
}
elsif (@ARGV == 2) {
    $keyfile = $ARGV[1];
}
else {
    usage();
}    
$user = $ARGV[0];
131
132
133
134

#
# Untaint the arguments.
#
Robert Ricci's avatar
Robert Ricci committed
135
if ($user =~ /^([-\w_]+)$/i) {
136
137
138
139
140
141
142
143
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
    $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;
    exit GenerateKeyFiles();
}

# Else, key parse mode ...
170
171
172
173
174
175
176
177
178
179
180
181
if ($iskey) {
    if ($keyfile =~ /^([-\w\s\.\@\+\/\=]*)$/) {
	$keyfile = $1;
    }
    else {
	fatal("Tainted key: $keyfile");
    }
    $keyline = $keyfile;
}
else {
    if ($keyfile =~ /^([-\w\.\/]+)$/) {
	$keyfile = $1;
182
     }
183
184
185
186
    else {
	fatal("Tainted filename: $keyfile");
    }
    if (! -e $keyfile) {
187
	fatal("No such file: $keyfile\n");
188
189
190
191
192
    }
    $keyline = `head -1 $keyfile`;
}

#
193
# Check user
194
#
195
196
197
198
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");
199
    }
200
201
202
203
204
205
206
207
208
209
210
211
212
213
    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;
214
	}
215
216
	else {
	    fatal("No DB info for $user!");
217
218
	}
    }
219
220
}

221
222
223
# Drop root privs, switching to user.
if (!$nobody) {
    $EUID = $USERUID;
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
}

#
# 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) = @_;
253
254
255
256
257
258
259

    # Enforce a reasonable length on the key.
    if (length($keyline) > 4096) {
	print "Key is too long!\n";
	print "Key: $keyline\n";
	return 0;
    }
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
    
    if ($keyline =~ /^(\d*\s\d*\s[0-9a-zA-Z]*) ([-\w\@\.]*)$/) {
        # 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 =~
	   /^(ssh-rsa|ssh-dss) ([-\w\.\@\+\/\=]*) ([-\w\@\.]*)$/) {
        # 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)) {
286
287
	print "Key cannot be parsed!\n";
	print "Key: $keyline\n";
288
289
	return 0;
    }
290

291
292
    # Do not enter into DB if in verify mode.
    if ($verify) {
Leigh B. Stoller's avatar
Leigh B. Stoller committed
293
	print "Key was good: $type\n";
294
295
296
297
	return 1;
    }

    #
298
    # Make up a comment field for the DB. 
299
300
301
302
303
304
305
    #
    if (!defined($comment)) {
	$comment = "$type-${user_email}";
    }
    $key = "$key $comment";

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

308
309
310
    #
    # Mark user record as modified so nodes are updated.
    #
311
    TBNodeUpdateAccountsByUID($user);
312

313
314
315
316
317
318
319
320
    my $chunked = "";

    while (length($key)) {
	$chunked .= substr($key, 0, 65, "");
	if (length($key)) {
	    $chunked .= "\n";
	}
    }
321
322
    print "SSH Public Key for '$user' added:\n";
    print "$chunked\n";
323
    
324
325
326
327
328
329
330
331
332
333
334
335
336
337
    # Generate new auth keys file. 
    if ($genmode) {
	GenerateKeyFiles();
    }

    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;
338
339
}

340
341
342
343
344
345
#
# 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()
346
{
347
    my $sshdir  = "$HOMEDIR/$user/.ssh";
348
349

    #
350
    # Set up the ssh key, but only if not done so already.
351
    #
352
353
354
355
    if (! -e "$sshdir") {
	mkdir("$sshdir", 0700) or
	    fatal("Could not mkdir $sshdir: $!");
    }
Leigh B. Stoller's avatar
Leigh B. Stoller committed
356
    if (! -f "$sshdir/identity" || $force) {
357
	print "Setting up ssh configuration for $user.\n";
Leigh B. Stoller's avatar
Leigh B. Stoller committed
358

359
360
361
	unlink("$sshdir/identity")
	    if (-f "$sshdir/identity");

Leigh B. Stoller's avatar
Leigh B. Stoller committed
362
	# Hmm, need to use -C option so comment field makes sense.
363
    
Leigh B. Stoller's avatar
Leigh B. Stoller committed
364
365
366
	if (system("$KEYGEN -t rsa1 -P '' ".
		   "-C '${user}" . "\@" . ${OURDOMAIN} . "' ".
		   "-f $sshdir/identity")) {
367
368
369
370
371
372
373
374
375
376
	    fatal("Failure in ssh-keygen!");
	}

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

	if ($ident =~ /(\d*\s\d*\s[0-9a-zA-Z]*)\s([\w\@\.]*)/) {
	    DBQueryFatal("replace into user_pubkeys ".
377
			 "values ('$user', 0, '$1 $2', now(), '$2')");
378
379
380
381
382
383
384
385
386
387
388
389

	    #
	    # Backwards compat. Remove later.
	    #
	    DBQueryFatal("update users set emulab_pubkey='$1 $2' ".
			 "where uid='$user'");
	}
	else {
	    fatal("Bad emulab public key: $ident\n");
	}
    }
    return GenerateKeyFiles();
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
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
#
# Generate the ssh key files for the user. The keys come from the DB, and
# are split into protocol 1 and protocol 2 keys. Then use the aux function
# to generate each file. 
#
sub GenerateKeyFiles()
{
    my @p1keys = ();
    my @p2keys = ();
    
    my $query_result =
	DBQueryFatal("select * from user_pubkeys where uid='$user'");


    while (my %row = $query_result->fetchhash()) {
	my $key = $row{'pubkey'};

	if ($key =~ /^\d+\s+.*$/) {
	    push(@p1keys, $key);
	}
	else {
	    push(@p2keys, $key);
	}
    }
    GenerateKeyFile(1, @p1keys);
    GenerateKeyFile(2, @p2keys);
    return 0;
}

#
# Generate ssh authorized_keys files. Either protocol 1 or 2.
# Returns 0 on success, -1 on failure.
#
#
sub GenerateKeyFile($$)
{
    my ($protocol, @pkeys) = @_;
    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;
	}
    }
    if ($protocol == 2) {
	$keyfile .= "2";
    }
    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");
    }
    return 0;
}

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

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