3 # Test suite for basic Heimdal external strength checking functionality.
5 # Written by Russ Allbery <eagle@eyrie.org>
6 # Copyright 2009, 2012, 2013, 2014
7 # The Board of Trustees of the Leland Stanford Junior University
9 # See LICENSE for licensing terms.
15 use lib "$ENV{SOURCE}/tap/perl";
17 use File::Copy qw(copy);
18 use Test::RRA qw(use_prereq);
19 use Test::RRA::Automake qw(test_file_path);
21 use_prereq('IPC::Run', 'run');
23 use_prereq('Perl6::Slurp', 'slurp');
24 use_prereq('Test::More', '0.87_01');
26 # Run the newly-built heimdal-strength command and return the status, output,
27 # and error output as a list.
29 # $principal - Principal to pass to the command
30 # $password - Password to pass to the command
32 # Returns: The exit status, standard output, and standard error as a list
33 # Throws: Text exception on failure to run the test program
34 sub run_heimdal_strength {
35 my ($principal, $password) = @_;
37 # Build the input to the strength checking program.
38 my $in = "principal: $principal\n";
39 $in .= "new-password: $password\n";
42 # Find the newly-built password checking program.
43 my $program = test_file_path('../tools/heimdal-strength');
45 # Run the password strength checker.
47 run([$program, $principal], \$in, \$out, \$err);
48 my $status = ($? >> 8);
51 return ($status, $out, $err);
54 # Run the newly-built heimdal-strength command to check a password and reports
55 # the results using Test::More. This uses the standard protocol for Heimdal
56 # external password strength checking programs.
58 # $test_ref - Reference to hash of test parameters
59 # name - The name of the test case
60 # principal - The principal changing its password
61 # password - The new password
62 # status - If present, the exit status (otherwise, it should be 0)
63 # error - If present, the expected rejection error
66 # Throws: Text exception on failure to run the test program
69 my $principal = $test_ref->{principal};
70 my $password = $test_ref->{password};
72 # Run the heimdal-strength command.
73 my ($status, $out, $err) = run_heimdal_strength($principal, $password);
76 # Check the results. If there is an error in the password, it should come
77 # on standard error; otherwise, standard output should be APPROVED. If
78 # there is a non-zero exit status, we expect the error on standard error
79 # and use that field to check for system errors.
80 is($status, $test_ref->{status} || 0, "$test_ref->{name} (status)");
81 if (defined($test_ref->{error})) {
82 is($err, $test_ref->{error}, '...error message');
83 is($out, q{}, '...no output');
85 is($err, q{}, '...no errors');
86 is($out, 'APPROVED', '...approved');
91 # Create a new krb5.conf file that includes arbitrary settings passed in via
94 # $settings_ref - Hash of keys and values to put into [appdefaults]
96 # Returns: Path to the new krb5.conf file
97 # Throws: Text exception if the new krb5.conf file cannot be created
98 sub create_krb5_conf {
99 my ($settings_ref) = @_;
101 # Paths for krb5.conf creation.
102 my $old = test_file_path('data/krb5.conf');
103 my $tmpdir = $ENV{BUILD} ? "$ENV{BUILD}/tmp" : 'tests/tmp';
104 my $new = "$tmpdir/krb5.conf";
106 # Create a temporary directory for the new file.
108 mkdir($tmpdir, 0777) or die "Cannot create $tmpdir: $!\n";
111 # Start with the testing krb5.conf file shipped in the package.
112 copy($old, $new) or die "Cannot copy $old to $new: $!\n";
114 # Append the local configuration.
115 open(my $config, '>>', $new) or die "Cannot append to $new: $!\n";
116 print {$config} "\n[appdefaults]\n krb5-strength = {\n"
117 or die "Cannot append to $new: $!\n";
118 for my $key (keys %{$settings_ref}) {
119 print {$config} q{ } x 8, $key, ' = ', $settings_ref->{$key}, "\n"
120 or die "Cannot append to $new: $!\n";
122 print {$config} " }\n"
123 or die "Cannot append to $new: $!\n";
124 close($config) or die "Cannot append to $new: $!\n";
126 # Return the path to the new file.
130 # Load a set of password test cases and return them as a list. The given file
131 # name is relative to data/passwords in the test suite.
133 # $file - The file name containing the test data in JSON
135 # Returns: List of anonymous hashes representing password test cases
136 # Throws: Text exception on failure to load the test data
137 sub load_password_tests {
139 my $path = test_file_path("data/passwords/$file");
141 # Load the test file data into memory.
142 my $testdata = slurp($path);
144 # Decode the JSON into Perl objects and return them.
145 my $json = JSON->new->utf8;
146 return $json->decode($testdata);
149 # Load the password tests from JSON. Accumulate a total count of tests for
152 for my $type (qw(cdb classes cracklib length letter principal sqlite)) {
153 my $tests = load_password_tests("$type.json");
154 $tests{$type} = $tests;
155 $count += scalar(@{$tests});
158 # We run the principal tests three times, for CrackLib, CDB, and SQLite.
159 $count += 2 * scalar(@{ $tests{principal} });
161 # We can now calculate our plan based on three tests for each password test,
162 # plus 21 additional tests for error handling.
163 plan(tests => $count * 3 + 21);
165 # Install the krb5.conf file with a configuration pointing to the test
166 # CrackLib dictionary.
167 my $datadir = $ENV{BUILD} ? "$ENV{BUILD}/data" : 'tests/data';
169 = create_krb5_conf({ password_dictionary => "$datadir/dictionary" });
170 local $ENV{KRB5_CONFIG} = $krb5_conf;
172 # Run the CrackLib password tests and based-on-principal tests from JSON.
173 note('CrackLib tests');
174 for my $test (@{ $tests{cracklib} }) {
175 check_password($test);
177 note('Generic tests with CrackLib');
178 for my $test (@{ $tests{principal} }) {
179 check_password($test);
182 # Install the krb5.conf file with a length restriction.
183 $krb5_conf = create_krb5_conf({ minimum_length => 12 });
184 local $ENV{KRB5_CONFIG} = $krb5_conf;
186 # Run the password length checks.
187 note('Password length checks');
188 for my $test (@{ $tests{length} }) {
189 check_password($test);
192 # Install the krb5.conf file for simple character class restrictions.
193 $krb5_conf = create_krb5_conf(
195 minimum_different => 8,
196 require_ascii_printable => 'true',
197 require_non_letter => 'true',
200 local $ENV{KRB5_CONFIG} = $krb5_conf;
202 # Run the simple character class tests.
203 note('Simple password character class checks');
204 for my $test (@{ $tests{letter} }) {
205 check_password($test);
208 # Install the krb5.conf file for complex character class restrictions.
209 my $classes = '8-19:lower,upper 8-15:digit 8-11:symbol';
210 $krb5_conf = create_krb5_conf({ require_classes => $classes });
211 local $ENV{KRB5_CONFIG} = $krb5_conf;
213 # Run the complex character class tests.
214 note('Complex password character class checks');
215 for my $test (@{ $tests{classes} }) {
216 check_password($test);
219 # Install the krb5.conf file with configuration pointing to the CDB
221 my $cdb_database = test_file_path('data/wordlist.cdb');
222 $krb5_conf = create_krb5_conf({ password_dictionary_cdb => $cdb_database });
223 local $ENV{KRB5_CONFIG} = $krb5_conf;
225 # Check whether we were built with CDB support. If so, run those tests.
226 my ($status, $output, $err) = run_heimdal_strength('test', 'password');
228 if ($status == 1 && $err =~ m{ not [ ] built [ ] with [ ] CDB }xms) {
229 my $total = scalar(@{ $tests{cdb} }) + scalar(@{ $tests{principal} });
230 skip('not built with CDB support', $total * 3);
233 # Run the CDB and principal password tests from JSON.
235 for my $test (@{ $tests{cdb} }) {
236 check_password($test);
238 note('Generic tests with CDB');
239 for my $test (@{ $tests{principal} }) {
240 check_password($test);
244 # Install the krb5.conf file with configuration pointing to the SQLite
246 my $sqlite_database = test_file_path('data/wordlist.sqlite');
248 = create_krb5_conf({ password_dictionary_sqlite => $sqlite_database });
249 local $ENV{KRB5_CONFIG} = $krb5_conf;
251 # Check whether we were built with SQLite support. If so, run those tests.
252 ($status, $output, $err) = run_heimdal_strength('test', 'password');
254 if ($status == 1 && $err =~ m{ not [ ] built [ ] with [ ] SQLite }xms) {
255 my $total = scalar(@{ $tests{sqlite} });
256 $total += scalar(@{ $tests{principal} });
257 skip('not built with SQLite support', $total * 3);
260 # Run the SQLite and principal password tests from JSON.
261 note('SQLite tests');
262 for my $test (@{ $tests{sqlite} }) {
263 check_password($test);
265 note('Generic tests with SQLite');
266 for my $test (@{ $tests{principal} }) {
267 check_password($test);
271 # Test error for an unknown character class.
272 $krb5_conf = create_krb5_conf({ require_classes => 'bogus' });
273 local $ENV{KRB5_CONFIG} = $krb5_conf;
274 my $error_prefix = 'Cannot initialize strength checking';
275 ($status, $output, $err) = run_heimdal_strength('test', 'password');
276 is($status, 1, 'Bad character class (status)');
277 is($output, q{}, '...no output');
278 is($err, "$error_prefix: unknown character class bogus\n", '...correct error');
280 # Test a variety of configuration syntax errors in require_classes.
281 my @bad_classes = qw(
282 8 8bogus 8:bogus 4-:bogus 4-bogus 4-8bogus
284 my $bad_message = 'bad character class requirement in configuration';
285 for my $bad_class (@bad_classes) {
286 $krb5_conf = create_krb5_conf({ require_classes => $bad_class });
287 local $ENV{KRB5_CONFIG} = $krb5_conf;
288 ($status, $output, $err) = run_heimdal_strength('test', 'password');
289 is($status, 1, "Bad class specification '$bad_class' (status)");
290 is($output, q{}, '...no output');
291 is($err, "$error_prefix: $bad_message: $bad_class\n", '...correct error');
294 # Clean up our temporary krb5.conf file on any exit.
296 my $tmpdir = $ENV{BUILD} ? "$ENV{BUILD}/tmp" : 'tests/tmp';
297 my $config = "$tmpdir/krb5.conf";
299 unlink($config) or warn "Cannot remove $config\n";