summaryrefslogtreecommitdiffstats
path: root/src/commands/sskm
blob: eb51f691c9f8d76e4c7287c4b394f1884deaa64f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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
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
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
#!/usr/bin/perl
use strict;
use warnings;

use lib $ENV{GL_LIBDIR};
use Gitolite::Rc;
use Gitolite::Common;

=for usage
Usage for this command is not that simple.  Please read the full documentation
in doc/sskm.mkd or online at http://gitolite.com/gitolite/sskm.html.
=cut

usage() if @ARGV and $ARGV[0] eq '-h';

my $rb = $rc{GL_REPO_BASE};
my $ab = $rc{GL_ADMIN_BASE};
# get to the keydir
_chdir("$ab/keydir");

# save arguments for later
my $operation = shift || 'list';
my $keyid     = shift || '';
# keyid must fit a very specific pattern
$keyid and $keyid !~ /^@[-0-9a-z_]+$/i and die "invalid keyid $keyid\n";

# get the actual userid and keytype
my $gl_user = $ENV{GL_USER};
my $keytype = '';
$keytype = $1 if $gl_user =~ s/^zzz-marked-for-(...)-//;
print STDERR "hello $gl_user, you are currently using "
  . (
    $keytype
    ? "a key in the 'marked for $keytype' state\n"
    : "a normal (\"active\") key\n"
  );

# ----
# first collect the keys

my ( @pubkeys, @marked_for_add, @marked_for_del );
# get the list of pubkey files for this user, including pubkeys marked for
# add/delete

for my $pubkey (`find . -type f -name "*.pub" | sort`) {
    chomp($pubkey);
    $pubkey =~ s(^./)();    # artifact of the find command

    my $user = $pubkey;
    $user =~ s(.*/)();                # foo/bar/baz.pub -> baz.pub
    $user =~ s/(\@[^.]+)?\.pub$//;    # baz.pub, baz@home.pub -> baz

    next unless $user eq $gl_user or $user =~ /^zzz-marked-for-...-$gl_user/;

    if ( $user =~ m(^zzz-marked-for-add-) ) {
        push @marked_for_add, $pubkey;
    } elsif ( $user =~ m(^zzz-marked-for-del-) ) {
        push @marked_for_del, $pubkey;
    } else {
        push @pubkeys, $pubkey;
    }
}

# ----
# list mode; just do it and exit
sub print_keylist {
    my ( $message, @list ) = @_;
    return unless @list;
    print "== $message ==\n";
    my $count = 1;
    for (@list) {
        my $fp = fingerprint($_);
        s/zzz-marked(\/|-for-...-)//g;
        print $count++ . ": $fp : $_\n";
    }
}
if ( $operation eq 'list' ) {
    print "you have the following keys:\n";
    print_keylist( "active keys",                          @pubkeys );
    print_keylist( "keys marked for addition/replacement", @marked_for_add );
    print_keylist( "keys marked for deletion",             @marked_for_del );
    print "\n\n";
    exit;
}

# ----
# please see docs for details on how a user interacts with this

if ( $keytype eq '' ) {
    # user logging in with a normal key
    die "valid operations: add, del, undo-add, confirm-del\n" unless $operation =~ /^(add|del|confirm-del|undo-add)$/;
    if ( $operation eq 'add' ) {
        print STDERR "please supply the new key on STDIN.  (I recommend you
        don't try to do this interactively, but use a pipe)\n";
        kf_add( $gl_user, $keyid, safe_stdin() );
    } elsif ( $operation eq 'del' ) {
        kf_del( $gl_user, $keyid );
    } elsif ( $operation eq 'confirm-del' ) {
        die "you dont have any keys marked for deletion\n" unless @marked_for_del;
        kf_confirm_del( $gl_user, $keyid );
    } elsif ( $operation eq 'undo-add' ) {
        die "you dont have any keys marked for addition\n" unless @marked_for_add;
        kf_undo_add( $gl_user, $keyid );
    }
} elsif ( $keytype eq 'del' ) {
    # user is using a key that was marked for deletion.  The only possible use
    # for this is that she changed her mind for some reason (maybe she marked
    # the wrong key for deletion) or is not able to get her client-side sshd
    # to stop using this key
    die "valid operations: undo-del\n" unless $operation eq 'undo-del';

    # reinstate the key
    kf_undo_del( $gl_user, $keyid );
} elsif ( $keytype eq 'add' ) {
    die "valid operations: confirm-add\n" unless $operation eq 'confirm-add';
    # user is trying to validate a key that has been previously marked for
    # addition.  This isn't interactive, but it *could* be... if someone asked
    kf_confirm_add( $gl_user, $keyid );
}

exit;

# ----

# make a temp clone and switch to it
our $TEMPDIR;
BEGIN { $TEMPDIR = `mktemp -d -t tmp.XXXXXXXXXX`; }
END { `/bin/rm -rf $TEMPDIR`; }

sub cd_temp_clone {
    chomp($TEMPDIR);
    hushed_git( "clone", "$rb/gitolite-admin.git", "$TEMPDIR" );
    chdir($TEMPDIR);
    my $hostname = `hostname`; chomp($hostname);
    hushed_git( "config", "--get", "user.email" ) and hushed_git( "config", "user.email", $ENV{USER} . "@" . $hostname );
    hushed_git( "config", "--get", "user.name" )  and hushed_git( "config", "user.name",  "$ENV{USER} on $hostname" );
}

sub fingerprint {
    my ($fp, $output) = ssh_fingerprint_file(shift);
    # Do not print the output of $output to an untrusted destination.
    die "does not seem to be a valid pubkey\n" unless $fp;
    return $fp;
}

sub safe_stdin {
    # read one line from STDIN
    my $data;
    my $ret = read STDIN, $data, 4096;
    # current pubkeys are approx 400 bytes so we go a little overboard
    die "could not read pubkey data" . ( defined($ret) ? "" : ": $!" ) . "\n" unless $ret;
    die "pubkey data seems to have more than one line\n" if $data =~ /\n./;
    return $data;
}

sub hushed_git {
    local (*STDOUT) = \*STDOUT;
    local (*STDERR) = \*STDERR;
    open( STDOUT, ">", "/dev/null" );
    open( STDERR, ">", "/dev/null" );
    system( "git", @_ );
}

sub highlander {
    # there can be only one
    my ( $keyid, $die_if_empty, @a ) = @_;
    # too many?
    if ( @a > 1 ) {
        print STDERR "
more than one key satisfies this condition, and I can't deal with that!
The keys are:

";
        print STDERR "\t" . join( "\n\t", @a ), "\n\n";
        exit 1;
    }
    # too few?
    die "no keys with " . ( $keyid || "empty" ) . " keyid found\n" if $die_if_empty and not @a;

    return @a;
}

sub kf_add {
    my ( $gl_user, $keyid, $keymaterial ) = @_;

    # add a new "marked for addition" key for $gl_user.
    cd_temp_clone();
    chdir("keydir");

    mkdir("zzz-marked");
    _print( "zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub", $keymaterial );
    hushed_git( "add", "." ) and die "git add failed\n";
    my $fp = fingerprint("zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub");
    hushed_git( "commit", "-m", "sskm: add $gl_user$keyid ($fp)" ) and die "git commit failed\n";
    system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}

sub kf_confirm_add {
    my ( $gl_user, $keyid ) = @_;
    # find entries in both @pubkeys and @marked_for_add whose basename matches $gl_user$keyid
    my @pk  = highlander( $keyid, 0, grep { m(^(.*/)?$gl_user$keyid.pub$) } @pubkeys );
    my @mfa = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub$) } @marked_for_add );

    cd_temp_clone();
    chdir("keydir");

    my $fp = fingerprint( $mfa[0] );
    if ( $pk[0] ) {
        hushed_git( "mv", "-f", $mfa[0], $pk[0] );
        hushed_git( "commit", "-m", "sskm: confirm-add (replace) $pk[0] ($fp)" ) and die "git commit failed\n";
    } else {
        hushed_git( "mv", "-f", $mfa[0], "$gl_user$keyid.pub" );
        hushed_git( "commit", "-m", "sskm: confirm-add $gl_user$keyid ($fp)" ) and die "git commit failed\n";
    }
    system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}

sub kf_undo_add {
    # XXX some code at start is shared with kf_confirm_add
    my ( $gl_user, $keyid ) = @_;
    my @mfa = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-add-$gl_user$keyid.pub$) } @marked_for_add );

    cd_temp_clone();
    chdir("keydir");

    my $fp = fingerprint( $mfa[0] );
    hushed_git( "rm", $mfa[0] );
    hushed_git( "commit", "-m", "sskm: undo-add $gl_user$keyid ($fp)" ) and die "git commit failed\n";
    system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}

sub kf_del {
    my ( $gl_user, $keyid ) = @_;

    cd_temp_clone();
    chdir("keydir");

    mkdir("zzz-marked");
    my @pk = highlander( $keyid, 1, grep { m(^(.*/)?$gl_user$keyid.pub$) } @pubkeys );

    my $fp = fingerprint( $pk[0] );
    hushed_git( "mv", $pk[0], "zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub" ) and die "git mv failed\n";
    hushed_git( "commit", "-m", "sskm: del $pk[0] ($fp)" ) and die "git commit failed\n";
    system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}

sub kf_confirm_del {
    my ( $gl_user, $keyid ) = @_;
    my @mfd = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub$) } @marked_for_del );

    cd_temp_clone();
    chdir("keydir");

    my $fp = fingerprint( $mfd[0] );
    hushed_git( "rm", $mfd[0] );
    hushed_git( "commit", "-m", "sskm: confirm-del $gl_user$keyid ($fp)" ) and die "git commit failed\n";
    system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}

sub kf_undo_del {
    my ( $gl_user, $keyid ) = @_;

    my @mfd = highlander( $keyid, 1, grep { m(^zzz-marked/zzz-marked-for-del-$gl_user$keyid.pub$) } @marked_for_del );

    print STDERR "
You're undeleting a key that is currently marked for deletion.
    Hit ENTER to undelete this key
    Hit Ctrl-C to cancel the undelete
Please see documentation for caveats on the undelete process as well as how to
actually delete it.
";
    <>;    # yeay... always wanted to do that -- throw away user input!

    cd_temp_clone();
    chdir("keydir");

    my $fp = fingerprint( $mfd[0] );
    hushed_git( "mv", "-f", $mfd[0], "$gl_user$keyid.pub" );
    hushed_git( "commit", "-m", "sskm: undo-del $gl_user$keyid ($fp)" ) and die "git commit failed\n";
    system("gitolite push >/dev/null 2>/dev/null") and die "git push failed\n";
}