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 (change destination port with "host -p PORT HOST...")
40 user: examine user SSH keys for weakness; examine all users if no
42 quick: check this host for weak keys (encompasses "user" plus
43 heuristics to find keys in /etc)
44 help: show this help screen
45 version: show version information
49 -c FILE: set the database cache file name (default: dowkd.db)
51 dowkd currently handles the following OpenSSH host and user keys,
52 provided they have been generated on a little-endian architecture
53 (such as i386 or amd64):
55 RSA/1024, RSA/2048, RSA1/1024, RSA1/2048
59 (The relevant OpenSSH versions in Debian do not support DSA key
60 generation with other sizes.)
62 OpenVPN shared also detected if they have been created on
63 little-endian architectures.
65 Unencrypted RSA private keys and PEM certificate files generated by
66 OpenSSL are detected, provided they use key lengths of 1024 or 2048
67 bits (again, only for little-endian architectures).
69 Note that the blacklist by dowkd may be incomplete; it is only
70 intended as a quick check.
80 my $db_version = '@DB_VERSION@';
81 my $program_version = '@PROGRAM_VERSION@';
82 my $changelog = <<'EOF';
87 my $db_file = 'dowkd.db';
93 warn "notice: creating database, please wait\n";
94 $db = tie
%db, 'DB_File', $db_file, O_RDWR
| O_CREAT
, 0777, $DB_BTREE
95 or die "error: could not open database: $!\n";
98 while (my $line = <DATA
>) {
99 next if $line =~ /^\**$/;
101 $line =~ /^[0-9a-f]{32}$/ or die "error: invalid data line";
102 $line =~ s/(..)/chr(hex($1))/ge;
106 $found or die "error: no blacklist data found in script\n";
108 # Set at the end so that no incomplete database is left behind.
109 $db{''} = $db_version;
116 $db = tie
%db, 'DB_File', $db_file, O_RDONLY
, 0777, $DB_BTREE
117 or die "error: could not open database: $!\n";
118 my $stored_version = $db{''};
119 $stored_version && $stored_version eq $db_version or create_db
;
126 sub safe_backtick
(@
) {
129 open $fh, '-|', @args
130 or die "error: failed to spawn $args[0]: $!\n";
136 @result = scalar(<$fh>);
139 $?
== 0 or return undef;
148 my $keys_vulnerable = 0;
151 print STDERR
"summary: keys found: $keys_found, weak keys: $keys_vulnerable\n";
154 sub check_hash
($$;$) {
155 my ($name, $hash, $descr) = @_;
157 if (exists $db{$hash}) {
159 $descr = $descr ?
" ($descr)" : '';
160 print "$name: weak key$descr\n";
164 sub ssh_fprint_file
($) {
166 my $data = safe_backtick qw
/ssh-keygen -l -f/, $name;
167 defined $data or return ();
168 my @data = $data =~ /^(\d+) ([0-9a-f]{2}(?::[0-9a-f]{2}){15})/;
169 return @data if @data == 2;
173 sub ssh_fprint_check
($$$$) {
174 my ($name, $type, $length, $hash) = @_;
175 $type =~ /^(?:rsa1?|dsa)\z/ or die;
177 && ($length == 1024 || $length == 2048 || $length == 4096))
178 || ($type eq 'dsa' && $length == 1024)
179 || ($type eq 'rsa1' && ($length == 1024 || $length == 2048))) {
181 $hash =~ s/(..)/chr(hex($1))/ge;
182 check_hash
$name, $hash, "OpenSSH/$type/$length";
183 } elsif ($type eq 'dsa') {
184 print "$name: $length bits DSA key not recommended\n";
186 warn "$name: warning: no blacklist for $type/$length key\n";
192 seek $tmp, 0, 0 or die "seek: $!";
193 truncate $tmp, 0 or die "truncate: $!";
196 sub cleanup_ssh_auth_line
($) {
199 $line =~ /^(?:ssh-(?:rsa|dss)\s|\d+\s+\d+\s+\d)/ and return $line;
202 if ($line =~ /^\s+(.*)/) {
206 if ($line =~ /^"(.*)/) {
210 if ($line =~ /^\\.(.*)/) {
211 # It doesn't matter if we don't deal with \000 properly, we
212 # just need to defuse the backslash character.
216 if ($line =~ /^[a-zA-Z0-9_=+-]+(.*)/) {
217 # Skip multiple harmless characters in one go.
221 if ($line =~ /^.(.*)/) {
222 # Other characters are stripped one by one.
226 return undef; # empty string, no key found
229 if ($line =~ /^"(.*)/) {
233 if ($line =~ /^\\.(.*)/) {
234 # See above, defuse the backslash.
238 if ($line =~ /^[^\\"]+(.*)/) {
242 return undef; # missing closing double quote
245 $line =~ /^(?:ssh-(?:rsa|dss)\s|\d+\s+\d+\s+\d)/ and return $line;
249 sub derive_ssh_auth_type
($) {
251 $line =~ /^ssh-rsa\s/ and return 'rsa';
252 $line =~ /^ssh-dss\s/ and return 'dsa';
253 $line =~ /^\d+\s/ and return 'rsa1';
257 sub from_ssh_auth_line
($$$) {
258 my ($tmp, $name, $line) = @_;
262 my $l = cleanup_ssh_auth_line
$line;
266 my $type = derive_ssh_auth_type
$line;
269 print $tmp "$line\n" or die "print: $!";
270 $tmp->flush or die "flush: $!";
271 my ($length, $hash) = ssh_fprint_file
"$tmp";
272 if ($length && $hash) {
273 ssh_fprint_check
"$name", $type, $length, $hash;
280 sub from_ssh_auth_file
($) {
283 unless (open $auth, '<', $name) {
284 warn "$name:0: error: open failed: $!\n";
288 my $tmp = new File
::Temp
;
290 while (my $line = <$auth>) {
291 next if $line =~ m/^\s*(#|$)/;
292 my $status = from_ssh_auth_line
$tmp, "$name:$.", $line;
294 $last_status and warn "$name:$.: warning: unparsable line\n";
296 $last_status = $status;
300 sub from_openvpn_key
($) {
303 unless (open $key, '<', $name) {
304 warn "$name:0: open failed: $!\n";
309 while (my $line = <$key>) {
311 if ($line =~ /^-----BEGIN OpenVPN Static key V1-----/) {
314 if ($line =~ /^([0-9a-f]{32})/) {
316 $line =~ s/(..)/chr(hex($1))/ge;
317 check_hash
"$name:$.", $line, "OpenVPN";
320 warn "$name:$.: warning: illegal OpenVPN file format\n";
327 sub openssl_modulus_check
($$) {
328 my ($name, $modulus) = @_;
330 if ($modulus =~ /^Modulus=([A-F0-9]+)$/) {
332 my $length = length($modulus) * 4;
333 if ($length == 1024 || $length == 2048) {
334 my $mod = substr $modulus, length($modulus) - 32;
336 my @mod = $mod =~ /(..)/g;
337 $mod = join('', map { chr(hex($_)) } reverse @mod);
338 check_hash
$name, $mod, "OpenSSL/RSA/$length";
340 warn "$name: warning: no blacklist for OpenSSL/RSA/$length key\n";
343 die "internal error: $modulus\n";
353 unless (open $src, '<', $name) {
354 warn "$name:0: open failed: $!\n";
358 while (my $line = <$src>) {
359 if ($line =~ /^-----BEGIN CERTIFICATE-----/) {
361 $tmp or $tmp = new File
::Temp
;
364 print $tmp $line or die "print: $!";
365 goto LAST
if $line =~ /^-----END CERTIFICATE-----/;
366 } while ($line = <$src>);
368 $tmp->flush or die "flush: $!";
369 my $mod = safe_backtick qw
/openssl x509 -noout -modulus -in/, $tmp;
371 openssl_modulus_check
"$name:$lineno", $mod;
374 warn "$name:$lineno: failed to parse certificate\n";
377 } elsif ($line =~ /^-----BEGIN RSA PRIVATE KEY-----/) {
379 $tmp or $tmp = new File
::Temp
;
382 print $tmp $line or die "print: $!";
383 goto LAST_RSA
if $line =~ /^-----END RSA PRIVATE KEY-----/;
384 } while ($line = <$src>);
386 $tmp->flush or die "flush: $!";
387 my $mod = safe_backtick qw
/openssl rsa -noout -modulus -in/, $tmp;
389 openssl_modulus_check
"$name:$lineno", $mod;
392 warn "$name:$lineno: failed to parse RSA private key\n";
401 sub from_ssh_host
($@
) {
402 my ($port, @names) = @_;
405 my ($name,$aliases,$addrtype,$length,@addrs) = gethostbyname $_;
406 @addrs or warn "warning: host not found: $_\n";
410 my @lines= safe_backtick qw
/ssh-keyscan -t/, 'rsa1,rsa,dsa', '-p',
413 my $tmp = new File
::Temp
;
414 for my $line (@lines) {
415 next if $line =~ /^#/;
416 my ($host, $data) = $line =~ /^(\S+) (.*)$/;
417 from_ssh_auth_line
$tmp, $host, $data
418 or die "$host: warning: unparsable line\n";
424 my ($name,$passwd,$uid,$gid,
425 $quota,$comment,$gcos,$dir,$shell,$expire) = getpwnam($user);
427 warn "warning: user $user does not exist\n";
430 for my $name (qw
/authorized_keys authorized_keys2
431 known_hosts known_hosts2
432 id_rsa
.pub id_dsa
.pub identity
.pub
/) {
433 my $file = "$dir/.ssh/$name";
434 from_ssh_auth_file
$file if -r
$file;
438 sub from_user_all
() {
439 # This was one loop initially, but does not work with some Perl
443 while (my $name = getpwent) {
447 from_user
$_ for @names;
450 sub from_any_file
($) {
452 from_openvpn_key
$name and return;
453 from_pem
$name and return;
454 from_ssh_auth_file
$name;
459 open $find, '-|', qw
!find
/etc
-type f
(
460 -name
*.key
-o
-name
*.pem
-o
-name
*.crt
461 ) -print0
! or die "error: could not spawn find: $!";
468 $?
== 0 or die "error: find failed with exit status $?\n";
469 for my $file (@files) {
470 -e
$file and from_any_file
$file;
474 if (@ARGV && $ARGV[0] eq '-c') {
476 $db_file = shift @ARGV if @ARGV;
480 my $cmd = shift @ARGV;
481 if ($cmd eq 'file') {
482 for my $name (@ARGV) {
485 } elsif ($cmd eq 'host') {
491 if ($ARGV[0] eq '-p') {
496 } elsif ($ARGV[0] =~ /-p(\d+)/) {
504 from_ssh_host
$port, @ARGV;
505 } elsif ($cmd eq 'user') {
507 from_user
$_ for @ARGV;
511 } elsif ($cmd eq 'quick') {
513 for my $file (qw
/ssh_host_rsa_key
.pub ssh_host_dsa_key
.pub
514 ssh_host_key ssh_known_hosts ssh_known_hosts2
/) {
515 -e
$file and from_ssh_auth_file
$file;
518 } elsif ($cmd eq 'help') {
521 } elsif ($cmd eq 'version') {
522 print "dowkd $program_version (database $db_version)\n\n$changelog";
525 die "error: invalid command, use \"help\" to get help\n";