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.
28 # This version is based on commit @PROGRAM_SHA1@
29 # in the GIT repository at <http://repo.or.cz/w/dowkd.git>.
36 usage: $0 [OPTIONS...] COMMAND [ARGUMENTS...]
40 file: examine files on the command line for weak keys
41 host: examine the specified hosts for weak SSH keys
42 (change destination port with "host -p PORT HOST...")
43 ssl: examine the specified hosts for weak X.509 keys
44 (change destination port with "ssl -p PORT HOST...")
45 user: examine user SSH keys for weakness; examine all users if no
47 quick: check this host for weak keys (encompasses "user" plus
48 heuristics to find keys in /etc)
49 help: show this help screen
50 version: show version information
54 -c FILE: set the database cache file name (default: dowkd.db)
56 dowkd currently handles the following OpenSSH host and user keys,
57 provided they have been generated on a little-endian architecture
58 (such as i386 or amd64):
60 RSA/1024, RSA/2048, RSA1/1024, RSA1/2048
64 (The relevant OpenSSH versions in Debian do not support DSA key
65 generation with other sizes.)
67 OpenVPN shared also detected if they have been created on
68 little-endian architectures.
70 Unencrypted RSA private keys and PEM certificate files generated by
71 OpenSSL are detected, provided they use key lengths of 1024, 2048 or
72 4096 bits (again, only for little-endian architectures).
74 Note that the blacklist by dowkd may be incomplete; it is only
75 intended as a quick check.
85 my $db_version = '@DB_VERSION@';
86 my $program_version = '@PROGRAM_VERSION@';
88 my $db_file = 'dowkd.db';
94 warn "notice: creating database, please wait\n";
95 $db = tie
%db, 'DB_File', $db_file, O_RDWR
| O_CREAT
, 0777, $DB_BTREE
96 or die "error: could not open database: $!\n";
99 while (my $line = <DATA
>) {
100 next if $line =~ /^\**$/;
102 $line =~ /^[0-9a-f]{32}$/ or die "error: invalid data line";
103 $line =~ s/(..)/chr(hex($1))/ge;
107 $found or die "error: no blacklist data found in script\n";
109 # Set at the end so that no incomplete database is left behind.
110 $db{''} = $db_version;
117 $db = tie
%db, 'DB_File', $db_file, O_RDONLY
, 0777, $DB_BTREE
118 or die "error: could not open database: $!\n";
119 my $stored_version = $db{''};
120 $stored_version && $stored_version eq $db_version or create_db
;
127 sub safe_backtick
(@
) {
130 open $fh, '-|', @args
131 or die "error: failed to spawn $args[0]: $!\n";
137 @result = scalar(<$fh>);
140 $?
== 0 or return undef;
148 sub safe_backtick_stderr
{
151 my $pid = open $fh, '-|';
155 $?
== 0 or return undef;
159 return join('', @result);
162 open STDIN
, '/dev/null' or die "error: could not redirect stdin: $!";
163 open STDERR
, '>&STDOUT' or die "error: could not redirect stderr: $!";
164 exec @args or die "exec: failed: $!";
169 my $keys_vulnerable = 0;
172 print STDERR
"summary: keys found: $keys_found, weak keys: $keys_vulnerable\n";
175 sub check_hash
($$;$) {
176 my ($name, $hash, $descr) = @_;
177 $hash && length($hash) == 16 or die "wrong hash size " . length($hash);
179 if (exists $db{$hash}) {
181 $descr = $descr ?
" ($descr)" : '';
182 print "$name: weak key$descr\n";
188 sub ssh_fprint_file
($) {
190 my $data = safe_backtick qw
/ssh-keygen -l -f/, $name;
191 defined $data or return ();
192 my @data = $data =~ /^(\d+) ([0-9a-f]{2}(?::[0-9a-f]{2}){15})/;
193 return @data if @data == 2;
197 sub ssh_fprint_check
($$$$) {
198 my ($name, $type, $length, $hash) = @_;
199 $type =~ /^(?:rsa1?|dsa)\z/ or die;
201 && ($length == 1024 || $length == 2048 || $length == 4096))
202 || ($type eq 'dsa' && $length == 1024)
203 || ($type eq 'rsa1' && ($length == 1024 || $length == 2048))) {
205 $hash =~ s/(..)/chr(hex($1))/ge;
206 check_hash
$name, $hash, "OpenSSH/$type/$length";
207 } elsif ($type eq 'dsa') {
208 print "$name: $length bits DSA key not recommended\n";
210 warn "$name: warning: no blacklist for $type/$length key\n";
216 seek $tmp, 0, 0 or die "seek: $!";
217 truncate $tmp, 0 or die "truncate: $!";
220 sub cleanup_ssh_auth_line
($) {
223 $line =~ /^(?:ssh-(?:rsa|dss)\s|\d+\s+\d+\s+\d)/ and return $line;
226 if ($line =~ /^\s+(.*)/) {
230 if ($line =~ /^"(.*)/) {
234 if ($line =~ /^\\.(.*)/) {
235 # It doesn't matter if we don't deal with \000 properly, we
236 # just need to defuse the backslash character.
240 if ($line =~ /^[a-zA-Z0-9_=+-]+(.*)/) {
241 # Skip multiple harmless characters in one go.
245 if ($line =~ /^.(.*)/) {
246 # Other characters are stripped one by one.
250 return undef; # empty string, no key found
253 if ($line =~ /^"(.*)/) {
257 if ($line =~ /^\\.(.*)/) {
258 # See above, defuse the backslash.
262 if ($line =~ /^[^\\"]+(.*)/) {
266 return undef; # missing closing double quote
269 $line =~ /^(?:ssh-(?:rsa|dss)\s|\d+\s+\d+\s+\d)/ and return $line;
273 sub derive_ssh_auth_type
($) {
275 $line =~ /^ssh-rsa\s/ and return 'rsa';
276 $line =~ /^ssh-dss\s/ and return 'dsa';
277 $line =~ /^\d+\s/ and return 'rsa1';
281 sub from_ssh_auth_line
($$$) {
282 my ($tmp, $name, $line) = @_;
286 my $l = cleanup_ssh_auth_line
$line;
290 my $type = derive_ssh_auth_type
$line;
293 print $tmp "$line\n" or die "print: $!";
294 $tmp->flush or die "flush: $!";
295 my ($length, $hash) = ssh_fprint_file
"$tmp";
296 if ($length && $hash) {
297 ssh_fprint_check
"$name", $type, $length, $hash;
304 sub from_ssh_auth_file
($) {
307 unless (open $auth, '<', $name) {
308 warn "$name:0: error: open failed: $!\n";
312 my $tmp = new File
::Temp
;
314 while (my $line = <$auth>) {
315 next if $line =~ m/^\s*(#|$)/;
316 my $status = from_ssh_auth_line
$tmp, "$name:$.", $line;
318 $last_status and warn "$name:$.: warning: unparsable line\n";
320 $last_status = $status;
324 sub from_openvpn_key
($) {
327 unless (open $key, '<', $name) {
328 warn "$name:0: open failed: $!\n";
333 while (my $line = <$key>) {
335 if ($line =~ /^-----BEGIN OpenVPN Static key V1-----/) {
338 if ($line =~ /^([0-9a-f]{32})/) {
340 $line =~ s/(..)/chr(hex($1))/ge;
341 check_hash
"$name:$.", $line, "OpenVPN";
344 warn "$name:$.: warning: illegal OpenVPN file format\n";
351 sub openssl_output_check
($$) {
352 my ($name, $output) = @_;
354 $output =~ /^(?:\s+RSA Public |Private-)Key: \((\d+) bit\)/m;
355 $length or die "internal error: could not parse OpenSSL output\n";
357 $output =~ /(?
:modulus
|\s
+Modulus\ \
(\d
+\ bit\
)):$ \s
+
358 ( (?
:^\s
+ (?
:[0-9a
-f
]{2}:)+$ \s
+)+
359 ^\s
+ (?
:[0-9a
-f
]{2}:)*(?
:[0-9a
-f
]{2})$ )/xm
;
360 $modulus or die "internal error: could not parse modulus\n";
361 $modulus =~ y/0-9a-f//cd;
362 my ($exponent) = $output =~ /^(?:\s+|public)Exponent: (\d+) \(0x/m;
363 $exponent or die "internal error: could not parse exponent\n";
365 if ($length == 1024 || $length == 2048 || $length == 4096) {
366 my $mod = substr $modulus, length($modulus) - 32;
368 my @mod = $mod =~ /(..)/g;
369 $mod = join('', map { chr(hex($_)) } reverse @mod);
370 length($mod) == 16 or die;
371 return if check_hash
$name, $mod, "OpenSSL/RSA/$length";
372 warn "$name: warning: no blacklist for OpenSSL/RSA/$length key (e=$exponent)\n"
373 if $exponent != 65537;
375 warn "$name: warning: no blacklist for OpenSSL/RSA/$length key\n";
385 unless (open $src, '<', $name) {
386 warn "$name:0: open failed: $!\n";
390 while (my $line = <$src>) {
391 if ($line =~ /^-----BEGIN CERTIFICATE-----/) {
393 $tmp or $tmp = new File
::Temp
;
396 print $tmp $line or die "print: $!";
397 goto LAST
if $line =~ /^-----END CERTIFICATE-----/;
398 } while ($line = <$src>);
400 $tmp->flush or die "flush: $!";
401 my $out = safe_backtick qw
/openssl x509 -noout -text -in/, $tmp;
403 openssl_output_check
"$name:$lineno", $out;
406 warn "$name:$lineno: failed to parse certificate\n";
409 } elsif ($line =~ /^-----BEGIN RSA PRIVATE KEY-----/) {
411 $tmp or $tmp = new File
::Temp
;
414 print $tmp $line or die "print: $!";
415 goto LAST_RSA
if $line =~ /^-----END RSA PRIVATE KEY-----/;
416 } while ($line = <$src>);
418 $tmp->flush or die "flush: $!";
419 my $out = safe_backtick qw
/openssl rsa -noout -text -in/, $tmp;
421 openssl_output_check
"$name:$lineno", $out;
424 warn "$name:$lineno: failed to parse RSA private key\n";
433 sub from_ssh_host
($@
) {
434 my ($port, @names) = @_;
437 my ($name,$aliases,$addrtype,$length,@addrs) = gethostbyname $_;
438 @addrs or warn "warning: host not found: $_\n";
442 my @lines= safe_backtick_stderr qw
/ssh-keyscan -t/, 'rsa1,rsa,dsa',
445 my $tmp = new File
::Temp
;
446 for my $line (@lines) {
447 next if $line =~ /^(?:#|no hostkey alg)/;
448 my ($host, $data) = $line =~ /^(\S+) (.*)$/;
449 $host && from_ssh_auth_line
$tmp, $host, $data
450 or die "$host: warning: unparsable line: $line";
454 sub from_ssl_host
($;$) {
455 my ($host, $port) = @_;
456 $port = $port || 443;
457 my @output = safe_backtick_stderr qw
/openssl s_client -connect/, "$host:$port";
458 if (@output && $output[0]) {
460 my $line = shift @output;
461 if ($line =~ /^-----BEGIN CERTIFICATE-----/) {
462 my $tmp = new File
::Temp
;
464 print $tmp $line or die "print: $!";
465 goto LAST
if $line =~ /^-----END CERTIFICATE-----/;
466 } while ($line = shift @output);
468 $tmp->flush or die "flush: $!";
469 my $out = safe_backtick qw
/openssl x509 -noout -text -in/, $tmp;
471 openssl_output_check
$host, $out;
474 # fall through to the warning message
478 warn "$host: could not obtain SSL server key\n";
483 my ($name,$passwd,$uid,$gid,
484 $quota,$comment,$gcos,$dir,$shell,$expire) = getpwnam($user);
486 warn "warning: user $user does not exist\n";
489 for my $name (qw
/authorized_keys authorized_keys2
490 known_hosts known_hosts2
491 id_rsa
.pub id_dsa
.pub identity
.pub
/) {
492 my $file = "$dir/.ssh/$name";
493 from_ssh_auth_file
$file if -r
$file;
497 sub from_user_all
() {
498 # This was one loop initially, but does not work with some Perl
502 while (my $name = getpwent) {
506 from_user
$_ for @names;
509 sub from_any_file
($) {
511 from_openvpn_key
$name and return;
512 from_pem
$name and return;
513 from_ssh_auth_file
$name;
518 open $find, '-|', qw
!find
/etc
-type f
(
519 -name
*.key
-o
-name
*.pem
-o
-name
*.crt
520 ) -print0
! or die "error: could not spawn find: $!";
527 $?
== 0 or die "error: find failed with exit status $?\n";
528 for my $file (@files) {
529 -e
$file and from_any_file
$file;
533 sub cli_get_port
(\@
$) {
534 my ($args, $port) = @_;
535 if ($args && @
$args) {
536 if ($args->[0] eq '-p') {
539 $port = shift @
$args;
541 } elsif ($args->[0] =~ /-p(\d+)/) {
549 if (@ARGV && $ARGV[0] eq '-c') {
551 $db_file = shift @ARGV if @ARGV;
555 my $cmd = shift @ARGV;
556 if ($cmd eq 'file') {
557 for my $name (@ARGV) {
560 } elsif ($cmd eq 'host') {
561 my $port = cli_get_port
@ARGV, 22;
566 from_ssh_host
$port, @ARGV;
567 } elsif ($cmd eq 'ssl') {
568 my $port = cli_get_port
@ARGV, 443;
573 for my $name (@ARGV) {
574 from_ssl_host
$name, $port;
576 } elsif ($cmd eq 'user') {
578 from_user
$_ for @ARGV;
582 } elsif ($cmd eq 'quick') {
584 for my $file (qw
/ssh_host_rsa_key
.pub ssh_host_dsa_key
.pub
585 ssh_host_key ssh_known_hosts ssh_known_hosts2
/) {
586 -e
$file and from_ssh_auth_file
$file;
589 } elsif ($cmd eq 'help') {
592 } elsif ($cmd eq 'version') {
593 print "dowkd $program_version (database $db_version)\n\n";
600 die "error: invalid command, use \"help\" to get help\n";