diff --git a/utils/ldapcertfs.py b/utils/ldapcertfs.py new file mode 100755 index 00000000..63351711 --- /dev/null +++ b/utils/ldapcertfs.py @@ -0,0 +1,421 @@ +#!/bin/bash /usr/scripts/python.sh +#-*- coding: utf-8 -*- +# ---------------------------------------------------------------------------- +# Copyright (c) 2010, Matteo Bertozzi +# Copyright (c) 2014, Valentin Samir +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the Matteo Bertozzi nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY Matteo Bertozzi ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL Matteo Bertozzi BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ---------------------------------------------------------------------------- +# How to mount the File-System: +# python ldapcertfs mnt [ldap_filter] +# +# How to umount the File-System? +# fusermount -u mnt + +import os +import sys +import ssl +import stat +import time +import fuse +import errno +import getpass + +from OpenSSL import crypto + +import lc_ldap.shortcuts +import gestion.secrets_new as secrets +from gestion.gen_confs.populate_sshFingerprint import get_machines +# Specify what Fuse API use: 0.2 +fuse.fuse_python_api = (0, 2) + +import logging +LOG_FILENAME = 'ldapcertfs.log' +logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG) + +class Pass: + pass + +class Item(object): + """ + An Item is an Object on Disk, it can be a Directory, File, Symlink, ... + """ + def __init__(self, mode, uid, gid): + # ----------------------------------- Metadata -- + self.atime = time.time() # time of last acces + self.mtime = self.atime # time of last modification + self.ctime = self.atime # time of last status change + + self.dev = 0 # device ID (if special file) + self.mode = mode # protection and file-type + self.uid = uid # user ID of owner + self.gid = gid # group ID of owner + + # ------------------------ Extended Attributes -- + self.xattr = {} + + # --------------------------------------- Data -- + if stat.S_ISDIR(mode): + self.data = set() + else: + self.data = '' + + def read(self, offset, length): + return self.data[offset:offset+length] + + def write(self, offset, data): + length = len(data) + self.data = self.data[:offset] + data + self.data[offset+length:] + return length + + def truncate(self, length): + if len(self.data) > length: + self.data = self.data[:length] + else: + self.data += '\x00'# (length - len(self.data)) + +def zstat(stat): + stat.st_mode = 0 + stat.st_ino = 0 + stat.st_dev = 0 + stat.st_nlink = 2 + stat.st_uid = 0 + stat.st_gid = 0 + stat.st_size = 0 + stat.st_atime = 0 + stat.st_mtime = 0 + stat.st_ctime = 0 + return stat + +class LdapCertFS(fuse.Fuse): + def __init__(self, ldap_filter, nopkey=False, decrypt=False, *args, **kwargs): + fuse.Fuse.__init__(self, *args, **kwargs) + + self.uid = os.getuid() + self.gid = os.getgid() + + self.nopkey=nopkey + self.decrypt = decrypt + self.ldap_filter = ldap_filter + + # dictionnnaire CN => certificat pour construire la chaine de certificat + # Il n'est utile ici que de renseigner des CN de CA intermédiaires + self.chain = { + 'CAcert Class 3 Root' : open('/etc/ssl/certs/cacert-chain.pem').read(), + } + # Les fichers certificats que l'on veux créer. fil est une liste représentant + # la concaténation des attributs ldap (bien formaté). chain est un joker pour + # construire la chaine de certificat + self.files = { + 'crt.pem' : {'file':['certificat'], 'mode':0444}, + 'key.pem' : {'file':['privatekey'], 'mode':0400}, + 'csr.pem' : {'file':['csr'], 'mode':0444}, + 'chain.pem' : {'file':['chain'], 'mode':0444}, + 'key_cert_chain.pem' : {'file':['privatekey', 'certificat', 'chain'], 'mode':0400}, + 'cert_chain.pem' : {'file':['certificat', 'chain'], 'mode':0400}, + } + + self._storage = {'/': Item(0755 | stat.S_IFDIR, self.uid, self.gid)} + + self.build_tree() + + def build_tree(self): + """Construit l'arborescence du système de fichier""" + self._storage = {'/': Item(0755 | stat.S_IFDIR, self.uid, self.gid)} + self.passphrase = {} + conn = lc_ldap.shortcuts.lc_ldap_readonly() + if self.ldap_filter == 'self': + machines = conn.search(u'mid=*', dn=conn.dn, scope=1) + elif self.ldap_filter: + machines = conn.search("(&(%s)(mid=*))" % self.ldap_filter, sizelimit=8000) + else: + machines = get_machines() + + for machine in machines: + if not machine.certificats(): + continue + if 'aid' in machine.dn and "cransAccount" in machine.proprio()['objectClass']: + uid = int(machine.proprio()["uidNumber"][0]) + gid = int(machine.proprio()["gidNumber"][0]) + else: + uid = 0 + gid = 0 + mpath = "/%s" % machine['host'][0] + self._storage[mpath]=Item(0755 | stat.S_IFDIR, uid, gid) + self._add_to_parent_dir(mpath) + for cert in machine.certificats(): + path = '%s/xid=%s' % (mpath, cert["xid"][0]) + self._storage[path]=Item(0755 | stat.S_IFDIR, uid, gid) + self._add_to_parent_dir(path) + for file, file_data in self.files.items(): + data = self._file_data(machine, cert, file) + if data: + fpath = '%s/%s' % (path, file) + self._storage[fpath]=Item(file_data['mode'] | stat.S_IFREG, uid, gid) + self._storage[fpath].data = data + self._add_to_parent_dir(fpath) + + for machine in machines: + if not machine.certificats(): + continue + mpath = "/%s" % machine['host'][0] + for cert in machine.certificats(): + path = 'xid=%s' % (cert["xid"][0]) + if cert['info']: + item = Item(0644 | stat.S_IFLNK, self.uid, self.gid) + item.data = path + newpath = '%s/%s' % (mpath, cert['info'][0]) + indice = 1 + while newpath in self._storage: + newpath = '/%s (%s)' % (cert['info'][0], indice) + indice+=1 + self._storage[newpath] = item + self._add_to_parent_dir(newpath) + + + def _file_data(self, machine, cert, file): + """Construit le contenue du fichier file utilisant le certificat cert de machine""" + data = "" + for dtype in self.files[file]['file']: + if dtype == "chain": + if cert['issuerCN'][0] in self.chain: + data+=self.chain[str(cert['issuerCN'][0])] + else: + return None + elif dtype == "certificat": + data+=ssl.DER_cert_to_PEM_cert(str(cert['certificat'][0])) + elif dtype == "privatekey": + if "privateKey" in cert['objectClass'] and cert['privatekey'] and not self.nopkey: + if self.decrypt: + while True: + if not cert['xid'][0] in self.passphrase: + if cert['encrypted'][0]: + if "machineCrans" in machine["objectClass"]: + passphrase=secrets.get('privatekey_passphrase') + else: + print "Passphrase de la clef %s de %s" % (cert['info'][0] if cert['info'] else ('xid=%s' % cert["xid"][0]), machine['host'][0]) + try: + passphrase = getpass.getpass() + except KeyboardInterrupt: + print "On passe la clef" + self.passphrase[cert['xid'][0]]=Pass() + return None + else: + passphrase=None + self.passphrase[cert['xid'][0]]=passphrase + elif isinstance(self.passphrase[cert['xid'][0]], Pass): + return None + try: + if self.passphrase[cert['xid'][0]]: + pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, str(cert['privatekey'][0]), self.passphrase[cert['xid'][0]]) + else: + pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, str(cert['privatekey'][0]), "") + break + except crypto.Error: + print "mauvais mot de pass" + del(self.passphrase[cert['xid'][0]]) + data+=str(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) + else: + data+=str(cert['privatekey'][0]) + else: + return None + elif cert[dtype]: + data+=str(cert[dtype][0]) + else: + return None + return data + + # --- Metadata ----------------------------------------------------------- + def getattr(self, path): + if not path in self._storage: + return -errno.ENOENT + + # Lookup Item and fill the stat struct + item = self._storage[path] + st = zstat(fuse.Stat()) + st.st_mode = item.mode + st.st_uid = item.uid + st.st_gid = item.gid + st.st_dev = item.dev + st.st_atime = item.atime + st.st_mtime = item.mtime + st.st_ctime = item.ctime + st.st_size = len(item.data) + return st + + def chmod(self, path, mode): + return -errno.EPERM + + def chown(self, path, uid, gid): + return -errno.EPERM + + def utime(self, path, times): + item = self._storage[path] + item.ctime = item.mtime = times[0] + + # --- Namespace ---------------------------------------------------------- + def unlink(self, path): + return -errno.EPERM + + def rename(self, oldpath, newpath): + return -errno.EPERM + + # --- Links -------------------------------------------------------------- + def symlink(self, path, newpath): + return -errno.EPERM + + def readlink(self, path): + return self._storage[path].data + + # --- Extra Attributes --------------------------------------------------- + def setxattr(self, path, name, value, flags): + return -errno.EPERM + + def getxattr(self, path, name, size): + value = self._storage[path].xattr.get(name, '') + if size == 0: # We are asked for size of the value + return len(value) + return value + + def listxattr(self, path, size): + attrs = self._storage[path].xattr.keys() + if size == 0: + return len(attrs) + len(''.join(attrs)) + return attrs + + def removexattr(self, path, name): + return -errno.EPERM + + # --- Files -------------------------------------------------------------- + def mknod(self, path, mode, dev): + return -errno.EPERM + + def create(self, path, flags, mode): + return -errno.EPERM + + def truncate(self, path, len): + return -errno.EPERM + + def read(self, path, size, offset): + return self._storage[path].read(offset, size) + + def write(self, path, buf, offset): + return -errno.EPERM + + # --- Directories -------------------------------------------------------- + def mkdir(self, path, mode): + return -errno.EPERM + + def rmdir(self, path): + return -errno.EPERM + + def readdir(self, path, offset): + dir_items = self._storage[path].data + for item in dir_items: + yield fuse.Direntry(item) + + def _add_to_parent_dir(self, path): + parent_path = os.path.dirname(path) + filename = os.path.basename(path) + self._storage[parent_path].data.add(filename) + + def _remove_from_parent_dir(self, path): + parent_path = os.path.dirname(path) + filename = os.path.basename(path) + self._storage[parent_path].data.remove(filename) + +def main(usage): + # valeurs par defaut des options + decrypt=False + nopkey=False + ldap_filter = 'self' + + # Récupération de l'option decrypt + if '--decrypt' in sys.argv[2:]: + decrypt=True + del(sys.argv[sys.argv.index('--decrypt')]) + + # Récupération de l'option nopkey + if '--nopkey' in sys.argv[2:]: + nopkey=True + del(sys.argv[sys.argv.index('--nopkey')]) + + # Récupération de l'option ldap-filter + try: + ldap_filter = unicode(sys.argv[sys.argv.index('--ldap-filter') + 1]) + if os.getuid() != 0: + raise EnvironmentError("Il faut être root pour choisir le filtre ldap") + del(sys.argv[sys.argv.index('--ldap-filter') + 1]) + except (IndexError, ValueError): + if os.getuid() == 0: + ldap_filter = None + finally: + try: del(sys.argv[sys.argv.index('--ldap-filter')]) + except (IndexError, ValueError): pass + + # Vérification que le point de montage est bien un dossier + end_option = False + for item in sys.argv[1:]: + if end_option or not item.startswith('-'): + if not os.path.isdir(item): + raise EnvironmentError("%s is not a dir" % item) + if item == '--': + end_option=True + + # Instanciation du FS + server = LdapCertFS(ldap_filter, nopkey=nopkey, decrypt=decrypt, version="%prog " + fuse.__version__, + usage=usage, + dash_s_do='setsingle') + + server.parse(errex=1) + server.main() + +if __name__ == '__main__': + usage=""" +LdapCertFS - Ldap Certificate File System + Les obtions spécifiques sont : + * --decrypt : pour déchiffrer les clefs privées (un prompt demande le + mot de passe si nécessaire. + * --nopkey : exclure les clefs privées lors de la construction du FS. + * --ldap-filter filtre : selectionner les machines à utiliser pour + construire le FS avec un filtre ldap. Nécéssite les droits root. + + Si --ldap-filter n'est pas spécifier : + * Si le programme est appelé par root, on utilises les machines + correspondants aux ips des interfaces de la machine physique. + * Sinon, on utilise les machines de l'utilisateur dans la base + de donnée. + +""" + fuse.Fuse.fusage + # On force à fornir au moint un paramètre (il faut au moins un point de montage) + if len(sys.argv)<2: + sys.stderr.write("%s\n" % usage.replace('%prog', sys.argv[0])) + sys.exit(1) + # On appel main et on affiche les exceptions EnvironmentError + try: + main(usage) + except (EnvironmentError) as e: + sys.stderr.write("Error: %s\n" % e) + sys.exit(1) +