components/krb5/patches/075-ldap-pw-hist.patch
author Tomas Kuthan <tomas.kuthan@oracle.com>
Fri, 04 Nov 2016 09:14:49 -0700
changeset 7246 b3414fa83399
permissions -rw-r--r--
18794793 MIT 1.8.3 resync removed pw history when using LDAP as a backend

#
# This patch cherry-picks Password history in LDAP KDB plugin feature from
# MIT krb5 1.15.
#
# It is 1-1 port of the following changesets:
#    44ad57d8d38efc944f64536354435f5b721c0ee0
#    d7f91ac2f6655e77bb3658c2c8cc6132f958a340
#    b46cce2ea8c0841f7f93db73eefcd180c87a3eae
#    9526953f36b39323ec07448a5f218d27c6f1c76f
#
# Patch source: upstream
#
# When upgrading to MIT krb5 1.15 this patch will be dropped.
#
--- a/src/include/kdb.h
+++ b/src/include/kdb.h
@@ -1,6 +1,6 @@
 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
 /*
- * Copyright 1990,1991 by the Massachusetts Institute of Technology.
+ * Copyright 1990, 1991, 2016 by the Massachusetts Institute of Technology.
  * All Rights Reserved.
  *
  * Export of this software from the United States of America may
@@ -209,6 +209,8 @@ typedef struct _krb5_db_entry_new {
 
     krb5_principal        princ;                /* Length, data */
     krb5_tl_data        * tl_data;              /* Linked list */
+
+    /* key_data must be sorted by kvno in descending order. */
     krb5_key_data       * key_data;             /* Array */
 } krb5_db_entry;
 
@@ -683,6 +685,19 @@ krb5_error_code krb5_db_check_allowed_to
                                                   const krb5_db_entry *server,
                                                   krb5_const_principal proxy);
 
+/**
+ * Sort an array of @a krb5_key_data keys in descending order by their kvno.
+ * Key data order within a kvno is preserved.
+ *
+ * @param key_data
+ *     The @a krb5_key_data array to sort.  This is sorted in place so the
+ *     array will be modified.
+ * @param key_data_length
+ *     The length of @a key_data.
+ */
+void
+krb5_dbe_sort_key_data(krb5_key_data *key_data, size_t key_data_length);
+
 /* default functions. Should not be directly called */
 /*
  *   Default functions prototype
--- a/src/lib/kadm5/admin.h
+++ b/src/lib/kadm5/admin.h
@@ -113,7 +113,7 @@ typedef long            kadm5_ret_t;
 #define KADM5_RANDKEY_USED      0x100000
 #endif
 #define KADM5_LOAD              0x200000
-#define KADM5_NOKEY             0x400000
+#define KADM5_KEY_HIST          0x400000
 
 /* all but KEY_DATA, TL_DATA, LOAD */
 #define KADM5_PRINCIPAL_NORMAL_MASK 0x41ffff
--- a/src/lib/kadm5/srv/svr_principal.c
+++ b/src/lib/kadm5/srv/svr_principal.c
@@ -1084,6 +1084,16 @@ check_pw_reuse(krb5_context context,
     return(0);
 }
 
+static void
+free_history_entry(krb5_context context, osa_pw_hist_ent *hist)
+{
+    int i;
+
+    for (i = 0; i < hist->n_key_data; i++)
+        krb5_free_key_data_contents(context, &hist->key_data[i]);
+    free(hist->key_data);
+}
+
 /*
  * Function: create_history_entry
  *
@@ -1097,7 +1107,7 @@ check_pw_reuse(krb5_context context,
  *      hist_key        (r) history keyblock to encrypt key data with
  *      n_key_data      (r) number of elements in key_data
  *      key_data        (r) keys to add to the history entry
- *      hist            (w) history entry to fill in
+ *      hist_out        (w) history entry to fill in
  *
  * Effects:
  *
@@ -1109,45 +1119,62 @@ check_pw_reuse(krb5_context context,
 static
 int create_history_entry(krb5_context context,
                          krb5_keyblock *hist_key, int n_key_data,
-                         krb5_key_data *key_data, osa_pw_hist_ent *hist)
+                         krb5_key_data *key_data, osa_pw_hist_ent *hist_out)
 {
-    krb5_error_code ret;
+    int i;
+    krb5_error_code ret = 0;
     krb5_keyblock key;
     krb5_keysalt salt;
-    int i;
+    krb5_ui_2 kvno;
+    osa_pw_hist_ent hist;
+
+    hist_out->key_data = NULL;
+    hist_out->n_key_data = 0;
+
+    if (n_key_data < 0)
+        return EINVAL;
+
+    memset(&key, 0, sizeof(key));
+    memset(&hist, 0, sizeof(hist));
+
+    if (n_key_data == 0)
+        goto cleanup;
 
-    hist->key_data = k5calloc(n_key_data, sizeof(krb5_key_data), &ret);
-    if (hist->key_data == NULL)
-        return ret;
+    hist.key_data = k5calloc(n_key_data, sizeof(krb5_key_data), &ret);
+    if (hist.key_data == NULL)
+        goto cleanup;
+
+    /* We only want to store the most recent kvno, and key_data should already
+     * be sorted in descending order by kvno. */
+    kvno = key_data[0].key_data_kvno;
 
     for (i = 0; i < n_key_data; i++) {
-        ret = krb5_dbe_decrypt_key_data(context, NULL, &key_data[i], &key,
+        if (key_data[i].key_data_kvno < kvno)
+            break;
+        ret = krb5_dbe_decrypt_key_data(context, NULL,
+                                        &key_data[i], &key,
                                         &salt);
         if (ret)
-            return ret;
+            goto cleanup;
 
         ret = krb5_dbe_encrypt_key_data(context, hist_key, &key, &salt,
                                         key_data[i].key_data_kvno,
-                                        &hist->key_data[i]);
+                                        &hist.key_data[hist.n_key_data]);
         if (ret)
-            return ret;
-
+            goto cleanup;
+        hist.n_key_data++;
         krb5_free_keyblock_contents(context, &key);
         /* krb5_free_keysalt(context, &salt); */
     }
 
-    hist->n_key_data = n_key_data;
-    return 0;
-}
-
-static
-void free_history_entry(krb5_context context, osa_pw_hist_ent *hist)
-{
-    int i;
-
-    for (i = 0; i < hist->n_key_data; i++)
-        krb5_free_key_data_contents(context, &hist->key_data[i]);
-    free(hist->key_data);
+    *hist_out = hist;
+    hist.n_key_data = 0;
+    hist.key_data = NULL;
+
+cleanup:
+    krb5_free_keyblock_contents(context, &key);
+    free_history_entry(context, &hist);
+    return ret;
 }
 
 /*
@@ -1526,11 +1553,14 @@ kadm5_chpass_principal_3(void *server_ha
                     goto done;
             }
 
-            ret = add_to_history(handle->context, hist_kvno, &adb, &pol,
-                                 &hist);
-            if (ret)
-                goto done;
-            hist_added = 1;
+            /* Don't save empty history. */
+            if (hist.n_key_data > 0) {
+                ret = add_to_history(handle->context, hist_kvno, &adb, &pol,
+                                     &hist);
+                if (ret)
+                    goto done;
+                hist_added = 1;
+            }
         }
 
         if (pol.pw_max_life)
@@ -1582,6 +1612,9 @@ kadm5_chpass_principal_3(void *server_ha
         KADM5_FAIL_AUTH_COUNT;
     /* | KADM5_CPW_FUNCTION */
 
+    if (hist_added)
+        kdb->mask |= KADM5_KEY_HIST;
+
     ret = k5_kadm5_hook_chpass(handle->context, handle->hook_handles,
                                KADM5_HOOK_STAGE_PRECOMMIT, principal, keepold,
                                new_n_ks_tuple, new_ks_tuple, password);
--- a/src/lib/kdb/kdb5.c
+++ b/src/lib/kdb/kdb5.c
@@ -1,6 +1,7 @@
 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
 /*
- * Copyright 2006, 2009, 2010 by the Massachusetts Institute of Technology.
+ * Copyright 2006, 2009, 2010, 2016 by the Massachusetts Institute of
+ * Technology.
  * All Rights Reserved.
  *
  * Export of this software from the United States of America may
@@ -758,7 +759,15 @@ krb5_db_get_principal(krb5_context kcont
         return status;
     if (v->get_principal == NULL)
         return KRB5_PLUGIN_OP_NOTSUPP;
-    return v->get_principal(kcontext, search_for, flags, entry);
+    status = v->get_principal(kcontext, search_for, flags, entry);
+    if (status)
+        return status;
+
+    /* Sort the keys in the db entry as some parts of krb5 expect it to be. */
+    if ((*entry)->key_data != NULL)
+        krb5_dbe_sort_key_data((*entry)->key_data, (*entry)->n_key_data);
+
+    return 0;
 }
 
 void
@@ -948,6 +957,26 @@ krb5_db_delete_principal(krb5_context kc
     return status;
 }
 
+/*
+ * Use a proxy function for iterate so that we can sort the keys before sending
+ * them to the callback.
+ */
+struct callback_proxy_args {
+    int (*func)(krb5_pointer, krb5_db_entry *);
+    krb5_pointer func_arg;
+};
+
+static int
+sort_entry_callback_proxy(krb5_pointer func_arg, krb5_db_entry *entry)
+{
+    struct callback_proxy_args *args = (struct callback_proxy_args *)func_arg;
+
+    /* Sort the keys in the db entry as some parts of krb5 expect it to be. */
+    if (entry && entry->key_data)
+        krb5_dbe_sort_key_data(entry->key_data, entry->n_key_data);
+    return args->func(args->func_arg, entry);
+}
+
 krb5_error_code
 krb5_db_iterate(krb5_context kcontext, char *match_entry,
                 int (*func)(krb5_pointer, krb5_db_entry *),
@@ -955,13 +984,20 @@ krb5_db_iterate(krb5_context kcontext, c
 {
     krb5_error_code status = 0;
     kdb_vftabl *v;
+    struct callback_proxy_args proxy_args;
 
     status = get_vftabl(kcontext, &v);
     if (status)
         return status;
     if (v->iterate == NULL)
         return KRB5_PLUGIN_OP_NOTSUPP;
-    return v->iterate(kcontext, match_entry, func, func_arg, iterflags);
+
+    /* Use the proxy function to sort key data before passing entries to
+     * callback. */
+    proxy_args.func = func;
+    proxy_args.func_arg = func_arg;
+    return v->iterate(kcontext, match_entry, sort_entry_callback_proxy,
+                      &proxy_args, iterflags);
 }
 
 /* Return a read only pointer alias to mkey list.  Do not free this! */
@@ -2570,3 +2606,22 @@ krb5_db_check_allowed_to_delegate(krb5_c
         return KRB5_PLUGIN_OP_NOTSUPP;
     return v->check_allowed_to_delegate(kcontext, client, server, proxy);
 }
+
+void
+krb5_dbe_sort_key_data(krb5_key_data *key_data, size_t key_data_length)
+{
+    size_t i, j;
+    krb5_key_data tmp;
+
+    /* Use insertion sort as a stable sort. */
+    for (i = 1; i < key_data_length; i++) {
+        j = i;
+        while (j > 0 &&
+               key_data[j - 1].key_data_kvno < key_data[j].key_data_kvno) {
+            tmp = key_data[j];
+            key_data[j] = key_data[j - 1];
+            key_data[j - 1] = tmp;
+            j--;
+        }
+    }
+}
--- a/src/lib/kdb/libkdb5.exports
+++ b/src/lib/kdb/libkdb5.exports
@@ -99,3 +99,4 @@ ulog_get_sno_status
 ulog_replay
 ulog_set_last
 xdr_kdb_incr_update_t
+krb5_dbe_sort_key_data
--- a/src/plugins/kdb/ldap/libkdb_ldap/ldap_misc.c
+++ b/src/plugins/kdb/ldap/libkdb_ldap/ldap_misc.c
@@ -40,6 +40,7 @@
 #include "ldap_pwd_policy.h"
 #include <time.h>
 #include <ctype.h>
+#include <kadm5/admin.h>
 
 #ifdef NEED_STRPTIME_PROTO
 extern char *strptime(const char *, const char *, struct tm *);
@@ -1324,6 +1325,22 @@ remove_overlapping_subtrees(char **list,
     *subtcount = count;
 }
 
+static void
+free_princ_ent_contents(osa_princ_ent_t princ_ent)
+{
+    unsigned int i;
+
+    for (i = 0; i < princ_ent->old_key_len; i++) {
+        k5_free_key_data(princ_ent->old_keys[i].n_key_data,
+                         princ_ent->old_keys[i].key_data);
+        princ_ent->old_keys[i].n_key_data = 0;
+        princ_ent->old_keys[i].key_data = NULL;
+    }
+    free(princ_ent->old_keys);
+    princ_ent->old_keys = NULL;
+    princ_ent->old_key_len = 0;
+}
+
 /*
  * Fill out a krb5_db_entry princ entry struct given a LDAP message containing
  * the results of a principal search of the directory.
@@ -1344,6 +1361,9 @@ populate_krb5_db_entry(krb5_context cont
     char **pnvalues = NULL, **ocvalues = NULL, **a2d2 = NULL;
     struct berval **ber_key_data = NULL, **ber_tl_data = NULL;
     krb5_tl_data userinfo_tl_data = { NULL }, **endp, *tl;
+    osa_princ_ent_rec princ_ent;
+
+    memset(&princ_ent, 0, sizeof(princ_ent));
 
     ret = krb5_copy_principal(context, princ, &entry->princ);
     if (ret)
@@ -1462,8 +1482,21 @@ populate_krb5_db_entry(krb5_context cont
         ret = krb5_ldap_policydn_to_name(context, pwdpolicydn, &polname);
         if (ret)
             goto cleanup;
+        princ_ent.policy = polname;
+        princ_ent.aux_attributes |= KADM5_POLICY;
+    }
+
+    ber_key_data = ldap_get_values_len(ld, ent, "krbpwdhistory");
+    if (ber_key_data != NULL) {
+        mask |= KDB_PWD_HISTORY_ATTR;
+        ret = krb5_decode_histkey(context, ber_key_data, &princ_ent);
+        if (ret)
+            goto cleanup;
+        ldap_value_free_len(ber_key_data);
+    }
 
-        ret = krb5_update_tl_kadm_data(context, entry, polname);
+    if (princ_ent.aux_attributes) {
+        ret = krb5_update_tl_kadm_data(context, entry, &princ_ent);
         if (ret)
             goto cleanup;
     }
@@ -1471,8 +1504,7 @@ populate_krb5_db_entry(krb5_context cont
     ber_key_data = ldap_get_values_len(ld, ent, "krbprincipalkey");
     if (ber_key_data != NULL) {
         mask |= KDB_SECRET_KEY_ATTR;
-        ret = krb5_decode_krbsecretkey(context, entry, ber_key_data,
-                                       &userinfo_tl_data, &mkvno);
+        ret = krb5_decode_krbsecretkey(context, entry, ber_key_data, &mkvno);
         if (ret)
             goto cleanup;
         if (mkvno != 0) {
@@ -1578,6 +1610,7 @@ cleanup:
     free(tktpolname);
     free(policydn);
     krb5_free_unparsed_name(context, user);
+    free_princ_ent_contents(&princ_ent);
     return ret;
 }
 
--- a/src/plugins/kdb/ldap/libkdb_ldap/ldap_principal.c
+++ b/src/plugins/kdb/ldap/libkdb_ldap/ldap_principal.c
@@ -59,6 +59,7 @@ char     *principal_attributes[] = { "kr
                                      "krbExtraData",
                                      "krbObjectReferences",
                                      "krbAllowedToDelegateTo",
+                                     "krbPwdHistory",
                                      NULL };
 
 /* Must match KDB_*_ATTR macros in ldap_principal.h.  */
@@ -77,14 +78,38 @@ static char *attributes_set[] = { "krbma
                                   "krbLastFailedAuth",
                                   "krbLoginFailedCount",
                                   "krbLastAdminUnlock",
+                                  "krbPwdHistory",
                                   NULL };
 
+
+static void
+k5_free_key_data_contents(krb5_key_data *key)
+{
+    int16_t i;
+
+    for (i = 0; i < key->key_data_ver; i++) {
+        zapfree(key->key_data_contents[i], key->key_data_length[i]);
+        key->key_data_contents[i] = NULL;
+    }
+}
+
+void
+k5_free_key_data(krb5_int16 n_key_data, krb5_key_data *key_data)
+{
+    int16_t i;
+
+    if (key_data == NULL)
+        return;
+    for (i = 0; i < n_key_data; i++)
+        k5_free_key_data_contents(&key_data[i]);
+    free(key_data);
+}
+
 void
 krb5_dbe_free_contents(krb5_context context, krb5_db_entry *entry)
 {
     krb5_tl_data        *tl_data_next=NULL;
     krb5_tl_data        *tl_data=NULL;
-    int i, j;
 
     if (entry->e_data)
         free(entry->e_data);
@@ -96,24 +121,7 @@ krb5_dbe_free_contents(krb5_context cont
             free(tl_data->tl_data_contents);
         free(tl_data);
     }
-    if (entry->key_data) {
-        for (i = 0; i < entry->n_key_data; i++) {
-            for (j = 0; j < entry->key_data[i].key_data_ver; j++) {
-                if (entry->key_data[i].key_data_length[j]) {
-                    if (entry->key_data[i].key_data_contents[j]) {
-                        memset(entry->key_data[i].key_data_contents[j],
-                               0,
-                               (unsigned) entry->key_data[i].key_data_length[j]);
-                        free (entry->key_data[i].key_data_contents[j]);
-                    }
-                }
-                entry->key_data[i].key_data_contents[j] = NULL;
-                entry->key_data[i].key_data_length[j] = 0;
-                entry->key_data[i].key_data_type[j] = 0;
-            }
-        }
-        free(entry->key_data);
-    }
+    k5_free_key_data(entry->n_key_data, entry->key_data);
     memset(entry, 0, sizeof(*entry));
     return;
 }
--- a/src/plugins/kdb/ldap/libkdb_ldap/ldap_principal.h
+++ b/src/plugins/kdb/ldap/libkdb_ldap/ldap_principal.h
@@ -32,6 +32,7 @@
 #define _LDAP_PRINCIPAL_H 1
 
 #include "ldap_tkt_policy.h"
+#include "princ_xdr.h"
 
 #define  KEYHEADER  12
 
@@ -82,6 +83,7 @@
 #define KDB_LAST_FAILED_ATTR                 0x001000
 #define KDB_FAIL_AUTH_COUNT_ATTR             0x002000
 #define KDB_LAST_ADMIN_UNLOCK_ATTR           0x004000
+#define KDB_PWD_HISTORY_ATTR                 0x008000
 
 /*
  * This is a private contract between krb5_ldap_lockout_audit()
@@ -112,6 +114,12 @@ krb5_ldap_iterate(krb5_context, char *,
                   krb5_pointer, krb5_flags);
 
 void
+k5_free_key_data(krb5_int16 n_key_data, krb5_key_data *key_data);
+
+void
+krb5_dbe_free_contents(krb5_context context, krb5_db_entry *entry);
+
+void
 krb5_dbe_free_contents(krb5_context, krb5_db_entry *);
 
 krb5_error_code
@@ -121,8 +129,11 @@ krb5_error_code
 krb5_ldap_parse_principal_name(char *, char **);
 
 krb5_error_code
+krb5_decode_histkey(krb5_context, struct berval **, osa_princ_ent_rec *);
+
+krb5_error_code
 krb5_decode_krbsecretkey(krb5_context, krb5_db_entry *, struct berval **,
-                         krb5_tl_data *, krb5_kvno *);
+                         krb5_kvno *);
 
 krb5_error_code
 berval2tl_data(struct berval *in, krb5_tl_data **out);
--- a/src/plugins/kdb/ldap/libkdb_ldap/ldap_principal2.c
+++ b/src/plugins/kdb/ldap/libkdb_ldap/ldap_principal2.c
@@ -1,6 +1,35 @@
 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
 /* plugins/kdb/ldap/libkdb_ldap/ldap_principal2.c */
 /*
+ * Copyright (C) 2016 by the Massachusetts Institute of Technology.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in
+ *   the documentation and/or other materials provided with the
+ *   distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+/*
  * Copyright (c) 2004-2005, Novell, Inc.
  * All rights reserved.
  *
@@ -362,13 +391,14 @@ asn1_encode_sequence_of_keys(krb5_key_da
 }
 
 static krb5_error_code
-asn1_decode_sequence_of_keys(krb5_data *in, krb5_key_data **out,
-                             krb5_int16 *n_key_data, krb5_kvno *mkvno)
+asn1_decode_sequence_of_keys(krb5_data *in, ldap_seqof_key_data *out)
 {
     krb5_error_code err;
     ldap_seqof_key_data *p;
     int i;
 
+    memset(out, 0, sizeof(*out));
+
     /*
      * This should be pushed back into other library initialization
      * code.
@@ -390,9 +420,7 @@ asn1_decode_sequence_of_keys(krb5_data *
             p->key_data[i].key_data_ver = 2;
     }
 
-    *out = p->key_data;
-    *n_key_data = p->n_key_data;
-    *mkvno = p->mkvno;
+    *out = *p;
     free(p);
     return 0;
 }
@@ -416,19 +444,24 @@ free_berdata(struct berval **array)
     }
 }
 
-/* Decoding ASN.1 encoded key */
-static struct berval **
-krb5_encode_krbsecretkey(krb5_key_data *key_data_in, int n_key_data,
-                         krb5_kvno mkvno) {
-    struct berval **ret = NULL;
-    int currkvno;
-    int num_versions = 1;
-    int i, j, last;
+/*
+ * Encode krb5_key_data into a berval struct for insertion into LDAP.
+ */
+static krb5_error_code
+encode_keys(krb5_key_data *key_data_in, int n_key_data, krb5_kvno mkvno,
+            struct berval **bval_out)
+{
     krb5_error_code err = 0;
+    int i;
     krb5_key_data *key_data = NULL;
+    struct berval *bval = NULL;
+    krb5_data *code;
 
-    if (n_key_data < 0)
-        return NULL;
+    *bval_out = NULL;
+    if (n_key_data <= 0) {
+        err = EINVAL;
+        goto cleanup;
+    }
 
     /* Make a shallow copy of the key data so we can alter it. */
     key_data = k5calloc(n_key_data, sizeof(*key_data), &err);
@@ -447,31 +480,68 @@ krb5_encode_krbsecretkey(krb5_key_data *
         }
     }
 
+    bval = k5alloc(sizeof(struct berval), &err);
+    if (bval == NULL)
+        goto cleanup;
+
+    err = asn1_encode_sequence_of_keys(key_data, n_key_data, mkvno, &code);
+    if (err)
+        goto cleanup;
+
+    /* Steal the data pointer from code for bval and discard code. */
+    bval->bv_len = code->length;
+    bval->bv_val = code->data;
+    free(code);
+
+    *bval_out = bval;
+    bval = NULL;
+
+cleanup:
+    free(key_data);
+    free(bval);
+    return err;
+}
+
+/* Decoding ASN.1 encoded key */
+static struct berval **
+krb5_encode_krbsecretkey(krb5_key_data *key_data, int n_key_data,
+                         krb5_kvno mkvno)
+{
+    struct berval **ret = NULL;
+    int currkvno;
+    int num_versions = 0;
+    int i, j, last;
+    krb5_error_code err = 0;
+
+    if (n_key_data < 0)
+        return NULL;
+
     /* Find the number of key versions */
-    for (i = 0; i < n_key_data - 1; i++)
-        if (key_data[i].key_data_kvno != key_data[i + 1].key_data_kvno)
-            num_versions++;
+    if (n_key_data > 0) {
+        for (i = 0, num_versions = 1; i < n_key_data - 1; i++) {
+            if (key_data[i].key_data_kvno != key_data[i + 1].key_data_kvno)
+                num_versions++;
+        }
+    }
 
-    ret = (struct berval **) calloc (num_versions + 1, sizeof (struct berval *));
+    ret = calloc(num_versions + 1, sizeof(struct berval *));
     if (ret == NULL) {
         err = ENOMEM;
         goto cleanup;
     }
-    for (i = 0, last = 0, j = 0, currkvno = key_data[0].key_data_kvno; i < n_key_data; i++) {
-        krb5_data *code;
+    ret[num_versions] = NULL;
+
+    /* n_key_data may be 0 if a principal is created without a key. */
+    if (n_key_data == 0)
+        goto cleanup;
+
+    currkvno = key_data[0].key_data_kvno;
+    for (i = 0, last = 0, j = 0; i < n_key_data; i++) {
         if (i == n_key_data - 1 || key_data[i + 1].key_data_kvno != currkvno) {
-            ret[j] = k5alloc(sizeof(struct berval), &err);
-            if (ret[j] == NULL)
-                goto cleanup;
-            err = asn1_encode_sequence_of_keys(key_data + last,
-                                               (krb5_int16)i - last + 1,
-                                               mkvno, &code);
+            err = encode_keys(key_data + last, (krb5_int16)i - last + 1, mkvno,
+                              &ret[j]);
             if (err)
                 goto cleanup;
-            /*CHECK_NULL(ret[j]); */
-            ret[j]->bv_len = code->length;
-            ret[j]->bv_val = code->data;
-            free(code);
             j++;
             last = i + 1;
 
@@ -479,11 +549,48 @@ krb5_encode_krbsecretkey(krb5_key_data *
                 currkvno = key_data[i + 1].key_data_kvno;
         }
     }
-    ret[num_versions] = NULL;
 
 cleanup:
+    if (err != 0) {
+        free_berdata(ret);
+        ret = NULL;
+    }
 
-    free(key_data);
+    return ret;
+}
+
+/*
+ * Encode a principal's key history for insertion into ldap.
+ */
+static struct berval **
+krb5_encode_histkey(osa_princ_ent_rec *princ_ent)
+{
+    unsigned int i;
+    krb5_error_code err = 0;
+    struct berval **ret = NULL;
+
+    if (princ_ent->old_key_len <= 0)
+        return NULL;
+
+    ret = k5calloc(princ_ent->old_key_len + 1, sizeof(struct berval *), &err);
+    if (ret == NULL)
+        goto cleanup;
+
+    for (i = 0; i < princ_ent->old_key_len; i++) {
+        if (princ_ent->old_keys[i].n_key_data <= 0) {
+            err = EINVAL;
+            goto cleanup;
+        }
+        err = encode_keys(princ_ent->old_keys[i].key_data,
+                          princ_ent->old_keys[i].n_key_data,
+                          princ_ent->admin_history_kvno, &ret[i]);
+        if (err)
+            goto cleanup;
+    }
+
+    ret[princ_ent->old_key_len] = NULL;
+
+cleanup:
     if (err != 0) {
         free_berdata(ret);
         ret = NULL;
@@ -1004,7 +1111,7 @@ krb5_ldap_put_principal(krb5_context con
         free (strval[0]);
     }
 
-    if (entry->mask & KADM5_POLICY) {
+    if (entry->mask & KADM5_POLICY || entry->mask & KADM5_KEY_HIST) {
         memset(&princ_ent, 0, sizeof(princ_ent));
         for (tl_data=entry->tl_data; tl_data; tl_data=tl_data->tl_data_next) {
             if (tl_data->tl_data_type == KRB5_TL_KADM_DATA) {
@@ -1014,7 +1121,9 @@ krb5_ldap_put_principal(krb5_context con
                 break;
             }
         }
+    }
 
+    if (entry->mask & KADM5_POLICY) {
         if (princ_ent.aux_attributes & KADM5_POLICY) {
             memset(strval, 0, sizeof(strval));
             if ((st = krb5_ldap_name_to_policydn (context, princ_ent.policy, &polname)) != 0)
@@ -1042,6 +1151,22 @@ krb5_ldap_put_principal(krb5_context con
             goto cleanup;
     }
 
+    if (entry->mask & KADM5_KEY_HIST) {
+        bersecretkey = krb5_encode_histkey(&princ_ent);
+        if (bersecretkey == NULL) {
+            st = ENOMEM;
+            goto cleanup;
+        }
+
+        st = krb5_add_ber_mem_ldap_mod(&mods, "krbpwdhistory",
+                                       LDAP_MOD_REPLACE | LDAP_MOD_BVALUES,
+                                       bersecretkey);
+        if (st != 0)
+            goto cleanup;
+        free_berdata(bersecretkey);
+        bersecretkey = NULL;
+    }
+
     if (entry->mask & KADM5_KEY_DATA || entry->mask & KADM5_KVNO) {
         krb5_kvno mkvno;
 
@@ -1376,22 +1501,62 @@ cleanup:
     return st;
 }
 
-krb5_error_code
-krb5_decode_krbsecretkey(krb5_context context, krb5_db_entry *entries,
-                         struct berval **bvalues,
-                         krb5_tl_data *userinfo_tl_data, krb5_kvno *mkvno)
+static void
+free_ldap_seqof_key_data(ldap_seqof_key_data *keysets, krb5_int16 n_keysets)
 {
-    char                        *user=NULL;
-    int                         i=0, j=0, noofkeys=0;
-    krb5_key_data               *key_data=NULL, *tmp;
-    krb5_error_code             st=0;
+    int i;
 
-    if ((st=krb5_unparse_name(context, entries->princ, &user)) != 0)
+    if (keysets == NULL)
+        return;
+
+    for (i = 0; i < n_keysets; i++)
+        k5_free_key_data(keysets[i].n_key_data, keysets[i].key_data);
+    free(keysets);
+}
+
+/*
+ * Decode keys from ldap search results.
+ *
+ * Arguments:
+ *  - bvalues
+ *      The ldap search results containing the key data.
+ *  - mkvno
+ *      The master kvno that the keys were encrypted with.
+ *  - keysets_out
+ *      The decoded keys in a ldap_seqof_key_data struct.  Must be freed using
+ *      free_ldap_seqof_key_data.
+ *  - n_keysets_out
+ *      The number of entries in keys_out.
+ *  - total_keys_out
+ *      An optional argument that if given will be set to the total number of
+ *      keys found throughout all the entries: sum(keys_out.n_key_data)
+ *      May be NULL.
+ */
+static krb5_error_code
+decode_keys(struct berval **bvalues, ldap_seqof_key_data **keysets_out,
+            krb5_int16 *n_keysets_out, krb5_int16 *total_keys_out)
+{
+    krb5_error_code err = 0;
+    krb5_int16 n_keys, i, ki, total_keys;
+    ldap_seqof_key_data *keysets = NULL;
+
+    *keysets_out = NULL;
+    *n_keysets_out = 0;
+    if (total_keys_out)
+        *total_keys_out = 0;
+
+    /* Precount the number of keys. */
+    for (n_keys = 0, i = 0; bvalues[i] != NULL; i++) {
+        if (bvalues[i]->bv_len > 0)
+            n_keys++;
+    }
+
+    keysets = k5calloc(n_keys, sizeof(ldap_seqof_key_data), &err);
+    if (keysets == NULL)
         goto cleanup;
+    memset(keysets, 0, n_keys * sizeof(ldap_seqof_key_data));
 
-    for (i=0; bvalues[i] != NULL; ++i) {
-        krb5_int16 n_kd;
-        krb5_key_data *kd;
+    for (i = 0, ki = 0, total_keys = 0; bvalues[i] != NULL; i++) {
         krb5_data in;
 
         if (bvalues[i]->bv_len == 0)
@@ -1399,39 +1564,131 @@ krb5_decode_krbsecretkey(krb5_context co
         in.length = bvalues[i]->bv_len;
         in.data = bvalues[i]->bv_val;
 
-        st = asn1_decode_sequence_of_keys (&in,
-                                           &kd,
-                                           &n_kd,
-                                           mkvno);
-
-        if (st != 0) {
-            const char *msg = error_message(st);
-            st = -1; /* Something more appropriate ? */
-            k5_setmsg(context, st,
-                      _("unable to decode stored principal key data (%s)"),
-                      msg);
-            goto cleanup;
-        }
-        noofkeys += n_kd;
-        tmp = key_data;
-        /* Allocate an extra key data to avoid allocating zero bytes. */
-        key_data = realloc(key_data, (noofkeys + 1) * sizeof (krb5_key_data));
-        if (key_data == NULL) {
-            key_data = tmp;
-            st = ENOMEM;
+        err = asn1_decode_sequence_of_keys(&in, &keysets[ki]);
+        if (err)
             goto cleanup;
-        }
-        for (j = 0; j < n_kd; j++)
-            key_data[noofkeys - n_kd + j] = kd[j];
-        free (kd);
+
+        if (total_keys_out)
+            total_keys += keysets[ki].n_key_data;
+        ki++;
+    }
+
+    if (total_keys_out)
+        *total_keys_out = total_keys;
+
+    *n_keysets_out = n_keys;
+    *keysets_out = keysets;
+    keysets = NULL;
+    n_keys = 0;
+
+cleanup:
+    free_ldap_seqof_key_data(keysets, n_keys);
+    return err;
+}
+
+krb5_error_code
+krb5_decode_krbsecretkey(krb5_context context, krb5_db_entry *entries,
+                         struct berval **bvalues, krb5_kvno *mkvno)
+{
+    krb5_key_data *key_data = NULL, *tmp;
+    krb5_error_code err = 0;
+    ldap_seqof_key_data *keysets = NULL;
+    krb5_int16 i, n_keysets = 0, total_keys = 0;
+
+    err = decode_keys(bvalues, &keysets, &n_keysets, &total_keys);
+    if (err != 0) {
+        k5_prependmsg(context, err,
+                      _("unable to decode stored principal key data"));
+        goto cleanup;
     }
 
-    entries->n_key_data = noofkeys;
+    key_data = k5calloc(total_keys, sizeof(krb5_key_data), &err);
+    if (key_data == NULL)
+        goto cleanup;
+    memset(key_data, 0, total_keys * sizeof(krb5_key_data));
+
+    if (n_keysets > 0)
+        *mkvno = keysets[0].mkvno;
+
+    /* Transfer key data values from keysets to a flat list in entries. */
+    tmp = key_data;
+    for (i = 0; i < n_keysets; i++) {
+        memcpy(tmp, keysets[i].key_data,
+               sizeof(krb5_key_data) * keysets[i].n_key_data);
+        tmp += keysets[i].n_key_data;
+        keysets[i].n_key_data = 0;
+    }
+    entries->n_key_data = total_keys;
     entries->key_data = key_data;
+    key_data = NULL;
 
 cleanup:
-    free (user);
-    return st;
+    free_ldap_seqof_key_data(keysets, n_keysets);
+    k5_free_key_data(total_keys, key_data);
+    return err;
+}
+
+static int
+compare_osa_pw_hist_ent(const void *left_in, const void *right_in)
+{
+    int kvno_left, kvno_right;
+    osa_pw_hist_ent *left = (osa_pw_hist_ent *)left_in;
+    osa_pw_hist_ent *right = (osa_pw_hist_ent *)right_in;
+
+    kvno_left = left->n_key_data ? left->key_data[0].key_data_kvno : 0;
+    kvno_right = right->n_key_data ? right->key_data[0].key_data_kvno : 0;
+    return kvno_left - kvno_right;
+}
+
+/*
+ * Decode the key history entries from an LDAP search.
+ *
+ * NOTE: the caller must free princ_ent->old_keys even on error.
+ */
+krb5_error_code
+krb5_decode_histkey(krb5_context context, struct berval **bvalues,
+                    osa_princ_ent_rec *princ_ent)
+{
+    krb5_error_code err = 0;
+    krb5_int16 i, n_keysets = 0;
+    ldap_seqof_key_data *keysets = NULL;
+
+    err = decode_keys(bvalues, &keysets, &n_keysets, NULL);
+    if (err != 0) {
+        k5_prependmsg(context, err,
+                      _("unable to decode stored principal pw history"));
+        goto cleanup;
+    }
+
+    princ_ent->old_keys = k5calloc(n_keysets, sizeof(osa_pw_hist_ent), &err);
+    if (princ_ent->old_keys == NULL)
+        goto cleanup;
+    princ_ent->old_key_len = n_keysets;
+
+    if (n_keysets > 0)
+        princ_ent->admin_history_kvno = keysets[0].mkvno;
+
+    /* Transfer key data pointers from keysets to princ_ent. */
+    for (i = 0; i < n_keysets; i++) {
+        princ_ent->old_keys[i].n_key_data = keysets[i].n_key_data;
+        princ_ent->old_keys[i].key_data = keysets[i].key_data;
+        keysets[i].n_key_data = 0;
+        keysets[i].key_data = NULL;
+    }
+
+    /* Sort the principal entries by kvno in ascending order. */
+    qsort(princ_ent->old_keys, princ_ent->old_key_len, sizeof(osa_pw_hist_ent),
+          &compare_osa_pw_hist_ent);
+
+    princ_ent->aux_attributes |= KADM5_KEY_HIST;
+
+    /* Set the next key to the end of the list.  The queue will be lengthened
+     * if it isn't full yet; the first entry will be replaced if it is full. */
+    princ_ent->old_key_next = princ_ent->old_key_len;
+
+cleanup:
+    free_ldap_seqof_key_data(keysets, n_keysets);
+    return err;
 }
 
 static char *
--- a/src/plugins/kdb/ldap/libkdb_ldap/princ_xdr.c
+++ b/src/plugins/kdb/ldap/libkdb_ldap/princ_xdr.c
@@ -204,20 +204,14 @@ krb5_lookup_tl_kadm_data(krb5_tl_data *t
 
 krb5_error_code
 krb5_update_tl_kadm_data(krb5_context context, krb5_db_entry *entry,
-			 char *policy_dn)
+			 osa_princ_ent_rec *princ_entry)
 {
     XDR xdrs;
-    osa_princ_ent_rec princ_entry;
     krb5_tl_data tl_data;
     krb5_error_code retval;
 
-    memset(&princ_entry, 0, sizeof(osa_princ_ent_rec));
-    princ_entry.admin_history_kvno = 2;
-    princ_entry.aux_attributes = KADM5_POLICY;
-    princ_entry.policy = policy_dn;
-
     xdralloc_create(&xdrs, XDR_ENCODE);
-    if (! ldap_xdr_osa_princ_ent_rec(&xdrs, &princ_entry)) {
+    if (! ldap_xdr_osa_princ_ent_rec(&xdrs, princ_entry)) {
 	xdr_destroy(&xdrs);
 	return KADM5_XDR_FAILURE;
     }
--- a/src/plugins/kdb/ldap/libkdb_ldap/princ_xdr.h
+++ b/src/plugins/kdb/ldap/libkdb_ldap/princ_xdr.h
@@ -57,6 +57,6 @@ krb5_lookup_tl_kadm_data(krb5_tl_data *t
 
 krb5_error_code
 krb5_update_tl_kadm_data(krb5_context context, krb5_db_entry *entry,
-			 char *policy_dn);
+                         osa_princ_ent_rec *princ_entry);
 
 #endif
--- a/src/tests/kdbtest.c
+++ b/src/tests/kdbtest.c
@@ -97,7 +97,7 @@ static krb5_tl_data tl3 = { &tl4, KRB5_T
                             U("\x12\x34\x5C\x01\x00\x00\x00\x08"
                               "\x3C\x74\x65\x73\x74\x2A\x3E\x00"
                               "\x00\x00\x08\x00\x00\x00\x00\x00"
-                              "\x00\x00\x00\x02\x00\x00\x00\x00") };
+                              "\x00\x00\x00\x00\x00\x00\x00\x00") };
 static krb5_tl_data tl2 = { &tl3, KRB5_TL_MOD_PRINC, 8, U("\5\6\7\0x@Y\0") };
 static krb5_tl_data tl1 = { &tl2, KRB5_TL_LAST_PWD_CHANGE, 4, U("\1\2\3\4") };
 
--- a/src/tests/t_kdb.py
+++ b/src/tests/t_kdb.py
@@ -337,6 +337,31 @@ realm.run([kadminl, 'modprinc', '+requir
 realm.kinit('canon', password('canon'))
 realm.kinit('alias', password('canon'), ['-C'])
 
+# Test password history.
+def test_pwhist(nhist):
+    def cpw(n, **kwargs):
+        realm.run([kadminl, 'cpw', '-pw', str(n), princ], **kwargs)
+    def cpw_fail(n):
+        cpw(n, expected_code=1)
+    output('*** Testing password history of size %d\n' % nhist)
+    princ = 'pwhistprinc' + str(nhist)
+    pol = 'pwhistpol' + str(nhist)
+    realm.run([kadminl, 'addpol', '-history', str(nhist), pol])
+    realm.run([kadminl, 'addprinc', '-policy', pol, '-nokey', princ])
+    for i in range(nhist):
+        # Set a password, then check that all previous passwords fail.
+        cpw(i)
+        for j in range(i + 1):
+            cpw_fail(j)
+    # Set one more new password, and make sure the oldest key is
+    # rotated out.
+    cpw(nhist)
+    cpw_fail(1)
+    cpw(0)
+
+for n in (1, 2, 3, 4, 5):
+    test_pwhist(n)
+
 # Regression test for #7980 (fencepost when dividing keys up by kvno).
 realm.run([kadminl, 'addprinc', '-randkey', '-e', 'aes256-cts,aes128-cts',
            'kvnoprinc'])
@@ -368,6 +393,13 @@ out = realm.run([kadminl, 'getprinc', 'k
 if 'Number of keys: 0' not in out:
     fail('After purgekeys -all, keys remain')
 
+# Test for 8354 (old password history entries when -keepold is used)
+realm.run([kadminl, 'addpol', '-history', '2', 'keepoldpasspol'])
+realm.run([kadminl, 'addprinc', '-policy', 'keepoldpasspol', '-pw', 'aaaa',
+           'keepoldpassprinc'])
+for p in ('bbbb', 'cccc', 'aaaa'):
+    realm.run([kadminl, 'cpw', '-keepold', '-pw', p, 'keepoldpassprinc'])
+
 realm.stop()
 
 # Briefly test dump and load.