From 276e69de874f269f6e9089aebb650a5e0814a626 Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Sun, 19 Oct 2014 17:04:40 +0200 Subject: DNSSEC: add ipa dnssec daemons Tickets: https://fedorahosted.org/freeipa/ticket/3801 https://fedorahosted.org/freeipa/ticket/4417 Design: https://fedorahosted.org/bind-dyndb-ldap/wiki/BIND9/Design/DNSSEC Reviewed-By: Jan Cholasta Reviewed-By: David Kupka --- ipapython/dnssec/__init__.py | 0 ipapython/dnssec/abshsm.py | 187 ++++++++++++++++++++++ ipapython/dnssec/bindmgr.py | 176 +++++++++++++++++++++ ipapython/dnssec/keysyncer.py | 181 ++++++++++++++++++++++ ipapython/dnssec/ldapkeydb.py | 351 ++++++++++++++++++++++++++++++++++++++++++ ipapython/dnssec/localhsm.py | 229 +++++++++++++++++++++++++++ ipapython/dnssec/odsmgr.py | 194 +++++++++++++++++++++++ ipapython/dnssec/syncrepl.py | 123 +++++++++++++++ ipapython/dnssec/temp.py | 23 +++ ipapython/setup.py.in | 2 +- 10 files changed, 1465 insertions(+), 1 deletion(-) create mode 100644 ipapython/dnssec/__init__.py create mode 100644 ipapython/dnssec/abshsm.py create mode 100644 ipapython/dnssec/bindmgr.py create mode 100644 ipapython/dnssec/keysyncer.py create mode 100644 ipapython/dnssec/ldapkeydb.py create mode 100755 ipapython/dnssec/localhsm.py create mode 100644 ipapython/dnssec/odsmgr.py create mode 100644 ipapython/dnssec/syncrepl.py create mode 100644 ipapython/dnssec/temp.py (limited to 'ipapython') diff --git a/ipapython/dnssec/__init__.py b/ipapython/dnssec/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ipapython/dnssec/abshsm.py b/ipapython/dnssec/abshsm.py new file mode 100644 index 000000000..cc8119865 --- /dev/null +++ b/ipapython/dnssec/abshsm.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +import _ipap11helper + +attrs_id2name = { + #_ipap11helper.CKA_ALLOWED_MECHANISMS: 'ipk11allowedmechanisms', + _ipap11helper.CKA_ALWAYS_AUTHENTICATE: 'ipk11alwaysauthenticate', + _ipap11helper.CKA_ALWAYS_SENSITIVE: 'ipk11alwayssensitive', + #_ipap11helper.CKA_CHECK_VALUE: 'ipk11checkvalue', + _ipap11helper.CKA_COPYABLE: 'ipk11copyable', + _ipap11helper.CKA_DECRYPT: 'ipk11decrypt', + _ipap11helper.CKA_DERIVE: 'ipk11derive', + #_ipap11helper.CKA_DESTROYABLE: 'ipk11destroyable', + _ipap11helper.CKA_ENCRYPT: 'ipk11encrypt', + #_ipap11helper.CKA_END_DATE: 'ipk11enddate', + _ipap11helper.CKA_EXTRACTABLE: 'ipk11extractable', + _ipap11helper.CKA_ID: 'ipk11id', + #_ipap11helper.CKA_KEY_GEN_MECHANISM: 'ipk11keygenmechanism', + _ipap11helper.CKA_KEY_TYPE: 'ipk11keytype', + _ipap11helper.CKA_LABEL: 'ipk11label', + _ipap11helper.CKA_LOCAL: 'ipk11local', + _ipap11helper.CKA_MODIFIABLE: 'ipk11modifiable', + _ipap11helper.CKA_NEVER_EXTRACTABLE: 'ipk11neverextractable', + _ipap11helper.CKA_PRIVATE: 'ipk11private', + #_ipap11helper.CKA_PUBLIC_KEY_INFO: 'ipapublickey', + #_ipap11helper.CKA_PUBLIC_KEY_INFO: 'ipk11publickeyinfo', + _ipap11helper.CKA_SENSITIVE: 'ipk11sensitive', + _ipap11helper.CKA_SIGN: 'ipk11sign', + _ipap11helper.CKA_SIGN_RECOVER: 'ipk11signrecover', + #_ipap11helper.CKA_START_DATE: 'ipk11startdate', + #_ipap11helper.CKA_SUBJECT: 'ipk11subject', + _ipap11helper.CKA_TRUSTED: 'ipk11trusted', + _ipap11helper.CKA_UNWRAP: 'ipk11unwrap', + #_ipap11helper.CKA_UNWRAP_TEMPLATE: 'ipk11unwraptemplate', + _ipap11helper.CKA_VERIFY: 'ipk11verify', + _ipap11helper.CKA_VERIFY_RECOVER: 'ipk11verifyrecover', + _ipap11helper.CKA_WRAP: 'ipk11wrap', + #_ipap11helper.CKA_WRAP_TEMPLATE: 'ipk11wraptemplate', + _ipap11helper.CKA_WRAP_WITH_TRUSTED: 'ipk11wrapwithtrusted', +} + +attrs_name2id = dict(zip(attrs_id2name.values(), attrs_id2name.keys())) + +# attribute: +# http://www.freeipa.org/page/V4/PKCS11_in_LDAP/Schema#ipk11KeyType +# +# mapping table: +# http://www.freeipa.org/page/V4/PKCS11_in_LDAP/Schema#CK_MECHANISM_TYPE +keytype_name2id = { + "rsa": _ipap11helper.KEY_TYPE_RSA, + "aes": _ipap11helper.KEY_TYPE_AES, + } + +keytype_id2name = dict(zip(keytype_name2id.values(), keytype_name2id.keys())) + +wrappingmech_name2id = { + "rsaPkcs": _ipap11helper.MECH_RSA_PKCS, + "rsaPkcsOaep": _ipap11helper.MECH_RSA_PKCS_OAEP, + "aesKeyWrap": _ipap11helper.MECH_AES_KEY_WRAP, + "aesKeyWrapPad": _ipap11helper.MECH_AES_KEY_WRAP_PAD + } + +wrappingmech_id2name = dict(zip(wrappingmech_name2id.values(), + wrappingmech_name2id.keys())) + + +bool_attr_names = set([ + 'ipk11alwaysauthenticate', + 'ipk11alwayssensitive', + 'ipk11copyable', + 'ipk11decrypt', + 'ipk11derive', + 'ipk11encrypt', + 'ipk11extractable', + 'ipk11local', + 'ipk11modifiable', + 'ipk11neverextractable', + 'ipk11private', + 'ipk11sensitive', + 'ipk11sign', + 'ipk11signrecover', + 'ipk11trusted', + 'ipk11unwrap', + 'ipk11verify', + 'ipk11verifyrecover', + 'ipk11wrap', + 'ipk11wrapwithtrusted', +]) + +modifiable_attrs_id2name = { + _ipap11helper.CKA_DECRYPT: 'ipk11decrypt', + _ipap11helper.CKA_DERIVE: 'ipk11derive', + _ipap11helper.CKA_ENCRYPT: 'ipk11encrypt', + _ipap11helper.CKA_EXTRACTABLE: 'ipk11extractable', + _ipap11helper.CKA_ID: 'ipk11id', + _ipap11helper.CKA_LABEL: 'ipk11label', + _ipap11helper.CKA_SENSITIVE: 'ipk11sensitive', + _ipap11helper.CKA_SIGN: 'ipk11sign', + _ipap11helper.CKA_SIGN_RECOVER: 'ipk11signrecover', + _ipap11helper.CKA_UNWRAP: 'ipk11unwrap', + _ipap11helper.CKA_VERIFY: 'ipk11verify', + _ipap11helper.CKA_VERIFY_RECOVER: 'ipk11verifyrecover', + _ipap11helper.CKA_WRAP: 'ipk11wrap', +} + +modifiable_attrs_name2id = dict(zip(modifiable_attrs_id2name.values(), + modifiable_attrs_id2name.keys())) + +def sync_pkcs11_metadata(log, source, target): + """sync ipk11 metadata from source object to target object""" + + # iterate over list of modifiable PKCS#11 attributes - this prevents us + # from attempting to set read-only attributes like CKA_LOCAL + for attr in modifiable_attrs_name2id: + if attr in source: + if source[attr] != target[attr]: + log.debug('Updating attribute %s from "%s" to "%s"', attr, repr(source[attr]), repr(target[attr])) + target[attr] = source[attr] + +def populate_pkcs11_metadata(source, target): + """populate all ipk11 metadata attributes in target object from source object""" + for attr in attrs_name2id: + if attr in source: + target[attr] = source[attr] + +def ldap2p11helper_api_params(ldap_key): + """prepare dict with metadata parameters suitable for key unwrapping""" + unwrap_params = {} + + # some attributes are just renamed + direct_param_map = { + "ipk11label": "label", + "ipk11id": "id", + "ipk11copyable": "cka_copyable", + "ipk11decrypt": "cka_decrypt", + "ipk11derive": "cka_derive", + "ipk11encrypt": "cka_encrypt", + "ipk11extractable": "cka_extractable", + "ipk11modifiable": "cka_modifiable", + "ipk11private": "cka_private", + "ipk11sensitive": "cka_sensitive", + "ipk11sign": "cka_sign", + "ipk11unwrap": "cka_unwrap", + "ipk11verify": "cka_verify", + "ipk11wrap": "cka_wrap", + "ipk11wrapwithtrusted": "cka_wrap_with_trusted" + } + + for ldap_name, p11h_name in direct_param_map.iteritems(): + if ldap_name in ldap_key: + unwrap_params[p11h_name] = ldap_key[ldap_name] + + # and some others needs conversion + + indirect_param_map = { + "ipk11keytype": ("key_type", keytype_name2id), + "ipawrappingmech": ("wrapping_mech", wrappingmech_name2id), + } + + for ldap_name, rules in indirect_param_map.iteritems(): + p11h_name, mapping = rules + if ldap_name in ldap_key: + unwrap_params[p11h_name] = mapping[ldap_key[ldap_name]] + + return unwrap_params + + +class AbstractHSM(object): + def _filter_replica_keys(self, all_keys): + replica_keys = {} + for key_id, key in all_keys.iteritems(): + if not key['ipk11label'].startswith('dnssec-replica:'): + continue + replica_keys[key_id] = key + return replica_keys + + def _filter_zone_keys(self, all_keys): + zone_keys = {} + for key_id, key in all_keys.iteritems(): + if key['ipk11label'] == u'dnssec-master' \ + or key['ipk11label'].startswith('dnssec-replica:'): + continue + zone_keys[key_id] = key + return zone_keys diff --git a/ipapython/dnssec/bindmgr.py b/ipapython/dnssec/bindmgr.py new file mode 100644 index 000000000..55765e16b --- /dev/null +++ b/ipapython/dnssec/bindmgr.py @@ -0,0 +1,176 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +from datetime import datetime +import dns.name +import errno +import os +import logging +import shutil +import stat +import subprocess + +from ipalib import api +import ipalib.constants +from ipapython.dn import DN +from ipapython import ipa_log_manager, ipautil +from ipaplatform.paths import paths + +from temp import TemporaryDirectory + +time_bindfmt = '%Y%m%d%H%M%S' + +# this daemon should run under ods:named user:group +# user has to be ods because ODSMgr.py sends signal to ods-enforcerd +FILE_PERM = (stat.S_IRUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IWUSR) +DIR_PERM = (stat.S_IRWXU | stat.S_IRWXG) + +class BINDMgr(object): + """BIND key manager. It does LDAP->BIND key files synchronization. + + One LDAP object with idnsSecKey object class will produce + single pair of BIND key files. + """ + def __init__(self, api): + self.api = api + self.log = ipa_log_manager.log_mgr.get_logger(self) + self.ldap_keys = {} + self.modified_zones = set() + + def notify_zone(self, zone): + cmd = ['rndc', 'sign', zone.to_text()] + output = ipautil.run(cmd)[0] + self.log.info(output) + + def dn2zone_name(self, dn): + """cn=KSK-20140813162153Z-cede9e182fc4af76c4bddbc19123a565,cn=keys,idnsname=test,cn=dns,dc=ipa,dc=example""" + # verify that metadata object is under DNS sub-tree + dn = DN(dn) + container = DN(self.api.env.container_dns, self.api.env.basedn) + idx = dn.rfind(container) + assert idx != -1, 'Metadata object %s is not inside %s' % (dn, container) + assert len(dn[idx - 1]) == 1, 'Multi-valued RDN as zone name is not supported' + return dns.name.from_text(dn[idx - 1]['idnsname']) + + def time_ldap2bindfmt(self, str_val): + dt = datetime.strptime(str_val, ipalib.constants.LDAP_GENERALIZED_TIME_FORMAT) + return dt.strftime(time_bindfmt) + + def dates2params(self, ldap_attrs): + attr2param = {'idnsseckeypublish': '-P', + 'idnsseckeyactivate': '-A', + 'idnsseckeyinactive': '-I', + 'idnsseckeydelete': '-D'} + + params = [] + for attr, param in attr2param.items(): + if attr in ldap_attrs: + params.append(param) + assert len(ldap_attrs[attr]) == 1, 'Timestamp %s is expected to be single-valued' % attr + params.append(self.time_ldap2bindfmt(ldap_attrs[attr][0])) + + return params + + def ldap_event(self, op, uuid, attrs): + """Record single LDAP event - key addition, deletion or modification. + + Change is only recorded to memory. + self.sync() has to be called to synchronize change to BIND.""" + assert op == 'add' or op == 'del' or op == 'mod' + zone = self.dn2zone_name(attrs['dn']) + self.modified_zones.add(zone) + zone_keys = self.ldap_keys.setdefault(zone, {}) + if op == 'add': + self.log.info('Key metadata %s added to zone %s' % (attrs['dn'], zone)) + zone_keys[uuid] = attrs + + elif op == 'del': + self.log.info('Key metadata %s deleted from zone %s' % (attrs['dn'], zone)) + zone_keys.pop(uuid) + + elif op == 'mod': + self.log.info('Key metadata %s updated in zone %s' % (attrs['dn'], zone)) + zone_keys[uuid] = attrs + + def install_key(self, zone, uuid, attrs, workdir): + """Run dnssec-keyfromlabel on given LDAP object. + :returns: base file name of output files, e.g. Kaaa.test.+008+19719""" + self.log.info('attrs: %s', attrs) + assert attrs.get('idnsseckeyzone', ['FALSE'])[0] == 'TRUE', \ + 'object %s is not a DNS zone key' % attrs['dn'] + + uri = "%s;pin-source=%s" % (attrs['idnsSecKeyRef'][0], paths.DNSSEC_SOFTHSM_PIN) + cmd = [paths.DNSSEC_KEYFROMLABEL, '-K', workdir, '-a', attrs['idnsSecAlgorithm'][0], '-l', uri] + cmd += self.dates2params(attrs) + if attrs.get('idnsSecKeySep', ['FALSE'])[0].upper() == 'TRUE': + cmd += ['-f', 'KSK'] + if attrs.get('idnsSecKeyRevoke', ['FALSE'])[0].upper() == 'TRUE': + cmd += ['-R', datetime.now().strftime(time_bindfmt)] + cmd.append(zone.to_text()) + + # keys has to be readable by ODS & named + basename = ipautil.run(cmd)[0].strip() + private_fn = "%s/%s.private" % (workdir, basename) + os.chmod(private_fn, FILE_PERM) + # this is useful mainly for debugging + with open("%s/%s.uuid" % (workdir, basename), 'w') as uuid_file: + uuid_file.write(uuid) + with open("%s/%s.dn" % (workdir, basename), 'w') as dn_file: + dn_file.write(attrs['dn']) + + def sync_zone(self, zone): + self.log.info('Synchronizing zone %s' % zone) + zone_path = os.path.join(paths.BIND_LDAP_DNS_ZONE_WORKDIR, + zone.to_text(omit_final_dot=True)) + try: + os.makedirs(zone_path) + except OSError as e: + if e.errno != errno.EEXIST: + raise e + + # fix HSM permissions + # TODO: move out + for prefix, dirs, files in os.walk(paths.DNSSEC_TOKENS_DIR, topdown=True): + for name in dirs: + fpath = os.path.join(prefix, name) + self.log.debug('Fixing directory permissions: %s', fpath) + os.chmod(fpath, DIR_PERM | stat.S_ISGID) + for name in files: + fpath = os.path.join(prefix, name) + self.log.debug('Fixing file permissions: %s', fpath) + os.chmod(fpath, FILE_PERM) + # TODO: move out + + with TemporaryDirectory(zone_path) as tempdir: + for uuid, attrs in self.ldap_keys[zone].items(): + self.install_key(zone, uuid, attrs, tempdir) + # keys were generated in a temporary directory, swap directories + target_dir = "%s/keys" % zone_path + try: + shutil.rmtree(target_dir) + except OSError as e: + if e.errno != errno.ENOENT: + raise e + shutil.move(tempdir, target_dir) + os.chmod(target_dir, DIR_PERM) + + self.notify_zone(zone) + + def sync(self): + """Synchronize list of zones in LDAP with BIND.""" + self.log.debug('Key metadata in LDAP: %s' % self.ldap_keys) + for zone in self.modified_zones: + self.sync_zone(zone) + + self.modified_zones = set() + + def diff_zl(self, s1, s2): + """Compute zones present in s1 but not present in s2. + + Returns: List of (uuid, name) tuples with zones present only in s1.""" + s1_extra = s1.uuids - s2.uuids + removed = [(uuid, name) for (uuid, name) in s1.mapping.items() + if uuid in s1_extra] + return removed diff --git a/ipapython/dnssec/keysyncer.py b/ipapython/dnssec/keysyncer.py new file mode 100644 index 000000000..1b2757326 --- /dev/null +++ b/ipapython/dnssec/keysyncer.py @@ -0,0 +1,181 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +import logging +import ldap.dn +import os + +from ipaplatform.paths import paths +from ipapython import ipautil + +from syncrepl import SyncReplConsumer +from odsmgr import ODSMgr +from bindmgr import BINDMgr + +SIGNING_ATTR = 'idnsSecInlineSigning' +OBJCLASS_ATTR = 'objectClass' + + +class KeySyncer(SyncReplConsumer): + def __init__(self, *args, **kwargs): + # hack + self.api = kwargs['ipa_api'] + del kwargs['ipa_api'] + + # DNSSEC master should have OpenDNSSEC installed + # TODO: Is this the best way? + if os.environ.get('ISMASTER', '0') == '1': + self.ismaster = True + self.odsmgr = ODSMgr() + else: + self.ismaster = False + + self.bindmgr = BINDMgr(self.api) + self.init_done = False + SyncReplConsumer.__init__(self, *args, **kwargs) + + def _get_objclass(self, attrs): + """Get object class. + + Given set of attributes has to have exactly one supported object class. + """ + supported_objclasses = set(['idnszone', 'idnsseckey', 'ipk11publickey']) + present_objclasses = set([o.lower() for o in attrs[OBJCLASS_ATTR]]).intersection(supported_objclasses) + assert len(present_objclasses) == 1, attrs[OBJCLASS_ATTR] + return present_objclasses.pop() + + def __get_signing_attr(self, attrs): + """Get SIGNING_ATTR from dictionary with LDAP zone attributes. + + Returned value is normalized to TRUE or FALSE, defaults to FALSE.""" + values = attrs.get(SIGNING_ATTR, ['FALSE']) + assert len(values) == 1, '%s is expected to be single-valued' \ + % SIGNING_ATTR + return values[0].upper() + + def __is_dnssec_enabled(self, attrs): + """Test if LDAP DNS zone with given attributes is DNSSEC enabled.""" + return self.__get_signing_attr(attrs) == 'TRUE' + + def __is_replica_pubkey(self, attrs): + vals = attrs.get('ipk11label', []) + if len(vals) != 1: + return False + return vals[0].startswith('dnssec-replica:') + + def application_add(self, uuid, dn, newattrs): + objclass = self._get_objclass(newattrs) + if objclass == 'idnszone': + self.zone_add(uuid, dn, newattrs) + elif objclass == 'idnsseckey': + self.key_meta_add(uuid, dn, newattrs) + elif objclass == 'ipk11publickey' and \ + self.__is_replica_pubkey(newattrs): + self.hsm_master_sync() + + def application_del(self, uuid, dn, oldattrs): + objclass = self._get_objclass(oldattrs) + if objclass == 'idnszone': + self.zone_del(uuid, dn, oldattrs) + elif objclass == 'idnsseckey': + self.key_meta_del(uuid, dn, oldattrs) + elif objclass == 'ipk11publickey' and \ + self.__is_replica_pubkey(oldattrs): + self.hsm_master_sync() + + def application_sync(self, uuid, dn, newattrs, oldattrs): + objclass = self._get_objclass(oldattrs) + if objclass == 'idnszone': + olddn = ldap.dn.str2dn(oldattrs['dn']) + newdn = ldap.dn.str2dn(newattrs['dn']) + assert olddn == newdn, 'modrdn operation is not supported' + + oldval = self.__get_signing_attr(oldattrs) + newval = self.__get_signing_attr(newattrs) + if oldval != newval: + if self.__is_dnssec_enabled(newattrs): + self.zone_add(uuid, olddn, newattrs) + else: + self.zone_del(uuid, olddn, oldattrs) + + elif objclass == 'idnsseckey': + self.key_metadata_sync(uuid, dn, oldattrs, newattrs) + + elif objclass == 'ipk11publickey' and \ + self.__is_replica_pubkey(newattrs): + self.hsm_master_sync() + + def syncrepl_refreshdone(self): + self.log.info('Initial LDAP dump is done, sychronizing with ODS and BIND') + self.init_done = True + self.ods_sync() + self.hsm_replica_sync() + self.hsm_master_sync() + self.bindmgr.sync() + + # idnsSecKey wrapper + # Assumption: metadata points to the same key blob all the time, + # i.e. it is not necessary to re-download blobs because of change in DNSSEC + # metadata - DNSSEC flags or timestamps. + def key_meta_add(self, uuid, dn, newattrs): + self.hsm_replica_sync() + self.bindmgr.ldap_event('add', uuid, newattrs) + self.bindmgr_sync() + + def key_meta_del(self, uuid, dn, oldattrs): + self.bindmgr.ldap_event('del', uuid, oldattrs) + self.bindmgr_sync() + self.hsm_replica_sync() + + def key_metadata_sync(self, uuid, dn, oldattrs, newattrs): + self.bindmgr.ldap_event('mod', uuid, newattrs) + self.bindmgr_sync() + + def bindmgr_sync(self): + if self.init_done: + self.bindmgr.sync() + + # idnsZone wrapper + def zone_add(self, uuid, dn, newattrs): + if not self.ismaster: + return + + if self.__is_dnssec_enabled(newattrs): + self.odsmgr.ldap_event('add', uuid, newattrs) + self.ods_sync() + + def zone_del(self, uuid, dn, oldattrs): + if not self.ismaster: + return + + if self.__is_dnssec_enabled(oldattrs): + self.odsmgr.ldap_event('del', uuid, oldattrs) + self.ods_sync() + + def ods_sync(self): + if not self.ismaster: + return + + if self.init_done: + self.odsmgr.sync() + + # triggered by modification to idnsSecKey objects + def hsm_replica_sync(self): + """Download keys from LDAP to local HSM.""" + if self.ismaster: + return + if not self.init_done: + return + ipautil.run([paths.IPA_DNSKEYSYNCD_REPLICA]) + + # triggered by modification to ipk11PublicKey objects + def hsm_master_sync(self): + """Download replica keys from LDAP to local HSM + & upload master and zone keys to LDAP.""" + if not self.ismaster: + return + if not self.init_done: + return + ipautil.run([paths.ODS_SIGNER]) diff --git a/ipapython/dnssec/ldapkeydb.py b/ipapython/dnssec/ldapkeydb.py new file mode 100644 index 000000000..e2e58f880 --- /dev/null +++ b/ipapython/dnssec/ldapkeydb.py @@ -0,0 +1,351 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +from binascii import hexlify +import collections +import sys +import time + +import ipalib +from ipapython.dn import DN +from ipapython import ipaldap +from ipapython import ipautil +from ipaserver.plugins.ldap2 import ldap2 +from ipaplatform.paths import paths + +from abshsm import attrs_name2id, attrs_id2name, bool_attr_names, populate_pkcs11_metadata, AbstractHSM +import _ipap11helper +import uuid + +def uri_escape(val): + """convert val to %-notation suitable for ID component in URI""" + assert len(val) > 0, "zero-length URI component detected" + hexval = hexlify(val) + out = '%' + out += '%'.join(hexval[i:i+2] for i in range(0, len(hexval), 2)) + return out + +def ldap_bool(val): + if val == 'TRUE' or val is True: + return True + elif val == 'FALSE' or val is False: + return False + else: + raise AssertionError('invalid LDAP boolean "%s"' % val) + +def get_default_attrs(object_classes): + # object class -> default attribute values mapping + defaults = { + u'ipk11publickey': { + 'ipk11copyable': True, + 'ipk11derive': False, + 'ipk11encrypt': False, + 'ipk11local': True, + 'ipk11modifiable': True, + 'ipk11private': True, + 'ipk11trusted': False, + 'ipk11verify': True, + 'ipk11verifyrecover': True, + 'ipk11wrap': False + }, + u'ipk11privatekey': { + 'ipk11alwaysauthenticate': False, + 'ipk11alwayssensitive': True, + 'ipk11copyable': True, + 'ipk11decrypt': False, + 'ipk11derive': False, + 'ipk11extractable': True, + 'ipk11local': True, + 'ipk11modifiable': True, + 'ipk11neverextractable': False, + 'ipk11private': True, + 'ipk11sensitive': True, + 'ipk11sign': True, + 'ipk11signrecover': True, + 'ipk11unwrap': False, + 'ipk11wrapwithtrusted': False + }, + u'ipk11secretkey': { + 'ipk11alwaysauthenticate': False, + 'ipk11alwayssensitive': True, + 'ipk11copyable': True, + 'ipk11decrypt': False, + 'ipk11derive': False, + 'ipk11encrypt': False, + 'ipk11extractable': True, + 'ipk11local': True, + 'ipk11modifiable': True, + 'ipk11neverextractable': False, + 'ipk11private': True, + 'ipk11sensitive': True, + 'ipk11sign': False, + 'ipk11trusted': False, + 'ipk11unwrap': True, + 'ipk11verify': False, + 'ipk11wrap': True, + 'ipk11wrapwithtrusted': False + } + } + + # get set of supported object classes + present_clss = set() + for cls in object_classes: + present_clss.add(cls.lower()) + present_clss.intersection_update(set(defaults.keys())) + if len(present_clss) <= 0: + raise AssertionError('none of "%s" object classes are supported' % + object_classes) + + result = {} + for cls in present_clss: + result.update(defaults[cls]) + return result + +class Key(collections.MutableMapping): + """abstraction to hide LDAP entry weirdnesses: + - non-normalized attribute names + - boolean attributes returned as strings + """ + def __init__(self, entry, ldap, ldapkeydb): + self.entry = entry + self.ldap = ldap + self.ldapkeydb = ldapkeydb + self.log = ldap.log.getChild(__name__) + + def __getitem__(self, key): + val = self.entry.single_value[key] + if key.lower() in bool_attr_names: + val = ldap_bool(val) + return val + + def __setitem__(self, key, value): + self.entry[key] = value + + def __delitem__(self, key): + del self.entry[key] + + def __iter__(self): + """generates list of ipa names of all PKCS#11 attributes present in the object""" + for ipa_name in self.entry.keys(): + lowercase = ipa_name.lower() + if lowercase in attrs_name2id: + yield lowercase + + def __len__(self): + return len(self.entry) + + def __str__(self): + return str(self.entry) + + def _cleanup_key(self): + """remove default values from LDAP entry""" + default_attrs = get_default_attrs(self.entry['objectclass']) + empty = object() + for attr in default_attrs: + if self.get(attr, empty) == default_attrs[attr]: + del self[attr] + +class ReplicaKey(Key): + # TODO: object class assert + def __init__(self, entry, ldap, ldapkeydb): + super(ReplicaKey, self).__init__(entry, ldap, ldapkeydb) + +class MasterKey(Key): + # TODO: object class assert + def __init__(self, entry, ldap, ldapkeydb): + super(MasterKey, self).__init__(entry, ldap, ldapkeydb) + + @property + def wrapped_entries(self): + """LDAP entires with wrapped data + + One entry = one blob + ipaWrappingKey pointer to unwrapping key""" + + keys = [] + if 'ipaSecretKeyRef' not in self.entry: + return keys + + for dn in self.entry['ipaSecretKeyRef']: + try: + obj = self.ldap.get_entry(dn) + keys.append(obj) + except ipalib.errors.NotFound: + continue + + return keys + + def add_wrapped_data(self, data, wrapping_mech, replica_key_id): + wrapping_key_uri = 'pkcs11:id=%s;type=public' \ + % uri_escape(replica_key_id) + # TODO: replace this with 'autogenerate' to prevent collisions + uuid_rdn = DN('ipk11UniqueId=%s' % uuid.uuid1()) + entry_dn = DN(uuid_rdn, self.ldapkeydb.base_dn) + # TODO: add ipaWrappingMech attribute + entry = self.ldap.make_entry(entry_dn, + objectClass=['ipaSecretKeyObject', 'ipk11Object'], + ipaSecretKey=data, + ipaWrappingKey=wrapping_key_uri, + ipaWrappingMech=wrapping_mech) + + self.log.info('adding master key 0x%s wrapped with replica key 0x%s to %s', + hexlify(self['ipk11id']), + hexlify(replica_key_id), + entry_dn) + self.ldap.add_entry(entry) + if 'ipaSecretKeyRef' not in self.entry: + self.entry['objectClass'] += ['ipaSecretKeyRefObject'] + self.entry.setdefault('ipaSecretKeyRef', []).append(entry_dn) + + +class LdapKeyDB(AbstractHSM): + def __init__(self, log, ldap, base_dn): + self.ldap = ldap + self.base_dn = base_dn + self.log = log + self.cache_replica_pubkeys_wrap = None + self.cache_masterkeys = None + self.cache_zone_keypairs = None + + def _get_key_dict(self, key_type, ldap_filter): + try: + objs = self.ldap.get_entries(base_dn=self.base_dn, + filter=ldap_filter) + except ipalib.errors.NotFound: + return {} + + keys = {} + for o in objs: + # add default values not present in LDAP + key = key_type(o, self.ldap, self) + default_attrs = get_default_attrs(key.entry['objectclass']) + for attr in default_attrs: + key.setdefault(attr, default_attrs[attr]) + + assert 'ipk11id' in o, 'key is missing ipk11Id in %s' % key.entry.dn + key_id = key['ipk11id'] + assert key_id not in keys, 'duplicate ipk11Id=0x%s in "%s" and "%s"' % (hexlify(key_id), key.entry.dn, keys[key_id].entry.dn) + assert 'ipk11label' in key, 'key "%s" is missing ipk11Label' % key.entry.dn + assert 'objectclass' in key.entry, 'key "%s" is missing objectClass attribute' % key.entry.dn + + keys[key_id] = key + + self._update_keys() + return keys + + def _update_key(self, key): + """remove default values from LDAP entry and write back changes""" + key._cleanup_key() + + try: + self.ldap.update_entry(key.entry) + except ipalib.errors.EmptyModlist: + pass + + def _update_keys(self): + for cache in [self.cache_masterkeys, self.cache_replica_pubkeys_wrap, + self.cache_zone_keypairs]: + if cache: + for key in cache.itervalues(): + self._update_key(key) + + def flush(self): + """write back content of caches to LDAP""" + self._update_keys() + self.cache_masterkeys = None + self.cache_replica_pubkeys_wrap = None + self.cache_zone_keypairs = None + + def _import_keys_metadata(self, source_keys): + """import key metadata from Key-compatible objects + + metadata from multiple source keys can be imported into single LDAP + object + + :param: source_keys is iterable of (Key object, PKCS#11 object class)""" + + entry_dn = DN('ipk11UniqueId=autogenerate', self.base_dn) + entry = self.ldap.make_entry(entry_dn, objectClass=['ipk11Object']) + new_key = Key(entry, self.ldap, self) + + for source_key, pkcs11_class in source_keys: + if pkcs11_class == _ipap11helper.KEY_CLASS_SECRET_KEY: + entry['objectClass'].append('ipk11SecretKey') + elif pkcs11_class == _ipap11helper.KEY_CLASS_PUBLIC_KEY: + entry['objectClass'].append('ipk11PublicKey') + elif pkcs11_class == _ipap11helper.KEY_CLASS_PRIVATE_KEY: + entry['objectClass'].append('ipk11PrivateKey') + else: + raise AssertionError('unsupported object class %s' % pkcs11_class) + + populate_pkcs11_metadata(source_key, new_key) + new_key._cleanup_key() + return new_key + + def import_master_key(self, mkey): + new_key = self._import_keys_metadata( + [(mkey, _ipap11helper.KEY_CLASS_SECRET_KEY)]) + self.ldap.add_entry(new_key.entry) + self.log.debug('imported master key metadata: %s', new_key.entry) + + def import_zone_key(self, pubkey, pubkey_data, privkey, + privkey_wrapped_data, wrapping_mech, master_key_id): + new_key = self._import_keys_metadata( + [(pubkey, _ipap11helper.KEY_CLASS_PUBLIC_KEY), + (privkey, _ipap11helper.KEY_CLASS_PRIVATE_KEY)]) + + new_key.entry['objectClass'].append('ipaPrivateKeyObject') + new_key.entry['ipaPrivateKey'] = privkey_wrapped_data + new_key.entry['ipaWrappingKey'] = 'pkcs11:id=%s;type=secret-key' \ + % uri_escape(master_key_id) + new_key.entry['ipaWrappingMech'] = wrapping_mech + + new_key.entry['objectClass'].append('ipaPublicKeyObject') + new_key.entry['ipaPublicKey'] = pubkey_data + + self.ldap.add_entry(new_key.entry) + self.log.debug('imported zone key id: 0x%s', hexlify(new_key['ipk11id'])) + + @property + def replica_pubkeys_wrap(self): + if self.cache_replica_pubkeys_wrap: + return self.cache_replica_pubkeys_wrap + + keys = self._filter_replica_keys( + self._get_key_dict(ReplicaKey, + '(&(objectClass=ipk11PublicKey)(ipk11Wrap=TRUE)(objectClass=ipaPublicKeyObject))')) + + self.cache_replica_pubkeys_wrap = keys + return keys + + @property + def master_keys(self): + if self.cache_masterkeys: + return self.cache_masterkeys + + keys = self._get_key_dict(MasterKey, + '(&(objectClass=ipk11SecretKey)(|(ipk11UnWrap=TRUE)(!(ipk11UnWrap=*)))(ipk11Label=dnssec-master))') + for key in keys.itervalues(): + prefix = 'dnssec-master' + assert key['ipk11label'] == prefix, \ + 'secret key dn="%s" ipk11id=0x%s ipk11label="%s" with ipk11UnWrap = TRUE does not have '\ + '"%s" key label' % ( + key.entry.dn, + hexlify(key['ipk11id']), + str(key['ipk11label']), + prefix) + + self.cache_masterkeys = keys + return keys + + @property + def zone_keypairs(self): + if self.cache_zone_keypairs: + return self.cache_zone_keypairs + + self.cache_zone_keypairs = self._filter_zone_keys( + self._get_key_dict(Key, + '(&(objectClass=ipk11PrivateKey)(objectClass=ipaPrivateKeyObject)(objectClass=ipk11PublicKey)(objectClass=ipaPublicKeyObject))')) + + return self.cache_zone_keypairs diff --git a/ipapython/dnssec/localhsm.py b/ipapython/dnssec/localhsm.py new file mode 100755 index 000000000..de49641b0 --- /dev/null +++ b/ipapython/dnssec/localhsm.py @@ -0,0 +1,229 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +from binascii import hexlify +import collections +import logging +import os +from pprint import pprint +import sys +import time + +from ipaplatform.paths import paths + +import _ipap11helper +from abshsm import attrs_name2id, attrs_id2name, AbstractHSM, keytype_id2name, keytype_name2id, ldap2p11helper_api_params + +private_key_api_params = set(["label", "id", "data", "unwrapping_key", + "wrapping_mech", "key_type", "cka_always_authenticate", "cka_copyable", + "cka_decrypt", "cka_derive", "cka_extractable", "cka_modifiable", + "cka_private", "cka_sensitive", "cka_sign", "cka_sign_recover", + "cka_unwrap", "cka_wrap_with_trusted"]) + +public_key_api_params = set(["label", "id", "data", "cka_copyable", + "cka_derive", "cka_encrypt", "cka_modifiable", "cka_private", + "cka_trusted", "cka_verify", "cka_verify_recover", "cka_wrap"]) + +class Key(collections.MutableMapping): + def __init__(self, p11, handle): + self.p11 = p11 + self.handle = handle + # sanity check CKA_ID and CKA_LABEL + try: + cka_id = self.p11.get_attribute(handle, _ipap11helper.CKA_ID) + assert len(cka_id) != 0, 'ipk11id length should not be 0' + except _ipap11helper.NotFound: + raise _ipap11helper.NotFound('key without ipk11id: handle %s' % handle) + + try: + cka_label = self.p11.get_attribute(handle, _ipap11helper.CKA_LABEL) + assert len(cka_label) != 0, 'ipk11label length should not be 0' + + except _ipap11helper.NotFound: + raise _ipap11helper.NotFound('key without ipk11label: id 0x%s' + % hexlify(cka_id)) + + def __getitem__(self, key): + key = key.lower() + try: + value = self.p11.get_attribute(self.handle, attrs_name2id[key]) + if key == 'ipk11keytype': + value = keytype_id2name[value] + return value + except _ipap11helper.NotFound: + raise KeyError() + + def __setitem__(self, key, value): + key = key.lower() + if key == 'ipk11keytype': + value = keytype_name2id[value] + + return self.p11.set_attribute(self.handle, attrs_name2id[key], value) + + def __delitem__(self, key): + raise _ipap11helper.Exception('__delitem__ is not supported') + + def __iter__(self): + """generates list of ipa names of all attributes present in the object""" + for pkcs11_id, ipa_name in attrs_id2name.iteritems(): + try: + self.p11.get_attribute(self.handle, pkcs11_id) + except _ipap11helper.NotFound: + continue + + yield ipa_name + + def __len__(self): + cnt = 0 + for attr in self: + cnt += 1 + return cnt + + def __str__(self): + d = {} + for ipa_name, value in self.iteritems(): + d[ipa_name] = value + + return str(d) + + def __repr__(self): + return self.__str__() + +class LocalHSM(AbstractHSM): + def __init__(self, library, slot, pin): + self.cache_replica_pubkeys = None + self.p11 = _ipap11helper.P11_Helper(slot, pin, library) + self.log = logging.getLogger() + + def __del__(self): + self.p11.finalize() + + def find_keys(self, **kwargs): + """Return dict with Key objects matching given criteria. + + CKA_ID is used as key so all matching objects have to have unique ID.""" + + # this is a hack for old p11-kit URI parser + # see https://bugs.freedesktop.org/show_bug.cgi?id=85057 + if 'uri' in kwargs: + kwargs['uri'] = kwargs['uri'].replace('type=', 'object-type=') + + handles = self.p11.find_keys(**kwargs) + keys = {} + for h in handles: + key = Key(self.p11, h) + o_id = key['ipk11id'] + assert o_id not in keys, 'duplicate ipk11Id = 0x%s; keys = %s' % ( + hexlify(o_id), keys) + keys[o_id] = key + + return keys + + @property + def replica_pubkeys(self): + return self._filter_replica_keys( + self.find_keys(objclass=_ipap11helper.KEY_CLASS_PUBLIC_KEY)) + + @property + def replica_pubkeys_wrap(self): + return self._filter_replica_keys( + self.find_keys(objclass=_ipap11helper.KEY_CLASS_PUBLIC_KEY, + cka_wrap=True)) + + @property + def master_keys(self): + """Get all usable DNSSEC master keys""" + keys = self.find_keys(objclass=_ipap11helper.KEY_CLASS_SECRET_KEY, label=u'dnssec-master', cka_unwrap=True) + + for key in keys.itervalues(): + prefix = 'dnssec-master' + assert key['ipk11label'] == prefix, \ + 'secret key ipk11id=0x%s ipk11label="%s" with ipk11UnWrap = TRUE does not have '\ + '"%s" key label' % (hexlify(key['ipk11id']), + str(key['ipk11label']), prefix) + + return keys + + @property + def active_master_key(self): + """Get one active DNSSEC master key suitable for key wrapping""" + keys = self.find_keys(objclass=_ipap11helper.KEY_CLASS_SECRET_KEY, + label=u'dnssec-master', cka_wrap=True, cka_unwrap=True) + assert len(keys) > 0, "DNSSEC master key with UN/WRAP = TRUE not found" + return keys.popitem()[1] + + @property + def zone_pubkeys(self): + return self._filter_zone_keys( + self.find_keys(objclass=_ipap11helper.KEY_CLASS_PUBLIC_KEY)) + + @property + def zone_privkeys(self): + return self._filter_zone_keys( + self.find_keys(objclass=_ipap11helper.KEY_CLASS_PRIVATE_KEY)) + + + def import_public_key(self, source, data): + params = ldap2p11helper_api_params(source) + # filter out params inappropriate for public keys + for par in set(params.keys()).difference(public_key_api_params): + del params[par] + params['data'] = data + + h = self.p11.import_public_key(**params) + return Key(self.p11, h) + + def import_private_key(self, source, data, unwrapping_key): + params = ldap2p11helper_api_params(source) + # filter out params inappropriate for private keys + for par in set(params.keys()).difference(private_key_api_params): + del params[par] + params['data'] = data + params['unwrapping_key'] = unwrapping_key.handle + + h = self.p11.import_wrapped_private_key(**params) + return Key(self.p11, h) + + + +if __name__ == '__main__': + if 'SOFTHSM2_CONF' not in os.environ: + os.environ['SOFTHSM2_CONF'] = paths.DNSSEC_SOFTHSM2_CONF + localhsm = LocalHSM(paths.LIBSOFTHSM2_SO, 0, + open(paths.DNSSEC_SOFTHSM_PIN).read()) + + print 'replica public keys: CKA_WRAP = TRUE' + print '====================================' + for pubkey_id, pubkey in localhsm.replica_pubkeys_wrap.iteritems(): + print hexlify(pubkey_id) + pprint(pubkey) + + print '' + print 'replica public keys: all' + print '========================' + for pubkey_id, pubkey in localhsm.replica_pubkeys.iteritems(): + print hexlify(pubkey_id) + pprint(pubkey) + + print '' + print 'master keys' + print '===========' + for mkey_id, mkey in localhsm.master_keys.iteritems(): + print hexlify(mkey_id) + pprint(mkey) + + print '' + print 'zone public keys' + print '================' + for key_id, key in localhsm.zone_pubkeys.iteritems(): + print hexlify(key_id) + pprint(key) + + print '' + print 'zone private keys' + print '=================' + for key_id, key in localhsm.zone_privkeys.iteritems(): + print hexlify(key_id) + pprint(key) diff --git a/ipapython/dnssec/odsmgr.py b/ipapython/dnssec/odsmgr.py new file mode 100644 index 000000000..a91b6c553 --- /dev/null +++ b/ipapython/dnssec/odsmgr.py @@ -0,0 +1,194 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +import logging +from lxml import etree +import dns.name +import subprocess + +from ipapython import ipa_log_manager, ipautil + +# hack: zone object UUID is stored as path to imaginary zone file +ENTRYUUID_PREFIX = "/var/lib/ipa/dns/zone/entryUUID/" +ENTRYUUID_PREFIX_LEN = len(ENTRYUUID_PREFIX) + + +class ZoneListReader(object): + def __init__(self): + self.names = set() # dns.name + self.uuids = set() # UUID strings + self.mapping = dict() # {UUID: dns.name} + self.log = ipa_log_manager.log_mgr.get_logger(self) + + def _add_zone(self, name, zid): + """Add zone & UUID to internal structures. + + Zone with given name and UUID must not exist.""" + # detect duplicate zone names + name = dns.name.from_text(name) + assert name not in self.names, \ + 'duplicate name (%s, %s) vs. %s' % (name, zid, self.mapping) + # duplicate non-None zid is not allowed + assert not zid or zid not in self.uuids, \ + 'duplicate UUID (%s, %s) vs. %s' % (name, zid, self.mapping) + + self.names.add(name) + self.uuids.add(zid) + self.mapping[zid] = name + + def _del_zone(self, name, zid): + """Remove zone & UUID from internal structures. + + Zone with given name and UUID must exist. + """ + name = dns.name.from_text(name) + assert zid is not None + assert name in self.names, \ + 'name (%s, %s) does not exist in %s' % (name, zid, self.mapping) + assert zid in self.uuids, \ + 'UUID (%s, %s) does not exist in %s' % (name, zid, self.mapping) + assert zid in self.mapping and name == self.mapping[zid], \ + 'pair {%s: %s} does not exist in %s' % (zid, name, self.mapping) + + self.names.remove(name) + self.uuids.remove(zid) + del self.mapping[zid] + + +class ODSZoneListReader(ZoneListReader): + """One-shot parser for ODS zonelist.xml.""" + def __init__(self, zonelist_text): + super(ODSZoneListReader, self).__init__() + xml = etree.fromstring(zonelist_text) + self._parse_zonelist(xml) + + def _parse_zonelist(self, xml): + """iterate over Zone elements with attribute 'name' and + add IPA zones to self.zones""" + for zone_xml in xml.xpath('/ZoneList/Zone[@name]'): + name, zid = self._parse_ipa_zone(zone_xml) + self._add_zone(name, zid) + + def _parse_ipa_zone(self, zone_xml): + """Extract zone name, input adapter and detect IPA zones. + + IPA zones have contains Adapters/Input/Adapter element with + attribute type = "File" and with value prefixed with ENTRYUUID_PREFIX. + + Returns: + tuple (zone name, ID) + """ + name = zone_xml.get('name') + in_adapters = zone_xml.xpath( + 'Adapters/Input/Adapter[@type="File" ' + 'and starts-with(text(), "%s")]' % ENTRYUUID_PREFIX) + assert len(in_adapters) == 1, 'only IPA zones are supported: %s' \ + % etree.tostring(zone_xml) + + path = in_adapters[0].text + # strip prefix from path + zid = path[ENTRYUUID_PREFIX_LEN:] + return (name, zid) + + +class LDAPZoneListReader(ZoneListReader): + def __init__(self): + super(LDAPZoneListReader, self).__init__() + + def process_ipa_zone(self, op, uuid, zone_ldap): + assert (op == 'add' or op == 'del'), 'unsupported op %s' % op + assert uuid is not None + assert 'idnsname' in zone_ldap, \ + 'LDAP zone UUID %s without idnsName' % uuid + assert len(zone_ldap['idnsname']) == 1, \ + 'LDAP zone UUID %s with len(idnsname) != 1' % uuid + + if op == 'add': + self._add_zone(zone_ldap['idnsname'][0], uuid) + elif op == 'del': + self._del_zone(zone_ldap['idnsname'][0], uuid) + + +class ODSMgr(object): + """OpenDNSSEC zone manager. It does LDAP->ODS synchronization. + + Zones with idnsSecInlineSigning attribute = TRUE in LDAP are added + or deleted from ODS as necessary. ODS->LDAP key synchronization + has to be solved seperatelly. + """ + def __init__(self): + self.log = ipa_log_manager.log_mgr.get_logger(self) + self.zl_ldap = LDAPZoneListReader() + + def ksmutil(self, params): + """Call ods-ksmutil with given parameters and return stdout. + + Raises CalledProcessError if returncode != 0. + """ + cmd = ['ods-ksmutil'] + params + return ipautil.run(cmd)[0] + + def get_ods_zonelist(self): + stdout = self.ksmutil(['zonelist', 'export']) + reader = ODSZoneListReader(stdout) + return reader + + def add_ods_zone(self, uuid, name): + zone_path = '%s%s' % (ENTRYUUID_PREFIX, uuid) + cmd = ['zone', 'add', '--zone', str(name), '--input', zone_path] + output = self.ksmutil(cmd) + self.log.info(output) + self.notify_enforcer() + + def del_ods_zone(self, name): + # ods-ksmutil blows up if zone name has period at the end + name = name.relativize(dns.name.root) + cmd = ['zone', 'delete', '--zone', str(name)] + output = self.ksmutil(cmd) + self.log.info(output) + self.notify_enforcer() + + def notify_enforcer(self): + cmd = ['notify'] + output = self.ksmutil(cmd) + self.log.info(output) + + def ldap_event(self, op, uuid, attrs): + """Record single LDAP event - zone addition or deletion. + + Change is only recorded to memory. + self.sync() have to be called to synchronize change to ODS.""" + assert op == 'add' or op == 'del' + self.zl_ldap.process_ipa_zone(op, uuid, attrs) + self.log.debug("LDAP zones: %s", self.zl_ldap.mapping) + + def sync(self): + """Synchronize list of zones in LDAP with ODS.""" + zl_ods = self.get_ods_zonelist() + self.log.debug("ODS zones: %s", zl_ods.mapping) + removed = self.diff_zl(zl_ods, self.zl_ldap) + self.log.info("Zones removed from LDAP: %s", removed) + added = self.diff_zl(self.zl_ldap, zl_ods) + self.log.info("Zones added to LDAP: %s", added) + for (uuid, name) in removed: + self.del_ods_zone(name) + for (uuid, name) in added: + self.add_ods_zone(uuid, name) + + def diff_zl(self, s1, s2): + """Compute zones present in s1 but not present in s2. + + Returns: List of (uuid, name) tuples with zones present only in s1.""" + s1_extra = s1.uuids - s2.uuids + removed = [(uuid, name) for (uuid, name) in s1.mapping.items() + if uuid in s1_extra] + return removed + + +if __name__ == '__main__': + ipa_log_manager.standard_logging_setup(debug=True) + ods = ODSMgr() + reader = ods.get_ods_zonelist() + ipa_log_manager.root_logger.info('ODS zones: %s', reader.mapping) diff --git a/ipapython/dnssec/syncrepl.py b/ipapython/dnssec/syncrepl.py new file mode 100644 index 000000000..2f657f599 --- /dev/null +++ b/ipapython/dnssec/syncrepl.py @@ -0,0 +1,123 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# +""" +This script implements a syncrepl consumer which syncs data from server +to a local dict. +""" + +# Import the python-ldap modules +import ldap +import ldapurl +# Import specific classes from python-ldap +from ldap.cidict import cidict +from ldap.ldapobject import ReconnectLDAPObject +from ldap.syncrepl import SyncreplConsumer + +# Import modules from Python standard lib +import signal +import time +import sys +import logging + +from ipapython import ipa_log_manager + + +class SyncReplConsumer(ReconnectLDAPObject, SyncreplConsumer): + """ + Syncrepl Consumer interface + """ + + def __init__(self, *args, **kwargs): + self.log = ipa_log_manager.log_mgr.get_logger(self) + # Initialise the LDAP Connection first + ldap.ldapobject.ReconnectLDAPObject.__init__(self, *args, **kwargs) + # Now prepare the data store + self.__data = cidict() + self.__data['uuids'] = cidict() + # We need this for later internal use + self.__presentUUIDs = cidict() + + def close_db(self): + # This is useless for dict + pass + + def syncrepl_get_cookie(self): + if 'cookie' in self.__data: + cookie = self.__data['cookie'] + self.log.debug('Current cookie is: %s', cookie) + return cookie + else: + self.log.debug('Current cookie is: None (not received yet)') + + def syncrepl_set_cookie(self, cookie): + self.log.debug('New cookie is: %s', cookie) + self.__data['cookie'] = cookie + + def syncrepl_entry(self, dn, attributes, uuid): + attributes = cidict(attributes) + # First we determine the type of change we have here + # (and store away the previous data for later if needed) + previous_attributes = cidict() + if uuid in self.__data['uuids']: + change_type = 'modify' + previous_attributes = self.__data['uuids'][uuid] + else: + change_type = 'add' + # Now we store our knowledge of the existence of this entry + # (including the DN as an attribute for convenience) + attributes['dn'] = dn + self.__data['uuids'][uuid] = attributes + # Debugging + self.log.debug('Detected %s of entry: %s %s', change_type, dn, uuid) + if change_type == 'modify': + self.application_sync(uuid, dn, attributes, previous_attributes) + else: + self.application_add(uuid, dn, attributes) + + def syncrepl_delete(self, uuids): + # Make sure we know about the UUID being deleted, just in case... + uuids = [uuid for uuid in uuids if uuid in self.__data['uuids']] + # Delete all the UUID values we know of + for uuid in uuids: + attributes = self.__data['uuids'][uuid] + dn = attributes['dn'] + self.log.debug('Detected deletion of entry: %s %s', dn, uuid) + self.application_del(uuid, dn, attributes) + del self.__data['uuids'][uuid] + + def syncrepl_present(self, uuids, refreshDeletes=False): + # If we have not been given any UUID values, + # then we have recieved all the present controls... + if uuids is None: + # We only do things if refreshDeletes is false + # as the syncrepl extension will call syncrepl_delete instead + # when it detects a delete notice + if refreshDeletes is False: + deletedEntries = [uuid for uuid in self.__data['uuids'].keys() + if uuid not in self.__presentUUIDs] + self.syncrepl_delete(deletedEntries) + # Phase is now completed, reset the list + self.__presentUUIDs = {} + else: + # Note down all the UUIDs we have been sent + for uuid in uuids: + self.__presentUUIDs[uuid] = True + + def application_add(self, uuid, dn, attributes): + self.log.info('Performing application add for: %s %s', dn, uuid) + self.log.debug('New attributes: %s', attributes) + return True + + def application_sync(self, uuid, dn, attributes, previous_attributes): + self.log.info('Performing application sync for: %s %s', dn, uuid) + self.log.debug('Old attributes: %s', previous_attributes) + self.log.debug('New attributes: %s', attributes) + return True + + def application_del(self, uuid, dn, previous_attributes): + self.log.info('Performing application delete for: %s %s', dn, uuid) + self.log.debug('Old attributes: %s', previous_attributes) + return True diff --git a/ipapython/dnssec/temp.py b/ipapython/dnssec/temp.py new file mode 100644 index 000000000..23ee377be --- /dev/null +++ b/ipapython/dnssec/temp.py @@ -0,0 +1,23 @@ +#!/usr/bin/python +# +# Copyright (C) 2014 FreeIPA Contributors see COPYING for license +# + +import errno +import shutil +import tempfile + +class TemporaryDirectory(object): + def __init__(self, root): + self.root = root + + def __enter__(self): + self.name = tempfile.mkdtemp(dir=self.root) + return self.name + + def __exit__(self, exc_type, exc_value, traceback): + try: + shutil.rmtree(self.name) + except OSError as e: + if e.errno != errno.ENOENT: + raise diff --git a/ipapython/setup.py.in b/ipapython/setup.py.in index a839f094a..6caf17905 100644 --- a/ipapython/setup.py.in +++ b/ipapython/setup.py.in @@ -65,7 +65,7 @@ def setup_package(): classifiers=filter(None, CLASSIFIERS.split('\n')), platforms = ["Linux", "Solaris", "Unix"], package_dir = {'ipapython': ''}, - packages = [ "ipapython" ], + packages = [ "ipapython", "ipapython.dnssec" ], ) finally: del sys.path[0] -- cgit