ac7d03
From 2f8033174775f55cb2377baf524fc36914aa38fa Mon Sep 17 00:00:00 2001
ac7d03
From: Simo Sorce <simo@redhat.com>
ac7d03
Date: Thu, 23 Mar 2017 13:02:00 -0400
ac7d03
Subject: [PATCH] Work around issues fetching session data
ac7d03
ac7d03
Unfortunately the MIT krb5 library has a severe limitation with FILE
ac7d03
ccaches when retrieving config data. It will always only search until
ac7d03
the first entry is found and return that one.
ac7d03
ac7d03
For FILE caches MIT krb5 does not support removing old entries when a
ac7d03
new one is stored, and storage happens only in append mode, so the end
ac7d03
result is that even if an update is stored it is never returned with the
ac7d03
standard krb5_cc_get_config() call.
ac7d03
ac7d03
To work around this issue we simply implement what krb5_cc_get_config()
ac7d03
does under the hood with the difference that we do not stop at the first
ac7d03
match but keep going until all ccache entries have been checked.
ac7d03
ac7d03
Related https://pagure.io/freeipa/issue/6775
ac7d03
ac7d03
Signed-off-by: Simo Sorce <simo@redhat.com>
ac7d03
Reviewed-By: Christian Heimes <cheimes@redhat.com>
ac7d03
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
ac7d03
---
ac7d03
 ipapython/session_storage.py | 213 ++++++++++++++++++++++++++++++++++++++-----
ac7d03
 1 file changed, 190 insertions(+), 23 deletions(-)
ac7d03
ac7d03
diff --git a/ipapython/session_storage.py b/ipapython/session_storage.py
ac7d03
index a88f9f7a75c73d4dc753183a100350d197d02199..f3094f60000aa6e3f4b27fe91092c4214936f651 100644
ac7d03
--- a/ipapython/session_storage.py
ac7d03
+++ b/ipapython/session_storage.py
ac7d03
@@ -13,6 +13,12 @@ try:
ac7d03
 except OSError as e:  # pragma: no cover
ac7d03
     raise ImportError(str(e))
ac7d03
 
ac7d03
+krb5_int32 = ctypes.c_int32
ac7d03
+krb5_error_code = krb5_int32
ac7d03
+krb5_magic = krb5_error_code
ac7d03
+krb5_enctype = krb5_int32
ac7d03
+krb5_octet = ctypes.c_uint8
ac7d03
+krb5_timestamp = krb5_int32
ac7d03
 
ac7d03
 class _krb5_context(ctypes.Structure):  # noqa
ac7d03
     """krb5/krb5.h struct _krb5_context"""
ac7d03
@@ -27,7 +33,7 @@ class _krb5_ccache(ctypes.Structure):  # noqa
ac7d03
 class _krb5_data(ctypes.Structure):  # noqa
ac7d03
     """krb5/krb5.h struct _krb5_data"""
ac7d03
     _fields_ = [
ac7d03
-        ("magic", ctypes.c_int32),
ac7d03
+        ("magic", krb5_magic),
ac7d03
         ("length", ctypes.c_uint),
ac7d03
         ("data", ctypes.c_char_p),
ac7d03
     ]
ac7d03
@@ -38,6 +44,63 @@ class krb5_principal_data(ctypes.Structure):  # noqa
ac7d03
     _fields_ = []
ac7d03
 
ac7d03
 
ac7d03
+class _krb5_keyblock(ctypes.Structure):  # noqa
ac7d03
+    """krb5/krb5.h struct _krb5_keyblock"""
ac7d03
+    _fields_ = [
ac7d03
+        ("magic", krb5_magic),
ac7d03
+        ("enctype", krb5_enctype),
ac7d03
+        ("length", ctypes.c_uint),
ac7d03
+        ("contents", ctypes.POINTER(krb5_octet))
ac7d03
+    ]
ac7d03
+
ac7d03
+
ac7d03
+class _krb5_ticket_times(ctypes.Structure):  # noqa
ac7d03
+    """krb5/krb5.h struct _krb5_ticket_times"""
ac7d03
+    _fields_ = [
ac7d03
+        ("authtime", krb5_timestamp),
ac7d03
+        ("starttime", krb5_timestamp),
ac7d03
+        ("endtime", krb5_timestamp),
ac7d03
+        ("renew_till", krb5_timestamp),
ac7d03
+    ]
ac7d03
+
ac7d03
+
ac7d03
+class _krb5_address(ctypes.Structure):  # noqa
ac7d03
+    """krb5/krb5.h struct _krb5_address"""
ac7d03
+    _fields_ = []
ac7d03
+
ac7d03
+
ac7d03
+class _krb5_authdata(ctypes.Structure):  # noqa
ac7d03
+    """krb5/krb5.h struct _krb5_authdata"""
ac7d03
+    _fields_ = []
ac7d03
+
ac7d03
+
ac7d03
+krb5_principal = ctypes.POINTER(krb5_principal_data)
ac7d03
+krb5_keyblock = _krb5_keyblock
ac7d03
+krb5_ticket_times = _krb5_ticket_times
ac7d03
+krb5_boolean = ctypes.c_uint
ac7d03
+krb5_flags = krb5_int32
ac7d03
+krb5_data = _krb5_data
ac7d03
+krb5_address_p = ctypes.POINTER(_krb5_address)
ac7d03
+krb5_authdata_p = ctypes.POINTER(_krb5_authdata)
ac7d03
+
ac7d03
+
ac7d03
+class _krb5_creds(ctypes.Structure):  # noqa
ac7d03
+    """krb5/krb5.h struct _krb5_creds"""
ac7d03
+    _fields_ = [
ac7d03
+        ("magic", krb5_magic),
ac7d03
+        ("client", krb5_principal),
ac7d03
+        ("server", krb5_principal),
ac7d03
+        ("keyblock", krb5_keyblock),
ac7d03
+        ("times", krb5_ticket_times),
ac7d03
+        ("is_skey", krb5_boolean),
ac7d03
+        ("ticket_flags", krb5_flags),
ac7d03
+        ("addresses", ctypes.POINTER(krb5_address_p)),
ac7d03
+        ("ticket", krb5_data),
ac7d03
+        ("second_ticket", krb5_data),
ac7d03
+        ("authdata", ctypes.POINTER(krb5_authdata_p))
ac7d03
+    ]
ac7d03
+
ac7d03
+
ac7d03
 class KRB5Error(Exception):
ac7d03
     pass
ac7d03
 
ac7d03
@@ -48,11 +111,13 @@ def krb5_errcheck(result, func, arguments):
ac7d03
         raise KRB5Error(result, func.__name__, arguments)
ac7d03
 
ac7d03
 
ac7d03
-krb5_principal = ctypes.POINTER(krb5_principal_data)
ac7d03
 krb5_context = ctypes.POINTER(_krb5_context)
ac7d03
 krb5_ccache = ctypes.POINTER(_krb5_ccache)
ac7d03
 krb5_data_p = ctypes.POINTER(_krb5_data)
ac7d03
 krb5_error = ctypes.c_int32
ac7d03
+krb5_creds = _krb5_creds
ac7d03
+krb5_pointer = ctypes.c_void_p
ac7d03
+krb5_cc_cursor = krb5_pointer
ac7d03
 
ac7d03
 krb5_init_context = LIBKRB5.krb5_init_context
ac7d03
 krb5_init_context.argtypes = (ctypes.POINTER(krb5_context), )
ac7d03
@@ -61,15 +126,15 @@ krb5_init_context.errcheck = krb5_errcheck
ac7d03
 
ac7d03
 krb5_free_context = LIBKRB5.krb5_free_context
ac7d03
 krb5_free_context.argtypes = (krb5_context, )
ac7d03
-krb5_free_context.retval = None
ac7d03
+krb5_free_context.restype = None
ac7d03
 
ac7d03
 krb5_free_principal = LIBKRB5.krb5_free_principal
ac7d03
 krb5_free_principal.argtypes = (krb5_context, krb5_principal)
ac7d03
-krb5_free_principal.retval = None
ac7d03
+krb5_free_principal.restype = None
ac7d03
 
ac7d03
 krb5_free_data_contents = LIBKRB5.krb5_free_data_contents
ac7d03
 krb5_free_data_contents.argtypes = (krb5_context, krb5_data_p)
ac7d03
-krb5_free_data_contents.retval = None
ac7d03
+krb5_free_data_contents.restype = None
ac7d03
 
ac7d03
 krb5_cc_default = LIBKRB5.krb5_cc_default
ac7d03
 krb5_cc_default.argtypes = (krb5_context, ctypes.POINTER(krb5_ccache), )
ac7d03
@@ -78,26 +143,79 @@ krb5_cc_default.errcheck = krb5_errcheck
ac7d03
 
ac7d03
 krb5_cc_close = LIBKRB5.krb5_cc_close
ac7d03
 krb5_cc_close.argtypes = (krb5_context, krb5_ccache, )
ac7d03
-krb5_cc_close.retval = krb5_error
ac7d03
+krb5_cc_close.restype = krb5_error
ac7d03
 krb5_cc_close.errcheck = krb5_errcheck
ac7d03
 
ac7d03
 krb5_parse_name = LIBKRB5.krb5_parse_name
ac7d03
 krb5_parse_name.argtypes = (krb5_context, ctypes.c_char_p,
ac7d03
                             ctypes.POINTER(krb5_principal), )
ac7d03
-krb5_parse_name.retval = krb5_error
ac7d03
+krb5_parse_name.restype = krb5_error
ac7d03
 krb5_parse_name.errcheck = krb5_errcheck
ac7d03
 
ac7d03
 krb5_cc_set_config = LIBKRB5.krb5_cc_set_config
ac7d03
 krb5_cc_set_config.argtypes = (krb5_context, krb5_ccache, krb5_principal,
ac7d03
                                ctypes.c_char_p, krb5_data_p, )
ac7d03
-krb5_cc_set_config.retval = krb5_error
ac7d03
+krb5_cc_set_config.restype = krb5_error
ac7d03
 krb5_cc_set_config.errcheck = krb5_errcheck
ac7d03
 
ac7d03
-krb5_cc_get_config = LIBKRB5.krb5_cc_get_config
ac7d03
-krb5_cc_get_config.argtypes = (krb5_context, krb5_ccache, krb5_principal,
ac7d03
-                               ctypes.c_char_p, krb5_data_p, )
ac7d03
-krb5_cc_get_config.retval = krb5_error
ac7d03
-krb5_cc_get_config.errcheck = krb5_errcheck
ac7d03
+krb5_cc_get_principal = LIBKRB5.krb5_cc_get_principal
ac7d03
+krb5_cc_get_principal.argtypes = (krb5_context, krb5_ccache,
ac7d03
+                                  ctypes.POINTER(krb5_principal), )
ac7d03
+krb5_cc_get_principal.restype = krb5_error
ac7d03
+krb5_cc_get_principal.errcheck = krb5_errcheck
ac7d03
+
ac7d03
+# krb5_build_principal is a variadic function but that can't be expressed
ac7d03
+# in a ctypes argtypes definition, so I explicitly listed the number of
ac7d03
+# arguments we actually use through the code for type checking purposes
ac7d03
+krb5_build_principal = LIBKRB5.krb5_build_principal
ac7d03
+krb5_build_principal.argtypes = (krb5_context, ctypes.POINTER(krb5_principal),
ac7d03
+                                 ctypes.c_uint, ctypes.c_char_p,
ac7d03
+                                 ctypes.c_char_p, ctypes.c_char_p,
ac7d03
+                                 ctypes.c_char_p, ctypes.c_char_p, )
ac7d03
+krb5_build_principal.restype = krb5_error
ac7d03
+krb5_build_principal.errcheck = krb5_errcheck
ac7d03
+
ac7d03
+krb5_cc_start_seq_get = LIBKRB5.krb5_cc_start_seq_get
ac7d03
+krb5_cc_start_seq_get.argtypes = (krb5_context, krb5_ccache,
ac7d03
+                                  ctypes.POINTER(krb5_cc_cursor), )
ac7d03
+krb5_cc_start_seq_get.restype = krb5_error
ac7d03
+krb5_cc_start_seq_get.errcheck = krb5_errcheck
ac7d03
+
ac7d03
+krb5_cc_next_cred = LIBKRB5.krb5_cc_next_cred
ac7d03
+krb5_cc_next_cred.argtypes = (krb5_context, krb5_ccache,
ac7d03
+                              ctypes.POINTER(krb5_cc_cursor),
ac7d03
+                              ctypes.POINTER(krb5_creds), )
ac7d03
+krb5_cc_next_cred.restype = krb5_error
ac7d03
+krb5_cc_next_cred.errcheck = krb5_errcheck
ac7d03
+
ac7d03
+krb5_cc_end_seq_get = LIBKRB5.krb5_cc_end_seq_get
ac7d03
+krb5_cc_end_seq_get.argtypes = (krb5_context, krb5_ccache,
ac7d03
+                                ctypes.POINTER(krb5_cc_cursor), )
ac7d03
+krb5_cc_end_seq_get.restype = krb5_error
ac7d03
+krb5_cc_end_seq_get.errcheck = krb5_errcheck
ac7d03
+
ac7d03
+krb5_free_cred_contents = LIBKRB5.krb5_free_cred_contents
ac7d03
+krb5_free_cred_contents.argtypes = (krb5_context, ctypes.POINTER(krb5_creds))
ac7d03
+krb5_free_cred_contents.restype = krb5_error
ac7d03
+krb5_free_cred_contents.errcheck = krb5_errcheck
ac7d03
+
ac7d03
+krb5_principal_compare = LIBKRB5.krb5_principal_compare
ac7d03
+krb5_principal_compare.argtypes = (krb5_context, krb5_principal,
ac7d03
+                                   krb5_principal, )
ac7d03
+krb5_principal_compare.restype = krb5_boolean
ac7d03
+
ac7d03
+krb5_unparse_name = LIBKRB5.krb5_unparse_name
ac7d03
+krb5_unparse_name.argtypes = (krb5_context, krb5_principal,
ac7d03
+                              ctypes.POINTER(ctypes.c_char_p), )
ac7d03
+krb5_unparse_name.restype = krb5_error
ac7d03
+krb5_unparse_name.errcheck = krb5_errcheck
ac7d03
+
ac7d03
+krb5_free_unparsed_name = LIBKRB5.krb5_free_unparsed_name
ac7d03
+krb5_free_unparsed_name.argtypes = (krb5_context, ctypes.c_char_p, )
ac7d03
+krb5_free_unparsed_name.restype = None
ac7d03
+
ac7d03
+CONF_REALM = "X-CACHECONF:"
ac7d03
+CONF_NAME = "krb5_ccache_conf_data"
ac7d03
 
ac7d03
 
ac7d03
 def store_data(princ_name, key, value):
ac7d03
@@ -144,29 +262,78 @@ def get_data(princ_name, key):
ac7d03
     """
ac7d03
     context = krb5_context()
ac7d03
     principal = krb5_principal()
ac7d03
+    srv_princ = krb5_principal()
ac7d03
     ccache = krb5_ccache()
ac7d03
-    data = _krb5_data()
ac7d03
+    pname_princ = krb5_principal()
ac7d03
+    pname = ctypes.c_char_p()
ac7d03
 
ac7d03
     try:
ac7d03
         krb5_init_context(ctypes.byref(context))
ac7d03
 
ac7d03
-        krb5_parse_name(context, ctypes.c_char_p(princ_name),
ac7d03
-                        ctypes.byref(principal))
ac7d03
-
ac7d03
         krb5_cc_default(context, ctypes.byref(ccache))
ac7d03
+        krb5_cc_get_principal(context, ccache, ctypes.byref(principal))
ac7d03
 
ac7d03
-        krb5_cc_get_config(context, ccache, principal, key,
ac7d03
-                           ctypes.byref(data))
ac7d03
-
ac7d03
-        return str(data.data)
ac7d03
+        # We need to parse and then unparse the name in case the pric_name
ac7d03
+        # passed in comes w/o a realm attached
ac7d03
+        krb5_parse_name(context, ctypes.c_char_p(princ_name),
ac7d03
+                        ctypes.byref(pname_princ))
ac7d03
+        krb5_unparse_name(context, pname_princ, ctypes.byref(pname))
ac7d03
+
ac7d03
+        krb5_build_principal(context, ctypes.byref(srv_princ),
ac7d03
+                             len(CONF_REALM), ctypes.c_char_p(CONF_REALM),
ac7d03
+                             ctypes.c_char_p(CONF_NAME), ctypes.c_char_p(key),
ac7d03
+                             pname, ctypes.c_char_p(None))
ac7d03
+
ac7d03
+        # Unfortunately we can't just use krb5_cc_get_config()
ac7d03
+        # because of bugs in some ccache handling code in krb5
ac7d03
+        # libraries that would always return the first entry
ac7d03
+        # stored and not the last one, which is the one we want.
ac7d03
+        cursor = krb5_cc_cursor()
ac7d03
+        creds = krb5_creds()
ac7d03
+        got_creds = False
ac7d03
+        krb5_cc_start_seq_get(context, ccache, ctypes.byref(cursor))
ac7d03
+        try:
ac7d03
+            while True:
ac7d03
+                checkcreds = krb5_creds()
ac7d03
+                # the next function will throw an error and break out of the
ac7d03
+                # while loop when we try to access past the last cred
ac7d03
+                krb5_cc_next_cred(context, ccache, ctypes.byref(cursor),
ac7d03
+                                  ctypes.byref(checkcreds))
ac7d03
+                if (krb5_principal_compare(context, principal,
ac7d03
+                                          checkcreds.client) == 1 and
ac7d03
+                    krb5_principal_compare(context, srv_princ,
ac7d03
+                                           checkcreds.server) == 1):
ac7d03
+                    if got_creds:
ac7d03
+                        krb5_free_cred_contents(context, ctypes.byref(creds))
ac7d03
+                    creds = checkcreds
ac7d03
+                    got_creds = True
ac7d03
+                    # We do not stop here, as we want the LAST entry
ac7d03
+                    # in the ccache for those ccaches that cannot delete
ac7d03
+                    # but only always append, like FILE
ac7d03
+                else:
ac7d03
+                    krb5_free_cred_contents(context,
ac7d03
+                                            ctypes.byref(checkcreds))
ac7d03
+        except KRB5Error:
ac7d03
+            pass
ac7d03
+        finally:
ac7d03
+            krb5_cc_end_seq_get(context, ccache, ctypes.byref(cursor))
ac7d03
+
ac7d03
+        if got_creds:
ac7d03
+            data = creds.ticket.data.decode('utf-8')
ac7d03
+            krb5_free_cred_contents(context, ctypes.byref(creds))
ac7d03
+            return data
ac7d03
 
ac7d03
     finally:
ac7d03
         if principal:
ac7d03
             krb5_free_principal(context, principal)
ac7d03
+        if srv_princ:
ac7d03
+            krb5_free_principal(context, srv_princ)
ac7d03
+        if pname_princ:
ac7d03
+            krb5_free_principal(context, pname_princ)
ac7d03
+        if pname:
ac7d03
+            krb5_free_unparsed_name(context, pname)
ac7d03
         if ccache:
ac7d03
             krb5_cc_close(context, ccache)
ac7d03
-        if data:
ac7d03
-            krb5_free_data_contents(context, data)
ac7d03
         if context:
ac7d03
             krb5_free_context(context)
ac7d03
 
ac7d03
-- 
ac7d03
2.12.1
ac7d03