Add API to interpret changepw result strings
authorGreg Hudson <ghudson@mit.edu>
Wed, 9 May 2012 17:40:38 +0000 (17:40 +0000)
committerGreg Hudson <ghudson@mit.edu>
Wed, 9 May 2012 17:40:38 +0000 (17:40 +0000)
Active Directory returns structured policy information in the
nominally UTF-8 result string field of a password change reply.  Add a
new API krb5_chpw_message() to convert a result string into a
displayable message, interpreting policy information if present.

Patch from stefw@gnome.org with changes.

ticket: 7128

git-svn-id: svn://anonsvn.mit.edu/krb5/trunk@25857 dc483132-0cff-0310-8789-dd5450dbe970

doc/rst_source/krb_appldev/refs/api/index.rst
src/clients/kpasswd/kpasswd.c
src/include/krb5/krb5.hin
src/lib/krb5/krb/chpw.c
src/lib/krb5/krb/gic_pwd.c
src/lib/krb5/libkrb5.exports
src/lib/krb5_32.def
src/tests/misc/Makefile.in
src/tests/misc/test_chpw_message.c [new file with mode: 0644]
src/util/collected-client-lib/libcollected.exports

index 8735b91a3999eaced5ad68d3d8448b6d24d793a6..743de67c41e018250c6ae0b867b608f824e8d156 100644 (file)
@@ -23,6 +23,7 @@ Frequently used public interfaces
    krb5_cc_new_unique.rst
    krb5_cc_resolve.rst
    krb5_change_password.rst
+   krb5_chpw_message.rst
    krb5_free_context.rst
    krb5_free_error_message.rst
    krb5_free_principal.rst
index b4b6eadd4d0c053e1f7973a80d44731129bc958b..7aed0f1ac898656aae43ae6778628b8624996ed4 100644 (file)
@@ -53,6 +53,7 @@ int main(int argc, char *argv[])
     krb5_ccache ccache;
     krb5_get_init_creds_opt *opts = NULL;
     krb5_creds creds;
+    char *message;
 
     char pw[1024];
     unsigned int pwlen;
@@ -154,11 +155,12 @@ int main(int argc, char *argv[])
     }
 
     if (result_code) {
-        printf("%.*s%s%.*s\n",
+        if (krb5_chpw_message(context, &result_string, &message) != 0)
+            message = NULL;
+        printf("%.*s%s%s\n",
                (int) result_code_string.length, result_code_string.data,
-               result_string.length?": ":"",
-               (int) result_string.length,
-               result_string.data ? result_string.data : "");
+               message ? ": " : "", message ? message : NULL);
+        krb5_free_string(context, message);
         krb5_get_init_creds_opt_free(context, opts);
         exit(2);
     }
index c267622dbb506e1d9ccd62dd8c95debdf8257e73..741477caf8b2d939f72a7cbc35ba32128ec6bae0 100644 (file)
@@ -4962,6 +4962,32 @@ krb5_set_password_using_ccache(krb5_context context, krb5_ccache ccache,
                                int *result_code, krb5_data *result_code_string,
                                krb5_data *result_string);
 
+/**
+ * Get a result message for changing or setting a password.
+ *
+ * @param [in]  context            Library context
+ * @param [in]  server_string      Data returned from the remote system
+ * @param [out] message_out        A message displayable to the user
+ *
+ * This function processes the @a server_string returned in the @a
+ * result_string parameter of krb5_change_password(), krb5_set_password(), and
+ * related functions, and returns a displayable string.  If @a server_string
+ * contains Active Directory structured policy information, it will be
+ * converted into human-readable text.
+ *
+ * Use krb5_free_string() to free @a message_out when it is no longer needed.
+ *
+ * @retval
+ * 0 Success
+ * @return
+ * Kerberos error codes
+ *
+ * @version First introduced in 1.11
+ */
+krb5_error_code KRB5_CALLCONV
+krb5_chpw_message(krb5_context context, const krb5_data *server_string,
+                  char **message_out);
+
 /**
  * Retrieve configuration profile from the context.
  *
index 7e43dcf1a1f0ef505affacf40c59a9da63e78dca..beb77cb986f9f10a8879df776900d00d5254db66 100644 (file)
@@ -5,6 +5,7 @@
 #include <string.h>
 
 #include "k5-int.h"
+#include "k5-unicode.h"
 #include "int-proto.h"
 #include "auth_con.h"
 
@@ -349,3 +350,168 @@ cleanup:
     }
     return ret;
 }
+
+/*
+ * Active Directory policy information is communicated in the result string
+ * field as a packed 30-byte sequence, starting with two zero bytes (so that
+ * the string appears as zero-length when interpreted as UTF-8).  The bytes
+ * correspond to the fields in the following structure, with each field in
+ * big-endian byte order.
+ */
+struct ad_policy_info {
+    uint16_t zero_bytes;
+    uint32_t min_length_password;
+    uint32_t password_history;
+    uint32_t password_properties; /* see defines below */
+    uint64_t expire;              /* in seconds * 10,000,000 */
+    uint64_t min_passwordage;     /* in seconds * 10,000,000 */
+};
+
+#define AD_POLICY_INFO_LENGTH      30
+#define AD_POLICY_TIME_TO_DAYS     (86400ULL * 10000000ULL)
+
+#define AD_POLICY_COMPLEX          0x00000001
+#define AD_POLICY_NO_ANON_CHANGE   0x00000002
+#define AD_POLICY_NO_CLEAR_CHANGE  0x00000004
+#define AD_POLICY_LOCKOUT_ADMINS   0x00000008
+#define AD_POLICY_STORE_CLEARTEXT  0x00000010
+#define AD_POLICY_REFUSE_CHANGE    0x00000020
+
+/* If buf already contains one or more sentences, add spaces to separate them
+ * from the next sentence. */
+static void
+add_spaces(struct k5buf *buf)
+{
+    if (krb5int_buf_len(buf) > 0)
+        krb5int_buf_add(buf, "  ");
+}
+
+static krb5_error_code
+decode_ad_policy_info(const krb5_data *data, char **msg_out)
+{
+    struct ad_policy_info policy;
+    uint64_t password_days;
+    const char *p;
+    char *msg;
+    struct k5buf buf;
+
+    *msg_out = NULL;
+    if (data->length != AD_POLICY_INFO_LENGTH)
+        return 0;
+
+    p = data->data;
+    policy.zero_bytes = load_16_be(p);
+    p += 2;
+
+    /* first two bytes are zeros */
+    if (policy.zero_bytes != 0)
+        return 0;
+
+    /* Read in the rest of structure */
+    policy.min_length_password = load_32_be(p);
+    p += 4;
+    policy.password_history = load_32_be(p);
+    p += 4;
+    policy.password_properties = load_32_be(p);
+    p += 4;
+    policy.expire = load_64_be(p);
+    p += 8;
+    policy.min_passwordage = load_64_be(p);
+    p += 8;
+
+    /* Check that we processed exactly the expected number of bytes. */
+    assert(p == data->data + AD_POLICY_INFO_LENGTH);
+
+    krb5int_buf_init_dynamic(&buf);
+
+    /*
+     * Update src/tests/misc/test_chpw_message.c if changing these strings!
+     */
+
+    if (policy.password_properties & AD_POLICY_COMPLEX) {
+        krb5int_buf_add(&buf,
+                        _("The password must include numbers or symbols.  "
+                          "Don't include any part of your name in the "
+                          "password."));
+    }
+    if (policy.min_length_password > 0) {
+        add_spaces(&buf);
+        krb5int_buf_add_fmt(&buf,
+                            ngettext("The password must contain at least %d "
+                                     "character.",
+                                     "The password must contain at least %d "
+                                     "characters.",
+                                     policy.min_length_password),
+                            policy.min_length_password);
+    }
+    if (policy.password_history) {
+        add_spaces(&buf);
+        krb5int_buf_add_fmt(&buf,
+                            ngettext("The password must be different from the "
+                                     "previous password.",
+                                     "The password must be different from the "
+                                     "previous %d passwords.",
+                                     policy.password_history),
+                            policy.password_history);
+    }
+    if (policy.min_passwordage) {
+        password_days = policy.min_passwordage / AD_POLICY_TIME_TO_DAYS;
+        if (password_days == 0)
+            password_days = 1;
+        add_spaces(&buf);
+        krb5int_buf_add_fmt(&buf,
+                            ngettext("The password can only be changed once a "
+                                     "day.",
+                                     "The password can only be changed every "
+                                     "%d days.", (int)password_days),
+                            (int)password_days);
+    }
+
+    msg = krb5int_buf_data(&buf);
+    if (msg == NULL)
+        return ENOMEM;
+
+    if (*msg != '\0')
+        *msg_out = msg;
+    else
+        free(msg);
+    return 0;
+}
+
+krb5_error_code KRB5_CALLCONV
+krb5_chpw_message(krb5_context context, const krb5_data *server_string,
+                  char **message_out)
+{
+    krb5_error_code ret;
+    krb5_data *string;
+    char *msg;
+
+    *message_out = NULL;
+
+    /* If server_string contains an AD password policy, construct a message
+     * based on that. */
+    ret = decode_ad_policy_info(server_string, &msg);
+    if (ret == 0 && msg != NULL) {
+        *message_out = msg;
+        return 0;
+    }
+
+    /* If server_string contains a valid UTF-8 string, return that. */
+    if (server_string->length > 0 &&
+        memchr(server_string->data, 0, server_string->length) == NULL &&
+        krb5int_utf8_normalize(server_string, &string,
+                               KRB5_UTF8_APPROX) == 0) {
+        *message_out = string->data; /* already null terminated */
+        free(string);
+        return 0;
+    }
+
+    /* server_string appears invalid, so try to be helpful. */
+    msg = strdup(_("Try a more complex password, or contact your "
+                   "administrator."));
+    if (msg == NULL)
+        return ENOMEM;
+
+    *message_out = msg;
+    return 0;
+}
index 68d28fe9d335fab1b4ea76feab7f578ba467a188..b25eb6da029c08babb80e8e67d399475204e6de2 100644 (file)
@@ -235,6 +235,7 @@ krb5_get_init_creds_password(krb5_context context,
     char banner[1024], pw0array[1024], pw1array[1024];
     krb5_prompt prompt[2];
     krb5_prompt_type prompt_types[sizeof(prompt)/sizeof(prompt[0])];
+    char *message;
 
     use_master = 0;
     as_reply = NULL;
@@ -413,19 +414,21 @@ krb5_get_init_creds_password(krb5_context context,
 
             /* the error was soft, so try again */
 
+            if (krb5_chpw_message(context, &result_string, &message) != 0)
+                message = NULL;
+
             /* 100 is I happen to know that no code_string will be longer
                than 100 chars */
 
-            if (result_string.length > (sizeof(banner)-100))
-                result_string.length = sizeof(banner)-100;
+            if (message != NULL && strlen(message) > (sizeof(banner) - 100))
+                message[sizeof(banner) - 100] = '\0';
 
             snprintf(banner, sizeof(banner),
-                     _("%.*s%s%.*s.  Please try again.\n"),
+                     _("%.*s%s%s.  Please try again.\n"),
                      (int) code_string.length, code_string.data,
-                     result_string.length ? ": " : "",
-                     (int) result_string.length,
-                     result_string.data ? result_string.data : "");
+                     message ? ": " : "", message ? message : "");
 
+            free(message);
             free(code_string.data);
             free(result_string.data);
         }
index cb73a147141c5b6650c667481251e718f0792aeb..6319c72a851ba505114532115caa84f97a6b31d0 100644 (file)
@@ -213,6 +213,7 @@ krb5_change_cache
 krb5_change_password
 krb5_check_clockskew
 krb5_check_transited_list
+krb5_chpw_message
 krb5_chpw_result_code_string
 krb5_clear_error_message
 krb5_copy_addr
index d5922d2d15bfa59160431029713593f57a854310..bfc211535a278a3e9e157f69a3265404c50cf6f9 100644 (file)
@@ -421,3 +421,6 @@ EXPORTS
        krb5_pac_sign                                   @395
        krb5_find_authdata                              @396
        krb5_check_clockskew                            @397
+
+; new in 1.11
+       krb5_chpw_message                               @398
index 5a4b329bc2bf2f10a3d1a2ce01f614e87ee0815e..8a601bf0dac32278f55e7dec026ef656cec364fd 100644 (file)
@@ -4,9 +4,13 @@ RUN_SETUP = @KRB5_RUN_ENV@
 PROG_LIBPATH=-L$(TOPLIBD)
 PROG_RPATH=$(KRB5_LIBDIR)
 
-OBJS=test_getpw.o
+OBJS=\
+       test_getpw.o \
+       test_chpw_message.o
+
 SRCS=\
        $(srcdir)/test_getpw.c \
+       $(srcdir)/test_chpw_message.c \
        $(srcdir)/test_getsockname.c \
        $(srcdir)/test_cxx_krb5.cpp \
        $(srcdir)/test_cxx_k5int.cpp \
@@ -14,10 +18,11 @@ SRCS=\
        $(srcdir)/test_cxx_rpc.cpp \
        $(srcdir)/test_cxx_kadm5.cpp
 
-all:: test_getpw
+all:: test_getpw test_chpw_message
 
-check:: test_getpw test_cxx_krb5 test_cxx_gss test_cxx_rpc test_cxx_k5int test_cxx_kadm5
+check:: test_getpw test_chpw_message test_cxx_krb5 test_cxx_gss test_cxx_rpc test_cxx_k5int test_cxx_kadm5
        $(RUN_SETUP) $(VALGRIND) ./test_getpw
+       $(RUN_SETUP) $(VALGRIND) ./test_chpw_message
        $(RUN_SETUP) $(VALGRIND) ./test_cxx_krb5
        $(RUN_SETUP) $(VALGRIND) ./test_cxx_k5int
        $(RUN_SETUP) $(VALGRIND) ./test_cxx_gss
@@ -27,6 +32,9 @@ check:: test_getpw test_cxx_krb5 test_cxx_gss test_cxx_rpc test_cxx_k5int test_c
 test_getpw: $(OUTPRE)test_getpw.$(OBJEXT) $(SUPPORT_DEPLIB)
        $(CC_LINK) $(ALL_CFLAGS) -o test_getpw $(OUTPRE)test_getpw.$(OBJEXT) $(SUPPORT_LIB)
 
+test_chpw_message: $(OUTPRE)test_chpw_message.$(OBJEXT) $(SUPPORT_DEPLIB)
+       $(CC_LINK) $(ALL_CFLAGS) -o test_chpw_message $(OUTPRE)test_chpw_message.$(OBJEXT) $(KRB5_BASE_LIBS) $(LIBS)
+
 test_getsockname: $(OUTPRE)test_getsockname.$(OBJEXT)
        $(CC_LINK) $(ALL_CFLAGS) -o test_getsockname $(OUTPRE)test_getsockname.$(OBJEXT) $(LIBS)
 
@@ -49,5 +57,5 @@ test_cxx_kadm5.$(OBJEXT): test_cxx_kadm5.cpp
 install::
 
 clean::
-       $(RM) test_getpw test_cxx_krb5 test_cxx_gss test_cxx_k5int test_cxx_rpc test_cxx_kadm5 *.o
+       $(RM) test_getpw test_chpw_message test_cxx_krb5 test_cxx_gss test_cxx_k5int test_cxx_rpc test_cxx_kadm5 *.o
 
diff --git a/src/tests/misc/test_chpw_message.c b/src/tests/misc/test_chpw_message.c
new file mode 100644 (file)
index 0000000..bf3169a
--- /dev/null
@@ -0,0 +1,174 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* tests/misc/test_getpw.c */
+/*
+ * Copyright (C) 2012 by the Red Hat Inc.
+ * All rights reserved.
+ *
+ * Export of this software from the United States of America may
+ *   require a specific license from the United States Government.
+ *   It is the responsibility of any person or organization contemplating
+ *   export to obtain such a license before exporting.
+ *
+ * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
+ * distribute this software and its documentation for any purpose and
+ * without fee is hereby granted, provided that the above copyright
+ * notice appear in all copies and that both that copyright notice and
+ * this permission notice appear in supporting documentation, and that
+ * the name of M.I.T. not be used in advertising or publicity pertaining
+ * to distribution of the software without specific, written prior
+ * permission.  Furthermore if you modify this software you must label
+ * your software as modified software and not distribute it in such a
+ * fashion that it might be confused with the original M.I.T. software.
+ * M.I.T. makes no representations about the suitability of
+ * this software for any purpose.  It is provided "as is" without express
+ * or implied warranty.
+ */
+
+#include "autoconf.h"
+#include "krb5.h"
+
+#include <sys/types.h>
+#include <assert.h>
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static krb5_data result_utf8 = {
+    0, 23, "This is a valid string.",
+};
+
+static krb5_data result_invalid_utf8 = {
+    0, 19, "\0This is not valid.",
+};
+
+static krb5_data result_ad_complex = {
+    0, 30,
+    "\0\0"             /* zero bytes */
+    "\0\0\0\0"         /* min length */
+    "\0\0\0\0"         /* history */
+    "\0\0\0\1"         /* properties, complex */
+    "\0\0\0\0\0\0\0\0" /* expire */
+    "\0\0\0\0\0\0\0\0" /* min age */
+};
+
+static krb5_data result_ad_length = {
+    0, 30,
+    "\0\0"             /* zero bytes */
+    "\0\0\0\x0d"       /* min length, 13 charaters */
+    "\0\0\0\0"         /* history */
+    "\0\0\0\0"         /* properties */
+    "\0\0\0\0\0\0\0\0" /* expire */
+    "\0\0\0\0\0\0\0\0" /* min age */
+};
+
+static krb5_data result_ad_history = {
+    0, 30,
+    "\0\0"             /* zero bytes */
+    "\0\0\0\0"         /* min length */
+    "\0\0\0\x09"       /* history, 9 passwords */
+    "\0\0\0\0"         /* properties */
+    "\0\0\0\0\0\0\0\0" /* expire */
+    "\0\0\0\0\0\0\0\0" /* min age */
+};
+
+static krb5_data result_ad_age = {
+    0, 30,
+    "\0\0"                        /* zero bytes */
+    "\0\0\0\0"                    /* min length */
+    "\0\0\0\0"                    /* history, 9 passwords */
+    "\0\0\0\0"                    /* properties */
+    "\0\0\0\0\0\0\0\0"            /* expire */
+    "\0\0\x01\x92\x54\xd3\x80\0"  /* min age, 2 days */
+};
+
+static krb5_data result_ad_all = {
+    0, 30,
+    "\0\0"                      /* zero bytes */
+    "\0\0\0\x05"                /* min length, 5 characters */
+    "\0\0\0\x0D"                /* history, 13 passwords */
+    "\0\0\0\x01"                /* properties, complex */
+    "\0\0\0\0\0\0\0\0"          /* expire */
+    "\0\0\0\xc9\x2a\x69\xc0\0"  /* min age, 1 day */
+};
+
+static void
+check(krb5_error_code code)
+{
+    if (code != 0) {
+        com_err("t_vfy_increds", code, NULL);
+        abort();
+    }
+}
+
+static void
+check_msg(const char *real, const char *expected)
+{
+    if (strstr(real, expected) == NULL) {
+        fprintf(stderr, "Expected to see: %s\n", expected);
+        abort();
+    }
+}
+
+int
+main(void)
+{
+    krb5_context context;
+    char *msg;
+
+    setlocale(LC_MESSAGES, "C");
+
+    check(krb5_init_context(&context));
+
+    /* Valid utf-8 data in the result should be returned as is */
+    check(krb5_chpw_message(context, &result_utf8, &msg));
+    printf("  UTF8 valid:   %s\n", msg);
+    check_msg(msg, "This is a valid string.");
+    free(msg);
+
+    /* Invalid data should have a generic message. */
+    check(krb5_chpw_message(context, &result_invalid_utf8, &msg));
+    printf("  UTF8 invalid: %s\n", msg);
+    check_msg(msg, "contact your administrator");
+    free(msg);
+
+    /* AD data with complex data requirement */
+    check(krb5_chpw_message(context, &result_ad_complex, &msg));
+    printf("  AD complex:   %s\n", msg);
+    check_msg(msg, "The password must include numbers or symbols.");
+    check_msg(msg, "Don't include any part of your name in the password.");
+    free(msg);
+
+    /* AD data with min password length */
+    check(krb5_chpw_message(context, &result_ad_length, &msg));
+    printf("  AD length:    %s\n", msg);
+    check_msg(msg, "The password must contain at least 13 characters.");
+    free(msg);
+
+    /* AD data with history requirements */
+    check(krb5_chpw_message(context, &result_ad_history, &msg));
+    printf("  AD history:   %s\n", msg);
+    check_msg(msg, "The password must be different from the previous 9 "
+              "passwords.");
+    free(msg);
+
+    /* AD data with minimum age */
+    check(krb5_chpw_message(context, &result_ad_age, &msg));
+    printf("  AD min age:   %s\n", msg);
+    check_msg(msg, "The password can only be changed every 2 days.");
+    free(msg);
+
+    /* AD data with all */
+    check(krb5_chpw_message(context, &result_ad_all, &msg));
+    printf("  AD all:       %s\n", msg);
+    check_msg(msg, "The password can only be changed once a day.");
+    check_msg(msg, "The password must be different from the previous 13 "
+              "passwords.");
+    check_msg(msg, "The password must contain at least 5 characters.");
+    check_msg(msg, "The password must include numbers or symbols.");
+    check_msg(msg, "Don't include any part of your name in the password.");
+    free(msg);
+
+    krb5_free_context(context);
+    exit(0);
+}
index fb91133fbb3bb10e57759f1cac49654285634fa7..6eb668dc5a17eca5cbf5a1b5b69a47f79963caf3 100644 (file)
@@ -147,6 +147,7 @@ krb5_sname_to_principal
 krb5_change_password
 krb5_set_password
 krb5_set_password_using_ccache
+krb5_chpw_message
 krb5_get_profile
 krb5_mk_safe
 krb5_mk_priv