/usr/lib/python2.7/dist-packages/ipaserver/secrets/kem.py is in python-ipaserver 4.7.0~pre1+git20180411-2ubuntu2.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 | # Copyright (C) 2015 IPA Project Contributors, see COPYING for license
from __future__ import print_function
import errno
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
# pylint: disable=relative-import
from custodia.message.kem import KEMKeysStore
from custodia.message.kem import KEY_USAGE_SIG, KEY_USAGE_ENC, KEY_USAGE_MAP
# pylint: enable=relative-import
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}))'
IPA_CHECK_QUERY = '(cn=enc/{host: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 check_host_keys(self, host):
conn = self.connect()
scope = ldap.SCOPE_SUBTREE
ldap_filter = self.build_filter(IPA_CHECK_QUERY, {'host': host})
r = conn.search_s(self.keysbase, scope, ldap_filter)
if not r:
raise ValueError("No public keys were found for %s" % host)
return True
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 _get_dn(self, usage, principal):
servicename, host = principal.split('@')[0].split('/')
name = '%s/%s' % (KEY_USAGE_MAP[usage], host)
service_rdn = ('cn', servicename) if servicename != 'host' else DN()
return DN(('cn', name), service_rdn, self.keysbase)
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)
dn = self._get_dn(usage, principal)
conn = self.connect()
try:
mods = [('objectClass', [b'nsContainer',
b'ipaKeyPolicy',
b'ipaPublicKeyObject',
b'groupOfPrincipals']),
('cn', dn[0].value.encode('utf-8')),
('ipaKeyUsage', RFC5280_USAGE_MAP[usage].encode('utf-8')),
('memberPrincipal', principal.encode('utf-8')),
('ipaPublicKey', public_key)]
conn.add_s(str(dn), mods)
except ldap.ALREADY_EXISTS:
mods = [(ldap.MOD_REPLACE, 'ipaPublicKey', public_key)]
conn.modify_s(str(dn), mods)
def del_key(self, usage, principal):
"""Delete key for host or service
:returns: DN of removed key or None when key was not found
"""
dn = self._get_dn(usage, principal)
conn = self.connect()
try:
conn.delete_s(str(dn))
except ldap.NO_SUCH_OBJECT:
return None
else:
return dn
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()
self.host = None
self.realm = None
self.ldap_uri = config.get('ldap_uri', None)
if conf.read(ipaconf):
self.host = conf.get('global', 'host')
self.realm = conf.get('global', 'realm')
if self.ldap_uri is None:
self.ldap_uri = conf.get('global', 'ldap_uri', raw=True)
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])
def remove_server_keys_file(self):
"""Remove keys from disk
The method does not fail when the file is missing.
"""
try:
os.unlink(self.config['server_keys'])
except OSError as e:
if e.errno != errno.ENOENT:
raise
return False
else:
return True
def remove_server_keys(self):
"""Remove keys from LDAP and disk
"""
self.remove_keys('host')
def remove_keys(self, servicename):
"""Remove keys from LDAP and disk
"""
self.remove_server_keys_file()
principal = '%s/%s@%s' % (servicename, self.host, self.realm)
if self.ldap_uri is not None:
ldapconn = KEMLdap(self.ldap_uri)
ldapconn.del_key(KEY_USAGE_SIG, principal)
ldapconn.del_key(KEY_USAGE_ENC, principal)
@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))
|