summaryrefslogtreecommitdiffstats
path: root/ipapython/dnssec/ldapkeydb.py
diff options
context:
space:
mode:
authorPetr Spacek <pspacek@redhat.com>2014-10-19 17:04:40 +0200
committerMartin Kosek <mkosek@redhat.com>2014-10-21 12:23:03 +0200
commit276e69de874f269f6e9089aebb650a5e0814a626 (patch)
tree829b68e2044ba4fd102b8eedf304f9b036f4c583 /ipapython/dnssec/ldapkeydb.py
parent5556b7f50e2939d0c61d852f2b0dcd82ba2fcf9c (diff)
downloadfreeipa-276e69de874f269f6e9089aebb650a5e0814a626.tar.gz
freeipa-276e69de874f269f6e9089aebb650a5e0814a626.tar.xz
freeipa-276e69de874f269f6e9089aebb650a5e0814a626.zip
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 <jcholast@redhat.com> Reviewed-By: David Kupka <dkupka@redhat.com>
Diffstat (limited to 'ipapython/dnssec/ldapkeydb.py')
-rw-r--r--ipapython/dnssec/ldapkeydb.py351
1 files changed, 351 insertions, 0 deletions
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