mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
ipapython: move dnssec, p11helper and secrets to ipaserver
The dnssec and secrets subpackages and the p11helper module depend on ipaplatform. Move them to ipaserver as they are used only on the server. https://fedorahosted.org/freeipa/ticket/6474 Reviewed-By: Stanislav Laznicka <slaznick@redhat.com>
This commit is contained in:
committed by
Martin Basti
parent
8e5d2c7014
commit
a1f260d021
0
ipaserver/secrets/__init__.py
Normal file
0
ipaserver/secrets/__init__.py
Normal file
109
ipaserver/secrets/client.py
Normal file
109
ipaserver/secrets/client.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# 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 ipaserver.secrets.kem import IPAKEMKeys
|
||||
from ipaserver.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
|
||||
45
ipaserver/secrets/common.py
Normal file
45
ipaserver/secrets/common.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# 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)
|
||||
228
ipaserver/secrets/kem.py
Normal file
228
ipaserver/secrets/kem.py
Normal file
@@ -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))
|
||||
261
ipaserver/secrets/store.py
Normal file
261
ipaserver/secrets/store.py
Normal file
@@ -0,0 +1,261 @@
|
||||
# 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 ipaserver.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
|
||||
Reference in New Issue
Block a user