From 13a0013f55e9bea8384234f5caa1a0b444749daf Mon Sep 17 00:00:00 2001 From: Greg Hudson Date: Wed, 9 May 2012 17:40:38 +0000 Subject: [PATCH] Add API to interpret changepw result strings 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 | 1 + src/clients/kpasswd/kpasswd.c | 10 +- src/include/krb5/krb5.hin | 26 +++ src/lib/krb5/krb/chpw.c | 166 +++++++++++++++++ src/lib/krb5/krb/gic_pwd.c | 15 +- src/lib/krb5/libkrb5.exports | 1 + src/lib/krb5_32.def | 3 + src/tests/misc/Makefile.in | 16 +- src/tests/misc/test_chpw_message.c | 174 ++++++++++++++++++ .../collected-client-lib/libcollected.exports | 1 + 10 files changed, 399 insertions(+), 14 deletions(-) create mode 100644 src/tests/misc/test_chpw_message.c diff --git a/doc/rst_source/krb_appldev/refs/api/index.rst b/doc/rst_source/krb_appldev/refs/api/index.rst index 8735b91a3..743de67c4 100644 --- a/doc/rst_source/krb_appldev/refs/api/index.rst +++ b/doc/rst_source/krb_appldev/refs/api/index.rst @@ -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 diff --git a/src/clients/kpasswd/kpasswd.c b/src/clients/kpasswd/kpasswd.c index b4b6eadd4..7aed0f1ac 100644 --- a/src/clients/kpasswd/kpasswd.c +++ b/src/clients/kpasswd/kpasswd.c @@ -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); } diff --git a/src/include/krb5/krb5.hin b/src/include/krb5/krb5.hin index c267622db..741477caf 100644 --- a/src/include/krb5/krb5.hin +++ b/src/include/krb5/krb5.hin @@ -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. * diff --git a/src/lib/krb5/krb/chpw.c b/src/lib/krb5/krb/chpw.c index 7e43dcf1a..beb77cb98 100644 --- a/src/lib/krb5/krb/chpw.c +++ b/src/lib/krb5/krb/chpw.c @@ -5,6 +5,7 @@ #include #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; +} diff --git a/src/lib/krb5/krb/gic_pwd.c b/src/lib/krb5/krb/gic_pwd.c index 68d28fe9d..b25eb6da0 100644 --- a/src/lib/krb5/krb/gic_pwd.c +++ b/src/lib/krb5/krb/gic_pwd.c @@ -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); } diff --git a/src/lib/krb5/libkrb5.exports b/src/lib/krb5/libkrb5.exports index cb73a1471..6319c72a8 100644 --- a/src/lib/krb5/libkrb5.exports +++ b/src/lib/krb5/libkrb5.exports @@ -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 diff --git a/src/lib/krb5_32.def b/src/lib/krb5_32.def index d5922d2d1..bfc211535 100644 --- a/src/lib/krb5_32.def +++ b/src/lib/krb5_32.def @@ -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 diff --git a/src/tests/misc/Makefile.in b/src/tests/misc/Makefile.in index 5a4b329bc..8a601bf0d 100644 --- a/src/tests/misc/Makefile.in +++ b/src/tests/misc/Makefile.in @@ -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 index 000000000..bf3169a25 --- /dev/null +++ b/src/tests/misc/test_chpw_message.c @@ -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 +#include +#include +#include +#include +#include + +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); +} diff --git a/src/util/collected-client-lib/libcollected.exports b/src/util/collected-client-lib/libcollected.exports index fb91133fb..6eb668dc5 100644 --- a/src/util/collected-client-lib/libcollected.exports +++ b/src/util/collected-client-lib/libcollected.exports @@ -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 -- 2.26.2