krb5 commit: Add certauth pluggable interface

Greg Hudson <[email protected]>
Newsgroups gmane.comp.encryption.kerberos.cvs
Message-ID <[email protected]>
https://github.com/krb5/krb5/commit/b619ce84470519bea65470be3263cd85fba94f57
commit b619ce84470519bea65470be3263cd85fba94f57
Author: Matt Rogers <[email protected]>
Date:   Tue Feb 28 15:55:24 2017 -0500

    Add certauth pluggable interface
    
    Add the header include/krb5/certauth_plugin.h, defining a pluggable
    interface to control authorization of PKINIT client certificates.
    
    Add the "pkinit_san" and "pkinit_eku" builtin certauth modules and
    related PKINIT crypto X.509 helper functions.  Add authorize_cert() as
    the entry function for certauth plugin module checks called in
    pkinit_server_verify_padata().  Modify kdcpreauth_moddata to hold the
    list of certauth module handles, and load the modules when the PKINIT
    kdcpreauth server plugin is initialized.  Change
    crypto_retrieve_X509_sans() to return ENOENT when no SAN is found.
    
    Add test modules in plugins/certauth/test.  Create t_certauth.py with
    basic certauth tests.  Add plugin interface documentation in
    doc/plugindev/certauth.rst and doc/admin/krb5_conf.rst.
    
    [[email protected]: simplified code, edited docs]
    
    ticket: 8561 (new)

 doc/admin/conf_files/krb5_conf.rst                 |   21 ++
 doc/plugindev/certauth.rst                         |   27 ++
 doc/plugindev/index.rst                            |    1 +
 src/Makefile.in                                    |    1 +
 src/configure.in                                   |    1 +
 src/include/Makefile.in                            |    1 +
 src/include/k5-int.h                               |    3 +-
 src/include/krb5/certauth_plugin.h                 |  103 ++++++
 src/lib/krb5/krb/plugin.c                          |    3 +-
 src/plugins/certauth/test/Makefile.in              |   20 ++
 src/plugins/certauth/test/certauth_test.exports    |    2 +
 src/plugins/certauth/test/deps                     |   14 +
 src/plugins/certauth/test/main.c                   |  209 ++++++++++++
 src/plugins/preauth/pkinit/pkinit_crypto.h         |    4 +
 src/plugins/preauth/pkinit/pkinit_crypto_openssl.c |   30 ++
 src/plugins/preauth/pkinit/pkinit_srv.c            |  335 +++++++++++++++++---
 src/plugins/preauth/pkinit/pkinit_trace.h          |    5 +
 src/tests/Makefile.in                              |    1 +
 src/tests/t_certauth.py                            |   47 +++
 19 files changed, 786 insertions(+), 42 deletions(-)

diff --git a/doc/admin/conf_files/krb5_conf.rst b/doc/admin/conf_files/krb5_conf.rst
index 653aad6..c0e4349 100644
--- a/doc/admin/conf_files/krb5_conf.rst
+++ b/doc/admin/conf_files/krb5_conf.rst
@@ -858,6 +858,27 @@ built-in modules exist for this interface:
     This module authorizes a principal to a local account if the
     principal name maps to the local account name.
 
+.. _certauth:
+
+certauth interface
+##################
+
+The certauth section (introduced in release 1.16) controls modules for
+the certificate authorization interface, which determines whether a
+certificate is allowed to preauthenticate a user via PKINIT.  The
+following built-in modules exist for this interface:
+
+**pkinit_san**
+    This module authorizes the certificate if it contains a PKINIT
+    Subject Alternative Name for the requested client principal, or a
+    Microsoft UPN SAN matching the principal if **pkinit_allow_upn**
+    is set to true for the realm.
+
+**pkinit_eku**
+    This module rejects the certificate if it does not contain an
+    Extended Key Usage attribute consistent with the
+    **pkinit_eku_checking** value for the realm.
+
 
 PKINIT options
 --------------
diff --git a/doc/plugindev/certauth.rst b/doc/plugindev/certauth.rst
new file mode 100644
index 0000000..8a7f7c5
--- /dev/null
+++ b/doc/plugindev/certauth.rst
@@ -0,0 +1,27 @@
+.. _certauth_plugin:
+
+PKINIT certificate authorization interface (certauth)
+=====================================================
+
+The certauth interface was first introduced in release 1.16.  It
+allows customization of the X.509 certificate attribute requirements
+placed on certificates used by PKINIT enabled clients.  For a detailed
+description of the certauth interface, see the header file
+``<krb5/certauth_plugin.h>``
+
+A certauth module implements the **authorize** method to determine
+whether a client's certificate is authorized to authenticate a client
+principal.  **authorize** receives the DER-encoded certificate, the
+requested client principal, and a pointer to the client's
+krb5_db_entry (for modules that link against libkdb5).  It returns the
+authorization status and optionally outputs a list of authentication
+indicator strings to be added to the ticket.  A module must use its
+own internal or library-provided ASN.1 certificate decoder.
+
+A module can optionally create and destroy module data with the
+**init** and **fini** methods.  Module data objects last for the
+lifetime of the KDC process.
+
+If a module allocates and returns a list of authentication indicators
+from **authorize**, it must also implement the **free_ind** method
+to free the list.
diff --git a/doc/plugindev/index.rst b/doc/plugindev/index.rst
index 3fb9217..67dbc27 100644
--- a/doc/plugindev/index.rst
+++ b/doc/plugindev/index.rst
@@ -31,5 +31,6 @@ Contents
    profile.rst
    gssapi.rst
    internal.rst
+   certauth.rst
 
 .. TODO: GSSAPI mechanism plugins
diff --git a/src/Makefile.in b/src/Makefile.in
index 245366f..a057f7c 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -17,6 +17,7 @@ SUBDIRS=util include lib \
 	plugins/pwqual/test \
 	plugins/authdata/greet_server \
 	plugins/authdata/greet_client \
+	plugins/certauth/test \
 	plugins/kdb/db2 \
 	@ldap_plugin_dir@ \
 	plugins/kdb/test \
diff --git a/src/configure.in b/src/configure.in
index 853e7bb..fa4bc84 100644
--- a/src/configure.in
+++ b/src/configure.in
@@ -1458,6 +1458,7 @@ dnl	ccapi ccapi/lib ccapi/lib/unix ccapi/server ccapi/server/unix ccapi/test
 
 	kdc slave config-files build-tools man doc include
 
+	plugins/certauth/test
 	plugins/hostrealm/test
 	plugins/localauth/test
 	plugins/kadm5_hook/test
diff --git a/src/include/Makefile.in b/src/include/Makefile.in
index f5b9218..0239338 100644
--- a/src/include/Makefile.in
+++ b/src/include/Makefile.in
@@ -140,6 +140,7 @@ install-headers-unix install: krb5/krb5.h profile.h
 	$(INSTALL_DATA) $(srcdir)/krb5.h $(DESTDIR)$(KRB5_INCDIR)$(S)krb5.h
 	$(INSTALL_DATA) $(srcdir)/kdb.h $(DESTDIR)$(KRB5_INCDIR)$(S)kdb.h
 	$(INSTALL_DATA) krb5/krb5.h $(DESTDIR)$(KRB5_INCDIR)$(S)krb5$(S)krb5.h
+	$(INSTALL_DATA) $(srcdir)/krb5/certauth_plugin.h $(DESTDIR)$(KRB5_INCDIR)$(S)krb5$(S)certauth_plugin.h
 	$(INSTALL_DATA) $(srcdir)/krb5/ccselect_plugin.h $(DESTDIR)$(KRB5_INCDIR)$(S)krb5$(S)ccselect_plugin.h
 	$(INSTALL_DATA) $(srcdir)/krb5/clpreauth_plugin.h $(DESTDIR)$(KRB5_INCDIR)$(S)krb5$(S)clpreauth_plugin.h
 	$(INSTALL_DATA) $(srcdir)/krb5/hostrealm_plugin.h $(DESTDIR)$(KRB5_INCDIR)$(S)krb5$(S)hostrealm_plugin.h
diff --git a/src/include/k5-int.h b/src/include/k5-int.h
index 2d1d293..ddeebc4 100644
--- a/src/include/k5-int.h
+++ b/src/include/k5-int.h
@@ -1155,7 +1155,8 @@ struct plugin_interface {
 #define PLUGIN_INTERFACE_AUDIT       7
 #define PLUGIN_INTERFACE_TLS         8
 #define PLUGIN_INTERFACE_KDCAUTHDATA 9
-#define PLUGIN_NUM_INTERFACES        10
+#define PLUGIN_INTERFACE_CERTAUTH    10
+#define PLUGIN_NUM_INTERFACES        11
 
 /* Retrieve the plugin module of type interface_id and name modname,
  * storing the result into module. */
diff --git a/src/include/krb5/certauth_plugin.h b/src/include/krb5/certauth_plugin.h
new file mode 100644
index 0000000..f22fc1e
--- /dev/null
+++ b/src/include/krb5/certauth_plugin.h
@@ -0,0 +1,103 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* include/krb5/certauth_plugin.h - certauth plugin header. */
+/*
+ * Copyright (C) 2017 by Red Hat, Inc.
+ * 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.
+ */
+
+/*
+ * Certificate authorization plugin interface.  The PKINIT server module uses
+ * this interface to check client certificate attributes after the certificate
+ * signature has been verified.
+ */
+#ifndef KRB5_CERTAUTH_PLUGIN_H
+#define KRB5_CERTAUTH_PLUGIN_H
+
+#include <krb5/krb5.h>
+#include <krb5/plugin.h>
+
+/* Abstract module data type. */
+typedef struct krb5_certauth_moddata_st *krb5_certauth_moddata;
+
+typedef struct _krb5_db_entry_new krb5_db_entry;
+
+/*
+ * Optional: Initialize module data.
+ */
+typedef krb5_error_code
+(*krb5_certauth_init_fn)(krb5_context context,
+                         krb5_certauth_moddata *moddata_out);
+
+/*
+ * Optional: Clean up the module data.
+ */
+typedef void
+(*krb5_certauth_fini_fn)(krb5_context context, krb5_certauth_moddata moddata);
+
+/*
+ * Mandatory:
+ * Return 0 if the DER-encoded cert is authorized for PKINIT authentication by
+ * princ; otherwise return one of the following error codes:
+ * - KRB5KDC_ERR_CLIENT_NAME_MISMATCH - incorrect SAN value
+ * - KRB5KDC_ERR_INCONSISTENT_KEY_PURPOSE - incorrect EKU
+ * - KRB5KDC_ERR_CERTIFICATE_MISMATCH - other extension error
+ * - KRB5_PLUGIN_NO_HANDLE - the module has no opinion about cert
+ *
+ * - opts is used by built-in modules to receive internal data, and must be
+ *   ignored by other modules.
+ * - db_entry receives the client principal database entry, and can be ignored
+ *   by modules that do not link with libkdb5.
+ * - *authinds_out optionally returns a null-terminated list of authentication
+ *   indicator strings upon KRB5_PLUGIN_NO_HANDLE or accepted authorization.
+ */
+typedef krb5_error_code
+(*krb5_certauth_authorize_fn)(krb5_context context,
+                              krb5_certauth_moddata moddata,
+                              const uint8_t *cert, size_t cert_len,
+                              krb5_const_principal princ, const void *opts,
+                              const krb5_db_entry *db_entry,
+                              char ***authinds_out);
+
+/*
+ * Free indicators allocated by a module.  Mandatory if authorize returns
+ * authentication indicators.
+ */
+typedef void
+(*krb5_certauth_free_indicator_fn)(krb5_context context,
+                                   krb5_certauth_moddata moddata,
+                                   char **authinds);
+
+typedef struct krb5_certauth_vtable_st {
+    char *name;
+    krb5_certauth_init_fn init;
+    krb5_certauth_fini_fn fini;
+    krb5_certauth_authorize_fn authorize;
+    krb5_certauth_free_indicator_fn free_ind;
+} *krb5_certauth_vtable;
+
+#endif /* KRB5_CERTAUTH_PLUGIN_H */
diff --git a/src/lib/krb5/krb/plugin.c b/src/lib/krb5/krb/plugin.c
index 7d64b7c..17dd6bd 100644
--- a/src/lib/krb5/krb/plugin.c
+++ b/src/lib/krb5/krb/plugin.c
@@ -57,7 +57,8 @@ const char *interface_names[] = {
     "hostrealm",
     "audit",
     "tls",
-    "kdcauthdata"
+    "kdcauthdata",
+    "certauth"
 };
 
 /* Return the context's interface structure for id, or NULL if invalid. */
diff --git a/src/plugins/certauth/test/Makefile.in b/src/plugins/certauth/test/Makefile.in
new file mode 100644
index 0000000..d352408
--- /dev/null
+++ b/src/plugins/certauth/test/Makefile.in
@@ -0,0 +1,20 @@
+mydir=plugins$(S)certauth$(S)test
+BUILDTOP=$(REL)..$(S)..$(S)..
+
+LIBBASE=certauth_test
+LIBMAJOR=0
+LIBMINOR=0
+RELDIR=../plugins/certauth/test
+SHLIB_EXPDEPS=$(KRB5_BASE_DEPLIBS)
+SHLIB_EXPLIBS=$(KRB5_BASE_LIBS)
+
+STLIBOBJS=main.o
+
+SRCS=$(srcdir)/main.c
+
+all-unix: all-libs
+install-unix:
+clean-unix:: clean-libs clean-libobjs
+
+@libnover_frag@
+@libobj_frag@
diff --git a/src/plugins/certauth/test/certauth_test.exports b/src/plugins/certauth/test/certauth_test.exports
new file mode 100644
index 0000000..1c8cd24
--- /dev/null
+++ b/src/plugins/certauth/test/certauth_test.exports
@@ -0,0 +1,2 @@
+certauth_test1_initvt
+certauth_test2_initvt
diff --git a/src/plugins/certauth/test/deps b/src/plugins/certauth/test/deps
new file mode 100644
index 0000000..2974b3b
--- /dev/null
+++ b/src/plugins/certauth/test/deps
@@ -0,0 +1,14 @@
+#
+# Generated makefile dependencies follow.
+#
+main.so main.po $(OUTPRE)main.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \
+  $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(top_srcdir)/include/k5-buf.h \
+  $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \
+  $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h \
+  $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h \
+  $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \
+  $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \
+  $(top_srcdir)/include/krb5/certauth_plugin.h $(top_srcdir)/include/krb5/plugin.h \
+  $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \
+  main.c
diff --git a/src/plugins/certauth/test/main.c b/src/plugins/certauth/test/main.c
new file mode 100644
index 0000000..7ef7377
--- /dev/null
+++ b/src/plugins/certauth/test/main.c
@@ -0,0 +1,209 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* plugins/certauth/main.c - certauth plugin test modules. */
+/*
+ * Copyright (C) 2017 by Red Hat, Inc.
+ * 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.
+ */
+
+#include <k5-int.h>
+#include "krb5/certauth_plugin.h"
+
+struct krb5_certauth_moddata_st {
+    int initialized;
+};
+
+/* Test module 1 returns OK with an indicator. */
+static krb5_error_code
+test1_authorize(krb5_context context, krb5_certauth_moddata moddata,
+                const uint8_t *cert, size_t cert_len,
+                krb5_const_principal princ, const void *opts,
+                const krb5_db_entry *db_entry, char ***authinds_out)
+{
+    char **ais = NULL;
+
+    ais = calloc(2, sizeof(*ais));
+    assert(ais != NULL);
+    ais[0] = strdup("test1");
+    assert(ais[0] != NULL);
+    *authinds_out = ais;
+    return KRB5_PLUGIN_NO_HANDLE;
+}
+
+static void
+test_free_ind(krb5_context context, krb5_certauth_moddata moddata,
+              char **authinds)
+{
+    size_t i;
+
+    if (authinds == NULL)
+        return;
+    for (i = 0; authinds[i] != NULL; i++)
+        free(authinds[i]);
+    free(authinds);
+}
+
+/* A basic moddata test. */
+static krb5_error_code
+test2_init(krb5_context context, krb5_certauth_moddata *moddata_out)
+{
+    krb5_certauth_moddata mod;
+
+    mod = calloc(1, sizeof(*mod));
+    assert(mod != NULL);
+    mod->initialized = 1;
+    *moddata_out = mod;
+    return 0;
+}
+
+static void
+test2_fini(krb5_context context, krb5_certauth_moddata moddata)
+{
+    free(moddata);
+}
+
+/* Return true if cert appears to contain the CN name, based on a search of the
+ * DER encoding. */
+static krb5_boolean
+has_cn(krb5_context context, const uint8_t *cert, size_t cert_len,
+       const char *name)
+{
+    krb5_boolean match = FALSE;
+    uint8_t name_len, cntag[5] = "\x06\x03\x55\x04\x03";
+    const uint8_t *c;
+    struct k5buf buf;
+    size_t c_left;
+
+    /* Construct a DER search string of the CN AttributeType encoding followed
+     * by a UTF8String encoding containing name as the AttributeValue. */
+    k5_buf_init_dynamic(&buf);
+    k5_buf_add_len(&buf, cntag, sizeof(cntag));
+    k5_buf_add(&buf, "\x0C");
+    assert(strlen(name) < 128);
+    name_len = strlen(name);
+    k5_buf_add_len(&buf, &name_len, 1);
+    k5_buf_add_len(&buf, name, name_len);
+    assert(k5_buf_status(&buf) == 0);
+
+    /* Check for the CN needle in the certificate haystack. */
+    c_left = cert_len;
+    c = memchr(cert, *cntag, c_left);
+    while (c != NULL) {
+        c_left = cert_len - (c - cert);
+        if (buf.len > c_left)
+            break;
+        if (memcmp(c, buf.data, buf.len) == 0) {
+            match = TRUE;
+            break;
+        }
+        assert(c_left >= 1);
+        c = memchr(c + 1, *cntag, c_left - 1);
+    }
+
+    k5_buf_free(&buf);
+    return match;
+}
+
+/*
+ * Test module 2 returns OK if princ matches the CN part of the subject name,
+ * and returns indicators of the module name and princ.
+ */
+static krb5_error_code
+test2_authorize(krb5_context context, krb5_certauth_moddata moddata,
+                const uint8_t *cert, size_t cert_len,
+                krb5_const_principal princ, const void *opts,
+                const krb5_db_entry *db_entry, char ***authinds_out)
+{
+    krb5_error_code ret;
+    char *name = NULL, **ais = NULL;
+
+    *authinds_out = NULL;
+
+    assert(moddata != NULL && moddata->initialized);
+
+    ret = krb5_unparse_name_flags(context, princ,
+                                  KRB5_PRINCIPAL_UNPARSE_NO_REALM, &name);
+    if (ret)
+        goto cleanup;
+
+    if (!has_cn(context, cert, cert_len, name)) {
+        ret = KRB5KDC_ERR_CERTIFICATE_MISMATCH;
+        goto cleanup;
+    }
+
+    /* Create an indicator list with the module name and CN. */
+    ais = calloc(3, sizeof(*ais));
+    assert(ais != NULL);
+    ais[0] = strdup("test2");
+    ais[1] = strdup(name);
+    assert(ais[0] != NULL && ais[1] != NULL);
+    *authinds_out = ais;
+
+    ais = NULL;
+
+cleanup:
+    krb5_free_unparsed_name(context, name);
+    return ret;
+}
+
+krb5_error_code
+certauth_test1_initvt(krb5_context context, int maj_ver, int min_ver,
+                      krb5_plugin_vtable vtable);
+krb5_error_code
+certauth_test1_initvt(krb5_context context, int maj_ver, int min_ver,
+                      krb5_plugin_vtable vtable)
+{
+    krb5_certauth_vtable vt;
+
+    if (maj_ver != 1)
+        return KRB5_PLUGIN_VER_NOTSUPP;
+    vt = (krb5_certauth_vtable)vtable;
+    vt->name = "test1";
+    vt->authorize = test1_authorize;
+    vt->free_ind = test_free_ind;
+    return 0;
+}
+
+krb5_error_code
+certauth_test2_initvt(krb5_context context, int maj_ver, int min_ver,
+                      krb5_plugin_vtable vtable);
+krb5_error_code
+certauth_test2_initvt(krb5_context context, int maj_ver, int min_ver,
+                      krb5_plugin_vtable vtable)
+{
+    krb5_certauth_vtable vt;
+
+    if (maj_ver != 1)
+        return KRB5_PLUGIN_VER_NOTSUPP;
+    vt = (krb5_certauth_vtable)vtable;
+    vt->name = "test2";
+    vt->authorize = test2_authorize;
+    vt->init = test2_init;
+    vt->fini = test2_fini;
+    vt->free_ind = test_free_ind;
+    return 0;
+}
diff --git a/src/plugins/preauth/pkinit/pkinit_crypto.h b/src/plugins/preauth/pkinit/pkinit_crypto.h
index b483aff..49b96b8 100644
--- a/src/plugins/preauth/pkinit/pkinit_crypto.h
+++ b/src/plugins/preauth/pkinit/pkinit_crypto.h
@@ -664,4 +664,8 @@ extern const size_t  krb5_pkinit_sha512_oid_len;
  */
 extern krb5_data const * const supported_kdf_alg_ids[];
 
+krb5_error_code
+crypto_encode_der_cert(krb5_context context, pkinit_req_crypto_context reqctx,
+		       uint8_t **der_out, size_t *der_len);
+
 #endif	/* _PKINIT_CRYPTO_H */
diff --git a/src/plugins/preauth/pkinit/pkinit_crypto_openssl.c b/src/plugins/preauth/pkinit/pkinit_crypto_openssl.c
index 2ea2a49..c79edae 100644
--- a/src/plugins/preauth/pkinit/pkinit_crypto_openssl.c
+++ b/src/plugins/preauth/pkinit/pkinit_crypto_openssl.c
@@ -2133,6 +2133,7 @@ crypto_retrieve_X509_sans(krb5_context context,
 
     if (!(ext = X509_get_ext(cert, l)) || !(ialt = X509V3_EXT_d2i(ext))) {
         pkiDebug("%s: found no subject alt name extensions\n", __FUNCTION__);
+        retval = ENOENT;
         goto cleanup;
     }
     num_sans = sk_GENERAL_NAME_num(ialt);
@@ -6165,3 +6166,32 @@ crypto_get_deferred_ids(krb5_context context,
     ret = (const pkinit_deferred_id *)deferred;
     return ret;
 }
+
+/* Return the received certificate as DER-encoded data. */
+krb5_error_code
+crypto_encode_der_cert(krb5_context context, pkinit_req_crypto_context reqctx,
+                       uint8_t **der_out, size_t *der_len)
+{
+    int len;
+    unsigned char *der, *p;
+
+    *der_out = NULL;
+    *der_len = 0;
+
+    if (reqctx->received_cert == NULL)
+        return EINVAL;
+    p = NULL;
+    len = i2d_X509(reqctx->received_cert, NULL);
+    if (len <= 0)
+        return EINVAL;
+    p = der = malloc(len);
+    if (p == NULL)
+        return ENOMEM;
+    if (i2d_X509(reqctx->received_cert, &p) <= 0) {
+        free(p);
+        return EINVAL;
+    }
+    *der_out = der;
+    *der_len = len;
+    return 0;
+}
diff --git a/src/plugins/preauth/pkinit/pkinit_srv.c b/src/plugins/preauth/pkinit/pkinit_srv.c
index b5638a3..731d14e 100644
--- a/src/plugins/preauth/pkinit/pkinit_srv.c
+++ b/src/plugins/preauth/pkinit/pkinit_srv.c
@@ -31,6 +31,25 @@
 
 #include <k5-int.h>
 #include "pkinit.h"
+#include "krb5/certauth_plugin.h"
+
+/* Aliases used by the built-in certauth modules */
+struct certauth_req_opts {
+    krb5_kdcpreauth_callbacks cb;
+    krb5_kdcpreauth_rock rock;
+    pkinit_kdc_context plgctx;
+    pkinit_kdc_req_context reqctx;
+};
+
+typedef struct certauth_module_handle_st {
+    struct krb5_certauth_vtable_st vt;
+    krb5_certauth_moddata moddata;
+} *certauth_handle;
+
+struct krb5_kdcpreauth_moddata_st {
+    pkinit_kdc_context *realm_contexts;
+    certauth_handle *certauth_modules;
+};
 
 static krb5_error_code
 pkinit_init_kdc_req_context(krb5_context, pkinit_kdc_req_context *blob);
@@ -51,6 +70,34 @@ pkinit_find_realm_context(krb5_context context,
                           krb5_kdcpreauth_moddata moddata,
                           krb5_principal princ);
 
+static void
+free_realm_contexts(krb5_context context, pkinit_kdc_context *realm_contexts)
+{
+    int i;
+
+    if (realm_contexts == NULL)
+        return;
+    for (i = 0; realm_contexts[i] != NULL; i++)
+        pkinit_server_plugin_fini_realm(context, realm_contexts[i]);
+    pkiDebug("%s: freeing context at %p\n", __FUNCTION__, realm_contexts);
+    free(realm_contexts);
+}
+
+static void
+free_certauth_handles(krb5_context context, certauth_handle *list)
+{
+    int i;
+
+    if (list == NULL)
+        return;
+    for (i = 0; list[i] != NULL; i++) {
+        if (list[i]->vt.fini != NULL)
+            list[i]->vt.fini(context, list[i]->moddata);
+        free(list[i]);
+    }
+    free(list);
+}
+
 static krb5_error_code
 pkinit_create_edata(krb5_context context,
                     pkinit_plg_crypto_context plg_cryptoctx,
@@ -123,7 +170,7 @@ verify_client_san(krb5_context context,
                   pkinit_kdc_req_context reqctx,
                   krb5_kdcpreauth_callbacks cb,
                   krb5_kdcpreauth_rock rock,
-                  krb5_principal client,
+                  krb5_const_principal client,
                   int *valid_san)
 {
     krb5_error_code retval;
@@ -134,12 +181,15 @@ verify_client_san(krb5_context context,
     char *client_string = NULL, *san_string;
 #endif
 
+    *valid_san = 0;
     retval = crypto_retrieve_cert_sans(context, plgctx->cryptoctx,
                                        reqctx->cryptoctx, plgctx->idctx,
                                        &princs,
                                        plgctx->opts->allow_upn ? &upns : NULL,
                                        NULL);
-    if (retval) {
+    if (retval == ENOENT) {
+        goto out;
+    } else if (retval) {
         pkiDebug("%s: error from retrieve_certificate_sans()\n", __FUNCTION__);
         retval = KRB5KDC_ERR_CLIENT_NAME_MISMATCH;
         goto out;
@@ -273,6 +323,73 @@ out:
     return retval;
 }
 
+
+/* Run the received, verified certificate through certauth modules, to verify
+ * that it is authorized to authenticate as client. */
+static krb5_error_code
+authorize_cert(krb5_context context, certauth_handle *certauth_modules,
+               pkinit_kdc_context plgctx, pkinit_kdc_req_context reqctx,
+               krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock,
+               krb5_principal client)
+{
+    krb5_error_code ret;
+    certauth_handle h;
+    struct certauth_req_opts opts;
+    krb5_boolean accepted = FALSE;
+    uint8_t *cert;
+    size_t i, cert_len;
+    void *db_ent = NULL;
+    char **ais = NULL, **ai = NULL;
+
+    /* Re-encode the received certificate into DER, which is extra work, but
+     * avoids creating an X.509 library dependency in the interface. */
+    ret = crypto_encode_der_cert(context, reqctx->cryptoctx, &cert, &cert_len);
+    if (ret)
+        goto cleanup;
+
+    /* Set options for the builtin module. */
+    opts.plgctx = plgctx;
+    opts.reqctx = reqctx;
+    opts.cb = cb;
+    opts.rock = rock;
+
+    db_ent = cb->client_entry(context, rock);
+
+    /*
+     * Check the certificate against each certauth module.  For the certificate
+     * to be authorized at least one module must return 0, and no module can an
+     * error code other than KRB5_PLUGIN_NO_HANDLE (pass).  Add indicators from
+     * modules that return 0 or pass.
+     */
+    ret = KRB5_PLUGIN_NO_HANDLE;
+    for (i = 0; certauth_modules != NULL && certauth_modules[i] != NULL; i++) {
+        h = certauth_modules[i];
+        ret = h->vt.authorize(context, h->moddata, cert, cert_len, client,
+                              &opts, db_ent, &ais);
+        if (ret == 0)
+            accepted = TRUE;
+        else if (ret != KRB5_PLUGIN_NO_HANDLE)
+            goto cleanup;
+
+        if (ais != NULL) {
+            /* Assert authentication indicators from the module. */
+            for (ai = ais; *ai != NULL; ai++) {
+                ret = cb->add_auth_indicator(context, rock, *ai);
+                if (ret)
+                    goto cleanup;
+            }
+            h->vt.free_ind(context, h->moddata, ais);
+            ais = NULL;
+        }
+    }
+
+    ret = accepted ? 0 : KRB5KDC_ERR_CLIENT_NAME_MISMATCH;
+
+cleanup:
+    free(cert);
+    return ret;
+}
+
 static void
 pkinit_server_verify_padata(krb5_context context,
                             krb5_data *req_pkt,
@@ -295,7 +412,6 @@ pkinit_server_verify_padata(krb5_context context,
     pkinit_kdc_req_context reqctx = NULL;
     krb5_checksum cksum = {0, 0, 0, NULL};
     krb5_data *der_req = NULL;
-    int valid_eku = 0, valid_san = 0;
     krb5_data k5data;
     int is_signed = 1;
     krb5_pa_data **e_data = NULL;
@@ -388,27 +504,11 @@ pkinit_server_verify_padata(krb5_context context,
         goto cleanup;
     }
     if (is_signed) {
-
-        retval = verify_client_san(context, plgctx, reqctx, cb, rock,
-                                   request->client, &valid_san);
-        if (retval)
-            goto cleanup;
-        if (!valid_san) {
-            pkiDebug("%s: did not find an acceptable SAN in user "
-                     "certificate\n", __FUNCTION__);
-            retval = KRB5KDC_ERR_CLIENT_NAME_MISMATCH;
-            goto cleanup;
-        }
-        retval = verify_client_eku(context, plgctx, reqctx, &valid_eku);
+        retval = authorize_cert(context, moddata->certauth_modules, plgctx,
+                                reqctx, cb, rock, request->client);
         if (retval)
             goto cleanup;
 
-        if (!valid_eku) {
-            pkiDebug("%s: did not find an acceptable EKU in user "
-                     "certificate\n", __FUNCTION__);
-            retval = KRB5KDC_ERR_INCONSISTENT_KEY_PURPOSE;
-            goto cleanup;
-        }
     } else { /* !is_signed */
         if (!krb5_principal_compare(context, request->client,
                                     krb5_anonymous_principal())) {
@@ -1245,11 +1345,15 @@ pkinit_find_realm_context(krb5_context context,
                           krb5_principal princ)
 {
     int i;
-    pkinit_kdc_context *realm_contexts = (pkinit_kdc_context *)moddata;
+    pkinit_kdc_context *realm_contexts;
 
     if (moddata == NULL)
         return NULL;
 
+    realm_contexts = moddata->realm_contexts;
+    if (realm_contexts == NULL)
+        return NULL;
+
     for (i = 0; realm_contexts[i] != NULL; i++) {
         pkinit_kdc_context p = realm_contexts[i];
 
@@ -1331,6 +1435,155 @@ errout:
     return retval;
 }
 
+static krb5_error_code
+pkinit_san_authorize(krb5_context context, krb5_certauth_moddata moddata,
+                     const uint8_t *cert, size_t cert_len,
+                     krb5_const_principal princ, const void *opts,
+                     const krb5_db_entry *db_entry, char ***authinds_out)
+{
+    krb5_error_code ret;
+    int valid_san;
+    const struct certauth_req_opts *req_opts = opts;
+
+    *authinds_out = NULL;
+
+    ret = verify_client_san(context, req_opts->plgctx, req_opts->reqctx,
+                            req_opts->cb, req_opts->rock, princ, &valid_san);
+    if (ret == ENOENT)
+        return KRB5_PLUGIN_NO_HANDLE;
+    else if (ret)
+        return ret;
+
+    if (!valid_san) {
+        pkiDebug("%s: did not find an acceptable SAN in user certificate\n",
+                 __FUNCTION__);
+        return KRB5KDC_ERR_CLIENT_NAME_MISMATCH;
+    }
+
+    return 0;
+}
+
+static krb5_error_code
+pkinit_eku_authorize(krb5_context context, krb5_certauth_moddata moddata,
+                     const uint8_t *cert, size_t cert_len,
+                     krb5_const_principal princ, const void *opts,
+                     const krb5_db_entry *db_entry, char ***authinds_out)
+{
+    krb5_error_code ret;
+    int valid_eku;
+    const struct certauth_req_opts *req_opts = opts;
+
+    *authinds_out = NULL;
+
+    /* Verify the client EKU. */
+    ret = verify_client_eku(context, req_opts->plgctx, req_opts->reqctx,
+                            &valid_eku);
+    if (ret)
+        return ret;
+
+    if (!valid_eku) {
+        pkiDebug("%s: did not find an acceptable EKU in user certificate\n",
+                 __FUNCTION__);
+        return KRB5KDC_ERR_INCONSISTENT_KEY_PURPOSE;
+    }
+
+    return 0;
+}
+
+static krb5_error_code
+certauth_pkinit_san_initvt(krb5_context context, int maj_ver, int min_ver,
+                           krb5_plugin_vtable vtable)
+{
+    krb5_certauth_vtable vt;
+
+    if (maj_ver != 1)
+        return KRB5_PLUGIN_VER_NOTSUPP;
+    vt = (krb5_certauth_vtable)vtable;
+    vt->name = "pkinit_san";
+    vt->authorize = pkinit_san_authorize;
+    return 0;
+}
+
+static krb5_error_code
+certauth_pkinit_eku_initvt(krb5_context context, int maj_ver, int min_ver,
+                           krb5_plugin_vtable vtable)
+{
+    krb5_certauth_vtable vt;
+
+    if (maj_ver != 1)
+        return KRB5_PLUGIN_VER_NOTSUPP;
+    vt = (krb5_certauth_vtable)vtable;
+    vt->name = "pkinit_eku";
+    vt->authorize = pkinit_eku_authorize;
+    return 0;
+}
+
+static krb5_error_code
+load_certauth_plugins(krb5_context context, certauth_handle **handle_out)
+{
+    krb5_error_code ret;
+    krb5_plugin_initvt_fn *modules = NULL, *mod;
+    certauth_handle *list = NULL, h;
+    size_t count;
+
+    /* Register the builtin modules. */
+    ret = k5_plugin_register(context, PLUGIN_INTERFACE_CERTAUTH,
+                             "pkinit_san", certauth_pkinit_san_initvt);
+    if (ret)
+        goto cleanup;
+
+    ret = k5_plugin_register(context, PLUGIN_INTERFACE_CERTAUTH,
+                             "pkinit_eku", certauth_pkinit_eku_initvt);
+    if (ret)
+        goto cleanup;
+
+    ret = k5_plugin_load_all(context, PLUGIN_INTERFACE_CERTAUTH, &modules);
+    if (ret)
+        goto cleanup;
+
+    /* Allocate handle list. */
+    for (count = 0; modules[count]; count++);
+    list = k5calloc(count + 1, sizeof(*list), &ret);
+    if (list == NULL)
+        goto cleanup;
+
+    /* Initialize each module, ignoring ones that fail. */
+    count = 0;
+    for (mod = modules; *mod != NULL; mod++) {
+        h = k5calloc(1, sizeof(*h), &ret);
+        if (h == NULL)
+            goto cleanup;
+
+        ret = (*mod)(context, 1, 1, (krb5_plugin_vtable)&h->vt);
+        if (ret) {
+            TRACE_CERTAUTH_VTINIT_FAIL(context, ret);
+            free(h);
+            continue;
+        }
+        h->moddata = NULL;
+        if (h->vt.init != NULL) {
+            ret = h->vt.init(context, &h->moddata);
+            if (ret) {
+                TRACE_CERTAUTH_INIT_FAIL(context, h->vt.name, ret);
+                free(h);
+                continue;
+            }
+        }
+        list[count++] = h;
+        list[count] = NULL;
+    }
+    list[count] = NULL;
+
+    ret = 0;
+    *handle_out = list;
+    list = NULL;
+
+cleanup:
+    k5_plugin_free_modules(context, modules);
+    free_certauth_handles(context, list);
+    return ret;
+}
+
 static int
 pkinit_server_plugin_init(krb5_context context,
                           krb5_kdcpreauth_moddata *moddata_out,
@@ -1338,6 +1591,8 @@ pkinit_server_plugin_init(krb5_context context,
 {
     krb5_error_code retval = ENOMEM;
     pkinit_kdc_context plgctx, *realm_contexts = NULL;
+    certauth_handle *certauth_modules = NULL;
+    krb5_kdcpreauth_moddata moddata;
     size_t  i, j;
     size_t numrealms;
 
@@ -1368,16 +1623,22 @@ pkinit_server_plugin_init(krb5_context context,
         goto errout;
     }
 
-    *moddata_out = (krb5_kdcpreauth_moddata)realm_contexts;
-    retval = 0;
-    pkiDebug("%s: returning context at %p\n", __FUNCTION__, realm_contexts);
+    retval = load_certauth_plugins(context, &certauth_modules);
+    if (retval)
+        goto errout;
 
-errout:
-    if (retval) {
-        pkinit_server_plugin_fini(context,
-                                  (krb5_kdcpreauth_moddata)realm_contexts);
-    }
+    moddata = k5calloc(1, sizeof(*moddata), &retval);
+    if (moddata == NULL)
+        goto errout;
+    moddata->realm_contexts = realm_contexts;
+    moddata->certauth_modules = certauth_modules;
+    *moddata_out = moddata;
+    pkiDebug("%s: returning context at %p\n", __FUNCTION__, moddata);
+    return 0;
 
+errout:
+    free_realm_contexts(context, realm_contexts);
+    free_certauth_handles(context, certauth_modules);
     return retval;
 }
 
@@ -1405,17 +1666,11 @@ static void
 pkinit_server_plugin_fini(krb5_context context,
                           krb5_kdcpreauth_moddata moddata)
 {
-    pkinit_kdc_context *realm_contexts = (pkinit_kdc_context *)moddata;
-    int i;
-
-    if (realm_contexts == NULL)
+    if (moddata == NULL)
         return;
-
-    for (i = 0; realm_contexts[i] != NULL; i++) {
-        pkinit_server_plugin_fini_realm(context, realm_contexts[i]);
-    }
-    pkiDebug("%s: freeing context at %p\n", __FUNCTION__, realm_contexts);
-    free(realm_contexts);
+    free_realm_contexts(context, moddata->realm_contexts);
+    free_certauth_handles(context, moddata->certauth_modules);
+    free(moddata);
 }
 
 static krb5_error_code
diff --git a/src/plugins/preauth/pkinit/pkinit_trace.h b/src/plugins/preauth/pkinit/pkinit_trace.h
index a3cc9cc..56821f1 100644
--- a/src/plugins/preauth/pkinit/pkinit_trace.h
+++ b/src/plugins/preauth/pkinit/pkinit_trace.h
@@ -93,4 +93,9 @@
 #define TRACE_PKINIT_OPENSSL_ERROR(c, msg)              \
     TRACE(c, "PKINIT OpenSSL error: {str}", msg)
 
+#define TRACE_CERTAUTH_VTINIT_FAIL(c, ret)                              \
+    TRACE(c, "certauth module failed to init vtable: {kerr}", ret)
+#define TRACE_CERTAUTH_INIT_FAIL(c, name, ret)                          \
+    TRACE(c, "certauth module {str} failed to init: {kerr}", name, ret)
+
 #endif /* PKINIT_TRACE_H */
diff --git a/src/tests/Makefile.in b/src/tests/Makefile.in
index 219d973..d20d748 100644
--- a/src/tests/Makefile.in
+++ b/src/tests/Makefile.in
@@ -171,6 +171,7 @@ check-pytests: unlockiter
 	$(RUNPYTEST) $(srcdir)/t_preauth.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_princflags.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_tabdump.py $(PYTESTFLAGS)
+	$(RUNPYTEST) $(srcdir)/t_certauth.py $(PYTESTFLAGS)
 
 clean:
 	$(RM) adata etinfo forward gcred hist hooks hrealm icinterleave icred
diff --git a/src/tests/t_certauth.py b/src/tests/t_certauth.py
new file mode 100644
index 0000000..e64a57b
--- /dev/null
+++ b/src/tests/t_certauth.py
@@ -0,0 +1,47 @@
+#!/usr/bin/python
+from k5test import *
+
+# Skip this test if pkinit wasn't built.
+if not os.path.exists(os.path.join(plugins, 'preauth', 'pkinit.so')):
+    skip_rest('certauth tests', 'PKINIT module not built')
+
+certs = os.path.join(srctop, 'tests', 'dejagnu', 'pkinit-certs')
+ca_pem = os.path.join(certs, 'ca.pem')
+kdc_pem = os.path.join(certs, 'kdc.pem')
+privkey_pem = os.path.join(certs, 'privkey.pem')
+user_pem = os.path.join(certs, 'user.pem')
+
+modpath = os.path.join(buildtop, 'plugins', 'certauth', 'test',
+                       'certauth_test.so')
+pkinit_krb5_conf = {'realms': {'$realm': {
+            'pkinit_anchors': 'FILE:%s' % ca_pem}},
+            'plugins': {'certauth': {'module': ['test1:' + modpath,
+                                                'test2:' + modpath],
+                                     'enable_only': ['test1', 'test2']}}}
+pkinit_kdc_conf = {'realms': {'$realm': {
+            'default_principal_flags': '+preauth',
+            'pkinit_eku_checking': 'none',
+            'pkinit_identity': 'FILE:%s,%s' % (kdc_pem, privkey_pem),
+            'pkinit_indicator': ['indpkinit1', 'indpkinit2']}}}
+
+file_identity = 'FILE:%s,%s' % (user_pem, privkey_pem)
+
+realm = K5Realm(krb5_conf=pkinit_krb5_conf, kdc_conf=pkinit_kdc_conf,
+                get_creds=False)
+
+# Let the test module match user to CN=user, with indicators.
+realm.kinit(realm.user_princ,
+            flags=['-X', 'X509_user_identity=%s' % file_identity])
+realm.klist(realm.user_princ)
+realm.run([kvno, realm.host_princ])
+realm.run(['./adata', realm.host_princ],
+          expected_msg='+97: [test1, test2, user, indpkinit1, indpkinit2]')
+
+# Let the test module mismatch with user2 to CN=user.
+realm.addprinc("[email protected]")
+out = realm.kinit("[email protected]",
+                  flags=['-X', 'X509_user_identity=%s' % file_identity],
+                  expected_code=1,
+                  expected_msg='kinit: Certificate mismatch')
+
+success("certauth tests")
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.