1 /* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 /* plugins/preauth/spake/spake_kdc.c - SPAKE kdcpreauth module */ 3 /* 4 * Copyright (C) 2015 by the Massachusetts Institute of Technology. 5 * All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * * Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 14 * * Redistributions in binary form must reproduce the above copyright 15 * notice, this list of conditions and the following disclaimer in 16 * the documentation and/or other materials provided with the 17 * distribution. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 22 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 23 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 27 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 28 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 30 * OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 #include "k5-int.h" 34 #include "k5-input.h" 35 #include "k5-spake.h" 36 37 #include "groups.h" 38 #include "trace.h" 39 #include "iana.h" 40 #include "util.h" 41 42 #include <krb5/kdcpreauth_plugin.h> 43 44 /* 45 * The SPAKE kdcpreauth module uses a secure cookie containing the following 46 * concatenated fields (all integer fields are big-endian): 47 * 48 * version (16-bit unsigned integer) 49 * stage (16-bit unsigned integer) 50 * group (32-bit signed integer) 51 * SPAKE value (32-bit unsigned length, followed by data) 52 * Transcript hash (32-bit unsigned length, followed by data) 53 * Zero or more instances of: 54 * second-factor number (32-bit signed integer) 55 * second-factor data (32-bit unsigned length, followed by data) 56 * 57 * The only currently supported version is 1. stage is 0 if the cookie was 58 * sent with a challenge message. stage is n>0 if the cookie was sent with an 59 * encdata message encrypted in K'[2n]. group indicates the group number used 60 * in the SPAKE challenge. The SPAKE value is the KDC private key for a 61 * stage-0 cookie, represented in the scalar marshalling form of the group; for 62 * other cookies, the SPAKE value is the SPAKE result K, represented in the 63 * group element marshalling form. The transcript hash is the intermediate 64 * hash after updating with the support and challenge messages for a stage-0 65 * cookie, or the final hash for other cookies. For a stage 0 cookie, there 66 * may be any number of second-factor records, including none (no record is 67 * generated for SF-NONE); for other cookies, there must be exactly one 68 * second-factor record corresponding to the factor type chosen by the client. 69 */ 70 71 /* From a k5input structure representing the remainder of a secure cookie 72 * plaintext, parse a four-byte length and data. */ 73 static void 74 parse_data(struct k5input *in, krb5_data *out) 75 { 76 out->length = k5_input_get_uint32_be(in); 77 out->data = (char *)k5_input_get_bytes(in, out->length); 78 out->magic = KV5M_DATA; 79 } 80 81 /* Parse a received cookie into its components. The pointers stored in the 82 * krb5_data outputs are aliases into cookie and should not be freed. */ 83 static krb5_error_code 84 parse_cookie(const krb5_data *cookie, int *stage_out, int32_t *group_out, 85 krb5_data *spake_out, krb5_data *thash_out, 86 krb5_data *factors_out) 87 { 88 struct k5input in; 89 int version, stage; 90 int32_t group; 91 krb5_data thash, spake, factors; 92 93 *spake_out = *thash_out = *factors_out = empty_data(); 94 k5_input_init(&in, cookie->data, cookie->length); 95 96 /* Parse and check the version, and read the other integer fields. */ 97 version = k5_input_get_uint16_be(&in); 98 if (version != 1) 99 return KRB5KDC_ERR_PREAUTH_FAILED; 100 stage = k5_input_get_uint16_be(&in); 101 group = k5_input_get_uint32_be(&in); 102 103 /* Parse the data fields. The factor data is anything remaining after the 104 * transcript hash. */ 105 parse_data(&in, &spake); 106 parse_data(&in, &thash); 107 if (in.status) 108 return in.status; 109 factors = make_data((char *)in.ptr, in.len); 110 111 *stage_out = stage; 112 *group_out = group; 113 *spake_out = spake; 114 *thash_out = thash; 115 *factors_out = factors; 116 return 0; 117 } 118 119 /* Marshal data into buf as a four-byte length followed by the contents. */ 120 static void 121 marshal_data(struct k5buf *buf, const krb5_data *data) 122 { 123 k5_buf_add_uint32_be(buf, data->length); 124 k5_buf_add_len(buf, data->data, data->length); 125 } 126 127 /* Marshal components into a cookie. */ 128 static krb5_error_code 129 make_cookie(int stage, int32_t group, const krb5_data *spake, 130 const krb5_data *thash, krb5_data *cookie_out) 131 { 132 struct k5buf buf; 133 134 *cookie_out = empty_data(); 135 k5_buf_init_dynamic_zap(&buf); 136 137 /* Marshal the version, stage, and group. */ 138 k5_buf_add_uint16_be(&buf, 1); 139 k5_buf_add_uint16_be(&buf, stage); 140 k5_buf_add_uint32_be(&buf, group); 141 142 /* Marshal the data fields. */ 143 marshal_data(&buf, spake); 144 marshal_data(&buf, thash); 145 146 /* When second factor support is implemented, we should add factor data 147 * here. */ 148 149 if (buf.data == NULL) 150 return ENOMEM; 151 *cookie_out = make_data(buf.data, buf.len); 152 return 0; 153 } 154 155 /* Add authentication indicators if any are configured for SPAKE. */ 156 static krb5_error_code 157 add_indicators(krb5_context context, const krb5_data *realm, 158 krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock) 159 { 160 krb5_error_code ret; 161 const char *keys[4]; 162 char *realmstr, **indicators, **ind; 163 164 realmstr = k5memdup0(realm->data, realm->length, &ret); 165 if (realmstr == NULL) 166 return ret; 167 keys[0] = KRB5_CONF_REALMS; 168 keys[1] = realmstr; 169 keys[2] = KRB5_CONF_SPAKE_PREAUTH_INDICATOR; 170 keys[3] = NULL; 171 ret = profile_get_values(context->profile, keys, &indicators); 172 free(realmstr); 173 if (ret == PROF_NO_RELATION) 174 return 0; 175 if (ret) 176 return ret; 177 178 for (ind = indicators; *ind != NULL && !ret; ind++) 179 ret = cb->add_auth_indicator(context, rock, *ind); 180 181 profile_free_list(indicators); 182 return ret; 183 } 184 185 /* Initialize a SPAKE module data object. */ 186 static krb5_error_code 187 spake_init(krb5_context context, krb5_kdcpreauth_moddata *moddata_out, 188 const char **realmnames) 189 { 190 krb5_error_code ret; 191 groupstate *gstate; 192 193 ret = group_init_state(context, TRUE, &gstate); 194 if (ret) 195 return ret; 196 *moddata_out = (krb5_kdcpreauth_moddata)gstate; 197 return 0; 198 } 199 200 /* Release a SPAKE module data object. */ 201 static void 202 spake_fini(krb5_context context, krb5_kdcpreauth_moddata moddata) 203 { 204 group_free_state((groupstate *)moddata); 205 } 206 207 /* 208 * Generate a SPAKE challenge message for the specified group. Use cb and rock 209 * to retrieve the initial reply key and to set a stage-0 cookie. Invoke 210 * either erespond or vrespond with the result. 211 */ 212 static void 213 send_challenge(krb5_context context, groupstate *gstate, int32_t group, 214 krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock, 215 const krb5_data *support, 216 krb5_kdcpreauth_edata_respond_fn erespond, 217 krb5_kdcpreauth_verify_respond_fn vrespond, void *arg) 218 { 219 krb5_error_code ret; 220 const krb5_keyblock *ikey; 221 krb5_pa_data **padata = NULL, *pa; 222 krb5_data kdcpriv = empty_data(), kdcpub = empty_data(), *der_msg = NULL; 223 krb5_data thash = empty_data(), cookie = empty_data(); 224 krb5_data wbytes = empty_data(); 225 krb5_spake_factor f, *flist[2]; 226 krb5_pa_spake msg; 227 228 ikey = cb->client_keyblock(context, rock); 229 if (ikey == NULL) { 230 ret = KRB5KDC_ERR_ETYPE_NOSUPP; 231 goto cleanup; 232 } 233 234 ret = derive_wbytes(context, group, ikey, &wbytes); 235 if (ret) 236 goto cleanup; 237 ret = group_keygen(context, gstate, group, &wbytes, &kdcpriv, &kdcpub); 238 if (ret) 239 goto cleanup; 240 241 /* Encode the challenge. When second factor support is implemented, we 242 * should construct a factor list instead of hardcoding SF-NONE. */ 243 f.type = SPAKE_SF_NONE; 244 f.data = NULL; 245 flist[0] = &f; 246 flist[1] = NULL; 247 msg.choice = SPAKE_MSGTYPE_CHALLENGE; 248 msg.u.challenge.group = group; 249 msg.u.challenge.pubkey = kdcpub; 250 msg.u.challenge.factors = flist; 251 ret = encode_krb5_pa_spake(&msg, &der_msg); 252 if (ret) 253 goto cleanup; 254 255 /* Initialize and update the transcript hash with the support message (if 256 * we received one) and challenge message. */ 257 ret = update_thash(context, gstate, group, &thash, support, der_msg); 258 if (ret) 259 goto cleanup; 260 261 /* Save the group, transcript hash, and private key in a stage-0 cookie. 262 * When second factor support is implemented, also save factor state. */ 263 ret = make_cookie(0, group, &kdcpriv, &thash, &cookie); 264 if (ret) 265 goto cleanup; 266 ret = cb->set_cookie(context, rock, KRB5_PADATA_SPAKE, &cookie); 267 if (ret) 268 goto cleanup; 269 270 ret = convert_to_padata(der_msg, &padata); 271 der_msg = NULL; 272 TRACE_SPAKE_SEND_CHALLENGE(context, group); 273 274 cleanup: 275 zapfree(wbytes.data, wbytes.length); 276 zapfree(kdcpriv.data, kdcpriv.length); 277 zapfree(cookie.data, cookie.length); 278 krb5_free_data_contents(context, &kdcpub); 279 krb5_free_data_contents(context, &thash); 280 krb5_free_data(context, der_msg); 281 282 if (erespond != NULL) { 283 assert(vrespond == NULL); 284 /* Grab the first pa-data element from the list, if we made one. */ 285 pa = (padata == NULL) ? NULL : padata[0]; 286 free(padata); 287 (*erespond)(arg, ret, pa); 288 } else { 289 assert(vrespond != NULL); 290 if (!ret) 291 ret = KRB5KDC_ERR_MORE_PREAUTH_DATA_REQUIRED; 292 (*vrespond)(arg, ret, NULL, padata, NULL); 293 } 294 } 295 296 /* Generate the METHOD-DATA entry indicating support for SPAKE. Include an 297 * optimistic challenge if configured to do so. */ 298 static void 299 spake_edata(krb5_context context, krb5_kdc_req *req, 300 krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock, 301 krb5_kdcpreauth_moddata moddata, krb5_preauthtype pa_type, 302 krb5_kdcpreauth_edata_respond_fn respond, void *arg) 303 { 304 const krb5_keyblock *ikey; 305 groupstate *gstate = (groupstate *)moddata; 306 krb5_data empty = empty_data(); 307 int32_t group; 308 309 /* SPAKE requires a client key, which cannot be a single-DES key. */ 310 ikey = cb->client_keyblock(context, rock); 311 if (ikey == NULL) { 312 (*respond)(arg, KRB5KDC_ERR_ETYPE_NOSUPP, NULL); 313 return; 314 } 315 316 group = group_optimistic_challenge(gstate); 317 if (group) { 318 send_challenge(context, gstate, group, cb, rock, &empty, respond, NULL, 319 arg); 320 } else { 321 /* No optimistic challenge configured; send an empty pa-data value. */ 322 (*respond)(arg, 0, NULL); 323 } 324 } 325 326 /* Choose a group from the client's support message and generate a 327 * challenge. */ 328 static void 329 verify_support(krb5_context context, groupstate *gstate, 330 krb5_spake_support *support, const krb5_data *der_msg, 331 krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock, 332 krb5_kdcpreauth_verify_respond_fn respond, void *arg) 333 { 334 krb5_error_code ret; 335 int32_t i, group; 336 337 for (i = 0; i < support->ngroups; i++) { 338 if (group_is_permitted(gstate, support->groups[i])) 339 break; 340 } 341 if (i == support->ngroups) { 342 TRACE_SPAKE_REJECT_SUPPORT(context); 343 ret = KRB5KDC_ERR_PREAUTH_FAILED; 344 goto error; 345 } 346 group = support->groups[i]; 347 TRACE_SPAKE_RECEIVE_SUPPORT(context, group); 348 349 send_challenge(context, gstate, group, cb, rock, der_msg, NULL, respond, 350 arg); 351 return; 352 353 error: 354 (*respond)(arg, ret, NULL, NULL, NULL); 355 } 356 357 /* 358 * From the client's response message, compute the SPAKE result and decrypt the 359 * factor reply. On success, either mark the reply as pre-authenticated and 360 * set a reply key in the pre-request module data, or generate an additional 361 * factor challenge and ask for another round of pre-authentication. 362 */ 363 static void 364 verify_response(krb5_context context, groupstate *gstate, 365 krb5_spake_response *resp, const krb5_data *realm, 366 krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock, 367 krb5_enc_tkt_part *enc_tkt_reply, 368 krb5_kdcpreauth_verify_respond_fn respond, void *arg) 369 { 370 krb5_error_code ret; 371 const krb5_keyblock *ikey; 372 krb5_keyblock *k1 = NULL, *reply_key = NULL; 373 krb5_data cookie, thash_in, kdcpriv, factors, *der_req; 374 krb5_data thash = empty_data(), der_factor = empty_data(); 375 krb5_data wbytes = empty_data(), spakeresult = empty_data(); 376 krb5_spake_factor *factor = NULL; 377 int stage; 378 int32_t group; 379 380 ikey = cb->client_keyblock(context, rock); 381 if (ikey == NULL) { 382 ret = KRB5KDC_ERR_ETYPE_NOSUPP; 383 goto cleanup; 384 } 385 386 /* Fetch the stage-0 cookie and parse it. (All of the krb5_data results 387 * are aliases into memory owned by rock). */ 388 if (!cb->get_cookie(context, rock, KRB5_PADATA_SPAKE, &cookie)) { 389 ret = KRB5KDC_ERR_PREAUTH_FAILED; 390 goto cleanup; 391 } 392 ret = parse_cookie(&cookie, &stage, &group, &kdcpriv, &thash_in, &factors); 393 if (ret) 394 goto cleanup; 395 if (stage != 0) { 396 /* The received cookie wasn't sent with a challenge. */ 397 ret = KRB5KDC_ERR_PREAUTH_FAILED; 398 goto cleanup; 399 } 400 TRACE_SPAKE_RECEIVE_RESPONSE(context, &resp->pubkey); 401 402 /* Update the transcript hash with the client public key. */ 403 ret = krb5int_copy_data_contents(context, &thash_in, &thash); 404 if (ret) 405 goto cleanup; 406 ret = update_thash(context, gstate, group, &thash, &resp->pubkey, NULL); 407 if (ret) 408 goto cleanup; 409 TRACE_SPAKE_KDC_THASH(context, &thash); 410 411 ret = derive_wbytes(context, group, ikey, &wbytes); 412 if (ret) 413 goto cleanup; 414 ret = group_result(context, gstate, group, &wbytes, &kdcpriv, 415 &resp->pubkey, &spakeresult); 416 if (ret) 417 goto cleanup; 418 419 /* Decrypt the response factor field using K'[1]. If the decryption 420 * integrity check fails, the client probably used the wrong password. */ 421 der_req = cb->request_body(context, rock); 422 ret = derive_key(context, gstate, group, ikey, &wbytes, &spakeresult, 423 &thash, der_req, 1, &k1); 424 if (ret) 425 goto cleanup; 426 ret = alloc_data(&der_factor, resp->factor.ciphertext.length); 427 if (ret) 428 goto cleanup; 429 ret = krb5_c_decrypt(context, k1, KRB5_KEYUSAGE_SPAKE, NULL, &resp->factor, 430 &der_factor); 431 if (ret == KRB5KRB_AP_ERR_BAD_INTEGRITY) 432 ret = KRB5KDC_ERR_PREAUTH_FAILED; 433 if (ret) 434 goto cleanup; 435 ret = decode_krb5_spake_factor(&der_factor, &factor); 436 if (ret) 437 goto cleanup; 438 439 /* 440 * When second factor support is implemented, we should verify the factor 441 * data here, and possibly generate an encdata message for another hop. 442 * This function may need to be split at this point to allow for 443 * asynchronous verification of the second-factor value. We might also 444 * need to collect authentication indicators from the second-factor module; 445 * alternatively the module could have access to cb and rock so that it can 446 * add indicators itself. 447 */ 448 if (factor->type != SPAKE_SF_NONE) { 449 ret = KRB5KDC_ERR_PREAUTH_FAILED; 450 goto cleanup; 451 } 452 453 ret = add_indicators(context, realm, cb, rock); 454 if (ret) 455 goto cleanup; 456 457 enc_tkt_reply->flags |= TKT_FLG_PRE_AUTH; 458 459 ret = derive_key(context, gstate, group, ikey, &wbytes, &spakeresult, 460 &thash, der_req, 0, &reply_key); 461 if (ret) 462 goto cleanup; 463 464 ret = cb->replace_reply_key(context, rock, reply_key, TRUE); 465 466 cleanup: 467 zapfree(wbytes.data, wbytes.length); 468 zapfree(der_factor.data, der_factor.length); 469 zapfree(spakeresult.data, spakeresult.length); 470 krb5_free_data_contents(context, &thash); 471 krb5_free_keyblock(context, k1); 472 krb5_free_keyblock(context, reply_key); 473 k5_free_spake_factor(context, factor); 474 (*respond)(arg, ret, NULL, NULL, NULL); 475 } 476 477 /* 478 * Decrypt and validate an additional second-factor reply. On success, either 479 * mark the reply as pre-authenticated and set a reply key in the pre-request 480 * module data, or generate an additional factor challenge and ask for another 481 * round of pre-authentication. 482 */ 483 static void 484 verify_encdata(krb5_context context, krb5_enc_data *enc, 485 krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock, 486 krb5_enc_tkt_part *enc_tkt_reply, 487 krb5_kdcpreauth_verify_respond_fn respond, void *arg) 488 { 489 /* 490 * When second factor support is implemented, we should process encdata 491 * message according to the factor type recorded in the cookie. If the 492 * second factor exchange finishes successfully, we should set 493 * TKT_FLG_PRE_AUTH, set the reply key to K'[0], and add any auth 494 * indicators from configuration (with a call to add_indicators()) or the 495 * second factor module (unless the module has access to cb and rock and 496 * can add indicators itself). 497 */ 498 (*respond)(arg, KRB5KDC_ERR_PREAUTH_FAILED, NULL, NULL, NULL); 499 } 500 501 /* 502 * Respond to a client padata message, either by generating a SPAKE challenge, 503 * generating an additional second-factor challenge, or marking the reply as 504 * pre-authenticated and setting an additional reply key in the pre-request 505 * module data. 506 */ 507 static void 508 spake_verify(krb5_context context, krb5_data *req_pkt, krb5_kdc_req *request, 509 krb5_enc_tkt_part *enc_tkt_reply, krb5_pa_data *data, 510 krb5_kdcpreauth_callbacks cb, krb5_kdcpreauth_rock rock, 511 krb5_kdcpreauth_moddata moddata, 512 krb5_kdcpreauth_verify_respond_fn respond, void *arg) 513 { 514 krb5_error_code ret; 515 krb5_pa_spake *pa_spake = NULL; 516 krb5_data in_data = make_data(data->contents, data->length); 517 groupstate *gstate = (groupstate *)moddata; 518 519 ret = decode_krb5_pa_spake(&in_data, &pa_spake); 520 if (ret) { 521 (*respond)(arg, ret, NULL, NULL, NULL); 522 } else if (pa_spake->choice == SPAKE_MSGTYPE_SUPPORT) { 523 verify_support(context, gstate, &pa_spake->u.support, &in_data, cb, 524 rock, respond, arg); 525 } else if (pa_spake->choice == SPAKE_MSGTYPE_RESPONSE) { 526 verify_response(context, gstate, &pa_spake->u.response, 527 &request->server->realm, cb, rock, enc_tkt_reply, 528 respond, arg); 529 } else if (pa_spake->choice == SPAKE_MSGTYPE_ENCDATA) { 530 verify_encdata(context, &pa_spake->u.encdata, cb, rock, enc_tkt_reply, 531 respond, arg); 532 } else { 533 ret = KRB5KDC_ERR_PREAUTH_FAILED; 534 k5_setmsg(context, ret, _("Unknown SPAKE request type")); 535 (*respond)(arg, ret, NULL, NULL, NULL); 536 } 537 538 k5_free_pa_spake(context, pa_spake); 539 } 540 541 krb5_error_code 542 kdcpreauth_spake_initvt(krb5_context context, int maj_ver, int min_ver, 543 krb5_plugin_vtable vtable); 544 545 krb5_error_code 546 kdcpreauth_spake_initvt(krb5_context context, int maj_ver, int min_ver, 547 krb5_plugin_vtable vtable) 548 { 549 krb5_kdcpreauth_vtable vt; 550 static krb5_preauthtype pa_types[] = { KRB5_PADATA_SPAKE, 0 }; 551 552 if (maj_ver != 1) 553 return KRB5_PLUGIN_VER_NOTSUPP; 554 vt = (krb5_kdcpreauth_vtable)vtable; 555 vt->name = "spake"; 556 vt->pa_type_list = pa_types; 557 vt->init = spake_init; 558 vt->fini = spake_fini; 559 vt->edata = spake_edata; 560 vt->verify = spake_verify; 561 return 0; 562 } 563