0b494d
From 240617674f83305b2a27899aa83f6af0caa69c9c Mon Sep 17 00:00:00 2001
0b494d
From: Christian Heimes <cheimes@redhat.com>
0b494d
Date: Wed, 12 Jun 2019 22:02:52 +0200
0b494d
Subject: [PATCH] Fix CustodiaClient ccache handling
0b494d
0b494d
A CustodiaClient object has to the process environment a bit, e.g. set
0b494d
up GSSAPI credentials. To reuse the credentials in libldap connections,
0b494d
it is also necessary to set up a custom ccache store and to set the
0b494d
environment variable KRBCCNAME temporarily.
0b494d
0b494d
Fixes: https://pagure.io/freeipa/issue/7964
0b494d
Co-Authored-By: Fraser Tweedale <ftweedal@redhat.com>
0b494d
Signed-off-by: Christian Heimes <cheimes@redhat.com>
0b494d
Reviewed-By: Christian Heimes <cheimes@redhat.com>
0b494d
Reviewed-By: Fraser Tweedale <ftweedal@redhat.com>
0b494d
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
0b494d
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
0b494d
---
0b494d
 install/tools/ipa-pki-retrieve-key |  33 ++++---
0b494d
 ipaserver/secrets/client.py        | 143 ++++++++++++++++-------------
0b494d
 2 files changed, 100 insertions(+), 76 deletions(-)
0b494d
0b494d
diff --git a/install/tools/ipa-pki-retrieve-key b/install/tools/ipa-pki-retrieve-key
0b494d
index 5056682c3cdaa734be2dadcffd7de0b2d80afaf9..192022b9b40f076e88fd95d5cc8cf8305901dcf5 100755
0b494d
--- a/install/tools/ipa-pki-retrieve-key
0b494d
+++ b/install/tools/ipa-pki-retrieve-key
0b494d
@@ -2,9 +2,8 @@
0b494d
 
0b494d
 from __future__ import print_function
0b494d
 
0b494d
+import argparse
0b494d
 import os
0b494d
-import sys
0b494d
-import traceback
0b494d
 
0b494d
 from ipalib import constants
0b494d
 from ipalib.config import Env
0b494d
@@ -16,27 +15,37 @@ def main():
0b494d
     env = Env()
0b494d
     env._finalize()
0b494d
 
0b494d
-    keyname = "ca_wrapped/" + sys.argv[1]
0b494d
-    servername = sys.argv[2]
0b494d
+    parser = argparse.ArgumentParser("ipa-pki-retrieve-key")
0b494d
+    parser.add_argument("keyname", type=str)
0b494d
+    parser.add_argument("servername", type=str)
0b494d
+
0b494d
+    args = parser.parse_args()
0b494d
+    keyname = "ca_wrapped/{}".format(args.keyname)
0b494d
 
0b494d
     service = constants.PKI_GSSAPI_SERVICE_NAME
0b494d
     client_keyfile = os.path.join(paths.PKI_TOMCAT, service + '.keys')
0b494d
     client_keytab = os.path.join(paths.PKI_TOMCAT, service + '.keytab')
0b494d
 
0b494d
+    for filename in [client_keyfile, client_keytab]:
0b494d
+        if not os.access(filename, os.R_OK):
0b494d
+            parser.error(
0b494d
+                "File '{}' missing or not readable.\n".format(filename)
0b494d
+            )
0b494d
+
0b494d
     # pylint: disable=no-member
0b494d
     client = CustodiaClient(
0b494d
-        client_service='%s@%s' % (service, env.host), server=servername,
0b494d
-        realm=env.realm, ldap_uri="ldaps://" + env.host,
0b494d
-        keyfile=client_keyfile, keytab=client_keytab,
0b494d
-        )
0b494d
+        client_service="{}@{}".format(service, env.host),
0b494d
+        server=args.servername,
0b494d
+        realm=env.realm,
0b494d
+        ldap_uri="ldaps://" + env.host,
0b494d
+        keyfile=client_keyfile,
0b494d
+        keytab=client_keytab,
0b494d
+    )
0b494d
 
0b494d
     # Print the response JSON to stdout; it is already in the format
0b494d
     # that Dogtag's ExternalProcessKeyRetriever expects
0b494d
     print(client.fetch_key(keyname, store=False))
0b494d
 
0b494d
 
0b494d
-try:
0b494d
+if __name__ == '__main__':
0b494d
     main()
0b494d
-except BaseException:
0b494d
-    traceback.print_exc()
0b494d
-    sys.exit(1)
0b494d
diff --git a/ipaserver/secrets/client.py b/ipaserver/secrets/client.py
0b494d
index 16e7856185aa9786007d3b7f8be0652f70fb4518..40df6c4e69cd673dd8e3c36fbf33f2cda8544a67 100644
0b494d
--- a/ipaserver/secrets/client.py
0b494d
+++ b/ipaserver/secrets/client.py
0b494d
@@ -1,93 +1,106 @@
0b494d
 # Copyright (C) 2015  IPA Project Contributors, see COPYING for license
0b494d
 
0b494d
 from __future__ import print_function, absolute_import
0b494d
+
0b494d
+import contextlib
0b494d
+import os
0b494d
+from base64 import b64encode
0b494d
+
0b494d
+
0b494d
 # pylint: disable=relative-import
0b494d
 from custodia.message.kem import KEMClient, KEY_USAGE_SIG, KEY_USAGE_ENC
0b494d
 # pylint: enable=relative-import
0b494d
 from jwcrypto.common import json_decode
0b494d
 from jwcrypto.jwk import JWK
0b494d
+from ipalib.krb_utils import krb5_format_service_principal_name
0b494d
 from ipaserver.secrets.kem import IPAKEMKeys
0b494d
-from ipaserver.secrets.store import iSecStore
0b494d
+from ipaserver.secrets.store import IPASecStore
0b494d
 from ipaplatform.paths import paths
0b494d
-from base64 import b64encode
0b494d
-import ldapurl
0b494d
 import gssapi
0b494d
-import os
0b494d
-import urllib3
0b494d
 import requests
0b494d
 
0b494d
 
0b494d
-class CustodiaClient(object):
0b494d
-
0b494d
-    def _client_keys(self):
0b494d
-        return self.ikk.server_keys
0b494d
-
0b494d
-    def _server_keys(self, server, realm):
0b494d
-        principal = 'host/%s@%s' % (server, realm)
0b494d
-        sk = JWK(**json_decode(self.ikk.find_key(principal, KEY_USAGE_SIG)))
0b494d
-        ek = JWK(**json_decode(self.ikk.find_key(principal, KEY_USAGE_ENC)))
0b494d
-        return (sk, ek)
0b494d
-
0b494d
-    def _ldap_uri(self, realm):
0b494d
-        dashrealm = '-'.join(realm.split('.'))
0b494d
-        socketpath = paths.SLAPD_INSTANCE_SOCKET_TEMPLATE % (dashrealm,)
0b494d
-        return 'ldapi://' + ldapurl.ldapUrlEscape(socketpath)
0b494d
-
0b494d
-    def _keystore(self, realm, ldap_uri, auth_type):
0b494d
-        config = dict()
0b494d
-        if ldap_uri is None:
0b494d
-            config['ldap_uri'] = self._ldap_uri(realm)
0b494d
-        else:
0b494d
-            config['ldap_uri'] = ldap_uri
0b494d
-        if auth_type is not None:
0b494d
-            config['auth_type'] = auth_type
0b494d
+@contextlib.contextmanager
0b494d
+def ccache_env(ccache):
0b494d
+    """Temporarily set KRB5CCNAME environment variable
0b494d
+    """
0b494d
+    orig_ccache = os.environ.get('KRB5CCNAME')
0b494d
+    os.environ['KRB5CCNAME'] = ccache
0b494d
+    try:
0b494d
+        yield
0b494d
+    finally:
0b494d
+        os.environ.pop('KRB5CCNAME', None)
0b494d
+        if orig_ccache is not None:
0b494d
+            os.environ['KRB5CCNAME'] = orig_ccache
0b494d
 
0b494d
-        return iSecStore(config)
0b494d
 
0b494d
-    def __init__(
0b494d
-            self, client_service, keyfile, keytab, server, realm,
0b494d
-            ldap_uri=None, auth_type=None):
0b494d
+class CustodiaClient(object):
0b494d
+    def __init__(self, client_service, keyfile, keytab, server, realm,
0b494d
+                 ldap_uri=None, auth_type=None):
0b494d
+        if client_service.endswith(realm) or "@" not in client_service:
0b494d
+            raise ValueError(
0b494d
+                "Client service name must be a GSS name (service@host), "
0b494d
+                "not '{}'.".format(client_service)
0b494d
+            )
0b494d
         self.client_service = client_service
0b494d
         self.keytab = keytab
0b494d
-
0b494d
-        # Init creds immediately to make sure they are valid.  Creds
0b494d
-        # can also be re-inited by _auth_header to avoid expiry.
0b494d
-        #
0b494d
-        self.creds = self.init_creds()
0b494d
-
0b494d
-        self.service_name = gssapi.Name('HTTP@%s' % (server,),
0b494d
-                                        gssapi.NameType.hostbased_service)
0b494d
         self.server = server
0b494d
+        self.realm = realm
0b494d
+        self.ldap_uri = ldap_uri
0b494d
+        self.auth_type = auth_type
0b494d
+        self.service_name = gssapi.Name(
0b494d
+            'HTTP@{}'.format(server), gssapi.NameType.hostbased_service
0b494d
+        )
0b494d
+        self.keystore = IPASecStore()
0b494d
+        # use in-process MEMORY ccache. Handler process don't need a TGT.
0b494d
+        token = b64encode(os.urandom(8)).decode('ascii')
0b494d
+        self.ccache = 'MEMORY:Custodia_{}'.format(token)
0b494d
+
0b494d
+        with ccache_env(self.ccache):
0b494d
+            # Init creds immediately to make sure they are valid.  Creds
0b494d
+            # can also be re-inited by _auth_header to avoid expiry.
0b494d
+            self.creds = self._init_creds()
0b494d
+
0b494d
+            self.ikk = IPAKEMKeys(
0b494d
+                {'server_keys': keyfile, 'ldap_uri': ldap_uri}
0b494d
+            )
0b494d
+            self.kemcli = KEMClient(
0b494d
+                self._server_keys(), self._client_keys()
0b494d
+            )
0b494d
 
0b494d
-        self.ikk = IPAKEMKeys({'server_keys': keyfile, 'ldap_uri': ldap_uri})
0b494d
-
0b494d
-        self.kemcli = KEMClient(self._server_keys(server, realm),
0b494d
-                                self._client_keys())
0b494d
-
0b494d
-        self.keystore = self._keystore(realm, ldap_uri, auth_type)
0b494d
-
0b494d
-        # FIXME: Remove warnings about missing subjAltName for the
0b494d
-        #        requests module
0b494d
-        urllib3.disable_warnings()
0b494d
+    def _client_keys(self):
0b494d
+        return self.ikk.server_keys
0b494d
 
0b494d
-    def init_creds(self):
0b494d
-        name = gssapi.Name(self.client_service,
0b494d
-                           gssapi.NameType.hostbased_service)
0b494d
-        store = {'client_keytab': self.keytab,
0b494d
-                 'ccache': 'MEMORY:Custodia_%s' % b64encode(
0b494d
-                     os.urandom(8)).decode('ascii')}
0b494d
+    def _server_keys(self):
0b494d
+        principal = krb5_format_service_principal_name(
0b494d
+            'host', self.server, self.realm
0b494d
+        )
0b494d
+        sk = JWK(**json_decode(self.ikk.find_key(principal, KEY_USAGE_SIG)))
0b494d
+        ek = JWK(**json_decode(self.ikk.find_key(principal, KEY_USAGE_ENC)))
0b494d
+        return sk, ek
0b494d
+
0b494d
+    def _init_creds(self):
0b494d
+        name = gssapi.Name(
0b494d
+            self.client_service, gssapi.NameType.hostbased_service
0b494d
+        )
0b494d
+        store = {
0b494d
+            'client_keytab': self.keytab,
0b494d
+            'ccache': self.ccache
0b494d
+        }
0b494d
         return gssapi.Credentials(name=name, store=store, usage='initiate')
0b494d
 
0b494d
     def _auth_header(self):
0b494d
-        if not self.creds or self.creds.lifetime < 300:
0b494d
-            self.creds = self.init_creds()
0b494d
-        ctx = gssapi.SecurityContext(name=self.service_name, creds=self.creds)
0b494d
+        if self.creds.lifetime < 300:
0b494d
+            self.creds = self._init_creds()
0b494d
+        ctx = gssapi.SecurityContext(
0b494d
+            name=self.service_name,
0b494d
+            creds=self.creds
0b494d
+        )
0b494d
         authtok = ctx.step()
0b494d
         return {'Authorization': 'Negotiate %s' % b64encode(
0b494d
             authtok).decode('ascii')}
0b494d
 
0b494d
     def fetch_key(self, keyname, store=True):
0b494d
-
0b494d
         # Prepare URL
0b494d
         url = 'https://%s/ipa/keys/%s' % (self.server, keyname)
0b494d
 
0b494d
@@ -99,9 +112,11 @@ class CustodiaClient(object):
0b494d
         headers = self._auth_header()
0b494d
 
0b494d
         # Perform request
0b494d
-        r = requests.get(url, headers=headers,
0b494d
-                         verify=paths.IPA_CA_CRT,
0b494d
-                         params={'type': 'kem', 'value': request})
0b494d
+        r = requests.get(
0b494d
+            url, headers=headers,
0b494d
+            verify=paths.IPA_CA_CRT,
0b494d
+            params={'type': 'kem', 'value': request}
0b494d
+        )
0b494d
         r.raise_for_status()
0b494d
         reply = r.json()
0b494d
 
0b494d
-- 
0b494d
2.20.1
0b494d