From a845a6d856f60876967258dfd5c39f8f97e8afd2 Mon Sep 17 00:00:00 2001 From: Graham Hagger Date: Fri, 22 Oct 2010 14:00:29 -0400 Subject: initial add of SSLCA plugin --- src/lib/Server/Plugins/SSLCA.py | 209 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 src/lib/Server/Plugins/SSLCA.py (limited to 'src') diff --git a/src/lib/Server/Plugins/SSLCA.py b/src/lib/Server/Plugins/SSLCA.py new file mode 100644 index 000000000..294f82f3f --- /dev/null +++ b/src/lib/Server/Plugins/SSLCA.py @@ -0,0 +1,209 @@ +import Bcfg2.Server.Plugin +import Bcfg2.Options +import os +from ConfigParser import ConfigParser, NoSectionError, NoOptionError +from M2Crypto import RSA, EVP, X509, m2 + +""" +How this should work.... + +V1.0 - Only handles localhost.key and localhost.crt, therefor +assuming we only care about a cert for www, or all ssl services +will use the same cert + +Initialiazation: +Grab options from bcfg2.conf +load cakey, cacert +cache other options + +Req comes in for key & cert +If key exists: + load key + cache key + return key +Else: + gen key + cache key + save key + return key +If cert exists: + load cert + If fails to verify against key: + gen cert + save cert + return cert + If aliases fail don't match + gen cert + save cert + return cert + return cert +Else: + gen cert + save cert + return cert + +V2.0 - Maybe create additional types, SSLCertPath, SSLKeyPath, +to allow generation of multiple certs/keys in arbitrary locations +""" + + +class SSLbase(Bcfg2.Server.Plugin.Plugin, + Bcfg2.Server.Plugin.Generator, + Bcfg2.Server.Plugin.DirectoryBacked): + """ + The sslca generator manages ssl certificates + and keys + """ + + name = 'SSLbase' + __version__ = '0.00000000001' + __author__ = 'ghagger@wgen.net' + + hostkey = 'localhost.key' + hostcert = 'localhost.crt' + + def __init__(self, core, datastore): + Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore) + Bcfg2.Server.Plugin.Generator.__init__(self) + try: + Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, self.data, + self.core.fam) + except OSError, ioerr: + self.logger.error("Failed to load SSLbase repository from %s" \ + % (self.data)) + self.logger.error(ioerr) + raise Bcfg2.Server.Plugin.PluginInitError + self.Entries = {'Path': + {'/etc/pki/tls/private/localhost.key': self.get_key, + '/etc/pki/tls/certs/localhost.crt': self.get_cert}} + # grab essential sslca configuration from bcfg2.conf + cp = ConfigParser() + cp.read(Bcfg2.Options.CFILE.value) + try: + ca_cert_filename = cp.get('sslca', 'ca_cert') + ca_key_filename = cp.get('sslca', 'ca_key') + self.ca_key_passphrase = cp.get('sslca', 'ca_key_passphrase') + self.cert_subject = cp.get('sslca', 'cert_subject') + self.cert_days = cp.get('sslca', 'cert_days') + self.pkey_bits = cp.get('sslca', 'pkey_bits') + except: + raise NoOptionError + self.ca_cert = X509.load_cert(ca_cert_filename) + self.ca_key = EVP.load_key(ca_key_filename, lambda x: self.ca_key_passphrase) + self._newkey = False + + def get_key(self, entry, metadata): + filename = self.hostkey+".H_%s" % metadata.hostname + if filename in self.entries.keys(): + entry.text = self.entries[filename].data + self.pkey = EVP.load_key_string(entry.text) + else: + (self.pkey, entry.text) = self.build_key(filename) + keyfile = open(self.data + '/' +filename, 'w') + keyfile.write(entry.text) + keyfile.close() + self._newkey = True + + def build_key(self, filename): + """Generate new private key for client.""" + rsa_key = RSA.gen_key(int(self.pkey_bits), m2.RSA_F4) + pkey = EVP.PKey() + pkey.assign_rsa(rsa_key) + keyfile = open(self.data + '/' +filename, 'w') + keyfile.write(pkey.as_pem(cipher=None)) + keyfile.close() + self._newkey = True + return pkey, pkey.as_pem(cipher=None) + + def get_cert(self, entry, metadata): + filename = self.hostcert + ".H_%s" % metadata.hostname + # load prexisting cert, if any + if filename in self.entries.keys() and self._newkey == False: + cert = X509.load_cert_string(self.entries[filename].data) + # check cert subjectAltNames match current aliases + cert_aliases = cert.get_ext('subjectAltName') + if cert_aliases: + if metadata.aliases != [alias.lstrip('DNS:') for alias in cert_aliases.get_value().split(', ')]: + entry.text = self.build_cert(filename, metadata) + return + entry.text = cert.as_text()+cert.as_string() + else: + entry.text = self.build_cert(filename, metadata) + + def get_serial(self): + serialpath = self.data + '/serial' + serial = 0 + if os.path.isfile(serialpath): + serialfile = open(serialpath, 'r') + serial = int(serialfile.read()) + serialfile.close() + serialfile = open(serialpath, 'w') + serial += 1 + serialfile.write(str(serial)) + serialfile.close() + return serial + + def build_cert(self, filename, metadata): + req = self.make_request(self.pkey, metadata) + serial = self.get_serial() + cert = self.make_cert(req, serial, metadata.aliases) + cert_out = cert.as_text()+cert.as_pem() + certfile = open(self.data + '/' +filename, 'w') + certfile.write(cert_out) + certfile.close() + cert_store = self.data + '/certstore' + if not os.path.isdir(cert_store): + os.mkdir(cert_store) + storefile = open(cert_store + '/' + str(serial) + '.pem', 'w') + storefile.write(cert_out) + storefile.close() + return cert_out + + def make_request(self, key, metadata): + req = X509.Request() + req.set_version(2) + req.set_pubkey(key) + name = X509.X509_Name() + parts = [a.split('=') for a in self.cert_subject.split(',')] + [setattr(name, k, v) for k,v in parts] + name.CN = metadata.hostname + req.set_subject_name(name) + req.sign(key, 'sha1') + return req + + def make_cert(self, req, serial, aliases): + pkey = req.get_pubkey() + if not req.verify(pkey): + raise ValueError, 'Error verifying request' + sub = req.get_subject() + cert = X509.X509() + cert.set_serial_number(serial) + cert.set_version(2) + cert.set_subject(sub) + cert.set_issuer(self.ca_cert) + cert.set_pubkey(pkey) + notBefore = m2.x509_get_not_before(cert.x509) + notAfter = m2.x509_get_not_after(cert.x509) + m2.x509_gmtime_adj(notBefore, 0) + m2.x509_gmtime_adj(notAfter, 60*60*24*long(self.cert_days)) + exts = [ + ('basicConstraints','CA:FALSE'), + ('subjectKeyIdentifier','hash'), + ('authorityKeyIdentifier','keyid,issuer:always'), + ('nsCertType','SSL Server'), + ] + if aliases: + exts.append(('subjectAltName', ','.join(['DNS:'+alias for alias in aliases]))) + for ext in exts: + cert.add_ext(X509.new_extension(ext[0],ext[1])) + cert.sign(self.ca_key, 'sha1') + return cert + + def HandleEvent(self, event=None): + """Local event handler that does something....""" + Bcfg2.Server.Plugin.DirectoryBacked.HandleEvent(self, event) + + def HandlesEntry(self, entry, _): + """Handle entries dynamically.""" + return entry.tag == 'Path' and (entry.get('name').endswith(self.hostkey) or entry.get('name').endswith(self.hostcert)) + -- cgit v1.2.3-1-g7c22 From df1d94eda634e2f00757af19b70e1abb6b1b98a2 Mon Sep 17 00:00:00 2001 From: Graham Hagger Date: Fri, 22 Oct 2010 15:15:03 -0400 Subject: moved some sslca docs around --- src/lib/Server/Plugins/SSLCA.py | 43 ----------------------------------------- 1 file changed, 43 deletions(-) (limited to 'src') diff --git a/src/lib/Server/Plugins/SSLCA.py b/src/lib/Server/Plugins/SSLCA.py index 294f82f3f..29acabbf4 100644 --- a/src/lib/Server/Plugins/SSLCA.py +++ b/src/lib/Server/Plugins/SSLCA.py @@ -4,49 +4,6 @@ import os from ConfigParser import ConfigParser, NoSectionError, NoOptionError from M2Crypto import RSA, EVP, X509, m2 -""" -How this should work.... - -V1.0 - Only handles localhost.key and localhost.crt, therefor -assuming we only care about a cert for www, or all ssl services -will use the same cert - -Initialiazation: -Grab options from bcfg2.conf -load cakey, cacert -cache other options - -Req comes in for key & cert -If key exists: - load key - cache key - return key -Else: - gen key - cache key - save key - return key -If cert exists: - load cert - If fails to verify against key: - gen cert - save cert - return cert - If aliases fail don't match - gen cert - save cert - return cert - return cert -Else: - gen cert - save cert - return cert - -V2.0 - Maybe create additional types, SSLCertPath, SSLKeyPath, -to allow generation of multiple certs/keys in arbitrary locations -""" - - class SSLbase(Bcfg2.Server.Plugin.Plugin, Bcfg2.Server.Plugin.Generator, Bcfg2.Server.Plugin.DirectoryBacked): -- cgit v1.2.3-1-g7c22 From 5228ffeba0e516ae3deeecde0403c1c7f9d639a6 Mon Sep 17 00:00:00 2001 From: Graham Hagger Date: Fri, 22 Oct 2010 16:05:05 -0400 Subject: ok, so i should have renamed the class to SSLCA a long time ago... doh --- src/lib/Server/Plugins/SSLCA.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/lib/Server/Plugins/SSLCA.py b/src/lib/Server/Plugins/SSLCA.py index 29acabbf4..734049417 100644 --- a/src/lib/Server/Plugins/SSLCA.py +++ b/src/lib/Server/Plugins/SSLCA.py @@ -4,7 +4,7 @@ import os from ConfigParser import ConfigParser, NoSectionError, NoOptionError from M2Crypto import RSA, EVP, X509, m2 -class SSLbase(Bcfg2.Server.Plugin.Plugin, +class SSLCA(Bcfg2.Server.Plugin.Plugin, Bcfg2.Server.Plugin.Generator, Bcfg2.Server.Plugin.DirectoryBacked): """ -- cgit v1.2.3-1-g7c22 From 196b67a03a5ff8b550f2dfe8efc95893c408b483 Mon Sep 17 00:00:00 2001 From: Graham Hagger Date: Wed, 27 Oct 2010 17:52:19 -0400 Subject: starting to establish the way sslca will truly work --- src/lib/Server/Plugins/SSLCA.py | 623 ++++++++++++++++++++++++++++++---------- 1 file changed, 471 insertions(+), 152 deletions(-) (limited to 'src') diff --git a/src/lib/Server/Plugins/SSLCA.py b/src/lib/Server/Plugins/SSLCA.py index 734049417..f18c18944 100644 --- a/src/lib/Server/Plugins/SSLCA.py +++ b/src/lib/Server/Plugins/SSLCA.py @@ -1,166 +1,485 @@ import Bcfg2.Server.Plugin -import Bcfg2.Options -import os -from ConfigParser import ConfigParser, NoSectionError, NoOptionError -from M2Crypto import RSA, EVP, X509, m2 - -class SSLCA(Bcfg2.Server.Plugin.Plugin, - Bcfg2.Server.Plugin.Generator, - Bcfg2.Server.Plugin.DirectoryBacked): - """ - The sslca generator manages ssl certificates - and keys - """ +from subprocess import Popen, PIPE +import lxml.etree +import posixpath +import logging +import pdb - name = 'SSLbase' - __version__ = '0.00000000001' - __author__ = 'ghagger@wgen.net' +class SSLCA(Bcfg2.Server.Plugin.GroupSpool): + #Bcfg2.Server.Plugin.Plugin, + #Bcfg2.Server.Plugin.Generator, + #Bcfg2.Server.Plugin.DirectoryBacked): - hostkey = 'localhost.key' - hostcert = 'localhost.crt' + """ + The SSLCA generator handles the creation and + management of ssl certificates and their keys. + """ + name = 'SSLCA' + __version__ = '$Id:$' + __author__ = 'g.hagger@gmail.com' + __child__ = Bcfg2.Server.Plugin.FileBacked def __init__(self, core, datastore): - Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore) - Bcfg2.Server.Plugin.Generator.__init__(self) - try: - Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, self.data, - self.core.fam) - except OSError, ioerr: - self.logger.error("Failed to load SSLbase repository from %s" \ - % (self.data)) - self.logger.error(ioerr) - raise Bcfg2.Server.Plugin.PluginInitError - self.Entries = {'Path': - {'/etc/pki/tls/private/localhost.key': self.get_key, - '/etc/pki/tls/certs/localhost.crt': self.get_cert}} - # grab essential sslca configuration from bcfg2.conf - cp = ConfigParser() - cp.read(Bcfg2.Options.CFILE.value) - try: - ca_cert_filename = cp.get('sslca', 'ca_cert') - ca_key_filename = cp.get('sslca', 'ca_key') - self.ca_key_passphrase = cp.get('sslca', 'ca_key_passphrase') - self.cert_subject = cp.get('sslca', 'cert_subject') - self.cert_days = cp.get('sslca', 'cert_days') - self.pkey_bits = cp.get('sslca', 'pkey_bits') - except: - raise NoOptionError - self.ca_cert = X509.load_cert(ca_cert_filename) - self.ca_key = EVP.load_key(ca_key_filename, lambda x: self.ca_key_passphrase) - self._newkey = False + Bcfg2.Server.Plugin.GroupSpool.__init__(self, core, datastore) +# Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore) +# Bcfg2.Server.Plugin.Generator.__init__(self) +# try: +# Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, self.data, +# self.core.fam) +# except OSError, ioerr: +# self.logger.error("Failed to load SSHbase repository from %s" \ +# % (self.data)) +# self.logger.error(ioerr) +# raise Bcfg2.Server.Plugin.PluginInitError +# + def HandleEvent(self, event=None): + action = event.code2str() + if event.filename[0] == '/': + return + epath = "".join([self.data, self.handles[event.requestID], + event.filename]) + if posixpath.isdir(epath): + ident = self.handles[event.requestID] + event.filename + else: + ident = self.handles[event.requestID][:-1] + + self.logger.error('ACTION: %s, IDENT %s, FILENAME %s' % (action, ident, event.filename)) + + if action in ['exists', 'created']: + if posixpath.isdir(epath): + self.AddDirectoryMonitor(epath[len(self.data):]) + if ident not in self.entries and posixpath.isfile(epath): + if event.filename.endswith('key.xml'): + self.Entries['Path'][ident] = self.get_key + elif event.filename.endswith('cert.xml'): + pass +# self.Entries['Path'][ident] = self.get_cert + else: + fname = "".join([ident, '/', event.filename]) + self.entries[fname] = self.__child__(epath) + self.entries[fname].HandleEvent(event) + if action == 'changed': + self.entries[ident].HandleEvent(event) + elif action == 'deleted': + fbase = self.handles[event.requestID] + event.filename + if fbase in self.entries: + # a directory was deleted + del self.entries[fbase] + del self.Entries['Path'][fbase] + else: + self.entries[ident].HandleEvent(event) def get_key(self, entry, metadata): - filename = self.hostkey+".H_%s" % metadata.hostname - if filename in self.entries.keys(): - entry.text = self.entries[filename].data - self.pkey = EVP.load_key_string(entry.text) + path = entry.get('name') + permdata = {'owner':'root', + 'group':'root', + 'type':'file', + 'perms':'644'} + [entry.attrib.__setitem__(key, permdata[key]) for key in permdata] + filename = "".join([path, '/', path.rsplit('/', 1)[1], '.H_', metadata.hostname]) + if filename not in self.entries.keys(): + key = self.build_key(filename, metadata) + open(self.data + filename, 'w').write(key) + entry.text = key else: - (self.pkey, entry.text) = self.build_key(filename) - keyfile = open(self.data + '/' +filename, 'w') - keyfile.write(entry.text) - keyfile.close() - self._newkey = True - - def build_key(self, filename): - """Generate new private key for client.""" - rsa_key = RSA.gen_key(int(self.pkey_bits), m2.RSA_F4) - pkey = EVP.PKey() - pkey.assign_rsa(rsa_key) - keyfile = open(self.data + '/' +filename, 'w') - keyfile.write(pkey.as_pem(cipher=None)) - keyfile.close() - self._newkey = True - return pkey, pkey.as_pem(cipher=None) + entry.text = self.entries[filename].data + + def build_key(self, filename, metadata): + # TODO read params + type = 'rsa' + bits = 2048 + if type == 'rsa': + cmd = "openssl genrsa %s " % bits + elif type == 'dsa': + cmd = "openssl dsaparam -noout -genkey %s" % bits + key = Popen(cmd, shell=True, stdout=PIPE).stdout.read() + return key def get_cert(self, entry, metadata): - filename = self.hostcert + ".H_%s" % metadata.hostname - # load prexisting cert, if any - if filename in self.entries.keys() and self._newkey == False: - cert = X509.load_cert_string(self.entries[filename].data) - # check cert subjectAltNames match current aliases - cert_aliases = cert.get_ext('subjectAltName') - if cert_aliases: - if metadata.aliases != [alias.lstrip('DNS:') for alias in cert_aliases.get_value().split(', ')]: - entry.text = self.build_cert(filename, metadata) - return - entry.text = cert.as_text()+cert.as_string() + path = entry.get('name') + permdata = {'owner':'root', + 'group':'root', + 'type':'file', + 'perms':'644'} + [entry.attrib.__setitem__(key, permdata[key]) for key in permdata] + filename = "".join([path, '/', path.rsplit('/', 1)[1], '.H_', metadata.hostname]) + if filename in self.entries.keys() and self.verify_cert(filename) : + entry.text = self.entries[filename].data else: - entry.text = self.build_cert(filename, metadata) - - def get_serial(self): - serialpath = self.data + '/serial' - serial = 0 - if os.path.isfile(serialpath): - serialfile = open(serialpath, 'r') - serial = int(serialfile.read()) - serialfile.close() - serialfile = open(serialpath, 'w') - serial += 1 - serialfile.write(str(serial)) - serialfile.close() - return serial - - def build_cert(self, filename, metadata): - req = self.make_request(self.pkey, metadata) - serial = self.get_serial() - cert = self.make_cert(req, serial, metadata.aliases) - cert_out = cert.as_text()+cert.as_pem() - certfile = open(self.data + '/' +filename, 'w') - certfile.write(cert_out) - certfile.close() - cert_store = self.data + '/certstore' - if not os.path.isdir(cert_store): - os.mkdir(cert_store) - storefile = open(cert_store + '/' + str(serial) + '.pem', 'w') - storefile.write(cert_out) - storefile.close() - return cert_out - - def make_request(self, key, metadata): - req = X509.Request() - req.set_version(2) - req.set_pubkey(key) - name = X509.X509_Name() - parts = [a.split('=') for a in self.cert_subject.split(',')] - [setattr(name, k, v) for k,v in parts] - name.CN = metadata.hostname - req.set_subject_name(name) - req.sign(key, 'sha1') - return req - - def make_cert(self, req, serial, aliases): - pkey = req.get_pubkey() - if not req.verify(pkey): - raise ValueError, 'Error verifying request' - sub = req.get_subject() - cert = X509.X509() - cert.set_serial_number(serial) - cert.set_version(2) - cert.set_subject(sub) - cert.set_issuer(self.ca_cert) - cert.set_pubkey(pkey) - notBefore = m2.x509_get_not_before(cert.x509) - notAfter = m2.x509_get_not_after(cert.x509) - m2.x509_gmtime_adj(notBefore, 0) - m2.x509_gmtime_adj(notAfter, 60*60*24*long(self.cert_days)) - exts = [ - ('basicConstraints','CA:FALSE'), - ('subjectKeyIdentifier','hash'), - ('authorityKeyIdentifier','keyid,issuer:always'), - ('nsCertType','SSL Server'), - ] - if aliases: - exts.append(('subjectAltName', ','.join(['DNS:'+alias for alias in aliases]))) - for ext in exts: - cert.add_ext(X509.new_extension(ext[0],ext[1])) - cert.sign(self.ca_key, 'sha1') - return cert + cert = self.build_cert(filename, metadata) + open(self.data + filename, 'w').write(cert) + entry.text = cert + + def verify_cert(self): + # TODO + # check cert matches key + # check expiry + pass + + def build_req(self): + pass + + def build_cert(self): + pass + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +#class SSLCAFile: +# +# def __init__(self, datastore, name, specific, encoding): +# self.data = datastore +# self.name = name +# self.specific = specific +# self.encoding = encoding +# if name.endswith('.xml'): +# self.xml = lxml.etree.parse(name) +# +# def handle_event(self, event=None): +# """Handle all fs events for this file.""" +# if event and event.code2str() == 'deleted': +# return +# +# def bind_entry(self, entry, metadata): +# pdb.set_trace() +# +# +#class SSLCAKeyFile(SSLCAFile): +# +# def __init__(self, datastore, name, specific, encoding): +# SSLCAFile.__init__(self, datastore, name, specific, encoding) +# key_attrs = self.xml.find('Key') +# self.bits = key_attrs.get('bits') +# self.type = key_attrs.get('type') +# +# def bind_entry(self, entry, metadata): +# """Build literal file information.""" +# if entry.tag == 'Path': +# entry.set('type', 'file') +# entry.text = self.get_key(entry, metadata) +# +# def get_key(self, entry, metadata): +# fname = +dir '.H_' + metadata.hostname +# # TODO add logic to get+verify key if hostfile exists & save if not +# pdb.set_trace() +# return self.build_key() +# +# def build_key(self): +# if self.type == 'rsa': +# cmd = "openssl genrsa %s " % self.bits +# elif self.type == 'dsa': +# cmd = "openssl dsaparam -noout -genkey %s" % self.bits +# key = Popen(cmd, shell=True, stdout=PIPE).stdout.read() +# return key +# +# +#class SSLCACertFile(SSLCAFile): +# +# def __init__(self, datastore, name, specific, encoding): +# SSLCAFile.__init__(self, datastore, name, specific, encoding) +# cert_attrs = self.xml.find('Cert') +# #self.format = cert_attrs.get('format') +# #self.key = cert_attrs.get('key') +# #self.ca = cert_attrs.get('ca') +# +# def bind_entry(self, entry, metadata): +# """Build literal file information.""" +# fname = entry.get('realname', entry.get('name')) +# if entry.tag == 'Path': +# entry.set('type', 'file') +# entry.text = 'booya cert' +# +# +#class SSLCAEntrySet(Bcfg2.Server.Plugin.EntrySet): +# """ +# Handles host and group specific entries +# """ +# def __init__(self, datastore, basename, path, entry_type, encoding): +# self.data = datastore +# Bcfg2.Server.Plugin.EntrySet.__init__(self, basename, path, entry_type, encoding) +# +# def entry_init(self, event): +# """Handle template and info file creation.""" +# logger = logging.getLogger('Bcfg2.Plugins.SSLCA') +# if event.filename in self.entries: +# logger.warn("Got duplicate add for %s" % event.filename) +# else: +# fpath = "%s/%s" % (self.path, event.filename) +# try: +# spec = self.specificity_from_filename(event.filename) +# except Bcfg2.Server.Plugin.SpecificityError: +# if not self.ignore.match(event.filename): +# logger.error("Could not process filename %s; ignoring" % fpath) +# return +# self.entries[event.filename] = self.entry_type(self.data, fpath, +# spec, self.encoding) +# self.entries[event.filename].handle_event(event) +# +# +#class SSLCA(Bcfg2.Server.Plugin.GroupSpool): +# """ +# The SSLCA generator handles the creation and +# management of ssl certificates and their keys. +# """ +# name = 'SSLCA' +# __version__ = '$Id:$' +# __author__ = 'g.hagger@gmail.com' +# filename_pattern = '(key|cert)\.xml' +# es_cls = SSLCAEntrySet +# +# def __init__(self, core, datastore): +# Bcfg2.Server.Plugin.GroupSpool.__init__(self, core, datastore) +# +# def HandleEvent(self, event): +# action = event.code2str() +# if event.filename[0] == '/': +# return +# epath = "".join([self.data, self.handles[event.requestID], +# event.filename]) +# if posixpath.isdir(epath): +# ident = self.handles[event.requestID] + event.filename +# else: +# ident = self.handles[event.requestID][:-1] +# +# if action in ['exists', 'created']: +# if posixpath.isdir(epath): +# self.AddDirectoryMonitor(epath[len(self.data):]) +# if ident not in self.entries and posixpath.isfile(epath): +# if event.filename.endswith('key.xml'): +# es_child_cls = SSLCAKeyFile +# elif event.filename.endswith('cert.xml'): +# es_child_cls = SSLCACertFile +# else: +# return +# dirpath = "".join([self.data, ident]) +# self.entries[ident] = self.es_cls(self.data, +# self.filename_pattern, +# dirpath, +# es_child_cls, +# self.encoding) +# self.Entries['Path'][ident] = self.entries[ident].bind_entry +# if not posixpath.isdir(epath): +# # do not pass through directory events +# self.entries[ident].handle_event(event) +# if action == 'changed': +# self.entries[ident].handle_event(event) +# elif action == 'deleted': +# fbase = self.handles[event.requestID] + event.filename +# if fbase in self.entries: +# # a directory was deleted +# del self.entries[fbase] +# del self.Entries['Path'][fbase] +# else: +# self.entries[ident].handle_event(event) + + + + + + + + - def HandleEvent(self, event=None): - """Local event handler that does something....""" - Bcfg2.Server.Plugin.DirectoryBacked.HandleEvent(self, event) - def HandlesEntry(self, entry, _): - """Handle entries dynamically.""" - return entry.tag == 'Path' and (entry.get('name').endswith(self.hostkey) or entry.get('name').endswith(self.hostcert)) +#import Bcfg2.Options +#import os +#from ConfigParser import ConfigParser, NoSectionError, NoOptionError +#from M2Crypto import RSA, EVP, X509, m2 +#class SSLCA(Bcfg2.Server.Plugin.Plugin, +# Bcfg2.Server.Plugin.Generator, +# Bcfg2.Server.Plugin.DirectoryBacked): +# """ +# The sslca generator manages ssl certificates +# and keys +# """ +# +# name = 'SSLbase' +# __version__ = '0.00000000001' +# __author__ = 'ghagger@wgen.net' +# +# hostkey = 'localhost.key' +# hostcert = 'localhost.crt' +# +# def __init__(self, core, datastore): +# Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore) +# Bcfg2.Server.Plugin.Generator.__init__(self) +# try: +# Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, self.data, +# self.core.fam) +# except OSError, ioerr: +# self.logger.error("Failed to load SSLbase repository from %s" \ +# % (self.data)) +# self.logger.error(ioerr) +# raise Bcfg2.Server.Plugin.PluginInitError +# self.Entries = {'Path': +# {'/etc/pki/tls/private/localhost.key': self.get_key, +# '/etc/pki/tls/certs/localhost.crt': self.get_cert}} +# # grab essential sslca configuration from bcfg2.conf +# cp = ConfigParser() +# cp.read(Bcfg2.Options.CFILE.value) +# try: +# ca_cert_filename = cp.get('sslca', 'ca_cert') +# ca_key_filename = cp.get('sslca', 'ca_key') +# self.ca_key_passphrase = cp.get('sslca', 'ca_key_passphrase') +# self.cert_subject = cp.get('sslca', 'cert_subject') +# self.cert_days = cp.get('sslca', 'cert_days') +# self.pkey_bits = cp.get('sslca', 'pkey_bits') +# except: +# raise NoOptionError +# self.ca_cert = X509.load_cert(ca_cert_filename) +# self.ca_key = EVP.load_key(ca_key_filename, lambda x: self.ca_key_passphrase) +# self._newkey = False +# +# def get_key(self, entry, metadata): +# filename = self.hostkey+".H_%s" % metadata.hostname +# if filename in self.entries.keys(): +# entry.text = self.entries[filename].data +# self.pkey = EVP.load_key_string(entry.text) +# else: +# (self.pkey, entry.text) = self.build_key(filename) +# keyfile = open(self.data + '/' +filename, 'w') +# keyfile.write(entry.text) +# keyfile.close() +# self._newkey = True +# +# def build_key(self, filename): +# """Generate new private key for client.""" +# rsa_key = RSA.gen_key(int(self.pkey_bits), m2.RSA_F4) +# pkey = EVP.PKey() +# pkey.assign_rsa(rsa_key) +# keyfile = open(self.data + '/' +filename, 'w') +# keyfile.write(pkey.as_pem(cipher=None)) +# keyfile.close() +# self._newkey = True +# return pkey, pkey.as_pem(cipher=None) +# +# def get_cert(self, entry, metadata): +# filename = self.hostcert + ".H_%s" % metadata.hostname +# # load prexisting cert, if any +# if filename in self.entries.keys() and self._newkey == False: +# cert = X509.load_cert_string(self.entries[filename].data) +# # check cert subjectAltNames match current aliases +# cert_aliases = cert.get_ext('subjectAltName') +# if cert_aliases: +# if metadata.aliases != [alias.lstrip('DNS:') for alias in cert_aliases.get_value().split(', ')]: +# entry.text = self.build_cert(filename, metadata) +# return +# entry.text = cert.as_text()+cert.as_string() +# else: +# entry.text = self.build_cert(filename, metadata) +# +# def get_serial(self): +# serialpath = self.data + '/serial' +# serial = 0 +# if os.path.isfile(serialpath): +# serialfile = open(serialpath, 'r') +# serial = int(serialfile.read()) +# serialfile.close() +# serialfile = open(serialpath, 'w') +# serial += 1 +# serialfile.write(str(serial)) +# serialfile.close() +# return serial +# +# def build_cert(self, filename, metadata): +# req = self.make_request(self.pkey, metadata) +# serial = self.get_serial() +# cert = self.make_cert(req, serial, metadata.aliases) +# cert_out = cert.as_text()+cert.as_pem() +# certfile = open(self.data + '/' +filename, 'w') +# certfile.write(cert_out) +# certfile.close() +# cert_store = self.data + '/certstore' +# if not os.path.isdir(cert_store): +# os.mkdir(cert_store) +# storefile = open(cert_store + '/' + str(serial) + '.pem', 'w') +# storefile.write(cert_out) +# storefile.close() +# return cert_out +# +# def make_request(self, key, metadata): +# req = X509.Request() +# req.set_version(2) +# req.set_pubkey(key) +# name = X509.X509_Name() +# parts = [a.split('=') for a in self.cert_subject.split(',')] +# [setattr(name, k, v) for k,v in parts] +# name.CN = metadata.hostname +# req.set_subject_name(name) +# req.sign(key, 'sha1') +# return req +# +# def make_cert(self, req, serial, aliases): +# pkey = req.get_pubkey() +# if not req.verify(pkey): +# raise ValueError, 'Error verifying request' +# sub = req.get_subject() +# cert = X509.X509() +# cert.set_serial_number(serial) +# cert.set_version(2) +# cert.set_subject(sub) +# cert.set_issuer(self.ca_cert) +# cert.set_pubkey(pkey) +# notBefore = m2.x509_get_not_before(cert.x509) +# notAfter = m2.x509_get_not_after(cert.x509) +# m2.x509_gmtime_adj(notBefore, 0) +# m2.x509_gmtime_adj(notAfter, 60*60*24*long(self.cert_days)) +# exts = [ +# ('basicConstraints','CA:FALSE'), +# ('subjectKeyIdentifier','hash'), +# ('authorityKeyIdentifier','keyid,issuer:always'), +# ('nsCertType','SSL Server'), +# ] +# if aliases: +# exts.append(('subjectAltName', ','.join(['DNS:'+alias for alias in aliases]))) +# for ext in exts: +# cert.add_ext(X509.new_extension(ext[0],ext[1])) +# cert.sign(self.ca_key, 'sha1') +# return cert +# +# def HandleEvent(self, event=None): +# """Local event handler that does something....""" +# Bcfg2.Server.Plugin.DirectoryBacked.HandleEvent(self, event) +# +# def HandlesEntry(self, entry, _): +# """Handle entries dynamically.""" +# return entry.tag == 'Path' and (entry.get('name').endswith(self.hostkey) or entry.get('name').endswith(self.hostcert)) +# -- cgit v1.2.3-1-g7c22 From c654cb28381ea0b623ac8c2d17063e6fccce445b Mon Sep 17 00:00:00 2001 From: Graham Hagger Date: Fri, 29 Oct 2010 16:05:46 -0400 Subject: SSLCA.py rewritten to use openssl binary --- src/lib/Server/Plugins/SSLCA.py | 531 +++++++++------------------------------- 1 file changed, 122 insertions(+), 409 deletions(-) (limited to 'src') diff --git a/src/lib/Server/Plugins/SSLCA.py b/src/lib/Server/Plugins/SSLCA.py index f18c18944..8f1154d7f 100644 --- a/src/lib/Server/Plugins/SSLCA.py +++ b/src/lib/Server/Plugins/SSLCA.py @@ -1,15 +1,22 @@ +""" +Notes: + +1. Put these notes in real docs!!! +2. dir structure for CA's must be correct +3. for subjectAltNames to work, openssl.conf must have copy_extensions on +""" + + import Bcfg2.Server.Plugin -from subprocess import Popen, PIPE import lxml.etree import posixpath -import logging +import tempfile +from subprocess import Popen, PIPE +from ConfigParser import ConfigParser + import pdb class SSLCA(Bcfg2.Server.Plugin.GroupSpool): - #Bcfg2.Server.Plugin.Plugin, - #Bcfg2.Server.Plugin.Generator, - #Bcfg2.Server.Plugin.DirectoryBacked): - """ The SSLCA generator handles the creation and management of ssl certificates and their keys. @@ -18,23 +25,12 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): __version__ = '$Id:$' __author__ = 'g.hagger@gmail.com' __child__ = Bcfg2.Server.Plugin.FileBacked + key_specs = {} + cert_specs = {} - def __init__(self, core, datastore): - Bcfg2.Server.Plugin.GroupSpool.__init__(self, core, datastore) -# Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore) -# Bcfg2.Server.Plugin.Generator.__init__(self) -# try: -# Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, self.data, -# self.core.fam) -# except OSError, ioerr: -# self.logger.error("Failed to load SSHbase repository from %s" \ -# % (self.data)) -# self.logger.error(ioerr) -# raise Bcfg2.Server.Plugin.PluginInitError -# def HandleEvent(self, event=None): action = event.code2str() - if event.filename[0] == '/': + if event.filename[0] == '/' or event.filename.startswith('CAs'): return epath = "".join([self.data, self.handles[event.requestID], event.filename]) @@ -45,49 +41,70 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): self.logger.error('ACTION: %s, IDENT %s, FILENAME %s' % (action, ident, event.filename)) + fname = "".join([ident, '/', event.filename]) + + + # TODO: check/fix handling of _all_ .xml file events vs hostfiles if action in ['exists', 'created']: if posixpath.isdir(epath): self.AddDirectoryMonitor(epath[len(self.data):]) if ident not in self.entries and posixpath.isfile(epath): if event.filename.endswith('key.xml'): + key_spec = dict(lxml.etree.parse(epath).find('Key').items()) + self.key_specs[ident] = { + 'bits': key_spec.get('bits', 2048), + 'type': key_spec.get('type', 'rsa') + } self.Entries['Path'][ident] = self.get_key elif event.filename.endswith('cert.xml'): - pass -# self.Entries['Path'][ident] = self.get_cert + cert_spec = dict(lxml.etree.parse(epath).find('Cert').items()) + self.cert_specs[ident] = { + 'ca': cert_spec.get('ca', 'default'), + 'format': cert_spec.get('format', 'pem'), + 'key': cert_spec.get('key'), + 'days': cert_spec.get('days', 365), + 'C': cert_spec.get('c'), + 'L': cert_spec.get('l'), + 'ST': cert_spec.get('st'), + 'OU': cert_spec.get('ou'), + 'O': cert_spec.get('o'), + 'emailAddress': cert_spec.get('emailaddress') + } + self.Entries['Path'][ident] = self.get_cert else: - fname = "".join([ident, '/', event.filename]) self.entries[fname] = self.__child__(epath) self.entries[fname].HandleEvent(event) if action == 'changed': - self.entries[ident].HandleEvent(event) + self.entries[fname].HandleEvent(event) elif action == 'deleted': - fbase = self.handles[event.requestID] + event.filename - if fbase in self.entries: + if fname in self.entries: # a directory was deleted - del self.entries[fbase] - del self.Entries['Path'][fbase] + del self.entries[fname] else: - self.entries[ident].HandleEvent(event) + self.entries[fname].HandleEvent(event) def get_key(self, entry, metadata): - path = entry.get('name') + # set path type and permissions, otherwise bcfg2 won't bind the file permdata = {'owner':'root', 'group':'root', 'type':'file', 'perms':'644'} [entry.attrib.__setitem__(key, permdata[key]) for key in permdata] + + # check if we already have a hostfile, or need to generate a new key + # TODO: verify key fits the specs + path = entry.get('name') filename = "".join([path, '/', path.rsplit('/', 1)[1], '.H_', metadata.hostname]) if filename not in self.entries.keys(): - key = self.build_key(filename, metadata) + key = self.build_key(filename, entry, metadata) open(self.data + filename, 'w').write(key) entry.text = key else: entry.text = self.entries[filename].data - def build_key(self, filename, metadata): - # TODO read params - type = 'rsa' - bits = 2048 + def build_key(self, filename, entry, metadata): + type = self.key_specs[entry.get('name')]['type'] + bits = self.key_specs[entry.get('name')]['bits'] if type == 'rsa': cmd = "openssl genrsa %s " % bits elif type == 'dsa': @@ -96,390 +113,86 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): return key def get_cert(self, entry, metadata): - path = entry.get('name') + # set path type and permissions, otherwise bcfg2 won't bind the file permdata = {'owner':'root', 'group':'root', 'type':'file', 'perms':'644'} [entry.attrib.__setitem__(key, permdata[key]) for key in permdata] + + path = entry.get('name') filename = "".join([path, '/', path.rsplit('/', 1)[1], '.H_', metadata.hostname]) - if filename in self.entries.keys() and self.verify_cert(filename) : + + # first - ensure we have a key to work with + key = self.cert_specs[entry.get('name')].get('key') + key_filename = "".join([key, '/', key.rsplit('/', 1)[1], '.H_', metadata.hostname]) + if key_filename not in self.entries: + e = lxml.etree.Element('Path') + e.attrib['name'] = key + self.core.Bind(e, metadata) + + # check if we have a valid hostfile + if filename in self.entries.keys() and self.verify_cert(): entry.text = self.entries[filename].data else: - cert = self.build_cert(filename, metadata) + cert = self.build_cert(entry, metadata) open(self.data + filename, 'w').write(cert) entry.text = cert def verify_cert(self): - # TODO - # check cert matches key - # check expiry - pass - - def build_req(self): - pass - - def build_cert(self): - pass - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#class SSLCAFile: -# -# def __init__(self, datastore, name, specific, encoding): -# self.data = datastore -# self.name = name -# self.specific = specific -# self.encoding = encoding -# if name.endswith('.xml'): -# self.xml = lxml.etree.parse(name) -# -# def handle_event(self, event=None): -# """Handle all fs events for this file.""" -# if event and event.code2str() == 'deleted': -# return -# -# def bind_entry(self, entry, metadata): -# pdb.set_trace() -# -# -#class SSLCAKeyFile(SSLCAFile): -# -# def __init__(self, datastore, name, specific, encoding): -# SSLCAFile.__init__(self, datastore, name, specific, encoding) -# key_attrs = self.xml.find('Key') -# self.bits = key_attrs.get('bits') -# self.type = key_attrs.get('type') -# -# def bind_entry(self, entry, metadata): -# """Build literal file information.""" -# if entry.tag == 'Path': -# entry.set('type', 'file') -# entry.text = self.get_key(entry, metadata) -# -# def get_key(self, entry, metadata): -# fname = +dir '.H_' + metadata.hostname -# # TODO add logic to get+verify key if hostfile exists & save if not -# pdb.set_trace() -# return self.build_key() -# -# def build_key(self): -# if self.type == 'rsa': -# cmd = "openssl genrsa %s " % self.bits -# elif self.type == 'dsa': -# cmd = "openssl dsaparam -noout -genkey %s" % self.bits -# key = Popen(cmd, shell=True, stdout=PIPE).stdout.read() -# return key -# -# -#class SSLCACertFile(SSLCAFile): -# -# def __init__(self, datastore, name, specific, encoding): -# SSLCAFile.__init__(self, datastore, name, specific, encoding) -# cert_attrs = self.xml.find('Cert') -# #self.format = cert_attrs.get('format') -# #self.key = cert_attrs.get('key') -# #self.ca = cert_attrs.get('ca') -# -# def bind_entry(self, entry, metadata): -# """Build literal file information.""" -# fname = entry.get('realname', entry.get('name')) -# if entry.tag == 'Path': -# entry.set('type', 'file') -# entry.text = 'booya cert' -# -# -#class SSLCAEntrySet(Bcfg2.Server.Plugin.EntrySet): -# """ -# Handles host and group specific entries -# """ -# def __init__(self, datastore, basename, path, entry_type, encoding): -# self.data = datastore -# Bcfg2.Server.Plugin.EntrySet.__init__(self, basename, path, entry_type, encoding) -# -# def entry_init(self, event): -# """Handle template and info file creation.""" -# logger = logging.getLogger('Bcfg2.Plugins.SSLCA') -# if event.filename in self.entries: -# logger.warn("Got duplicate add for %s" % event.filename) -# else: -# fpath = "%s/%s" % (self.path, event.filename) -# try: -# spec = self.specificity_from_filename(event.filename) -# except Bcfg2.Server.Plugin.SpecificityError: -# if not self.ignore.match(event.filename): -# logger.error("Could not process filename %s; ignoring" % fpath) -# return -# self.entries[event.filename] = self.entry_type(self.data, fpath, -# spec, self.encoding) -# self.entries[event.filename].handle_event(event) -# -# -#class SSLCA(Bcfg2.Server.Plugin.GroupSpool): -# """ -# The SSLCA generator handles the creation and -# management of ssl certificates and their keys. -# """ -# name = 'SSLCA' -# __version__ = '$Id:$' -# __author__ = 'g.hagger@gmail.com' -# filename_pattern = '(key|cert)\.xml' -# es_cls = SSLCAEntrySet -# -# def __init__(self, core, datastore): -# Bcfg2.Server.Plugin.GroupSpool.__init__(self, core, datastore) -# -# def HandleEvent(self, event): -# action = event.code2str() -# if event.filename[0] == '/': -# return -# epath = "".join([self.data, self.handles[event.requestID], -# event.filename]) -# if posixpath.isdir(epath): -# ident = self.handles[event.requestID] + event.filename -# else: -# ident = self.handles[event.requestID][:-1] -# -# if action in ['exists', 'created']: -# if posixpath.isdir(epath): -# self.AddDirectoryMonitor(epath[len(self.data):]) -# if ident not in self.entries and posixpath.isfile(epath): -# if event.filename.endswith('key.xml'): -# es_child_cls = SSLCAKeyFile -# elif event.filename.endswith('cert.xml'): -# es_child_cls = SSLCACertFile -# else: -# return -# dirpath = "".join([self.data, ident]) -# self.entries[ident] = self.es_cls(self.data, -# self.filename_pattern, -# dirpath, -# es_child_cls, -# self.encoding) -# self.Entries['Path'][ident] = self.entries[ident].bind_entry -# if not posixpath.isdir(epath): -# # do not pass through directory events -# self.entries[ident].handle_event(event) -# if action == 'changed': -# self.entries[ident].handle_event(event) -# elif action == 'deleted': -# fbase = self.handles[event.requestID] + event.filename -# if fbase in self.entries: -# # a directory was deleted -# del self.entries[fbase] -# del self.Entries['Path'][fbase] -# else: -# self.entries[ident].handle_event(event) - - - - - - - - - - -#import Bcfg2.Options -#import os -#from ConfigParser import ConfigParser, NoSectionError, NoOptionError -#from M2Crypto import RSA, EVP, X509, m2 + return False + + def build_cert(self, entry, metadata): + req_config = self.build_req_config(entry, metadata) + req = self.build_request(req_config, entry) + ca_config = "".join([self.data, '/CAs/', self.cert_specs[entry.get('name')]['ca'], '/', 'openssl.cnf']) + days = self.cert_specs[entry.get('name')]['days'] + cmd = "openssl ca -config %s -in %s -days %s -batch -passin pass:TODO!!!!" % (ca_config, req, days) + pdb.set_trace() + cert = Popen(cmd, shell=True, stdout=PIPE).stdout.read() + # TODO: remove tempfiles + return cert + + def build_req_config(self, entry, metadata): + # create temp request config file + conffile = open(tempfile.mkstemp()[1], 'w') + cp = ConfigParser({}) + cp.optionxform = str + defaults = { + 'req': { + 'default_md': 'sha1', + 'distinguished_name': 'req_distinguished_name', + 'req_extensions': 'v3_req', + 'x509_extensions': 'v3_req', + 'prompt': 'no' + }, + 'req_distinguished_name': {}, + 'v3_req': { + 'subjectAltName': '@alt_names' + }, + 'alt_names': {} + } + for section in defaults.keys(): + cp.add_section(section) + for key in defaults[section]: + cp.set(section, key, defaults[section][key]) + x = 1 + for alias in metadata.aliases: + cp.set('alt_names', 'DNS.'+str(x), alias) + x += 1 + for item in ['C', 'L', 'ST', 'O', 'OU', 'emailAddress']: + if self.cert_specs[entry.get('name')][item]: + cp.set('req_distinguished_name', item, self.cert_specs[entry.get('name')][item]) + cp.set('req_distinguished_name', 'CN', metadata.hostname) + cp.write(conffile) + conffile.close() + return conffile.name + + def build_request(self, req_config, entry): + req = tempfile.mkstemp()[1] + key = self.cert_specs[entry.get('name')]['key'] + days = self.cert_specs[entry.get('name')]['days'] + cmd = "openssl req -new -config %s -days %s -key %s -text -out %s" % (req_config, days, key, req) + res = Popen(cmd, shell=True, stdout=PIPE).stdout.read() + return req -#class SSLCA(Bcfg2.Server.Plugin.Plugin, -# Bcfg2.Server.Plugin.Generator, -# Bcfg2.Server.Plugin.DirectoryBacked): -# """ -# The sslca generator manages ssl certificates -# and keys -# """ -# -# name = 'SSLbase' -# __version__ = '0.00000000001' -# __author__ = 'ghagger@wgen.net' -# -# hostkey = 'localhost.key' -# hostcert = 'localhost.crt' -# -# def __init__(self, core, datastore): -# Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore) -# Bcfg2.Server.Plugin.Generator.__init__(self) -# try: -# Bcfg2.Server.Plugin.DirectoryBacked.__init__(self, self.data, -# self.core.fam) -# except OSError, ioerr: -# self.logger.error("Failed to load SSLbase repository from %s" \ -# % (self.data)) -# self.logger.error(ioerr) -# raise Bcfg2.Server.Plugin.PluginInitError -# self.Entries = {'Path': -# {'/etc/pki/tls/private/localhost.key': self.get_key, -# '/etc/pki/tls/certs/localhost.crt': self.get_cert}} -# # grab essential sslca configuration from bcfg2.conf -# cp = ConfigParser() -# cp.read(Bcfg2.Options.CFILE.value) -# try: -# ca_cert_filename = cp.get('sslca', 'ca_cert') -# ca_key_filename = cp.get('sslca', 'ca_key') -# self.ca_key_passphrase = cp.get('sslca', 'ca_key_passphrase') -# self.cert_subject = cp.get('sslca', 'cert_subject') -# self.cert_days = cp.get('sslca', 'cert_days') -# self.pkey_bits = cp.get('sslca', 'pkey_bits') -# except: -# raise NoOptionError -# self.ca_cert = X509.load_cert(ca_cert_filename) -# self.ca_key = EVP.load_key(ca_key_filename, lambda x: self.ca_key_passphrase) -# self._newkey = False -# -# def get_key(self, entry, metadata): -# filename = self.hostkey+".H_%s" % metadata.hostname -# if filename in self.entries.keys(): -# entry.text = self.entries[filename].data -# self.pkey = EVP.load_key_string(entry.text) -# else: -# (self.pkey, entry.text) = self.build_key(filename) -# keyfile = open(self.data + '/' +filename, 'w') -# keyfile.write(entry.text) -# keyfile.close() -# self._newkey = True -# -# def build_key(self, filename): -# """Generate new private key for client.""" -# rsa_key = RSA.gen_key(int(self.pkey_bits), m2.RSA_F4) -# pkey = EVP.PKey() -# pkey.assign_rsa(rsa_key) -# keyfile = open(self.data + '/' +filename, 'w') -# keyfile.write(pkey.as_pem(cipher=None)) -# keyfile.close() -# self._newkey = True -# return pkey, pkey.as_pem(cipher=None) -# -# def get_cert(self, entry, metadata): -# filename = self.hostcert + ".H_%s" % metadata.hostname -# # load prexisting cert, if any -# if filename in self.entries.keys() and self._newkey == False: -# cert = X509.load_cert_string(self.entries[filename].data) -# # check cert subjectAltNames match current aliases -# cert_aliases = cert.get_ext('subjectAltName') -# if cert_aliases: -# if metadata.aliases != [alias.lstrip('DNS:') for alias in cert_aliases.get_value().split(', ')]: -# entry.text = self.build_cert(filename, metadata) -# return -# entry.text = cert.as_text()+cert.as_string() -# else: -# entry.text = self.build_cert(filename, metadata) -# -# def get_serial(self): -# serialpath = self.data + '/serial' -# serial = 0 -# if os.path.isfile(serialpath): -# serialfile = open(serialpath, 'r') -# serial = int(serialfile.read()) -# serialfile.close() -# serialfile = open(serialpath, 'w') -# serial += 1 -# serialfile.write(str(serial)) -# serialfile.close() -# return serial -# -# def build_cert(self, filename, metadata): -# req = self.make_request(self.pkey, metadata) -# serial = self.get_serial() -# cert = self.make_cert(req, serial, metadata.aliases) -# cert_out = cert.as_text()+cert.as_pem() -# certfile = open(self.data + '/' +filename, 'w') -# certfile.write(cert_out) -# certfile.close() -# cert_store = self.data + '/certstore' -# if not os.path.isdir(cert_store): -# os.mkdir(cert_store) -# storefile = open(cert_store + '/' + str(serial) + '.pem', 'w') -# storefile.write(cert_out) -# storefile.close() -# return cert_out -# -# def make_request(self, key, metadata): -# req = X509.Request() -# req.set_version(2) -# req.set_pubkey(key) -# name = X509.X509_Name() -# parts = [a.split('=') for a in self.cert_subject.split(',')] -# [setattr(name, k, v) for k,v in parts] -# name.CN = metadata.hostname -# req.set_subject_name(name) -# req.sign(key, 'sha1') -# return req -# -# def make_cert(self, req, serial, aliases): -# pkey = req.get_pubkey() -# if not req.verify(pkey): -# raise ValueError, 'Error verifying request' -# sub = req.get_subject() -# cert = X509.X509() -# cert.set_serial_number(serial) -# cert.set_version(2) -# cert.set_subject(sub) -# cert.set_issuer(self.ca_cert) -# cert.set_pubkey(pkey) -# notBefore = m2.x509_get_not_before(cert.x509) -# notAfter = m2.x509_get_not_after(cert.x509) -# m2.x509_gmtime_adj(notBefore, 0) -# m2.x509_gmtime_adj(notAfter, 60*60*24*long(self.cert_days)) -# exts = [ -# ('basicConstraints','CA:FALSE'), -# ('subjectKeyIdentifier','hash'), -# ('authorityKeyIdentifier','keyid,issuer:always'), -# ('nsCertType','SSL Server'), -# ] -# if aliases: -# exts.append(('subjectAltName', ','.join(['DNS:'+alias for alias in aliases]))) -# for ext in exts: -# cert.add_ext(X509.new_extension(ext[0],ext[1])) -# cert.sign(self.ca_key, 'sha1') -# return cert -# -# def HandleEvent(self, event=None): -# """Local event handler that does something....""" -# Bcfg2.Server.Plugin.DirectoryBacked.HandleEvent(self, event) -# -# def HandlesEntry(self, entry, _): -# """Handle entries dynamically.""" -# return entry.tag == 'Path' and (entry.get('name').endswith(self.hostkey) or entry.get('name').endswith(self.hostcert)) -# -- cgit v1.2.3-1-g7c22 From 347f9f2814dc8cdfc12a0c38fcb78c942622bf5d Mon Sep 17 00:00:00 2001 From: Graham Hagger Date: Mon, 1 Nov 2010 16:10:52 -0400 Subject: Now reads ca key passphrases from bcfg2.conf Corrected handling of modifications to xml files --- src/lib/Server/Plugins/SSLCA.py | 87 ++++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/lib/Server/Plugins/SSLCA.py b/src/lib/Server/Plugins/SSLCA.py index 8f1154d7f..d2137f23f 100644 --- a/src/lib/Server/Plugins/SSLCA.py +++ b/src/lib/Server/Plugins/SSLCA.py @@ -8,9 +8,11 @@ Notes: import Bcfg2.Server.Plugin +import Bcfg2.Options import lxml.etree import posixpath import tempfile +import os from subprocess import Popen, PIPE from ConfigParser import ConfigParser @@ -27,8 +29,13 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): __child__ = Bcfg2.Server.Plugin.FileBacked key_specs = {} cert_specs = {} + ca_passphrases = {} def HandleEvent(self, event=None): + """ + Updates which files this plugin handles based upon filesystem events. + Allows configuration items to be added/removed without server restarts. + """ action = event.code2str() if event.filename[0] == '/' or event.filename.startswith('CAs'): return @@ -39,16 +46,10 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): else: ident = self.handles[event.requestID][:-1] - self.logger.error('ACTION: %s, IDENT %s, FILENAME %s' % (action, ident, event.filename)) - fname = "".join([ident, '/', event.filename]) - - # TODO: check/fix handling of _all_ .xml file events vs hostfiles - if action in ['exists', 'created']: - if posixpath.isdir(epath): - self.AddDirectoryMonitor(epath[len(self.data):]) - if ident not in self.entries and posixpath.isfile(epath): + if event.filename.endswith('.xml'): + if action in ['exists', 'created', 'changed']: if event.filename.endswith('key.xml'): key_spec = dict(lxml.etree.parse(epath).find('Key').items()) self.key_specs[ident] = { @@ -58,8 +59,9 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): self.Entries['Path'][ident] = self.get_key elif event.filename.endswith('cert.xml'): cert_spec = dict(lxml.etree.parse(epath).find('Cert').items()) + ca = cert_spec.get('ca', 'default') self.cert_specs[ident] = { - 'ca': cert_spec.get('ca', 'default'), + 'ca': ca, 'format': cert_spec.get('format', 'pem'), 'key': cert_spec.get('key'), 'days': cert_spec.get('days', 365), @@ -70,20 +72,33 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): 'O': cert_spec.get('o'), 'emailAddress': cert_spec.get('emailaddress') } + cp = ConfigParser() + cp.read(self.core.cfile) + self.ca_passphrases[ca] = cp.get('sslca', ca+'_passphrase') self.Entries['Path'][ident] = self.get_cert - else: + if action == 'deleted': + if ident in self.Entries['Path']: + del self.Entries['Path'][ident] + else: + if action in ['exists', 'created']: + if posixpath.isdir(epath): + self.AddDirectoryMonitor(epath[len(self.data):]) + if ident not in self.entries and posixpath.isfile(epath): self.entries[fname] = self.__child__(epath) self.entries[fname].HandleEvent(event) - if action == 'changed': - self.entries[fname].HandleEvent(event) - elif action == 'deleted': - if fname in self.entries: - # a directory was deleted - del self.entries[fname] - else: + if action == 'changed': self.entries[fname].HandleEvent(event) + elif action == 'deleted': + if fname in self.entries: + del self.entries[fname] + else: + self.entries[fname].HandleEvent(event) def get_key(self, entry, metadata): + """ + either grabs a prexisting key hostfile, or triggers the generation + of a new key if one doesn't exist. + """ # set path type and permissions, otherwise bcfg2 won't bind the file permdata = {'owner':'root', 'group':'root', @@ -103,6 +118,9 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): entry.text = self.entries[filename].data def build_key(self, filename, entry, metadata): + """ + generates a new key according the the specification + """ type = self.key_specs[entry.get('name')]['type'] bits = self.key_specs[entry.get('name')]['bits'] if type == 'rsa': @@ -113,6 +131,10 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): return key def get_cert(self, entry, metadata): + """ + either grabs a prexisting cert hostfile, or triggers the generation + of a new cert if one doesn't exist. + """ # set path type and permissions, otherwise bcfg2 won't bind the file permdata = {'owner':'root', 'group':'root', @@ -140,20 +162,38 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): entry.text = cert def verify_cert(self): - return False + """ + check that a certificate validates against the ca cert, + and that it has not expired. + """ + # TODO: verify key validates and has not expired + # possibly also ensure no less than x days until expiry + return True def build_cert(self, entry, metadata): + """ + creates a new certificate according to the specification + """ req_config = self.build_req_config(entry, metadata) req = self.build_request(req_config, entry) - ca_config = "".join([self.data, '/CAs/', self.cert_specs[entry.get('name')]['ca'], '/', 'openssl.cnf']) + ca = self.cert_specs[entry.get('name')]['ca'] + ca_config = "".join([self.data, '/CAs/', ca, '/', 'openssl.cnf']) days = self.cert_specs[entry.get('name')]['days'] - cmd = "openssl ca -config %s -in %s -days %s -batch -passin pass:TODO!!!!" % (ca_config, req, days) - pdb.set_trace() + passphrase = self.ca_passphrases[ca] + cmd = "openssl ca -config %s -in %s -days %s -batch -passin pass:%s" % (ca_config, req, days, passphrase) cert = Popen(cmd, shell=True, stdout=PIPE).stdout.read() - # TODO: remove tempfiles + try: + os.unlink(req_config) + os.unlink(req) + except OSError: + self.logger.error("Failed to unlink temporary files") return cert def build_req_config(self, entry, metadata): + """ + generates a temporary openssl configuration file that is + used to generate the required certificate request + """ # create temp request config file conffile = open(tempfile.mkstemp()[1], 'w') cp = ConfigParser({}) @@ -189,6 +229,9 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): return conffile.name def build_request(self, req_config, entry): + """ + creates the certificate request + """ req = tempfile.mkstemp()[1] key = self.cert_specs[entry.get('name')]['key'] days = self.cert_specs[entry.get('name')]['days'] -- cgit v1.2.3-1-g7c22 From 6bbd4d6797d763777188d3984808f1ff692b2376 Mon Sep 17 00:00:00 2001 From: Graham Hagger Date: Tue, 2 Nov 2010 21:51:00 -0400 Subject: fixed lookup of ca options in bcfg2.conf, removing CA trees from SSLCA repo --- src/lib/Server/Plugins/SSLCA.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/lib/Server/Plugins/SSLCA.py b/src/lib/Server/Plugins/SSLCA.py index d2137f23f..823bf7fa0 100644 --- a/src/lib/Server/Plugins/SSLCA.py +++ b/src/lib/Server/Plugins/SSLCA.py @@ -29,7 +29,7 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): __child__ = Bcfg2.Server.Plugin.FileBacked key_specs = {} cert_specs = {} - ca_passphrases = {} + CAs = {} def HandleEvent(self, event=None): """ @@ -37,7 +37,7 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): Allows configuration items to be added/removed without server restarts. """ action = event.code2str() - if event.filename[0] == '/' or event.filename.startswith('CAs'): + if event.filename[0] == '/': return epath = "".join([self.data, self.handles[event.requestID], event.filename]) @@ -74,7 +74,7 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): } cp = ConfigParser() cp.read(self.core.cfile) - self.ca_passphrases[ca] = cp.get('sslca', ca+'_passphrase') + self.CAs[ca] = dict(cp.items('sslca_'+ca)) self.Entries['Path'][ident] = self.get_cert if action == 'deleted': if ident in self.Entries['Path']: @@ -177,10 +177,13 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): req_config = self.build_req_config(entry, metadata) req = self.build_request(req_config, entry) ca = self.cert_specs[entry.get('name')]['ca'] - ca_config = "".join([self.data, '/CAs/', ca, '/', 'openssl.cnf']) + ca_config = self.CAs[ca]['config'] days = self.cert_specs[entry.get('name')]['days'] - passphrase = self.ca_passphrases[ca] - cmd = "openssl ca -config %s -in %s -days %s -batch -passin pass:%s" % (ca_config, req, days, passphrase) + passphrase = self.CAs[ca].get('passphrase') + if passphrase: + cmd = "openssl ca -config %s -in %s -days %s -batch -passin pass:%s" % (ca_config, req, days, passphrase) + else: + cmd = "openssl ca -config %s -in %s -days %s -batch" % (ca_config, req, days) cert = Popen(cmd, shell=True, stdout=PIPE).stdout.read() try: os.unlink(req_config) -- cgit v1.2.3-1-g7c22 From e0208c832fa922cf3958f58f023bd13d053ff879 Mon Sep 17 00:00:00 2001 From: Graham Hagger Date: Wed, 3 Nov 2010 11:00:53 -0400 Subject: added verification of existing certs --- src/lib/Server/Plugins/SSLCA.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/lib/Server/Plugins/SSLCA.py b/src/lib/Server/Plugins/SSLCA.py index 823bf7fa0..a961e744a 100644 --- a/src/lib/Server/Plugins/SSLCA.py +++ b/src/lib/Server/Plugins/SSLCA.py @@ -154,20 +154,25 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): self.core.Bind(e, metadata) # check if we have a valid hostfile - if filename in self.entries.keys() and self.verify_cert(): + if filename in self.entries.keys() and self.verify_cert(filename, entry): entry.text = self.entries[filename].data else: cert = self.build_cert(entry, metadata) open(self.data + filename, 'w').write(cert) entry.text = cert - def verify_cert(self): + def verify_cert(self, filename, entry): """ check that a certificate validates against the ca cert, and that it has not expired. """ - # TODO: verify key validates and has not expired - # possibly also ensure no less than x days until expiry + chaincert = self.CAs[self.cert_specs[entry.get('name')]['ca']].get('chaincert') + cert = "".join([self.data, '/', filename]) + cmd = "openssl verify -CAfile %s %s" % (chaincert, cert) + proc = Popen(cmd, shell=True) + proc.communicate() + if proc.returncode != 0: + return False return True def build_cert(self, entry, metadata): -- cgit v1.2.3-1-g7c22