summaryrefslogtreecommitdiffstats
path: root/ipaserver/dnssec/keysyncer.py
blob: a8dc92354eef051cba83e83f2a4c89c5b1319bda (plain)
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
#
# Copyright (C) 2014  FreeIPA Contributors see COPYING for license
#

import ldap.dn
import os

import dns.name

from ipaplatform.paths import paths
from ipapython import ipautil

from ipaserver.dnssec.syncrepl import SyncReplConsumer
from ipaserver.dnssec.odsmgr import ODSMgr
from ipaserver.dnssec.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
        self.dnssec_zones = set()
        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(self.dnssec_zones)

    # 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(self.dnssec_zones)

    def key_meta_del(self, uuid, dn, oldattrs):
        self.bindmgr.ldap_event('del', uuid, oldattrs)
        self.bindmgr_sync(self.dnssec_zones)
        self.hsm_replica_sync()

    def key_metadata_sync(self, uuid, dn, oldattrs, newattrs):
        self.bindmgr.ldap_event('mod', uuid, newattrs)
        self.bindmgr_sync(self.dnssec_zones)

    def bindmgr_sync(self, dnssec_zones):
        if self.init_done:
            self.bindmgr.sync(dnssec_zones)

    # idnsZone wrapper
    def zone_add(self, uuid, dn, newattrs):
        zone = dns.name.from_text(newattrs['idnsname'][0])
        if self.__is_dnssec_enabled(newattrs):
            self.dnssec_zones.add(zone)
        else:
            self.dnssec_zones.discard(zone)

        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):
        zone = dns.name.from_text(oldattrs['idnsname'][0])
        self.dnssec_zones.discard(zone)

        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, 'ipa-hsm-update'])