#!/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 # dictionnnaire CommonName => certificat_dict pour construire la chaine de certificat # certificat_dict est 'content' => PEM ou 'path' => chemin vers fichier contenent le PEM # Il n'est utile ici que de renseigner des CN de CA intermédiaires CHAIN = { 'CAcert Class 3 Root' : {'path':'/etc/ssl/certs/cacert.org.pem'}, } import os import sys import ssl import stat import ldap import time import fuse import errno import pickle import hashlib import getpass from OpenSSL import crypto import lc_ldap.shortcuts import gestion.secrets_new as secrets # Specify what Fuse API use: 0.2 fuse.fuse_python_api = (0, 2) import logging import logging.handlers logger = logging.getLogger('ldapcertfs') logger.setLevel(logging.DEBUG) formatter = logging.Formatter('%(name)s: [%(levelname)s] %(message)s') handler = logging.handlers.SysLogHandler(address = '/dev/log') try: handler.addFormatter(formatter) except AttributeError: handler.formatter = formatter logger.addHandler(handler) CACHE = {} CACHE_TIMEOUT = 60 CACHE_DIR = "/var/lib/ldapcertfs/" CACHE_STATIC_UPDATE = 3600 CACHE_STATIC_EXPIRE = 3600 * 24 * 30 LAST_CLEAN = 0 try: os.mkdir(CACHE_DIR, 0700) except OSError as e: if e.errno != 17: # File exists raise os.chmod(CACHE_DIR, 0700) def clean_static_cache(storage={}): global LAST_CLEAN if time.time() - LAST_CLEAN > CACHE_STATIC_UPDATE: LAST_CLEAN = time.time() for file in os.listdir(CACHE_DIR): path = os.path.join(CACHE_DIR, file) if time.time() - os.path.getmtime(path) > CACHE_STATIC_EXPIRE and not path in storage: os.remove(path) clean_static_cache() def get_from_static_cache(key): path = os.path.join(CACHE_DIR, hashlib.md5(key).hexdigest()) if os.path.isfile(path): with open(path) as f: return (os.path.getmtime(path), pickle.load(f)) else: raise ValueError("Not Found") def set_to_static_cache(key, value): try: (mtime, old_value) = get_from_static_cache(key) except ValueError: old_value = None if old_value != value: path = os.path.join(CACHE_DIR, hashlib.md5(key).hexdigest()) with open(path, 'w+') as f: pickle.dump(value, f) os.chmod(path, 0600) def set_to_cache(keys, value, now=None, null=False): global CACHE if now is None: now = time.time() if not isinstance(keys, list): keys = [keys] for key in keys: if key or null: CACHE[key] = (now, value) def get_from_cache(key, now=None, expire=CACHE_TIMEOUT): global CACHE if now is None: now = time.time() if key in CACHE: (ts, v) = CACHE[key] if now - ts < expire or expire < 0: return v else: raise ValueError("Expired") else: raise ValueError("Not Found") 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 __eq__(self, obj): if isinstance(obj, Item): return self.dev == obj.dev and \ self.mode == obj.mode and \ self.uid == obj.uid and \ self.gid == obj.gid and \ self.xattr == obj.xattr and \ self.data == obj.data and \ abs(self.mtime - obj.mtime) < CACHE_STATIC_UPDATE else: return False def __ne__(self, obj): return not self == obj 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 = CHAIN # 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':0444}, } self.conn = lc_ldap.shortcuts.lc_ldap_readonly() self._storage = {} self.passphrase = {} def _func_cache(self, func, *args, **kwargs): rec = kwargs.pop('rec', False) kwargs_l = ["%s=%s" % kv for kv in kwargs.items()] kwargs_l.sort() serial = "%s(%s,%s)" % (func.__name__, ", ".join(args), ", ".join(kwargs_l)) try: return get_from_cache(serial) except ValueError: try: objects = func(*args, **kwargs) set_to_cache(serial, objects) return objects except ldap.SERVER_DOWN: try: self.conn = lc_ldap.shortcuts.lc_ldap_readonly() if not rec: logger.warning("ldap down, retrying") kwargs['rec'] = True self.func_cache(func, *args, **kwargs) except Exception as e: logger.error("uncaught exception %r" % e) # Si le serveur est down on essaye de fournir un ancienne valeur du cache try: return get_from_cache(serial, expire=-1) except ValueError: logger.critical("fail to return a valid result, I will probably crash next to this") return [] def search_cache(self, filter, **kwargs): return self._func_cache(self.conn.search, filter, **kwargs) def get_local_machines(self): return self._func_cache(self.conn.get_local_machines) def make_root(self): self._storage['/'] = Item(0755 | stat.S_IFDIR, self.uid, self.gid) if self.ldap_filter == 'self': machines = self.search_cache(u'mid=*', dn=self.conn.dn, scope=1) elif self.ldap_filter: machines = self.search_cache("(&(%s)(mid=*))" % self.ldap_filter, sizelimit=8000) else: machines = self.get_local_machines() for machine in machines: if not machine.certificats(): continue if 'aid' in machine.dn and "cransAccount" in machine.proprio()['objectClass']: self.uid = int(machine.proprio()["uidNumber"][0]) self.gid = int(machine.proprio()["gidNumber"][0]) else: self.uid = 0 self.gid = 0 mpath = "/%s" % machine['host'][0] if not mpath in self._storage: self._storage[mpath]=Item(0755 | stat.S_IFDIR, self.uid, self.gid) self._add_to_parent_dir(mpath) def make_machine(self, hostname): machine = self.search_cache(u"host=%s" % hostname)[0] mpath = '/%s' % machine['host'][0] self._storage[mpath]=Item(0755 | stat.S_IFDIR, self.uid, self.gid) for cert in machine.certificats(refresh=True): xpath = '%s/xid=%s' % (mpath, cert["xid"][0]) if not xpath in self._storage: self._storage[xpath]=Item(0755 | stat.S_IFDIR, self.uid, self.gid) # Si info on met un lien symbolique info => xid=id if cert['info']: item = Item(0644 | stat.S_IFLNK, self.uid, self.gid) item.data = 'xid=%s' % cert["xid"][0] spath = '%s/%s' % (mpath, cert['info'][0]) indice = 1 while spath in self._storage and self._storage[spath].data != item.data: spath = '%s/%s (%s)' % (mpath, cert['info'][0], indice) indice += 1 self._storage[spath] = item self._add_to_parent_dir(spath) self._add_to_parent_dir(xpath) self._add_to_parent_dir(mpath) def make_cert(self, xid): cert = self.search_cache(u"xid=%s" % xid)[0] xpath = '/%s/xid=%s' % (cert.machine()['host'][0], cert["xid"][0]) self._storage[xpath]=Item(0755 | stat.S_IFDIR, self.uid, self.gid) for file, file_data in self.files.items(): self.make_file(xid, file) self._add_to_parent_dir(xpath) def make_file(self, xid, file): cert = self.search_cache(u"xid=%s" % xid)[0] xpath = '/%s/xid=%s' % (cert.machine()['host'][0], cert["xid"][0]) file_data = self.files[file] data = self._file_data(cert.machine(), cert, file) if data: fpath = '%s/%s' % (xpath, file) self._storage[fpath]=Item(file_data['mode'] | stat.S_IFREG, self.uid, self.gid) self._storage[fpath].data = data self._add_to_parent_dir(fpath) 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 'x509Cert' in cert['objectClass'] and cert['issuerCN'][0] in self.chain: if 'path' in self.chain[str(cert['issuerCN'][0])]: data += open(self.chain[str(cert['issuerCN'][0])]['path']).read() elif 'content' in self.chain[str(cert['issuerCN'][0])]: data += self.chain[str(cert['issuerCN'][0])]['content'] else: return None elif dtype == "certificat" and 'x509Cert' in cert['objectClass']: 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 def parse_path(self, path): machine_host = None xid = None file = None paths = path.split('/')[1:] if paths: machine_host = paths[0] paths = paths[1:] if paths: if paths[0].startswith("xid="): xid = paths[0][4:] paths = paths[1:] if paths: file = paths[0] return (machine_host, xid, file) def build_path(self, path): try: get_from_cache(path) except ValueError: if path in self._storage: del self._storage[path] if path == "/": self.make_root() else: (machine_host, xid, file) = self.parse_path(path) if machine_host and not xid and not file: self.make_machine(machine_host) elif machine_host and xid and not file: self.make_cert(xid) elif machine_host and xid and file: self.make_file(xid, file) set_to_cache(path, True) set_to_static_cache(path, self._storage[path]) # --- Metadata ----------------------------------------------------------- def getattr(self, path): try: self.build_path(path) clean_static_cache(self._storage) except Exception as e: logger.warning("Erreur : %s\n%s" % (e, traceback.format_exc())) if not path in self._storage: try: (ts, v) = get_from_static_cache(path) self._storage[path] = v except ValueError: pass 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) if parent_path in self._storage: 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[1:]: decrypt=True del(sys.argv[sys.argv.index('--decrypt')]) # Récupération de l'option nopkey if '--nopkey' in sys.argv[1:]: 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) break 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, fuse.FuseError) as e: sys.stderr.write("Error: %s\n" % e) sys.exit(1)