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 (both rsa1 and rsa format)
58 (The relevant OpenSSH versions in Debian do not support DSA key
59 generation with other sizes.)
61 OpenVPN shared also detected if they have been created on
62 little-endian architectures.
64 Unencrypted RSA private keys and PEM certificate files generated by
65 OpenSSL are detected, provided they use key lengths of 1024 or 2048
66 bits (again, only for little-endian architectures).
68 Note that the blacklist by dowkd may be incomplete; it is only
69 intended as a quick check.
79 my $db_version = '@DB_VERSION@';
80 my $program_version = '@PROGRAM_VERSION@';
81 my $changelog = <<'EOF';
86 my $db_file = 'dowkd.db';
92 warn "notice: creating database, please wait\n";
93 $db = tie
%db, 'DB_File', $db_file, O_RDWR
| O_CREAT
, 0777, $DB_BTREE
94 or die "error: could not open database: $!\n";
97 while (my $line = <DATA
>) {
98 next if $line =~ /^\**$/;
100 $line =~ /^[0-9a-f]{32}$/ or die "error: invalid data line";
101 $line =~ s/(..)/chr(hex($1))/ge;
105 $found or die "error: no blacklist data found in script\n";
107 # Set at the end so that no incomplete database is left behind.
108 $db{''} = $db_version;
115 $db = tie
%db, 'DB_File', $db_file, O_RDONLY
, 0777, $DB_BTREE
116 or die "error: could not open database: $!\n";
117 my $stored_version = $db{''};
118 $stored_version && $stored_version eq $db_version or create_db
;
125 sub safe_backtick
(@
) {
128 open $fh, '-|', @args
129 or die "error: failed to spawn $args[0]: $!\n";
135 @result = scalar(<$fh>);
138 $?
== 0 or return undef;
147 my $keys_vulnerable = 0;
150 print STDERR
"summary: keys found: $keys_found, weak keys: $keys_vulnerable\n";
153 sub check_hash
($$;$) {
154 my ($name, $hash, $descr) = @_;
156 if (exists $db{$hash}) {
158 $descr = $descr ?
" ($descr)" : '';
159 print "$name: weak key$descr\n";
163 sub ssh_fprint_file
($) {
165 my $data = safe_backtick qw
/ssh-keygen -l -f/, $name;
166 defined $data or return ();
167 my @data = $data =~ /^(\d+) ([0-9a-f]{2}(?::[0-9a-f]{2}){15})/;
168 return @data if @data == 2;
172 sub ssh_fprint_check
($$$$) {
173 my ($name, $type, $length, $hash) = @_;
174 $type =~ /^(?:rsa1?|dsa)\z/ or die;
176 && ($length == 1024 || $length == 2048 || $length == 4096))
177 || ($type eq 'dsa' && $length == 1024)
178 || ($type eq 'rsa1' && $length == 1024)) {
180 $hash =~ s/(..)/chr(hex($1))/ge;
181 check_hash
$name, $hash, "OpenSSH/$type/$length";
182 } elsif ($type eq 'dsa') {
183 print "$name: $length bits DSA key not recommended\n";
185 warn "$name: warning: no blacklist for $type/$length key\n";
191 seek $tmp, 0, 0 or die "seek: $!";
192 truncate $tmp, 0 or die "truncate: $!";
195 sub cleanup_ssh_auth_line
($) {
198 $line =~ /^(?:ssh-(?:rsa|dss)\s|\d+\s+\d+\s+\d)/ and return $line;
201 if ($line =~ /^\s+(.*)/) {
205 if ($line =~ /^"(.*)/) {
209 if ($line =~ /^\\.(.*)/) {
210 # It doesn't matter if we don't deal with \000 properly, we
211 # just need to defuse the backslash character.
215 if ($line =~ /^[a-zA-Z0-9_=+-]+(.*)/) {
216 # Skip multiple harmless characters in one go.
220 if ($line =~ /^.(.*)/) {
221 # Other characters are stripped one by one.
225 return undef; # empty string, no key found
228 if ($line =~ /^"(.*)/) {
232 if ($line =~ /^\\.(.*)/) {
233 # See above, defuse the backslash.
237 if ($line =~ /^[^\\"]+(.*)/) {
241 return undef; # missing closing double quote
244 $line =~ /^(?:ssh-(?:rsa|dss)\s|\d+\s+\d+\s+\d)/ and return $line;
248 sub derive_ssh_auth_type
($) {
250 $line =~ /^ssh-rsa\s/ and return 'rsa';
251 $line =~ /^ssh-dss\s/ and return 'dsa';
252 $line =~ /^\d+\s/ and return 'rsa1';
256 sub from_ssh_auth_line
($$$) {
257 my ($tmp, $name, $line) = @_;
261 my $l = cleanup_ssh_auth_line
$line;
265 my $type = derive_ssh_auth_type
$line;
268 print $tmp "$line\n" or die "print: $!";
269 $tmp->flush or die "flush: $!";
270 my ($length, $hash) = ssh_fprint_file
"$tmp";
271 if ($length && $hash) {
272 ssh_fprint_check
"$name", $type, $length, $hash;
279 sub from_ssh_auth_file
($) {
282 unless (open $auth, '<', $name) {
283 warn "$name:0: error: open failed: $!\n";
287 my $tmp = new File
::Temp
;
289 while (my $line = <$auth>) {
290 next if $line =~ m/^\s*(#|$)/;
291 my $status = from_ssh_auth_line
$tmp, "$name:$.", $line;
293 $last_status and warn "$name:$.: warning: unparsable line\n";
295 $last_status = $status;
299 sub from_openvpn_key
($) {
302 unless (open $key, '<', $name) {
303 warn "$name:0: open failed: $!\n";
308 while (my $line = <$key>) {
310 if ($line =~ /^-----BEGIN OpenVPN Static key V1-----/) {
313 if ($line =~ /^([0-9a-f]{32})/) {
315 $line =~ s/(..)/chr(hex($1))/ge;
316 check_hash
"$name:$.", $line, "OpenVPN";
319 warn "$name:$.: warning: illegal OpenVPN file format\n";
326 sub openssl_modulus_check
($$) {
327 my ($name, $modulus) = @_;
329 if ($modulus =~ /^Modulus=([A-F0-9]+)$/) {
331 my $length = length($modulus) * 4;
332 if ($length == 1024 || $length == 2048) {
333 my $mod = substr $modulus, length($modulus) - 32;
335 my @mod = $mod =~ /(..)/g;
336 $mod = join('', map { chr(hex($_)) } reverse @mod);
337 check_hash
$name, $mod, "OpenSSL/RSA/$length";
339 warn "$name: warning: no blacklist for OpenSSL/RSA/$length key\n";
342 die "internal error: $modulus\n";
352 unless (open $src, '<', $name) {
353 warn "$name:0: open failed: $!\n";
357 while (my $line = <$src>) {
358 if ($line =~ /^-----BEGIN CERTIFICATE-----/) {
360 $tmp or $tmp = new File
::Temp
;
363 print $tmp $line or die "print: $!";
364 goto LAST
if $line =~ /^-----END CERTIFICATE-----/;
365 } while ($line = <$src>);
367 $tmp->flush or die "flush: $!";
368 my $mod = safe_backtick qw
/openssl x509 -noout -modulus -in/, $tmp;
370 openssl_modulus_check
"$name:$lineno", $mod;
373 warn "$name:$lineno: failed to parse certificate\n";
376 } elsif ($line =~ /^-----BEGIN RSA PRIVATE KEY-----/) {
378 $tmp or $tmp = new File
::Temp
;
381 print $tmp $line or die "print: $!";
382 goto LAST_RSA
if $line =~ /^-----END RSA PRIVATE KEY-----/;
383 } while ($line = <$src>);
385 $tmp->flush or die "flush: $!";
386 my $mod = safe_backtick qw
/openssl rsa -noout -modulus -in/, $tmp;
388 openssl_modulus_check
"$name:$lineno", $mod;
391 warn "$name:$lineno: failed to parse RSA private key\n";
400 sub from_ssh_host
($@
) {
401 my ($port, @names) = @_;
404 my ($name,$aliases,$addrtype,$length,@addrs) = gethostbyname $_;
405 @addrs or warn "warning: host not found: $_\n";
409 my @lines= safe_backtick qw
/ssh-keyscan -t/, 'rsa1,rsa,dsa', '-p',
412 my $tmp = new File
::Temp
;
413 for my $line (@lines) {
414 next if $line =~ /^#/;
415 my ($host, $data) = $line =~ /^(\S+) (.*)$/;
416 from_ssh_auth_line
$tmp, $host, $data
417 or die "$host: warning: unparsable line\n";
423 my ($name,$passwd,$uid,$gid,
424 $quota,$comment,$gcos,$dir,$shell,$expire) = getpwnam($user);
426 warn "warning: user $user does not exist\n";
429 for my $name (qw
/authorized_keys authorized_keys2
430 known_hosts known_hosts2
431 id_rsa
.pub id_dsa
.pub identity
.pub
/) {
432 my $file = "$dir/.ssh/$name";
433 from_ssh_auth_file
$file if -r
$file;
437 sub from_user_all
() {
438 # This was one loop initially, but does not work with some Perl
442 while (my $name = getpwent) {
446 from_user
$_ for @names;
449 sub from_any_file
($) {
451 from_openvpn_key
$name and return;
452 from_pem
$name and return;
453 from_ssh_auth_file
$name;
458 open $find, '-|', qw
!find
/etc
-type f
(
459 -name
*.key
-o
-name
*.pem
-o
-name
*.crt
460 ) -print0
! or die "error: could not spawn find: $!";
467 $?
== 0 or die "error: find failed with exit status $?\n";
468 for my $file (@files) {
469 -e
$file and from_any_file
$file;
473 if (@ARGV && $ARGV[0] eq '-c') {
475 $db_file = shift @ARGV if @ARGV;
479 my $cmd = shift @ARGV;
480 if ($cmd eq 'file') {
481 for my $name (@ARGV) {
484 } elsif ($cmd eq 'host') {
490 if ($ARGV[0] eq '-p') {
495 } elsif ($ARGV[0] =~ /-p(\d+)/) {
503 from_ssh_host
$port, @ARGV;
504 } elsif ($cmd eq 'user') {
506 from_user
$_ for @ARGV;
510 } elsif ($cmd eq 'quick') {
512 for my $file (qw
/ssh_host_rsa_key
.pub ssh_host_dsa_key
.pub
513 ssh_host_key known_hosts known_hosts2
/) {
514 -e
$file and from_ssh_auth_file
$file;
517 } elsif ($cmd eq 'help') {
520 } elsif ($cmd eq 'version') {
521 print "dowkd $program_version (database $db_version)\n\n$changelog";
524 die "error: invalid command, use \"help\" to get help\n";