#!/usr/bin/perl # # Test suite for basic Heimdal external strength checking functionality. # # Written by Russ Allbery # Copyright 2009, 2012, 2013 # The Board of Trustees of the Leland Stanford Junior University # # See LICENSE for licensing terms. use 5.006; use strict; use warnings; use lib "$ENV{SOURCE}/tap/perl"; use File::Copy qw(copy); use Test::RRA qw(use_prereq); use Test::RRA::Automake qw(test_file_path); use_prereq('IPC::Run', 'run'); use_prereq('JSON'); use_prereq('Perl6::Slurp', 'slurp'); use_prereq('Test::More', '0.87_01'); # Run the newly-built heimdal-strength command and return the status, output, # and error output as a list. # # $principal - Principal to pass to the command # $password - Password to pass to the command # # Returns: The exit status, standard output, and standard error as a list # Throws: Text exception on failure to run the test program sub run_heimdal_strength { my ($principal, $password) = @_; # Build the input to the strength checking program. my $in = "principal: $principal\n"; $in .= "new-password: $password\n"; $in .= "end\n"; # Find the newly-built password checking program. my $program = test_file_path('../tools/heimdal-strength'); # Run the password strength checker. my ($out, $err); run([$program, $principal], \$in, \$out, \$err); my $status = ($? >> 8); # Return the results. return ($status, $out, $err); } # Run the newly-built heimdal-strength command to check a password and reports # the results using Test::More. This uses the standard protocol for Heimdal # external password strength checking programs. # # $test_ref - Reference to hash of test parameters # name - The name of the test case # principal - The principal changing its password # password - The new password # status - If present, the exit status (otherwise, it should be 0) # error - If present, the expected rejection error # # Returns: undef # Throws: Text exception on failure to run the test program sub check_password { my ($test_ref) = @_; my $principal = $test_ref->{principal}; my $password = $test_ref->{password}; # Run the heimdal-strength command. my ($status, $out, $err) = run_heimdal_strength($principal, $password); chomp($out, $err); # Check the results. If there is an error in the password, it should come # on standard error; otherwise, standard output should be APPROVED. If # there is a non-zero exit status, we expect the error on standard error # and use that field to check for system errors. is($status, $test_ref->{status} || 0, "$test_ref->{name} (status)"); if (defined($test_ref->{error})) { is($err, $test_ref->{error}, '...error message'); is($out, q{}, '...no output'); } else { is($err, q{}, '...no errors'); is($out, 'APPROVED', '...approved'); } return; } # Create a new krb5.conf file that includes arbitrary settings passed in via # a hash reference. # # $settings_ref - Hash of keys and values to put into [appdefaults] # # Returns: Path to the new krb5.conf file # Throws: Text exception if the new krb5.conf file cannot be created sub create_krb5_conf { my ($settings_ref) = @_; # Paths for krb5.conf creation. my $old = test_file_path('data/krb5.conf'); my $tmpdir = $ENV{BUILD} ? "$ENV{BUILD}/tmp" : 'tests/tmp'; my $new = "$tmpdir/krb5.conf"; # Create a temporary directory for the new file. if (!-d $tmpdir) { mkdir($tmpdir, 0777) or die "Cannot create $tmpdir: $!\n"; } # Start with the testing krb5.conf file shipped in the package. copy($old, $new) or die "Cannot copy $old to $new: $!\n"; # Append the local configuration. open(my $config, '>>', $new) or die "Cannot append to $new: $!\n"; print {$config} "\n[appdefaults]\n krb5-strength = {\n" or die "Cannot append to $new: $!\n"; for my $key (keys %{$settings_ref}) { print {$config} q{ } x 8, $key, ' = ', $settings_ref->{$key}, "\n" or die "Cannot append to $new: $!\n"; } print {$config} " }\n" or die "Cannot append to $new: $!\n"; close($config) or die "Cannot append to $new: $!\n"; # Return the path to the new file. return $new; } # Load a set of password test cases and return them as a list. The given file # name is relative to data/passwords in the test suite. # # $file - The file name containing the test data in JSON # # Returns: List of anonymous hashes representing password test cases # Throws: Text exception on failure to load the test data sub load_password_tests { my ($file) = @_; my $path = test_file_path("data/passwords/$file"); # Load the test file data into memory. my $testdata = slurp($path); # Decode the JSON into Perl objects and return them. my $json = JSON->new->utf8; return $json->decode($testdata); } # Load the password tests from JSON. Accumulate a total count of tests for # the testing plan. my (%tests, $count); for my $type (qw(cdb classes cracklib length letter principal)) { my $tests = load_password_tests("$type.json"); $tests{$type} = $tests; $count += scalar(@{$tests}); } # We run the principal tests twice, once for CrackLib and once for CDB. $count += scalar(@{ $tests{principal} }); # We can now calculate our plan based on three tests for each password test, # plus 21 additional tests for error handling. plan(tests => $count * 3 + 21); # Install the krb5.conf file with a configuration pointing to the test # CrackLib dictionary. my $datadir = $ENV{BUILD} ? "$ENV{BUILD}/data" : 'tests/data'; my $krb5_conf = create_krb5_conf({ password_dictionary => "$datadir/dictionary" }); local $ENV{KRB5_CONFIG} = $krb5_conf; # Run the CrackLib password tests and based-on-principal tests from JSON. note('CrackLib tests'); for my $test (@{ $tests{cracklib} }) { check_password($test); } note('Generic tests with CrackLib'); for my $test (@{ $tests{principal} }) { check_password($test); } # Install the krb5.conf file with a length restriction. $krb5_conf = create_krb5_conf({ minimum_length => 12 }); local $ENV{KRB5_CONFIG} = $krb5_conf; # Run the password length checks. note('Password length checks'); for my $test (@{ $tests{length} }) { check_password($test); } # Install the krb5.conf file for simple character class restrictions. $krb5_conf = create_krb5_conf( { require_ascii_printable => 'true', require_non_letter => 'true', } ); local $ENV{KRB5_CONFIG} = $krb5_conf; # Run the simple character class tests. note('Simple password character class checks'); for my $test (@{ $tests{letter} }) { check_password($test); } # Install the krb5.conf file for complex character class restrictions. my $classes = '8-19:lower,upper 8-15:digit 8-11:symbol'; $krb5_conf = create_krb5_conf({ require_classes => $classes }); local $ENV{KRB5_CONFIG} = $krb5_conf; # Run the complex character class tests. note('Complex password character class checks'); for my $test (@{ $tests{classes} }) { check_password($test); } # Install the krb5.conf file with configuration pointing to the CDB # dictionary. my $cdb_database = test_file_path('data/wordlist.cdb'); $krb5_conf = create_krb5_conf({ password_dictionary_cdb => $cdb_database }); local $ENV{KRB5_CONFIG} = $krb5_conf; # Check whether we were built with CDB support. If so, run those tests. my ($status, $output, $err) = run_heimdal_strength('test', 'password'); SKIP: { if ($status == 1 && $err =~ m{ not [ ] built [ ] with [ ] CDB }xms) { my $total = scalar(@{ $tests{cdb} }) + scalar(@{ $tests{principal} }); skip('not built with CDB support', $total * 3); } # Run the CDB and principal password tests from JSON. note('CDB tests'); for my $test (@{ $tests{cdb} }) { check_password($test); } note('Generic tests with CDB'); for my $test (@{ $tests{principal} }) { check_password($test); } } # Test error for an unknown character class. $krb5_conf = create_krb5_conf({ require_classes => 'bogus' }); local $ENV{KRB5_CONFIG} = $krb5_conf; my $error_prefix = 'Cannot initialize strength checking'; ($status, $output, $err) = run_heimdal_strength('test', 'password'); is($status, 1, 'Bad character class (status)'); is($output, q{}, '...no output'); is($err, "$error_prefix: unknown character class bogus\n", '...correct error'); # Test a variety of configuration syntax errors in require_classes. my @bad_classes = qw( 8 8bogus 8:bogus 4-:bogus 4-bogus 4-8bogus ); my $bad_message = 'bad character class requirement in configuration'; for my $bad_class (@bad_classes) { $krb5_conf = create_krb5_conf({ require_classes => $bad_class }); local $ENV{KRB5_CONFIG} = $krb5_conf; ($status, $output, $err) = run_heimdal_strength('test', 'password'); is($status, 1, "Bad class specification '$bad_class' (status)"); is($output, q{}, '...no output'); is($err, "$error_prefix: $bad_message: $bad_class\n", '...correct error'); } # Clean up our temporary krb5.conf file on any exit. END { my $tmpdir = $ENV{BUILD} ? "$ENV{BUILD}/tmp" : 'tests/tmp'; my $config = "$tmpdir/krb5.conf"; if (-f $config) { unlink($config) or warn "Cannot remove $config\n"; rmdir($tmpdir); } }