creole/creole/cert.py

638 lines
24 KiB
Python

# -*- coding: utf-8 -*-
###########################################################################
#
# Eole NG - 2007
# Copyright Pole de Competence Eole (Ministere Education - Academie Dijon)
# Licence CeCill cf /root/LicenceEole.txt
# eole@ac-dijon.fr
#
# libsecure.py
#
# classes utilitaires pour lancement des services en https
#
###########################################################################
"""
points d'entrée de l'api
- gen_certif -> génère **un** certif
- gen_certs -> génère tous les certifs
cf creole/doc/certifs.txt
"""
# certains imports sont utilisés dans les fragments de code installés
# dans /usr/share/eole/certs
from os.path import join, splitext, basename, dirname, isdir, isfile, islink, exists, realpath
from os import unlink, symlink, stat
import os, glob, time
from shutil import copy
from subprocess import Popen, PIPE
from OpenSSL import SSL
import re
from .i18n import _
# chemin du certificat eole par défaut
from .config import cert_file, key_file, SSL_LAST_FILE
from .client import CreoleClient
from pyeole.process import system_out, system_code
client = CreoleClient()
global regexp_get_subject
regexp_get_subject = None
def prep_dir() :
"""
Création de l'arborescence pour openssl
"""
#on génère le random
load_default_conf_if_needed()
rand_file = os.path.join(ssl_dir, ".rand")
if not os.path.isfile(rand_file) :
cmd_random = "/bin/dd if=/dev/urandom of=%s bs=1k count=16 >/dev/null 2>&1" % (rand_file)
cmd = Popen(cmd_random, shell=True)
res = cmd.wait()
if res != 0:
raise Exception(_(u"! Error while generating entropy file !"))
#on crée les fichiers pour gerer la pki
file_serial = os.path.join(ssl_dir, "serial")
if not os.path.isfile(file_serial) :
f = file(file_serial, "w")
f.write(str(start_index))
f.close()
file_index = os.path.join(ssl_dir, "index.txt")
if not os.path.isfile(file_index) :
f = file(file_index, "w")
f.close()
newcerts = os.path.join(ssl_dir, "newcerts")
if not os.path.isdir(newcerts):
os.makedirs(newcerts)
if not os.path.isdir(key_dir):
os.makedirs(key_dir)
if not os.path.isdir(cert_dir):
os.makedirs(cert_dir)
if not os.path.isdir(req_dir):
os.makedirs(req_dir)
if not os.path.isdir(local_ca_dir):
os.makedirs(local_ca_dir)
##cmd = Popen("chmod 611 %s" % (key_dir), shell=True)
dhfile = os.path.join(ssl_dir, "dh")
if not os.path.isfile(dhfile):
gen_dh = '/usr/bin/openssl dhparam -out "%s" 1024 >/dev/null 2>&1' % (dhfile)
Popen(gen_dh, shell=True)
def sup_passwd(tmp_keyfile, keyfile) :
"""
Supression de la passphrase sur la clef privée
"""
load_default_conf_if_needed()
key_cmd = '/usr/bin/openssl rsa -in "%s" -passin pass:secret -out "%s" >/dev/null 2>&1' % (tmp_keyfile, keyfile)
cmd = Popen(key_cmd, shell=True)
res = cmd.wait()
if res != 0:
raise Exception(_(u'! Error while generating ssl key in {0} !').format(keyfile))
def finalise_cert (certfile, keyfile, key_user='', key_grp='', key_chmod='',
cert_user='', cert_grp='', cert_chmod=''):
"""
Finalisation du certif
"""
load_default_conf_if_needed()
if key_user != '':
try:
res = Popen("chown %s %s" % (key_user, keyfile), shell=True).wait()
assert res == 0
except:
print _(u"\n! Rights on {0} can't be modified").format(keyfile)
return False
if key_grp != '':
try:
res=Popen("/bin/chgrp %s %s" % (key_grp, keyfile), shell=True).wait()
assert res == 0
except:
print _(u"\n! Rights on {0} can't be modified").format(keyfile)
return False
if key_chmod != '':
try:
res = Popen("/bin/chmod %s %s" % (key_chmod, keyfile), shell=True).wait()
assert res == 0
except:
print _(u"\n! Rights on {0} can't be modified").format(keyfile)
return False
if cert_user != '':
try:
res = Popen("/bin/chown %s %s" % (cert_user, certfile), shell=True).wait()
assert res == 0
except:
print _(u"\n! Rights on {0} can't be modified").format(certfile)
return False
if cert_grp != '':
try:
res = Popen("/bin/chgrp %s %s" % (cert_grp, certfile), shell=True).wait()
assert res == 0
except:
print _(u"\n! Rights on {0} can't be modified").format(certfile)
return False
if cert_chmod != '':
try:
res = Popen("/bin/chmod %s %s" % (cert_chmod, certfile), shell=True).wait()
assert res == 0
except:
print _(u"\n! Rights on {0} can't be modified").format(certfile)
return False
return True
def is_simple_cert(cert_file):
"""
Teste si le fichier contient un simple certificat ou une chaîne.
:param cert_file: chemin du fichier à tester
:type cert_file: str
"""
with open(cert_file, 'r') as pem:
cert_num = len(re.findall(r'-+BEGIN CERTIFICATE-+', pem.read()))
return cert_num == 1
def get_certs_catalog(simple=True):
"""
Créer un dictionnaire des certificats présents
pour accélérer la reconstitution de la chaîne
de certificats intermédiaires.
:param simple: filtre sur les certificats à référencer
:type simple: booléen
"""
global certs_catalog
certs_catalog = {}
for cert_file in glob.glob(os.path.join(ssl_dir, 'certs/*')):
try:
if simple and is_simple_cert(cert_file):
certs_catalog[get_subject(certfile=cert_file)] = cert_file
elif not simple:
certs_catalog[get_subject(certfile=cert_file)] = cert_file
except:
continue
return certs_catalog
def get_certs_chain(certs):
"""
Récupération de la chaîne de certificats
:param certs: liste des certificats dans l'ordre de la chaîne.
:type certs: liste de chemins
"""
global certs_catalog, ca_issuer
load_default_conf_if_needed()
subject = get_subject(certfile=certs[-1])
issuer = get_issuer_subject(certfile=certs[-1])
if ca_issuer is None:
ca_issuer = get_issuer_subject(certfile=ca_file)
if subject == issuer:
pass
elif issuer == ca_issuer:
certs.append(ca_file)
else:
try:
if certs_catalog is None:
certs_catalog = get_certs_catalog()
certs.append(certs_catalog[issuer])
get_certs_chain(certs)
except KeyError as e:
print _(u"Certificate chain incomplete.")
return certs
def get_intermediate_certs(cert):
"""
Récupération de la liste des certificats intermédiaires.
:param cert: chemin du certificat pour lequel on reconstitue la chaîne
:type cert:
"""
load_default_conf_if_needed()
try:
chain = get_certs_chain([cert,])[1:-1]
except:
chain = []
return chain
def concat_fic(dst_fic, in_fics, overwrite=False, need_link=True):
"""
Concaténation d'une liste de fichiers dans un fichier de destination
(le contenu d'origine est conservé)
"""
load_default_conf_if_needed()
if need_link:
remove_link(dst_fic)
if type(in_fics) != list:
in_fics = [in_fics]
for fic in in_fics:
if not os.path.isfile(fic):
print _(u"Error: file {0} does not exist").format(fic)
data = ""
for fic_src in in_fics:
f_src = file(fic_src)
data += f_src.read().rstrip() + '\n'
f_src.close()
if overwrite:
f_dst = file(dst_fic, "w")
else:
f_dst = file(dst_fic, "a+")
f_dst.write(data)
f_dst.close()
if need_link:
build_link(dst_fic, in_fics)
def gen_certs(regen=False, merge=True):
"""
Génère la ca puis les certificats
"""
load_default_conf_if_needed()
verif_ca()
ca_generated = gen_ca(regen)
if merge:
merge_ca()
if ca_generated:
regen = True
certif_loader(regen=regen)
def verif_ca():
"""
vérifie que la ca est générée correctement (serial > 0xstart_index) et cn valide
"""
load_default_conf_if_needed()
# gestion des anciennes version de ca.crt
if os.path.isfile(ca_dest_file) and not os.path.isfile(ca_file):
# on reprend le premier certificat présent dans ca.crt dans ca_local.crt
ca_certs = open(ca_dest_file).read().strip()
tag_begin = '-----BEGIN CERTIFICATE-----'
try:
ca_data = tag_begin + ca_certs.split(tag_begin)[1]
local_ca = open(ca_file, 'w')
local_ca.write(ca_data)
local_ca.close()
except IndexError:
# impossible de reprendre la ca actuelle, elle sera regénérée
pass
serial = int(eval('0x%s'%start_index))
# vérification de la valeur actuelle du ca
# vérification du cn de la ca
if os.path.isfile(ca_file):
cmd = Popen(['/usr/bin/openssl', 'x509', '-in', ca_file, '-subject', '-noout'], stdout=PIPE)
if cmd.wait() != 0:
unlink(ca_file)
prep_dir()
if os.path.isfile(file_serial):
serial = open(file_serial).read().strip()
# conversion en hexa
serial = int(serial, 16)
if serial < min_serial:
if os.path.isfile(ca_file):
unlink(ca_file)
unlink(file_serial)
for f_index in glob.glob(os.path.join(ssl_dir, 'index*')):
unlink(f_index)
for f_cert in glob.glob(os.path.join(newcerts_dir, '*.pem')):
unlink(f_cert)
prep_dir()
def gen_ca(regen=False, del_passwd=True, extensions="SERVEUR"):
"""
Generation ca
"""
load_default_conf_if_needed()
generated = False
prep_dir()
if not os.path.isfile(ca_conf_file):
raise Exception(_(u"Certificate configuration template can not be found:\n\t{0}\n").format(ca_conf_file))
if regen or (not os.path.isfile(ca_keyfile)) or (not os.path.isfile(ca_file)):
print("* " + _(u"Generating CA certificate"))
remove_link(ca_file)
## On genère le certif de l'ac
ca_gen = '/usr/bin/openssl req -x509 -config %s -newkey rsa:%s -days %s -keyout "%s" -out "%s" -extensions %s >/dev/null 2>&1' % (ca_conf_file, ssl_default_key_bits, ssl_default_cert_time, tmp_keyfile, ca_file, extensions)
cmd = Popen(ca_gen, shell=True)
if cmd.wait() != 0:
raise Exception(_(u"Error while generating CA"))
if del_passwd:
sup_passwd(tmp_keyfile, ca_keyfile)
if os.path.isfile(tmp_keyfile):
unlink(tmp_keyfile)
generated = True
## application des droits
finalise_cert(ca_file, ca_keyfile, key_chmod='600')
build_link(ca_file)
## génération d'une crl
if not os.path.isfile(os.path.join(ssl_dir, 'eole.crl')):
print(_(u"Generating certificate revocation list (CRL)"))
crl_gen = '/usr/bin/openssl ca -gencrl -config %s -crldays %s -out %s/eole.crl >/dev/null 2>&1' % (ca_conf_file, ssl_default_cert_time, ssl_dir)
cmd = Popen(crl_gen, shell=True)
if cmd.wait() != 0:
raise Exception(_(u"Error while generating CRL ({0}/eole.crl)").format(ssl_dir))
return generated
def merge_ca():
"""
concatène toutes les ca utiles dans ca.crt
"""
load_default_conf_if_needed()
## concaténation des certificats education
ca_list = [ca_file, os.path.join(cert_dir, 'ACInfraEducation.pem')]
## concaténation de certificats supplémentaires si définis
for ca_perso in glob.glob(os.path.join(local_ca_dir,'*.*')):
if os.path.isfile(ca_perso):
ca_list.append(ca_perso)
concat_fic(ca_dest_file, ca_list, True, False)
def gen_certif(certfile, keyfile=None, key_user='', key_grp='', key_chmod='',
cert_user='', cert_grp='', cert_chmod='', regen=False, copy_key=False,
del_passwd=True, signe_req=True, container=None, client_cert=False,
cert_conf_file=None):
"""
Génération des requêtes de certificats et signature par la CA
"""
if not cert_conf_file:
if client_cert:
cert_conf_file = client_conf_file
else:
cert_conf_file = conf_file
load_default_conf_if_needed()
if not os.path.isfile(cert_conf_file):
raise Exception(_(u"Certificate configuration template can not be found:\n\t{0}\n").format(cert_conf_file))
basefile = os.path.splitext(certfile)[0]
if keyfile is None:
keyfile = "%s.key" % (basefile)
if container != None:
cpath = client.get_container(name=container)['path']
certfile = cpath + certfile
keyfile = cpath + keyfile
if regen or not os.path.isfile(certfile) or not os.path.isfile(keyfile):
remove_link(certfile)
if not isdir(dirname(certfile)):
raise Exception(_(u"Folder {0} does not exist.").format(dirname(certfile)))
if not isdir(dirname(keyfile)):
raise Exception(_(u"Folder {0} does not exist.").format(dirname(keyfile)))
# certificat absent ou regénération demandée
fic_p10 = os.path.join(req_dir, "%s.p10" % (os.path.basename(basefile)))
# génération de la requête de certificat x509 et d'un simili certificat auto-signé
if exists(keyfile):
gen_req = '/usr/bin/openssl req -new -key "%s" -days %s -config %s -out "%s" >/dev/null 2>&1' % (
keyfile, ssl_default_cert_time, cert_conf_file, fic_p10)
new_key = False
else:
gen_req = '/usr/bin/openssl req -new -newkey rsa:%s -days %s -config %s -keyout "%s" -out "%s" >/dev/null 2>&1' % (
ssl_default_key_bits, ssl_default_cert_time, cert_conf_file, tmp_keyfile, fic_p10)
new_key = True
cmd = Popen(gen_req, shell=True)
if cmd.wait() != 0:
raise Exception(_(u'! Error while generating certificate request {0} !').format(fic_p10))
if new_key:
if del_passwd:
sup_passwd(tmp_keyfile, keyfile)
else:
copy(tmp_keyfile, keyfile)
if os.path.isfile(tmp_keyfile):
unlink(tmp_keyfile)
if signe_req:
# on signe la requête
ca_signe = '/usr/bin/openssl ca -in "%s" -config %s -out "%s" -batch -notext >/dev/null 2>&1' % (fic_p10, cert_conf_file, certfile)
cmd = Popen(ca_signe, shell=True)
if cmd.wait() != 0:
raise Exception(_(u'! Error while signing certificate request {0} !') % fic_p10)
print(_(u"* Certificate {0} successfully generated").format(certfile))
if copy_key:
concat_fic(certfile, [keyfile], need_link=False)
finalise_cert(certfile, keyfile, key_user=key_user,
key_grp=key_grp, key_chmod=key_chmod,
cert_user=cert_user, cert_grp=cert_grp,
cert_chmod=cert_chmod)
build_link(certfile)
def remove_link(name, remove_broken_link=True):
load_default_conf_if_needed()
if not name.startswith(join(ssl_dir, 'certs')):
return
for cert_link in glob.glob(os.path.join(ssl_dir, 'certs/*')):
if islink(cert_link):
if remove_broken_link and not exists(cert_link):
#print 'ok lien cassé pour {} donc supprimé'.format(cert_link)
unlink(cert_link)
elif str(name) == realpath(cert_link):
#print 'ok suppression lien {} comme demandé ({})'.format(cert_link, name)
unlink(cert_link)
def build_link(name, concats=[]):
load_default_conf_if_needed()
if not name.startswith(join(ssl_dir, 'certs')):
return
def _check_contats_link(link):
# supprimer tous les liens vers les fichiers utilises pour la concatenation
if islink(link):
if realpath(link) in concats:
#print 'ok suppression du link {} ({} est dans {})'.format(link, realpath(link), concats)
unlink(link)
def _check_link(fp, suffix):
# calcul du bon suffix utilise dans le nom
# si le fichier existe avec le suffix courant, ajoute 1 au numero de suffix
new_name = join(dir_name, fp) + '.' + str(suffix)
if islink(new_name):
#print 'pas de suppression du link {} ({} n\'est pas dans {})'.format(new_name, realpath(new_name), concats)
return _check_link(fp, suffix + 1)
#else:
# print "ok ce n'est pas un link {}".format(new_name)
return new_name
def _build_link(ret):
# creer un lien a partir du hash du subject
if ret != '':
fp = ret.split('\n')[0]
if fp.isalnum():
if concats != []:
for link in glob.glob(join(dir_name, fp) + '.*'):
_check_contats_link(link)
new_name = _check_link(fp, 0)
#print 'ok creation du link {} vers {}'.format(new_name, name)
symlink(name, new_name)
return stat(new_name).st_mtime
return 0
dir_name = dirname(name)
subject_fp = ["/usr/bin/openssl", "x509", "-subject_hash", "-fingerprint", "-noout", "-in", name]
subject_fp_old = ["/usr/bin/openssl", "x509", "-subject_hash_old", "-fingerprint", "-noout", "-in", name]
new_timestamp = _build_link(system_out(subject_fp)[1])
new_timestamp = max(_build_link(system_out(subject_fp_old)[1]), new_timestamp)
if isfile(SSL_LAST_FILE):
try:
fh = open(SSL_LAST_FILE, 'r')
timestamp = float(fh.read().strip())
except ValueError:
timestamp = 0
if new_timestamp > timestamp:
fh = open(SSL_LAST_FILE, 'w')
fh.write(str(new_timestamp))
fh.close()
def rehash_if_needed():
load_default_conf_if_needed()
need_rehash = False
if isfile(SSL_LAST_FILE):
try:
fh = open(SSL_LAST_FILE, 'r')
timestamp = int(float(fh.read().strip()))
for cert_link in glob.glob(os.path.join(ssl_dir, 'certs/*')):
try:
if timestamp < int(stat(cert_link).st_mtime):
need_rehash = True
break
except:
pass
except ValueError:
import traceback
traceback.print_exc()
need_rehash = True
else:
need_rehash = True
if need_rehash:
system_code(['/usr/bin/c_rehash'])
new_timestamp = 0
for cert_link in glob.glob(os.path.join(ssl_dir, 'certs/*')):
if isfile(cert_link):
timestamp = stat(cert_link).st_mtime
if timestamp > new_timestamp:
new_timestamp = timestamp
fh = open(SSL_LAST_FILE, 'w')
fh.write(str(new_timestamp))
fh.close()
# gen_certif utils reader
def certif_loader(regen=None):
"""charge les fichiers permettant de générer les certificats
"""
load_default_conf_if_needed()
# XXX FIXME : changer le path de data vers les paquets container,
# XXX FIXME et déplacer les .gen_cert
files = glob.glob(join('/usr/share/eole/certs', '*_*.gen_cert'))
files.sort()
for fname in files:
# puts name in global namespace because we need it in execfile's
# namespace in rules_loader
name = splitext(basename(fname))[0].split('_')[1]
# exec gen_certs
execfile(fname, globals(),locals())
def get_subject(cert=None, certfile=None):
"""
récupère le subject d'un certificat.
spécifier obligatoirement un des deux paramètres :
- cert : contenu du certificat
- certfile : nom du fichier du certificat
"""
load_default_conf_if_needed()
global regexp_get_subject
if None not in (cert, certfile):
raise Exception(_(u'cert or certfile must be None'))
if cert == certfile:
raise Exception(_(u'cert or certfile must be set'))
if certfile != None:
cmd = ['openssl', 'x509', '-in', certfile, '-subject', '-noout']
stdin = None
else:
cmd = ['openssl', 'x509', '-subject', '-noout']
stdin = cert
ret = system_out(cmd=cmd, stdin=stdin)
if ret[0] != 0:
raise Exception(_(u'error in {0}: {1}').format(' '.join(cmd), str(ret[2])))
ret = ret[1].rstrip()
if not ret.startswith("subject= "):
raise Exception(_(u'Invalid certificate subject: {0} ').format(ret))
if regexp_get_subject is None:
regexp_get_subject = re.compile('^subject= (.*)/CN=(.*)')
return regexp_get_subject.findall(ret)[0]
def get_issuer_subject(cert=None, certfile=None):
"""
récupère le subject de la CA d'un certificat.
spécifier obligatoirement un des deux paramètres :
- cert : contenu du certificat
- certfile : nom du fichier du certificat
"""
load_default_conf_if_needed()
if None not in (cert, certfile):
raise Exception(_(u'cert or certfile must be None'))
if cert == certfile:
raise Exception(_(u'cert or certfile must be set'))
if certfile != None:
cmd = ['openssl', 'x509', '-in', certfile, '-issuer', '-noout']
stdin = None
else:
cmd = ['openssl', 'x509', '-issuer', '-noout']
stdin = cert
ret = system_out(cmd=cmd, stdin=stdin)
if ret[0] != 0:
raise Exception(_(u'error in {0}: {1}').format(' '.join(cmd), str(ret[2])))
ret = ret[1].rstrip()
if not ret.startswith("issuer= "):
raise Exception(_(u'Invalid certificate issuer: {0} ').format(ret))
regexp = '^issuer= (.*)/CN=(.*)'
return re.findall(regexp, ret)[0]
def load_conf(ssl_dico):
global ssl_dir, cert_dir, key_dir, tmp_keyfile, file_serial, req_dir
global local_ca_dir, newcerts_dir, ca_conf_file, conf_file, client_conf_file
global ca_file, ca_dest_file, ca_keyfile, start_index, min_serial
global ssl_default_key_bits, ssl_default_cert_time
global certs_catalog
ssl_dir = ssl_dico.get('ssl_dir', ssl_dir)
cert_dir = ssl_dico.get('cert_dir', os.path.join(ssl_dir, "certs"))
key_dir = ssl_dico.get('key_dir', os.path.join(ssl_dir, "private"))
tmp_keyfile = ssl_dico.get('tmp_keyfile', os.path.join(key_dir, "tmpkey.key"))
file_serial = ssl_dico.get('file_serial', os.path.join(ssl_dir, "serial"))
req_dir = ssl_dico.get('req_dir', os.path.join(ssl_dir, "req"))
local_ca_dir = ssl_dico.get('local_ca_dir', os.path.join(ssl_dir, "local_ca"))
newcerts_dir = ssl_dico.get('newcerts_dir', os.path.join(ssl_dir, "newcerts"))
ca_conf_file = ssl_dico.get('ca_conf_file', ca_conf_file)
conf_file = ssl_dico.get('conf_file', conf_file)
client_conf_file = ssl_dico.get('client_conf_file', conf_file)
# chemin de la CA
ca_file = ssl_dico.get('ca_file', os.path.join(cert_dir, "ca_local.crt"))
ca_dest_file = ssl_dico.get('ca_dest_file', os.path.join(cert_dir, "ca.crt"))
ca_keyfile = ssl_dico.get('ca_keyfile', os.path.join(key_dir, "ca.key"))
# index
start_index = ssl_dico.get('start_index', hex(int(time.time()))[2:])
min_serial = int(eval('0x30'))
ssl_default_key_bits = ssl_dico.get('ssl_default_key_bits', client.get_creole('ssl_default_key_bits', 2048))
ssl_default_cert_time = ssl_dico.get('ssl_default_cert_time', client.get_creole('ssl_default_cert_time', 1096))
def load_default_conf_if_needed():
"""creoled n'est pas forcement démarré à ce moment là
ne charger la configuration par défaut qu'à l'utilisation de la lib
et non a l'importantion
#8448
"""
global ssl_dir
if ssl_dir == None:
load_conf({'ssl_dir': '/etc/ssl',
'ca_conf_file': '/etc/eole/ssl/ca-eole.conf',
'conf_file': '/etc/eole/ssl/certif-eole.conf',
'client_conf_file': '/etc/eole/ssl/client-eole.conf'})
ssl_dir=None
ca_conf_file=None
client_conf_file=None
conf_file=None
certs_catalog = None
ca_issuer = None