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