]> eyrie.org Git - kerberos/krb5-strength.git/blob - tests/tools/heimdal-strength-t
290707f9177d0048b72f610826cb03a50e951d4e
[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 2009, 2012, 2013, 2014
7 #     The Board of Trustees of the Leland Stanford Junior University
8 #
9 # See LICENSE for licensing terms.
10
11 use 5.006;
12 use strict;
13 use warnings;
14
15 use lib "$ENV{SOURCE}/tap/perl";
16
17 use File::Copy qw(copy);
18 use Test::RRA qw(use_prereq);
19 use Test::RRA::Automake qw(test_file_path);
20
21 use_prereq('IPC::Run', 'run');
22 use_prereq('JSON');
23 use_prereq('Perl6::Slurp', 'slurp');
24 use_prereq('Test::More',   '0.87_01');
25
26 # Run the newly-built heimdal-strength command and return the status, output,
27 # and error output as a list.
28 #
29 # $principal - Principal to pass to the command
30 # $password  - Password to pass to the command
31 #
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) = @_;
36
37     # Build the input to the strength checking program.
38     my $in = "principal: $principal\n";
39     $in .= "new-password: $password\n";
40     $in .= "end\n";
41
42     # Find the newly-built password checking program.
43     my $program = test_file_path('../tools/heimdal-strength');
44
45     # Run the password strength checker.
46     my ($out, $err);
47     run([$program, $principal], \$in, \$out, \$err);
48     my $status = ($? >> 8);
49
50     # Return the results.
51     return ($status, $out, $err);
52 }
53
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.
57 #
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
64 #
65 # Returns: undef
66 #  Throws: Text exception on failure to run the test program
67 sub check_password {
68     my ($test_ref) = @_;
69     my $principal  = $test_ref->{principal};
70     my $password   = $test_ref->{password};
71
72     # Run the heimdal-strength command.
73     my ($status, $out, $err) = run_heimdal_strength($principal, $password);
74     chomp($out, $err);
75
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');
84     } else {
85         is($err, q{},        '...no errors');
86         is($out, 'APPROVED', '...approved');
87     }
88     return;
89 }
90
91 # Create a new krb5.conf file that includes arbitrary settings passed in via
92 # a hash reference.
93 #
94 # $settings_ref - Hash of keys and values to put into [appdefaults]
95 #
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) = @_;
100
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";
105
106     # Create a temporary directory for the new file.
107     if (!-d $tmpdir) {
108         mkdir($tmpdir, 0777) or die "Cannot create $tmpdir: $!\n";
109     }
110
111     # Start with the testing krb5.conf file shipped in the package.
112     copy($old, $new) or die "Cannot copy $old to $new: $!\n";
113
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";
121     }
122     print {$config} "    }\n"
123       or die "Cannot append to $new: $!\n";
124     close($config) or die "Cannot append to $new: $!\n";
125
126     # Return the path to the new file.
127     return $new;
128 }
129
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.
132 #
133 # $file - The file name containing the test data in JSON
134 #
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 {
138     my ($file) = @_;
139     my $path = test_file_path("data/passwords/$file");
140
141     # Load the test file data into memory.
142     my $testdata = slurp($path);
143
144     # Decode the JSON into Perl objects and return them.
145     my $json = JSON->new->utf8;
146     return $json->decode($testdata);
147 }
148
149 # Load the password tests from JSON.  Accumulate a total count of tests for
150 # the testing plan.
151 my (%tests, $count);
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});
156 }
157
158 # We run the principal tests three times, for CrackLib, CDB, and SQLite.
159 $count += 2 * scalar(@{ $tests{principal} });
160
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);
164
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';
168 my $krb5_conf = create_krb5_conf(
169     {
170         password_dictionary => "$datadir/dictionary",
171     }
172 );
173 local $ENV{KRB5_CONFIG} = $krb5_conf;
174
175 # Run the CrackLib password tests and based-on-principal tests from JSON.
176 note('CrackLib tests');
177 for my $test (@{ $tests{cracklib} }) {
178     check_password($test);
179 }
180 note('Generic tests with CrackLib');
181 for my $test (@{ $tests{principal} }) {
182     check_password($test);
183 }
184
185 # Install the krb5.conf file with a length restriction.
186 $krb5_conf = create_krb5_conf({ minimum_length => 12 });
187 local $ENV{KRB5_CONFIG} = $krb5_conf;
188
189 # Run the password length checks.
190 note('Password length checks');
191 for my $test (@{ $tests{length} }) {
192     check_password($test);
193 }
194
195 # Install the krb5.conf file for simple character class restrictions.
196 $krb5_conf = create_krb5_conf(
197     {
198         minimum_different       => 8,
199         require_ascii_printable => 'true',
200         require_non_letter      => 'true',
201     }
202 );
203 local $ENV{KRB5_CONFIG} = $krb5_conf;
204
205 # Run the simple character class tests.
206 note('Simple password character class checks');
207 for my $test (@{ $tests{letter} }) {
208     check_password($test);
209 }
210
211 # Install the krb5.conf file for complex character class restrictions.
212 my $classes = '8-19:lower,upper 8-15:digit 8-11:symbol';
213 $krb5_conf = create_krb5_conf({ require_classes => $classes });
214 local $ENV{KRB5_CONFIG} = $krb5_conf;
215
216 # Run the complex character class tests.
217 note('Complex password character class checks');
218 for my $test (@{ $tests{classes} }) {
219     check_password($test);
220 }
221
222 # Install the krb5.conf file with configuration pointing to the CDB
223 # dictionary.
224 my $cdb_database = test_file_path('data/wordlist.cdb');
225 $krb5_conf = create_krb5_conf({ password_dictionary_cdb => $cdb_database });
226 local $ENV{KRB5_CONFIG} = $krb5_conf;
227
228 # Check whether we were built with CDB support.  If so, run those tests.
229 my ($status, $output, $err) = run_heimdal_strength('test', 'password');
230 SKIP: {
231     if ($status == 1 && $err =~ m{ not [ ] built [ ] with [ ] CDB }xms) {
232         my $total = scalar(@{ $tests{cdb} }) + scalar(@{ $tests{principal} });
233         skip('not built with CDB support', $total * 3);
234     }
235
236     # Run the CDB and principal password tests from JSON.
237     note('CDB tests');
238     for my $test (@{ $tests{cdb} }) {
239         check_password($test);
240     }
241     note('Generic tests with CDB');
242     for my $test (@{ $tests{principal} }) {
243         check_password($test);
244     }
245 }
246
247 # Install the krb5.conf file with configuration pointing to the SQLite
248 # dictionary.
249 my $sqlite_database = test_file_path('data/wordlist.sqlite');
250 $krb5_conf = create_krb5_conf(
251     {
252         password_dictionary_sqlite => $sqlite_database,
253     }
254 );
255 local $ENV{KRB5_CONFIG} = $krb5_conf;
256
257 # Check whether we were built with SQLite support.  If so, run those tests.
258 ($status, $output, $err) = run_heimdal_strength('test', 'password');
259 SKIP: {
260     if ($status == 1 && $err =~ m{ not [ ] built [ ] with [ ] SQLite }xms) {
261         my $total = scalar(@{ $tests{sqlite} });
262         $total += scalar(@{ $tests{principal} });
263         skip('not built with SQLite support', $total * 3);
264     }
265
266     # Run the SQLite and principal password tests from JSON.
267     note('SQLite tests');
268     for my $test (@{ $tests{sqlite} }) {
269         check_password($test);
270     }
271     note('Generic tests with SQLite');
272     for my $test (@{ $tests{principal} }) {
273         check_password($test);
274     }
275 }
276
277 # Test error for an unknown character class.
278 $krb5_conf = create_krb5_conf({ require_classes => 'bogus' });
279 local $ENV{KRB5_CONFIG} = $krb5_conf;
280 my $error_prefix = 'Cannot initialize strength checking';
281 ($status, $output, $err) = run_heimdal_strength('test', 'password');
282 is($status, 1,   'Bad character class (status)');
283 is($output, q{}, '...no output');
284 is($err, "$error_prefix: unknown character class bogus\n", '...correct error');
285
286 # Test a variety of configuration syntax errors in require_classes.
287 my @bad_classes = qw(
288   8 8bogus 8:bogus 4-:bogus 4-bogus 4-8bogus
289 );
290 my $bad_message = 'bad character class requirement in configuration';
291 for my $bad_class (@bad_classes) {
292     $krb5_conf = create_krb5_conf({ require_classes => $bad_class });
293     local $ENV{KRB5_CONFIG} = $krb5_conf;
294     ($status, $output, $err) = run_heimdal_strength('test', 'password');
295     is($status, 1,   "Bad class specification '$bad_class' (status)");
296     is($output, q{}, '...no output');
297     is($err, "$error_prefix: $bad_message: $bad_class\n", '...correct error');
298 }
299
300 # Clean up our temporary krb5.conf file on any exit.
301 END {
302     my $tmpdir = $ENV{BUILD} ? "$ENV{BUILD}/tmp" : 'tests/tmp';
303     my $config = "$tmpdir/krb5.conf";
304     if (-f $config) {
305         unlink($config) or warn "Cannot remove $config\n";
306         rmdir($tmpdir);
307     }
308 }