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/lib/Server/Plugins/SSLCA.py') 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