
An implementation of draft-zhu-negoex-04 for MIT Kerberos was developed in 2011. This has been recently integrated, with many fixes from Greg Hudson. This commit ports it to Heimdal. The implementation has been interoperability tested with MIT Kerberos and Windows, using the GSS EAP mechanism developed as part of the Moonshot project. The SPNEGO code was also updated to import the state machine from Apple which improves mechListMIC processing and avoids discarding initial context tokens generated during mechanism probing, that can be used for optimistic tokens. Finally, to aid in testing, the GSS-API mechanism glue configuration file can be changed using the environment variable GSS_MECH_CONFIG. This environment variable name, along with the format of the configuration file, is compatible with MIT (although it would be difficult for a single mechanism binary to support both implementations).
1019 lines
29 KiB
C
1019 lines
29 KiB
C
/*
|
|
* Copyright (C) 2011-2019 PADL Software Pty Ltd.
|
|
* 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 "spnego_locl.h"
|
|
|
|
/*
|
|
* The initial context token emitted by the initiator is a INITIATOR_NEGO
|
|
* message followed by zero or more INITIATOR_META_DATA tokens, and zero
|
|
* or one AP_REQUEST tokens.
|
|
*
|
|
* Upon receiving this, the acceptor computes the list of mutually supported
|
|
* authentication mechanisms and performs the metadata exchange. The output
|
|
* token is ACCEPTOR_NEGO followed by zero or more ACCEPTOR_META_DATA tokens,
|
|
* and zero or one CHALLENGE tokens.
|
|
*
|
|
* Once the metadata exchange is complete and a mechanism is selected, the
|
|
* selected mechanism's context token exchange continues with AP_REQUEST and
|
|
* CHALLENGE messages.
|
|
*
|
|
* Once the context token exchange is complete, VERIFY messages are sent to
|
|
* authenticate the entire exchange.
|
|
*/
|
|
|
|
static void
|
|
zero_and_release_buffer_set(gss_buffer_set_t *pBuffers)
|
|
{
|
|
OM_uint32 tmpMinor;
|
|
gss_buffer_set_t buffers = *pBuffers;
|
|
size_t i;
|
|
|
|
if (buffers != GSS_C_NO_BUFFER_SET) {
|
|
for (i = 0; i < buffers->count; i++)
|
|
memset_s(buffers->elements[i].value,
|
|
buffers->elements[i].length, 0,
|
|
buffers->elements[i].length);
|
|
|
|
gss_release_buffer_set(&tmpMinor, &buffers);
|
|
}
|
|
|
|
*pBuffers = GSS_C_NO_BUFFER_SET;
|
|
}
|
|
|
|
static OM_uint32
|
|
buffer_set_to_crypto(OM_uint32 *minor,
|
|
krb5_context context,
|
|
gss_buffer_set_t buffers,
|
|
krb5_crypto *crypto)
|
|
{
|
|
krb5_error_code ret;
|
|
krb5_keyblock keyblock;
|
|
OM_uint32 tmp;
|
|
|
|
/*
|
|
* Returned keys must be in two buffers, with the key contents in
|
|
* the first and the enctype as a 32-bit little-endian integer in
|
|
* the second.
|
|
*/
|
|
if (buffers->count != 2 ||
|
|
buffers->elements[1].length != sizeof(tmp)) {
|
|
*minor = (OM_uint32)NEGOEX_NO_VERIFY_KEY;
|
|
return GSS_S_FAILURE;
|
|
}
|
|
|
|
if (*crypto != NULL) {
|
|
krb5_crypto_destroy(context, *crypto);
|
|
*crypto = NULL;
|
|
}
|
|
|
|
keyblock.keyvalue.data = buffers->elements[0].value;
|
|
keyblock.keyvalue.length = buffers->elements[0].length;
|
|
_gss_mg_decode_le_uint32(buffers->elements[1].value, &tmp);
|
|
keyblock.keytype = tmp;
|
|
|
|
ret = krb5_crypto_init(context, &keyblock, 0, crypto);
|
|
if (ret) {
|
|
*minor = ret;
|
|
return GSS_S_FAILURE;
|
|
}
|
|
|
|
return GSS_S_COMPLETE;
|
|
}
|
|
|
|
static OM_uint32
|
|
get_session_keys(OM_uint32 *minor,
|
|
krb5_context context,
|
|
struct negoex_auth_mech *mech)
|
|
{
|
|
OM_uint32 major, tmpMinor;
|
|
gss_buffer_set_t buffers = GSS_C_NO_BUFFER_SET;
|
|
|
|
major = gss_inquire_sec_context_by_oid(&tmpMinor, mech->mech_context,
|
|
GSS_C_INQ_NEGOEX_KEY, &buffers);
|
|
if (major == GSS_S_COMPLETE) {
|
|
major = buffer_set_to_crypto(minor, context,
|
|
buffers, &mech->crypto);
|
|
zero_and_release_buffer_set(&buffers);
|
|
if (major != GSS_S_COMPLETE)
|
|
return major;
|
|
}
|
|
|
|
major = gss_inquire_sec_context_by_oid(&tmpMinor, mech->mech_context,
|
|
GSS_C_INQ_NEGOEX_VERIFY_KEY,
|
|
&buffers);
|
|
if (major == GSS_S_COMPLETE) {
|
|
major = buffer_set_to_crypto(minor, context,
|
|
buffers, &mech->verify_crypto);
|
|
zero_and_release_buffer_set(&buffers);
|
|
if (major != GSS_S_COMPLETE)
|
|
return major;
|
|
}
|
|
|
|
return GSS_S_COMPLETE;
|
|
}
|
|
|
|
static OM_uint32
|
|
emit_initiator_nego(OM_uint32 *minor, gssspnego_ctx ctx)
|
|
{
|
|
uint8_t random[32];
|
|
struct negoex_auth_mech *mech;
|
|
size_t i = 0;
|
|
|
|
krb5_generate_random_block(random, sizeof(random));
|
|
|
|
HEIM_TAILQ_FOREACH(mech, &ctx->negoex_mechs, links)
|
|
_gss_negoex_log_auth_scheme(ctx->flags.local, ++i, mech->scheme);
|
|
|
|
return _gss_negoex_add_nego_message(minor, ctx, INITIATOR_NEGO, random);
|
|
}
|
|
|
|
static OM_uint32
|
|
process_initiator_nego(OM_uint32 *minor,
|
|
gssspnego_ctx ctx,
|
|
struct negoex_message *messages,
|
|
size_t nmessages)
|
|
{
|
|
struct nego_message *msg;
|
|
size_t i;
|
|
|
|
heim_assert(!ctx->flags.local && ctx->negoex_step == 1,
|
|
"NegoEx INITIATOR_NEGO token received after first leg");
|
|
|
|
msg = _gss_negoex_locate_nego_message(messages, nmessages, INITIATOR_NEGO);
|
|
if (msg == NULL) {
|
|
*minor = (OM_uint32)NEGOEX_MISSING_NEGO_MESSAGE;
|
|
return GSS_S_DEFECTIVE_TOKEN;
|
|
}
|
|
|
|
for (i = 0; i < msg->nschemes; i++)
|
|
_gss_negoex_log_auth_scheme(ctx->flags.local, i + 1, &msg->schemes[i * GUID_LENGTH]);
|
|
|
|
_gss_negoex_restrict_auth_schemes(ctx, msg->schemes, msg->nschemes);
|
|
|
|
return GSS_S_COMPLETE;
|
|
}
|
|
|
|
static OM_uint32
|
|
emit_acceptor_nego(OM_uint32 *minor, gssspnego_ctx ctx)
|
|
{
|
|
uint8_t random[32];
|
|
|
|
krb5_generate_random_block(random, 32);
|
|
|
|
return _gss_negoex_add_nego_message(minor, ctx, ACCEPTOR_NEGO, random);
|
|
}
|
|
|
|
static OM_uint32
|
|
process_acceptor_nego(OM_uint32 *minor,
|
|
gssspnego_ctx ctx,
|
|
struct negoex_message *messages,
|
|
size_t nmessages)
|
|
{
|
|
struct nego_message *msg;
|
|
|
|
msg = _gss_negoex_locate_nego_message(messages, nmessages, ACCEPTOR_NEGO);
|
|
if (msg == NULL) {
|
|
*minor = (OM_uint32)NEGOEX_MISSING_NEGO_MESSAGE;
|
|
return GSS_S_DEFECTIVE_TOKEN;
|
|
}
|
|
|
|
/*
|
|
* Reorder and prune our mech list to match the acceptor's list (or a
|
|
* subset of it).
|
|
*/
|
|
_gss_negoex_common_auth_schemes(ctx, msg->schemes, msg->nschemes);
|
|
|
|
return GSS_S_COMPLETE;
|
|
}
|
|
|
|
static void
|
|
query_meta_data(gssspnego_ctx ctx,
|
|
struct gssspnego_optimistic_ctx *opt,
|
|
gss_cred_id_t cred,
|
|
OM_uint32 req_flags)
|
|
{
|
|
OM_uint32 major, minor;
|
|
struct negoex_auth_mech *p, *next;
|
|
|
|
/*
|
|
* Note that if we received an optimistic context token from SPNEGO,
|
|
* then we will call QMD after ISC, rather than before. Mechanisms
|
|
* must be prepared to handle this and must not assume the context
|
|
* will be NULL on entry.
|
|
*/
|
|
HEIM_TAILQ_FOREACH_SAFE(p, &ctx->negoex_mechs, links, next) {
|
|
if (opt != NULL && memcmp(opt->scheme, p->scheme, GUID_LENGTH) == 0)
|
|
p->mech_context = opt->gssctx;;
|
|
|
|
major = gssspi_query_meta_data(&minor, p->oid, cred, &p->mech_context,
|
|
ctx->target_name, req_flags, &p->metadata);
|
|
/* GSS_Query_meta_data failure removes mechanism from list. */
|
|
if (major != GSS_S_COMPLETE)
|
|
_gss_negoex_delete_auth_mech(ctx, p);
|
|
}
|
|
}
|
|
|
|
static void
|
|
exchange_meta_data(gssspnego_ctx ctx,
|
|
gss_cred_id_t cred,
|
|
OM_uint32 req_flags,
|
|
struct negoex_message *messages,
|
|
size_t nmessages)
|
|
{
|
|
OM_uint32 major, minor;
|
|
struct negoex_auth_mech *mech;
|
|
enum message_type type;
|
|
struct exchange_message *msg;
|
|
uint32_t i;
|
|
|
|
type = ctx->flags.local ? ACCEPTOR_META_DATA : INITIATOR_META_DATA;
|
|
|
|
for (i = 0; i < nmessages; i++) {
|
|
if (messages[i].type != type)
|
|
continue;
|
|
msg = &messages[i].u.e;
|
|
|
|
mech = _gss_negoex_locate_auth_scheme(ctx, msg->scheme);
|
|
if (mech == NULL)
|
|
continue;
|
|
|
|
major = gssspi_exchange_meta_data(&minor, mech->oid, cred,
|
|
&mech->mech_context,
|
|
ctx->target_name,
|
|
req_flags, &msg->token);
|
|
/* GSS_Exchange_meta_data failure removes mechanism from list. */
|
|
if (major != GSS_S_COMPLETE)
|
|
_gss_negoex_delete_auth_mech(ctx, mech);
|
|
}
|
|
}
|
|
|
|
static void
|
|
release_mech_crypto(struct negoex_auth_mech *mech)
|
|
{
|
|
krb5_context context = NULL;
|
|
|
|
if (mech->crypto || mech->verify_crypto)
|
|
context = _gss_mg_krb5_context();
|
|
|
|
if (mech->crypto) {
|
|
krb5_crypto_destroy(context, mech->crypto);
|
|
mech->crypto = NULL;
|
|
}
|
|
|
|
if (mech->verify_crypto) {
|
|
krb5_crypto_destroy(context, mech->verify_crypto);
|
|
mech->verify_crypto = NULL;
|
|
}
|
|
|
|
mech->sent_checksum = FALSE;
|
|
}
|
|
|
|
/*
|
|
* In the initiator, if we are processing the acceptor's first reply, discard
|
|
* the optimistic context if the acceptor ignored the optimistic token. If the
|
|
* acceptor continued the optimistic mech, discard all other mechs.
|
|
*/
|
|
static void
|
|
check_optimistic_result(gssspnego_ctx ctx,
|
|
struct negoex_message *messages,
|
|
size_t nmessages)
|
|
{
|
|
struct negoex_auth_mech *mech;
|
|
OM_uint32 tmpMinor;
|
|
|
|
heim_assert(ctx->flags.local && ctx->negoex_step == 2,
|
|
"NegoEx optimistic result should only be checked in second leg");
|
|
|
|
/* Do nothing if we didn't make an optimistic context. */
|
|
mech = HEIM_TAILQ_FIRST(&ctx->negoex_mechs);
|
|
if (mech == NULL || mech->mech_context == GSS_C_NO_CONTEXT)
|
|
return;
|
|
|
|
/*
|
|
* If the acceptor used the optimistic token, it will send an acceptor
|
|
* token or a checksum (or both) in its first reply.
|
|
*/
|
|
if (_gss_negoex_locate_exchange_message(messages, nmessages,
|
|
CHALLENGE) != NULL ||
|
|
_gss_negoex_locate_verify_message(messages, nmessages) != NULL) {
|
|
/*
|
|
* The acceptor continued the optimistic mech, and metadata exchange
|
|
* didn't remove it. Commit to this mechanism.
|
|
*/
|
|
_gss_negoex_select_auth_mech(ctx, mech);
|
|
} else {
|
|
/*
|
|
* The acceptor ignored the optimistic token. Restart the mech.
|
|
*/
|
|
gss_delete_sec_context(&tmpMinor, &mech->mech_context, GSS_C_NO_BUFFER);
|
|
release_mech_crypto(mech);
|
|
mech->complete = FALSE;
|
|
}
|
|
}
|
|
|
|
/* Perform an initiator step of the underlying mechanism exchange. */
|
|
static OM_uint32
|
|
mech_init(OM_uint32 *minor,
|
|
struct gssspnego_optimistic_ctx *opt,
|
|
gssspnego_ctx ctx,
|
|
gss_cred_id_t cred,
|
|
OM_uint32 req_flags,
|
|
OM_uint32 time_req,
|
|
const gss_channel_bindings_t input_chan_bindings,
|
|
struct negoex_message *messages,
|
|
size_t nmessages,
|
|
gss_buffer_t output_token,
|
|
int *mech_error)
|
|
{
|
|
OM_uint32 major, first_major = GSS_S_COMPLETE, first_minor = 0;
|
|
struct negoex_auth_mech *mech = NULL;
|
|
gss_buffer_t input_token = GSS_C_NO_BUFFER;
|
|
struct exchange_message *msg;
|
|
int first_mech;
|
|
krb5_context context = _gss_mg_krb5_context();
|
|
|
|
output_token->value = NULL;
|
|
output_token->length = 0;
|
|
|
|
*mech_error = FALSE;
|
|
|
|
/* Allow disabling of optimistic token for testing. */
|
|
if (ctx->negoex_step == 1 &&
|
|
secure_getenv("NEGOEX_NO_OPTIMISTIC_TOKEN") != NULL)
|
|
return GSS_S_COMPLETE;
|
|
|
|
if (HEIM_TAILQ_EMPTY(&ctx->negoex_mechs)) {
|
|
*minor = (OM_uint32)NEGOEX_NO_AVAILABLE_MECHS;
|
|
return GSS_S_FAILURE;
|
|
}
|
|
|
|
/*
|
|
* Get the input token. The challenge could be for the optimistic mech,
|
|
* which we might have discarded in metadata exchange, so ignore the
|
|
* challenge if it doesn't match the first auth mech.
|
|
*/
|
|
mech = HEIM_TAILQ_FIRST(&ctx->negoex_mechs);
|
|
msg = _gss_negoex_locate_exchange_message(messages, nmessages, CHALLENGE);
|
|
if (msg != NULL && GUID_EQ(msg->scheme, mech->scheme))
|
|
input_token = &msg->token;
|
|
|
|
if (mech->complete)
|
|
return GSS_S_COMPLETE;
|
|
|
|
first_mech = TRUE;
|
|
major = GSS_S_BAD_MECH;
|
|
|
|
while (!HEIM_TAILQ_EMPTY(&ctx->negoex_mechs)) {
|
|
mech = HEIM_TAILQ_FIRST(&ctx->negoex_mechs);
|
|
|
|
/*
|
|
* If SPNEGO generated an optimistic token when probing available
|
|
* mechanisms, we can reuse it here. This avoids a potentially
|
|
* expensive and redundant call to GSS_Init_sec_context();
|
|
*/
|
|
if (opt != NULL && memcmp(opt->scheme, mech->scheme, GUID_LENGTH) == 0) {
|
|
heim_assert(ctx->negoex_step == 1,
|
|
"SPNEGO optimistic token only valid for NegoEx first leg");
|
|
|
|
major = _gss_copy_buffer(minor, &opt->optimistic_token, output_token);
|
|
if (GSS_ERROR(major))
|
|
return major;
|
|
|
|
ctx->negotiated_mech_type = opt->negotiated_mech_type;
|
|
ctx->mech_flags = opt->optimistic_flags;
|
|
ctx->mech_time_rec = opt->optimistic_time_rec;
|
|
|
|
mech->mech_context = opt->gssctx;
|
|
opt->gssctx = NULL; /* steal it */
|
|
|
|
mech->complete = opt->complete;
|
|
major = GSS_S_COMPLETE;
|
|
} else {
|
|
major = gss_init_sec_context(minor, cred, &mech->mech_context,
|
|
ctx->target_name, mech->oid,
|
|
req_flags, time_req,
|
|
input_chan_bindings, input_token,
|
|
&ctx->negotiated_mech_type, output_token,
|
|
&ctx->mech_flags, &ctx->mech_time_rec);
|
|
if (major == GSS_S_COMPLETE)
|
|
mech->complete = 1;
|
|
else if (GSS_ERROR(major)) {
|
|
gss_mg_collect_error(mech->oid, major, *minor);
|
|
*mech_error = TRUE;
|
|
}
|
|
}
|
|
if (!GSS_ERROR(major))
|
|
return get_session_keys(minor, context, mech);
|
|
|
|
/* Remember the error we got from the first mech. */
|
|
if (first_mech) {
|
|
first_major = major;
|
|
first_minor = *minor;
|
|
}
|
|
|
|
/* If we still have multiple mechs to try, move on to the next one. */
|
|
_gss_negoex_delete_auth_mech(ctx, mech);
|
|
first_mech = FALSE;
|
|
input_token = GSS_C_NO_BUFFER;
|
|
}
|
|
|
|
if (HEIM_TAILQ_EMPTY(&ctx->negoex_mechs)) {
|
|
major = first_major;
|
|
*minor = first_minor;
|
|
}
|
|
|
|
return major;
|
|
}
|
|
|
|
/* Perform an acceptor step of the underlying mechanism exchange. */
|
|
static OM_uint32
|
|
mech_accept(OM_uint32 *minor,
|
|
gssspnego_ctx ctx,
|
|
gss_cred_id_t cred,
|
|
const gss_channel_bindings_t input_chan_bindings,
|
|
struct negoex_message *messages,
|
|
size_t nmessages,
|
|
gss_buffer_t output_token,
|
|
gss_cred_id_t *deleg_cred,
|
|
int *mech_error)
|
|
{
|
|
OM_uint32 major, tmpMinor;
|
|
struct negoex_auth_mech *mech;
|
|
struct exchange_message *msg;
|
|
krb5_context context = _gss_mg_krb5_context();
|
|
|
|
heim_assert(!ctx->flags.local && !HEIM_TAILQ_EMPTY(&ctx->negoex_mechs),
|
|
"Acceptor NegoEx function called in wrong sequence");
|
|
|
|
*mech_error = FALSE;
|
|
|
|
msg = _gss_negoex_locate_exchange_message(messages, nmessages, AP_REQUEST);
|
|
if (msg == NULL) {
|
|
/*
|
|
* No input token is okay on the first request or if the mech is
|
|
* complete.
|
|
*/
|
|
if (ctx->negoex_step == 1 ||
|
|
HEIM_TAILQ_FIRST(&ctx->negoex_mechs)->complete)
|
|
return GSS_S_COMPLETE;
|
|
*minor = (OM_uint32)NEGOEX_MISSING_AP_REQUEST_MESSAGE;
|
|
return GSS_S_DEFECTIVE_TOKEN;
|
|
}
|
|
|
|
if (ctx->negoex_step == 1) {
|
|
/*
|
|
* Ignore the optimistic token if it isn't for our most preferred
|
|
* mech.
|
|
*/
|
|
mech = HEIM_TAILQ_FIRST(&ctx->negoex_mechs);
|
|
if (!GUID_EQ(msg->scheme, mech->scheme)) {
|
|
_gss_mg_log(10, "negoex ignored optimistic token as not for preferred mech");
|
|
return GSS_S_COMPLETE;
|
|
}
|
|
} else {
|
|
/* The initiator has selected a mech; discard other entries. */
|
|
mech = _gss_negoex_locate_auth_scheme(ctx, msg->scheme);
|
|
if (mech == NULL) {
|
|
*minor = (OM_uint32)NEGOEX_NO_AVAILABLE_MECHS;
|
|
return GSS_S_FAILURE;
|
|
}
|
|
_gss_negoex_select_auth_mech(ctx, mech);
|
|
}
|
|
|
|
if (mech->complete)
|
|
return GSS_S_COMPLETE;
|
|
|
|
if (ctx->mech_src_name != GSS_C_NO_NAME)
|
|
gss_release_name(&tmpMinor, &ctx->mech_src_name);
|
|
if (deleg_cred && *deleg_cred != GSS_C_NO_CREDENTIAL)
|
|
gss_release_cred(&tmpMinor, deleg_cred);
|
|
|
|
major = gss_accept_sec_context(minor, &mech->mech_context, cred,
|
|
&msg->token, input_chan_bindings,
|
|
&ctx->mech_src_name, &ctx->negotiated_mech_type,
|
|
output_token, &ctx->mech_flags,
|
|
&ctx->mech_time_rec, deleg_cred);
|
|
if (major == GSS_S_COMPLETE)
|
|
mech->complete = 1;
|
|
|
|
if (!GSS_ERROR(major)) {
|
|
if (major == GSS_S_COMPLETE &&
|
|
!gss_oid_equal(ctx->negotiated_mech_type, mech->oid))
|
|
_gss_mg_log(1, "negoex client didn't send the mech they said they would");
|
|
|
|
major = get_session_keys(minor, context, mech);
|
|
} else if (ctx->negoex_step == 1) {
|
|
gss_mg_collect_error(ctx->negotiated_mech_type, major, *minor);
|
|
*mech_error = TRUE;
|
|
|
|
/* This was an optimistic token; pretend this never happened. */
|
|
major = GSS_S_COMPLETE;
|
|
*minor = 0;
|
|
gss_release_buffer(&tmpMinor, output_token);
|
|
gss_delete_sec_context(&tmpMinor, &mech->mech_context, GSS_C_NO_BUFFER);
|
|
}
|
|
|
|
return major;
|
|
}
|
|
|
|
static krb5_keyusage
|
|
verify_keyusage(gssspnego_ctx ctx, int make_checksum)
|
|
{
|
|
/* Of course, these are the wrong way around in the spec. */
|
|
return (ctx->flags.local ^ !make_checksum) ?
|
|
NEGOEX_KEYUSAGE_ACCEPTOR_CHECKSUM : NEGOEX_KEYUSAGE_INITIATOR_CHECKSUM;
|
|
}
|
|
|
|
static OM_uint32
|
|
verify_checksum(OM_uint32 *minor,
|
|
gssspnego_ctx ctx,
|
|
struct negoex_message *messages,
|
|
size_t nmessages,
|
|
gss_const_buffer_t input_token,
|
|
int *send_alert_out)
|
|
{
|
|
krb5_error_code ret;
|
|
struct negoex_auth_mech *mech = HEIM_TAILQ_FIRST(&ctx->negoex_mechs);
|
|
struct verify_message *msg;
|
|
krb5_context context = _gss_mg_krb5_context();
|
|
krb5_crypto_iov iov[3];
|
|
krb5_keyusage usage = verify_keyusage(ctx, FALSE);
|
|
|
|
*send_alert_out = FALSE;
|
|
heim_assert(mech != NULL, "Invalid null mech when verifying NegoEx checksum");
|
|
|
|
/*
|
|
* The other party may not be ready to send a verify token yet, or (in the
|
|
* first initiator step) may send one for a mechanism we don't support.
|
|
*/
|
|
msg = _gss_negoex_locate_verify_message(messages, nmessages);
|
|
if (msg == NULL || !GUID_EQ(msg->scheme, mech->scheme))
|
|
return GSS_S_COMPLETE;
|
|
|
|
/*
|
|
* A recoverable error may cause us to be unable to verify a token from the
|
|
* other party. In this case we should send an alert.
|
|
*/
|
|
if (mech->verify_crypto == NULL) {
|
|
*send_alert_out = TRUE;
|
|
return GSS_S_COMPLETE;
|
|
}
|
|
|
|
if (!krb5_checksum_is_keyed(context, msg->cksum_type)) {
|
|
*minor = (OM_uint32)NEGOEX_INVALID_CHECKSUM;
|
|
return GSS_S_BAD_SIG;
|
|
}
|
|
|
|
/*
|
|
* Verify the checksum over the existing transcript and the portion of the
|
|
* input token leading up to the verify message.
|
|
*/
|
|
iov[0].flags = KRB5_CRYPTO_TYPE_DATA;
|
|
ret = krb5_storage_to_data(ctx->negoex_transcript, &iov[0].data);
|
|
if (ret) {
|
|
*minor = ret;
|
|
return GSS_S_FAILURE;
|
|
}
|
|
|
|
iov[1].flags = KRB5_CRYPTO_TYPE_DATA;
|
|
iov[1].data.data = input_token->value;
|
|
iov[1].data.length = msg->offset_in_token;
|
|
|
|
iov[2].flags = KRB5_CRYPTO_TYPE_CHECKSUM;
|
|
iov[2].data.data = (uint8_t *)msg->cksum;
|
|
iov[2].data.length = msg->cksum_len;
|
|
|
|
ret = krb5_verify_checksum_iov(context, mech->verify_crypto, usage,
|
|
iov, sizeof(iov) / sizeof(iov[0]), NULL);
|
|
if (ret == 0)
|
|
mech->verified_checksum = TRUE;
|
|
else
|
|
*minor = ret;
|
|
|
|
krb5_data_free(&iov[0].data);
|
|
|
|
return (ret == 0) ? GSS_S_COMPLETE : GSS_S_FAILURE;
|
|
}
|
|
|
|
static OM_uint32
|
|
make_checksum(OM_uint32 *minor, gssspnego_ctx ctx)
|
|
{
|
|
krb5_error_code ret;
|
|
krb5_context context = _gss_mg_krb5_context();
|
|
krb5_data d;
|
|
krb5_keyusage usage = verify_keyusage(ctx, TRUE);
|
|
krb5_checksum cksum;
|
|
struct negoex_auth_mech *mech = HEIM_TAILQ_FIRST(&ctx->negoex_mechs);
|
|
OM_uint32 major;
|
|
|
|
heim_assert(mech != NULL, "Invalid null mech when making NegoEx checksum");
|
|
|
|
if (mech->crypto == NULL) {
|
|
if (mech->complete) {
|
|
*minor = (OM_uint32)NEGOEX_NO_VERIFY_KEY;
|
|
return GSS_S_UNAVAILABLE;
|
|
} else {
|
|
return GSS_S_COMPLETE;
|
|
}
|
|
}
|
|
|
|
ret = krb5_storage_to_data(ctx->negoex_transcript, &d);
|
|
if (ret) {
|
|
*minor = ret;
|
|
return GSS_S_FAILURE;
|
|
}
|
|
|
|
ret = krb5_create_checksum(context, mech->crypto,
|
|
usage, 0, d.data, d.length, &cksum);
|
|
krb5_data_free(&d);
|
|
if (ret) {
|
|
*minor = ret;
|
|
return GSS_S_FAILURE;
|
|
}
|
|
|
|
major = _gss_negoex_add_verify_message(minor, ctx, mech->scheme,
|
|
cksum.cksumtype,
|
|
cksum.checksum.data,
|
|
cksum.checksum.length);
|
|
free_Checksum(&cksum);
|
|
|
|
if (major == GSS_S_COMPLETE)
|
|
mech->sent_checksum = TRUE;
|
|
|
|
return major;
|
|
}
|
|
|
|
/*
|
|
* If the other side sent a VERIFY_NO_KEY pulse alert, clear the checksum state
|
|
* on the mechanism so that we send another VERIFY message.
|
|
*/
|
|
static void
|
|
process_alerts(gssspnego_ctx ctx,
|
|
struct negoex_message *messages,
|
|
uint32_t nmessages)
|
|
{
|
|
struct alert_message *msg;
|
|
struct negoex_auth_mech *mech;
|
|
|
|
msg = _gss_negoex_locate_alert_message(messages, nmessages);
|
|
if (msg != NULL && msg->verify_no_key) {
|
|
mech = _gss_negoex_locate_auth_scheme(ctx, msg->scheme);
|
|
if (mech != NULL)
|
|
release_mech_crypto(mech);
|
|
}
|
|
}
|
|
|
|
static OM_uint32
|
|
make_output_token(OM_uint32 *minor,
|
|
gssspnego_ctx ctx,
|
|
gss_buffer_t mech_output_token,
|
|
int send_alert,
|
|
gss_buffer_t output_token)
|
|
{
|
|
OM_uint32 major, tmpMinor;
|
|
struct negoex_auth_mech *mech;
|
|
enum message_type type;
|
|
off_t old_transcript_len;
|
|
|
|
output_token->length = 0;
|
|
output_token->value = NULL;
|
|
|
|
old_transcript_len = krb5_storage_seek(ctx->negoex_transcript, 0, SEEK_CUR);
|
|
|
|
/*
|
|
* If the mech is complete and we previously sent a checksum, we just
|
|
* processed the last leg and don't need to send another token.
|
|
*/
|
|
if (mech_output_token->length == 0 &&
|
|
HEIM_TAILQ_FIRST(&ctx->negoex_mechs)->sent_checksum)
|
|
return GSS_S_COMPLETE;
|
|
|
|
if (ctx->negoex_step == 1) {
|
|
if (ctx->flags.local)
|
|
major = emit_initiator_nego(minor, ctx);
|
|
else
|
|
major = emit_acceptor_nego(minor, ctx);
|
|
if (major != GSS_S_COMPLETE)
|
|
return major;
|
|
|
|
type = ctx->flags.local ? INITIATOR_META_DATA : ACCEPTOR_META_DATA;
|
|
HEIM_TAILQ_FOREACH(mech, &ctx->negoex_mechs, links) {
|
|
if (mech->metadata.length > 0) {
|
|
major = _gss_negoex_add_exchange_message(minor, ctx,
|
|
type, mech->scheme,
|
|
&mech->metadata);
|
|
if (major != GSS_S_COMPLETE)
|
|
return major;
|
|
}
|
|
}
|
|
}
|
|
|
|
mech = HEIM_TAILQ_FIRST(&ctx->negoex_mechs);
|
|
|
|
if (mech_output_token->length > 0) {
|
|
type = ctx->flags.local ? AP_REQUEST : CHALLENGE;
|
|
major = _gss_negoex_add_exchange_message(minor, ctx,
|
|
type, mech->scheme,
|
|
mech_output_token);
|
|
if (major != GSS_S_COMPLETE)
|
|
return major;
|
|
}
|
|
|
|
if (send_alert) {
|
|
major = _gss_negoex_add_verify_no_key_alert(minor, ctx, mech->scheme);
|
|
if (major != GSS_S_COMPLETE)
|
|
return major;
|
|
}
|
|
|
|
/* Try to add a VERIFY message if we haven't already done so. */
|
|
if (!mech->sent_checksum) {
|
|
major = make_checksum(minor, ctx);
|
|
if (major != GSS_S_COMPLETE)
|
|
return major;
|
|
}
|
|
|
|
heim_assert(ctx->negoex_transcript != NULL, "NegoEx context uninitialized");
|
|
|
|
output_token->length =
|
|
krb5_storage_seek(ctx->negoex_transcript, 0, SEEK_CUR) - old_transcript_len;
|
|
output_token->value = malloc(output_token->length);
|
|
if (output_token->value == NULL) {
|
|
*minor = ENOMEM;
|
|
return GSS_S_FAILURE;
|
|
}
|
|
|
|
krb5_storage_seek(ctx->negoex_transcript, old_transcript_len, SEEK_SET);
|
|
|
|
if (krb5_storage_read(ctx->negoex_transcript,
|
|
output_token->value,
|
|
output_token->length) != output_token->length) {
|
|
*minor = ERANGE;
|
|
gss_release_buffer(&tmpMinor, output_token);
|
|
return GSS_S_FAILURE;
|
|
}
|
|
|
|
krb5_storage_seek(ctx->negoex_transcript, 0, SEEK_END);
|
|
|
|
return GSS_S_COMPLETE;
|
|
}
|
|
|
|
OM_uint32
|
|
_gss_negoex_init(OM_uint32 *minor,
|
|
struct gssspnego_optimistic_ctx *opt,
|
|
gssspnego_ctx ctx,
|
|
gss_cred_id_t cred,
|
|
OM_uint32 req_flags,
|
|
OM_uint32 time_req,
|
|
const gss_channel_bindings_t input_chan_bindings,
|
|
gss_const_buffer_t input_token,
|
|
gss_buffer_t output_token)
|
|
{
|
|
OM_uint32 major, tmpMinor;
|
|
gss_buffer_desc mech_output_token = GSS_C_EMPTY_BUFFER;
|
|
struct negoex_message *messages = NULL;
|
|
struct negoex_auth_mech *mech;
|
|
size_t nmessages = 0;
|
|
int send_alert = FALSE, mech_error = FALSE;
|
|
|
|
if (ctx->negoex_step == 0 && input_token != GSS_C_NO_BUFFER &&
|
|
input_token->length != 0)
|
|
return GSS_S_DEFECTIVE_TOKEN;
|
|
|
|
major = _gss_negoex_begin(minor, ctx);
|
|
if (major != GSS_S_COMPLETE)
|
|
goto cleanup;
|
|
|
|
ctx->negoex_step++;
|
|
|
|
if (input_token != GSS_C_NO_BUFFER && input_token->length > 0) {
|
|
major = _gss_negoex_parse_token(minor, ctx, input_token,
|
|
&messages, &nmessages);
|
|
if (major != GSS_S_COMPLETE)
|
|
goto cleanup;
|
|
}
|
|
|
|
process_alerts(ctx, messages, nmessages);
|
|
|
|
if (ctx->negoex_step == 1) {
|
|
/* Choose a random conversation ID. */
|
|
krb5_generate_random_block(ctx->negoex_conv_id, GUID_LENGTH);
|
|
|
|
/* Query each mech for its metadata (this may prune the mech list). */
|
|
query_meta_data(ctx, opt, cred, req_flags);
|
|
} else if (ctx->negoex_step == 2) {
|
|
/* See if the mech processed the optimistic token. */
|
|
check_optimistic_result(ctx, messages, nmessages);
|
|
|
|
/* Pass the acceptor metadata to each mech to prune the list. */
|
|
exchange_meta_data(ctx, cred, req_flags, messages, nmessages);
|
|
|
|
/* Process the ACCEPTOR_NEGO message. */
|
|
major = process_acceptor_nego(minor, ctx, messages, nmessages);
|
|
if (major != GSS_S_COMPLETE)
|
|
goto cleanup;
|
|
}
|
|
|
|
/*
|
|
* Process the input token and/or produce an output token. This may prune
|
|
* the mech list, but on success there will be at least one mech entry.
|
|
*/
|
|
major = mech_init(minor, opt, ctx, cred, req_flags, time_req,
|
|
input_chan_bindings, messages, nmessages,
|
|
&mech_output_token, &mech_error);
|
|
if (major != GSS_S_COMPLETE)
|
|
goto cleanup;
|
|
heim_assert(!HEIM_TAILQ_EMPTY(&ctx->negoex_mechs),
|
|
"Invalid empty NegoEx mechanism list");
|
|
|
|
/*
|
|
* At this point in step 2 we have performed the metadata exchange and
|
|
* chosen a mech we can use, so discard any fallback mech entries.
|
|
*/
|
|
if (ctx->negoex_step == 2)
|
|
_gss_negoex_select_auth_mech(ctx, HEIM_TAILQ_FIRST(&ctx->negoex_mechs));
|
|
|
|
major = verify_checksum(minor, ctx, messages, nmessages, input_token,
|
|
&send_alert);
|
|
if (major != GSS_S_COMPLETE)
|
|
goto cleanup;
|
|
|
|
if (input_token != GSS_C_NO_BUFFER) {
|
|
if (krb5_storage_write(ctx->negoex_transcript,
|
|
input_token->value,
|
|
input_token->length) != input_token->length) {
|
|
major = GSS_S_FAILURE;
|
|
*minor = ENOMEM;
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
major = make_output_token(minor, ctx, &mech_output_token, send_alert,
|
|
output_token);
|
|
if (major != GSS_S_COMPLETE)
|
|
goto cleanup;
|
|
|
|
mech = HEIM_TAILQ_FIRST(&ctx->negoex_mechs);
|
|
major = (mech->complete && mech->verified_checksum) ? GSS_S_COMPLETE :
|
|
GSS_S_CONTINUE_NEEDED;
|
|
|
|
cleanup:
|
|
free(messages);
|
|
gss_release_buffer(&tmpMinor, &mech_output_token);
|
|
_gss_negoex_end(ctx);
|
|
|
|
if (GSS_ERROR(major)) {
|
|
if (!mech_error) {
|
|
krb5_context context = _gss_mg_krb5_context();
|
|
|
|
gss_mg_set_error_string(GSS_SPNEGO_MECHANISM,
|
|
major, *minor,
|
|
"NegoEx failed to initialize security context: %s",
|
|
krb5_get_error_message(context, *minor));
|
|
}
|
|
|
|
_gss_negoex_release_context(ctx);
|
|
}
|
|
|
|
return major;
|
|
}
|
|
|
|
OM_uint32
|
|
_gss_negoex_accept(OM_uint32 *minor,
|
|
gssspnego_ctx ctx,
|
|
gss_cred_id_t cred,
|
|
gss_const_buffer_t input_token,
|
|
const gss_channel_bindings_t input_chan_bindings,
|
|
gss_buffer_t output_token,
|
|
gss_cred_id_t *deleg_cred)
|
|
{
|
|
OM_uint32 major, tmpMinor;
|
|
gss_buffer_desc mech_output_token = GSS_C_EMPTY_BUFFER;
|
|
struct negoex_message *messages = NULL;
|
|
struct negoex_auth_mech *mech;
|
|
size_t nmessages;
|
|
int send_alert = FALSE, mech_error = FALSE;
|
|
|
|
if (input_token == GSS_C_NO_BUFFER || input_token->length == 0) {
|
|
major = GSS_S_DEFECTIVE_TOKEN;
|
|
goto cleanup;
|
|
}
|
|
|
|
major = _gss_negoex_begin(minor, ctx);
|
|
if (major != GSS_S_COMPLETE)
|
|
goto cleanup;
|
|
|
|
ctx->negoex_step++;
|
|
|
|
major = _gss_negoex_parse_token(minor, ctx, input_token,
|
|
&messages, &nmessages);
|
|
if (major != GSS_S_COMPLETE)
|
|
goto cleanup;
|
|
|
|
process_alerts(ctx, messages, nmessages);
|
|
|
|
if (ctx->negoex_step == 1) {
|
|
/*
|
|
* Read the INITIATOR_NEGO message to prune the candidate mech list.
|
|
*/
|
|
major = process_initiator_nego(minor, ctx, messages, nmessages);
|
|
if (major != GSS_S_COMPLETE)
|
|
goto cleanup;
|
|
|
|
/*
|
|
* Pass the initiator metadata to each mech to prune the list, and
|
|
* query each mech for its acceptor metadata (which may also prune the
|
|
* list).
|
|
*/
|
|
exchange_meta_data(ctx, cred, 0, messages, nmessages);
|
|
query_meta_data(ctx, NULL, cred, 0);
|
|
|
|
if (HEIM_TAILQ_EMPTY(&ctx->negoex_mechs)) {
|
|
*minor = (OM_uint32)NEGOEX_NO_AVAILABLE_MECHS;
|
|
major = GSS_S_FAILURE;
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Process the input token and possibly produce an output token. This may
|
|
* prune the list to a single mech. Continue on error if an output token
|
|
* is generated, so that we send the token to the initiator.
|
|
*/
|
|
major = mech_accept(minor, ctx, cred, input_chan_bindings,
|
|
messages, nmessages, &mech_output_token,
|
|
deleg_cred, &mech_error);
|
|
if (major != GSS_S_COMPLETE && mech_output_token.length == 0)
|
|
goto cleanup;
|
|
|
|
if (major == GSS_S_COMPLETE) {
|
|
major = verify_checksum(minor, ctx, messages, nmessages, input_token,
|
|
&send_alert);
|
|
if (major != GSS_S_COMPLETE)
|
|
goto cleanup;
|
|
}
|
|
|
|
if (krb5_storage_write(ctx->negoex_transcript,
|
|
input_token->value,
|
|
input_token->length) != input_token->length) {
|
|
major = GSS_S_FAILURE;
|
|
*minor = ENOMEM;
|
|
goto cleanup;
|
|
}
|
|
|
|
major = make_output_token(minor, ctx, &mech_output_token, send_alert,
|
|
output_token);
|
|
if (major != GSS_S_COMPLETE)
|
|
goto cleanup;
|
|
|
|
mech = HEIM_TAILQ_FIRST(&ctx->negoex_mechs);
|
|
major = (mech->complete && mech->verified_checksum) ? GSS_S_COMPLETE :
|
|
GSS_S_CONTINUE_NEEDED;
|
|
|
|
cleanup:
|
|
free(messages);
|
|
gss_release_buffer(&tmpMinor, &mech_output_token);
|
|
_gss_negoex_end(ctx);
|
|
|
|
if (GSS_ERROR(major)) {
|
|
if (!mech_error) {
|
|
krb5_context context = _gss_mg_krb5_context();
|
|
|
|
gss_mg_set_error_string(GSS_SPNEGO_MECHANISM,
|
|
major, *minor,
|
|
"NegoEx failed to accept security context: %s",
|
|
krb5_get_error_message(context, *minor));
|
|
}
|
|
|
|
_gss_negoex_release_context(ctx);
|
|
}
|
|
|
|
return major;
|
|
}
|