krb5: import Heimdal-597.121.1 AS/TGS client

Sync with most changes in AS/TGS client from Apple's Heimdal-597.121.1
(opensource.apple.com).

Changes include:

 - FAST support in TGS client
 - Refactored pre-auth client to be more easily extensible
 - Pin KDC host and AD site name in API calls

Note the completely refactored TGS client loop is not imported as that was
considered too intrusive.
This commit is contained in:
Luke Howard
2021-08-09 19:32:21 +10:00
parent b5a58df8eb
commit 47282cae34
15 changed files with 3028 additions and 1351 deletions

View File

@@ -32,7 +32,9 @@
*/
#include "krb5_locl.h"
#ifndef WIN32
#include <heim-ipc.h>
#endif
KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
_krb5_fast_cf2(krb5_context context,
@@ -93,3 +95,772 @@ _krb5_fast_armor_key(krb5_context context,
armorkey,
armor_crypto);
}
static krb5_error_code
check_fast(krb5_context context, struct krb5_fast_state *state)
{
if (state && (state->flags & KRB5_FAST_EXPECTED)) {
krb5_set_error_message(context, KRB5KRB_AP_ERR_MODIFIED,
"Expected FAST, but no FAST "
"was in the response from the KDC");
return KRB5KRB_AP_ERR_MODIFIED;
}
return 0;
}
static krb5_error_code
make_local_fast_ap_fxarmor(krb5_context context,
krb5_ccache armor_ccache,
krb5_const_realm realm,
krb5_data *armor_value,
krb5_keyblock *armor_key,
krb5_crypto *armor_crypto)
{
krb5_auth_context auth_context = NULL;
krb5_creds cred, *credp = NULL;
krb5_error_code ret;
krb5_data empty;
krb5_const_realm tgs_realm;
krb5_data_zero(&empty);
memset(&cred, 0, sizeof(cred));
ret = krb5_auth_con_init (context, &auth_context);
if (ret)
goto out;
ret = krb5_cc_get_principal(context, armor_ccache, &cred.client);
if (ret)
goto out;
/*
* Make sure we don't ask for a krbtgt/WELLKNOWN:ANONYMOUS
*/
if (krb5_principal_is_anonymous(context, cred.client,
KRB5_ANON_MATCH_UNAUTHENTICATED))
tgs_realm = realm;
else
tgs_realm = cred.client->realm;
ret = krb5_make_principal(context, &cred.server,
tgs_realm,
KRB5_TGS_NAME,
tgs_realm,
NULL);
if (ret)
goto out;
ret = krb5_get_credentials(context, 0, armor_ccache, &cred, &credp);
if (ret)
goto out;
ret = krb5_auth_con_add_AuthorizationData(context, auth_context,
KRB5_AUTHDATA_FX_FAST_ARMOR,
&empty);
if (ret)
goto out;
ret = krb5_mk_req_extended(context,
&auth_context,
AP_OPTS_USE_SUBKEY,
NULL,
credp,
armor_value);
if (ret)
goto out;
ret = _krb5_fast_armor_key(context,
auth_context->local_subkey,
auth_context->keyblock,
armor_key,
armor_crypto);
if (ret)
goto out;
out:
if (auth_context)
krb5_auth_con_free(context, auth_context);
if (credp)
krb5_free_creds(context, credp);
krb5_free_principal(context, cred.server);
krb5_free_principal(context, cred.client);
return ret;
}
#ifndef WIN32
static heim_base_once_t armor_service_once = HEIM_BASE_ONCE_INIT;
static heim_ipc armor_service = NULL;
static void
fast_armor_init_ipc(void *ctx)
{
heim_ipc *ipc = ctx;
heim_ipc_init_context("ANY:org.h5l.armor-service", ipc);
}
#endif
static krb5_error_code
make_fast_ap_fxarmor(krb5_context context,
struct krb5_fast_state *state,
krb5_const_realm realm,
KrbFastArmor **armor)
{
KrbFastArmor *fxarmor = NULL;
krb5_error_code ret;
ALLOC(fxarmor, 1);
if (fxarmor == NULL) {
ret = ENOMEM;
goto out;
}
if (state->flags & KRB5_FAST_AP_ARMOR_SERVICE) {
#ifdef WIN32
krb5_set_error_message(context, ENOTSUP, "Fast armor IPC service not supportted yet on Windows");
ret = ENOTSUP;
goto out;
#else
KERB_ARMOR_SERVICE_REPLY msg;
krb5_data request, reply;
heim_base_once_f(&armor_service_once, &armor_service, fast_armor_init_ipc);
if (armor_service == NULL) {
krb5_set_error_message(context, ENOENT, "Failed to open fast armor service");
ret = ENOENT;
goto out;
}
krb5_data_zero(&reply);
request.data = rk_UNCONST(realm);
request.length = strlen(realm);
ret = heim_ipc_call(armor_service, &request, &reply, NULL);
if (ret) {
krb5_set_error_message(context, ret, "Failed to get armor service credential");
goto out;
}
ret = decode_KERB_ARMOR_SERVICE_REPLY(reply.data, reply.length, &msg, NULL);
krb5_data_free(&reply);
if (ret)
goto out;
ret = copy_KrbFastArmor(fxarmor, &msg.armor);
if (ret) {
free_KERB_ARMOR_SERVICE_REPLY(&msg);
goto out;
}
ret = krb5_copy_keyblock_contents(context, &msg.armor_key, &state->armor_key);
free_KERB_ARMOR_SERVICE_REPLY(&msg);
if (ret)
goto out;
ret = krb5_crypto_init(context, &state->armor_key, 0, &state->armor_crypto);
if (ret)
goto out;
#endif /* WIN32 */
} else {
fxarmor->armor_type = 1;
ret = make_local_fast_ap_fxarmor(context,
state->armor_ccache,
realm,
&fxarmor->armor_value,
&state->armor_key,
&state->armor_crypto);
if (ret)
goto out;
}
*armor = fxarmor;
fxarmor = NULL;
out:
if (fxarmor) {
free_KrbFastArmor(fxarmor);
free(fxarmor);
}
return ret;
}
static krb5_error_code
unwrap_fast_rep(krb5_context context,
struct krb5_fast_state *state,
PA_DATA *pa,
KrbFastResponse *fastrep)
{
PA_FX_FAST_REPLY fxfastrep;
krb5_error_code ret;
memset(&fxfastrep, 0, sizeof(fxfastrep));
ret = decode_PA_FX_FAST_REPLY(pa->padata_value.data,
pa->padata_value.length,
&fxfastrep, NULL);
if (ret)
return ret;
if (fxfastrep.element == choice_PA_FX_FAST_REPLY_armored_data) {
krb5_data data;
ret = krb5_decrypt_EncryptedData(context,
state->armor_crypto,
KRB5_KU_FAST_REP,
&fxfastrep.u.armored_data.enc_fast_rep,
&data);
if (ret)
goto out;
ret = decode_KrbFastResponse(data.data, data.length, fastrep, NULL);
krb5_data_free(&data);
if (ret)
goto out;
} else {
ret = KRB5KDC_ERR_PREAUTH_FAILED;
goto out;
}
out:
free_PA_FX_FAST_REPLY(&fxfastrep);
return ret;
}
static krb5_error_code
set_anon_principal(krb5_context context, PrincipalName **p)
{
ALLOC((*p), 1);
if (*p == NULL)
goto fail;
(*p)->name_type = KRB5_NT_PRINCIPAL;
ALLOC_SEQ(&(*p)->name_string, 2);
if ((*p)->name_string.val == NULL)
goto fail;
(*p)->name_string.val[0] = strdup(KRB5_WELLKNOWN_NAME);
if ((*p)->name_string.val[0] == NULL)
goto fail;
(*p)->name_string.val[1] = strdup(KRB5_ANON_NAME);
if ((*p)->name_string.val[1] == NULL)
goto fail;
return 0;
fail:
if (*p) {
if ((*p)->name_string.val) {
free((*p)->name_string.val[0]);
free((*p)->name_string.val[1]);
free((*p)->name_string.val);
}
free(*p);
}
return krb5_enomem(context);
}
krb5_error_code
_krb5_fast_create_armor(krb5_context context,
struct krb5_fast_state *state,
const char *realm)
{
krb5_error_code ret;
if (state->armor_crypto == NULL) {
if (state->armor_ccache || state->armor_ac || (state->flags & KRB5_FAST_AP_ARMOR_SERVICE)) {
/*
* Instead of keeping state in FX_COOKIE in the KDC, we
* rebuild a new armor key for every request, because this
* is what the MIT KDC expect and RFC6113 is vage about
* what the behavior should be.
*/
state->type = choice_PA_FX_FAST_REQUEST_armored_data;
} else {
return check_fast(context, state);
}
}
if (state->type == choice_PA_FX_FAST_REQUEST_armored_data) {
if (state->armor_crypto)
krb5_crypto_destroy(context, state->armor_crypto);
krb5_free_keyblock_contents(context, &state->armor_key);
/*
* If we have a armor auth context, its because the caller
* wants us to do an implicit FAST armor (TGS-REQ).
*/
if (state->armor_ac) {
heim_assert((state->flags & KRB5_FAST_AS_REQ) == 0, "FAST AS with AC");
ret = _krb5_fast_armor_key(context,
state->armor_ac->local_subkey,
state->armor_ac->keyblock,
&state->armor_key,
&state->armor_crypto);
if (ret)
goto out;
} else {
heim_assert((state->flags & KRB5_FAST_AS_REQ) != 0, "FAST TGS without AC");
if (state->armor_data) {
free_KrbFastArmor(state->armor_data);
free(state->armor_data);
}
ret = make_fast_ap_fxarmor(context, state, realm,
&state->armor_data);
if (ret)
goto out;
}
} else {
heim_abort("unknown state type: %d", (int)state->type);
}
out:
return ret;
}
krb5_error_code
_krb5_fast_wrap_req(krb5_context context,
struct krb5_fast_state *state,
krb5_data *checksum_data,
KDC_REQ *req)
{
PA_FX_FAST_REQUEST fxreq;
krb5_error_code ret;
KrbFastReq fastreq;
krb5_data data, aschecksum_data;
size_t size = 0;
if (state->flags & KRB5_FAST_DISABLED) {
_krb5_debug(context, 10, "fast disabled, not doing any fast wrapping");
return 0;
}
memset(&fxreq, 0, sizeof(fxreq));
memset(&fastreq, 0, sizeof(fastreq));
krb5_data_zero(&data);
krb5_data_zero(&aschecksum_data);
if (state->armor_crypto == NULL)
return check_fast(context, state);
state->flags |= KRB5_FAST_EXPECTED;
fastreq.fast_options.hide_client_names = 1;
ret = copy_KDC_REQ_BODY(&req->req_body, &fastreq.req_body);
if (ret)
goto out;
/*
* In the case of a AS-REQ, remove all account names. Want to this
* for TGS-REQ too, but due to layering this is tricky.
*
* 1. TGS-REQ need checksum of REQ-BODY
* 2. FAST needs checksum of TGS-REQ, so, FAST needs to happen after TGS-REQ
* 3. FAST privacy mangaling needs to happen before TGS-REQ does the checksum in 1.
*
* So lets not modify the bits for now for TGS-REQ
*/
if (state->flags & KRB5_FAST_AS_REQ) {
free_KDC_REQ_BODY(&req->req_body);
req->req_body.realm = strdup(KRB5_ANON_REALM);
if (req->req_body.realm == NULL) {
ret = krb5_enomem(context);
goto out;
}
ret = set_anon_principal(context, &req->req_body.cname);
if (ret)
goto out;
ALLOC(req->req_body.till, 1);
*req->req_body.till = 0;
heim_assert(checksum_data == NULL, "checksum data not NULL");
ASN1_MALLOC_ENCODE(KDC_REQ_BODY,
aschecksum_data.data,
aschecksum_data.length,
&req->req_body,
&size, ret);
if (ret)
goto out;
heim_assert(aschecksum_data.length == size, "ASN.1 internal error");
checksum_data = &aschecksum_data;
}
if (req->padata) {
ret = copy_METHOD_DATA(req->padata, &fastreq.padata);
free_METHOD_DATA(req->padata);
if (ret)
goto out;
} else {
ALLOC(req->padata, 1);
if (req->padata == NULL) {
ret = krb5_enomem(context);
goto out;
}
}
ASN1_MALLOC_ENCODE(KrbFastReq, data.data, data.length, &fastreq, &size, ret);
if (ret)
goto out;
heim_assert(data.length == size, "ASN.1 internal error");
fxreq.element = state->type;
if (state->type == choice_PA_FX_FAST_REQUEST_armored_data) {
fxreq.u.armored_data.armor = state->armor_data;
state->armor_data = NULL;
if (ret)
goto out;
heim_assert(state->armor_crypto != NULL,
"FAST armor key missing when FAST started");
ret = krb5_create_checksum(context, state->armor_crypto,
KRB5_KU_FAST_REQ_CHKSUM, 0,
checksum_data->data,
checksum_data->length,
&fxreq.u.armored_data.req_checksum);
if (ret)
goto out;
ret = krb5_encrypt_EncryptedData(context, state->armor_crypto,
KRB5_KU_FAST_ENC,
data.data,
data.length,
0,
&fxreq.u.armored_data.enc_fast_req);
krb5_data_free(&data);
if (ret)
goto out;
} else {
krb5_data_free(&data);
heim_assert(false, "unknown FAST type, internal error");
}
ASN1_MALLOC_ENCODE(PA_FX_FAST_REQUEST, data.data, data.length, &fxreq, &size, ret);
if (ret)
goto out;
heim_assert(data.length == size, "ASN.1 internal error");
ret = krb5_padata_add(context, req->padata, KRB5_PADATA_FX_FAST, data.data, data.length);
if (ret)
goto out;
krb5_data_zero(&data);
out:
free_KrbFastReq(&fastreq);
free_PA_FX_FAST_REQUEST(&fxreq);
krb5_data_free(&data);
krb5_data_free(&aschecksum_data);
return ret;
}
krb5_error_code
_krb5_fast_unwrap_error(krb5_context context,
int32_t nonce,
struct krb5_fast_state *state,
METHOD_DATA *md,
KRB_ERROR *error)
{
KrbFastResponse fastrep;
krb5_error_code ret;
PA_DATA *pa;
int idx;
if (state->armor_crypto == NULL)
return check_fast(context, state);
memset(&fastrep, 0, sizeof(fastrep));
if (error->error_code != KRB5_KDC_ERR_MORE_PREAUTH_DATA_REQUIRED)
_krb5_debug(context, 10, "using FAST without FAST outer error code");
idx = 0;
pa = krb5_find_padata(md->val, md->len, KRB5_PADATA_FX_FAST, &idx);
if (pa == NULL) {
ret = KRB5_KDCREP_MODIFIED;
krb5_set_error_message(context, ret,
N_("FAST fast response is missing FX-FAST", ""));
goto out;
}
ret = unwrap_fast_rep(context, state, pa, &fastrep);
if (ret)
goto out;
if (fastrep.strengthen_key || nonce != (int32_t)fastrep.nonce) {
ret = KRB5KDC_ERR_PREAUTH_FAILED;
goto out;
}
idx = 0;
pa = krb5_find_padata(fastrep.padata.val, fastrep.padata.len, KRB5_PADATA_FX_ERROR, &idx);
if (pa == NULL) {
ret = KRB5_KDCREP_MODIFIED;
krb5_set_error_message(context, ret, N_("No wrapped error", ""));
goto out;
}
free_KRB_ERROR(error);
ret = krb5_rd_error(context, &pa->padata_value, error);
if (ret)
goto out;
if (error->e_data)
_krb5_debug(context, 10, "FAST wrapped KBB_ERROR contained e_data: %d",
(int)error->e_data->length);
free_METHOD_DATA(md);
md->val = fastrep.padata.val;
md->len = fastrep.padata.len;
fastrep.padata.val = NULL;
fastrep.padata.len = 0;
out:
free_KrbFastResponse(&fastrep);
return ret;
}
krb5_error_code
_krb5_fast_unwrap_kdc_rep(krb5_context context, int32_t nonce,
krb5_data *chksumdata,
struct krb5_fast_state *state, AS_REP *rep)
{
KrbFastResponse fastrep;
krb5_error_code ret;
PA_DATA *pa = NULL;
int idx = 0;
if (state == NULL || state->armor_crypto == NULL || rep->padata == NULL)
return check_fast(context, state);
/* find PA_FX_FAST_REPLY */
pa = krb5_find_padata(rep->padata->val, rep->padata->len,
KRB5_PADATA_FX_FAST, &idx);
if (pa == NULL)
return check_fast(context, state);
memset(&fastrep, 0, sizeof(fastrep));
ret = unwrap_fast_rep(context, state, pa, &fastrep);
if (ret)
goto out;
free_METHOD_DATA(rep->padata);
ret = copy_METHOD_DATA(&fastrep.padata, rep->padata);
if (ret)
goto out;
if (fastrep.strengthen_key) {
if (state->strengthen_key)
krb5_free_keyblock(context, state->strengthen_key);
ret = krb5_copy_keyblock(context, fastrep.strengthen_key, &state->strengthen_key);
if (ret)
goto out;
}
if (nonce != (int32_t)fastrep.nonce) {
ret = KRB5KDC_ERR_PREAUTH_FAILED;
goto out;
}
if (fastrep.finished) {
PrincipalName cname;
krb5_realm crealm = NULL;
if (chksumdata == NULL) {
ret = KRB5KDC_ERR_PREAUTH_FAILED;
goto out;
}
ret = krb5_verify_checksum(context, state->armor_crypto,
KRB5_KU_FAST_FINISHED,
chksumdata->data, chksumdata->length,
&fastrep.finished->ticket_checksum);
if (ret)
goto out;
/* update */
ret = copy_Realm(&fastrep.finished->crealm, &crealm);
if (ret)
goto out;
free_Realm(&rep->crealm);
rep->crealm = crealm;
ret = copy_PrincipalName(&fastrep.finished->cname, &cname);
if (ret)
goto out;
free_PrincipalName(&rep->cname);
rep->cname = cname;
} else if (chksumdata) {
/* expected fastrep.finish but didn't get it */
ret = KRB5KDC_ERR_PREAUTH_FAILED;
}
out:
free_KrbFastResponse(&fastrep);
return ret;
}
void
_krb5_fast_free(krb5_context context, struct krb5_fast_state *state)
{
if (state->armor_ccache) {
if (state->flags & KRB5_FAST_ANON_PKINIT_ARMOR)
krb5_cc_destroy(context, state->armor_ccache);
else
krb5_cc_close(context, state->armor_ccache);
}
if (state->armor_service)
krb5_free_principal(context, state->armor_service);
if (state->armor_crypto)
krb5_crypto_destroy(context, state->armor_crypto);
if (state->strengthen_key)
krb5_free_keyblock(context, state->strengthen_key);
krb5_free_keyblock_contents(context, &state->armor_key);
if (state->armor_data) {
free_KrbFastArmor(state->armor_data);
free(state->armor_data);
}
if (state->anon_pkinit_ctx)
krb5_init_creds_free(context, state->anon_pkinit_ctx);
if (state->anon_pkinit_opt)
krb5_get_init_creds_opt_free(context, state->anon_pkinit_opt);
memset(state, 0, sizeof(*state));
}
krb5_error_code
_krb5_fast_anon_pkinit_step(krb5_context context,
krb5_init_creds_context ctx,
struct krb5_fast_state *state,
krb5_data *in,
krb5_data *out,
krb5_krbhst_info *hostinfo,
unsigned int *flags)
{
krb5_error_code ret;
krb5_const_realm realm = _krb5_init_creds_get_cred_client(context, ctx)->realm;
krb5_init_creds_context anon_pk_ctx;
krb5_principal principal = NULL, anon_pk_client;
krb5_ccache ccache = NULL;
krb5_creds cred;
krb5_data data = { 3, rk_UNCONST("yes") };
memset(&cred, 0, sizeof(cred));
if (state->anon_pkinit_opt == NULL) {
ret = krb5_get_init_creds_opt_alloc(context, &state->anon_pkinit_opt);
if (ret)
goto out;
krb5_get_init_creds_opt_set_tkt_life(state->anon_pkinit_opt, 60);
krb5_get_init_creds_opt_set_anonymous(state->anon_pkinit_opt, TRUE);
ret = krb5_make_principal(context, &principal, realm,
KRB5_WELLKNOWN_NAME, KRB5_ANON_NAME, NULL);
if (ret)
return ret;
ret = krb5_get_init_creds_opt_set_pkinit(context,
state->anon_pkinit_opt,
principal,
NULL, NULL, NULL, NULL,
KRB5_GIC_OPT_PKINIT_ANONYMOUS |
KRB5_GIC_OPT_PKINIT_NO_KDC_ANCHOR,
NULL, NULL, NULL);
if (ret)
goto out;
ret = krb5_init_creds_init(context, principal, NULL, NULL,
_krb5_init_creds_get_cred_starttime(context, ctx),
state->anon_pkinit_opt,
&state->anon_pkinit_ctx);
if (ret)
goto out;
}
anon_pk_ctx = state->anon_pkinit_ctx;
ret = krb5_init_creds_step(context, anon_pk_ctx, in, out, hostinfo, flags);
if (ret ||
(*flags & KRB5_INIT_CREDS_STEP_FLAG_CONTINUE))
goto out;
ret = krb5_process_last_request(context, state->anon_pkinit_opt, anon_pk_ctx);
if (ret)
goto out;
ret = krb5_cc_new_unique(context, "MEMORY", NULL, &ccache);
if (ret)
goto out;
ret = krb5_init_creds_get_creds(context, anon_pk_ctx, &cred);
if (ret)
goto out;
if (!cred.flags.b.enc_pa_rep) {
ret = KRB5KDC_ERR_BADOPTION; /* KDC does not support FAST */
goto out;
}
anon_pk_client = _krb5_init_creds_get_cred_client(context, anon_pk_ctx);
ret = krb5_cc_initialize(context, ccache, anon_pk_client);
if (ret)
goto out;
ret = krb5_cc_store_cred(context, ccache, &cred);
if (ret)
goto out;
ret = krb5_cc_set_config(context, ccache, cred.server,
"fast_avail", &data);
if (ret)
return ret;
if (_krb5_pk_is_kdc_verified(context, state->anon_pkinit_opt))
state->flags |= KRB5_FAST_KDC_VERIFIED;
else
state->flags &= ~(KRB5_FAST_KDC_VERIFIED);
state->armor_ccache = ccache;
ccache = NULL;
krb5_init_creds_free(context, state->anon_pkinit_ctx);
state->anon_pkinit_ctx = NULL;
krb5_get_init_creds_opt_free(context, state->anon_pkinit_opt);
state->anon_pkinit_opt = NULL;
*flags |= KRB5_INIT_CREDS_STEP_FLAG_CONTINUE;
out:
krb5_free_principal(context, principal);
krb5_free_cred_contents(context, &cred);
if (ccache)
krb5_cc_destroy(context, ccache);
return ret;
}