krb5 commit: Use KCM_OP_RETRIEVE in KCM client

Greg Hudson <[email protected]>
Newsgroups gmane.comp.encryption.kerberos.cvs
Message-ID <[email protected]>
https://github.com/krb5/krb5/commit/795ebba8c039be172ab93cd41105c73ffdba0fdb
commit 795ebba8c039be172ab93cd41105c73ffdba0fdb
Author: Greg Hudson <[email protected]>
Date:   Fri Mar 26 23:38:54 2021 -0400

    Use KCM_OP_RETRIEVE in KCM client
    
    In kcm_retrieve(), try KCM_OP_RETRIEVE.  Fall back to iteration if the
    server doesn't implement it, or if we can an answer incompatible with
    KRB5_TC_SUPPORTED_KTYPES.
    
    In kcmserver.py, implement partial decoding for creds and cred tags so
    that we can do a basic principal name match.
    
    ticket: 8997 (new)

 src/include/kcm.h            |    2 +-
 src/lib/krb5/ccache/cc_kcm.c |   52 +++++++++++++++++++++++++++++++++++++++--
 src/tests/kcmserver.py       |   44 +++++++++++++++++++++++++++++++++--
 src/tests/t_ccache.py        |   11 ++++++--
 4 files changed, 99 insertions(+), 10 deletions(-)

diff --git a/src/include/kcm.h b/src/include/kcm.h
index 9b66f1c..85c20d3 100644
--- a/src/include/kcm.h
+++ b/src/include/kcm.h
@@ -87,7 +87,7 @@ typedef enum kcm_opcode {
     KCM_OP_INITIALIZE,          /*          (name, princ) -> ()          */
     KCM_OP_DESTROY,             /*                 (name) -> ()          */
     KCM_OP_STORE,               /*           (name, cred) -> ()          */
-    KCM_OP_RETRIEVE,
+    KCM_OP_RETRIEVE,            /* (name, flags, credtag) -> (cred)      */
     KCM_OP_GET_PRINCIPAL,       /*                 (name) -> (princ)     */
     KCM_OP_GET_CRED_UUID_LIST,  /*                 (name) -> (uuid, ...) */
     KCM_OP_GET_CRED_BY_UUID,    /*           (name, uuid) -> (cred)      */
diff --git a/src/lib/krb5/ccache/cc_kcm.c b/src/lib/krb5/ccache/cc_kcm.c
index 46705f1..23fcf13 100644
--- a/src/lib/krb5/ccache/cc_kcm.c
+++ b/src/lib/krb5/ccache/cc_kcm.c
@@ -826,9 +826,55 @@ static krb5_error_code KRB5_CALLCONV
 kcm_retrieve(krb5_context context, krb5_ccache cache, krb5_flags flags,
              krb5_creds *mcred, krb5_creds *cred_out)
 {
-    /* There is a KCM opcode for retrieving creds, but Heimdal's client doesn't
-     * use it.  It causes the KCM daemon to actually make a TGS request. */
-    return k5_cc_retrieve_cred_default(context, cache, flags, mcred, cred_out);
+    krb5_error_code ret;
+    struct kcmreq req = EMPTY_KCMREQ;
+    krb5_creds cred;
+    krb5_enctype *enctypes = NULL;
+
+    memset(&cred, 0, sizeof(cred));
+
+    /* Include KCM_GC_CACHED in flags to prevent Heimdal's sssd from making a
+     * TGS request itself. */
+    kcmreq_init(&req, KCM_OP_RETRIEVE, cache);
+    k5_buf_add_uint32_be(&req.reqbuf, map_tcflags(flags) | KCM_GC_CACHED);
+    k5_marshal_mcred(&req.reqbuf, mcred);
+    ret = cache_call(context, cache, &req);
+
+    /* Fall back to iteration if the server does not support retrieval. */
+    if (ret == KRB5_FCC_INTERNAL || ret == KRB5_CC_IO) {
+        ret = k5_cc_retrieve_cred_default(context, cache, flags, mcred,
+                                          cred_out);
+        goto cleanup;
+    }
+    if (ret)
+        goto cleanup;
+
+    ret = k5_unmarshal_cred(req.reply.ptr, req.reply.len, 4, &cred);
+    if (ret)
+        goto cleanup;
+
+    /* In rare cases we might retrieve a credential with a session key this
+     * context can't support, in which case we must retry using iteration. */
+    if (flags & KRB5_TC_SUPPORTED_KTYPES) {
+        ret = krb5_get_tgs_ktypes(context, cred.server, &enctypes);
+        if (ret)
+            goto cleanup;
+        if (!k5_etypes_contains(enctypes, cred.keyblock.enctype)) {
+            ret = k5_cc_retrieve_cred_default(context, cache, flags, mcred,
+                                              cred_out);
+            goto cleanup;
+        }
+    }
+
+    *cred_out = cred;
+    memset(&cred, 0, sizeof(cred));
+
+cleanup:
+    kcmreq_free(&req);
+    krb5_free_cred_contents(context, &cred);
+    free(enctypes);
+    /* Heimdal's KCM returns KRB5_CC_END if no cred is found. */
+    return (ret == KRB5_CC_END) ? KRB5_CC_NOTFOUND : map_invalid(ret);
 }
 
 static krb5_error_code KRB5_CALLCONV
diff --git a/src/tests/kcmserver.py b/src/tests/kcmserver.py
index 8c5e66f..25e6f2b 100644
--- a/src/tests/kcmserver.py
+++ b/src/tests/kcmserver.py
@@ -40,6 +40,7 @@ class KCMOpcodes(object):
     INITIALIZE = 4
     DESTROY = 5
     STORE = 6
+    RETRIEVE = 7
     GET_PRINCIPAL = 8
     GET_CRED_UUID_LIST = 9
     GET_CRED_BY_UUID = 10
@@ -54,6 +55,7 @@ class KCMOpcodes(object):
 
 
 class KRB5Errors(object):
+    KRB5_CC_NOTFOUND = -1765328243
     KRB5_CC_END = -1765328242
     KRB5_CC_NOSUPP = -1765328137
     KRB5_FCC_NOFILE = -1765328189
@@ -86,11 +88,29 @@ def get_cache(name):
     return cache
 
 
+def unpack_data(argbytes):
+    dlen, = struct.unpack('>L', argbytes[:4])
+    return argbytes[4:dlen+4], argbytes[dlen+4:]
+
+
 def unmarshal_name(argbytes):
     offset = argbytes.find(b'\0')
     return argbytes[0:offset], argbytes[offset+1:]
 
 
+def unmarshal_princ(argbytes):
+    # Ignore the type at argbytes[0:4].
+    ncomps, = struct.unpack('>L', argbytes[4:8])
+    realm, rest = unpack_data(argbytes[8:])
+    comps = []
+    for i in range(ncomps):
+        comp, rest = unpack_data(rest)
+        comps.append(comp)
+    # Asssume no quoting is needed.
+    princ = b'/'.join(comps) + b'@' + realm
+    return princ, rest
+
+
 def op_gen_new(argbytes):
     # Does not actually check for uniqueness.
     global next_unique
@@ -126,6 +146,22 @@ def op_store(argbytes):
     return 0, b''
 
 
+def op_retrieve(argbytes):
+    name, rest = unmarshal_name(argbytes)
+    # Ignore the flags at rest[0:4] and the header at rest[4:8].
+    # Assume there are client and server creds in the tag and match
+    # only against them.
+    cprinc, rest = unmarshal_princ(rest[8:])
+    sprinc, rest = unmarshal_princ(rest)
+    cache = get_cache(name)
+    for cred in (cache.creds[u] for u in cache.cred_uuids):
+        cred_cprinc, rest = unmarshal_princ(cred)
+        cred_sprinc, rest = unmarshal_princ(rest)
+        if cred_cprinc == cprinc and cred_sprinc == sprinc:
+            return 0, cred
+    return KRB5Errors.KRB5_CC_NOTFOUND, b''
+
+
 def op_get_principal(argbytes):
     name, rest = unmarshal_name(argbytes)
     cache = get_cache(name)
@@ -199,6 +235,7 @@ ophandlers = {
     KCMOpcodes.INITIALIZE : op_initialize,
     KCMOpcodes.DESTROY : op_destroy,
     KCMOpcodes.STORE : op_store,
+    KCMOpcodes.RETRIEVE : op_retrieve,
     KCMOpcodes.GET_PRINCIPAL : op_get_principal,
     KCMOpcodes.GET_CRED_UUID_LIST : op_get_cred_uuid_list,
     KCMOpcodes.GET_CRED_BY_UUID : op_get_cred_by_uuid,
@@ -243,10 +280,11 @@ def service_request(s):
     return True
 
 parser = optparse.OptionParser()
-parser.add_option('-c', '--credlist', action='store_true', dest='credlist',
-                  default=False, help='Support KCM_OP_GET_CRED_LIST')
+parser.add_option('-f', '--fallback', action='store_true', dest='fallback',
+                  default=False, help='Do not support RETRIEVE/GET_CRED_LIST')
 (options, args) = parser.parse_args()
-if not options.credlist:
+if options.fallback:
+    del ophandlers[KCMOpcodes.RETRIEVE]
     del ophandlers[KCMOpcodes.GET_CRED_LIST]
 
 server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
diff --git a/src/tests/t_ccache.py b/src/tests/t_ccache.py
index 90040fb..6ea9fb9 100755
--- a/src/tests/t_ccache.py
+++ b/src/tests/t_ccache.py
@@ -25,7 +25,7 @@ from k5test import *
 kcm_socket_path = os.path.join(os.getcwd(), 'testdir', 'kcm')
 conf = {'libdefaults': {'kcm_socket': kcm_socket_path,
                         'kcm_mach_service': '-'}}
-realm = K5Realm(create_host=False, krb5_conf=conf)
+realm = K5Realm(krb5_conf=conf)
 
 keyctl = which('keyctl')
 out = realm.run([klist, '-c', 'KEYRING:process:abcd'], expected_code=1)
@@ -71,6 +71,11 @@ def collection_test(realm, ccname):
     realm.kinit('alice', password('alice'))
     realm.run([klist], expected_msg='Default principal: alice@')
     realm.run([klist, '-A', '-s'])
+    realm.run([kvno, realm.host_princ], expected_msg = 'kvno = 1')
+    realm.run([kvno, realm.host_princ], expected_msg = 'kvno = 1')
+    out = realm.run([klist])
+    if out.count(realm.host_princ) != 1:
+        fail('Wrong number of service tickets in cache')
     realm.run([kdestroy])
     output = realm.run([klist], expected_code=1)
     if 'No credentials cache' not in output and 'not found' not in output:
@@ -126,14 +131,14 @@ def collection_test(realm, ccname):
 
 collection_test(realm, 'DIR:' + os.path.join(realm.testdir, 'cc'))
 
-# Test KCM without and with GET_CRED_LIST support.
+# Test KCM with and without RETRIEVE and GET_CRED_LIST support.
 kcmserver_path = os.path.join(srctop, 'tests', 'kcmserver.py')
 kcmd = realm.start_server([sys.executable, kcmserver_path, kcm_socket_path],
                           'starting...')
 collection_test(realm, 'KCM:')
 stop_daemon(kcmd)
 os.remove(kcm_socket_path)
-realm.start_server([sys.executable, kcmserver_path, '-c', kcm_socket_path],
+realm.start_server([sys.executable, kcmserver_path, '-f', kcm_socket_path],
                    'starting...')
 collection_test(realm, 'KCM:')
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.