Add OpenSSL blacklists
[dowkd.git] / dowkd.in
blob2cf4eb853d57bd9467735bbd628dc8fca5e0d71b
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.
10 use strict;
11 use warnings;
13 sub help () {
14 print <<EOF;
15 usage: $0 [OPTIONS...] COMMAND [ARGUMENTS...]
17 COMMAND is one of:
19 file: examine files on the command line for weak keys
20 host: examine the specified hosts for weak SSH keys
21 user: examine user SSH keys for weakness; examine all users if no
22 users are given
23 help: show this help screen
25 OPTIONS is one pf:
27 -c FILE: set the database cache file name (default: dowkd.db)
29 dowkd currently handles the following OpenSSH host and user keys,
30 provided they have been generated on a little-endian architecture
31 (such as i386 or amd64): RSA/1024, RSA/2048 and DSA/1024. (The
32 OpenSSH version in Debian does not support DSA key generation with)
33 other sizes.
35 OpenVPN shared also detected on little-endian architecture.
37 Note that the blacklist by dowkd may be incomplete; it is only
38 intended as a quick check.
40 EOF
43 use DB_File;
44 use File::Temp;
45 use Fcntl;
46 use IO::Handle;
48 my $db_version = '1';
50 my $db_file = 'dowkd.db';
52 my $db;
53 my %db;
55 sub create_db () {
56 $db = tie %db, 'DB_File', $db_file, O_RDWR | O_CREAT, 0777, $DB_BTREE
57 or die "error: could not open database: $!\n";
59 $db{''} = $db_version;
60 while (my $line = <DATA>) {
61 next if $line =~ /^\**$/;
62 chomp $line;
63 $line =~ /^[0-9a-f]{32}$/ or die "error: invalid data line";
64 $line =~ s/(..)/chr(hex($1))/ge;
65 $db{$line} = '';
68 $db->sync;
71 sub open_db () {
72 if (-r $db_file) {
73 $db = tie %db, 'DB_File', $db_file, O_RDONLY, 0777, $DB_BTREE
74 or die "error: could not open database: $!\n";
75 my $stored_version = $db{''};
76 $stored_version && $stored_version eq $db_version or create_db;
77 } else {
78 unlink $db_file;
79 create_db;
83 sub safe_backtick (@) {
84 my @args = @_;
85 my $fh;
86 open $fh, '-|', @args
87 or die "error: failed to spawn $args[0]: $!\n";
88 my @result;
89 if (wantarray) {
90 @result = <$fh>;
91 } else {
92 local $/;
93 @result = scalar(<$fh>);
95 close $fh;
96 $? == 0 or return undef;
97 if (wantarray) {
98 return @result;
99 } else {
100 return $result[0];
104 my $keys_found = 0;
105 my $keys_vulnerable = 0;
107 sub print_stats () {
108 print STDERR "summary: keys found: $keys_found, weak keys: $keys_vulnerable\n";
111 sub check_hash ($$) {
112 my ($name, $hash) = @_;
113 ++$keys_found;
114 if (exists $db{$hash}) {
115 ++$keys_vulnerable;
116 print "$name: weak key\n";
120 sub ssh_fprint_file ($) {
121 my $name = shift;
122 my $data = safe_backtick qw/ssh-keygen -l -f/, $name;
123 defined $data or return ();
124 my @data = $data =~ /^(\d+) ([0-9a-f]{2}(?::[0-9a-f]{2}){15})/;
125 return @data if @data == 2;
126 return ();
129 sub ssh_fprint_check ($$$) {
130 my ($name, $length, $hash) = @_;
131 if ($length == 1024 || $length == 2048) {
132 $hash =~ y/://d;
133 $hash =~ s/(..)/chr(hex($1))/ge;
134 check_hash $name, $hash;
135 } else {
136 warn "$name: warning: no suitable blacklist\n";
140 sub from_ssh_key_file ($) {
141 my $name = shift;
142 my ($length, $hash) = ssh_fprint_file $name;
143 if ($length && $hash) {
144 ssh_fprint_check "$name:1", $length, $hash;
145 } else {
146 warn "$name:1: warning: failed to parse SSH key file\n";
150 sub clear_tmp ($) {
151 my $tmp = shift;
152 seek $tmp, 0, 0 or die "seek: $!";
153 truncate $tmp, 0 or die "truncate: $!";
156 sub from_ssh_auth_file ($) {
157 my $name = shift;
158 my $auth;
159 unless (open $auth, '<', $name) {
160 warn "$name:0: error: open failed: $!\n";
161 return;
163 my $tmp = new File::Temp;
164 while (my $line = <$auth>) {
165 chomp $line;
166 next if $line =~ m/^\s*(#|$)/;
167 my $lineno = $.;
168 clear_tmp $tmp;
169 print $tmp "$line\n" or die "print: $!";
170 $tmp->flush;
171 my ($length, $hash) = ssh_fprint_file "$tmp";
172 if ($length && $hash) {
173 ssh_fprint_check "$name:$lineno", $length, $hash;
174 } else {
175 warn "$name:$lineno: warning: unparsable line\n";
180 sub from_openvpn_key ($) {
181 my $name = shift;
182 my $key;
183 unless (open $key, '<', $name) {
184 warn "$name:0: open failed: $!\n";
185 return 1;
188 my $marker;
189 while (my $line = <$key>) {
190 return 0 if $. > 10;
191 if ($line =~ /^-----BEGIN OpenVPN Static key V1-----/) {
192 $marker = 1;
193 } elsif ($marker) {
194 if ($line =~ /^([0-9a-f]{32})/) {
195 $line = $1;
196 $line =~ s/(..)/chr(hex($1))/ge;
197 check_hash "$name:$.", $line;
198 return 1;
199 } else {
200 warn "$name:$.: warning: illegal OpenVPN file format\n";
201 return 1;
207 sub from_ssh_host (@) {
208 my @names = @_;
209 my @lines;
210 push @lines, safe_backtick qw/ssh-keyscan -t rsa/, @names;
211 push @lines, safe_backtick qw/ssh-keyscan -t dsa/, @names;
213 my $tmp = new File::Temp;
214 for my $line (@lines) {
215 next if $line =~ /^#/;
216 my ($host, $data) = $line =~ /^(\S+) (.*)$/;
217 clear_tmp $tmp;
218 print $tmp "$data\n" or die "print: $!";
219 $tmp->flush;
220 my ($length, $hash) = ssh_fprint_file "$tmp";
221 if ($length && $hash) {
222 ssh_fprint_check "$host", $length, $hash;
223 } else {
224 warn "$host: warning: unparsable line\n";
229 sub from_user ($) {
230 my $user = shift;
231 my ($name,$passwd,$uid,$gid,
232 $quota,$comment,$gcos,$dir,$shell,$expire) = getpwnam($user);
233 my $file = "$dir/.ssh/authorized_keys";
234 from_ssh_auth_file $file if -r $file;
235 $file = "$dir/.ssh/authorized_keys2";
236 from_ssh_auth_file $file if -r $file;
237 $file = "$dir/.ssh/known_hosts";
238 from_ssh_auth_file $file if -r $file;
239 $file = "$dir/.ssh/known_hosts2";
240 from_ssh_auth_file $file if -r $file;
241 $file = "$dir/.ssh/id_rsa.pub";
242 from_ssh_key_file $file if -r $file;
243 $file = "$dir/.ssh/id_dsa.pub";
244 from_ssh_key_file $file if -r $file;
247 sub from_user_all () {
248 # This was one loop initially, but does not work with some Perl
249 # versions.
250 setpwent;
251 my @names;
252 while (my $name = getpwent) {
253 push @names, $name;
255 endpwent;
256 from_user $_ for @names;
259 if (@ARGV && $ARGV[0] eq '-c') {
260 shift @ARGV;
261 $db_file = shift @ARGV if @ARGV;
263 if (@ARGV) {
264 open_db;
265 my $cmd = shift @ARGV;
266 if ($cmd eq 'file') {
267 for my $name (@ARGV) {
268 next if from_openvpn_key $name;
269 from_ssh_auth_file $name;
271 } elsif ($cmd eq 'host') {
272 from_ssh_host @ARGV;
273 } elsif ($cmd eq 'user') {
274 if (@ARGV) {
275 from_user $_ for @ARGV;
276 } else {
277 from_user_all;
279 } elsif ($cmd eq 'help') {
280 help;
281 exit 0;
282 } else {
283 die "error: invalid command, use \"help\" to get help\n";
285 print_stats;
286 } else {
287 help;
288 exit 1;
291 my %hash;
293 __DATA__