krb5 commit: Add RBCD client support

Greg Hudson ghudson at mit.edu
Mon Sep 9 10:33:10 EDT 2019


https://github.com/krb5/krb5/commit/c426ef2ca2ba45dbf96f5380cf7d153ec0679424
commit c426ef2ca2ba45dbf96f5380cf7d153ec0679424
Author: Isaac Boukris <iboukris at gmail.com>
Date:   Thu Jun 20 05:00:06 2019 +0000

    Add RBCD client support
    
    When making S4U2Proxy requests, include a PA-PAC-OPTIONS pa-data
    element advertising resource-based constrained delegation support.  If
    the KDC returns a referral TGT for the initial request and advertises
    RBCD support, chase referrals to the target realm with both a regular
    and proxy TGT, and make an S4U2Proxy request to the target realm with
    the proxy TGT as evidence ticket.
    
    Because cross-realm S4U2Proxy requests must use referrals, an explicit
    foreign realm in the server name cannot be honored.  In the GSSAPI
    krb5 mech, if a host-based server name is used, omit the realm (if one
    was obtained from [domain_realm] or similar) when calling
    krb5_get_credentials() for constrained delegation.
    
    [ghudson at mit.edu: rewrote commit message; made style changes]
    
    ticket: 8479

 src/include/k5-int.h                   |   13 ++
 src/include/krb5/krb5.hin              |    1 +
 src/lib/gssapi/krb5/init_sec_context.c |    9 +-
 src/lib/krb5/asn.1/asn1_k_encode.c     |   22 +++
 src/lib/krb5/krb/gc_via_tkt.c          |    9 +-
 src/lib/krb5/krb/s4u_creds.c           |  306 ++++++++++++++++++++++++++++++--
 src/lib/krb5/libkrb5.exports           |    2 +
 7 files changed, 345 insertions(+), 17 deletions(-)

diff --git a/src/include/k5-int.h b/src/include/k5-int.h
index 1d991e7..77d7abc 100644
--- a/src/include/k5-int.h
+++ b/src/include/k5-int.h
@@ -559,6 +559,13 @@ typedef struct _krb5_secure_cookie {
     krb5_pa_data **data;
 } krb5_secure_cookie;
 
+typedef struct _krb5_pa_pac_options {
+    krb5_flags options;
+} krb5_pa_pac_options;
+
+/* In PAC options, indicates Resource-Based Constrained Delegation support. */
+#define KRB5_PA_PAC_OPTIONS_RBCD 0x10000000
+
 #include <stdlib.h>
 #include <string.h>
 
@@ -1536,6 +1543,9 @@ encode_utf8_strings(krb5_data *const *ut8fstrings, krb5_data **);
 krb5_error_code
 encode_krb5_secure_cookie(const krb5_secure_cookie *, krb5_data **);
 
+krb5_error_code
+encode_krb5_pa_pac_options(const krb5_pa_pac_options *, krb5_data **);
+
 /*************************************************************************
  * End of prototypes for krb5_encode.c
  *************************************************************************/
@@ -1718,6 +1728,9 @@ decode_utf8_strings(const krb5_data *, krb5_data ***);
 krb5_error_code
 decode_krb5_secure_cookie(const krb5_data *, krb5_secure_cookie **);
 
+krb5_error_code
+decode_krb5_pa_pac_options(const krb5_data *, krb5_pa_pac_options **);
+
 struct _krb5_key_data;          /* kdb.h */
 
 struct ldap_seqof_key_data {
diff --git a/src/include/krb5/krb5.hin b/src/include/krb5/krb5.hin
index d65cf8f..eed38fd 100644
--- a/src/include/krb5/krb5.hin
+++ b/src/include/krb5/krb5.hin
@@ -1879,6 +1879,7 @@ krb5_verify_checksum(krb5_context context, krb5_cksumtype ctype,
 #define KRB5_ENCPADATA_REQ_ENC_PA_REP   149 /**< RFC 6806 */
 #define KRB5_PADATA_AS_FRESHNESS        150 /**< RFC 8070 */
 #define KRB5_PADATA_SPAKE               151
+#define KRB5_PADATA_PAC_OPTIONS         167 /**< MS-KILE and MS-SFU */
 
 #define KRB5_SAM_USE_SAD_AS_KEY         0x80000000
 #define KRB5_SAM_SEND_ENCRYPTED_SAD     0x40000000
diff --git a/src/lib/gssapi/krb5/init_sec_context.c b/src/lib/gssapi/krb5/init_sec_context.c
index 949debc..6ce3ad1 100644
--- a/src/lib/gssapi/krb5/init_sec_context.c
+++ b/src/lib/gssapi/krb5/init_sec_context.c
@@ -129,6 +129,7 @@ static krb5_error_code get_credentials(context, cred, server, now,
     krb5_error_code     code;
     krb5_creds          in_creds, evidence_creds, mcreds, *result_creds = NULL;
     krb5_flags          flags = 0;
+    krb5_principal_data server_data;
 
     *out_creds = NULL;
 
@@ -139,8 +140,14 @@ static krb5_error_code get_credentials(context, cred, server, now,
 
     assert(cred->name != NULL);
 
+    /* Remove assumed realm from host-based S4U2Proxy requests as they must
+     * start in the client realm. */
+    server_data = *server->princ;
+    if (cred->impersonator != NULL && server_data.type == KRB5_NT_SRV_HST)
+        server_data.realm = empty_data();
+    in_creds.server = &server_data;
+
     in_creds.client = cred->name->princ;
-    in_creds.server = server->princ;
     in_creds.times.endtime = endtime;
     in_creds.authdata = NULL;
     in_creds.keyblock.enctype = 0;
diff --git a/src/lib/krb5/asn.1/asn1_k_encode.c b/src/lib/krb5/asn.1/asn1_k_encode.c
index a026ab3..39fa8e3 100644
--- a/src/lib/krb5/asn.1/asn1_k_encode.c
+++ b/src/lib/krb5/asn.1/asn1_k_encode.c
@@ -1732,6 +1732,28 @@ DEFSEQTYPE(secure_cookie, krb5_secure_cookie, secure_cookie_fields);
 MAKE_ENCODER(encode_krb5_secure_cookie, secure_cookie);
 MAKE_DECODER(decode_krb5_secure_cookie, secure_cookie);
 
+/*
+ * -- based on MS-KILE and MS-SFU
+ * PAC-OPTIONS-FLAGS ::= BIT STRING {
+ *     claims(0),
+ *     branch-aware(1),
+ *     forward-to-full-dc(2),
+ *     resource-based-constrained-delegation(3)
+ * }
+ *
+ * PA-PAC-OPTIONS ::= SEQUENCE {
+ *     flags [0] PAC-OPTIONS-FLAGS
+ * }
+ */
+DEFFIELD(pa_pac_options_0, krb5_pa_pac_options, options, 0, krb5_flags);
+static const struct atype_info *pa_pac_options_fields[] = {
+    &k5_atype_pa_pac_options_0
+};
+DEFSEQTYPE(pa_pac_options, krb5_pa_pac_options, pa_pac_options_fields);
+MAKE_ENCODER(encode_krb5_pa_pac_options, pa_pac_options);
+MAKE_DECODER(decode_krb5_pa_pac_options, pa_pac_options);
+
+
 DEFFIELD(spake_factor_0, krb5_spake_factor, type, 0, int32);
 DEFFIELD(spake_factor_1, krb5_spake_factor, data, 1, opt_ostring_data_ptr);
 static const struct atype_info *spake_factor_fields[] = {
diff --git a/src/lib/krb5/krb/gc_via_tkt.c b/src/lib/krb5/krb/gc_via_tkt.c
index 3d0859b..5ac8a52 100644
--- a/src/lib/krb5/krb/gc_via_tkt.c
+++ b/src/lib/krb5/krb/gc_via_tkt.c
@@ -257,9 +257,12 @@ krb5int_process_tgs_reply(krb5_context context,
         /* Final hop, check whether KDC supports S4U2Self */
         if (krb5_principal_compare(context, dec_rep->client, in_cred->server))
             retval = KRB5KDC_ERR_PADATA_TYPE_NOSUPP;
-    } else if ((kdcoptions & KDC_OPT_CNAME_IN_ADDL_TKT) == 0) {
-        /* XXX for constrained delegation this check must be performed by caller
-         * as we don't have access to the key to decrypt the evidence ticket.
+    } else if ((kdcoptions & KDC_OPT_CNAME_IN_ADDL_TKT) == 0 ||
+               IS_TGS_PRINC(dec_rep->ticket->server)) {
+        /*
+         * For constrained delegation this check must be performed by caller,
+         * as we can't decrypt the evidence ticket.  However, if it is a
+         * referral the client should match the TGT client like normal.
          */
         if (!krb5_principal_compare(context, dec_rep->client, tkt->client))
             retval = KRB5_KDCREP_MODIFIED;
diff --git a/src/lib/krb5/krb/s4u_creds.c b/src/lib/krb5/krb/s4u_creds.c
index 26c15fe..71e2a08 100644
--- a/src/lib/krb5/krb/s4u_creds.c
+++ b/src/lib/krb5/krb/s4u_creds.c
@@ -725,6 +725,50 @@ cleanup:
     return code;
 }
 
+static krb5_error_code
+check_rbcd_support(krb5_context context, krb5_pa_data **padata)
+{
+    krb5_error_code code;
+    krb5_pa_data *pa;
+    krb5_pa_pac_options *pac_options;
+    krb5_data der_pac_options;
+
+    pa = krb5int_find_pa_data(context, padata, KRB5_PADATA_PAC_OPTIONS);
+    if (pa == NULL)
+        return KRB5KDC_ERR_PADATA_TYPE_NOSUPP;
+
+    der_pac_options = make_data(pa->contents, pa->length);
+    code = decode_krb5_pa_pac_options(&der_pac_options, &pac_options);
+    if (code)
+        return code;
+
+    if (!(pac_options->options & KRB5_PA_PAC_OPTIONS_RBCD))
+        code = KRB5KDC_ERR_PADATA_TYPE_NOSUPP;
+
+    free(pac_options);
+    return code;
+}
+
+static krb5_error_code
+add_rbcd_padata(krb5_context context, krb5_pa_data ***in_padata)
+{
+    krb5_error_code code;
+    krb5_pa_pac_options pac_options;
+    krb5_data *der_pac_options = NULL;
+
+    memset(&pac_options, 0, sizeof(pac_options));
+    pac_options.options |= KRB5_PA_PAC_OPTIONS_RBCD;
+
+    code = encode_krb5_pa_pac_options(&pac_options, &der_pac_options);
+    if (code)
+        return code;
+
+    code = k5_add_pa_data_from_data(in_padata, KRB5_PADATA_PAC_OPTIONS,
+                                    der_pac_options);
+    krb5_free_data(context, der_pac_options);
+    return code;
+}
+
 /* Set *tgt_out to a local TGT for the client realm retrieved from ccache. */
 static krb5_error_code
 get_client_tgt(krb5_context context, krb5_flags options, krb5_ccache ccache,
@@ -748,8 +792,11 @@ get_client_tgt(krb5_context context, krb5_flags options, krb5_ccache ccache,
     return code;
 }
 
-/* Copy req_server to *out_server.  If req_server has the referral realm, set
- * the realm of *out_server to realm. */
+/*
+ * Copy req_server to *out_server.  If req_server has the referral realm, set
+ * the realm of *out_server to realm.  Otherwise the S4U2Proxy request will
+ * fail unless the specified realm is the same as the TGT (or an alias to it).
+ */
 static krb5_error_code
 normalize_server_princ(krb5_context context, const krb5_data *realm,
                        krb5_principal req_server, krb5_principal *out_server)
@@ -776,14 +823,162 @@ normalize_server_princ(krb5_context context, const krb5_data *realm,
     return 0;
 }
 
+/* Return an error if server is present in referral_list. */
+static krb5_error_code
+check_referral_path(krb5_context context, krb5_principal server,
+                    krb5_creds **referral_list, int referral_count)
+{
+    int i;
+
+    for (i = 0; i < referral_count; i++) {
+        if (krb5_principal_compare(context, server, referral_list[i]->server))
+            return KRB5_KDC_UNREACH;
+    }
+    return 0;
+}
+
+/*
+ * Make TGS requests for in_creds using *tgt_inout, following referrals until
+ * the requested service ticket is issued.  Replace *tgt_inout with the final
+ * TGT used, or free it and set it to NULL on error.  Place the final creds
+ * received in *creds_out.
+ */
+static krb5_error_code
+chase_referrals(krb5_context context, krb5_creds *in_creds, krb5_flags kdcopt,
+                krb5_creds **tgt_inout, krb5_creds **creds_out)
+{
+    krb5_error_code code;
+    krb5_creds *referral_tgts[KRB5_REFERRAL_MAXHOPS] = { NULL };
+    krb5_creds mcreds, *tgt, *tkt = NULL;
+    krb5_principal_data server;
+    int referral_count = 0, i;
+
+    tgt = *tgt_inout;
+    *tgt_inout = NULL;
+    *creds_out = NULL;
+
+    mcreds = *in_creds;
+    server = *in_creds->server;
+    mcreds.server = &server;
+
+    for (referral_count = 0; referral_count < KRB5_REFERRAL_MAXHOPS;
+         referral_count++) {
+        code = krb5_get_cred_via_tkt(context, tgt, kdcopt, tgt->addresses,
+                                     &mcreds, &tkt);
+        if (code)
+            goto cleanup;
+
+        if (krb5_principal_compare_any_realm(context, mcreds.server,
+                                             tkt->server)) {
+            *creds_out = tkt;
+            *tgt_inout = tgt;
+            tkt = tgt = NULL;
+            goto cleanup;
+        }
+
+        if (!IS_TGS_PRINC(tkt->server)) {
+            code = KRB5KRB_AP_WRONG_PRINC;
+            goto cleanup;
+        }
+
+        if (data_eq(tgt->server->data[1], tkt->server->data[1])) {
+            code = KRB5_ERR_HOST_REALM_UNKNOWN;
+            goto cleanup;
+        }
+
+        code = check_referral_path(context, tkt->server, referral_tgts,
+                                   referral_count);
+        if (code)
+            goto cleanup;
+
+        referral_tgts[referral_count] = tgt;
+        tgt = tkt;
+        tkt = NULL;
+        server.realm = tgt->server->data[1];
+    }
+
+    /* Max hop count exceeded. */
+    code = KRB5_KDCREP_MODIFIED;
+
+cleanup:
+    for (i = 0; i < KRB5_REFERRAL_MAXHOPS; i++)
+        krb5_free_creds(context, referral_tgts[i]);
+    krb5_free_creds(context, tkt);
+    krb5_free_creds(context, tgt);
+    return code;
+}
+
+/*
+ * Make non-S4U2Proxy TGS requests for in_creds using *tgt_inout, following
+ * referrals until the requested service ticket is returned.  Discard the
+ * service ticket, but replace *tgt_inout with the final referral TGT.
+ */
+static krb5_error_code
+get_tgt_to_target_realm(krb5_context context, krb5_creds *in_creds,
+                        krb5_flags req_kdcopt, krb5_creds **tgt_inout)
+{
+    krb5_error_code code;
+    krb5_flags kdcopt;
+    krb5_creds mcreds, *out;
+
+    mcreds = *in_creds;
+    mcreds.second_ticket = empty_data();
+    kdcopt = FLAGS2OPTS((*tgt_inout)->ticket_flags) | req_kdcopt;
+
+    code = chase_referrals(context, &mcreds, kdcopt, tgt_inout, &out);
+    krb5_free_creds(context, out);
+
+    return code;
+}
+
+/*
+ * Make TGS requests for a cross-TGT to realm using *tgt_inout, following
+ * alternate TGS replies until the requested TGT is issued.  Replace *tgt_inout
+ * with the result.  Do nothing if *tgt_inout is already a cross-TGT for realm.
+ */
+static krb5_error_code
+get_target_realm_proxy_tgt(krb5_context context, const krb5_data *realm,
+                           krb5_flags req_kdcopt, krb5_creds **tgt_inout)
+{
+    krb5_error_code code;
+    krb5_creds mcreds, *out;
+    krb5_principal tgs;
+    krb5_flags flags;
+
+    if (data_eq(*realm, (*tgt_inout)->server->data[1]))
+        return 0;
+
+    code = krb5int_tgtname(context, realm, &(*tgt_inout)->server->data[1],
+                           &tgs);
+    if (code)
+        return code;
+
+    memset(&mcreds, 0, sizeof(mcreds));
+    mcreds.client = (*tgt_inout)->client;
+    mcreds.server = tgs;
+    flags = req_kdcopt | FLAGS2OPTS((*tgt_inout)->ticket_flags);
+
+    code = chase_referrals(context, &mcreds, flags, tgt_inout, &out);
+    krb5_free_principal(context, tgs);
+    if (code)
+        return code;
+
+    krb5_free_creds(context, *tgt_inout);
+    *tgt_inout = out;
+
+    return 0;
+}
+
 krb5_error_code
 k5_get_proxy_cred_from_kdc(krb5_context context, krb5_flags options,
                            krb5_ccache ccache, krb5_creds *in_creds,
                            krb5_creds **out_creds)
 {
     krb5_error_code code;
-    krb5_flags kdcopt, flags;
+    krb5_flags flags, req_kdcopt = 0;
     krb5_principal server = NULL;
+    krb5_pa_data **in_padata = NULL;
+    krb5_pa_data **enc_padata = NULL;
     krb5_creds mcreds, *tgt = NULL, *tkt = NULL;
 
     *out_creds = NULL;
@@ -803,27 +998,110 @@ k5_get_proxy_cred_from_kdc(krb5_context context, krb5_flags options,
     if (code)
         goto cleanup;
 
-    kdcopt = KDC_OPT_CNAME_IN_ADDL_TKT;
+    code = add_rbcd_padata(context, &in_padata);
+    if (code)
+        goto cleanup;
+
     if (options & KRB5_GC_CANONICALIZE)
-        kdcopt |= KDC_OPT_CANONICALIZE;
+        req_kdcopt |= KDC_OPT_CANONICALIZE;
     if (options & KRB5_GC_FORWARDABLE)
-        kdcopt |= KDC_OPT_FORWARDABLE;
+        req_kdcopt |= KDC_OPT_FORWARDABLE;
     if (options & KRB5_GC_NO_TRANSIT_CHECK)
-        kdcopt |= KDC_OPT_DISABLE_TRANSITED_CHECK;
+        req_kdcopt |= KDC_OPT_DISABLE_TRANSITED_CHECK;
 
     mcreds = *in_creds;
     mcreds.server = server;
 
-    flags = kdcopt | FLAGS2OPTS(tgt->ticket_flags);
+    flags = req_kdcopt | FLAGS2OPTS(tgt->ticket_flags) |
+        KDC_OPT_CNAME_IN_ADDL_TKT | KDC_OPT_CANONICALIZE;
     code = krb5_get_cred_via_tkt_ext(context, tgt, flags, tgt->addresses,
-                                     NULL, &mcreds, NULL, NULL, NULL, NULL,
-                                     &tkt, NULL);
+                                     in_padata, &mcreds, NULL, NULL, NULL,
+                                     &enc_padata, &tkt, NULL);
+
+    /*
+     * If the server principal name included a foreign realm which wasn't an
+     * alias for the local realm, the KDC won't be able to decrypt the TGT.
+     * Windows KDCs will return a BAD_INTEGRITY error in this case, while MIT
+     * KDCs will return S_PRINCIPAL_UNKNOWN.  We cannot distinguish the latter
+     * error from the service principal actually being unknown in the realm,
+     * but set a comprehensible error message for the BAD_INTEGRITY error.
+     */
+    if (code == KRB5KRB_AP_ERR_BAD_INTEGRITY &&
+        !krb5_realm_compare(context, in_creds->client, server)) {
+        k5_setmsg(context, code, _("Realm specified but S4U2Proxy must use "
+                                   "referral realm"));
+    }
+
     if (code)
         goto cleanup;
 
-    if (!krb5_principal_compare(context, server, tkt->server)) {
-        code = KRB5KRB_AP_WRONG_PRINC;
-        goto cleanup;
+    if (!krb5_principal_compare_any_realm(context, server, tkt->server)) {
+        /* Make sure we got a referral. */
+        if (!IS_TGS_PRINC(tkt->server)) {
+            code = KRB5KRB_AP_WRONG_PRINC;
+            goto cleanup;
+        }
+
+        /*
+         * Make sure the KDC supports S4U and resource-based constrained
+         * delegation; otherwise we might have gotten a regular TGT referral
+         * rather than a proxy TGT referral.
+         */
+        code = check_rbcd_support(context, enc_padata);
+        if (code)
+            goto cleanup;
+
+        krb5_free_pa_data(context, enc_padata);
+        enc_padata = NULL;
+
+        /*
+         * Replace tgt with a regular (not proxy) TGT to the target realm, by
+         * making a normal TGS request and following referrals.  Per [MS-SFU]
+         * 3.1.5.2.2, we need this TGT to make the final TGS request.
+         */
+        code = get_tgt_to_target_realm(context, &mcreds, req_kdcopt, &tgt);
+        if (code)
+            goto cleanup;
+
+        /*
+         * Replace tkt with a proxy TGT (meaning, one obtained using the
+         * referral TGT we got from the first S4U2Proxy request) to the target
+         * realm, if it isn't already one.
+         */
+        code = get_target_realm_proxy_tgt(context, &tgt->server->data[1],
+                                          req_kdcopt, &tkt);
+        if (code)
+            goto cleanup;
+
+        krb5_free_data_contents(context, &server->realm);
+        code = krb5int_copy_data_contents(context, &tgt->server->data[1],
+                                          &server->realm);
+        if (code)
+            goto cleanup;
+
+        /* Make an S4U2Proxy request to the target realm using the regular TGT,
+         * with the proxy TGT as the evidence ticket. */
+        mcreds.second_ticket = tkt->ticket;
+        tkt->ticket = empty_data();
+        krb5_free_creds(context, tkt);
+        tkt = NULL;
+        flags = req_kdcopt | FLAGS2OPTS(tgt->ticket_flags) |
+            KDC_OPT_CNAME_IN_ADDL_TKT | KDC_OPT_CANONICALIZE;
+        code = krb5_get_cred_via_tkt_ext(context, tgt, flags, tgt->addresses,
+                                         in_padata, &mcreds, NULL, NULL, NULL,
+                                         &enc_padata, &tkt, NULL);
+        free(mcreds.second_ticket.data);
+        if (code)
+            goto cleanup;
+
+        code = check_rbcd_support(context, enc_padata);
+        if (code)
+            goto cleanup;
+
+        if (!krb5_principal_compare(context, server, tkt->server)) {
+            code = KRB5KRB_AP_WRONG_PRINC;
+            goto cleanup;
+        }
     }
 
     if (!krb5_principal_compare(context, in_creds->server, tkt->server)) {
@@ -844,6 +1122,8 @@ cleanup:
     krb5_free_creds(context, tgt);
     krb5_free_creds(context, tkt);
     krb5_free_principal(context, server);
+    krb5_free_pa_data(context, in_padata);
+    krb5_free_pa_data(context, enc_padata);
     return code;
 }
 
diff --git a/src/lib/krb5/libkrb5.exports b/src/lib/krb5/libkrb5.exports
index 1d124a0..f036b1a 100644
--- a/src/lib/krb5/libkrb5.exports
+++ b/src/lib/krb5/libkrb5.exports
@@ -34,6 +34,7 @@ decode_krb5_pa_fx_fast_request
 decode_krb5_pa_otp_challenge
 decode_krb5_pa_otp_req
 decode_krb5_pa_otp_enc_req
+decode_krb5_pa_pac_options
 decode_krb5_pa_pac_req
 decode_krb5_pa_s4u_x509_user
 decode_krb5_pa_spake
@@ -86,6 +87,7 @@ encode_krb5_pa_fx_fast_reply
 encode_krb5_pa_otp_challenge
 encode_krb5_pa_otp_req
 encode_krb5_pa_otp_enc_req
+encode_krb5_pa_pac_options
 encode_krb5_pa_s4u_x509_user
 encode_krb5_pa_spake
 encode_krb5_padata_sequence


More information about the cvs-krb5 mailing list