Initial version
[dowkd.git] / dowkd.in
blob3160dad86943480faf24b948ee4d7f845b5d73f2
1 #!/usr/bin/perl
3 # Debian/OpenSSL Weak Key Detector
5 # Written by Florian Weimer <fw@deneb.enyo.de>, with blacklist data
6 # from Kees Cook, Peter Palfrader and James Strandboge.
8 # Patches and comments are welcome.
10 use strict;
11 use warnings;
13 sub help () {
14 print <<EOF;
15 usage: $0 [OPTIONS...] COMMAND [ARGUMENTS...]
17 COMMAND is one of:
19 file: examine files on the command line for weak keys
20 host: examine the specified hosts for weak SSH keys
21 user: examine user SSH keys for weakness; examine all users if no
22 users are given
23 help: show this help screen
25 OPTIONS is one pf:
27 -c FILE: set the database cache file name (default: dowkd.db)
29 dowkd currently handles OpenSSH host and user keys and OpenVPN shared
30 secrets, as long as they use default key lengths and have been created
31 on a little-endian architecture (such as i386 or amd64). Note that
32 the blacklist by dowkd may be incomplete; it is only intended as a
33 quick check.
35 EOF
38 use DB_File;
39 use File::Temp;
40 use Fcntl;
41 use IO::Handle;
43 my $db_version = '1';
45 my $db_file = 'dowkd.db';
47 my $db;
48 my %db;
50 sub create_db () {
51 $db->close if $db;
52 $db = tie %db, 'DB_File', $db_file, O_RDWR | O_CREAT, 0777, $DB_BTREE
53 or die "error: could not open database: $!\n";
55 $db{''} = $db_version;
56 while (my $line = <DATA>) {
57 next if $line =~ /^\**$/;
58 chomp $line;
59 $line =~ /^[0-9a-f]{32}$/ or die "error: invalid data line";
60 $line =~ s/(..)/chr(hex($1))/ge;
61 $db{$line} = '';
64 $db->sync;
67 sub open_db () {
68 if (-r $db_file) {
69 $db = tie %db, 'DB_File', $db_file, O_RDONLY, 0777, $DB_BTREE
70 or die "error: could not open database: $!\n";
71 my $stored_version = $db{''};
72 $stored_version && $stored_version eq $db_version or create_db;
73 } else {
74 unlink $db_file;
75 create_db;
79 sub safe_backtick (@) {
80 my @args = @_;
81 my $fh;
82 open $fh, '-|', @args
83 or die "error: failed to spawn $args[0]: $!\n";
84 my @result;
85 if (wantarray) {
86 @result = <$fh>;
87 } else {
88 local $/;
89 @result = scalar(<$fh>);
91 close $fh;
92 $? == 0 or die("error: $args[0] failed with exit status "
93 . ($? >> 8) . "\n");
94 if (wantarray) {
95 return @result;
96 } else {
97 return $result[0];
101 sub check_hash ($$) {
102 my ($name, $hash) = @_;
103 if (exists $db{$hash}) {
104 print "$name\n";
108 sub ssh_fprint_file ($) {
109 my $name = shift;
110 my $data = safe_backtick qw/ssh-keygen -l -f/, $name;
111 my @data = $data =~ /^(\d+) ([0-9a-f]{2}(?::[0-9a-f]{2}){15})/;
112 return @data if @data == 2;
113 return ();
116 sub ssh_fprint_check ($$$) {
117 my ($name, $length, $hash) = @_;
118 if ($length == 1024 || $length == 2048) {
119 $hash =~ y/://d;
120 $hash =~ s/(..)/chr(hex($1))/ge;
121 check_hash $name, $hash;
122 } else {
123 warn "$name: warning: no suitable blacklist\n";
127 sub from_ssh_key_file ($) {
128 my $name = shift;
129 my $data = safe_backtick qw/ssh-keygen -l -f/, $name;
130 my ($length, $hash) = ssh_fprint_file $name;
131 if ($length && $hash) {
132 ssh_fprint_check "$name:1", $length, $hash;
133 } else {
134 warn "$name:1: warning: failed to parse SSH key file\n";
138 sub clear_tmp ($) {
139 my $tmp = shift;
140 seek $tmp, 0, 0 or die "seek: $!";
141 truncate $tmp, 0 or die "truncate: $!";
144 sub from_ssh_auth_file ($) {
145 my $name = shift;
146 my $auth;
147 unless (open $auth, '<', $name) {
148 warn "$name:0: error: open failed: $!\n";
149 return;
151 my $tmp = new File::Temp;
152 while (my $line = <$auth>) {
153 chomp $line;
154 my $lineno = $.;
155 clear_tmp $tmp;
156 print $tmp "$line\n" or die "print: $!";
157 $tmp->flush;
158 my ($length, $hash) = ssh_fprint_file "$tmp";
159 if ($length && $hash) {
160 ssh_fprint_check "$name:$lineno", $length, $hash;
161 } else {
162 warn "$name:$lineno: warning: unparsable line\n";
167 sub from_openvpn_key ($) {
168 my $name = shift;
169 my $key;
170 unless (open $key, '<', $name) {
171 warn "$name:0: open failed: $!\n";
172 return 1;
175 my $marker;
176 while (my $line = <$key>) {
177 return 0 if $. > 10;
178 if ($line =~ /^-----BEGIN OpenVPN Static key V1-----/) {
179 $marker = 1;
180 } elsif ($marker) {
181 if ($line =~ /^([0-9a-f]{32})/) {
182 $line = $1;
183 $line =~ s/(..)/chr(hex($1))/ge;
184 check_hash "$name:$.", $line;
185 return 1;
186 } else {
187 warn "$name:$.: warning: illegal OpenVPN file format\n";
188 return 1;
194 sub from_ssh_host (@) {
195 my @names = @_;
196 my @lines;
197 push @lines, safe_backtick qw/ssh-keyscan -t rsa/, @names;
198 push @lines, safe_backtick qw/ssh-keyscan -t dsa/, @names;
200 my $tmp = new File::Temp;
201 for my $line (@lines) {
202 next if $line =~ /^#/;
203 my ($host, $data) = $line =~ /^(\S+) (.*)$/;
204 clear_tmp $tmp;
205 print $tmp "$data\n" or die "print: $!";
206 $tmp->flush;
207 my ($length, $hash) = ssh_fprint_file "$tmp";
208 if ($length && $hash) {
209 ssh_fprint_check "$host", $length, $hash;
210 } else {
211 warn "$host: warning: unparsable line\n";
216 sub from_user ($) {
217 my $user = shift;
218 my ($name,$passwd,$uid,$gid,
219 $quota,$comment,$gcos,$dir,$shell,$expire) = getpwnam($user);
220 my $file = "$dir/.ssh/authorized_keys";
221 from_ssh_auth_file $file if -r $file;
222 $file = "$dir/.ssh/id_rsa.pub";
223 from_ssh_key_file $file if -r $file;
224 $file = "$dir/.ssh/id_dsa.pub";
225 from_ssh_key_file $file if -r $file;
228 sub from_user_all () {
229 setpwent;
230 while (my $name = getpwent) {
231 from_user $name;
233 endpwent;
236 if (@ARGV && $ARGV[0] eq '-c') {
237 shift @ARGV;
238 $db_file = shift @ARGV if @ARGV;
240 if (@ARGV) {
241 open_db;
242 my $cmd = shift @ARGV;
243 if ($cmd eq 'file') {
244 for my $name (@ARGV) {
245 next if from_openvpn_key $name;
246 from_ssh_auth_file $name;
248 } elsif ($cmd eq 'host') {
249 from_ssh_host @ARGV;
250 } elsif ($cmd eq 'user') {
251 if (@ARGV) {
252 from_user $_ for @ARGV;
253 } else {
254 from_user_all;
256 } elsif ($cmd eq 'help') {
257 help;
258 } else {
259 die "error: invalid command, use \"help\" to get help\n";
261 } else {
262 help;
263 exit 1;
266 my %hash;
268 __DATA__