]> eyrie.org Git - kerberos/krb5-strength.git/blobdiff - tools/heimdal-history
Update hash iterations in heimdal-history
[kerberos/krb5-strength.git] / tools / heimdal-history
index 1309e90c6dbb7de92c31f0ab444561f4c7128b6f..dbf8fc5890d34a515ec46beaeebc52a570f30b84 100755 (executable)
@@ -3,13 +3,9 @@
 # Password history via Heimdal external strength checking.
 #
 # This script is meant to be called via the Heimdal external password strength
-# checking interface and maintains per-user password history that also rejects
-# one-character permutations.  Password history is stored as Crypt::PBKDF2
-# hashes with random salt for each password.
-#
-# Written by Russ Allbery <eagle@eyrie.org>
-# Copyright 2013, 2014
-#     The Board of Trustees of the Leland Stanford Junior University
+# checking interface and maintains per-user password history.  Password
+# history is stored as Crypt::PBKDF2 hashes with random salt for each
+# password.
 
 ##############################################################################
 # Declarations and configuration
@@ -21,14 +17,14 @@ use strict;
 use warnings;
 
 use DB_File::Lock;
+use Const::Fast qw(const);
 use Crypt::PBKDF2;
 use Fcntl qw(O_CREAT O_RDWR);
 use File::Basename qw(basename);
 use Getopt::Long::Descriptive qw(describe_options);
 use IPC::Run qw(run);
-use JSON qw(encode_json decode_json);
+use JSON::MaybeXS qw(encode_json decode_json);
 use POSIX qw(setgid setuid);
-use Readonly;
 use Sys::Syslog qw(openlog syslog LOG_AUTH LOG_INFO LOG_WARNING);
 
 # The most convenient interface to Berkeley DB files is ties.
@@ -37,40 +33,44 @@ use Sys::Syslog qw(openlog syslog LOG_AUTH LOG_INFO LOG_WARNING);
 # The number of PBKDF2 iterations to use when hashing passwords.  This number
 # should be chosen so as to force the hash operation to take approximately 0.1
 # seconds on current hardware.
-Readonly my $HASH_ITERATIONS => 65536;
+const my $HASH_ITERATIONS => 45144;
 
 # Path to the history database.  Currently, this must be a Berkeley DB file in
 # the old DB_HASH format.  Keys will be principal names, and values will be a
 # JSON array of hashes.  Each hash will have two keys: timestamp, which holds
 # the seconds since UNIX epoch at which the history entry was stored, and
 # hash, which holds the Crypt::PBKDF2 LDAP-style password hash.
-Readonly my $HISTORY_PATH => '/var/lib/heimdal-history/history.db';
+const my $HISTORY_PATH => '/var/lib/heimdal-history/history.db';
 
 # User and group used to do all password history lookups and writes, assuming
 # that this program is invoked as root and can therefore change UID and GID.
-Readonly my $HISTORY_USER  => '_history';
-Readonly my $HISTORY_GROUP => '_history';
+const my $HISTORY_USER => '_history';
+const my $HISTORY_GROUP => '_history';
 
 # Path to the Berkeley DB file (DB_HASH format) that stores statistics on
 # password length of accepted passwords.  Each successful password validation
 # will increase the counter for that length.  This is read and written with
 # $HISTORY_USER and $HISTORY_GROUP.
-Readonly my $LENGTH_STATS_PATH => '/var/lib/heimdal-history/lengths.db';
+const my $LENGTH_STATS_PATH => '/var/lib/heimdal-history/lengths.db';
 
 # The message to return to the user if we reject the password because it was
 # found in the user's history.
-Readonly my $REJECT_MESSAGE => 'password was previously used';
+const my $REJECT_MESSAGE => 'Password was previously used';
 
 # The path to the external strength checking program to run.  This is done
 # first before checking history, and if it fails, that failure is returned as
 # the failure for this program.
-Readonly my $STRENGTH_PROGRAM => '/usr/bin/heimdal-strength';
+const my $STRENGTH_PROGRAM => '/usr/bin/heimdal-strength';
 
 # User and group used to do password strength checking.  Generally, this
 # doesn't require any privileges since the strength dictionary is
 # world-readable.
-Readonly my $STRENGTH_USER  => 'nobody';
-Readonly my $STRENGTH_GROUP => 'nogroup';
+const my $STRENGTH_USER => 'nobody';
+const my $STRENGTH_GROUP => 'nogroup';
+
+# Global boolean variable saying whether to log with syslog.  This is set
+# based on the presence of the -q (--quiet) command-line option.
+my $SYSLOG = 1;
 
 ##############################################################################
 # Utility functions
@@ -152,7 +152,8 @@ sub encode_log_message {
 # - error:     an error message explaining the anomalous situation
 #
 # Values containing whitespace are quoted with double quotes, with any
-# internal double quotes doubled.
+# internal double quotes doubled.  No logging will be done if $SYSLOG is
+# false.
 #
 # $principal - Principal for which we checked a password
 # $error     - The error message
@@ -160,10 +161,13 @@ sub encode_log_message {
 # Returns: undef
 sub log_error {
     my ($principal, $error) = @_;
+    if (!$SYSLOG) {
+        return;
+    }
     my $message = encode_log_message(
-        action    => 'check',
+        action => 'check',
         principal => $principal,
-        error     => $error,
+        error => $error,
     );
     syslog(LOG_WARNING, '%s', $message);
     return;
@@ -179,7 +183,8 @@ sub log_error {
 # - reason:    the reason for a rejection
 #
 # Values containing whitespace are quoted with double quotes, with any
-# internal double quotes doubled.
+# internal double quotes doubled.  No logging will be done if $SYSLOG is
+# false.
 #
 # $principal - Principal for which we checked a password
 # $result    - "accepted" or "rejected" per above
@@ -188,12 +193,15 @@ sub log_error {
 # Returns: undef
 sub log_result {
     my ($principal, $result, $reason) = @_;
+    if (!$SYSLOG) {
+        return;
+    }
 
     # Create the message.
     my %message = (
-        action    => 'check',
+        action => 'check',
         principal => $principal,
-        result    => $result,
+        result => $result,
     );
     if ($result eq 'rejected' && defined($reason)) {
         $message{reason} = $reason;
@@ -213,14 +221,16 @@ sub log_result {
 # PBKDF2 using SHA-2 as the underlying hash function.  As of version 0.133330,
 # this uses SHA-256.
 #
-# $password - Password to hash
+# $password   - Password to hash
+# $iterations - Optional iteration count, defaulting to $HASH_ITERATIONS
 #
 # Returns: Hash encoded in the LDAP-compatible Crypt::PBKDF2 format
 sub password_hash {
-    my ($password) = @_;
+    my ($password, $iterations) = @_;
+    $iterations //= $HASH_ITERATIONS;
     my $hasher = Crypt::PBKDF2->new(
         hash_class => 'HMACSHA2',
-        iterations => $HASH_ITERATIONS,
+        iterations => $iterations,
     );
     return $hasher->generate($password);
 }
@@ -262,6 +272,85 @@ sub is_in_history {
     return;
 }
 
+##############################################################################
+# Benchmarking
+##############################################################################
+
+# Perform a binary search for a number of hash iterations that makes password
+# hashing take the given target time on the current system.
+#
+# Assumptions:
+#
+# * The system load is low enough that this benchmark result is meaningful
+#   and not heavily influenced by other programs running on the system.  The
+#   binary search may be unstable if the system load is too variable.
+#
+# * The static "password" string used for benchmarking will exhibit similar
+#   performance to the statistically average password.
+#
+# Information about the iteration search process is printed to standard output
+# while the search runs.
+#
+# $target - The elapsed time, in real seconds, we're aiming for
+# $delta  - The permissible delta around the target time
+#
+# Returns: The number of hash iterations with that performance characteristic
+#  Throws: Text exception on failure to write to standard output
+sub find_iteration_count {
+    my ($target, $delta) = @_;
+    my $high = 0;
+    my $low = 0;
+
+    # A static password to use for benchmarking.
+    my $password = 'this is a benchmark';
+
+    # Start at the current configured iteration count.  If this doesn't take
+    # long enough, it becomes the new low mark and we try double that
+    # iteration count.  Otherwise, do binary search.
+    #
+    # We time twenty iterations each time, chosen because it avoids the
+    # warnings from Benchmark about too few iterations for a reliable count.
+    require Benchmark;
+    my $iterations = $HASH_ITERATIONS;
+    while (1) {
+        my $hash = sub { password_hash($password, $iterations) };
+        my $times = Benchmark::timethis(20, $hash, q{}, 'none');
+
+        # Extract the CPU time from the formatted time string.  This will be
+        # the total time for all of the iterations, so divide by the iteration
+        # count to recover the time per iteration.
+        my $report = Benchmark::timestr($times);
+        my ($time) = ($report =~ m{ ([\d.]+) [ ] CPU }xms);
+        $time = $time / 20;
+
+        # Tell the user what we discovered.
+        say {*STDOUT} "Performing $iterations iterations takes $time seconds"
+          or die "$0: cannot write to standard output: $!\n";
+
+        # If this is what we're looking for, we're done.
+        if (abs($time - $target) < $delta) {
+            last;
+        }
+
+        # Determine the new iteration target.
+        if ($time > $target) {
+            $high = $iterations;
+        } else {
+            $low = $iterations;
+        }
+        if ($time < $target && $high == 0) {
+            $iterations = $iterations * 2;
+        } else {
+            $iterations = int(($high + $low) / 2);
+        }
+    }
+
+    # Report the result and return it.
+    say {*STDOUT} "Use $iterations iterations"
+      or die "$0: cannot write to standard output: $!\n";
+    return $iterations;
+}
+
 ##############################################################################
 # Database
 ##############################################################################
@@ -288,8 +377,10 @@ sub check_history {
     {
         my %history;
         my $mode = O_CREAT | O_RDWR;
-        tie(%history, 'DB_File::Lock', [$path, $mode, oct(600)], 'write')
-          or die "$0: cannot open $path: $!\n";
+        tie(
+            %history, 'DB_File::Lock', $path, $mode, oct(600), $DB_HASH,
+            'write',
+        ) or die "$0: cannot open $path: $!\n";
         $history_json = $history{$principal};
     }
 
@@ -326,7 +417,7 @@ sub write_history {
     # Open and lock the database for write.
     my %history;
     my $mode = O_CREAT | O_RDWR;
-    tie(%history, 'DB_File::Lock', [$path, $mode, oct(600)], 'write')
+    tie(%history, 'DB_File::Lock', $path, $mode, oct(600), $DB_HASH, 'write')
       or die "$0: cannot open $path: $!\n";
 
     # Read the existing history.  If the existing history is corrupt, treat
@@ -375,7 +466,7 @@ sub update_length_counts {
     # Open and lock the database for write.
     my %lengths;
     my $mode = O_CREAT | O_RDWR;
-    tie(%lengths, 'DB_File::Lock', [$path, $mode, oct(600)], 'write')
+    tie(%lengths, 'DB_File::Lock', $path, $mode, oct(600), $DB_HASH, 'write')
       or return;
 
     # Write each of the hashes.
@@ -386,31 +477,32 @@ sub update_length_counts {
 }
 
 ##############################################################################
-# Heimdal password strength protocol
+# Heimdal password quality protocol
 ##############################################################################
 
-# Run another external password strength checker and return the results.  This
+# Run another external password quality checker and return the results.  This
 # allows us to chain to another program that handles the actual strength
 # checking prior to handling history.
 #
+# $path      - Password quality check program to run
 # $principal - Principal attempting to change their password
 # $password  - The new password
 #
-# Returns: Scalar context: true if the password was accepted, false otherwise
-#          List context: whether the password is okay, the exit status of the
-#            strength checking program, and the error message if the first
-#            element is false
-#  Throws: Text exception on failure to execute the program, or read or write
-#          from it or to it, or if it fails without an error
+# Returns: A list of three elements:
+#            - whether the password is okay
+#            - the exit status of the quality checking program
+#            - the error message if the first element is false
+# Throws: Text exception on failure to execute the program, or read or
+#         write from it or to it, or if it fails without an error
 sub strength_check {
-    my ($principal, $password) = @_;
+    my ($path, $principal, $password) = @_;
 
-    # Run the external strength checking program.  If we're root, we'll run it
+    # Run the external quality checking program.  If we're root, we'll run it
     # as the strength checking user and group.
     my $in = "principal: $principal\nnew-password: $password\nend\n";
     my $init = sub { drop_privileges($STRENGTH_USER, $STRENGTH_GROUP) };
     my ($out, $err);
-    run([$STRENGTH_PROGRAM, $principal], \$in, \$out, \$err, init => $init);
+    run([$path, $principal], \$in, \$out, \$err, init => $init);
     my $status = ($? >> 8);
 
     # Check the results.
@@ -426,10 +518,10 @@ sub strength_check {
     }
 
     # Return the results.
-    return wantarray ? ($okay, $err, $status) : $okay;
+    return ($okay, $err, $status);
 }
 
-# Read a Heimdal external password strength checking request from the provided
+# Read a Heimdal external password quality checking request from the provided
 # file handle and return the principal (ignored for our application) and the
 # password.
 #
@@ -445,8 +537,7 @@ sub strength_check {
 #
 # $fh - File handle from which to read
 #
-# Returns: Scalar context: the password
-#          List context: a list of the password and the principal
+# Returns: List of the password and the principal
 #  Throws: Text exception on any protocol violations or IO errors
 sub read_change_data {
     my ($fh) = @_;
@@ -478,9 +569,7 @@ sub read_change_data {
     }
 
     # Return the results.
-    my $password  = $data{'new-password'};
-    my $principal = $data{principal};
-    return wantarray ? ($password, $principal) : $password;
+    return ($data{'new-password'}, $data{principal});
 }
 
 ##############################################################################
@@ -495,14 +584,19 @@ my $fullpath = $0;
 local $0 = basename($0);
 
 # Parse the argument list.
+#<<<
 my ($opt, $usage) = describe_options(
     '%c %o',
-    ['database|d=s', 'Path to the history database, overriding the default'],
-    ['help|h',       'Print usage message and exit'],
-    ['manual|man|m', 'Print full manual and exit'],
-    ['stats|S=s',    'Path to hash of length statistics'],
-    ['strength|s=s', 'Path to strength checking program to run'],
+    ['benchmark|b=f', 'Benchmark hash iterations for this target time'],
+    ['check-only|c',  'Check password history without updating database'],
+    ['database|d=s',  'Path to the history database, overriding the default'],
+    ['help|h',        'Print usage message and exit'],
+    ['manual|man|m',  'Print full manual and exit'],
+    ['quiet|q',       'Suppress logging to syslog'],
+    ['stats|S=s',     'Path to database of length statistics'],
+    ['strength|s=s',  'Path to strength checking program to run'],
 );
+#>>>
 if ($opt->help) {
     print {*STDOUT} $usage->text
       or die "$0: cannot write to standard output: $!\n";
@@ -513,16 +607,28 @@ if ($opt->help) {
     exec('perldoc', '-t', $fullpath);
 }
 my $database = $opt->database || $HISTORY_PATH;
-my $stats_db = $opt->stats    || $LENGTH_STATS_PATH;
+my $stats_db = $opt->stats || $LENGTH_STATS_PATH;
+my $strength = $opt->strength || $STRENGTH_PROGRAM;
+
+# If asked to do benchmarking, ignore other arguments and just do that.
+# Currently, we hard-code a 0.005-second granularity on our binary search.
+if ($opt->benchmark) {
+    find_iteration_count($opt->benchmark, 0.005);
+    exit(0);
+}
 
 # Open syslog for result reporting.
-openlog($0, 'pid', LOG_AUTH);
+if ($opt->quiet) {
+    $SYSLOG = 0;
+} else {
+    openlog($0, 'pid', LOG_AUTH);
+}
 
 # Read the principal and password that we're supposed to check.
 my ($password, $principal) = read_change_data(\*STDIN);
 
 # Delegate to the external strength checking program.
-my ($okay, $error, $status) = strength_check($principal, $password);
+my ($okay, $error, $status) = strength_check($strength, $principal, $password);
 if (!$okay) {
     log_result($principal, 'rejected', $error);
     warn "$error\n";
@@ -542,10 +648,12 @@ if (check_history($database, $principal, $password)) {
 # The password is accepted.  Record it, update the length counter, and return
 # success.
 log_result($principal, 'accepted');
-write_history($database, $principal, $password);
+if (!$opt->check_only) {
+    write_history($database, $principal, $password);
+    update_length_counts($stats_db, length($password));
+}
 say {*STDOUT} 'APPROVED'
   or die "$0: cannot write to standard output: $!\n";
-update_length_counts($stats_db, length($password));
 exit(0);
 
 __END__
@@ -555,9 +663,10 @@ __END__
 ##############################################################################
 
 =for stopwords
-heimdal-history heimdal-strength Heimdal -hm BerkeleyDB timestamps POSIX
+heimdal-history heimdal-strength Heimdal -chmq BerkeleyDB timestamps POSIX
 whitespace API Allbery sublicense MERCHANTABILITY NONINFRINGEMENT syslog
-pseudorandom JSON LDAP-compatible PBKDF2 SHA-256
+pseudorandom JSON LDAP-compatible PBKDF2 SHA-256 KDC SPDX-License-Identifier
+MIT
 
 =head1 NAME
 
@@ -565,53 +674,62 @@ heimdal-history - Password history via Heimdal external strength checking
 
 =head1 SYNOPSIS
 
-B<heimdal-history> [B<-hm>] [B<-d> I<database>] [B<-S> I<length-stats-db>]
-    [B<-s> I<strength-program>] [B<principal>]
+B<heimdal-history> [B<-chmq>] [B<-b> I<target-time>] [B<-d> I<database>]
+    [B<-S> I<length-stats-db>] [B<-s> I<strength-program>] [B<principal>]
 
 =head1 DESCRIPTION
 
-B<heimdal-history> is an implementation of password history via the
-Heimdal external password strength checking interface.  It stores separate
-history for each principal, hashed using Crypt::PBKDF2 with
-randomly-generated salt.  (The randomness is from a weak pseudorandom
-number generator, not strongly random.)
+B<heimdal-history> is an implementation of password history via the Heimdal
+external password strength checking interface.  It stores separate history for
+each principal, hashed using Crypt::PBKDF2 with randomly-generated salt.  (The
+randomness is from a weak pseudorandom number generator, not strongly random.)
+Password history is stored indefinitely (implementing infinite history); older
+password hashes are never removed by this program.
 
 Password history is stored in a BerkeleyDB DB_HASH file.  The key is the
-principal.  The value is a JSON array of objects, each of which has two
-keys.  C<timestamp> contains the time when the history entry was added (in
-POSIX seconds since UNIX epoch), and C<hash> contains the hash of a
-previously-used password in the Crypt::PBKDF2 LDAP-compatible format.
-Passwords are hashed using PBKDF2 (from PKCS#5) with SHA-256 as the
-underlying hash function using a number of rounds configured in this
-script.  See L<Crypt::PBKDF2> for more information.
-
-B<heimdal-history> also checks password strength before checking history.
-It does so by invoking another program that also uses the Heimdal external
+principal.  The value is a JSON array of objects, each of which has two keys.
+C<timestamp> contains the time when the history entry was added (in POSIX
+seconds since UNIX epoch), and C<hash> contains the hash of a previously-used
+password in the Crypt::PBKDF2 LDAP-compatible format.  Passwords are hashed
+using PBKDF2 (from PKCS#5) with SHA-256 as the underlying hash function using
+a number of rounds configured in this script.  See L<Crypt::PBKDF2> for more
+information.
+
+B<heimdal-history> also checks password strength before checking history.  It
+does so by invoking another program that also uses the Heimdal external
 password strength checking interface.  By default, it runs
-B</usr/bin/heimdal-strength>.  Only if that program approves the password
-does it hash it and check history.
+B</usr/bin/heimdal-strength>.  Only if that program approves the password does
+it hash it and check history.
+
+For more information on how to set up password history, see L</CONFIGURATION>
+below.
 
-As with any implementation of the Heimdal external password strength
-checking protocol, B<heimdal-history> expects, on standard input:
+As with any implementation of the Heimdal external password strength checking
+protocol, B<heimdal-history> expects, on standard input:
 
     principal: <principal>
     new-password: <password>
     end
 
 (with no leading whitespace).  <principal> is the principal changing its
-password (passed to the other password strength checking program but
-otherwise unused here), and <password> is the new password.  There must
-be exactly one space after the colon.  Any subsequent spaces are taken to
-be part of the principal or password.
+password (passed to the other password strength checking program but otherwise
+unused here), and <password> is the new password.  There must be exactly one
+space after the colon.  Any subsequent spaces are taken to be part of the
+principal or password.
+
+If the password is accepted, B<heimdal-history> will assume that it will be
+used and will update the history database to record the new password.  It will
+also update the password length statistics database to account for the new
+password.
 
-If invoked as root, B<heimdal-history> will run the external strength
-checking program as user C<nobody> and group C<nogroup>, and will check
-and write to the history database as user C<_history> and group
-C<_history>.  These users must exist on the system if it is run as root.
+If invoked as root, B<heimdal-history> will run the external strength checking
+program as user C<nobody> and group C<nogroup>, and will check and write to
+the history database as user C<_history> and group C<_history>.  These users
+must exist on the system if it is run as root.
 
-The result of each password check will be logged to syslog (priority
-LOG_INFO, facility LOG_AUTH).  Each log line will be a set of key/value
-pairs in the format C<< I<key>=I<value> >>.  The keys are:
+The result of each password check will be logged to syslog (priority LOG_INFO,
+facility LOG_AUTH).  Each log line will be a set of key/value pairs in the
+format C<< I<key>=I<value> >>.  The keys are:
 
 =over 4
 
@@ -625,12 +743,12 @@ The principal for which a password was checked.
 
 =item error
 
-An internal error message that did not stop the history check, but which
-may indicate that something is wrong with the history database (such as
-corrupted entries or invalid hashes).  If this key is present, neither
-C<result> nor C<reason> will be present.  There will be a subsequent log
-message from the same invocation giving the final result of the history
-check (assuming B<heimdal-history> doesn't exit with a fatal error).
+An internal error message that did not stop the history check, but which may
+indicate that something is wrong with the history database (such as corrupted
+entries or invalid hashes).  If this key is present, neither C<result> nor
+C<reason> will be present.  There will be a subsequent log message from the
+same invocation giving the final result of the history check (assuming
+B<heimdal-history> doesn't exit with a fatal error).
 
 =item result
 
@@ -642,19 +760,36 @@ If the password was rejected, the reason for the rejection.
 
 =back
 
-The value will be surrounded with double quotes if it contains a double
-quote or space.  Any double quotes in the value will be doubled, so C<">
-becomes C<"">.
+The value will be surrounded with double quotes if it contains a double quote
+or space.  Any double quotes in the value will be doubled, so C<"> becomes
+C<"">.
 
 =head1 OPTIONS
 
 =over 4
 
+=item B<-b> I<target-time>, B<--benchmark>=I<target-time>
+
+Do not do a password history check.  Instead, benchmark the hash algorithm
+with various possible iteration counts and find an iteration count that
+results in I<target-time> seconds of computation time required to hash a
+password (which should be a real number).  A result will be considered
+acceptable if it is within 0.005 seconds of the target time.  The results will
+be printed to standard output and then B<heimdal-history> will exit
+successfully.
+
+=item B<-c>, B<--check-only>
+
+Check password history and password strength and print the results as normal,
+but do not update the history or length statistics databases.  This is a
+read-only mode of operation that will not make any changes to the underlying
+database, only report if a password would currently be accepted.
+
 =item B<-d> I<database>, B<--database>=I<database>
 
 Use I<database> as the history database file instead of the default
-(F</var/lib/heimdal-history/history.db>).  Primarily used for testing,
-since Heimdal won't pass this argument.
+(F</var/lib/heimdal-history/history.db>).  Primarily used for testing, since
+Heimdal won't pass this argument.
 
 =item B<-h>, B<--help>
 
@@ -664,29 +799,64 @@ Print a short usage message and exit.
 
 Display this manual and exit.
 
+=item B<-q>, B<--quiet>
+
+Suppress logging to syslog and only return the results on standard output and
+standard error.  Primarily used for testing, since Heimdal won't pass this
+argument.
+
 =item B<-S> I<length-stats-db>, B<--stats>=I<length-stats-db>
 
 Use I<length-stats-db> as the database file for password length statistics
-instead of the default (F</var/lib/heimdal-history/lengths.db>).
-Primarily used for testing, since Heimdal won't pass this argument.
+instead of the default (F</var/lib/heimdal-history/lengths.db>).  Primarily
+used for testing, since Heimdal won't pass this argument.
 
 =item B<-s> I<strength-program>, B<--strength>=I<strength-program>
 
-Run I<strength-program> as the external strength-checking program instead
-of the default (F</usr/bin/heimdal-strength>).  Primarily used for
-testing, since Heimdal won't pass this argument.
+Run I<strength-program> as the external strength-checking program instead of
+the default (F</usr/bin/heimdal-strength>).  Primarily used for testing, since
+Heimdal won't pass this argument.
 
 =back
 
+=head1 CONFIGURATION
+
+Additional setup is required to use this history implementation with your
+Heimdal KDC.
+
+First, ensure that its dependencies are installed, and then examine the local
+configuration settings at the top of the B<heimdal-history> program.  By
+default, it requires a C<_history> user and C<_history> group be present on
+the system, and all history information will be read and written as that user
+and group.  It also requires a C<nobody> user and C<nogroup> group to be
+present (this should be the default with most variants of UNIX), and all
+strength checking will be done as that user and group.  It uses various files
+in F</var/lib/heimdal-history> to store history and statistical information by
+default, so if using the defaults, create that directory and ensure it is
+writable by the C<_history> user.
+
+Once that setup is done, change your C<[password_quality]> configuration in
+F<krb5.conf> or F<kdc.conf> to:
+
+    [password_quality]
+        policies         = external-check
+        external_program = /usr/local/bin/heimdal-history
+
+The B<heimdal-history> program will automatically also run B<heimdal-strength>
+as well, looking for it in F</usr/bin>.  Change the C<$STRENGTH_PROGRAM>
+setting at the top of the script if you have that program in a different
+location.  You should continue to configure B<heimdal-strength> as if you were
+running it directly.
+
 =head1 RETURN STATUS
 
-On approval of the password, B<heimdal-history> will print C<APPROVED> and
-newline to standard output and exit with status 0.
+On approval of the password, B<heimdal-history> will print C<APPROVED> and a
+newline to standard output and exit with status 0.
 
-If the password is rejected by the strength checking program or if it (or
-a version with a single character removed) matches one of the hashes stored
-in the password history, B<heimdal-history> will print the reason for
-rejection to standard error and exit with status 0.
+If the password is rejected by the strength checking program or if it (or a
+version with a single character removed) matches one of the hashes stored in
+the password history, B<heimdal-history> will print the reason for rejection
+to standard error and exit with status 0.
 
 On any internal error, B<heimdal-history> will print the error to standard
 error and exit with a non-zero status.
@@ -697,36 +867,35 @@ error and exit with a non-zero status.
 
 =item F</usr/bin/heimdal-strength>
 
-The default password strength checking program.  This program must follow
-the Heimdal external password strength checking API.
+The default password strength checking program.  This program must follow the
+Heimdal external password strength checking API.
 
 =item F</var/lib/heimdal-history/history.db>
 
-The default database path.  If B<heimdal-strength> is run as root, this
-file needs to be readable and writable by user C<_history> and group
-C<_history>.  If it doesn't exist, it will be created with mode 0600.
+The default database path.  If B<heimdal-strength> is run as root, this file
+needs to be readable and writable by user C<_history> and group C<_history>.
+If it doesn't exist, it will be created with mode 0600.
 
 =item F</var/lib/heimdal-history/history.db.lock>
 
-The lock file used to synchronize access to the history database.  As with
-the history database, if B<heimdal-strength> is run as root, this file
-needs to be readable and writable by user C<_history> and group
-C<_history>.
+The lock file used to synchronize access to the history database.  As with the
+history database, if B<heimdal-strength> is run as root, this file needs to be
+readable and writable by user C<_history> and group C<_history>.
 
 =item F</var/lib/heimdal-history/lengths.db>
 
-The default length statistics path, which will be a BerkeleyDB DB_HASH
-file of password lengths to counts of passwords with that length.  If
+The default length statistics path, which will be a BerkeleyDB DB_HASH file of
+password lengths to counts of passwords with that length.  If
 B<heimdal-strength> is run as root, this file needs to be readable and
-writable by user C<_history> and group C<_history>.  If it doesn't exist,
-it will be created with mode 0600.
+writable by user C<_history> and group C<_history>.  If it doesn't exist, it
+will be created with mode 0600.
 
 =item F</var/lib/heimdal-history/lengths.db.lock>
 
-The lock file used to synchronize access to the length statistics
-database.  As with the length statistics database, if B<heimdal-strength>
-is run as root, this file needs to be readable and writable by user
-C<_history> and group C<_history>.
+The lock file used to synchronize access to the length statistics database.
+As with the length statistics database, if B<heimdal-strength> is run as root,
+this file needs to be readable and writable by user C<_history> and group
+C<_history>.
 
 =back
 
@@ -736,29 +905,37 @@ Russ Allbery <eagle@eyrie.org>
 
 =head1 COPYRIGHT AND LICENSE
 
-Copyright 2013, 2014 The Board of Trustees of the Leland Stanford Junior
+Copyright 2016-2017, 2020, 2023 Russ Allbery <eagle@eyrie.org>
+
+Copyright 2013-2014 The Board of Trustees of the Leland Stanford Junior
 University
 
-Permission is hereby granted, free of charge, to any person obtaining a
-copy of this software and associated documentation files (the "Software"),
-to deal in the Software without restriction, including without limitation
-the rights to use, copy, modify, merge, publish, distribute, sublicense,
-and/or sell copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
 
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
-THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-DEALINGS IN THE SOFTWARE.
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+SPDX-License-Identifier: MIT
 
 =head1 SEE ALSO
 
 L<Crypt::PBKDF2>, L<heimdal-strength(1)>
 
 =cut
+
+# Local Variables:
+# copyright-at-end-flag: t
+# End: