diff options
Diffstat (limited to 'src/lib/Bcfg2/Server')
-rw-r--r-- | src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py | 65 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py | 5 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugins/FileProbes.py | 6 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugins/SEModules.py | 6 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugins/SSLCA.py | 491 |
5 files changed, 324 insertions, 249 deletions
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py index 3a78b4847..73550cd9d 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py @@ -11,8 +11,39 @@ from Bcfg2.Server.Plugins.Cfg import CfgGenerator, SETUP try: import genshi.core from genshi.template import TemplateLoader, NewTextTemplate - from genshi.template.eval import UndefinedError + from genshi.template.eval import UndefinedError, Suite + #: True if Genshi libraries are available HAS_GENSHI = True + + def _genshi_removes_blank_lines(): + """ Genshi 0.5 uses the Python :mod:`compiler` package to + compile genshi snippets to AST. Genshi 0.6 uses some bespoke + magic, because compiler has been deprecated. + :func:`compiler.parse` produces an AST that removes all excess + whitespace (e.g., blank lines), while + :func:`genshi.template.astutil.parse` does not. In order to + determine which actual line of code an error occurs on, we + need to know which is in use and how it treats blank lines. + I've beat my head against this for hours and the best/only way + I can find is to compile some genshi code with an error and + see which line it's on.""" + code = """d = dict() + +d['a']""" + try: + Suite(code).execute(dict()) + except KeyError: + line = traceback.extract_tb(sys.exc_info()[2])[-1][1] + if line == 2: + return True + else: + return False + + #: True if Genshi removes all blank lines from a code block before + #: executing it; False indicates that Genshi only removes leading + #: and trailing blank lines. See + #: :func:`_genshi_removes_blank_lines` for an explanation of this. + GENSHI_REMOVES_BLANK_LINES = _genshi_removes_blank_lines() except ImportError: TemplateLoader = None # pylint: disable=C0103 HAS_GENSHI = False @@ -111,7 +142,17 @@ class CfgGenshiGenerator(CfgGenerator): # the traceback is just the beginning of the block. err = exc[1] stack = traceback.extract_tb(exc[2]) - lineno, func = stack[-1][1:3] + + # find the right frame of the stack + for frame in reversed(stack): + if frame[0] == self.name: + lineno, func = frame[1:3] + break + else: + # couldn't even find the stack frame, wtf. + raise PluginExecutionError("%s: %s" % + (err.__class__.__name__, err)) + execs = [contents for etype, contents, _ in self.template.stream if etype == self.template.EXEC] @@ -129,18 +170,20 @@ class CfgGenshiGenerator(CfgGenerator): # else, no EXEC blocks -- WTF? if contents: # we now have the bogus block, but we need to get the - # offending line. To get there, we do (line number - # given in the exception) - (firstlineno from the - # internal genshi code object of the snippet) + 1 = - # (line number of the line with an error within the - # block, with all multiple line breaks elided to a - # single line break) - real_lineno = lineno - contents.code.co_firstlineno - src = re.sub(r'\n\n+', '\n', contents.source).splitlines() + # offending line. To get there, we do (line number given + # in the exception) - (firstlineno from the internal + # genshi code object of the snippet) = (line number of the + # line with an error within the block, with blank lines + # removed as appropriate for + # :attr:`GENSHI_REMOVES_BLANK_LINES`) + code = contents.source.strip().splitlines() + if GENSHI_REMOVES_BLANK_LINES: + code = [l for l in code if l.strip()] try: + line = code[lineno - contents.code.co_firstlineno] raise PluginExecutionError("%s: %s at '%s'" % (err.__class__.__name__, err, - src[real_lineno])) + line)) except IndexError: raise PluginExecutionError("%s: %s" % (err.__class__.__name__, err)) diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py index db6810e7c..f8712213e 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py @@ -11,7 +11,8 @@ import Bcfg2.Options import Bcfg2.Server.Plugin import Bcfg2.Server.Lint # pylint: disable=W0622 -from Bcfg2.Compat import u_str, unicode, b64encode, walk_packages, any +from Bcfg2.Compat import u_str, unicode, b64encode, walk_packages, any, \ + oct_mode # pylint: enable=W0622 LOGGER = logging.getLogger(__name__) @@ -538,7 +539,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet): entry.get("name")) fname = os.path.join(self.path, generator.name) entry.set('mode', - str(oct(stat.S_IMODE(os.stat(fname).st_mode)))) + oct_mode(stat.S_IMODE(os.stat(fname).st_mode))) try: return generator.get_data(entry, metadata) except: diff --git a/src/lib/Bcfg2/Server/Plugins/FileProbes.py b/src/lib/Bcfg2/Server/Plugins/FileProbes.py index 8bd1d3504..5ec0d7280 100644 --- a/src/lib/Bcfg2/Server/Plugins/FileProbes.py +++ b/src/lib/Bcfg2/Server/Plugins/FileProbes.py @@ -24,7 +24,7 @@ import sys import pwd import grp import Bcfg2.Client.XML -from Bcfg2.Compat import b64encode +from Bcfg2.Compat import b64encode, oct_mode path = "%s" @@ -41,7 +41,7 @@ data = Bcfg2.Client.XML.Element("ProbedFileData", name=path, owner=pwd.getpwuid(stat[4])[0], group=grp.getgrgid(stat[5])[0], - mode=oct(stat[0] & 4095)) + mode=oct_mode(stat[0] & 4095)) try: data.text = b64encode(open(path).read()) except: @@ -101,7 +101,7 @@ class FileProbes(Bcfg2.Server.Plugin.Plugin, for data in datalist: if data.text is None: - self.logger.error("Got null response to %s file probe from %s" + self.logger.error("Got null response to %s file probe from %s" % (data.get('name'), metadata.hostname)) else: try: diff --git a/src/lib/Bcfg2/Server/Plugins/SEModules.py b/src/lib/Bcfg2/Server/Plugins/SEModules.py index 3edfb72a3..fa47f9496 100644 --- a/src/lib/Bcfg2/Server/Plugins/SEModules.py +++ b/src/lib/Bcfg2/Server/Plugins/SEModules.py @@ -40,8 +40,8 @@ class SEModules(Bcfg2.Server.Plugin.GroupSpool): #: objects as its EntrySet children. es_child_cls = SEModuleData - #: SEModules manages ``SELinux`` entries - entry_type = 'SELinux' + #: SEModules manages ``SEModule`` entries + entry_type = 'SEModule' #: The SEModules plugin is experimental experimental = True @@ -68,7 +68,7 @@ class SEModules(Bcfg2.Server.Plugin.GroupSpool): return name.lstrip("/") def HandlesEntry(self, entry, metadata): - if entry.tag in self.Entries and entry.get('type') == 'module': + if entry.tag in self.Entries: return self._get_module_filename(entry) in self.Entries[entry.tag] return Bcfg2.Server.Plugin.GroupSpool.HandlesEntry(self, entry, metadata) diff --git a/src/lib/Bcfg2/Server/Plugins/SSLCA.py b/src/lib/Bcfg2/Server/Plugins/SSLCA.py index b3a49c047..f83c04e87 100644 --- a/src/lib/Bcfg2/Server/Plugins/SSLCA.py +++ b/src/lib/Bcfg2/Server/Plugins/SSLCA.py @@ -3,253 +3,164 @@ certificates and their keys. """ import os import sys -import Bcfg2.Server.Plugin -import Bcfg2.Options -import lxml.etree +import logging import tempfile +import lxml.etree from subprocess import Popen, PIPE, STDOUT -from Bcfg2.Compat import ConfigParser, md5 +import Bcfg2.Options +import Bcfg2.Server.Plugin +from Bcfg2.Compat import ConfigParser from Bcfg2.Server.Plugin import PluginExecutionError +LOGGER = logging.getLogger(__name__) -class SSLCA(Bcfg2.Server.Plugin.GroupSpool): - """ The SSLCA generator handles the creation and management of ssl - certificates and their keys. """ - __author__ = 'g.hagger@gmail.com' - __child__ = Bcfg2.Server.Plugin.FileBacked - key_specs = {} - cert_specs = {} - CAs = {} - def __init__(self, core, datastore): - Bcfg2.Server.Plugin.GroupSpool.__init__(self, core, datastore) - self.infoxml = dict() +class SSLCAXMLSpec(Bcfg2.Server.Plugin.StructFile): + """ Base class to handle key.xml and cert.xml """ + attrs = dict() + tag = None + + def get_spec(self, metadata): + """ Get a specification for the type of object described by + this SSLCA XML file for the given client metadata object """ + entries = [e for e in self.Match(metadata) if e.tag == self.tag] + if len(entries) == 0: + raise PluginExecutionError("No matching %s entry found for %s " + "in %s" % (self.tag, + metadata.hostname, + self.name)) + elif len(entries) > 1: + LOGGER.warning("More than one matching %s entry found for %s in " + "%s; using first match" % (self.tag, + metadata.hostname, + self.name)) + rv = dict() + for attr, default in self.attrs.items(): + val = entries[0].get(attr.lower(), default) + if default in ['true', 'false']: + rv[attr] = val == 'true' + else: + rv[attr] = val + return rv + + +class SSLCAKeySpec(SSLCAXMLSpec): + """ Handle key.xml files """ + attrs = dict(bits='2048', type='rsa') + tag = 'Key' - 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] == '/': - return - epath = "".join([self.data, self.handles[event.requestID], - event.filename]) - if os.path.isdir(epath): - ident = self.handles[event.requestID] + event.filename - else: - ident = self.handles[event.requestID][:-1] - fname = os.path.join(ident, event.filename) +class SSLCACertSpec(SSLCAXMLSpec): + """ Handle cert.xml files """ + attrs = dict(ca='default', + format='pem', + key=None, + days='365', + C=None, + L=None, + ST=None, + OU=None, + O=None, + emailAddress=None, + append_chain='false') + tag = 'Cert' - if event.filename.endswith('.xml'): + def get_spec(self, metadata): + rv = SSLCAXMLSpec.get_spec(self, metadata) + rv['subjectaltname'] = [e.text for e in self.Match(metadata) + if e.tag == "SubjectAltName"] + return rv + + +class SSLCADataFile(Bcfg2.Server.Plugin.SpecificData): + """ Handle key and cert files """ + def bind_entry(self, entry, _): + """ Bind the data in the file to the given abstract entry """ + entry.text = self.data + return entry + + +class SSLCAEntrySet(Bcfg2.Server.Plugin.EntrySet): + """ Entry set to handle SSLCA entries and XML files """ + def __init__(self, _, path, entry_type, encoding, parent=None): + Bcfg2.Server.Plugin.EntrySet.__init__(self, os.path.basename(path), + path, entry_type, encoding) + self.parent = parent + self.key = None + self.cert = None + + def handle_event(self, event): + action = event.code2str() + fpath = os.path.join(self.path, event.filename) + + if event.filename == 'key.xml': + if action in ['exists', 'created', 'changed']: + self.key = SSLCAKeySpec(fpath) + self.key.HandleEvent(event) + elif event.filename == 'cert.xml': if action in ['exists', 'created', 'changed']: - if event.filename.endswith('key.xml'): - key_spec = lxml.etree.parse(epath, - parser=Bcfg2.Server.XMLParser - ).find('Key') - 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'): - cert_spec = lxml.etree.parse(epath, - parser=Bcfg2.Server.XMLParser - ).find('Cert') - ca = cert_spec.get('ca', 'default') - self.cert_specs[ident] = { - 'ca': ca, - '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'), - 'append_chain': - cert_spec.get('append_chain', - 'false').lower() == 'true', - } - self.CAs[ca] = dict(self.core.setup.cfp.items('sslca_%s' % - ca)) - self.Entries['Path'][ident] = self.get_cert - elif event.filename.endswith("info.xml"): - self.infoxml[ident] = Bcfg2.Server.Plugin.InfoXML(epath) - self.infoxml[ident].HandleEvent(event) - if action == 'deleted': - if ident in self.Entries['Path']: - del self.Entries['Path'][ident] + self.cert = SSLCACertSpec(fpath) + self.cert.HandleEvent(event) else: - if action in ['exists', 'created']: - if os.path.isdir(epath): - self.AddDirectoryMonitor(epath[len(self.data):]) - if ident not in self.entries and os.path.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: - del self.entries[fname] - else: - self.entries[fname].HandleEvent(event) - - def get_key(self, entry, metadata): + Bcfg2.Server.Plugin.EntrySet.handle_event(self, event) + + def build_key(self, entry, metadata): """ either grabs a prexisting key hostfile, or triggers the generation of a new key if one doesn't exist. """ - # 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 = os.path.join(path, "%s.H_%s" % (os.path.basename(path), - metadata.hostname)) - if filename not in list(self.entries.keys()): - self.logger.info("SSLCA: Generating new key %s" % filename) - key = self.build_key(entry) - open(self.data + filename, 'w').write(key) - entry.text = key - self.entries[filename] = self.__child__(self.data + filename) - self.entries[filename].HandleEvent() - else: - entry.text = self.entries[filename].data - - entry.set("type", "file") - if path in self.infoxml: - Bcfg2.Server.Plugin.bind_info(entry, metadata, - infoxml=self.infoxml[path]) - else: - Bcfg2.Server.Plugin.bind_info(entry, metadata) - - def build_key(self, entry): - """ generates a new key according the the specification """ - ktype = self.key_specs[entry.get('name')]['type'] - bits = self.key_specs[entry.get('name')]['bits'] + filename = "%s.H_%s" % (os.path.basename(entry.get('name')), + metadata.hostname) + self.logger.info("SSLCA: Generating new key %s" % filename) + key_spec = self.key.get_spec(metadata) + ktype = key_spec['type'] + bits = key_spec['bits'] if ktype == 'rsa': cmd = ["openssl", "genrsa", bits] elif ktype == 'dsa': cmd = ["openssl", "dsaparam", "-noout", "-genkey", bits] self.debug_log("SSLCA: Generating new key: %s" % " ".join(cmd)) - return Popen(cmd, stdout=PIPE).stdout.read() - - 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. - """ - path = entry.get('name') - filename = os.path.join(path, "%s.H_%s" % (os.path.basename(path), - metadata.hostname)) - - # first - ensure we have a key to work with - key = self.cert_specs[entry.get('name')].get('key') - key_filename = os.path.join(key, "%s.H_%s" % (os.path.basename(key), - metadata.hostname)) - if key_filename not in self.entries: - el = lxml.etree.Element('Path') - el.set('name', key) - self.core.Bind(el, metadata) - - # check if we have a valid hostfile - if (filename in self.entries.keys() and - self.verify_cert(filename, key_filename, entry)): - entry.text = self.entries[filename].data - else: - self.logger.info("SSLCA: Generating new cert %s" % filename) - cert = self.build_cert(key_filename, entry, metadata) - open(self.data + filename, 'w').write(cert) - self.entries[filename] = self.__child__(self.data + filename) - self.entries[filename].HandleEvent() - entry.text = cert - - entry.set("type", "file") - if path in self.infoxml: - Bcfg2.Server.Plugin.bind_info(entry, metadata, - infoxml=self.infoxml[path]) - else: - Bcfg2.Server.Plugin.bind_info(entry, metadata) - - def verify_cert(self, filename, key_filename, entry): - """ Perform certification verification against the CA and - against the key """ - ca = self.CAs[self.cert_specs[entry.get('name')]['ca']] - do_verify = ca.get('chaincert') - if do_verify: - return (self.verify_cert_against_ca(filename, entry) and - self.verify_cert_against_key(filename, key_filename)) - return True - - def verify_cert_against_ca(self, filename, entry): - """ - check that a certificate validates against the ca cert, - and that it has not expired. - """ - ca = self.CAs[self.cert_specs[entry.get('name')]['ca']] - chaincert = ca.get('chaincert') - cert = self.data + filename - cmd = ["openssl", "verify"] - is_root = ca.get('root_ca', "false").lower() == 'true' - if is_root: - cmd.append("-CAfile") - else: - # verifying based on an intermediate cert - cmd.extend(["-purpose", "sslserver", "-untrusted"]) - cmd.extend([chaincert, cert]) - self.debug_log("SSLCA: Verifying %s against CA: %s" % - (entry.get("name"), " ".join(cmd))) - res = Popen(cmd, stdout=PIPE, stderr=STDOUT).stdout.read() - if res == cert + ": OK\n": - self.debug_log("SSLCA: %s verified successfully against CA" % - entry.get("name")) - return True - self.logger.warning("SSLCA: %s failed verification against CA: %s" % - (entry.get("name"), res)) - return False - - def verify_cert_against_key(self, filename, key_filename): - """ - check that a certificate validates against its private key. - """ - cert = self.data + filename - key = self.data + key_filename - cert_md5 = \ - md5(Popen(["openssl", "x509", "-noout", "-modulus", "-in", cert], - stdout=PIPE, - stderr=STDOUT).stdout.read().strip()).hexdigest() - key_md5 = \ - md5(Popen(["openssl", "rsa", "-noout", "-modulus", "-in", key], - stdout=PIPE, - stderr=STDOUT).stdout.read().strip()).hexdigest() - if cert_md5 == key_md5: - self.debug_log("SSLCA: %s verified successfully against key %s" % - (filename, key_filename)) - return True - self.logger.warning("SSLCA: %s failed verification against key %s" % - (filename, key_filename)) - return False + proc = Popen(cmd, stdout=PIPE, stderr=PIPE) + key, err = proc.communicate() + if proc.wait(): + raise PluginExecutionError("SSLCA: Failed to generate key %s for " + "%s: %s" % (entry.get("name"), + metadata.hostname, err)) + open(os.path.join(self.path, filename), 'w').write(key) + return key - def build_cert(self, key_filename, entry, metadata): - """ - creates a new certificate according to the specification - """ + def build_cert(self, entry, metadata, keyfile): + """ generate a new cert """ + filename = "%s.H_%s" % (os.path.basename(entry.get('name')), + metadata.hostname) + self.logger.info("SSLCA: Generating new cert %s" % filename) + cert_spec = self.cert.get_spec(metadata) + ca = self.parent.get_ca(cert_spec['ca']) req_config = None req = None try: - req_config = self.build_req_config(entry, metadata) - req = self.build_request(key_filename, req_config, entry) - ca = self.cert_specs[entry.get('name')]['ca'] - ca_config = self.CAs[ca]['config'] - days = self.cert_specs[entry.get('name')]['days'] - passphrase = self.CAs[ca].get('passphrase') - cmd = ["openssl", "ca", "-config", ca_config, "-in", req, + req_config = self.build_req_config(metadata) + req = self.build_request(keyfile, req_config, metadata) + days = cert_spec['days'] + cmd = ["openssl", "ca", "-config", ca['config'], "-in", req, "-days", days, "-batch"] + passphrase = ca.get('passphrase') if passphrase: cmd.extend(["-passin", "pass:%s" % passphrase]) + + def _scrub_pass(arg): + """ helper to scrub the passphrase from the + argument list """ + if arg.startswith("pass:"): + return "pass:******" + else: + return arg + else: + _scrub_pass = lambda a: a + self.debug_log("SSLCA: Generating new certificate: %s" % - " ".join(cmd)) + " ".join(_scrub_pass(a) for a in cmd)) proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) (cert, err) = proc.communicate() if proc.wait(): @@ -266,12 +177,13 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): except OSError: self.logger.error("SSLCA: Failed to unlink temporary files: %s" % sys.exc_info()[1]) - if (self.cert_specs[entry.get('name')]['append_chain'] and - self.CAs[ca]['chaincert']): - cert += open(self.CAs[ca]['chaincert']).read() + if cert_spec['append_chain'] and 'chaincert' in ca: + cert += open(self.parent.get_ca(ca)['chaincert']).read() + + open(os.path.join(self.path, filename), 'w').write(cert) return cert - def build_req_config(self, entry, metadata): + def build_req_config(self, metadata): """ generates a temporary openssl configuration file that is used to generate the required certificate request @@ -298,16 +210,17 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): cfp.add_section(section) for key in defaults[section]: cfp.set(section, key, defaults[section][key]) + cert_spec = self.cert.get_spec(metadata) altnamenum = 1 - altnames = list(metadata.aliases) + altnames = cert_spec['subjectaltname'] + altnames.extend(list(metadata.aliases)) altnames.append(metadata.hostname) for altname in altnames: cfp.set('alt_names', 'DNS.' + str(altnamenum), altname) altnamenum += 1 for item in ['C', 'L', 'ST', 'O', 'OU', 'emailAddress']: - if self.cert_specs[entry.get('name')][item]: - cfp.set('req_distinguished_name', item, - self.cert_specs[entry.get('name')][item]) + if cert_spec[item]: + cfp.set('req_distinguished_name', item, cert_spec[item]) cfp.set('req_distinguished_name', 'CN', metadata.hostname) self.debug_log("SSLCA: Writing temporary request config to %s" % fname) try: @@ -317,16 +230,15 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): "config file: %s" % sys.exc_info()[1]) return fname - def build_request(self, key_filename, req_config, entry): + def build_request(self, keyfile, req_config, metadata): """ creates the certificate request """ fd, req = tempfile.mkstemp() os.close(fd) - days = self.cert_specs[entry.get('name')]['days'] - key = self.data + key_filename + days = self.cert.get_spec(metadata)['days'] cmd = ["openssl", "req", "-new", "-config", req_config, - "-days", days, "-key", key, "-text", "-out", req] + "-days", days, "-key", keyfile, "-text", "-out", req] self.debug_log("SSLCA: Generating new CSR: %s" % " ".join(cmd)) proc = Popen(cmd, stdout=PIPE, stderr=PIPE) err = proc.communicate()[1] @@ -334,3 +246,122 @@ class SSLCA(Bcfg2.Server.Plugin.GroupSpool): raise PluginExecutionError("SSLCA: Failed to generate CSR: %s" % err) return req + + def verify_cert(self, filename, keyfile, entry, metadata): + """ Perform certification verification against the CA and + against the key """ + ca = self.parent.get_ca(self.cert.get_spec(metadata)['ca']) + do_verify = ca.get('chaincert') + if do_verify: + return (self.verify_cert_against_ca(filename, entry, metadata) and + self.verify_cert_against_key(filename, keyfile)) + return True + + def verify_cert_against_ca(self, filename, entry, metadata): + """ + check that a certificate validates against the ca cert, + and that it has not expired. + """ + ca = self.parent.get_ca(self.cert.get_spec(metadata)['ca']) + chaincert = ca.get('chaincert') + cert = os.path.join(self.path, filename) + cmd = ["openssl", "verify"] + is_root = ca.get('root_ca', "false").lower() == 'true' + if is_root: + cmd.append("-CAfile") + else: + # verifying based on an intermediate cert + cmd.extend(["-purpose", "sslserver", "-untrusted"]) + cmd.extend([chaincert, cert]) + self.debug_log("SSLCA: Verifying %s against CA: %s" % + (entry.get("name"), " ".join(cmd))) + res = Popen(cmd, stdout=PIPE, stderr=STDOUT).stdout.read() + if res == cert + ": OK\n": + self.debug_log("SSLCA: %s verified successfully against CA" % + entry.get("name")) + return True + self.logger.warning("SSLCA: %s failed verification against CA: %s" % + (entry.get("name"), res)) + return False + + def verify_cert_against_key(self, filename, keyfile): + """ + check that a certificate validates against its private key. + """ + def _modulus(fname, ftype="x509"): + """ get the modulus from the given file """ + cmd = ["openssl", ftype, "-noout", "-modulus", "-in", fname] + self.debug_log("SSLCA: Getting modulus of %s for verification: %s" + % (fname, " ".join(cmd))) + proc = Popen(cmd, stdout=PIPE, stderr=PIPE) + rv, err = proc.communicate() + if proc.wait(): + self.logger.warning("SSLCA: Failed to get modulus of %s: %s" % + (fname, err)) + return rv.strip() # pylint: disable=E1103 + + certfile = os.path.join(self.path, filename) + cert = _modulus(certfile) + key = _modulus(keyfile, ftype="rsa") + if cert == key: + self.debug_log("SSLCA: %s verified successfully against key %s" % + (filename, keyfile)) + return True + self.logger.warning("SSLCA: %s failed verification against key %s" % + (filename, keyfile)) + return False + + def bind_entry(self, entry, metadata): + if self.key: + self.bind_info_to_entry(entry, metadata) + try: + return self.best_matching(metadata).bind_entry(entry, metadata) + except PluginExecutionError: + entry.text = self.build_key(entry, metadata) + return entry + elif self.cert: + key = self.cert.get_spec(metadata)['key'] + cleanup_keyfile = False + try: + keyfile = self.parent.entries[key].best_matching(metadata).name + except PluginExecutionError: + cleanup_keyfile = True + # create a temp file with the key in it + fd, keyfile = tempfile.mkstemp() + os.chmod(keyfile, 384) # 0600 + el = lxml.etree.Element('Path', name=key) + self.parent.core.Bind(el, metadata) + os.fdopen(fd, 'w').write(el.text) + + try: + self.bind_info_to_entry(entry, metadata) + try: + best = self.best_matching(metadata) + if self.verify_cert(best.name, keyfile, entry, metadata): + return best.bind_entry(entry, metadata) + except PluginExecutionError: + pass + # if we get here, it's because either a) there was no best + # matching entry; or b) the existing cert did not verify + entry.text = self.build_cert(entry, metadata, keyfile) + return entry + finally: + if cleanup_keyfile: + try: + os.unlink(keyfile) + except OSError: + err = sys.exc_info()[1] + self.logger.error("SSLCA: Failed to unlink temporary " + "key %s: %s" % (keyfile, err)) + + +class SSLCA(Bcfg2.Server.Plugin.GroupSpool): + """ The SSLCA generator handles the creation and management of ssl + certificates and their keys. """ + __author__ = 'g.hagger@gmail.com' + es_cls = lambda self, *args: SSLCAEntrySet(*args, parent=self) + es_child_cls = SSLCADataFile + + def get_ca(self, name): + """ get a dict describing a CA from the config file """ + return dict(self.core.setup.cfp.items("sslca_%s" % name)) |