]> eyrie.org Git - kerberos/krb5-strength.git/blob - tests/tools/heimdal-strength-t
Coding style cleanup and tests for minimum classes
[kerberos/krb5-strength.git] / tests / tools / heimdal-strength-t
1 #!/usr/bin/perl
2 #
3 # Test suite for basic Heimdal external strength checking functionality.
4 #
5 # Written by Russ Allbery <eagle@eyrie.org>
6 # Copyright 2016 Russ Allbery <eagle@eyrie.org>
7 # Copyright 2009, 2012, 2013, 2014
8 #     The Board of Trustees of the Leland Stanford Junior University
9 #
10 # See LICENSE for licensing terms.
11
12 use 5.006;
13 use strict;
14 use warnings;
15
16 use lib "$ENV{SOURCE}/tap/perl";
17
18 use File::Copy qw(copy);
19 use Test::RRA qw(use_prereq);
20 use Test::RRA::Automake qw(test_file_path);
21
22 use_prereq('IPC::Run', 'run');
23 use_prereq('JSON');
24 use_prereq('Perl6::Slurp', 'slurp');
25 use_prereq('Test::More',   '0.87_01');
26
27 # Run the newly-built heimdal-strength command and return the status, output,
28 # and error output as a list.  If told to expect an immediate error, does not
29 # pass input to the process.
30 #
31 # $principal - Principal to pass to the command
32 # $password  - Password to pass to the command
33 # $error     - Whether to expect an immediate error
34 #
35 # Returns: The exit status, standard output, and standard error as a list
36 #  Throws: Text exception on failure to run the test program
37 sub run_heimdal_strength {
38     my ($principal, $password, $error) = @_;
39
40     # Build the input to the strength checking program.
41     my $in = q{};
42     if (!$error) {
43         $in .= "principal: $principal\n";
44         $in .= "new-password: $password\n";
45         $in .= "end\n";
46     }
47
48     # Find the newly-built password checking program.
49     my $program = test_file_path('../tools/heimdal-strength');
50
51     # Run the password strength checker.
52     my ($out, $err);
53     my $harness = run([$program, $principal], \$in, \$out, \$err);
54     my $status = $? >> 8;
55
56     # Return the results.
57     return ($status, $out, $err);
58 }
59
60 # Run the newly-built heimdal-strength command to check a password and reports
61 # the results using Test::More.  This uses the standard protocol for Heimdal
62 # external password strength checking programs.
63 #
64 # $test_ref - Reference to hash of test parameters
65 #   name      - The name of the test case
66 #   principal - The principal changing its password
67 #   password  - The new password
68 #   status    - If present, the exit status (otherwise, it should be 0)
69 #   error     - If present, the expected rejection error
70 #
71 # Returns: undef
72 #  Throws: Text exception on failure to run the test program
73 sub check_password {
74     my ($test_ref) = @_;
75     my $principal  = $test_ref->{principal};
76     my $password   = $test_ref->{password};
77
78     # Run the heimdal-strength command.
79     my ($status, $out, $err) = run_heimdal_strength($principal, $password);
80     chomp($out, $err);
81
82     # Check the results.  If there is an error in the password, it should come
83     # on standard error; otherwise, standard output should be APPROVED.  If
84     # there is a non-zero exit status, we expect the error on standard error
85     # and use that field to check for system errors.
86     is($status, $test_ref->{status} || 0, "$test_ref->{name} (status)");
87     if (defined($test_ref->{error})) {
88         is($err, $test_ref->{error}, '...error message');
89         is($out, q{}, '...no output');
90     } else {
91         is($err, q{},        '...no errors');
92         is($out, 'APPROVED', '...approved');
93     }
94     return;
95 }
96
97 # Create a new krb5.conf file that includes arbitrary settings passed in via
98 # a hash reference.
99 #
100 # $settings_ref - Hash of keys and values to put into [appdefaults]
101 #
102 # Returns: Path to the new krb5.conf file
103 #  Throws: Text exception if the new krb5.conf file cannot be created
104 sub create_krb5_conf {
105     my ($settings_ref) = @_;
106
107     # Paths for krb5.conf creation.
108     my $old    = test_file_path('data/krb5.conf');
109     my $tmpdir = $ENV{BUILD} ? "$ENV{BUILD}/tmp" : 'tests/tmp';
110     my $new    = "$tmpdir/krb5.conf";
111
112     # Create a temporary directory for the new file.
113     if (!-d $tmpdir) {
114         mkdir($tmpdir, 0777) or die "Cannot create $tmpdir: $!\n";
115     }
116
117     # Start with the testing krb5.conf file shipped in the package.
118     copy($old, $new) or die "Cannot copy $old to $new: $!\n";
119
120     # Append the local configuration.
121     open(my $config, '>>', $new) or die "Cannot append to $new: $!\n";
122     print {$config} "\n[appdefaults]\n    krb5-strength = {\n"
123       or die "Cannot append to $new: $!\n";
124     for my $key (keys %{$settings_ref}) {
125         print {$config} q{ } x 8, $key, ' = ', $settings_ref->{$key}, "\n"
126           or die "Cannot append to $new: $!\n";
127     }
128     print {$config} "    }\n"
129       or die "Cannot append to $new: $!\n";
130     close($config) or die "Cannot append to $new: $!\n";
131
132     # Return the path to the new file.
133     return $new;
134 }
135
136 # Load a set of password test cases and return them as a list.  The given file
137 # name is relative to data/passwords in the test suite.
138 #
139 # $file - The file name containing the test data in JSON
140 #
141 # Returns: List of anonymous hashes representing password test cases
142 #  Throws: Text exception on failure to load the test data
143 sub load_password_tests {
144     my ($file) = @_;
145     my $path = test_file_path("data/passwords/$file");
146
147     # Load the test file data into memory.
148     my $testdata = slurp($path);
149
150     # Decode the JSON into Perl objects and return them.
151     my $json = JSON->new->utf8;
152     return $json->decode($testdata);
153 }
154
155 # Test a required_classes syntax error.  Takes the string for required_classes
156 # and verifies that the appropriate error message is returned.
157 #
158 # $bad_class - Bad class specification
159 #
160 # Returns: undef
161 sub test_require_classes_syntax {
162     my ($bad_class)  = @_;
163     my $error_prefix = 'Cannot initialize strength checking';
164     my $bad_message  = 'bad character class requirement in configuration';
165     my $bad_minimum  = 'bad character class minimum in configuration';
166
167     # Run heimdal-strength.
168     my $krb5_conf = create_krb5_conf({ require_classes => $bad_class });
169     local $ENV{KRB5_CONFIG} = $krb5_conf;
170     my ($status, $output, $err) = run_heimdal_strength('test', 'password', 1);
171
172     # Check the results.
173     is($status, 1,   "Bad class specification '$bad_class' (status)");
174     is($output, q{}, '...no output');
175     my $expected;
176     if ($bad_class =~ m{ \A (\d+ [^-]*) \z | : (\d+) \z }xms) {
177         my $minimum = $1 || $2;
178         $expected = "$error_prefix: $bad_minimum: $minimum\n";
179     } else {
180         $expected = "$error_prefix: $bad_message: $bad_class\n";
181     }
182     is($err, $expected, '...correct error');
183     return;
184 }
185
186 # Load the password tests from JSON.  Accumulate a total count of tests for
187 # the testing plan.
188 my (%tests, $count);
189 for my $type (qw(cdb classes cracklib length letter principal sqlite)) {
190     my $tests = load_password_tests("$type.json");
191     $tests{$type} = $tests;
192     $count += scalar(@{$tests});
193 }
194
195 # We run the principal tests three times, for CrackLib, CDB, and SQLite.
196 $count += 2 * scalar(@{ $tests{principal} });
197
198 # We run the length checks twice.
199 $count += scalar(@{ $tests{length} });
200
201 # We can now calculate our plan based on three tests for each password test,
202 # plus 27 additional tests for error handling.
203 plan(tests => $count * 3 + 27);
204
205 # Install the krb5.conf file with a configuration pointing to the test
206 # CrackLib dictionary.
207 my $datadir = $ENV{BUILD} ? "$ENV{BUILD}/data" : 'tests/data';
208 my $krb5_conf
209   = create_krb5_conf({ password_dictionary => "$datadir/dictionary" });
210 local $ENV{KRB5_CONFIG} = $krb5_conf;
211
212 # Run the CrackLib password tests and based-on-principal tests from JSON.
213 note('CrackLib tests');
214 for my $test (@{ $tests{cracklib} }) {
215     check_password($test);
216 }
217 note('Generic tests with CrackLib');
218 for my $test (@{ $tests{principal} }) {
219     check_password($test);
220 }
221
222 # Install the krb5.conf file with a length restriction.
223 $krb5_conf = create_krb5_conf({ minimum_length => 12 });
224 local $ENV{KRB5_CONFIG} = $krb5_conf;
225
226 # Run the password length checks.
227 note('Password length checks');
228 for my $test (@{ $tests{length} }) {
229     check_password($test);
230 }
231
232 # Add a CrackLib dictionary and a maximum password length setting.
233 $krb5_conf = create_krb5_conf(
234     {
235         password_dictionary => "$datadir/dictionary",
236         minimum_length      => 12,
237         cracklib_maxlen     => 11,
238     }
239 );
240 local $ENV{KRB5_CONFIG} = $krb5_conf;
241
242 # Run the length checks again.  They should have the same result, even though
243 # there's a CrackLib dictionary, since the dictionary hit is above the minimum
244 # length.
245 note('Password length checks with cracklib_maxlen');
246 for my $test (@{ $tests{length} }) {
247     check_password($test);
248 }
249
250 # Install the krb5.conf file for simple character class restrictions.
251 $krb5_conf = create_krb5_conf(
252     {
253         minimum_different       => 8,
254         require_ascii_printable => 'true',
255         require_non_letter      => 'true',
256     }
257 );
258 local $ENV{KRB5_CONFIG} = $krb5_conf;
259
260 # Run the simple character class tests.
261 note('Simple password character class checks');
262 for my $test (@{ $tests{letter} }) {
263     check_password($test);
264 }
265
266 # Install the krb5.conf file for complex character class restrictions.
267 my $classes = '8-19:lower,upper 8-15:digit 8-11:symbol 24-24:3';
268 $krb5_conf = create_krb5_conf({ require_classes => $classes });
269 local $ENV{KRB5_CONFIG} = $krb5_conf;
270
271 # Run the complex character class tests.
272 note('Complex password character class checks');
273 for my $test (@{ $tests{classes} }) {
274     check_password($test);
275 }
276
277 # Install the krb5.conf file with configuration pointing to the CDB
278 # dictionary.
279 my $cdb_database = test_file_path('data/wordlist.cdb');
280 $krb5_conf = create_krb5_conf({ password_dictionary_cdb => $cdb_database });
281 local $ENV{KRB5_CONFIG} = $krb5_conf;
282
283 # Check whether we were built with CDB support.  If so, run those tests.
284 my ($status, $output, $err) = run_heimdal_strength('test', 'password');
285 SKIP: {
286     if ($status == 1 && $err =~ m{ not [ ] built [ ] with [ ] CDB }xms) {
287         my $total = scalar(@{ $tests{cdb} }) + scalar(@{ $tests{principal} });
288         skip('not built with CDB support', $total * 3);
289     }
290
291     # Run the CDB and principal password tests from JSON.
292     note('CDB tests');
293     for my $test (@{ $tests{cdb} }) {
294         check_password($test);
295     }
296     note('Generic tests with CDB');
297     for my $test (@{ $tests{principal} }) {
298         check_password($test);
299     }
300 }
301
302 # Install the krb5.conf file with configuration pointing to the SQLite
303 # dictionary.
304 my $sqlite_database = test_file_path('data/wordlist.sqlite');
305 $krb5_conf
306   = create_krb5_conf({ password_dictionary_sqlite => $sqlite_database });
307 local $ENV{KRB5_CONFIG} = $krb5_conf;
308
309 # Check whether we were built with SQLite support.  If so, run those tests.
310 ($status, $output, $err) = run_heimdal_strength('test', 'password');
311 SKIP: {
312     if ($status == 1 && $err =~ m{ not [ ] built [ ] with [ ] SQLite }xms) {
313         my $total = scalar(@{ $tests{sqlite} });
314         $total += scalar(@{ $tests{principal} });
315         skip('not built with SQLite support', $total * 3);
316     }
317
318     # Run the SQLite and principal password tests from JSON.
319     note('SQLite tests');
320     for my $test (@{ $tests{sqlite} }) {
321         check_password($test);
322     }
323     note('Generic tests with SQLite');
324     for my $test (@{ $tests{principal} }) {
325         check_password($test);
326     }
327 }
328
329 # Test error for an unknown character class.
330 $krb5_conf = create_krb5_conf({ require_classes => 'bogus' });
331 local $ENV{KRB5_CONFIG} = $krb5_conf;
332 my $error_prefix = 'Cannot initialize strength checking';
333 ($status, $output, $err) = run_heimdal_strength('test', 'password', 1);
334 is($status, 1,   'Bad character class (status)');
335 is($output, q{}, '...no output');
336 is($err, "$error_prefix: unknown character class bogus\n", '...correct error');
337
338 # Test a variety of configuration syntax errors in require_classes.
339 my @bad_classes = qw(
340   8 8bogus 8:bogus 4-:bogus 4-bogus 4-8bogus 10:3 10-11:5
341 );
342 for my $bad_class (@bad_classes) {
343     test_require_classes_syntax($bad_class);
344 }
345
346 # Clean up our temporary krb5.conf file on any exit.
347 END {
348     my $tmpdir = $ENV{BUILD} ? "$ENV{BUILD}/tmp" : 'tests/tmp';
349     my $config = "$tmpdir/krb5.conf";
350     if (-f $config) {
351         unlink($config) or warn "Cannot remove $config\n";
352         rmdir($tmpdir);
353     }
354 }