diff options
Diffstat (limited to 'ipaserver/secrets/kem.py')
-rw-r--r-- | ipaserver/secrets/kem.py | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/ipaserver/secrets/kem.py b/ipaserver/secrets/kem.py new file mode 100644 index 000000000..143caaf6c --- /dev/null +++ b/ipaserver/secrets/kem.py @@ -0,0 +1,228 @@ +# 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 ipaserver.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)) |