krb5 commit: Add NegoEx tests

Greg Hudson <[email protected]>
Newsgroups gmane.comp.encryption.kerberos.cvs
Message-ID <[email protected]>
https://github.com/krb5/krb5/commit/02b3171380d5683212ae1deae67a8c6cd19e290a
commit 02b3171380d5683212ae1deae67a8c6cd19e290a
Author: Greg Hudson <[email protected]>
Date:   Fri Sep 27 11:04:45 2019 -0400

    Add NegoEx tests
    
    Add a mock NegoEx-only GSS module, a test program which establishes a
    SPNEGO context, and a Python script to exercise a variety of NegoEx
    negotiation scenarios.
    
    ticket: 8851

 .gitignore                                         |    1 +
 src/Makefile.in                                    |    1 +
 src/configure.ac                                   |    1 +
 src/plugins/gssapi/negoextest/Makefile.in          |   20 ++
 src/plugins/gssapi/negoextest/deps                 |   15 +
 .../gssapi/negoextest/gss_negoextest.exports       |   13 +
 src/plugins/gssapi/negoextest/main.c               |  340 ++++++++++++++++++++
 src/tests/gssapi/Makefile.in                       |   47 ++--
 src/tests/gssapi/deps                              |    4 +
 src/tests/gssapi/t_context.c                       |   60 ++++
 src/tests/gssapi/t_negoex.py                       |  142 ++++++++
 11 files changed, 623 insertions(+), 21 deletions(-)

diff --git a/.gitignore b/.gitignore
index bc15f78..adf6615 100644
--- a/.gitignore
+++ b/.gitignore
@@ -474,6 +474,7 @@ local.properties
 /src/tests/gssapi/t_add_cred
 /src/tests/gssapi/t_ccselect
 /src/tests/gssapi/t_ciflags
+/src/tests/gssapi/t_context
 /src/tests/gssapi/t_credstore
 /src/tests/gssapi/t_enctypes
 /src/tests/gssapi/t_err
diff --git a/src/Makefile.in b/src/Makefile.in
index 691f86c..56c7a4e 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -13,6 +13,7 @@ SUBDIRS=util include lib \
 	@audit_plugin@ \
 	plugins/kadm5_hook/test \
 	plugins/kadm5_auth/test \
+	plugins/gssapi/negoextest \
 	plugins/hostrealm/test \
 	plugins/localauth/test \
 	plugins/pwqual/test \
diff --git a/src/configure.ac b/src/configure.ac
index 171bad4..79b0bd0 100644
--- a/src/configure.ac
+++ b/src/configure.ac
@@ -1499,6 +1499,7 @@ V5_AC_OUTPUT_MAKEFILE(.
 	kdc kprop config-files build-tools man doc include
 
 	plugins/certauth/test
+	plugins/gssapi/negoextest
 	plugins/hostrealm/test
 	plugins/localauth/test
 	plugins/kadm5_hook/test
diff --git a/src/plugins/gssapi/negoextest/Makefile.in b/src/plugins/gssapi/negoextest/Makefile.in
new file mode 100644
index 0000000..62e8a37
--- /dev/null
+++ b/src/plugins/gssapi/negoextest/Makefile.in
@@ -0,0 +1,20 @@
+mydir=plugins$(S)gssapi$(S)negoextest
+BUILDTOP=$(REL)..$(S)..$(S)..
+
+LIBBASE=gss_negoextest
+LIBMAJOR=0
+LIBMINOR=0
+RELDIR=../plugins/gssapi/negoextest
+SHLIB_EXPDEPS=$(GSS_DEPLIBS) $(KRB5_BASE_DEPLIBS)
+SHLIB_EXPLIBS=$(GSS_LIBS) $(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/gssapi/negoextest/deps b/src/plugins/gssapi/negoextest/deps
new file mode 100644
index 0000000..1e18ff4
--- /dev/null
+++ b/src/plugins/gssapi/negoextest/deps
@@ -0,0 +1,15 @@
+#
+# Generated makefile dependencies follow.
+#
+main.so main.po $(OUTPRE)main.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \
+  $(BUILDTOP)/include/gssapi/gssapi.h $(BUILDTOP)/include/gssapi/gssapi_alloc.h \
+  $(BUILDTOP)/include/gssapi/gssapi_ext.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/plugin.h \
+  $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \
+  main.c
diff --git a/src/plugins/gssapi/negoextest/gss_negoextest.exports b/src/plugins/gssapi/negoextest/gss_negoextest.exports
new file mode 100644
index 0000000..7825ab1
--- /dev/null
+++ b/src/plugins/gssapi/negoextest/gss_negoextest.exports
@@ -0,0 +1,13 @@
+gss_accept_sec_context
+gss_acquire_cred
+gss_acquire_cred_with_password
+gss_delete_sec_context
+gss_display_status
+gss_import_name
+gss_init_sec_context
+gss_inquire_sec_context_by_oid
+gss_release_cred
+gss_release_name
+gssspi_exchange_meta_data
+gssspi_query_mechanism_info
+gssspi_query_meta_data
diff --git a/src/plugins/gssapi/negoextest/main.c b/src/plugins/gssapi/negoextest/main.c
new file mode 100644
index 0000000..6c340f4
--- /dev/null
+++ b/src/plugins/gssapi/negoextest/main.c
@@ -0,0 +1,340 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* plugins/gssapi/negoextest/main.c - GSS test module for NegoEx */
+/*
+ * Copyright (C) 2019 by the Massachusetts Institute of Technology.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in
+ *   the documentation and/or other materials provided with the
+ *   distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "k5-int.h"
+#include <gssapi/gssapi.h>
+#include <gssapi/gssapi_ext.h>
+#include <gssapi/gssapi_alloc.h>
+
+struct test_context {
+    int initiator;
+    uint8_t hops;               /* hops remaining; 0 means established */
+};
+
+OM_uint32 KRB5_CALLCONV
+gss_init_sec_context(OM_uint32 *minor_status,
+                     gss_cred_id_t claimant_cred_handle,
+                     gss_ctx_id_t *context_handle, gss_name_t target_name,
+                     gss_OID mech_type, OM_uint32 req_flags,
+                     OM_uint32 time_req,
+                     gss_channel_bindings_t input_chan_bindings,
+                     gss_buffer_t input_token, gss_OID *actual_mech,
+                     gss_buffer_t output_token, OM_uint32 *ret_flags,
+                     OM_uint32 *time_rec)
+{
+    struct test_context *ctx = (struct test_context *)*context_handle;
+    OM_uint32 major;
+    gss_buffer_desc tok;
+    const char *envstr;
+    uint8_t hops, mech_last_octet;
+
+    if (input_token == GSS_C_NO_BUFFER || input_token->length == 0) {
+        envstr = getenv("HOPS");
+        hops = (envstr != NULL) ? atoi(envstr) : 1;
+        assert(hops > 0);
+    } else if (input_token->length == 4 &&
+               memcmp(input_token->value, "fail", 4) == 0) {
+        *minor_status = 12345;
+        return GSS_S_FAILURE;
+    } else {
+        hops = ((uint8_t *)input_token->value)[0];
+    }
+
+    mech_last_octet = ((uint8_t *)mech_type->elements)[mech_type->length - 1];
+    envstr = getenv("INIT_FAIL");
+    if (envstr != NULL && atoi(envstr) == mech_last_octet)
+        return GSS_S_FAILURE;
+
+    if (ctx == NULL) {
+        ctx = malloc(sizeof(*ctx));
+        assert(ctx != NULL);
+        ctx->initiator = 1;
+        ctx->hops = hops;
+        *context_handle = (gss_ctx_id_t)ctx;
+    } else if (ctx != NULL) {
+        assert(ctx->initiator);
+        ctx->hops--;
+        assert(ctx->hops == hops);
+    }
+
+    if (ctx->hops > 0) {
+        /* Generate a token containing the remaining hop count. */
+        ctx->hops--;
+        tok.value = &ctx->hops;
+        tok.length = 1;
+        major = gss_encapsulate_token(&tok, mech_type, output_token);
+        assert(major == GSS_S_COMPLETE);
+    }
+
+    return (ctx->hops > 0) ? GSS_S_CONTINUE_NEEDED : GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_accept_sec_context(OM_uint32 *minor_status, gss_ctx_id_t *context_handle,
+                       gss_cred_id_t verifier_cred_handle,
+                       gss_buffer_t input_token,
+                       gss_channel_bindings_t input_chan_bindings,
+                       gss_name_t *src_name, gss_OID *mech_type,
+                       gss_buffer_t output_token, OM_uint32 *ret_flags,
+                       OM_uint32 *time_rec,
+                       gss_cred_id_t *delegated_cred_handle)
+{
+    struct test_context *ctx = (struct test_context *)*context_handle;
+    uint8_t hops, mech_last_octet;
+    const char *envstr;
+
+    /*
+     * The unwrapped token sits at the end and is just one byte giving the
+     * remaining number of hops.  The final octet of the mech encoding should
+     * be just prior to it.
+     */
+    assert(input_token->length >= 2);
+    hops = ((uint8_t *)input_token->value)[input_token->length - 1];
+    mech_last_octet = ((uint8_t *)input_token->value)[input_token->length - 2];
+
+    envstr = getenv("ACCEPT_FAIL");
+    if (envstr != NULL && atoi(envstr) == mech_last_octet) {
+        output_token->value = gssalloc_strdup("fail");
+        assert(output_token->value != NULL);
+        output_token->length = 4;
+        return GSS_S_FAILURE;
+    }
+
+    if (*context_handle == GSS_C_NO_CONTEXT) {
+        ctx = malloc(sizeof(*ctx));
+        assert(ctx != NULL);
+        ctx->initiator = 0;
+        ctx->hops = hops;
+        *context_handle = (gss_ctx_id_t)ctx;
+    } else {
+        assert(!ctx->initiator);
+        ctx->hops--;
+        assert(ctx->hops == hops);
+    }
+
+    if (ctx->hops > 0) {
+        /* Generate a token containing the remaining hop count. */
+        ctx->hops--;
+        output_token->value = gssalloc_malloc(1);
+        assert(output_token->value != NULL);
+        memcpy(output_token->value, &ctx->hops, 1);
+        output_token->length = 1;
+    }
+
+    return (ctx->hops > 0) ? GSS_S_CONTINUE_NEEDED : GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_delete_sec_context(OM_uint32 *minor_status, gss_ctx_id_t *context_handle,
+                       gss_buffer_t output_token)
+{
+    free(*context_handle);
+    *context_handle = GSS_C_NO_CONTEXT;
+    return GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_acquire_cred(OM_uint32 *minor_status, gss_name_t desired_name,
+                 OM_uint32 time_req, gss_OID_set desired_mechs,
+                 gss_cred_usage_t cred_usage,
+                 gss_cred_id_t *output_cred_handle, gss_OID_set *actual_mechs,
+                 OM_uint32 *time_rec)
+{
+    return GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_acquire_cred_with_password(OM_uint32 *minor_status,
+                               const gss_name_t desired_name,
+                               const gss_buffer_t password, OM_uint32 time_req,
+                               const gss_OID_set desired_mechs,
+                               gss_cred_usage_t cred_usage,
+                               gss_cred_id_t *output_cred_handle,
+                               gss_OID_set *actual_mechs, OM_uint32 *time_rec)
+{
+    return GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_release_cred(OM_uint32 *minor_status, gss_cred_id_t *cred_handle)
+{
+    return GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_import_name(OM_uint32 *minor_status, gss_buffer_t input_name_buffer,
+                gss_OID input_name_type, gss_name_t *output_name)
+{
+    static int dummy;
+
+    /*
+     * We don't need to remember anything about names, but we do need to
+     * distinguish them from GSS_C_NO_NAME (to determine the direction of
+     * gss_query_meta_data() and gss_exchange_meta_data()), so assign an
+     * arbitrary data pointer.
+     */
+    *output_name = (gss_name_t)&dummy;
+    return GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_release_name(OM_uint32 *minor_status, gss_name_t *input_name)
+{
+    return GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_display_status(OM_uint32 *minor_status, OM_uint32 status_value,
+                   int status_type, gss_OID mech_type,
+                   OM_uint32 *message_context, gss_buffer_t status_string)
+{
+    if (status_type == GSS_C_MECH_CODE && status_value == 12345) {
+        status_string->value = gssalloc_strdup("failure from acceptor");
+        assert(status_string->value != NULL);
+        status_string->length = strlen(status_string->value);
+        return GSS_S_COMPLETE;
+    }
+    return GSS_S_BAD_STATUS;
+}
+
+OM_uint32 KRB5_CALLCONV
+gssspi_query_meta_data(OM_uint32 *minor_status, gss_const_OID mech_oid,
+                       gss_cred_id_t cred_handle, gss_ctx_id_t *context_handle,
+                       const gss_name_t targ_name, OM_uint32 req_flags,
+                       gss_buffer_t meta_data)
+{
+    const char *envstr;
+    uint8_t mech_last_octet;
+    int initiator = (targ_name != GSS_C_NO_NAME);
+
+    mech_last_octet = ((uint8_t *)mech_oid->elements)[mech_oid->length - 1];
+    envstr = getenv(initiator ? "INIT_QUERY_FAIL" : "ACCEPT_QUERY_FAIL");
+    if (envstr != NULL && atoi(envstr) == mech_last_octet)
+        return GSS_S_FAILURE;
+    envstr = getenv(initiator ? "INIT_QUERY_NONE" : "ACCEPT_QUERY_NONE");
+    if (envstr != NULL && atoi(envstr) == mech_last_octet)
+        return GSS_S_COMPLETE;
+
+    meta_data->value = gssalloc_strdup("X");
+    meta_data->length = 1;
+    return GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gssspi_exchange_meta_data(OM_uint32 *minor_status, gss_const_OID mech_oid,
+                          gss_cred_id_t cred_handle,
+                          gss_ctx_id_t *context_handle,
+                          const gss_name_t targ_name, OM_uint32 req_flags,
+                          gss_const_buffer_t meta_data)
+{
+    const char *envstr;
+    uint8_t mech_last_octet;
+    int initiator = (targ_name != GSS_C_NO_NAME);
+
+    mech_last_octet = ((uint8_t *)mech_oid->elements)[mech_oid->length - 1];
+    envstr = getenv(initiator ? "INIT_EXCHANGE_FAIL" : "ACCEPT_EXCHANGE_FAIL");
+    if (envstr != NULL && atoi(envstr) == mech_last_octet)
+        return GSS_S_FAILURE;
+
+    assert(meta_data->length == 1 && memcmp(meta_data->value, "X", 1) == 0);
+    return GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gssspi_query_mechanism_info(OM_uint32 *minor_status, gss_const_OID mech_oid,
+                            unsigned char auth_scheme[16])
+{
+    /* Copy the mech OID encoding and right-pad it with zeros. */
+    memset(auth_scheme, 0, 16);
+    assert(mech_oid->length <= 16);
+    memcpy(auth_scheme, mech_oid->elements, mech_oid->length);
+    return GSS_S_COMPLETE;
+}
+
+OM_uint32 KRB5_CALLCONV
+gss_inquire_sec_context_by_oid(OM_uint32 *minor_status,
+                               const gss_ctx_id_t context_handle,
+                               const gss_OID desired_object,
+                               gss_buffer_set_t *data_set)
+{
+    struct test_context *ctx = (struct test_context *)context_handle;
+    OM_uint32 major;
+    uint8_t keybytes[32] = { 0 };
+    uint8_t typebytes[4];
+    gss_buffer_desc key, type;
+    const char *envstr;
+    int ask_verify;
+
+    if (gss_oid_equal(desired_object, GSS_C_INQ_NEGOEX_KEY))
+        ask_verify = 0;
+    else if (gss_oid_equal(desired_object, GSS_C_INQ_NEGOEX_VERIFY_KEY))
+        ask_verify = 1;
+    else
+        return GSS_S_UNAVAILABLE;
+
+    /*
+     * By default, make a key available only if the context is established.
+     * This can be overridden to "always", "init-always", "accept-always",
+     * or "never".
+     */
+    envstr = getenv("KEY");
+    if (envstr != NULL && strcmp(envstr, "never") == 0) {
+        return GSS_S_UNAVAILABLE;
+    } else if (ctx->hops > 0) {
+        if (envstr == NULL)
+            return GSS_S_UNAVAILABLE;
+        else if (strcmp(envstr, "init-always") == 0 && !ctx->initiator)
+            return GSS_S_UNAVAILABLE;
+        else if (strcmp(envstr, "accept-always") == 0 && ctx->initiator)
+            return GSS_S_UNAVAILABLE;
+    }
+
+    /* Perturb the key so that each side's verifier key is equal to the other's
+     * checksum key. */
+    keybytes[0] = ask_verify ^ ctx->initiator;
+
+    /* Supply an all-zeros aes256-sha1 negoex key. */
+    if (gss_oid_equal(desired_object, GSS_C_INQ_NEGOEX_KEY) ||
+        gss_oid_equal(desired_object, GSS_C_INQ_NEGOEX_VERIFY_KEY)) {
+        store_32_le(ENCTYPE_AES256_CTS_HMAC_SHA1_96, typebytes);
+        key.value = keybytes;
+        key.length = sizeof(keybytes);
+        type.value = typebytes;
+        type.length = sizeof(typebytes);
+        major = gss_add_buffer_set_member(minor_status, &key, data_set);
+        if (major != GSS_S_COMPLETE)
+            return major;
+        return gss_add_buffer_set_member(minor_status, &type, data_set);
+    }
+
+    return GSS_S_UNAVAILABLE;
+}
diff --git a/src/tests/gssapi/Makefile.in b/src/tests/gssapi/Makefile.in
index a7b8da4..5cc1e0f 100644
--- a/src/tests/gssapi/Makefile.in
+++ b/src/tests/gssapi/Makefile.in
@@ -10,10 +10,11 @@ LOCALINCLUDES = -I$(srcdir)/../../lib/gssapi/mechglue \
 
 SRCS=	$(srcdir)/ccinit.c $(srcdir)/ccrefresh.c $(srcdir)/common.c \
 	$(srcdir)/t_accname.c $(srcdir)/t_add_cred.c $(srcdir)/t_ccselect.c \
-	$(srcdir)/t_ciflags.c $(srcdir)/t_credstore.c $(srcdir)/t_enctypes.c \
-	$(srcdir)/t_err.c $(srcdir)/t_export_cred.c $(srcdir)/t_export_name.c \
-	$(srcdir)/t_gssexts.c $(srcdir)/t_imp_cred.c $(srcdir)/t_imp_name.c \
-	$(srcdir)/t_invalid.c $(srcdir)/t_inq_cred.c $(srcdir)/t_inq_ctx.c \
+	$(srcdir)/t_ciflags.c $(srcdir)/t_context.c $(srcdir)/t_credstore.c \
+	$(srcdir)/t_enctypes.c $(srcdir)/t_err.c $(srcdir)/t_export_cred.c \
+	$(srcdir)/t_export_name.c $(srcdir)/t_gssexts.c \
+	$(srcdir)/t_imp_cred.c $(srcdir)/t_imp_name.c $(srcdir)/t_invalid.c \
+	$(srcdir)/t_inq_cred.c $(srcdir)/t_inq_ctx.c \
 	$(srcdir)/t_inq_mechs_name.c $(srcdir)/t_iov.c \
 	$(srcdir)/t_lifetime.c $(srcdir)/t_namingexts.c $(srcdir)/t_oid.c \
 	$(srcdir)/t_pcontok.c $(srcdir)/t_prf.c $(srcdir)/t_s4u.c \
@@ -21,20 +22,20 @@ SRCS=	$(srcdir)/ccinit.c $(srcdir)/ccrefresh.c $(srcdir)/common.c \
 	$(srcdir)/t_spnego.c $(srcdir)/t_srcattrs.c
 
 OBJS=	ccinit.o ccrefresh.o common.o t_accname.o t_add_cred.o t_ccselect.o \
-	t_ciflags.o t_credstore.o t_enctypes.o t_err.o t_export_cred.o \
-	t_export_name.o t_gssexts.o t_imp_cred.o t_imp_name.o t_invalid.o \
-	t_inq_cred.o t_inq_ctx.o t_inq_mechs_name.o t_iov.o t_lifetime.o \
-	t_namingexts.o t_oid.o t_pcontok.o t_prf.o t_s4u.o t_s4u2proxy_krb5.o \
-	t_saslname.o t_spnego.o t_srcattrs.o
+	t_ciflags.o t_context.o t_credstore.o t_enctypes.o t_err.o \
+	t_export_cred.o t_export_name.o t_gssexts.o t_imp_cred.o t_imp_name.o \
+	t_invalid.o t_inq_cred.o t_inq_ctx.o t_inq_mechs_name.o t_iov.o \
+	t_lifetime.o t_namingexts.o t_oid.o t_pcontok.o t_prf.o t_s4u.o \
+	t_s4u2proxy_krb5.o t_saslname.o t_spnego.o t_srcattrs.o
 
 COMMON_DEPS= common.o $(GSS_DEPLIBS) $(KRB5_BASE_DEPLIBS)
 COMMON_LIBS= common.o $(GSS_LIBS) $(KRB5_BASE_LIBS)
 
-all: ccinit ccrefresh t_accname t_add_cred t_ccselect t_ciflags t_credstore \
-	t_enctypes t_err t_export_cred t_export_name t_gssexts t_imp_cred \
-	t_imp_name t_invalid t_inq_cred t_inq_ctx t_inq_mechs_name t_iov \
-	t_lifetime t_namingexts t_oid t_pcontok t_prf t_s4u t_s4u2proxy_krb5 \
-	t_saslname t_spnego t_srcattrs
+all: ccinit ccrefresh t_accname t_add_cred t_ccselect t_ciflags t_context \
+	t_credstore t_enctypes t_err t_export_cred t_export_name t_gssexts \
+	t_imp_cred t_imp_name t_invalid t_inq_cred t_inq_ctx t_inq_mechs_name \
+	t_iov t_lifetime t_namingexts t_oid t_pcontok t_prf t_s4u \
+	t_s4u2proxy_krb5 t_saslname t_spnego t_srcattrs
 
 check-unix: t_oid
 	$(RUN_TEST) ./t_invalid
@@ -43,9 +44,9 @@ check-unix: t_oid
 	$(RUN_TEST) ./t_imp_name
 
 check-pytests: ccinit ccrefresh t_accname t_add_cred t_ccselect t_ciflags \
-	t_credstore t_enctypes t_err t_export_cred t_export_name t_imp_cred \
-	t_inq_cred t_inq_ctx t_inq_mechs_name t_iov t_lifetime t_pcontok \
-	t_s4u t_s4u2proxy_krb5 t_spnego t_srcattrs
+	t_context t_credstore t_enctypes t_err t_export_cred t_export_name \
+	t_imp_cred t_inq_cred t_inq_ctx t_inq_mechs_name t_iov t_lifetime \
+	t_pcontok t_s4u t_s4u2proxy_krb5 t_spnego t_srcattrs
 	$(RUNPYTEST) $(srcdir)/t_gssapi.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_ccselect.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_client_keytab.py $(PYTESTFLAGS)
@@ -53,6 +54,7 @@ check-pytests: ccinit ccrefresh t_accname t_add_cred t_ccselect t_ciflags \
 	$(RUNPYTEST) $(srcdir)/t_export_cred.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_s4u.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_authind.py $(PYTESTFLAGS)
+	$(RUNPYTEST) $(srcdir)/t_negoex.py $(PYTESTFLAGS)
 
 ccinit: ccinit.o $(KRB5_BASE_DEPLIBS)
 	$(CC_LINK) -o ccinit ccinit.o $(KRB5_BASE_LIBS)
@@ -66,6 +68,8 @@ t_ccselect: t_ccselect.o $(COMMON_DEPS)
 	$(CC_LINK) -o $@ t_ccselect.o $(COMMON_LIBS)
 t_ciflags: t_ciflags.o $(COMMON_DEPS)
 	$(CC_LINK) -o $@ t_ciflags.o $(COMMON_LIBS)
+t_context: t_context.o $(COMMON_DEPS)
+	$(CC_LINK) -o $@ t_context.o $(COMMON_LIBS)
 t_credstore: t_credstore.o $(COMMON_DEPS)
 	$(CC_LINK) -o $@ t_credstore.o $(COMMON_LIBS)
 t_enctypes: t_enctypes.o $(COMMON_DEPS)
@@ -115,7 +119,8 @@ t_srcattrs: t_srcattrs.o $(COMMON_DEPS)
 
 clean:
 	$(RM) ccinit ccrefresh t_accname t_add_cred t_ccselect t_ciflags
-	$(RM) t_credstore t_enctypes t_err t_export_cred t_export_name
-	$(RM) t_gssexts t_imp_cred t_imp_name t_invalid t_inq_cred t_inq_ctx
-	$(RM) t_inq_mechs_name t_iov t_lifetime t_namingexts t_oid t_pcontok
-	$(RM) t_prf t_s4u t_s4u2proxy_krb5 t_saslname t_spnego t_srcattrs
+	$(RM) t_context t_credstore t_enctypes t_err t_export_cred
+	$(RM) t_export_name t_gssexts t_imp_cred t_imp_name t_invalid
+	$(RM) t_inq_cred t_inq_ctx t_inq_mechs_name t_iov t_lifetime
+	$(RM) t_namingexts t_oid t_pcontok t_prf t_s4u t_s4u2proxy_krb5
+	$(RM) t_saslname t_spnego t_srcattrs
diff --git a/src/tests/gssapi/deps b/src/tests/gssapi/deps
index c8905c9..acd0e96 100644
--- a/src/tests/gssapi/deps
+++ b/src/tests/gssapi/deps
@@ -41,6 +41,10 @@ $(OUTPRE)t_ciflags.$(OBJEXT): $(BUILDTOP)/include/gssapi/gssapi.h \
   $(BUILDTOP)/include/gssapi/gssapi_ext.h $(BUILDTOP)/include/gssapi/gssapi_krb5.h \
   $(BUILDTOP)/include/krb5/krb5.h $(COM_ERR_DEPS) $(top_srcdir)/include/krb5.h \
   common.h t_ciflags.c
+$(OUTPRE)t_context.$(OBJEXT): $(BUILDTOP)/include/gssapi/gssapi.h \
+  $(BUILDTOP)/include/gssapi/gssapi_ext.h $(BUILDTOP)/include/gssapi/gssapi_krb5.h \
+  $(BUILDTOP)/include/krb5/krb5.h $(COM_ERR_DEPS) $(top_srcdir)/include/krb5.h \
+  common.h t_context.c
 $(OUTPRE)t_credstore.$(OBJEXT): $(BUILDTOP)/include/gssapi/gssapi.h \
   $(BUILDTOP)/include/gssapi/gssapi_ext.h $(BUILDTOP)/include/gssapi/gssapi_krb5.h \
   $(BUILDTOP)/include/krb5/krb5.h $(COM_ERR_DEPS) $(top_srcdir)/include/krb5.h \
diff --git a/src/tests/gssapi/t_context.c b/src/tests/gssapi/t_context.c
new file mode 100644
index 0000000..65381ac
--- /dev/null
+++ b/src/tests/gssapi/t_context.c
@@ -0,0 +1,60 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* tests/gssapi/t_context.c - Simple context establishment harness */
+/*
+ * Copyright (C) 2019 by the Massachusetts Institute of Technology.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in
+ *   the documentation and/or other materials provided with the
+ *   distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "common.h"
+
+int
+main(int argc, char *argv[])
+{
+    OM_uint32 minor, flags;
+    gss_name_t tname;
+    gss_ctx_id_t ictx, actx;
+
+    if (argc < 2) {
+        fprintf(stderr, "Usage: %s targetname [acceptorname]\n", argv[0]);
+        return 1;
+    }
+
+    tname = import_name(argv[1]);
+
+    flags = GSS_C_REPLAY_FLAG | GSS_C_SEQUENCE_FLAG;
+    establish_contexts(&mech_spnego, GSS_C_NO_CREDENTIAL, GSS_C_NO_CREDENTIAL,
+                       tname, flags, &ictx, &actx, NULL, NULL, NULL);
+
+    (void)gss_release_name(&minor, &tname);
+    (void)gss_delete_sec_context(&minor, &ictx, NULL);
+    (void)gss_delete_sec_context(&minor, &actx, NULL);
+    return 0;
+}
diff --git a/src/tests/gssapi/t_negoex.py b/src/tests/gssapi/t_negoex.py
new file mode 100644
index 0000000..88470d2
--- /dev/null
+++ b/src/tests/gssapi/t_negoex.py
@@ -0,0 +1,142 @@
+from k5test import *
+
+# The next arc after 2.25 is supposed to be a single-integer UUID, but
+# since our gss_str_to_oid() can't handle arc values that don't fit in
+# an unsigned long, we use random unsigned 32-bit integers instead.
+# The final octet if the OID encoding will be used to identify the
+# mechanism when changing the behavior of just one mech.
+nxtest_oid1 = '2.25.1414534758' # final octet is 102 (0x66)
+nxtest_oid2 = '2.25.1175737388' # final octet is 44 (0x2C)
+nxtest_path = os.path.join(buildtop, 'plugins', 'gssapi', 'negoextest',
+                           'gss_negoextest.so')
+
+# Test gss_add_cred().
+realm = K5Realm(create_kdb=False)
+with open(realm.gss_mech_config, 'w') as f:
+    f.write('negoextest %s %s\n' % (nxtest_oid1, nxtest_path))
+    f.write('negoextest %s %s\n' % (nxtest_oid2, nxtest_path))
+
+def test(envvars, **kw):
+    # Python 3.5: e = {**realm.env, **vars}
+    e = realm.env.copy()
+    e.update(envvars)
+    realm.run(['./t_context', 'h:host'], env=e, **kw)
+
+# Test varying numbers of hops, and spot-check that messages are sent
+# in the appropriate sequence.
+
+mark('One hop')
+msgs = ('sending [0]INITIATOR_NEGO: c0a28569-66ac-0000-0000-000000000000 '
+        'd1b08469-2ca8-0000-0000-000000000000',
+        'sending [1]INITIATOR_META_DATA: c0a28569-66ac',
+        'sending [2]INITIATOR_META_DATA: d1b08469-2ca8',
+        'sending [3]AP_REQUEST: c0a28569-66ac',
+        'sending [4]VERIFY: c0a28569-66ac',
+        'received [0]INITIATOR_NEGO: c0a28569-66ac-0000-0000-000000000000 '
+        'd1b08469-2ca8-0000-0000-000000000000',
+        'received [1]INITIATOR_META_DATA: c0a28569-66ac',
+        'received [2]INITIATOR_META_DATA: d1b08469-2ca8',
+        'received [3]AP_REQUEST: c0a28569-66ac',
+        'received [4]VERIFY: c0a28569-66ac',
+        'sending [5]ACCEPTOR_NEGO: c0a28569-66ac-0000-0000-000000000000 '
+        'd1b08469-2ca8-0000-0000-000000000000',
+        'sending [6]ACCEPTOR_META_DATA: c0a28569-66ac',
+        'sending [7]ACCEPTOR_META_DATA: d1b08469-2ca8',
+        'sending [8]VERIFY: c0a28569-66ac',
+        'received [5]ACCEPTOR_NEGO: c0a28569-66ac-0000-0000-000000000000 '
+        'd1b08469-2ca8-0000-0000-000000000000',
+        'received [6]ACCEPTOR_META_DATA: c0a28569-66ac',
+        'received [7]ACCEPTOR_META_DATA: d1b08469-2ca8',
+        'received [8]VERIFY: c0a28569-66ac')
+test({'HOPS': '1'}, expected_trace=msgs)
+
+mark('Two hops')
+msgs = ('sending [7]CHALLENGE', 'sending [8]VERIFY', 'received [8]VERIFY',
+        'sending [9]VERIFY')
+test({'HOPS': '2'}, expected_trace=msgs)
+
+mark('Three hops')
+msgs = ('sending [8]AP_REQUEST', 'sending [9]VERIFY', 'received [8]AP_REQUEST',
+        'sending [10]VERIFY')
+test({'HOPS': '3'}, expected_trace=msgs)
+
+mark('Four hops')
+msgs = ('sending [9]CHALLENGE', 'sending [10]VERIFY', 'received [9]CHALLENGE',
+        'sending [11]VERIFY')
+test({'HOPS': '4'}, expected_trace=msgs)
+
+mark('Early keys, three hops')
+msgs = ('sending [4]VERIFY', 'sending [9]VERIFY', 'sending [10]AP_REQUEST')
+test({'HOPS': '3', 'KEY': 'always'}, expected_trace=msgs)
+
+mark('Early keys, four hops')
+msgs = ('sending [4]VERIFY', 'sending [9]VERIFY', 'sending [10]AP_REQUEST',
+        'sending [11]CHALLENGE')
+test({'HOPS': '4', 'KEY': 'always'}, expected_trace=msgs)
+
+mark('No keys')
+test({'KEY': 'never'}, expected_code=1, expected_msg='No NegoEx verify key')
+
+mark('No optimistic token')
+msgs = ('sending [3]ACCEPTOR_NEGO', 'sending [6]AP_REQUEST',
+        'sending [7]VERIFY', 'sending [8]VERIFY')
+test({'NEGOEX_NO_OPTIMISTIC_TOKEN': ''}, expected_trace=msgs)
+
+mark('First mech initiator query fail')
+msgs = ('sending [0]INITIATOR_NEGO: d1b08469-2ca8-0000-0000-000000000000',
+        'sending [2]AP_REQUEST', 'sending [3]VERIFY',
+        'sending [4]ACCEPTOR_NEGO: d1b08469-2ca8-0000-0000-000000000000',
+        'sending [6]VERIFY')
+test({'INIT_QUERY_FAIL': '102'}, expected_trace=msgs)
+
+mark('First mech acceptor query fail')
+msgs = ('sending [0]INITIATOR_NEGO: c0a28569-66ac-0000-0000-000000000000 '
+        'd1b08469-2ca8-0000-0000-000000000000',
+        'sending [3]AP_REQUEST: c0a28569-66ac',
+        'sending [4]VERIFY: c0a28569-66ac',
+        'sending [5]ACCEPTOR_NEGO: d1b08469-2ca8-0000-0000-000000000000',
+        'sending [7]AP_REQUEST: d1b08469-2ca8',
+        'sending [8]VERIFY: d1b08469-2ca8',
+        'sending [9]VERIFY: d1b08469-2ca8')
+test({'ACCEPT_QUERY_FAIL': '102'}, expected_trace=msgs)
+
+# Same messages as previous test.
+mark('First mech acceptor exchange fail')
+test({'ACCEPT_EXCHANGE_FAIL': '102'}, expected_trace=msgs)
+
+# Fail the optimistic mech's gss_exchange_meta_data() in the
+# initiator.  Since the acceptor has effectively selected the
+# optimistic mech, this causes the authentication to fail.
+mark('First mech initiator exchange fail, one hop')
+test({'HOPS': '1', 'INIT_EXCHANGE_FAIL': '102'}, expected_code=1,
+     expected_msg='No mutually supported NegoEx authentication schemes')
+mark('First mech initiator exchange fail, two hops, early keys')
+test({'HOPS': '2', 'INIT_EXCHANGE_FAIL': '102', 'KEY': 'always'},
+     expected_code=1,
+     expected_msg='No mutually supported NegoEx authentication schemes')
+mark('First mech initiator exchange fail, two hops')
+test({'HOPS': '2', 'INIT_EXCHANGE_FAIL': '102'}, expected_code=1,
+     expected_msg='No mutually supported NegoEx authentication schemes')
+
+mark('First mech init_sec_context fail')
+msgs = ('sending [0]INITIATOR_NEGO: d1b08469-2ca8-0000-0000-000000000000',
+        'sending [2]AP_REQUEST', 'sending [3]VERIFY', 'sending [6]VERIFY')
+test({'INIT_FAIL': '102'}, expected_trace=msgs)
+
+mark('First mech accept_sec_context fail')
+test({'HOPS': '2', 'ACCEPT_FAIL': '102'}, expected_code=1,
+     expected_msg='failure from acceptor')
+
+mark('ALERT from acceptor to initiator')
+msgs = ('sending [3]AP_REQUEST', 'sending [4]VERIFY', 'sending [8]CHALLENGE',
+        'sending [9]ALERT', 'received [9]ALERT', 'sending [10]AP_REQUEST',
+        'sending [11]VERIFY', 'sending [12]VERIFY')
+test({'HOPS': '3', 'KEY': 'init-always'}, expected_trace=msgs)
+
+mark('ALERT from initiator to acceptor')
+msgs = ('sending [3]AP_REQUEST', 'sending [7]CHALLENGE', 'sending [8]VERIFY',
+        'sending [9]AP_REQUEST', 'sending [10]ALERT', 'received [10]ALERT',
+        'sending [11]CHALLENGE', 'sending [12]VERIFY', 'sending [13]VERIFY')
+test({'HOPS': '4', 'KEY': 'accept-always'}, expected_trace=())
+
+success('NegoEx 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.