diff options
Diffstat (limited to 'ipapython/secrets')
| -rw-r--r-- | ipapython/secrets/__init__.py | 0 | ||||
| -rw-r--r-- | ipapython/secrets/client.py | 109 | ||||
| -rw-r--r-- | ipapython/secrets/common.py | 45 | ||||
| -rw-r--r-- | ipapython/secrets/kem.py | 228 | ||||
| -rw-r--r-- | ipapython/secrets/store.py | 261 |
5 files changed, 0 insertions, 643 deletions
diff --git a/ipapython/secrets/__init__.py b/ipapython/secrets/__init__.py deleted file mode 100644 index e69de29bb..000000000 --- a/ipapython/secrets/__init__.py +++ /dev/null diff --git a/ipapython/secrets/client.py b/ipapython/secrets/client.py deleted file mode 100644 index d9cc7d0f5..000000000 --- a/ipapython/secrets/client.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (C) 2015 IPA Project Contributors, see COPYING for license - -from __future__ import print_function -from custodia.message.kem import KEMClient, KEY_USAGE_SIG, KEY_USAGE_ENC -from jwcrypto.common import json_decode -from jwcrypto.jwk import JWK -from ipapython.secrets.kem import IPAKEMKeys -from ipapython.secrets.store import iSecStore -from ipaplatform.paths import paths -from base64 import b64encode -import ldapurl -import gssapi -import os -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 - - return iSecStore(config) - - def __init__( - self, client_service, keyfile, keytab, server, realm, - ldap_uri=None, auth_type=None): - 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.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 missig subjAltName - requests.packages.urllib3.disable_warnings() - - 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))} - 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) - authtok = ctx.step() - return {'Authorization': 'Negotiate %s' % b64encode(authtok)} - - 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 - r = requests.get(url, headers=headers, - 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 diff --git a/ipapython/secrets/common.py b/ipapython/secrets/common.py deleted file mode 100644 index 2b906b649..000000000 --- a/ipapython/secrets/common.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (C) 2015 IPA Project Contributors, see COPYING for license -from __future__ import print_function -import ldap -import ldap.sasl -import ldap.filter - - -class iSecLdap(object): - - def __init__(self, uri, auth_type=None): - self.uri = uri - if auth_type is not None: - self.auth_type = auth_type - else: - if uri.startswith('ldapi'): - self.auth_type = 'EXTERNAL' - else: - self.auth_type = 'GSSAPI' - self._basedn = None - - @property - def basedn(self): - if self._basedn is None: - conn = self.connect() - r = conn.search_s('', ldap.SCOPE_BASE) - self._basedn = r[0][1]['defaultnamingcontext'][0] - return self._basedn - - def connect(self): - conn = ldap.initialize(self.uri) - if self.auth_type == 'EXTERNAL': - auth_tokens = ldap.sasl.external(None) - elif self.auth_type == 'GSSAPI': - auth_tokens = ldap.sasl.sasl({}, 'GSSAPI') - else: - raise ValueError( - 'Invalid authentication type: %s' % self.auth_type) - conn.sasl_interactive_bind_s('', auth_tokens) - return conn - - def build_filter(self, formatstr, args): - escaped_args = dict() - for key, value in args.iteritems(): - escaped_args[key] = ldap.filter.escape_filter_chars(value) - return formatstr.format(**escaped_args) diff --git a/ipapython/secrets/kem.py b/ipapython/secrets/kem.py deleted file mode 100644 index 7f92c9f89..000000000 --- a/ipapython/secrets/kem.py +++ /dev/null @@ -1,228 +0,0 @@ -# Copyright (C) 2015 IPA Project Contributors, see COPYING for license - -from __future__ import print_function -import os - -# pylint: disable=import-error -from six.moves.configparser import ConfigParser -# pylint: enable=import-error - -from ipaplatform.paths import paths -from ipapython.dn import DN -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa, ec -from custodia.message.kem import KEMKeysStore -from custodia.message.kem import KEY_USAGE_SIG, KEY_USAGE_ENC, KEY_USAGE_MAP -from jwcrypto.common import json_decode, json_encode -from jwcrypto.common import base64url_encode -from jwcrypto.jwk import JWK -from ipapython.secrets.common import iSecLdap -from binascii import unhexlify -import ldap - - -IPA_REL_BASE_DN = 'cn=custodia,cn=ipa,cn=etc' -IPA_KEYS_QUERY = '(&(ipaKeyUsage={usage:s})(memberPrincipal={princ:s}))' -RFC5280_USAGE_MAP = {KEY_USAGE_SIG: 'digitalSignature', - KEY_USAGE_ENC: 'dataEncipherment'} - - -class KEMLdap(iSecLdap): - - @property - def keysbase(self): - return '%s,%s' % (IPA_REL_BASE_DN, self.basedn) - - def _encode_int(self, i): - I = hex(i).rstrip("L").lstrip("0x") - return base64url_encode(unhexlify((len(I) % 2) * '0' + I)) - - def _parse_public_key(self, ipa_public_key): - public_key = serialization.load_der_public_key(ipa_public_key, - default_backend()) - num = public_key.public_numbers() - if isinstance(num, rsa.RSAPublicNumbers): - return {'kty': 'RSA', - 'e': self._encode_int(num.e), - 'n': self._encode_int(num.n)} - elif isinstance(num, ec.EllipticCurvePublicNumbers): - if num.curve.name == 'secp256r1': - curve = 'P-256' - elif num.curve.name == 'secp384r1': - curve = 'P-384' - elif num.curve.name == 'secp521r1': - curve = 'P-521' - else: - raise TypeError('Unsupported Elliptic Curve') - return {'kty': 'EC', - 'crv': curve, - 'x': self._encode_int(num.x), - 'y': self._encode_int(num.y)} - else: - raise TypeError('Unknown Public Key type') - - def get_key(self, usage, principal): - conn = self.connect() - scope = ldap.SCOPE_SUBTREE - - ldap_filter = self.build_filter(IPA_KEYS_QUERY, - {'usage': RFC5280_USAGE_MAP[usage], - 'princ': principal}) - r = conn.search_s(self.keysbase, scope, ldap_filter) - if len(r) != 1: - raise ValueError("Incorrect number of results (%d) searching for" - "public key for %s" % (len(r), principal)) - ipa_public_key = r[0][1]['ipaPublicKey'][0] - jwk = self._parse_public_key(ipa_public_key) - jwk['use'] = KEY_USAGE_MAP[usage] - return json_encode(jwk) - - def _format_public_key(self, key): - if isinstance(key, str): - jwkey = json_decode(key) - if 'kty' not in jwkey: - raise ValueError('Invalid key, missing "kty" attribute') - if jwkey['kty'] == 'RSA': - pubnum = rsa.RSAPublicNumbers(jwkey['e'], jwkey['n']) - pubkey = pubnum.public_key(default_backend()) - elif jwkey['kty'] == 'EC': - if jwkey['crv'] == 'P-256': - curve = ec.SECP256R1 - elif jwkey['crv'] == 'P-384': - curve = ec.SECP384R1 - elif jwkey['crv'] == 'P-521': - curve = ec.SECP521R1 - else: - raise TypeError('Unsupported Elliptic Curve') - pubnum = ec.EllipticCurvePublicNumbers( - jwkey['x'], jwkey['y'], curve) - pubkey = pubnum.public_key(default_backend()) - else: - raise ValueError('Unknown key type: %s' % jwkey['kty']) - elif isinstance(key, rsa.RSAPublicKey): - pubkey = key - elif isinstance(key, ec.EllipticCurvePublicKey): - pubkey = key - else: - raise TypeError('Unknown key type: %s' % type(key)) - - return pubkey.public_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PublicFormat.SubjectPublicKeyInfo) - - def set_key(self, usage, principal, key): - """ - Write key for the host or service. - - Service keys are nested one level beneath the 'cn=custodia' - container, in the 'cn=<servicename>' container; this allows - fine-grained control over key management permissions for - specific services. - - The container is assumed to exist. - - """ - public_key = self._format_public_key(key) - conn = self.connect() - servicename, host = principal.split('@')[0].split('/') - name = '%s/%s' % (KEY_USAGE_MAP[usage], host) - service_rdn = ('cn', servicename) if servicename != 'host' else DN() - dn = str(DN(('cn', name), service_rdn, self.keysbase)) - try: - mods = [('objectClass', ['nsContainer', - 'ipaKeyPolicy', - 'ipaPublicKeyObject', - 'groupOfPrincipals']), - ('cn', name), - ('ipaKeyUsage', RFC5280_USAGE_MAP[usage]), - ('memberPrincipal', principal), - ('ipaPublicKey', public_key)] - conn.add_s(dn, mods) - except Exception: # pylint: disable=broad-except - # This may fail if the entry already exists - mods = [(ldap.MOD_REPLACE, 'ipaPublicKey', public_key)] - conn.modify_s(dn, mods) - - -def newServerKeys(path, keyid): - skey = JWK(generate='RSA', use='sig', kid=keyid) - ekey = JWK(generate='RSA', use='enc', kid=keyid) - with open(path, 'w') as f: - os.fchmod(f.fileno(), 0o600) - os.fchown(f.fileno(), 0, 0) - f.write('[%s,%s]' % (skey.export(), ekey.export())) - return [skey.get_op_key('verify'), ekey.get_op_key('encrypt')] - - -class IPAKEMKeys(KEMKeysStore): - """A KEM Keys Store. - - This is a store that holds public keys of registered - clients allowed to use KEM messages. It takes the form - of an authorizer merely for the purpose of attaching - itself to a 'request' so that later on the KEM Parser - can fetch the appropariate key to verify/decrypt an - incoming request and make the payload available. - - The KEM Parser will actually perform additional - authorization checks in this case. - - SimplePathAuthz is extended here as we want to attach the - store only to requests on paths we are configured to - manage. - """ - - def __init__(self, config=None, ipaconf=paths.IPA_DEFAULT_CONF): - super(IPAKEMKeys, self).__init__(config) - conf = ConfigParser() - conf.read(ipaconf) - self.host = conf.get('global', 'host') - self.realm = conf.get('global', 'realm') - self.ldap_uri = config.get('ldap_uri', None) - if self.ldap_uri is None: - self.ldap_uri = conf.get('global', 'ldap_uri', None) - self._server_keys = None - - def find_key(self, kid, usage): - if kid is None: - raise TypeError('Key ID is None, should be a SPN') - conn = KEMLdap(self.ldap_uri) - return conn.get_key(usage, kid) - - def generate_server_keys(self): - self.generate_keys('host') - - def generate_keys(self, servicename): - principal = '%s/%s@%s' % (servicename, self.host, self.realm) - # Neutralize the key with read if any - self._server_keys = None - # Generate private key and store it - pubkeys = newServerKeys(self.config['server_keys'], principal) - # Store public key in LDAP - ldapconn = KEMLdap(self.ldap_uri) - ldapconn.set_key(KEY_USAGE_SIG, principal, pubkeys[0]) - ldapconn.set_key(KEY_USAGE_ENC, principal, pubkeys[1]) - - @property - def server_keys(self): - if self._server_keys is None: - with open(self.config['server_keys']) as f: - jsonkeys = f.read() - dictkeys = json_decode(jsonkeys) - self._server_keys = (JWK(**dictkeys[KEY_USAGE_SIG]), - JWK(**dictkeys[KEY_USAGE_ENC])) - return self._server_keys - - -# Manual testing -if __name__ == '__main__': - IKK = IPAKEMKeys({'paths': '/', - 'server_keys': '/etc/ipa/custodia/server.keys'}) - IKK.generate_server_keys() - print(('SIG', IKK.server_keys[0].export_public())) - print(('ENC', IKK.server_keys[1].export_public())) - print(IKK.find_key('host/%s@%s' % (IKK.host, IKK.realm), - usage=KEY_USAGE_SIG)) - print(IKK.find_key('host/%s@%s' % (IKK.host, IKK.realm), - usage=KEY_USAGE_ENC)) diff --git a/ipapython/secrets/store.py b/ipapython/secrets/store.py deleted file mode 100644 index 30a87d4a5..000000000 --- a/ipapython/secrets/store.py +++ /dev/null @@ -1,261 +0,0 @@ -# Copyright (C) 2015 IPA Project Contributors, see COPYING for license - -from __future__ import print_function -from base64 import b64encode, b64decode -from custodia.store.interface import CSStore -from jwcrypto.common import json_decode, json_encode -from ipaplatform.paths import paths -from ipapython import ipautil -from ipapython.secrets.common import iSecLdap -import ldap -import os -import shutil -import sys -import tempfile - - -class UnknownKeyName(Exception): - pass - - -class DBMAPHandler(object): - - def __init__(self, config, dbmap, nickname): - raise NotImplementedError - - def export_key(self): - raise NotImplementedError - - def import_key(self, value): - raise NotImplementedError - - -def log_error(error): - print(error, file=sys.stderr) - - -def PKI_TOMCAT_password_callback(): - password = None - with open(paths.PKI_TOMCAT_PASSWORD_CONF) as f: - for line in f.readlines(): - key, value = line.strip().split('=') - if key == 'internal': - password = value - break - return password - - -def HTTPD_password_callback(): - with open(paths.ALIAS_PWDFILE_TXT) as f: - password = f.read() - return password - - -class NSSWrappedCertDB(DBMAPHandler): - ''' - Store that extracts private keys from an NSSDB, wrapped with the - private key of the primary CA. - ''' - - def __init__(self, config, dbmap, nickname): - if 'path' not in dbmap: - raise ValueError( - 'Configuration does not provide NSSDB path') - if 'pwcallback' not in dbmap: - raise ValueError( - 'Configuration does not provide Password Calback') - if 'wrap_nick' not in dbmap: - raise ValueError( - 'Configuration does not provide nickname of wrapping key') - self.nssdb_path = dbmap['path'] - self.nssdb_password = dbmap['pwcallback']() - self.wrap_nick = dbmap['wrap_nick'] - self.target_nick = nickname - - def export_key(self): - tdir = tempfile.mkdtemp(dir=paths.TMP) - try: - nsspwfile = os.path.join(tdir, 'nsspwfile') - with open(nsspwfile, 'w+') as f: - f.write(self.nssdb_password) - wrapped_key_file = os.path.join(tdir, 'wrapped_key') - certificate_file = os.path.join(tdir, 'certificate') - ipautil.run([ - paths.PKI, '-d', self.nssdb_path, '-C', nsspwfile, - 'ca-authority-key-export', - '--wrap-nickname', self.wrap_nick, - '--target-nickname', self.target_nick, - '-o', wrapped_key_file]) - ipautil.run([ - paths.CERTUTIL, '-d', self.nssdb_path, - '-L', '-n', self.target_nick, - '-a', '-o', certificate_file]) - with open(wrapped_key_file, 'r') as f: - wrapped_key = f.read() - with open(certificate_file, 'r') as f: - certificate = f.read() - finally: - shutil.rmtree(tdir) - return json_encode({ - 'wrapped_key': b64encode(wrapped_key), - 'certificate': certificate}) - - -class NSSCertDB(DBMAPHandler): - - def __init__(self, config, dbmap, nickname): - if 'type' not in dbmap or dbmap['type'] != 'NSSDB': - raise ValueError('Invalid type "%s",' - ' expected "NSSDB"' % (dbmap['type'],)) - if 'path' not in dbmap: - raise ValueError('Configuration does not provide NSSDB path') - if 'pwcallback' not in dbmap: - raise ValueError('Configuration does not provide Password Calback') - self.nssdb_path = dbmap['path'] - self.nickname = nickname - self.nssdb_password = dbmap['pwcallback']() - - def export_key(self): - tdir = tempfile.mkdtemp(dir=paths.TMP) - try: - nsspwfile = os.path.join(tdir, 'nsspwfile') - with open(nsspwfile, 'w+') as f: - f.write(self.nssdb_password) - pk12pwfile = os.path.join(tdir, 'pk12pwfile') - password = b64encode(os.urandom(16)) - with open(pk12pwfile, 'w+') as f: - f.write(password) - pk12file = os.path.join(tdir, 'pk12file') - ipautil.run([paths.PK12UTIL, - "-d", self.nssdb_path, - "-o", pk12file, - "-n", self.nickname, - "-k", nsspwfile, - "-w", pk12pwfile]) - with open(pk12file, 'r') as f: - data = f.read() - finally: - shutil.rmtree(tdir) - return json_encode({'export password': password, - 'pkcs12 data': b64encode(data)}) - - def import_key(self, value): - v = json_decode(value) - tdir = tempfile.mkdtemp(dir=paths.TMP) - try: - nsspwfile = os.path.join(tdir, 'nsspwfile') - with open(nsspwfile, 'w+') as f: - f.write(self.nssdb_password) - pk12pwfile = os.path.join(tdir, 'pk12pwfile') - with open(pk12pwfile, 'w+') as f: - f.write(v['export password']) - pk12file = os.path.join(tdir, 'pk12file') - with open(pk12file, 'w+') as f: - f.write(b64decode(v['pkcs12 data'])) - ipautil.run([paths.PK12UTIL, - "-d", self.nssdb_path, - "-i", pk12file, - "-n", self.nickname, - "-k", nsspwfile, - "-w", pk12pwfile]) - finally: - shutil.rmtree(tdir) - - -# Exfiltrate the DM password Hash so it can be set in replica's and this -# way let a replica be install without knowing the DM password and yet -# still keep the DM password synchronized across replicas -class DMLDAP(DBMAPHandler): - - def __init__(self, config, dbmap, nickname): - if 'type' not in dbmap or dbmap['type'] != 'DMLDAP': - raise ValueError('Invalid type "%s",' - ' expected "DMLDAP"' % (dbmap['type'],)) - if nickname != 'DMHash': - raise UnknownKeyName("Unknown Key Named '%s'" % nickname) - self.ldap = iSecLdap(config['ldap_uri'], - config.get('auth_type', None)) - - def export_key(self): - conn = self.ldap.connect() - r = conn.search_s('cn=config', ldap.SCOPE_BASE, - attrlist=['nsslapd-rootpw']) - if len(r) != 1: - raise RuntimeError('DM Hash not found!') - return json_encode({'dmhash': r[0][1]['nsslapd-rootpw'][0]}) - - def import_key(self, value): - v = json_decode(value) - conn = self.ldap.connect() - mods = [(ldap.MOD_REPLACE, 'nsslapd-rootpw', str(v['dmhash']))] - conn.modify_s('cn=config', mods) - - -NAME_DB_MAP = { - 'ca': { - 'type': 'NSSDB', - 'path': paths.PKI_TOMCAT_ALIAS_DIR, - 'handler': NSSCertDB, - 'pwcallback': PKI_TOMCAT_password_callback, - }, - 'ca_wrapped': { - 'handler': NSSWrappedCertDB, - 'path': paths.PKI_TOMCAT_ALIAS_DIR, - 'pwcallback': PKI_TOMCAT_password_callback, - 'wrap_nick': 'caSigningCert cert-pki-ca', - }, - 'ra': { - 'type': 'NSSDB', - 'path': paths.HTTPD_ALIAS_DIR, - 'handler': NSSCertDB, - 'pwcallback': HTTPD_password_callback, - }, - 'dm': { - 'type': 'DMLDAP', - 'handler': DMLDAP, - } -} - - -class IPASecStore(CSStore): - - def __init__(self, config=None): - self.config = config - - def _get_handler(self, key): - path = key.split('/', 3) - if len(path) != 3 or path[0] != 'keys': - raise ValueError('Invalid name') - if path[1] not in NAME_DB_MAP: - raise UnknownKeyName("Unknown DB named '%s'" % path[1]) - dbmap = NAME_DB_MAP[path[1]] - return dbmap['handler'](self.config, dbmap, path[2]) - - def get(self, key): - try: - key_handler = self._get_handler(key) - value = key_handler.export_key() - except Exception as e: # pylint: disable=broad-except - log_error('Error retrievieng key "%s": %s' % (key, str(e))) - value = None - return value - - def set(self, key, value, replace=False): - try: - key_handler = self._get_handler(key) - key_handler.import_key(value) - except Exception as e: # pylint: disable=broad-except - log_error('Error storing key "%s": %s' % (key, str(e))) - - def list(self, keyfilter=None): - raise NotImplementedError - - def cut(self, key): - raise NotImplementedError - - def span(self, key): - raise NotImplementedError - - -# backwards compatibility with FreeIPA 4.3 and 4.4. -iSecStore = IPASecStore |
