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, RSA/2048 and DSA/1024. (The
50 OpenSSH version in Debian does not support DSA key generation with)
53 OpenVPN shared also detected on little-endian architecture.
55 Note that the blacklist by dowkd may be incomplete; it is only
56 intended as a quick check.
68 my $db_file = 'dowkd.db';
74 warn "notice: creating database, please wait\n";
75 $db = tie
%db, 'DB_File', $db_file, O_RDWR
| O_CREAT
, 0777, $DB_BTREE
76 or die "error: could not open database: $!\n";
78 $db{''} = $db_version;
79 while (my $line = <DATA
>) {
80 next if $line =~ /^\**$/;
82 $line =~ /^[0-9a-f]{32}$/ or die "error: invalid data line";
83 $line =~ s/(..)/chr(hex($1))/ge;
92 $db = tie
%db, 'DB_File', $db_file, O_RDONLY
, 0777, $DB_BTREE
93 or die "error: could not open database: $!\n";
94 my $stored_version = $db{''};
95 $stored_version && $stored_version eq $db_version or create_db
;
102 sub safe_backtick
(@
) {
105 open $fh, '-|', @args
106 or die "error: failed to spawn $args[0]: $!\n";
112 @result = scalar(<$fh>);
115 $?
== 0 or return undef;
124 my $keys_vulnerable = 0;
127 print STDERR
"summary: keys found: $keys_found, weak keys: $keys_vulnerable\n";
130 sub check_hash
($$;$) {
131 my ($name, $hash, $descr) = @_;
133 if (exists $db{$hash}) {
135 $descr = $descr ?
" ($descr)" : '';
136 print "$name: weak key$descr\n";
140 sub ssh_fprint_file
($) {
142 my $data = safe_backtick qw
/ssh-keygen -l -f/, $name;
143 defined $data or return ();
144 my @data = $data =~ /^(\d+) ([0-9a-f]{2}(?::[0-9a-f]{2}){15})/;
145 return @data if @data == 2;
149 sub ssh_fprint_check
($$$$) {
150 my ($name, $type, $length, $hash) = @_;
151 $type =~ /^(?:rsa1?|dsa)\z/ or die;
152 if (($type eq 'rsa' && ($length == 1024 || $length == 2048))
153 || ($type eq 'dsa' && $length == 1024)) {
155 $hash =~ s/(..)/chr(hex($1))/ge;
156 check_hash
$name, $hash, "OpenSSH/$type/$length";
157 } elsif ($type eq 'dsa') {
158 print "$name: $length bits DSA key not recommended\n";
160 warn "$name: warning: no blacklist for $type/$length key\n";
166 seek $tmp, 0, 0 or die "seek: $!";
167 truncate $tmp, 0 or die "truncate: $!";
170 sub cleanup_ssh_auth_line
($) {
173 $line =~ /^(?:ssh-(?:rsa|dss)\s|\d+\s+\d+\s+\d)/ and return $line;
176 if ($line =~ /^\s+(.*)/) {
180 if ($line =~ /^"(.*)/) {
184 if ($line =~ /^\\.(.*)/) {
185 # It doesn't matter if we don't deal with \000 properly, we
186 # just need to defuse the backslash character.
190 if ($line =~ /^[a-zA-Z0-9_=+-]+(.*)/) {
191 # Skip multiple harmless characters in one go.
195 if ($line =~ /^.(.*)/) {
196 # Other characters are stripped one by one.
200 return undef; # empty string, no key found
203 if ($line =~ /^"(.*)/) {
207 if ($line =~ /^\\.(.*)/) {
208 # See above, defuse the backslash.
212 if ($line =~ /^[^\\"]+(.*)/) {
216 return undef; # missing closing double quote
219 $line =~ /^(?:ssh-(?:rsa|dss)\s|\d+\s+\d+\s+\d)/ and return $line;
223 sub derive_ssh_auth_type
($) {
225 $line =~ /^ssh-rsa\s/ and return 'rsa';
226 $line =~ /^ssh-dss\s/ and return 'dsa';
227 $line =~ /^\d+\s/ and return 'rsa1';
231 sub from_ssh_auth_line
($$$) {
232 my ($tmp, $name, $line) = @_;
234 return if $line =~ m/^\s*(#|$)/;
237 my $l = cleanup_ssh_auth_line
$line;
241 my $type = derive_ssh_auth_type
$line;
244 print $tmp "$line\n" or die "print: $!";
245 $tmp->flush or die "flush: $!";
246 my ($length, $hash) = ssh_fprint_file
"$tmp";
247 if ($length && $hash) {
248 ssh_fprint_check
"$name", $type, $length, $hash;
253 warn "$name: warning: unparsable line\n";
256 sub from_ssh_auth_fd
($$) {
257 my ($name, $auth) = @_;
258 my $tmp = new File
::Temp
;
259 while (my $line = <$auth>) {
260 from_ssh_auth_line
$tmp, "$name:$.", $line;
264 sub from_ssh_auth_file
($) {
267 unless (open $auth, '<', $name) {
268 warn "$name:0: error: open failed: $!\n";
271 return from_ssh_auth_fd
$name, $auth;
274 sub from_openvpn_key
($) {
277 unless (open $key, '<', $name) {
278 warn "$name:0: open failed: $!\n";
283 while (my $line = <$key>) {
285 if ($line =~ /^-----BEGIN OpenVPN Static key V1-----/) {
288 if ($line =~ /^([0-9a-f]{32})/) {
290 $line =~ s/(..)/chr(hex($1))/ge;
291 check_hash
"$name:$.", $line, "OpenVPN";
294 warn "$name:$.: warning: illegal OpenVPN file format\n";
301 sub openssl_modulus_check
($$) {
302 my ($name, $modulus) = @_;
304 if ($modulus =~ /^Modulus=([A-F0-9]+)$/) {
306 my $length = length($modulus) * 4;
307 if ($length == 1024 || $length == 2048) {
308 my $mod = substr $modulus, length($modulus) - 32;
310 my @mod = $mod =~ /(..)/g;
311 $mod = join('', map { chr(hex($_)) } reverse @mod);
312 check_hash
$name, $mod, "OpenSSL/RSA/$length";
314 warn "$name: warning: no blacklist for OpenSSL/RSA/$length key\n";
317 die "internal error: $modulus\n";
327 unless (open $src, '<', $name) {
328 warn "$name:0: open failed: $!\n";
332 while (my $line = <$src>) {
333 if ($line =~ /^-----BEGIN CERTIFICATE-----/) {
335 $tmp or $tmp = new File
::Temp
;
338 print $tmp $line or die "print: $!";
339 goto LAST
if $line =~ /^-----END CERTIFICATE-----/;
340 } while ($line = <$src>);
342 $tmp->flush or die "flush: $!";
343 my $mod = safe_backtick qw
/openssl x509 -noout -modulus -in/, $tmp;
345 openssl_modulus_check
"$name:$lineno", $mod;
348 warn "$name:$lineno: failed to parse certificate\n";
351 } elsif ($line =~ /^-----BEGIN RSA PRIVATE KEY-----/) {
353 $tmp or $tmp = new File
::Temp
;
356 print $tmp $line or die "print: $!";
357 goto LAST_RSA
if $line =~ /^-----END RSA PRIVATE KEY-----/;
358 } while ($line = <$src>);
360 $tmp->flush or die "flush: $!";
361 my $mod = safe_backtick qw
/openssl rsa -noout -modulus -in/, $tmp;
363 openssl_modulus_check
"$name:$lineno", $mod;
366 warn "$name:$lineno: failed to parse RSA private key\n";
375 sub from_ssh_host
(@
) {
379 my ($name,$aliases,$addrtype,$length,@addrs) = gethostbyname $_;
380 @addrs or warn "warning: host not found: $_\n";
385 push @lines, safe_backtick qw
/ssh-keyscan -t rsa/, @names;
386 push @lines, safe_backtick qw
/ssh-keyscan -t dsa/, @names;
388 my $tmp = new File
::Temp
;
389 for my $line (@lines) {
390 next if $line =~ /^#/;
391 my ($host, $data) = $line =~ /^(\S+) (.*)$/;
392 from_ssh_auth_line
$tmp, $host, $data;
398 my ($name,$passwd,$uid,$gid,
399 $quota,$comment,$gcos,$dir,$shell,$expire) = getpwnam($user);
401 warn "warning: user $user does not exist\n";
404 for my $name (qw
/authorized_keys authorized_keys2
405 known_hosts known_hosts2
406 id_rsa
.pub id_dsa
.pub identity
.pub
/) {
407 my $file = "$dir/.ssh/$name";
408 from_ssh_auth_file
$file if -r
$file;
412 sub from_user_all
() {
413 # This was one loop initially, but does not work with some Perl
417 while (my $name = getpwent) {
421 from_user
$_ for @names;
424 if (@ARGV && $ARGV[0] eq '-c') {
426 $db_file = shift @ARGV if @ARGV;
430 my $cmd = shift @ARGV;
431 if ($cmd eq 'file') {
432 for my $name (@ARGV) {
433 next if from_openvpn_key
$name;
434 next if from_pem
$name;
435 from_ssh_auth_file
$name;
437 } elsif ($cmd eq 'host') {
439 } elsif ($cmd eq 'user') {
441 from_user
$_ for @ARGV;
445 } elsif ($cmd eq 'help') {
449 die "error: invalid command, use \"help\" to get help\n";