/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ /* kdc/tgs_policy.c */ /* * Copyright (C) 2012 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 "kdc_util.h" /* * Routines that validate a TGS request; checks a lot of things. :-) * * Returns a Kerberos protocol error number, which is _not_ the same * as a com_err error number! */ struct tgsflagrule { krb5_flags reqflags; /* Flag(s) in TGS-REQ */ krb5_flags checkflag; /* Flags to check against */ char *status; /* Status string */ int err; /* Protocol error code */ }; /* Service principal TGS policy checking functions */ typedef int (check_tgs_svc_pol_fn)(krb5_kdc_req *, krb5_db_entry *, krb5_ticket *, krb5_timestamp, const char **); static check_tgs_svc_pol_fn check_tgs_svc_deny_opts; static check_tgs_svc_pol_fn check_tgs_svc_deny_all; static check_tgs_svc_pol_fn check_tgs_svc_reqd_flags; static check_tgs_svc_pol_fn check_tgs_svc_time; static check_tgs_svc_pol_fn * const svc_pol_fns[] = { check_tgs_svc_deny_opts, check_tgs_svc_deny_all, check_tgs_svc_reqd_flags, check_tgs_svc_time }; static const struct tgsflagrule tgsflagrules[] = { { KDC_OPT_FORWARDED, TKT_FLG_FORWARDABLE, "TGT NOT FORWARDABLE", KDC_ERR_BADOPTION }, { KDC_OPT_PROXY, TKT_FLG_PROXIABLE, "TGT NOT PROXIABLE", KDC_ERR_BADOPTION }, { (KDC_OPT_ALLOW_POSTDATE | KDC_OPT_POSTDATED), TKT_FLG_MAY_POSTDATE, "TGT NOT POSTDATABLE", KDC_ERR_BADOPTION }, { KDC_OPT_VALIDATE, TKT_FLG_INVALID, "VALIDATE VALID TICKET", KDC_ERR_BADOPTION }, { KDC_OPT_RENEW, TKT_FLG_RENEWABLE, "TICKET NOT RENEWABLE", KDC_ERR_BADOPTION } }; /* * Some TGS-REQ options require that the ticket have corresponding flags set. */ static int check_tgs_opts(krb5_kdc_req *req, krb5_ticket *tkt, const char **status) { size_t i; size_t nrules = sizeof(tgsflagrules) / sizeof(tgsflagrules[0]); const struct tgsflagrule *r; for (i = 0; i < nrules; i++) { r = &tgsflagrules[i]; if (r->reqflags & req->kdc_options) { if (!(r->checkflag & tkt->enc_part2->flags)) { *status = r->status; return r->err; } } } if (isflagset(tkt->enc_part2->flags, TKT_FLG_INVALID) && !isflagset(req->kdc_options, KDC_OPT_VALIDATE)) { *status = "TICKET NOT VALID"; return KRB_AP_ERR_TKT_NYV; } return 0; } static const struct tgsflagrule svcdenyrules[] = { { KDC_OPT_RENEWABLE, KRB5_KDB_DISALLOW_RENEWABLE, "NON-RENEWABLE TICKET", KDC_ERR_POLICY }, { KDC_OPT_ALLOW_POSTDATE, KRB5_KDB_DISALLOW_POSTDATED, "NON-POSTDATABLE TICKET", KDC_ERR_CANNOT_POSTDATE }, { KDC_OPT_ENC_TKT_IN_SKEY, KRB5_KDB_DISALLOW_DUP_SKEY, "DUP_SKEY DISALLOWED", KDC_ERR_POLICY } }; /* * A service principal can forbid some TGS-REQ options. */ static int check_tgs_svc_deny_opts(krb5_kdc_req *req, krb5_db_entry *server, krb5_ticket *tkt, krb5_timestamp kdc_time, const char **status) { size_t i; size_t nrules = sizeof(svcdenyrules) / sizeof(svcdenyrules[0]); const struct tgsflagrule *r; for (i = 0; i < nrules; i++) { r = &svcdenyrules[i]; if (!(r->reqflags & req->kdc_options)) continue; if (r->checkflag & server->attributes) { *status = r->status; return r->err; } } return 0; } /* * A service principal can deny all TGS-REQs for it. */ static int check_tgs_svc_deny_all(krb5_kdc_req *req, krb5_db_entry *server, krb5_ticket *tkt, krb5_timestamp kdc_time, const char **status) { if (server->attributes & KRB5_KDB_DISALLOW_ALL_TIX) { *status = "SERVER LOCKED OUT"; return KDC_ERR_S_PRINCIPAL_UNKNOWN; } if ((server->attributes & KRB5_KDB_DISALLOW_SVR) && !(req->kdc_options & KDC_OPT_ENC_TKT_IN_SKEY)) { *status = "SERVER NOT ALLOWED"; return KDC_ERR_MUST_USE_USER2USER; } if (server->attributes & KRB5_KDB_DISALLOW_TGT_BASED) { if (krb5_is_tgs_principal(tkt->server)) { *status = "TGT BASED NOT ALLOWED"; return KDC_ERR_POLICY; } } return 0; } /* * A service principal can require certain TGT flags. */ static int check_tgs_svc_reqd_flags(krb5_kdc_req *req, krb5_db_entry *server, krb5_ticket *tkt, krb5_timestamp kdc_time, const char **status) { if (server->attributes & KRB5_KDB_REQUIRES_HW_AUTH) { if (!(tkt->enc_part2->flags & TKT_FLG_HW_AUTH)) { *status = "NO HW PREAUTH"; return KRB_ERR_GENERIC; } } if (server->attributes & KRB5_KDB_REQUIRES_PRE_AUTH) { if (!(tkt->enc_part2->flags & TKT_FLG_PRE_AUTH)) { *status = "NO PREAUTH"; return KRB_ERR_GENERIC; } } return 0; } static int check_tgs_svc_time(krb5_kdc_req *req, krb5_db_entry *server, krb5_ticket *tkt, krb5_timestamp kdc_time, const char **status) { if (server->expiration && ts_after(kdc_time, server->expiration)) { *status = "SERVICE EXPIRED"; return KDC_ERR_SERVICE_EXP; } return 0; } static int check_tgs_svc_policy(krb5_kdc_req *req, krb5_db_entry *server, krb5_ticket *tkt, krb5_timestamp kdc_time, const char **status) { int errcode; size_t i; size_t nfns = sizeof(svc_pol_fns) / sizeof(svc_pol_fns[0]); for (i = 0; i < nfns; i++) { errcode = svc_pol_fns[i](req, server, tkt, kdc_time, status); if (errcode != 0) return errcode; } return 0; } /* * Check header ticket timestamps against the current time. */ static int check_tgs_times(krb5_kdc_req *req, krb5_ticket_times *times, krb5_timestamp kdc_time, const char **status) { krb5_timestamp starttime; /* For validating a postdated ticket, check the start time vs. the KDC time. */ if (req->kdc_options & KDC_OPT_VALIDATE) { starttime = times->starttime ? times->starttime : times->authtime; if (ts_after(starttime, kdc_time)) { *status = "NOT_YET_VALID"; return KRB_AP_ERR_TKT_NYV; } } /* * Check the renew_till time. The endtime was already * been checked in the initial authentication check. */ if ((req->kdc_options & KDC_OPT_RENEW) && ts_after(kdc_time, times->renew_till)) { *status = "TKT_EXPIRED"; return KRB_AP_ERR_TKT_EXPIRED; } return 0; } /* Check for local user tickets issued by foreign realms. This check is * skipped for S4U2Self requests. */ static int check_tgs_lineage(krb5_db_entry *server, krb5_ticket *tkt, krb5_boolean is_crossrealm, const char **status) { if (is_crossrealm && data_eq(tkt->enc_part2->client->realm, server->princ->realm)) { *status = "INVALID LINEAGE"; return KDC_ERR_POLICY; } return 0; } static int check_tgs_s4u2self(kdc_realm_t *kdc_active_realm, krb5_kdc_req *req, krb5_db_entry *server, krb5_ticket *tkt, krb5_timestamp kdc_time, krb5_pa_s4u_x509_user *s4u_x509_user, krb5_db_entry *client, krb5_boolean is_crossrealm, krb5_boolean is_referral, const char **status, krb5_pa_data ***e_data) { krb5_db_entry empty_server = { 0 }; /* If the server is local, check that the request is for self. */ if (!is_referral && !is_client_db_alias(kdc_context, server, tkt->enc_part2->client)) { *status = "INVALID_S4U2SELF_REQUEST_SERVER_MISMATCH"; return KDC_ERR_C_PRINCIPAL_UNKNOWN; /* match Windows error */ } /* S4U2Self requests must use options valid for AS requests. */ if (req->kdc_options & AS_INVALID_OPTIONS) { *status = "INVALID S4U2SELF OPTIONS"; return KDC_ERR_BADOPTION; } /* * Valid S4U2Self requests can occur in the following combinations: * * (1) local TGT, local user, local server * (2) cross TGT, local user, issuing referral * (3) cross TGT, non-local user, issuing referral * (4) cross TGT, non-local user, local server * * The first case is for a single-realm S4U2Self scenario; the second, * third, and fourth cases are for the initial, intermediate (if any), and * final cross-realm requests in a multi-realm scenario. */ if (!is_crossrealm && is_referral) { /* This could happen if the requesting server no longer exists, and we * found a referral instead. Treat this as a server lookup failure. */ *status = "LOOKING_UP_SERVER"; return KDC_ERR_S_PRINCIPAL_UNKNOWN; } if (client != NULL && is_crossrealm && !is_referral) { /* A local server should not need a cross-realm TGT to impersonate * a local principal. */ *status = "NOT_CROSS_REALM_REQUEST"; return KDC_ERR_C_PRINCIPAL_UNKNOWN; /* match Windows error */ } if (client == NULL && !is_crossrealm) { /* * The server is asking to impersonate a principal from another realm, * using a local TGT. It should instead ask that principal's realm and * follow referrals back to us. */ *status = "S4U2SELF_CLIENT_NOT_OURS"; return KDC_ERR_POLICY; /* match Windows error */ } if (client == NULL && s4u_x509_user->user_id.user->length == 0) { /* * Only a KDC in the client realm can handle a certificate-only * S4U2Self request. Other KDCs require a principal name and ignore * the subject-certificate field. */ *status = "INVALID_XREALM_S4U2SELF_REQUEST"; return KDC_ERR_POLICY; /* match Windows error */ } if (client != NULL) { /* Validate the client policy. Use an empty server principal to bypass * server policy checks. */ return validate_as_request(kdc_active_realm, req, client, &empty_server, kdc_time, status, e_data); } return 0; } static int check_tgs_s4u2proxy(kdc_realm_t *kdc_active_realm, krb5_kdc_req *req, krb5_db_entry *server, krb5_ticket *tkt, const krb5_ticket *stkt, krb5_db_entry *stkt_server, krb5_boolean is_crossrealm, krb5_boolean is_referral, const char **status) { /* A second ticket must be present in the request. */ if (stkt == NULL) { *status = "NO_2ND_TKT"; return KDC_ERR_BADOPTION; } /* Constrained delegation is mutually exclusive with renew/forward/etc. * (and therefore requires the header ticket to be a TGT). */ if (req->kdc_options & (NON_TGT_OPTION | KDC_OPT_ENC_TKT_IN_SKEY)) { *status = "INVALID_S4U2PROXY_OPTIONS"; return KDC_ERR_BADOPTION; } /* Can't get a TGT (otherwise it would be unconstrained delegation). */ if (krb5_is_tgs_principal(req->server)) { *status = "NOT_ALLOWED_TO_DELEGATE"; return KDC_ERR_POLICY; } /* * An S4U2Proxy request must be an initial request to the impersonator's * realm (possibly for a target resource in the same realm), or a final * cross-realm RBCD request to the resource realm. Intermediate * referral-chasing requests do not use the CNAME-IN-ADDL-TKT flag. */ /* For an initial or same-realm request, the second ticket server and * header ticket client must be the same principal. */ if (!is_crossrealm && !is_client_db_alias(kdc_context, stkt_server, tkt->enc_part2->client)) { *status = "EVIDENCE_TICKET_MISMATCH"; return KDC_ERR_SERVER_NOMATCH; } /* * For a cross-realm request, the second ticket must be a referral TGT to * our realm with the impersonator as client. (Unlike the header ticket, * the second ticket contains authdata for the subject client.) The target * server must also be local, so we must not be issuing a referral. */ if (is_crossrealm && (is_referral || !is_cross_tgs_principal(stkt_server->princ) || !data_eq(stkt_server->princ->data[1], server->princ->realm) || !krb5_principal_compare(kdc_context, stkt->enc_part2->client, tkt->enc_part2->client))) { *status = "XREALM_EVIDENCE_TICKET_MISMATCH"; return KDC_ERR_BADOPTION; } return 0; } static int check_tgs_u2u(kdc_realm_t *kdc_active_realm, krb5_kdc_req *req, const krb5_ticket *stkt, krb5_db_entry *server, const char **status) { /* A second ticket must be present in the request. */ if (stkt == NULL) { *status = "NO_2ND_TKT"; return KDC_ERR_BADOPTION; } /* The second ticket must be a TGT to the server realm. */ if (!is_local_tgs_principal(stkt->server) || !data_eq(stkt->server->data[1], server->princ->realm)) { *status = "2ND_TKT_NOT_TGS"; return KDC_ERR_POLICY; } /* The second ticket client must match the requested server. */ if (!is_client_db_alias(kdc_context, server, stkt->enc_part2->client)) { *status = "2ND_TKT_MISMATCH"; return KDC_ERR_SERVER_NOMATCH; } return 0; } /* * Some TGS-REQ options allow for a non-TGS principal in the ticket. Do some * checks that are peculiar to these cases. (e.g., ticket service principal * matches requested service principal) */ static int check_tgs_nontgt(kdc_realm_t *kdc_active_realm, krb5_kdc_req *req, krb5_ticket *tkt, const char **status) { if (!krb5_principal_compare(kdc_context, tkt->server, req->server)) { *status = "SERVER DIDN'T MATCH TICKET FOR RENEW/FORWARD/ETC"; return KDC_ERR_SERVER_NOMATCH; } /* Cannot proxy ticket granting tickets. */ if ((req->kdc_options & KDC_OPT_PROXY) && krb5_is_tgs_principal(req->server)) { *status = "CAN'T PROXY TGT"; return KDC_ERR_BADOPTION; } return 0; } /* * Do some checks for a normal TGS-REQ (where the ticket service must be a TGS * principal). */ static int check_tgs_tgt(kdc_realm_t *kdc_active_realm, krb5_kdc_req *req, krb5_ticket *tkt, const char **status) { /* Make sure it's a TGS principal. */ if (!krb5_is_tgs_principal(tkt->server)) { *status = "BAD TGS SERVER NAME"; return KRB_AP_ERR_NOT_US; } /* TGS principal second component must match service realm. */ if (!data_eq(*krb5_princ_component(kdc_context, tkt->server, 1), *krb5_princ_realm(kdc_context, req->server))) { *status = "BAD TGS SERVER INSTANCE"; return KRB_AP_ERR_NOT_US; } return 0; } int validate_tgs_request(kdc_realm_t *kdc_active_realm, krb5_kdc_req *request, krb5_db_entry *server, krb5_ticket *ticket, const krb5_ticket *stkt, krb5_db_entry *stkt_server, krb5_timestamp kdc_time, krb5_pa_s4u_x509_user *s4u_x509_user, krb5_db_entry *s4u2self_client, krb5_boolean is_crossrealm, krb5_boolean is_referral, const char **status, krb5_pa_data ***e_data) { int errcode; krb5_error_code ret; /* Depends only on request and ticket. */ errcode = check_tgs_opts(request, ticket, status); if (errcode != 0) return errcode; /* Depends only on request, ticket times, and current time. */ errcode = check_tgs_times(request, &ticket->enc_part2->times, kdc_time, status); if (errcode != 0) return errcode; errcode = check_tgs_svc_policy(request, server, ticket, kdc_time, status); if (errcode != 0) return errcode; if (request->kdc_options & NON_TGT_OPTION) errcode = check_tgs_nontgt(kdc_active_realm, request, ticket, status); else errcode = check_tgs_tgt(kdc_active_realm, request, ticket, status); if (errcode != 0) return errcode; /* Check the hot list */ if (check_hot_list(ticket)) { *status = "HOT_LIST"; return(KRB_AP_ERR_REPEAT); } if (s4u_x509_user != NULL) { errcode = check_tgs_s4u2self(kdc_active_realm, request, server, ticket, kdc_time, s4u_x509_user, s4u2self_client, is_crossrealm, is_referral, status, e_data); } else { errcode = check_tgs_lineage(server, ticket, is_crossrealm, status); } if (errcode != 0) return errcode; if (request->kdc_options & KDC_OPT_ENC_TKT_IN_SKEY) { errcode = check_tgs_u2u(kdc_active_realm, request, stkt, server, status); if (errcode != 0) return errcode; } if (request->kdc_options & KDC_OPT_CNAME_IN_ADDL_TKT) { errcode = check_tgs_s4u2proxy(kdc_active_realm, request, server, ticket, stkt, stkt_server, is_crossrealm, is_referral, status); if (errcode != 0) return errcode; } if (check_anon(kdc_active_realm, ticket->enc_part2->client, request->server) != 0) { *status = "ANONYMOUS NOT ALLOWED"; return(KDC_ERR_POLICY); } /* Perform KDB module policy checks. */ ret = krb5_db_check_policy_tgs(kdc_context, request, server, ticket, status, e_data); if (ret && ret != KRB5_PLUGIN_OP_NOTSUPP) return errcode_to_protocol(ret); return 0; }