Path: blob/main/crypto/krb5/src/plugins/preauth/spake/spake_kdc.c
34889 views
/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */1/* plugins/preauth/spake/spake_kdc.c - SPAKE kdcpreauth module */2/*3* Copyright (C) 2015 by the Massachusetts Institute of Technology.4* All rights reserved.5*6* Redistribution and use in source and binary forms, with or without7* modification, are permitted provided that the following conditions8* are met:9*10* * Redistributions of source code must retain the above copyright11* notice, this list of conditions and the following disclaimer.12*13* * Redistributions in binary form must reproduce the above copyright14* notice, this list of conditions and the following disclaimer in15* the documentation and/or other materials provided with the16* distribution.17*18* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS19* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT20* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS21* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE22* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,23* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES24* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR25* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)26* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,27* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)28* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED29* OF THE POSSIBILITY OF SUCH DAMAGE.30*/3132#include "k5-int.h"33#include "k5-input.h"34#include "k5-spake.h"3536#include "groups.h"37#include "trace.h"38#include "iana.h"39#include "util.h"4041#include <krb5/kdcpreauth_plugin.h>4243/*44* The SPAKE kdcpreauth module uses a secure cookie containing the following45* concatenated fields (all integer fields are big-endian):46*47* version (16-bit unsigned integer)48* stage (16-bit unsigned integer)49* group (32-bit signed integer)50* SPAKE value (32-bit unsigned length, followed by data)51* Transcript hash (32-bit unsigned length, followed by data)52* Zero or more instances of:53* second-factor number (32-bit signed integer)54* second-factor data (32-bit unsigned length, followed by data)55*56* The only currently supported version is 1. stage is 0 if the cookie was57* sent with a challenge message. stage is n>0 if the cookie was sent with an58* encdata message encrypted in K'[2n]. group indicates the group number used59* in the SPAKE challenge. The SPAKE value is the KDC private key for a60* stage-0 cookie, represented in the scalar marshalling form of the group; for61* other cookies, the SPAKE value is the SPAKE result K, represented in the62* group element marshalling form. The transcript hash is the intermediate63* hash after updating with the support and challenge messages for a stage-064* cookie, or the final hash for other cookies. For a stage 0 cookie, there65* may be any number of second-factor records, including none (no record is66* generated for SF-NONE); for other cookies, there must be exactly one67* second-factor record corresponding to the factor type chosen by the client.68*/6970/* From a k5input structure representing the remainder of a secure cookie71* plaintext, parse a four-byte length and data. */72static void73parse_data(struct k5input *in, krb5_data *out)74{75out->length = k5_input_get_uint32_be(in);76out->data = (char *)k5_input_get_bytes(in, out->length);77out->magic = KV5M_DATA;78}7980/* Parse a received cookie into its components. The pointers stored in the81* krb5_data outputs are aliases into cookie and should not be freed. */82static krb5_error_code83parse_cookie(const krb5_data *cookie, int *stage_out, int32_t *group_out,84krb5_data *spake_out, krb5_data *thash_out,85krb5_data *factors_out)86{87struct k5input in;88int version, stage;89int32_t group;90krb5_data thash, spake, factors;9192*spake_out = *thash_out = *factors_out = empty_data();93k5_input_init(&in, cookie->data, cookie->length);9495/* Parse and check the version, and read the other integer fields. */96version = k5_input_get_uint16_be(&in);97if (version != 1)98return KRB5KDC_ERR_PREAUTH_FAILED;99stage = k5_input_get_uint16_be(&in);100group = k5_input_get_uint32_be(&in);101102/* Parse the data fields. The factor data is anything remaining after the103* transcript hash. */104parse_data(&in, &spake);105parse_data(&in, &thash);106if (in.status)107return in.status;108factors = make_data((char *)in.ptr, in.len);109110*stage_out = stage;111*group_out = group;112*spake_out = spake;113*thash_out = thash;114*factors_out = factors;115return 0;116}117118/* Marshal data into buf as a four-byte length followed by the contents. */119static void120marshal_data(struct k5buf *buf, const krb5_data *data)121{122k5_buf_add_uint32_be(buf, data->length);123k5_buf_add_len(buf, data->data, data->length);124}125126/* Marshal components into a cookie. */127static krb5_error_code128make_cookie(int stage, int32_t group, const krb5_data *spake,129const krb5_data *thash, krb5_data *cookie_out)130{131struct k5buf buf;132133*cookie_out = empty_data();134k5_buf_init_dynamic_zap(&buf);135136/* Marshal the version, stage, and group. */137k5_buf_add_uint16_be(&buf, 1);138k5_buf_add_uint16_be(&buf, stage);139k5_buf_add_uint32_be(&buf, group);140141/* Marshal the data fields. */142marshal_data(&buf, spake);143marshal_data(&buf, thash);144145/* When second factor support is implemented, we should add factor data146* here. */147148if (buf.data == NULL)149return ENOMEM;150*cookie_out = make_data(buf.data, buf.len);151return 0;152}153154/* Add authentication indicators if any are configured for SPAKE. */155static krb5_error_code156add_indicators(krb5_context context, const krb5_data *realm,157krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock)158{159krb5_error_code ret;160const char *keys[4];161char *realmstr, **indicators, **ind;162163realmstr = k5memdup0(realm->data, realm->length, &ret);164if (realmstr == NULL)165return ret;166keys[0] = KRB5_CONF_REALMS;167keys[1] = realmstr;168keys[2] = KRB5_CONF_SPAKE_PREAUTH_INDICATOR;169keys[3] = NULL;170ret = profile_get_values(context->profile, keys, &indicators);171free(realmstr);172if (ret == PROF_NO_RELATION)173return 0;174if (ret)175return ret;176177for (ind = indicators; *ind != NULL && !ret; ind++)178ret = cb->add_auth_indicator(context, rock, *ind);179180profile_free_list(indicators);181return ret;182}183184/* Initialize a SPAKE module data object. */185static krb5_error_code186spake_init(krb5_context context, krb5_kdcpreauth_moddata *moddata_out,187const char **realmnames)188{189krb5_error_code ret;190groupstate *gstate;191192ret = group_init_state(context, TRUE, &gstate);193if (ret)194return ret;195*moddata_out = (krb5_kdcpreauth_moddata)gstate;196return 0;197}198199/* Release a SPAKE module data object. */200static void201spake_fini(krb5_context context, krb5_kdcpreauth_moddata moddata)202{203group_free_state((groupstate *)moddata);204}205206/*207* Generate a SPAKE challenge message for the specified group. Use cb and rock208* to retrieve the initial reply key and to set a stage-0 cookie. Invoke209* either erespond or vrespond with the result.210*/211static void212send_challenge(krb5_context context, groupstate *gstate, int32_t group,213krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock,214const krb5_data *support,215krb5_kdcpreauth_edata_respond_fn erespond,216krb5_kdcpreauth_verify_respond_fn vrespond, void *arg)217{218krb5_error_code ret;219const krb5_keyblock *ikey;220krb5_pa_data **padata = NULL, *pa;221krb5_data kdcpriv = empty_data(), kdcpub = empty_data(), *der_msg = NULL;222krb5_data thash = empty_data(), cookie = empty_data();223krb5_data wbytes = empty_data();224krb5_spake_factor f, *flist[2];225krb5_pa_spake msg;226227ikey = cb->client_keyblock(context, rock);228if (ikey == NULL) {229ret = KRB5KDC_ERR_ETYPE_NOSUPP;230goto cleanup;231}232233ret = derive_wbytes(context, group, ikey, &wbytes);234if (ret)235goto cleanup;236ret = group_keygen(context, gstate, group, &wbytes, &kdcpriv, &kdcpub);237if (ret)238goto cleanup;239240/* Encode the challenge. When second factor support is implemented, we241* should construct a factor list instead of hardcoding SF-NONE. */242f.type = SPAKE_SF_NONE;243f.data = NULL;244flist[0] = &f;245flist[1] = NULL;246msg.choice = SPAKE_MSGTYPE_CHALLENGE;247msg.u.challenge.group = group;248msg.u.challenge.pubkey = kdcpub;249msg.u.challenge.factors = flist;250ret = encode_krb5_pa_spake(&msg, &der_msg);251if (ret)252goto cleanup;253254/* Initialize and update the transcript hash with the support message (if255* we received one) and challenge message. */256ret = update_thash(context, gstate, group, &thash, support, der_msg);257if (ret)258goto cleanup;259260/* Save the group, transcript hash, and private key in a stage-0 cookie.261* When second factor support is implemented, also save factor state. */262ret = make_cookie(0, group, &kdcpriv, &thash, &cookie);263if (ret)264goto cleanup;265ret = cb->set_cookie(context, rock, KRB5_PADATA_SPAKE, &cookie);266if (ret)267goto cleanup;268269ret = convert_to_padata(der_msg, &padata);270der_msg = NULL;271TRACE_SPAKE_SEND_CHALLENGE(context, group);272273cleanup:274zapfree(wbytes.data, wbytes.length);275zapfree(kdcpriv.data, kdcpriv.length);276zapfree(cookie.data, cookie.length);277krb5_free_data_contents(context, &kdcpub);278krb5_free_data_contents(context, &thash);279krb5_free_data(context, der_msg);280281if (erespond != NULL) {282assert(vrespond == NULL);283/* Grab the first pa-data element from the list, if we made one. */284pa = (padata == NULL) ? NULL : padata[0];285free(padata);286(*erespond)(arg, ret, pa);287} else {288assert(vrespond != NULL);289if (!ret)290ret = KRB5KDC_ERR_MORE_PREAUTH_DATA_REQUIRED;291(*vrespond)(arg, ret, NULL, padata, NULL);292}293}294295/* Generate the METHOD-DATA entry indicating support for SPAKE. Include an296* optimistic challenge if configured to do so. */297static void298spake_edata(krb5_context context, krb5_kdc_req *req,299krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock,300krb5_kdcpreauth_moddata moddata, krb5_preauthtype pa_type,301krb5_kdcpreauth_edata_respond_fn respond, void *arg)302{303const krb5_keyblock *ikey;304groupstate *gstate = (groupstate *)moddata;305krb5_data empty = empty_data();306int32_t group;307308/* SPAKE requires a client key, which cannot be a single-DES key. */309ikey = cb->client_keyblock(context, rock);310if (ikey == NULL) {311(*respond)(arg, KRB5KDC_ERR_ETYPE_NOSUPP, NULL);312return;313}314315group = group_optimistic_challenge(gstate);316if (group) {317send_challenge(context, gstate, group, cb, rock, &empty, respond, NULL,318arg);319} else {320/* No optimistic challenge configured; send an empty pa-data value. */321(*respond)(arg, 0, NULL);322}323}324325/* Choose a group from the client's support message and generate a326* challenge. */327static void328verify_support(krb5_context context, groupstate *gstate,329krb5_spake_support *support, const krb5_data *der_msg,330krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock,331krb5_kdcpreauth_verify_respond_fn respond, void *arg)332{333krb5_error_code ret;334int32_t i, group;335336for (i = 0; i < support->ngroups; i++) {337if (group_is_permitted(gstate, support->groups[i]))338break;339}340if (i == support->ngroups) {341TRACE_SPAKE_REJECT_SUPPORT(context);342ret = KRB5KDC_ERR_PREAUTH_FAILED;343goto error;344}345group = support->groups[i];346TRACE_SPAKE_RECEIVE_SUPPORT(context, group);347348send_challenge(context, gstate, group, cb, rock, der_msg, NULL, respond,349arg);350return;351352error:353(*respond)(arg, ret, NULL, NULL, NULL);354}355356/*357* From the client's response message, compute the SPAKE result and decrypt the358* factor reply. On success, either mark the reply as pre-authenticated and359* set a reply key in the pre-request module data, or generate an additional360* factor challenge and ask for another round of pre-authentication.361*/362static void363verify_response(krb5_context context, groupstate *gstate,364krb5_spake_response *resp, const krb5_data *realm,365krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock,366krb5_enc_tkt_part *enc_tkt_reply,367krb5_kdcpreauth_verify_respond_fn respond, void *arg)368{369krb5_error_code ret;370const krb5_keyblock *ikey;371krb5_keyblock *k1 = NULL, *reply_key = NULL;372krb5_data cookie, thash_in, kdcpriv, factors, *der_req;373krb5_data thash = empty_data(), der_factor = empty_data();374krb5_data wbytes = empty_data(), spakeresult = empty_data();375krb5_spake_factor *factor = NULL;376int stage;377int32_t group;378379ikey = cb->client_keyblock(context, rock);380if (ikey == NULL) {381ret = KRB5KDC_ERR_ETYPE_NOSUPP;382goto cleanup;383}384385/* Fetch the stage-0 cookie and parse it. (All of the krb5_data results386* are aliases into memory owned by rock). */387if (!cb->get_cookie(context, rock, KRB5_PADATA_SPAKE, &cookie)) {388ret = KRB5KDC_ERR_PREAUTH_FAILED;389goto cleanup;390}391ret = parse_cookie(&cookie, &stage, &group, &kdcpriv, &thash_in, &factors);392if (ret)393goto cleanup;394if (stage != 0) {395/* The received cookie wasn't sent with a challenge. */396ret = KRB5KDC_ERR_PREAUTH_FAILED;397goto cleanup;398}399TRACE_SPAKE_RECEIVE_RESPONSE(context, &resp->pubkey);400401/* Update the transcript hash with the client public key. */402ret = krb5int_copy_data_contents(context, &thash_in, &thash);403if (ret)404goto cleanup;405ret = update_thash(context, gstate, group, &thash, &resp->pubkey, NULL);406if (ret)407goto cleanup;408TRACE_SPAKE_KDC_THASH(context, &thash);409410ret = derive_wbytes(context, group, ikey, &wbytes);411if (ret)412goto cleanup;413ret = group_result(context, gstate, group, &wbytes, &kdcpriv,414&resp->pubkey, &spakeresult);415if (ret)416goto cleanup;417418/* Decrypt the response factor field using K'[1]. If the decryption419* integrity check fails, the client probably used the wrong password. */420der_req = cb->request_body(context, rock);421ret = derive_key(context, gstate, group, ikey, &wbytes, &spakeresult,422&thash, der_req, 1, &k1);423if (ret)424goto cleanup;425ret = alloc_data(&der_factor, resp->factor.ciphertext.length);426if (ret)427goto cleanup;428ret = krb5_c_decrypt(context, k1, KRB5_KEYUSAGE_SPAKE, NULL, &resp->factor,429&der_factor);430if (ret == KRB5KRB_AP_ERR_BAD_INTEGRITY)431ret = KRB5KDC_ERR_PREAUTH_FAILED;432if (ret)433goto cleanup;434ret = decode_krb5_spake_factor(&der_factor, &factor);435if (ret)436goto cleanup;437438/*439* When second factor support is implemented, we should verify the factor440* data here, and possibly generate an encdata message for another hop.441* This function may need to be split at this point to allow for442* asynchronous verification of the second-factor value. We might also443* need to collect authentication indicators from the second-factor module;444* alternatively the module could have access to cb and rock so that it can445* add indicators itself.446*/447if (factor->type != SPAKE_SF_NONE) {448ret = KRB5KDC_ERR_PREAUTH_FAILED;449goto cleanup;450}451452ret = add_indicators(context, realm, cb, rock);453if (ret)454goto cleanup;455456enc_tkt_reply->flags |= TKT_FLG_PRE_AUTH;457458ret = derive_key(context, gstate, group, ikey, &wbytes, &spakeresult,459&thash, der_req, 0, &reply_key);460if (ret)461goto cleanup;462463ret = cb->replace_reply_key(context, rock, reply_key, TRUE);464465cleanup:466zapfree(wbytes.data, wbytes.length);467zapfree(der_factor.data, der_factor.length);468zapfree(spakeresult.data, spakeresult.length);469krb5_free_data_contents(context, &thash);470krb5_free_keyblock(context, k1);471krb5_free_keyblock(context, reply_key);472k5_free_spake_factor(context, factor);473(*respond)(arg, ret, NULL, NULL, NULL);474}475476/*477* Decrypt and validate an additional second-factor reply. On success, either478* mark the reply as pre-authenticated and set a reply key in the pre-request479* module data, or generate an additional factor challenge and ask for another480* round of pre-authentication.481*/482static void483verify_encdata(krb5_context context, krb5_enc_data *enc,484krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock,485krb5_enc_tkt_part *enc_tkt_reply,486krb5_kdcpreauth_verify_respond_fn respond, void *arg)487{488/*489* When second factor support is implemented, we should process encdata490* message according to the factor type recorded in the cookie. If the491* second factor exchange finishes successfully, we should set492* TKT_FLG_PRE_AUTH, set the reply key to K'[0], and add any auth493* indicators from configuration (with a call to add_indicators()) or the494* second factor module (unless the module has access to cb and rock and495* can add indicators itself).496*/497(*respond)(arg, KRB5KDC_ERR_PREAUTH_FAILED, NULL, NULL, NULL);498}499500/*501* Respond to a client padata message, either by generating a SPAKE challenge,502* generating an additional second-factor challenge, or marking the reply as503* pre-authenticated and setting an additional reply key in the pre-request504* module data.505*/506static void507spake_verify(krb5_context context, krb5_data *req_pkt, krb5_kdc_req *request,508krb5_enc_tkt_part *enc_tkt_reply, krb5_pa_data *data,509krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock,510krb5_kdcpreauth_moddata moddata,511krb5_kdcpreauth_verify_respond_fn respond, void *arg)512{513krb5_error_code ret;514krb5_pa_spake *pa_spake = NULL;515krb5_data in_data = make_data(data->contents, data->length);516groupstate *gstate = (groupstate *)moddata;517518ret = decode_krb5_pa_spake(&in_data, &pa_spake);519if (ret) {520(*respond)(arg, ret, NULL, NULL, NULL);521} else if (pa_spake->choice == SPAKE_MSGTYPE_SUPPORT) {522verify_support(context, gstate, &pa_spake->u.support, &in_data, cb,523rock, respond, arg);524} else if (pa_spake->choice == SPAKE_MSGTYPE_RESPONSE) {525verify_response(context, gstate, &pa_spake->u.response,526&request->server->realm, cb, rock, enc_tkt_reply,527respond, arg);528} else if (pa_spake->choice == SPAKE_MSGTYPE_ENCDATA) {529verify_encdata(context, &pa_spake->u.encdata, cb, rock, enc_tkt_reply,530respond, arg);531} else {532ret = KRB5KDC_ERR_PREAUTH_FAILED;533k5_setmsg(context, ret, _("Unknown SPAKE request type"));534(*respond)(arg, ret, NULL, NULL, NULL);535}536537k5_free_pa_spake(context, pa_spake);538}539540krb5_error_code541kdcpreauth_spake_initvt(krb5_context context, int maj_ver, int min_ver,542krb5_plugin_vtable vtable);543544krb5_error_code545kdcpreauth_spake_initvt(krb5_context context, int maj_ver, int min_ver,546krb5_plugin_vtable vtable)547{548krb5_kdcpreauth_vtable vt;549static krb5_preauthtype pa_types[] = { KRB5_PADATA_SPAKE, 0 };550551if (maj_ver != 1)552return KRB5_PLUGIN_VER_NOTSUPP;553vt = (krb5_kdcpreauth_vtable)vtable;554vt->name = "spake";555vt->pa_type_list = pa_types;556vt->init = spake_init;557vt->fini = spake_fini;558vt->edata = spake_edata;559vt->verify = spake_verify;560return 0;561}562563564