mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-23 07:33:27 -06:00
139038c58b
Related: https://pagure.io/freeipa/issue/9278 Signed-off-by: Florence Blanc-Renaud <flo@redhat.com> Reviewed-By: Stanislav Levin <slev@altlinux.org>
137 lines
4.3 KiB
Python
137 lines
4.3 KiB
Python
# Copyright (C) 2015 IPA Project Contributors, see COPYING for license
|
|
|
|
from __future__ import print_function, absolute_import
|
|
|
|
import contextlib
|
|
import os
|
|
import secrets
|
|
from base64 import b64encode
|
|
|
|
|
|
# pylint: disable=relative-import
|
|
from ipaserver.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 IPASecStore
|
|
from ipaplatform.paths import paths
|
|
import gssapi
|
|
import requests
|
|
|
|
|
|
@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
|
|
|
|
|
|
class CustodiaClient:
|
|
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
|
|
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.
|
|
self.ccache = 'MEMORY:Custodia_{}'.format(secrets.token_hex())
|
|
|
|
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()
|
|
)
|
|
|
|
def _client_keys(self):
|
|
return self.ikk.server_keys
|
|
|
|
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 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)
|
|
|
|
# Prepare signed/encrypted request
|
|
encalg = ('RSA-OAEP', 'A256CBC-HS512')
|
|
request = self.kemcli.make_request(keyname, encalg=encalg)
|
|
|
|
# Prepare Authentication header
|
|
headers = self._auth_header()
|
|
|
|
# Perform request
|
|
# pylint: disable-next=missing-timeout
|
|
r = requests.get(
|
|
url, headers=headers,
|
|
verify=paths.IPA_CA_CRT,
|
|
params={'type': 'kem', 'value': request}
|
|
)
|
|
r.raise_for_status()
|
|
reply = r.json()
|
|
|
|
if 'type' not in reply or reply['type'] != 'kem':
|
|
raise RuntimeError('Invlid JSON response type')
|
|
|
|
value = self.kemcli.parse_reply(keyname, reply['value'])
|
|
|
|
if store:
|
|
self.keystore.set('keys/%s' % keyname, value)
|
|
else:
|
|
return value
|
|
|
|
return None
|