3 # Debian/OpenSSL Weak Key Detector
5 # Copyright (C) 2008, Florian Weimer <fw@deneb.enyo.de>
7 # Permission to use, copy, modify, and distribute this software for
8 # any purpose with or without fee is hereby granted, provided that the
9 # above copyright notice and this permission notice appear in all
12 # THE SOFTWARE IS PROVIDED "AS IS" AND FLORIAN WEIMER AND HIS
13 # CONTRIBUTORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE
14 # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
15 # NO EVENT SHALL FLORIAN WEIMER OR HIS CONTRIBUTORS BE LIABLE FOR ANY
16 # SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
17 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
18 # AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
19 # OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
22 # Blacklist data has been provided by Kees Cook, Peter Palfrader and
25 # Patches and comments are welcome. Please send them to
26 # <fw@deneb.enyo.de>, and use "dowkd" in the subject line.
33 usage: $0 [OPTIONS...] COMMAND [ARGUMENTS...]
37 file: examine files on the command line for weak keys
38 host: examine the specified hosts for weak SSH keys
39 user: examine user SSH keys for weakness; examine all users if no
41 help: show this help screen
45 -c FILE: set the database cache file name (default: dowkd.db)
47 dowkd currently handles the following OpenSSH host and user keys,
48 provided they have been generated on a little-endian architecture
49 (such as i386 or amd64): RSA/1024 (both rsa1 and rsa format), RSA/2048
50 and DSA/1024. (The relevant OpenSSH versions in Debian do not support
51 DSA key generation with other sizes.)
53 OpenVPN shared also detected on little-endian architecture.
55 Unencrypted RSA private keys and PEM certificate files generated by
56 OpenSSL are detected, provided they use key lengths of 1024 or 2048
59 Note that the blacklist by dowkd may be incomplete; it is only
60 intended as a quick check.
72 my $db_file = 'dowkd.db';
78 warn "notice: creating database, please wait\n";
79 $db = tie
%db, 'DB_File', $db_file, O_RDWR
| O_CREAT
, 0777, $DB_BTREE
80 or die "error: could not open database: $!\n";
82 $db{''} = $db_version;
83 while (my $line = <DATA
>) {
84 next if $line =~ /^\**$/;
86 $line =~ /^[0-9a-f]{32}$/ or die "error: invalid data line";
87 $line =~ s/(..)/chr(hex($1))/ge;
96 $db = tie
%db, 'DB_File', $db_file, O_RDONLY
, 0777, $DB_BTREE
97 or die "error: could not open database: $!\n";
98 my $stored_version = $db{''};
99 $stored_version && $stored_version eq $db_version or create_db
;
106 sub safe_backtick
(@
) {
109 open $fh, '-|', @args
110 or die "error: failed to spawn $args[0]: $!\n";
116 @result = scalar(<$fh>);
119 $?
== 0 or return undef;
128 my $keys_vulnerable = 0;
131 print STDERR
"summary: keys found: $keys_found, weak keys: $keys_vulnerable\n";
134 sub check_hash
($$;$) {
135 my ($name, $hash, $descr) = @_;
137 if (exists $db{$hash}) {
139 $descr = $descr ?
" ($descr)" : '';
140 print "$name: weak key$descr\n";
144 sub ssh_fprint_file
($) {
146 my $data = safe_backtick qw
/ssh-keygen -l -f/, $name;
147 defined $data or return ();
148 my @data = $data =~ /^(\d+) ([0-9a-f]{2}(?::[0-9a-f]{2}){15})/;
149 return @data if @data == 2;
153 sub ssh_fprint_check
($$$$) {
154 my ($name, $type, $length, $hash) = @_;
155 $type =~ /^(?:rsa1?|dsa)\z/ or die;
156 if (($type eq 'rsa' && ($length == 1024 || $length == 2048))
157 || ($type eq 'dsa' && $length == 1024)
158 || ($type eq 'rsa1' && $length == 1024)) {
160 $hash =~ s/(..)/chr(hex($1))/ge;
161 check_hash
$name, $hash, "OpenSSH/$type/$length";
162 } elsif ($type eq 'dsa') {
163 print "$name: $length bits DSA key not recommended\n";
165 warn "$name: warning: no blacklist for $type/$length key\n";
171 seek $tmp, 0, 0 or die "seek: $!";
172 truncate $tmp, 0 or die "truncate: $!";
175 sub cleanup_ssh_auth_line
($) {
178 $line =~ /^(?:ssh-(?:rsa|dss)\s|\d+\s+\d+\s+\d)/ and return $line;
181 if ($line =~ /^\s+(.*)/) {
185 if ($line =~ /^"(.*)/) {
189 if ($line =~ /^\\.(.*)/) {
190 # It doesn't matter if we don't deal with \000 properly, we
191 # just need to defuse the backslash character.
195 if ($line =~ /^[a-zA-Z0-9_=+-]+(.*)/) {
196 # Skip multiple harmless characters in one go.
200 if ($line =~ /^.(.*)/) {
201 # Other characters are stripped one by one.
205 return undef; # empty string, no key found
208 if ($line =~ /^"(.*)/) {
212 if ($line =~ /^\\.(.*)/) {
213 # See above, defuse the backslash.
217 if ($line =~ /^[^\\"]+(.*)/) {
221 return undef; # missing closing double quote
224 $line =~ /^(?:ssh-(?:rsa|dss)\s|\d+\s+\d+\s+\d)/ and return $line;
228 sub derive_ssh_auth_type
($) {
230 $line =~ /^ssh-rsa\s/ and return 'rsa';
231 $line =~ /^ssh-dss\s/ and return 'dsa';
232 $line =~ /^\d+\s/ and return 'rsa1';
236 sub from_ssh_auth_line
($$$) {
237 my ($tmp, $name, $line) = @_;
239 return if $line =~ m/^\s*(#|$)/;
242 my $l = cleanup_ssh_auth_line
$line;
246 my $type = derive_ssh_auth_type
$line;
249 print $tmp "$line\n" or die "print: $!";
250 $tmp->flush or die "flush: $!";
251 my ($length, $hash) = ssh_fprint_file
"$tmp";
252 if ($length && $hash) {
253 ssh_fprint_check
"$name", $type, $length, $hash;
258 warn "$name: warning: unparsable line\n";
261 sub from_ssh_auth_file
($) {
264 unless (open $auth, '<', $name) {
265 warn "$name:0: error: open failed: $!\n";
269 my $tmp = new File
::Temp
;
270 while (my $line = <$auth>) {
271 from_ssh_auth_line
$tmp, "$name:$.", $line;
275 sub from_openvpn_key
($) {
278 unless (open $key, '<', $name) {
279 warn "$name:0: open failed: $!\n";
284 while (my $line = <$key>) {
286 if ($line =~ /^-----BEGIN OpenVPN Static key V1-----/) {
289 if ($line =~ /^([0-9a-f]{32})/) {
291 $line =~ s/(..)/chr(hex($1))/ge;
292 check_hash
"$name:$.", $line, "OpenVPN";
295 warn "$name:$.: warning: illegal OpenVPN file format\n";
302 sub openssl_modulus_check
($$) {
303 my ($name, $modulus) = @_;
305 if ($modulus =~ /^Modulus=([A-F0-9]+)$/) {
307 my $length = length($modulus) * 4;
308 if ($length == 1024 || $length == 2048) {
309 my $mod = substr $modulus, length($modulus) - 32;
311 my @mod = $mod =~ /(..)/g;
312 $mod = join('', map { chr(hex($_)) } reverse @mod);
313 check_hash
$name, $mod, "OpenSSL/RSA/$length";
315 warn "$name: warning: no blacklist for OpenSSL/RSA/$length key\n";
318 die "internal error: $modulus\n";
328 unless (open $src, '<', $name) {
329 warn "$name:0: open failed: $!\n";
333 while (my $line = <$src>) {
334 if ($line =~ /^-----BEGIN CERTIFICATE-----/) {
336 $tmp or $tmp = new File
::Temp
;
339 print $tmp $line or die "print: $!";
340 goto LAST
if $line =~ /^-----END CERTIFICATE-----/;
341 } while ($line = <$src>);
343 $tmp->flush or die "flush: $!";
344 my $mod = safe_backtick qw
/openssl x509 -noout -modulus -in/, $tmp;
346 openssl_modulus_check
"$name:$lineno", $mod;
349 warn "$name:$lineno: failed to parse certificate\n";
352 } elsif ($line =~ /^-----BEGIN RSA PRIVATE KEY-----/) {
354 $tmp or $tmp = new File
::Temp
;
357 print $tmp $line or die "print: $!";
358 goto LAST_RSA
if $line =~ /^-----END RSA PRIVATE KEY-----/;
359 } while ($line = <$src>);
361 $tmp->flush or die "flush: $!";
362 my $mod = safe_backtick qw
/openssl rsa -noout -modulus -in/, $tmp;
364 openssl_modulus_check
"$name:$lineno", $mod;
367 warn "$name:$lineno: failed to parse RSA private key\n";
376 sub from_ssh_host
(@
) {
380 my ($name,$aliases,$addrtype,$length,@addrs) = gethostbyname $_;
381 @addrs or warn "warning: host not found: $_\n";
386 push @lines, safe_backtick qw
/ssh-keyscan -t rsa/, @names;
387 push @lines, safe_backtick qw
/ssh-keyscan -t dsa/, @names;
389 my $tmp = new File
::Temp
;
390 for my $line (@lines) {
391 next if $line =~ /^#/;
392 my ($host, $data) = $line =~ /^(\S+) (.*)$/;
393 from_ssh_auth_line
$tmp, $host, $data;
399 my ($name,$passwd,$uid,$gid,
400 $quota,$comment,$gcos,$dir,$shell,$expire) = getpwnam($user);
402 warn "warning: user $user does not exist\n";
405 for my $name (qw
/authorized_keys authorized_keys2
406 known_hosts known_hosts2
407 id_rsa
.pub id_dsa
.pub identity
.pub
/) {
408 my $file = "$dir/.ssh/$name";
409 from_ssh_auth_file
$file if -r
$file;
413 sub from_user_all
() {
414 # This was one loop initially, but does not work with some Perl
418 while (my $name = getpwent) {
422 from_user
$_ for @names;
425 if (@ARGV && $ARGV[0] eq '-c') {
427 $db_file = shift @ARGV if @ARGV;
431 my $cmd = shift @ARGV;
432 if ($cmd eq 'file') {
433 for my $name (@ARGV) {
434 next if from_openvpn_key
$name;
435 next if from_pem
$name;
436 from_ssh_auth_file
$name;
438 } elsif ($cmd eq 'host') {
440 } elsif ($cmd eq 'user') {
442 from_user
$_ for @ARGV;
446 } elsif ($cmd eq 'help') {
450 die "error: invalid command, use \"help\" to get help\n";