summaryrefslogtreecommitdiffstats
path: root/ipapython/secrets
diff options
context:
space:
mode:
Diffstat (limited to 'ipapython/secrets')
-rw-r--r--ipapython/secrets/__init__.py0
-rw-r--r--ipapython/secrets/client.py109
-rw-r--r--ipapython/secrets/common.py45
-rw-r--r--ipapython/secrets/kem.py228
-rw-r--r--ipapython/secrets/store.py261
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