18794793 MIT 1.8.3 resync removed pw history when using LDAP as a backend
authorTomas Kuthan <tomas.kuthan@oracle.com>
Fri, 04 Nov 2016 09:14:49 -0700
changeset 7246 b3414fa83399
parent 7245 934578b959f0
child 7247 595712971717
18794793 MIT 1.8.3 resync removed pw history when using LDAP as a backend
components/krb5/patches/075-ldap-pw-hist.patch
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/components/krb5/patches/075-ldap-pw-hist.patch	Fri Nov 04 09:14:49 2016 -0700
@@ -0,0 +1,1126 @@
+#
+# 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.