Custodia's server.keys file contain the private RSA keys for encrypting
and signing Custodia messages. The file was created with permission 644
and is only secured by permission 700 of the directory
/etc/ipa/custodia. The installer and upgrader ensure that the file
has 600.

The server.keys file and all keys are now removed when during
uninstallation of a server, too.

https://bugzilla.redhat.com/show_bug.cgi?id=1353936
https://fedorahosted.org/freeipa/ticket/6015
https://fedorahosted.org/freeipa/ticket/6056
From de8f0f42f84eb5ce5e3efaf4336cbfab17793d21 Mon Sep 17 00:00:00 2001
From: Christian Heimes <chei...@redhat.com>
Date: Fri, 8 Jul 2016 20:06:57 +0200
Subject: [PATCH] Secure permission and cleanup Custodia server.keys

Custodia's server.keys file contain the private RSA keys for encrypting
and signing Custodia messages. The file was created with permission 644
and is only secured by permission 700 of the directory
/etc/ipa/custodia. The installer and upgrader ensure that the file
has 600.

The server.keys file and all keys are now removed when during
uninstallation of a server, too.

https://bugzilla.redhat.com/show_bug.cgi?id=1353936
https://fedorahosted.org/freeipa/ticket/6015
https://fedorahosted.org/freeipa/ticket/6056
---
 ipapython/secrets/kem.py              | 58 ++++++++++++++++++++++++++++++-----
 ipaserver/install/custodiainstance.py | 25 +++++++++++----
 2 files changed, 70 insertions(+), 13 deletions(-)

diff --git a/ipapython/secrets/kem.py b/ipapython/secrets/kem.py
index d45efe8cc4fb63ae9d8c0b2c920fd1f9e5331a9d..a9238e1f7bf8d8cef393ad6b6d997c5cebea13f4 100644
--- a/ipapython/secrets/kem.py
+++ b/ipapython/secrets/kem.py
@@ -15,6 +15,8 @@ from jwcrypto.jwk import JWK
 from ipapython.secrets.common import iSecLdap
 from binascii import unhexlify
 import ldap
+import errno
+import os
 
 
 IPA_REL_BASE_DN = 'cn=custodia,cn=ipa,cn=etc'
@@ -66,7 +68,7 @@ class KEMLdap(iSecLdap):
                                          'princ': principal})
         r = conn.search_s(self.keysbase, scope, ldap_filter)
         if len(r) != 1:
-            raise ValueError("Incorrect number of results (%d) searching for"
+            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)
@@ -139,11 +141,29 @@ class KEMLdap(iSecLdap):
             mods = [(ldap.MOD_REPLACE, 'ipaPublicKey', public_key)]
             conn.modify_s(dn, mods)
 
+    def remove_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 not r:
+            return False
+        for entry in r:
+            dn = r[0][0]
+            conn.delete_s(dn)
+        return True
+
 
 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:
+    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')]
 
@@ -177,6 +197,9 @@ class IPAKEMKeys(KEMKeysStore):
             self.ldap_uri = conf.get('global', 'ldap_uri', None)
         self._server_keys = None
 
+    def get_principal(self, servicename):
+        return '%s/%s@%s' % (servicename, self.host, self.realm)
+
     def find_key(self, kid, usage):
         if kid is None:
             raise TypeError('Key ID is None, should be a SPN')
@@ -187,7 +210,7 @@ class IPAKEMKeys(KEMKeysStore):
         self.generate_keys('host')
 
     def generate_keys(self, servicename):
-        principal = '%s/%s@%s' % (servicename, self.host, self.realm)
+        principal = self.get_principal(servicename)
         # Neutralize the key with read if any
         self._server_keys = None
         # Generate private key and store it
@@ -197,6 +220,23 @@ class IPAKEMKeys(KEMKeysStore):
         ldapconn.set_key(KEY_USAGE_SIG, principal, pubkeys[0])
         ldapconn.set_key(KEY_USAGE_ENC, principal, pubkeys[1])
 
+    def remove_server_keys(self):
+        self.remove_keys('host')
+
+    def remove_keys(self, servicename):
+        principal = self.get_principal(servicename)
+        self._server_keys = None
+        # remove keys from LDAP
+        ldapconn = KEMLdap(self.ldap_uri)
+        ldapconn.remove_key(KEY_USAGE_SIG, principal)
+        ldapconn.remove_key(KEY_USAGE_ENC, principal)
+        # remove server.keys file
+        try:
+            os.unlink(self.config['server_keys'])
+        except OSError as e:
+            if e.errno != errno.ENOENT:
+                raise
+
     @property
     def server_keys(self):
         if self._server_keys is None:
@@ -213,9 +253,13 @@ if __name__ == '__main__':
     IKK = IPAKEMKeys({'paths': '/',
                       'server_keys': '/etc/ipa/custodia/server.keys'})
     IKK.generate_server_keys()
+    principal = IKK.get_principal('host')
+    print(IKK.find_key(principal, KEY_USAGE_SIG))
+
     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))
+    print(IKK.find_key(principal, usage=KEY_USAGE_SIG))
+    print(IKK.find_key(principal, usage=KEY_USAGE_ENC))
+
+    IKK.remove_server_keys()
+    print(IKK.find_key(principal, usage=KEY_USAGE_ENC))
diff --git a/ipaserver/install/custodiainstance.py b/ipaserver/install/custodiainstance.py
index fd30430bbf9c39e7153986999199474cfca60d09..d195d5df7f939ef2a0a9372448b4fb6ae748fd7b 100644
--- a/ipaserver/install/custodiainstance.py
+++ b/ipaserver/install/custodiainstance.py
@@ -15,6 +15,7 @@ from jwcrypto.common import json_decode
 import functools
 import shutil
 import os
+import stat
 import tempfile
 import pwd
 
@@ -47,10 +48,8 @@ class CustodiaInstance(SimpleServiceInstance):
                         LDAP_URI=installutils.realm_to_ldapi_uri(self.realm),
                         UID=httpd_info.pw_uid, GID=httpd_info.pw_gid)
         conf = ipautil.template_file(template, sub_dict)
-        fd = open(self.config_file, "w+")
-        fd.write(conf)
-        fd.flush()
-        fd.close()
+        with open(self.config_file, "w") as f:
+            f.write(conf)
 
     def create_instance(self, dm_password=None):
         suffix = ipautil.realm_to_suffix(self.realm)
@@ -65,14 +64,28 @@ class CustodiaInstance(SimpleServiceInstance):
         sysupgrade.set_upgrade_state('custodia', 'installed', True)
 
     def __gen_keys(self):
-        KeyStore = IPAKEMKeys({'server_keys': self.server_keys,
+        keystore = IPAKEMKeys({'server_keys': self.server_keys,
                                'ldap_uri': self.ldap_uri})
-        KeyStore.generate_server_keys()
+        keystore.generate_server_keys()
+
+    def __remove_keys(self):
+        keystore = IPAKEMKeys({'server_keys': self.server_keys,
+                               'ldap_uri': self.ldap_uri})
+        keystore.remove_server_keys()
 
     def upgrade_instance(self):
         if not sysupgrade.get_upgrade_state("custodia", "installed"):
             root_logger.info("Custodia service is being configured")
             self.create_instance()
+        mode = os.stat(self.server_keys).st_mode
+        if stat.S_IMODE(mode) != 0o600:
+            root_logger.info("Secure server.keys mode")
+            os.chmod(self.server_keys, 0o600)
+
+    def uninstall(self):
+        super(CustodiaInstance, self).uninstall()
+        root_logger.info("Remove Custodia keys")
+        self.__remove_keys()
 
     def create_replica(self, master_host_name):
         suffix = ipautil.realm_to_suffix(self.realm)
-- 
2.7.4

Attachment: signature.asc
Description: OpenPGP digital signature

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to