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