krb5 commit: Pass PKINIT identity prompts to the responder cb

Greg Hudson ghudson at MIT.EDU
Thu Jul 18 00:58:56 EDT 2013


https://github.com/krb5/krb5/commit/e8b63198029c632d097822104d6e17c9a67ef1a5
commit e8b63198029c632d097822104d6e17c9a67ef1a5
Author: Nalin Dahyabhai <nalin at dahyabhai.net>
Date:   Mon Jul 15 13:11:00 2013 -0400

    Pass PKINIT identity prompts to the responder cb
    
    Use the list of deferred identity prompts and warnings, which we have
    after calling pkinit_identity_initialize(), to build a list of questions
    to supply to responder callbacks.
    
    Before calling pkinit_identity_prompt() to actually load identities that
    are protected, save any passwords and PINs which a responder callback
    may have supplied.
    
    Because pkinit_client_prep_questions() can be called multiple times, and
    we don't want to try to load all of our identities each of those times,
    take some steps to ensure that we only call pkinit_identity_initialize()
    and pkinit_identity_prompt() once per request.
    
    ticket: 7680

 src/include/krb5/krb5.hin                |   44 ++++++
 src/plugins/preauth/pkinit/pkinit.h      |    3 +
 src/plugins/preauth/pkinit/pkinit_clnt.c |  234 +++++++++++++++++++++++++++---
 3 files changed, 262 insertions(+), 19 deletions(-)

diff --git a/src/include/krb5/krb5.hin b/src/include/krb5/krb5.hin
index 270ad82..faeabc7 100644
--- a/src/include/krb5/krb5.hin
+++ b/src/include/krb5/krb5.hin
@@ -6485,6 +6485,50 @@ krb5_prompter_posix(krb5_context context, void *data, const char *name,
 #define KRB5_RESPONDER_OTP_FLAGS_SEPARATE_PIN  0x0008
 
 /**
+ * PKINIT responder question
+ *
+ * The PKINIT responder question is asked when the client needs a password
+ * that's being used to protect key information, and is formatted as a JSON
+ * object.  A specific identity's flags value, if not zero, is the bitwise-OR
+ * of one or more of the KRB5_RESPONDER_PKINIT_FLAGS_TOKEN_* flags defined
+ * below, and possibly other flags to be added later.  Any resemblance to
+ * similarly-named CKF_* values in the PKCS#11 API should not be depended on.
+ *
+ *  @n {
+ *  @n     identity <string> : flags <number>,
+ *  @n     ...
+ *  @n }
+ *
+ * The answer to the question MUST be JSON formatted:
+ *
+ *  @n {
+ *  @n     identity <string> : password <string>,
+ *  @n     ...
+ *  @n }
+ *
+ * @version New in 1.12
+ */
+#define KRB5_RESPONDER_QUESTION_PKINIT "pkinit"
+
+/**
+ * This flag indicates that an incorrect PIN was supplied at least once since
+ * the last time the correct PIN was supplied.
+ */
+#define KRB5_RESPONDER_PKINIT_FLAGS_TOKEN_USER_PIN_COUNT_LOW       (1 << 0)
+
+/**
+ * This flag indicates that supplying an incorrect PIN will cause the token to
+ * lock itself.
+ */
+#define KRB5_RESPONDER_PKINIT_FLAGS_TOKEN_USER_PIN_FINAL_TRY       (1 << 1)
+
+/**
+ * This flag indicates that the user PIN is locked, and you can't log in to the
+ * token with it.
+ */
+#define KRB5_RESPONDER_PKINIT_FLAGS_TOKEN_USER_PIN_LOCKED          (1 << 2)
+
+/**
  * A container for a set of preauthentication questions and answers
  *
  * A responder context is supplied by the krb5 authentication system to a @ref
diff --git a/src/plugins/preauth/pkinit/pkinit.h b/src/plugins/preauth/pkinit/pkinit.h
index 38a43f5..f9e1485 100644
--- a/src/plugins/preauth/pkinit/pkinit.h
+++ b/src/plugins/preauth/pkinit/pkinit.h
@@ -231,6 +231,9 @@ struct _pkinit_req_context {
     int do_identity_matching;
     krb5_preauthtype pa_type;
     int rfc6112_kdc;
+    int identity_initialized;
+    int identity_prompted;
+    krb5_error_code identity_prompt_retval;
 };
 typedef struct _pkinit_req_context *pkinit_req_context;
 
diff --git a/src/plugins/preauth/pkinit/pkinit_clnt.c b/src/plugins/preauth/pkinit/pkinit_clnt.c
index 748b25e..f708856 100644
--- a/src/plugins/preauth/pkinit/pkinit_clnt.c
+++ b/src/plugins/preauth/pkinit/pkinit_clnt.c
@@ -40,6 +40,7 @@
 #include <sys/stat.h>
 
 #include "pkinit.h"
+#include "k5-json.h"
 
 /*
  * It is anticipated that all the special checks currently
@@ -1050,6 +1051,188 @@ pkinit_client_profile(krb5_context context,
     }
 }
 
+/*
+ * Convert a PKCS11 token flags value to the subset that we're interested in
+ * passing along to our API callers.
+ */
+static long long
+pkinit_client_get_token_flags(unsigned long pkcs11_token_flags)
+{
+    long long flags = 0;
+
+    if (pkcs11_token_flags & CKF_USER_PIN_COUNT_LOW)
+        flags |= KRB5_RESPONDER_PKINIT_FLAGS_TOKEN_USER_PIN_COUNT_LOW;
+    if (pkcs11_token_flags & CKF_USER_PIN_FINAL_TRY)
+        flags |= KRB5_RESPONDER_PKINIT_FLAGS_TOKEN_USER_PIN_FINAL_TRY;
+    if (pkcs11_token_flags & CKF_USER_PIN_LOCKED)
+        flags |= KRB5_RESPONDER_PKINIT_FLAGS_TOKEN_USER_PIN_LOCKED;
+    return flags;
+}
+
+/*
+ * Phase one of loading client identity information - call
+ * identity_initialize() to load any identities which we can without requiring
+ * help from the calling user, and use their names of those which we can't load
+ * to construct the challenge for the responder callback.
+ */
+static krb5_error_code
+pkinit_client_prep_questions(krb5_context context,
+                             krb5_clpreauth_moddata moddata,
+                             krb5_clpreauth_modreq modreq,
+                             krb5_get_init_creds_opt *opt,
+                             krb5_clpreauth_callbacks cb,
+                             krb5_clpreauth_rock rock,
+                             krb5_kdc_req *request,
+                             krb5_data *encoded_request_body,
+                             krb5_data *encoded_previous_request,
+                             krb5_pa_data *pa_data)
+{
+    krb5_error_code retval;
+    pkinit_context plgctx = (pkinit_context)moddata;
+    pkinit_req_context reqctx = (pkinit_req_context)modreq;
+    int i, n;
+    const pkinit_deferred_id *deferred_ids;
+    const char *identity;
+    unsigned long ck_flags;
+    char *encoded;
+    k5_json_object jval = NULL;
+    k5_json_number jflag = NULL;
+
+    if (!reqctx->identity_initialized) {
+        pkinit_client_profile(context, plgctx, reqctx, cb, rock,
+                              &request->server->realm);
+        retval = pkinit_identity_initialize(context, plgctx->cryptoctx,
+                                            reqctx->cryptoctx, reqctx->idopts,
+                                            reqctx->idctx, cb, rock,
+                                            request->client);
+        if (retval != 0) {
+            TRACE_PKINIT_CLIENT_NO_IDENTITY(context);
+            pkiDebug("pkinit_identity_initialize returned %d (%s)\n",
+                     retval, error_message(retval));
+        }
+
+        reqctx->identity_initialized = TRUE;
+        crypto_free_cert_info(context, plgctx->cryptoctx,
+                              reqctx->cryptoctx, reqctx->idctx);
+        if (retval != 0) {
+            pkiDebug("%s: not asking responder question\n", __FUNCTION__);
+            retval = 0;
+            goto cleanup;
+        }
+    }
+
+    deferred_ids = crypto_get_deferred_ids(context, reqctx->idctx);
+    for (i = 0; deferred_ids != NULL && deferred_ids[i] != NULL; i++)
+        continue;
+    n = i;
+
+    /* Create the top-level object. */
+    retval = k5_json_object_create(&jval);
+    if (retval != 0)
+        goto cleanup;
+
+    for (i = 0; i < n; i++) {
+        /* Add an entry to the top-level object for the identity. */
+        identity = deferred_ids[i]->identity;
+        ck_flags = deferred_ids[i]->ck_flags;
+        /* Calculate the flags value for the bits that that we care about. */
+        retval = k5_json_number_create(pkinit_client_get_token_flags(ck_flags),
+                                       &jflag);
+        if (retval != 0)
+            goto cleanup;
+        retval = k5_json_object_set(jval, identity, jflag);
+        if (retval != 0)
+            goto cleanup;
+        k5_json_release(jflag);
+        jflag = NULL;
+    }
+
+    /* Encode and done. */
+    retval = k5_json_encode(jval, &encoded);
+    if (retval == 0) {
+        cb->ask_responder_question(context, rock,
+                                   KRB5_RESPONDER_QUESTION_PKINIT,
+                                   encoded);
+        pkiDebug("%s: asking question '%s'\n", __FUNCTION__, encoded);
+        free(encoded);
+    }
+
+cleanup:
+    k5_json_release(jval);
+    k5_json_release(jflag);
+
+    pkiDebug("%s returning %d\n", __FUNCTION__, retval);
+
+    return retval;
+}
+
+/*
+ * Parse data supplied by the application's responder callback, saving off any
+ * PINs and passwords for identities which we noted needed them.
+ */
+struct save_one_password_data {
+    krb5_context context;
+    krb5_clpreauth_modreq modreq;
+    const char *caller;
+};
+
+static void
+save_one_password(void *arg, const char *key, k5_json_value val)
+{
+    struct save_one_password_data *data = arg;
+    pkinit_req_context reqctx = (pkinit_req_context)data->modreq;
+    const char *password;
+
+    if (k5_json_get_tid(val) == K5_JSON_TID_STRING) {
+        password = k5_json_string_utf8(val);
+        pkiDebug("%s: \"%s\": %p\n", data->caller, key, password);
+        crypto_set_deferred_id(data->context, reqctx->idctx, key, password);
+    }
+}
+
+static krb5_error_code
+pkinit_client_parse_answers(krb5_context context,
+                            krb5_clpreauth_moddata moddata,
+                            krb5_clpreauth_modreq modreq,
+                            krb5_clpreauth_callbacks cb,
+                            krb5_clpreauth_rock rock)
+{
+    krb5_error_code retval;
+    const char *encoded;
+    k5_json_value jval;
+    struct save_one_password_data data;
+
+    data.context = context;
+    data.modreq = modreq;
+    data.caller = __FUNCTION__;
+
+    encoded = cb->get_responder_answer(context, rock,
+                                       KRB5_RESPONDER_QUESTION_PKINIT);
+    if (encoded == NULL)
+        return 0;
+
+    pkiDebug("pkinit_client_parse_answers: %s\n", encoded);
+
+    retval = k5_json_decode(encoded, &jval);
+    if (retval != 0)
+        goto cleanup;
+
+    /* Expect that the top-level answer is an object. */
+    if (k5_json_get_tid(jval) != K5_JSON_TID_OBJECT) {
+        retval = EINVAL;
+        goto cleanup;
+    }
+
+    /* Store the passed-in per-identity passwords. */
+    k5_json_object_iterate(jval, &save_one_password, &data);
+    retval = 0;
+
+cleanup:
+    if (jval != NULL)
+        k5_json_release(jval);
+    return retval;
+}
+
 static krb5_error_code
 pkinit_client_process(krb5_context context, krb5_clpreauth_moddata moddata,
                       krb5_clpreauth_modreq modreq,
@@ -1107,29 +1290,41 @@ pkinit_client_process(krb5_context context, krb5_clpreauth_moddata moddata,
     if (processing_request) {
         pkinit_client_profile(context, plgctx, reqctx, cb, rock,
                               &request->server->realm);
-        retval = pkinit_identity_initialize(context, plgctx->cryptoctx, NULL,
-                                            reqctx->idopts, reqctx->idctx,
-                                            cb, rock, request->client);
+        /* Pull in PINs and passwords for identities which we deferred
+         * loading earlier. */
+        retval = pkinit_client_parse_answers(context, moddata, modreq,
+                                             cb, rock);
         if (retval) {
-            TRACE_PKINIT_CLIENT_NO_IDENTITY(context);
-            pkiDebug("pkinit_identity_prompt returned %d (%s)\n",
-                     retval, error_message(retval));
+            if (retval == KRB5KRB_ERR_GENERIC)
+                pkiDebug("pkinit responder answers were invalid\n");
             return retval;
         }
-        /*
-         * Load identities (again, potentially), prompting, if we can, for
-         * anything for which we didn't get an answer from the responder
-         * callback.
-         */
-        pkinit_identity_set_prompter(reqctx->idctx, prompter, prompter_data);
-        retval = pkinit_identity_prompt(context, plgctx->cryptoctx,
-                                        reqctx->cryptoctx, reqctx->idopts,
-                                        reqctx->idctx, cb, rock,
-                                        reqctx->do_identity_matching,
-                                        request->client);
-        if (retval) {
+        if (!reqctx->identity_prompted) {
+            reqctx->identity_prompted = TRUE;
+            /*
+             * Load identities (again, potentially), prompting, if we can, for
+             * anything for which we didn't get an answer from the responder
+             * callback.
+             */
+            pkinit_identity_set_prompter(reqctx->idctx, prompter,
+                                         prompter_data);
+            retval = pkinit_identity_prompt(context, plgctx->cryptoctx,
+                                            reqctx->cryptoctx, reqctx->idopts,
+                                            reqctx->idctx, cb, rock,
+                                            reqctx->do_identity_matching,
+                                            request->client);
+            pkinit_identity_set_prompter(reqctx->idctx, NULL, NULL);
+            reqctx->identity_prompt_retval = retval;
+            if (retval) {
+                TRACE_PKINIT_CLIENT_NO_IDENTITY(context);
+                pkiDebug("pkinit_identity_prompt returned %d (%s)\n",
+                         retval, error_message(retval));
+                return retval;
+            }
+        } else if (reqctx->identity_prompt_retval) {
+            retval = reqctx->identity_prompt_retval;
             TRACE_PKINIT_CLIENT_NO_IDENTITY(context);
-            pkiDebug("pkinit_identity_prompt returned %d (%s)\n",
+            pkiDebug("pkinit_identity_prompt previously returned %d (%s)\n",
                      retval, error_message(retval));
             return retval;
         }
@@ -1506,6 +1701,7 @@ clpreauth_pkinit_initvt(krb5_context context, int maj_ver, int min_ver,
     vt->fini = pkinit_client_plugin_fini;
     vt->flags = pkinit_client_get_flags;
     vt->request_init = pkinit_client_req_init;
+    vt->prep_questions = pkinit_client_prep_questions;
     vt->request_fini = pkinit_client_req_fini;
     vt->process = pkinit_client_process;
     vt->tryagain = pkinit_client_tryagain;


More information about the cvs-krb5 mailing list