minor typo in pattern...
[gitolite.git] / src / commands / sshkeys-lint
blobc7f0c81268b1aac308dd49edb5872f260ae2efa3
1 #!/usr/bin/perl
2 use strict;
3 use warnings;
5 # complete rewrite of the sshkeys-lint program. Usage has changed, see
6 # usage() function or run without arguments.
8 use Getopt::Long;
9 my $admin = 0;
10 my $quiet = 0;
11 my $help = 0;
12 GetOptions( 'admin|a=s' => \$admin, 'quiet|q' => \$quiet, 'help|h' => \$help );
14 use Data::Dumper;
15 $Data::Dumper::Deepcopy = 1;
16 $|++;
18 my $in_gl_section = 0;
19 my $warnings = 0;
20 my $KEYTYPE_REGEX = qr/\b(?:ssh-(?:rsa|dss|ed25519)|ecdsa-sha2-nistp(?:256|384|521))\b/;
22 sub dbg {
23 use Data::Dumper;
24 for my $i (@_) {
25 print STDERR "DBG: " . Dumper($i);
29 sub msg {
30 my $warning = shift;
31 return if $quiet and not $warning;
32 $warnings++ if $warning;
33 print "sshkeys-lint: " . ( $warning ? "WARNING: " : "" ) . $_ for @_;
36 usage() if $help;
38 our @pubkeyfiles = @ARGV; @ARGV = ();
39 my $kd = "$ENV{HOME}/.gitolite/keydir";
40 if ( not @pubkeyfiles ) {
41 chomp( @pubkeyfiles = `find $kd -type f -name "*.pub" | sort` );
44 if ( -t STDIN ) {
45 @ARGV = ("$ENV{HOME}/.ssh/authorized_keys");
48 # ------------------------------------------------------------------------
50 my @authkeys;
51 my %seen_fprints;
52 my %pkf_by_fp;
53 msg 0, "==== checking authkeys file:\n";
54 fill_authkeys(); # uses up STDIN
56 if ($admin) {
57 my $fp = fprint("$admin.pub");
58 my $fpu = ( $fp && $seen_fprints{$fp}{user} || 'no access' );
59 # dbg("fpu = $fpu, admin=$admin");
60 #<<<
61 die "\t\t*** FATAL ***\n" .
62 "$admin.pub maps to $fpu, not $admin.\n" .
63 "You will not be able to access gitolite with this key.\n" .
64 "Look for the 'ssh troubleshooting' link in http://gitolite.com/gitolite/ssh.html.\n"
65 if $fpu ne "user $admin";
66 #>>>
69 msg 0, "==== checking pubkeys:\n" if @pubkeyfiles;
70 for my $pkf (@pubkeyfiles) {
71 # get the short name for the pubkey file
72 ( my $pkfsn = $pkf ) =~ s(^$kd/)();
74 my $fp = fprint($pkf);
75 next unless $fp;
76 msg 1, "$pkfsn appears to be a COPY of $pkf_by_fp{$fp}\n" if $pkf_by_fp{$fp};
77 $pkf_by_fp{$fp} ||= $pkfsn;
78 my $fpu = ( $seen_fprints{$fp}{user} || 'no access' );
79 msg 0, "$pkfsn maps to $fpu\n";
82 if ($warnings) {
83 print "\n$warnings warnings found\n";
86 exit $warnings;
88 # ------------------------------------------------------------------------
89 sub fill_authkeys {
90 while (<>) {
91 my $seq = $.;
92 next if ak_comment($_); # also sets/clears $in_gl_section global
93 my $fp = fprint($_);
94 my $user = user($_);
96 check( $seq, $fp, $user );
98 $authkeys[$seq]{fprint} = $fp;
99 $authkeys[$seq]{ustatus} = $user;
103 sub check {
104 my ( $seq, $fp, $user ) = @_;
106 msg 1, "line $seq, $user key found *outside* gitolite section!\n"
107 if $user =~ /^user / and not $in_gl_section;
109 msg 1, "line $seq, $user key found *inside* gitolite section!\n"
110 if $user !~ /^user / and $in_gl_section;
112 if ( $seen_fprints{$fp} ) {
113 #<<<
114 msg 1, "authkeys line $seq ($user) will be ignored by sshd; " .
115 "same key found on line " .
116 $seen_fprints{$fp}{seq} . " (" .
117 $seen_fprints{$fp}{user} . ")\n";
118 return;
119 #>>>
122 $seen_fprints{$fp}{seq} = $seq;
123 $seen_fprints{$fp}{user} = $user;
126 sub user {
127 my $user = '';
128 $user ||= "user $1" if /^command=.*gitolite-shell (.*?)"/;
129 $user ||= "unknown command" if /^command/;
130 $user ||= "shell access" if /$KEYTYPE_REGEX/;
132 return $user;
135 sub ak_comment {
136 local $_ = shift;
137 $in_gl_section = 1 if /^# gitolite start/;
138 $in_gl_section = 0 if /^# gitolite end/;
139 die "gitosis? what's that?\n" if /^#.*gitosis/;
140 return /^\s*(#|$)/;
143 sub fprint {
144 local $_ = shift;
145 my ( $fh, $tempfn, $in );
146 if ( /$KEYTYPE_REGEX/ ) {
147 # an actual key was passed. Since ssh-keygen requires an actual file,
148 # make a temp file to take the data and pass on to ssh-keygen
149 s/^.* ($KEYTYPE_REGEX)/$1/;
150 use File::Temp qw(tempfile);
151 ( $fh, $tempfn ) = tempfile();
152 $in = $tempfn;
153 print $fh $_;
154 close $fh;
155 } else {
156 # a filename was passed
157 $in = $_;
159 # dbg("in = $in");
160 -f $in or die "file not found: $in\n";
161 open( $fh, "ssh-keygen -l -f $in |" ) or die "could not fork: $!\n";
162 my $fp = <$fh>;
163 # dbg("fp = $fp");
164 close $fh;
165 unlink $tempfn if $tempfn;
166 warn "$fp\n" unless $fp =~ /([0-9a-f][0-9a-f](:[0-9a-f][0-9a-f])+)/ or $fp =~ m(SHA256:([A-Za-z0-9+/]+));
168 return $1;
171 # ------------------------------------------------------------------------
172 sub usage {
173 print <<EOF;
175 Usage: gitolite sshkeys-lint [-q] [optional list of pubkey filenames]
176 (optionally, STDIN can be a pipe or redirected from a file; see below)
178 Look for potential problems in ssh keys.
180 sshkeys-lint expects:
181 - the contents of an authorized_keys file via STDIN, otherwise it uses
182 \$HOME/.ssh/authorized_keys
183 - one or more pubkey filenames as arguments, otherwise it uses all the keys
184 found (recursively) in \$HOME/.gitolite/keydir
186 The '-q' option will print only warnings instead of all mappings.
188 Note that this runs ssh-keygen -l for each line in the authkeys file and each
189 pubkey in the argument list, so be wary of running it on something huge. This
190 is meant for troubleshooting.
193 exit 1;