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 help: show this help screen
43 version: show version information
47 -c FILE: set the database cache file name (default: dowkd.db)
49 dowkd currently handles the following OpenSSH host and user keys,
50 provided they have been generated on a little-endian architecture
51 (such as i386 or amd64):
53 RSA/1024, RSA/2048 (both rsa1 and rsa format)
56 (The relevant OpenSSH versions in Debian do not support DSA key
57 generation with other sizes.)
59 OpenVPN shared also detected if they have been created on
60 little-endian architectures.
62 Unencrypted RSA private keys and PEM certificate files generated by
63 OpenSSL are detected, provided they use key lengths of 1024 or 2048
64 bits (again, only for little-endian architectures).
66 Note that the blacklist by dowkd may be incomplete; it is only
67 intended as a quick check.
77 my $db_version = '@DB_VERSION@';
78 my $program_version = '@PROGRAM_VERSION@';
79 my $changelog = <<'EOF';
84 my $db_file = 'dowkd.db';
90 warn "notice: creating database, please wait\n";
91 $db = tie
%db, 'DB_File', $db_file, O_RDWR
| O_CREAT
, 0777, $DB_BTREE
92 or die "error: could not open database: $!\n";
95 while (my $line = <DATA
>) {
96 next if $line =~ /^\**$/;
98 $line =~ /^[0-9a-f]{32}$/ or die "error: invalid data line";
99 $line =~ s/(..)/chr(hex($1))/ge;
103 $found or die "error: no blacklist data found in script\n";
105 # Set at the end so that no incomplete database is left behind.
106 $db{''} = $db_version;
113 $db = tie
%db, 'DB_File', $db_file, O_RDONLY
, 0777, $DB_BTREE
114 or die "error: could not open database: $!\n";
115 my $stored_version = $db{''};
116 $stored_version && $stored_version eq $db_version or create_db
;
123 sub safe_backtick
(@
) {
126 open $fh, '-|', @args
127 or die "error: failed to spawn $args[0]: $!\n";
133 @result = scalar(<$fh>);
136 $?
== 0 or return undef;
145 my $keys_vulnerable = 0;
148 print STDERR
"summary: keys found: $keys_found, weak keys: $keys_vulnerable\n";
151 sub check_hash
($$;$) {
152 my ($name, $hash, $descr) = @_;
154 if (exists $db{$hash}) {
156 $descr = $descr ?
" ($descr)" : '';
157 print "$name: weak key$descr\n";
161 sub ssh_fprint_file
($) {
163 my $data = safe_backtick qw
/ssh-keygen -l -f/, $name;
164 defined $data or return ();
165 my @data = $data =~ /^(\d+) ([0-9a-f]{2}(?::[0-9a-f]{2}){15})/;
166 return @data if @data == 2;
170 sub ssh_fprint_check
($$$$) {
171 my ($name, $type, $length, $hash) = @_;
172 $type =~ /^(?:rsa1?|dsa)\z/ or die;
174 && ($length == 1024 || $length == 2048 || $length == 4096))
175 || ($type eq 'dsa' && $length == 1024)
176 || ($type eq 'rsa1' && $length == 1024)) {
178 $hash =~ s/(..)/chr(hex($1))/ge;
179 check_hash
$name, $hash, "OpenSSH/$type/$length";
180 } elsif ($type eq 'dsa') {
181 print "$name: $length bits DSA key not recommended\n";
183 warn "$name: warning: no blacklist for $type/$length key\n";
189 seek $tmp, 0, 0 or die "seek: $!";
190 truncate $tmp, 0 or die "truncate: $!";
193 sub cleanup_ssh_auth_line
($) {
196 $line =~ /^(?:ssh-(?:rsa|dss)\s|\d+\s+\d+\s+\d)/ and return $line;
199 if ($line =~ /^\s+(.*)/) {
203 if ($line =~ /^"(.*)/) {
207 if ($line =~ /^\\.(.*)/) {
208 # It doesn't matter if we don't deal with \000 properly, we
209 # just need to defuse the backslash character.
213 if ($line =~ /^[a-zA-Z0-9_=+-]+(.*)/) {
214 # Skip multiple harmless characters in one go.
218 if ($line =~ /^.(.*)/) {
219 # Other characters are stripped one by one.
223 return undef; # empty string, no key found
226 if ($line =~ /^"(.*)/) {
230 if ($line =~ /^\\.(.*)/) {
231 # See above, defuse the backslash.
235 if ($line =~ /^[^\\"]+(.*)/) {
239 return undef; # missing closing double quote
242 $line =~ /^(?:ssh-(?:rsa|dss)\s|\d+\s+\d+\s+\d)/ and return $line;
246 sub derive_ssh_auth_type
($) {
248 $line =~ /^ssh-rsa\s/ and return 'rsa';
249 $line =~ /^ssh-dss\s/ and return 'dsa';
250 $line =~ /^\d+\s/ and return 'rsa1';
254 sub from_ssh_auth_line
($$$) {
255 my ($tmp, $name, $line) = @_;
257 return if $line =~ m/^\s*(#|$)/;
260 my $l = cleanup_ssh_auth_line
$line;
264 my $type = derive_ssh_auth_type
$line;
267 print $tmp "$line\n" or die "print: $!";
268 $tmp->flush or die "flush: $!";
269 my ($length, $hash) = ssh_fprint_file
"$tmp";
270 if ($length && $hash) {
271 ssh_fprint_check
"$name", $type, $length, $hash;
276 warn "$name: warning: unparsable line\n";
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
;
288 while (my $line = <$auth>) {
289 from_ssh_auth_line
$tmp, "$name:$.", $line;
293 sub from_openvpn_key
($) {
296 unless (open $key, '<', $name) {
297 warn "$name:0: open failed: $!\n";
302 while (my $line = <$key>) {
304 if ($line =~ /^-----BEGIN OpenVPN Static key V1-----/) {
307 if ($line =~ /^([0-9a-f]{32})/) {
309 $line =~ s/(..)/chr(hex($1))/ge;
310 check_hash
"$name:$.", $line, "OpenVPN";
313 warn "$name:$.: warning: illegal OpenVPN file format\n";
320 sub openssl_modulus_check
($$) {
321 my ($name, $modulus) = @_;
323 if ($modulus =~ /^Modulus=([A-F0-9]+)$/) {
325 my $length = length($modulus) * 4;
326 if ($length == 1024 || $length == 2048) {
327 my $mod = substr $modulus, length($modulus) - 32;
329 my @mod = $mod =~ /(..)/g;
330 $mod = join('', map { chr(hex($_)) } reverse @mod);
331 check_hash
$name, $mod, "OpenSSL/RSA/$length";
333 warn "$name: warning: no blacklist for OpenSSL/RSA/$length key\n";
336 die "internal error: $modulus\n";
346 unless (open $src, '<', $name) {
347 warn "$name:0: open failed: $!\n";
351 while (my $line = <$src>) {
352 if ($line =~ /^-----BEGIN CERTIFICATE-----/) {
354 $tmp or $tmp = new File
::Temp
;
357 print $tmp $line or die "print: $!";
358 goto LAST
if $line =~ /^-----END CERTIFICATE-----/;
359 } while ($line = <$src>);
361 $tmp->flush or die "flush: $!";
362 my $mod = safe_backtick qw
/openssl x509 -noout -modulus -in/, $tmp;
364 openssl_modulus_check
"$name:$lineno", $mod;
367 warn "$name:$lineno: failed to parse certificate\n";
370 } elsif ($line =~ /^-----BEGIN RSA PRIVATE KEY-----/) {
372 $tmp or $tmp = new File
::Temp
;
375 print $tmp $line or die "print: $!";
376 goto LAST_RSA
if $line =~ /^-----END RSA PRIVATE KEY-----/;
377 } while ($line = <$src>);
379 $tmp->flush or die "flush: $!";
380 my $mod = safe_backtick qw
/openssl rsa -noout -modulus -in/, $tmp;
382 openssl_modulus_check
"$name:$lineno", $mod;
385 warn "$name:$lineno: failed to parse RSA private key\n";
394 sub from_ssh_host
($@
) {
395 my ($port, @names) = @_;
398 my ($name,$aliases,$addrtype,$length,@addrs) = gethostbyname $_;
399 @addrs or warn "warning: host not found: $_\n";
404 push @lines, safe_backtick qw
/ssh-keyscan -t rsa -p/, $port, @names;
405 push @lines, safe_backtick qw
/ssh-keyscan -t dsa -p/, $port, @names;
407 my $tmp = new File
::Temp
;
408 for my $line (@lines) {
409 next if $line =~ /^#/;
410 my ($host, $data) = $line =~ /^(\S+) (.*)$/;
411 from_ssh_auth_line
$tmp, $host, $data;
417 my ($name,$passwd,$uid,$gid,
418 $quota,$comment,$gcos,$dir,$shell,$expire) = getpwnam($user);
420 warn "warning: user $user does not exist\n";
423 for my $name (qw
/authorized_keys authorized_keys2
424 known_hosts known_hosts2
425 id_rsa
.pub id_dsa
.pub identity
.pub
/) {
426 my $file = "$dir/.ssh/$name";
427 from_ssh_auth_file
$file if -r
$file;
431 sub from_user_all
() {
432 # This was one loop initially, but does not work with some Perl
436 while (my $name = getpwent) {
440 from_user
$_ for @names;
443 if (@ARGV && $ARGV[0] eq '-c') {
445 $db_file = shift @ARGV if @ARGV;
449 my $cmd = shift @ARGV;
450 if ($cmd eq 'file') {
451 for my $name (@ARGV) {
452 next if from_openvpn_key
$name;
453 next if from_pem
$name;
454 from_ssh_auth_file
$name;
456 } elsif ($cmd eq 'host') {
462 if ($ARGV[0] eq '-p') {
467 } elsif ($ARGV[0] =~ /-p(\d+)/) {
475 from_ssh_host
$port, @ARGV;
476 } elsif ($cmd eq 'user') {
478 from_user
$_ for @ARGV;
482 } elsif ($cmd eq 'help') {
485 } elsif ($cmd eq 'version') {
486 print "dowkd $program_version (database $db_version)\n\n$changelog";
489 die "error: invalid command, use \"help\" to get help\n";