krb5 commit: Add three kvno options from Heimdal kgetcred

Greg Hudson <[email protected]>
Newsgroups gmane.comp.encryption.kerberos.cvs
Message-ID <[email protected]>
https://github.com/krb5/krb5/commit/876bab8418d7dd134c9d9db812ee2118d5ad58f0
commit 876bab8418d7dd134c9d9db812ee2118d5ad58f0
Author: Greg Hudson <[email protected]>
Date:   Wed Jun 17 20:48:38 2020 -0400

    Add three kvno options from Heimdal kgetcred
    
    Add the flags --cached-only and --no-store, which pass the
    corresponding options to krb5_get_credentials().  Add the option
    --out-cache to write the retrieved credentials to a specified output
    cache.
    
    Add a Python test script for kvno command-line options, including
    tests for the new options.
    
    ticket: 8917 (new)

 doc/user/user_commands/kvno.rst |   13 +++++
 src/clients/kvno/Makefile.in    |    3 +
 src/clients/kvno/kvno.c         |  115 ++++++++++++++++++++++++++++-----------
 src/clients/kvno/t_kvno.py      |   75 +++++++++++++++++++++++++
 src/man/kvno.man                |   13 +++++
 5 files changed, 187 insertions(+), 32 deletions(-)

diff --git a/doc/user/user_commands/kvno.rst b/doc/user/user_commands/kvno.rst
index 3892f0c..7183135 100644
--- a/doc/user/user_commands/kvno.rst
+++ b/doc/user/user_commands/kvno.rst
@@ -74,6 +74,19 @@ OPTIONS
     client principal with the X.509 certificate in *cert_file*.  The
     certificate file must be in PEM format.
 
+**--cached-only**
+    Only retrieve credentials already present in the cache, not from
+    the KDC.
+
+**--no-store**
+    Do not store retrieved credentials in the cache.  If
+    **--out-cache** is also specified, credentials will still be
+    stored into the output credential cache.
+
+**--out-cache** *ccache*
+    Initialize *ccache* and store all retrieved credentials into it.
+    Do not store acquired credentials in the input cache.
+
 **--u2u** *ccache*
     Requests a user-to-user ticket.  *ccache* must contain a local
     krbtgt ticket for the server principal.  The reported version
diff --git a/src/clients/kvno/Makefile.in b/src/clients/kvno/Makefile.in
index 1c3f793..5ba8772 100644
--- a/src/clients/kvno/Makefile.in
+++ b/src/clients/kvno/Makefile.in
@@ -26,6 +26,9 @@ kvno: kvno.o $(KRB5_BASE_DEPLIBS)
 ##WIN32##	link $(EXE_LINKOPTS) /out:$@ $**
 ##WIN32##	$(_VC_MANIFEST_EMBED_EXE)
 
+check-pytests: kvno
+	$(RUNPYTEST) $(srcdir)/t_kvno.py $(PYTESTFLAGS)
+
 clean-unix::
 	$(RM) kvno.o kvno
 
diff --git a/src/clients/kvno/kvno.c b/src/clients/kvno/kvno.c
index 2472c0c..9d85864 100644
--- a/src/clients/kvno/kvno.c
+++ b/src/clients/kvno/kvno.c
@@ -44,14 +44,17 @@ xusage()
     fprintf(stderr, _("usage: %s [-C] [-u] [-c ccache] [-e etype]\n"), prog);
     fprintf(stderr, _("\t[-k keytab] [-S sname] [{-I | -U} for_user | "
                       "[-F cert_file] [-P]]\n"));
-    fprintf(stderr, _("\t[--u2u ccache] service1 service2 ...\n"));
+    fprintf(stderr, _("\t[--cached-only] [--no-store] [--out-cache ccache] "
+                      "[--u2u ccache]\n"));
+    fprintf(stderr, _("\tservice1 service2 ...\n"));
     exit(1);
 }
 
 static void do_v5_kvno(int argc, char *argv[], char *ccachestr, char *etypestr,
-                       char *keytab_name, char *sname, int canon, int unknown,
-                       char *for_user, int for_user_enterprise,
-                       char *for_user_cert_file, int proxy,
+                       char *keytab_name, char *sname, int cached_only,
+                       int canon, int no_store, int unknown, char *for_user,
+                       int for_user_enterprise, char *for_user_cert_file,
+                       int proxy, const char *out_ccname,
                        const char *u2u_ccname);
 
 #include <com_err.h>
@@ -61,18 +64,21 @@ static void extended_com_err_fn(const char *myprog, errcode_t code,
 int
 main(int argc, char *argv[])
 {
-    enum { OPTION_U2U = 256 };
-    struct option lopts[] = {
-        { "u2u", 1, NULL, OPTION_U2U },
-        { NULL, 0, NULL, 0 }
-    };
+    enum { OPTION_U2U = 256, OPTION_OUT_CACHE = 257 };
     const char *shopts = "uCc:e:hk:qPS:I:U:F:";
     int option;
     char *etypestr = NULL, *ccachestr = NULL, *keytab_name = NULL;
     char *sname = NULL, *for_user = NULL, *u2u_ccname = NULL;
-    char *for_user_cert_file = NULL;
+    char *for_user_cert_file = NULL, *out_ccname = NULL;
     int canon = 0, unknown = 0, proxy = 0, for_user_enterprise = 0;
-    int impersonate = 0;
+    int impersonate = 0, cached_only = 0, no_store = 0;
+    struct option lopts[] = {
+        { "cached-only", 0, &cached_only, 1 },
+        { "no-store", 0, &no_store, 1 },
+        { "out-cache", 1, NULL, OPTION_OUT_CACHE },
+        { "u2u", 1, NULL, OPTION_U2U },
+        { NULL, 0, NULL, 0 }
+    };
 
     setlocale(LC_ALL, "");
     set_com_err_hook(extended_com_err_fn);
@@ -135,6 +141,12 @@ main(int argc, char *argv[])
         case OPTION_U2U:
             u2u_ccname = optarg;
             break;
+        case OPTION_OUT_CACHE:
+            out_ccname = optarg;
+            break;
+        case 0:
+            /* If this option set a flag, do nothing else now. */
+            break;
         default:
             xusage();
             break;
@@ -159,8 +171,9 @@ main(int argc, char *argv[])
         xusage();
 
     do_v5_kvno(argc - optind, argv + optind, ccachestr, etypestr, keytab_name,
-               sname, canon, unknown, for_user, for_user_enterprise,
-               for_user_cert_file, proxy, u2u_ccname);
+               sname, cached_only, canon, no_store, unknown, for_user,
+               for_user_enterprise, for_user_cert_file, proxy, out_ccname,
+               u2u_ccname);
     return 0;
 }
 
@@ -274,14 +287,16 @@ static krb5_error_code
 kvno(const char *name, krb5_ccache ccache, krb5_principal me,
      krb5_enctype etype, krb5_keytab keytab, const char *sname,
      krb5_flags options, int unknown, krb5_principal for_user_princ,
-     krb5_data *for_user_cert, int proxy, krb5_data *u2u_ticket)
+     krb5_data *for_user_cert, int proxy, krb5_data *u2u_ticket,
+     krb5_creds **creds_out)
 {
     krb5_error_code ret;
     krb5_principal server = NULL;
     krb5_ticket *ticket = NULL;
-    krb5_creds in_creds, *out_creds = NULL;
+    krb5_creds in_creds, *creds = NULL;
     char *princ = NULL;
 
+    *creds_out = NULL;
     memset(&in_creds, 0, sizeof(in_creds));
 
     if (sname != NULL) {
@@ -321,13 +336,12 @@ kvno(const char *name, krb5_ccache ccache, krb5_principal me,
         in_creds.client = for_user_princ;
         in_creds.server = me;
         ret = krb5_get_credentials_for_user(context, options, ccache,
-                                            &in_creds, for_user_cert,
-                                            &out_creds);
+                                            &in_creds, for_user_cert, &creds);
     } else {
         in_creds.client = me;
         in_creds.server = server;
         ret = krb5_get_credentials(context, options, ccache, &in_creds,
-                                   &out_creds);
+                                   &creds);
     }
 
     if (ret) {
@@ -336,7 +350,7 @@ kvno(const char *name, krb5_ccache ccache, krb5_principal me,
     }
 
     /* We need a native ticket. */
-    ret = krb5_decode_ticket(&out_creds->ticket, &ticket);
+    ret = krb5_decode_ticket(&creds->ticket, &ticket);
     if (ret) {
         com_err(prog, ret, _("while decoding ticket for %s"), princ);
         goto cleanup;
@@ -362,15 +376,15 @@ kvno(const char *name, krb5_ccache ccache, krb5_principal me,
     }
 
     if (proxy) {
-        in_creds.client = out_creds->client;
-        out_creds->client = NULL;
-        krb5_free_creds(context, out_creds);
-        out_creds = NULL;
+        in_creds.client = creds->client;
+        creds->client = NULL;
+        krb5_free_creds(context, creds);
+        creds = NULL;
         in_creds.server = server;
 
         ret = krb5_get_credentials_for_proxy(context, KRB5_GC_CANONICALIZE,
                                              ccache, &in_creds, ticket,
-                                             &out_creds);
+                                             &creds);
         krb5_free_principal(context, in_creds.client);
         if (ret) {
             com_err(prog, ret, _("%s: constrained delegation failed"),
@@ -379,10 +393,13 @@ kvno(const char *name, krb5_ccache ccache, krb5_principal me,
         }
     }
 
+    *creds_out = creds;
+    creds = NULL;
+
 cleanup:
     krb5_free_principal(context, server);
     krb5_free_ticket(context, ticket);
-    krb5_free_creds(context, out_creds);
+    krb5_free_creds(context, creds);
     krb5_free_unparsed_name(context, princ);
     return ret;
 }
@@ -428,19 +445,28 @@ cleanup:
 
 static void
 do_v5_kvno(int count, char *names[], char * ccachestr, char *etypestr,
-           char *keytab_name, char *sname, int canon, int unknown,
-           char *for_user, int for_user_enterprise,
-           char *for_user_cert_file, int proxy, const char *u2u_ccname)
+           char *keytab_name, char *sname, int cached_only, int canon,
+           int no_store, int unknown, char *for_user, int for_user_enterprise,
+           char *for_user_cert_file, int proxy, const char *out_ccname,
+           const char *u2u_ccname)
 {
     krb5_error_code ret;
-    int i, errors, flags;
+    int i, errors, flags, initialized = 0;
     krb5_enctype etype;
-    krb5_ccache ccache;
+    krb5_ccache ccache, out_ccache = NULL;
     krb5_principal me;
     krb5_keytab keytab = NULL;
     krb5_principal for_user_princ = NULL;
-    krb5_flags options = canon ? KRB5_GC_CANONICALIZE : 0;
+    krb5_flags options = 0;
     krb5_data cert_data = empty_data(), *user_cert = NULL, *u2u_ticket = NULL;
+    krb5_creds *creds;
+
+    if (canon)
+        options |= KRB5_GC_CANONICALIZE;
+    if (cached_only)
+        options |= KRB5_GC_CACHED;
+    if (no_store || out_ccname != NULL)
+        options |= KRB5_GC_NO_STORE;
 
     ret = krb5_init_context(&context);
     if (ret) {
@@ -467,6 +493,14 @@ do_v5_kvno(int count, char *names[], char * ccachestr, char *etypestr,
         exit(1);
     }
 
+    if (out_ccname != NULL) {
+        ret = krb5_cc_resolve(context, out_ccname, &out_ccache);
+        if (ret) {
+            com_err(prog, ret, _("while resolving output ccache"));
+            exit(1);
+        }
+    }
+
     if (keytab_name != NULL) {
         ret = krb5_kt_resolve(context, keytab_name, &keytab);
         if (ret) {
@@ -513,8 +547,25 @@ do_v5_kvno(int count, char *names[], char * ccachestr, char *etypestr,
     errors = 0;
     for (i = 0; i < count; i++) {
         if (kvno(names[i], ccache, me, etype, keytab, sname, options, unknown,
-                 for_user_princ, user_cert, proxy, u2u_ticket) != 0)
+                 for_user_princ, user_cert, proxy, u2u_ticket, &creds) != 0) {
             errors++;
+        } else if (out_ccache != NULL) {
+            if (!initialized) {
+                ret = krb5_cc_initialize(context, out_ccache, creds->client);
+                if (ret) {
+                    com_err(prog, ret, _("while initializing output ccache"));
+                    exit(1);
+                }
+                initialized = 1;
+            }
+            ret = krb5_cc_store_cred(context, out_ccache, creds);
+            if (ret) {
+                com_err(prog, ret, _("while storing creds in output ccache"));
+                exit(1);
+            }
+        }
+
+        krb5_free_creds(context, creds);
     }
 
     if (keytab != NULL)
diff --git a/src/clients/kvno/t_kvno.py b/src/clients/kvno/t_kvno.py
new file mode 100644
index 0000000..e98b90e
--- /dev/null
+++ b/src/clients/kvno/t_kvno.py
@@ -0,0 +1,75 @@
+from k5test import *
+
+realm = K5Realm()
+
+def check_cache(ccache, expected_services):
+    # Fetch the klist output and skip past the header.
+    lines = realm.run([klist, '-c', ccache]).splitlines()
+    lines = lines[4:]
+
+    # For each line not beginning with an indent, match against the
+    # expected service principals.
+    svcs = {x: True for x in expected_services}
+    for l in lines:
+        if not l.startswith('\t'):
+            svcprinc = l.split()[4]
+            if svcprinc in svcs:
+                del svcs[svcprinc]
+            else:
+                fail('unexpected service princ ' + svcprinc)
+
+    if svcs:
+        fail('services not found in klist output: ' + ' '.join(svcs.keys()))
+
+
+mark('no options')
+realm.run([kvno, realm.user_princ], expected_msg='[email protected]: kvno = 1')
+check_cache(realm.ccache, [realm.krbtgt_princ, realm.user_princ])
+
+mark('-e')
+msgs = ('etypes requested in TGS request: camellia128-cts',
+        '/KDC has no support for encryption type')
+realm.run([kvno, '-e', 'camellia128-cts', realm.host_princ],
+          expected_code=1, expected_trace=msgs)
+
+mark('--cached-only')
+realm.run([kvno, '--cached-only', realm.user_princ], expected_msg='kvno = 1')
+realm.run([kvno, '--cached-only', realm.host_princ],
+          expected_code=1, expected_msg='Matching credential not found')
+check_cache(realm.ccache, [realm.krbtgt_princ, realm.user_princ])
+
+mark('--no-store')
+realm.run([kvno, '--no-store', realm.host_princ], expected_msg='kvno = 1')
+check_cache(realm.ccache, [realm.krbtgt_princ, realm.user_princ])
+
+mark('--out-cache') # and multiple services
+out_ccache = os.path.join(realm.testdir, 'ccache.out')
+realm.run([kvno, '--out-cache', out_ccache,
+           realm.host_princ, realm.admin_princ])
+check_cache(realm.ccache, [realm.krbtgt_princ, realm.user_princ])
+check_cache(out_ccache, [realm.host_princ, realm.admin_princ])
+
+mark('--out-cache --cached-only') # tests out-cache overwriting, and -q
+realm.run([kvno, '--out-cache', out_ccache, '--cached-only', realm.host_princ],
+          expected_code=1, expected_msg='Matching credential not found')
+out = realm.run([kvno, '-q', '--out-cache', out_ccache, '--cached-only',
+                 realm.user_princ])
+if out:
+    fail('unexpected kvno output with -q')
+check_cache(out_ccache, [realm.user_princ])
+
+mark('-U') # and -c
+svc_ccache = os.path.join(realm.testdir, 'ccache.svc')
+realm.run([kinit, '-k', '-c', svc_ccache, realm.host_princ])
+realm.run([kvno, '-c', svc_ccache, '-U', 'user', realm.host_princ])
+realm.run([klist, '-c', svc_ccache], expected_msg='for client user@')
+realm.run([kvno, '-c', svc_ccache, '-U', 'user', '--out-cache', out_ccache,
+           realm.host_princ])
+out = realm.run([klist, '-c', out_ccache])
+if ('Default principal: [email protected]' not in out):
+    fail('wrong default principal in klist output')
+
+# More S4U options are tested in tests/gssapi/t_s4u.py.
+# --u2u is tested in tests/t_u2u.py.
+
+success('kvno tests')
diff --git a/src/man/kvno.man b/src/man/kvno.man
index aac6e26..15e44c9 100644
--- a/src/man/kvno.man
+++ b/src/man/kvno.man
@@ -95,6 +95,19 @@ Specifies that protocol transition is to be used, identifying the
 client principal with the X.509 certificate in \fIcert_file\fP\&.  The
 certificate file must be in PEM format.
 .TP
+\fB\-\-cached\-only\fP
+Only retrieve credentials already present in the cache, not from
+the KDC.
+.TP
+\fB\-\-no\-store\fP
+Do not store retrieved credentials in the cache.  If
+\fB\-\-out\-cache\fP is also specified, credentials will still be
+stored into the output credential cache.
+.TP
+\fB\-\-out\-cache\fP \fIccache\fP
+Initialize \fIccache\fP and store all retrieved credentials into it.
+Do not store acquired credentials in the input cache.
+.TP
 \fB\-\-u2u\fP \fIccache\fP
 Requests a user\-to\-user ticket.  \fIccache\fP must contain a local
 krbtgt ticket for the server principal.  The reported version
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.