Improve patch submission instructions
[dowkd.git] / dowkd.in
blob10de332ee967a660556494993dd24340d2e3dfa3
1 #!/usr/bin/perl
3 # Debian/OpenSSL Weak Key Detector
5 # Written by Florian Weimer <fw@deneb.enyo.de>, with blacklist data
6 # from Kees Cook, Peter Palfrader and James Strandboge.
8 # Patches and comments are welcome. Please send them to
9 # <fw@deneb.enyo.de>, and use "dowkd" in the subject line.
11 use strict;
12 use warnings;
14 sub help () {
15 print <<EOF;
16 usage: $0 [OPTIONS...] COMMAND [ARGUMENTS...]
18 COMMAND is one of:
20 file: examine files on the command line for weak keys
21 host: examine the specified hosts for weak SSH keys
22 user: examine user SSH keys for weakness; examine all users if no
23 users are given
24 help: show this help screen
26 OPTIONS is one pf:
28 -c FILE: set the database cache file name (default: dowkd.db)
30 dowkd currently handles the following OpenSSH host and user keys,
31 provided they have been generated on a little-endian architecture
32 (such as i386 or amd64): RSA/1024, RSA/2048 and DSA/1024. (The
33 OpenSSH version in Debian does not support DSA key generation with)
34 other sizes.
36 OpenVPN shared also detected on little-endian architecture.
38 Note that the blacklist by dowkd may be incomplete; it is only
39 intended as a quick check.
41 EOF
44 use DB_File;
45 use File::Temp;
46 use Fcntl;
47 use IO::Handle;
49 my $db_version = '1';
51 my $db_file = 'dowkd.db';
53 my $db;
54 my %db;
56 sub create_db () {
57 $db = tie %db, 'DB_File', $db_file, O_RDWR | O_CREAT, 0777, $DB_BTREE
58 or die "error: could not open database: $!\n";
60 $db{''} = $db_version;
61 while (my $line = <DATA>) {
62 next if $line =~ /^\**$/;
63 chomp $line;
64 $line =~ /^[0-9a-f]{32}$/ or die "error: invalid data line";
65 $line =~ s/(..)/chr(hex($1))/ge;
66 $db{$line} = '';
69 $db->sync;
72 sub open_db () {
73 if (-r $db_file) {
74 $db = tie %db, 'DB_File', $db_file, O_RDONLY, 0777, $DB_BTREE
75 or die "error: could not open database: $!\n";
76 my $stored_version = $db{''};
77 $stored_version && $stored_version eq $db_version or create_db;
78 } else {
79 unlink $db_file;
80 create_db;
84 sub safe_backtick (@) {
85 my @args = @_;
86 my $fh;
87 open $fh, '-|', @args
88 or die "error: failed to spawn $args[0]: $!\n";
89 my @result;
90 if (wantarray) {
91 @result = <$fh>;
92 } else {
93 local $/;
94 @result = scalar(<$fh>);
96 close $fh;
97 $? == 0 or return undef;
98 if (wantarray) {
99 return @result;
100 } else {
101 return $result[0];
105 my $keys_found = 0;
106 my $keys_vulnerable = 0;
108 sub print_stats () {
109 print STDERR "summary: keys found: $keys_found, weak keys: $keys_vulnerable\n";
112 sub check_hash ($$) {
113 my ($name, $hash) = @_;
114 ++$keys_found;
115 if (exists $db{$hash}) {
116 ++$keys_vulnerable;
117 print "$name: weak key\n";
121 sub ssh_fprint_file ($) {
122 my $name = shift;
123 my $data = safe_backtick qw/ssh-keygen -l -f/, $name;
124 defined $data or return ();
125 my @data = $data =~ /^(\d+) ([0-9a-f]{2}(?::[0-9a-f]{2}){15})/;
126 return @data if @data == 2;
127 return ();
130 sub ssh_fprint_check ($$$) {
131 my ($name, $length, $hash) = @_;
132 if ($length == 1024 || $length == 2048) {
133 $hash =~ y/://d;
134 $hash =~ s/(..)/chr(hex($1))/ge;
135 check_hash $name, $hash;
136 } else {
137 warn "$name: warning: no suitable blacklist\n";
141 sub from_ssh_key_file ($) {
142 my $name = shift;
143 my ($length, $hash) = ssh_fprint_file $name;
144 if ($length && $hash) {
145 ssh_fprint_check "$name:1", $length, $hash;
146 } else {
147 warn "$name:1: warning: failed to parse SSH key file\n";
151 sub clear_tmp ($) {
152 my $tmp = shift;
153 seek $tmp, 0, 0 or die "seek: $!";
154 truncate $tmp, 0 or die "truncate: $!";
157 sub from_ssh_auth_file ($) {
158 my $name = shift;
159 my $auth;
160 unless (open $auth, '<', $name) {
161 warn "$name:0: error: open failed: $!\n";
162 return;
164 my $tmp = new File::Temp;
165 while (my $line = <$auth>) {
166 chomp $line;
167 next if $line =~ m/^\s*(#|$)/;
168 my $lineno = $.;
169 clear_tmp $tmp;
170 print $tmp "$line\n" or die "print: $!";
171 $tmp->flush;
172 my ($length, $hash) = ssh_fprint_file "$tmp";
173 if ($length && $hash) {
174 ssh_fprint_check "$name:$lineno", $length, $hash;
175 } else {
176 warn "$name:$lineno: warning: unparsable line\n";
181 sub from_openvpn_key ($) {
182 my $name = shift;
183 my $key;
184 unless (open $key, '<', $name) {
185 warn "$name:0: open failed: $!\n";
186 return 1;
189 my $marker;
190 while (my $line = <$key>) {
191 return 0 if $. > 10;
192 if ($line =~ /^-----BEGIN OpenVPN Static key V1-----/) {
193 $marker = 1;
194 } elsif ($marker) {
195 if ($line =~ /^([0-9a-f]{32})/) {
196 $line = $1;
197 $line =~ s/(..)/chr(hex($1))/ge;
198 check_hash "$name:$.", $line;
199 return 1;
200 } else {
201 warn "$name:$.: warning: illegal OpenVPN file format\n";
202 return 1;
208 sub from_ssh_host (@) {
209 my @names = @_;
210 my @lines;
211 push @lines, safe_backtick qw/ssh-keyscan -t rsa/, @names;
212 push @lines, safe_backtick qw/ssh-keyscan -t dsa/, @names;
214 my $tmp = new File::Temp;
215 for my $line (@lines) {
216 next if $line =~ /^#/;
217 my ($host, $data) = $line =~ /^(\S+) (.*)$/;
218 clear_tmp $tmp;
219 print $tmp "$data\n" or die "print: $!";
220 $tmp->flush;
221 my ($length, $hash) = ssh_fprint_file "$tmp";
222 if ($length && $hash) {
223 ssh_fprint_check "$host", $length, $hash;
224 } else {
225 warn "$host: warning: unparsable line\n";
230 sub from_user ($) {
231 my $user = shift;
232 my ($name,$passwd,$uid,$gid,
233 $quota,$comment,$gcos,$dir,$shell,$expire) = getpwnam($user);
234 my $file = "$dir/.ssh/authorized_keys";
235 from_ssh_auth_file $file if -r $file;
236 $file = "$dir/.ssh/authorized_keys2";
237 from_ssh_auth_file $file if -r $file;
238 $file = "$dir/.ssh/known_hosts";
239 from_ssh_auth_file $file if -r $file;
240 $file = "$dir/.ssh/known_hosts2";
241 from_ssh_auth_file $file if -r $file;
242 $file = "$dir/.ssh/id_rsa.pub";
243 from_ssh_key_file $file if -r $file;
244 $file = "$dir/.ssh/id_dsa.pub";
245 from_ssh_key_file $file if -r $file;
248 sub from_user_all () {
249 # This was one loop initially, but does not work with some Perl
250 # versions.
251 setpwent;
252 my @names;
253 while (my $name = getpwent) {
254 push @names, $name;
256 endpwent;
257 from_user $_ for @names;
260 if (@ARGV && $ARGV[0] eq '-c') {
261 shift @ARGV;
262 $db_file = shift @ARGV if @ARGV;
264 if (@ARGV) {
265 open_db;
266 my $cmd = shift @ARGV;
267 if ($cmd eq 'file') {
268 for my $name (@ARGV) {
269 next if from_openvpn_key $name;
270 from_ssh_auth_file $name;
272 } elsif ($cmd eq 'host') {
273 from_ssh_host @ARGV;
274 } elsif ($cmd eq 'user') {
275 if (@ARGV) {
276 from_user $_ for @ARGV;
277 } else {
278 from_user_all;
280 } elsif ($cmd eq 'help') {
281 help;
282 exit 0;
283 } else {
284 die "error: invalid command, use \"help\" to get help\n";
286 print_stats;
287 } else {
288 help;
289 exit 1;
292 my %hash;
294 __DATA__