Ottomata has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/359960 )

Change subject: Initial commit of certpy
......................................................................

Initial commit of certpy

Bug: T166167
Change-Id: Ie1a251caa6eaafc3d53120eff85ddb5e7b77369c
---
A .gitignore
A .gitreview
A README.md
A certpy/__init__.py
A certpy/ca.py
A certpy/certificate.py
A certpy/config.py
A certpy/key.py
A certpy/main.py
A certpy/puppet_sign_cert.rb
A certpy/util.py
A examples/example.certs.yaml
A setup.py
13 files changed, 1,774 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/operations/software/certpy 
refs/changes/60/359960/1

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..09ee3be
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,68 @@
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+.hypothesis/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+#Ipython Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+venv
+virtualenv
diff --git a/.gitreview b/.gitreview
new file mode 100644
index 0000000..bf41f50
--- /dev/null
+++ b/.gitreview
@@ -0,0 +1,6 @@
+[gerrit]
+host=gerrit.wikimedia.org
+port=29418
+project=operations/software/certpy.git
+defaultbranch=master
+defaultrebase=0
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ef377a4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,156 @@
+# certpy
+
+Manages and generates OpenSSL key and certificate files declared in a YAML 
manifest.
+
+Generation of the following file types is currently supported:
+
+- ```.key``` - OpenSSL private key in .pem format.
+- ```.crt``` - OpenSSL public key certificate in .pem format
+- ```.p12``` - PKCS#12 key store file.
+- ```.jks``` - Java keystore file.
+
+## Usage
+
+```
+certpy -h
+Reads in Certificate and CA manifest configuration and manages
+OpenSSL keys, certificates, and authorities in various formats and stores.
+
+Usage: certpy [options] <manifest_path>
+
+    <manifest_path> is the path to the certificate and authority manifest 
config file(s).
+                    If this is a directory, then all files that match 
--manifest-glob
+                    (default '*.certs.yaml') will be loaded as manifests.
+
+Options:
+    -h --help                   Show this help message and exit.
+    -d --working-dir            cd to this directory before generating 
anything.
+                                This allows relative file paths in the 
manifest to
+                                be generated in a different location than the 
current cwd.
+    -G --generate-certs         Generate all certificate (excluding CA certs).
+    -A --generate-authorities   Generate all CA certficiate files, if possible.
+    -F --force                  If given a generate option without --force, 
any existing files will not
+                                be overwritten.  If want to overwrite files, 
provide --force.
+    -v --verbose                Turn on verbose debug logging.
+
+```
+
+certpy's CLI works with a YAML manifest.  The manifest declares various 
certificate and key
+parameters that are then used to instantiate model classes that can generate 
key and
+certificate files.
+
+
+### certpy manifest .yaml
+
+The manifest yaml attempts to match the kwargs that can be used to instantiate
+various model classes.  This allows new model subclasses to be created and
+instantiated with manfiest configuration without having to write code to
+handle the config -> code instantiation.
+
+A manifest can declare 2 top level configration keys, ```authorities``` and 
```certs```
+
+#### ```certs```
+
+The ```certs``` config object should be a hash of certificate common names to
+certificate parameters.
+
+```
+certs:
+  # Common name of the certificate
+  hostname1.example.org:
+    # Directory where OpenSSL files will live
+    path: certificates/hostname1.example.org
+    # Name of the CA to use for this certificate.  This must match
+    # A name of an authority in the authorities manifest. (optional)
+    ca: rootCa
+    # x509 subject
+    subject:
+      C: US
+    # DNS alternate names to put in the SAN (optional)
+    dns_alt_names: [example.org]
+    # Password to use for keystore files.
+    password: qwerty
+    # Key class configuration
+    key:
+      # Fully qualified (with module name if needed) class name to use for the 
key.
+      type: ECKey
+      # Private key password.  Optional.  If not provided,
+      # the certificate's keystore password will be used.
+      password: qwerty
+
+  hostname2.example.org
+    ...
+````
+
+#### ```authorities```
+
+The ```authorities``` config object should be a hash of CA names to CA and CA 
certificate
+parameters.
+
+```
+authorities:
+  # The name of this CA.  This will be used as the CA cert's common name.
+  rootCa:
+    # Fully qualified (with module name if needed) class name to use for the 
CA class.
+    # If not given, SelfSigningCA will be used. (TODO)
+    type: SelfSigningCA
+    # A Certificate config object, with the same structure used to declare 
certs in
+    # the certificates config object.
+    cert:
+      path: .certificates/rootCa
+      subject:
+        C: US
+      password: qwerty
+      key:
+        type: RSAKey
+        password: qwerty
+  #TODO: document and test PuppetCA.
+  puppet:
+    type: PuppetCA
+```
+
+Note that some authorities here may not be generateable, and as such will not 
need to declare
+the specifics of the CA certs.
+
+## certpy as module
+certpy can be used as a python library to model OpenSSL Keys, Certificates and 
CAs.  The
+underlying code does not depend on a Python implementation of OpenSSL, but 
instead
+shells out to the openssl CLI and the Java keytool CLI in order to generate 
and verify
+certificate files in different formats.
+
+There are 3 top level models:
+
+### ```Key```
+A ```Key``` class represents an OpenSSL private key.  See docuemntation for 
```certpy.key```
+for more information and instructions on how to implement more ```Key``` 
subclasses.
+
+Currently this package provides an ```RSAKey``` and an ```ECKey``` (elliptic 
curve) ```Key``` implementations.
+
+### ```Certificate```
+A ```Certificate``` class represents OpenSSL certificate and files.  It is 
used to ensure that
+supported file formats exist, and to generate them if they don't.  
```Certificate```
+is not meant to be subclassed.
+
+### ```CA```
+A ``CA`` class is an implementation of a Certificate Authority.  It is meant 
to wrap the
+``Certificate`` class, as often a ``CA`` has it's own key and certificate 
files.  However, the
+main purpose is to provide an interface for ```Certificate```s to generate CSR
+(Certificate Signing Requests) and have a CA sign and generate a signed public 
```.crt```
+file.  It is not necessary for ```CA``` subclasses to be given a 
````Certfificate```` instance,
+but they will need to at least instantiate a ```self.ca_cert``` 
```Certificate``` instance
+themselves, even with dummy values, so that logging and status reporting of 
certificate file existance works.
+
+Every ```CA``` subclass should implement the ```sign``` and ```verify``` 
methods, and if possible,
+a ```generate``` method.
+
+Currently this package provides a ```SelfSigningCA``` and ```PuppetCA``` 
implementations.
+
+#### ```SelfSigningCa```
+This ```CA``` uses its own Certificate instance to signing other Certificate 
files.
+
+#### ```PuppetCA```
+This ```CA``` uses a puppet master CA instance to sign CSRs and generate 
```.crt``` files.
+Because Puppet manages its own certificate files, usage of this ```CA``` must 
be done on the
+same node as the puppet master CA, so that it can copy the Puppet generated 
```.crt.pem``` file
+out of the Puppet CA paths into the ```Certificate```'s expected ```.crt``` 
file path.
+
diff --git a/certpy/__init__.py b/certpy/__init__.py
new file mode 100755
index 0000000..0427621
--- /dev/null
+++ b/certpy/__init__.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+TODO: docs here
+"""
+
+from .ca import *
+from .certificate import *
+from .config import *
+from .key import *
+from .util import *
+from .main import *
diff --git a/certpy/ca.py b/certpy/ca.py
new file mode 100644
index 0000000..44d7d2b
--- /dev/null
+++ b/certpy/ca.py
@@ -0,0 +1,270 @@
+# -*- coding: utf-8 -*-
+
+"""
+A CA class needs to be able to:
+    1. Sign a CSR and generate a new certificate file
+    2. Verify the a certificate file is signed with itself
+
+If relevant, we also want a CA class to be able to support self generation,
+i.e. a self signing root CA.
+
+As such, a CA subclass class that can used by a Certificate should implement 
the following
+3 methods: sign, verify, and optionally, generate.  All subclasses must also 
set
+self.cert to a Certificate instance, even if the Certificate files cannot be
+referenced or generated.  (This is needed for comprehensive status printing.)
+
+    '''
+    Given a Certificate instance cert, with a existant .csr and .key file,
+    this signs the certificate, resulting in the creation of cert.crt_file.
+
+    :param cert Certificate
+    '''
+    def sign(self, cert):
+        ...
+
+    '''
+    Given a Certificate instance cert, this should verify that cert.crt_file
+    there is signed by this CA.
+
+    :param cert Certificate
+    :return boolean
+    '''
+    def verify(self, cert):
+        ...
+
+    '''
+    Implement this if for CAs that can generate themselves.  If the CA cannot 
generate
+    itself, the base CA class will raise a NotImplementedError.
+    Usually, generate will be implemented using a self signed Certificate 
(CA-less)
+    instance given to your CA class' constructor.
+    '''
+    def generate(self):
+        ...
+
+
+"""
+
+
+from .key import Key, RSAKey
+from .certificate import Certificate, Subject, default_subject # TODO REMOVE
+from .util import openssl, run_command, mkdirs, get_class_logger
+
+import logging
+import os
+import tempfile
+
+__all__ = ('CA', 'SelfSigningCA', 'PuppetCA')
+
+
+class CA(object):
+    """
+    Base 'abstract' class for CAs.  You should implement these following 
methods
+    in subclass CAs classes.
+    """
+    def __init__(self, **kwargs):
+        """
+        Constructor.  All CA subclass construtors should take **kwargs and
+        should all set self.name.
+        """
+        raise NotImplementedError('__init__ not implemented for ' +  str(self))
+
+    def generate(self, force=False):
+        raise NotImplementedError('generate not implemented for ' + str(self))
+
+    def sign(self, cert):
+        raise NotImplementedError('sign not implemented for ' + str(self))
+
+    def verify(self, cert):
+        raise NotImplementedError('verify not implemented for ' + str(self))
+
+    def __repr__(self):
+        if hasattr(self, 'name'):
+            return '{}({})'.format(self.__class__.__name__, self.name)
+        else:
+            return self.__class__.__name__
+
+
+class SelfSigningCA(CA):
+    """
+    A SelfSigningCA uses a self-signed (CA-less) Certificate to sign
+    other Certificates.
+    """
+    def __init__(self, cert, **kwargs):
+        """
+        :param cert     CA Certificate instance. This should
+                        represent the CA's key and certificate files.
+        """
+        self.ca_cert = cert
+        self.name = self.ca_cert.name
+        self.log = get_class_logger(self)
+
+
+    def sign(self, cert):
+        """
+        Signs a Certificate instance with this CA's certificate.  This requires
+        that cert.csr_file has been generated and exists, as it will be used 
when
+        signing the new certificate.
+
+        :param cert  Certificate instance to sign.
+        """
+        command = [
+            openssl,
+            'x509',
+            '-req',
+            '-CAcreateserial',
+            '-CA', self.ca_cert.crt_file,
+            '-CAkey', self.ca_cert.key.key_file,
+            '-in', cert.csr_file,
+            '-out', cert.crt_file
+        ]
+        if cert.digest:
+            command += ['-{}'.format(cert.digest)]
+        if cert.expiry_days:
+            command += ['-days', str(cert.expiry_days)]
+        # If this certificate's CSR was created with a CSR config file,
+        # then we should also pass the same config file when signing.
+        # This ensure that the requested SANs are included in the signed
+        # certificate.
+        if cert.csr_conf_file:
+            # TODO: perhaps somehow parameterize what extensions to use?
+            command += ['-extfile', cert.csr_conf_file]
+        if cert.dns_alt_names:
+            command+= ['-extensions', 'v3_req']
+
+        self.log.info('Signing CSR from {} with {}'.format(cert.csr_file, 
self))
+        if not run_command(command, creates=cert.crt_file):
+            raise RuntimeError('Signing CSR {} failed'.format(cert.csr_file), 
self)
+
+
+    def verify(self, cert):
+        """
+        Verifies that cert was signed with this CA.
+
+        :param cert Certificate instance to verify
+        :return boolean
+        """
+        # Verify that this CA was used to sign cert.crt_file.
+        command = [
+            openssl,
+            'verify',
+            '-CAfile', self.ca_cert.crt_file,
+            cert.crt_file
+        ]
+        return run_command(command)
+
+
+    def generate(self, force=False):
+        """
+        Generates this CA's key and certificate files by calling 
ca_cert.genearte().
+
+        :param force    If true, files will be re-generated even if they 
already exist.
+        """
+        self.log.info('Generating CA certificate')
+        return self.ca_cert.generate(force=force)
+
+    def __repr__(self):
+        return '{}(key_file={}, crt_file={})'.format(
+            self.__class__.__name__, self.ca_cert.key.key_file, 
self.ca_cert.crt_file
+        )
+
+
+
+# TODO: This has not been tested!!!
+class PuppetCA(CA):
+    """
+    A PuppetCA signs and generates a new Certificate using the Puppet CA HTTP 
API and CLI.
+    It does this by shelling out to a custom ruby script that imports and 
extends
+    Puppet internals to support wildcard certificates.  This CA cannot be 
generated, as
+    it requires an already configured Puppet CA.  This class can only be used 
on
+    the same node where the Puppet CA lives, as it will attempt to copy the 
certificate file
+    that Puppet CA generates into cert.crt_file.
+
+    :param puppet_hostname      HTTP hostname for Puppet API
+    :param puppet_port          HTTP port for Puppet API
+    :param puppet_ca_path       Path where Puppet CA stores its certificate 
files.
+    :param puppet_sign_script   Path of custom ruby script to use for signing 
certificates with Puppet CA
+    """
+    def __init__(
+            self,
+            puppet_hostname='puppet',
+            puppet_port=8140,
+            puppet_ca_path='/var/lib/puppet/server/ssl/ca',
+            puppet_sign_script='/usr/local/bin/puppet-sign-cert',
+            **kwargs
+        ):
+
+        self.name = '{}:{}'.format(puppet_hostname, puppet_port)
+        self.log = get_class_logger(self)
+
+        self.puppet_hostname = puppet_hostname
+        self.puppet_port = puppet_port
+        self.puppet_ca_path = puppet_ca_path
+        self.puppet_sign_script = puppet_sign_script
+
+        # Instantiate a ca_cert that will be used when a cert
+        # needs to reference its CA certificate.  This is needed
+        # by Certificates where ca is a PuppetCA in order to
+        # generate p12 and jks files that include the Puppet CA certificate.
+        self.ca_cert = Certificate(
+            self.name,
+            self.puppet_ca_path,
+            # Dummy password and subject.
+            password=None,
+            subject={},
+            # read_only = True ensures that no accidental call
+            # to generate() on this CA cert will ever run.
+            read_only = True
+        )
+        # Puppet CA stores certs with different filenames than our Certificate 
convention,
+        # so set them to what they should be.
+        # TODO: we could do this by create a PuppetCACertificate subclass of 
Certificate.  Hm.
+        self.ca_cert.key_file = os.path.join(self.puppet_ca_path, 'ca_key.pem')
+        self.ca_cert.crt_file = os.path.join(self.puppet_ca_path, 'ca_crt.pem')
+
+
+    def sign(self, cert):
+        """
+        Signs a Certificate instance using Puppet CA. The generated signed 
certificate file
+        will be copied out of puppet_cert_path to cert.crt_file.  This requires
+        that cert.csr_file has been generated and exists, as it will be used 
when
+        signing the new certificate.
+
+        :param cert Certificate instance
+        """
+        #  TODO verify that cert.crt_file exists
+
+        # call out to puppet_sign_cert.rb and then copy file from puppetmaster 
dirs into base_path
+        command = [
+            self.puppet_sign_script,
+            '-H', self.puppet_hostname,
+            '-P', str(self.puppet_port),
+            cert.csr_file
+        ]
+        self.log.info('Submitting CSR {} to Puppet CA'.format(cert.csr_file))
+
+        if not run_command(command):
+            raise RuntimeError('Signing CSR {} with Puppet CA 
failed'.format(cert.csr_file), self)
+
+
+        # Path to the signed certificate file that puppet will generate.
+        puppet_crt_file = os.path.join(self.puppet_cert_path, 'signed', 
'{}.pem'.format(cert.name))
+        if not os.path.exists(puppet_crt_file):
+            raise RuntimeError(
+                'Signing CSR {} with Puppet CA succeeded, but {} does not 
exist. '
+                'This should not happen.'.format(cert.csr_file, 
puppet_crt_file),
+                self
+            )
+
+        # COPY new puppet .crt (.pem) file into crt_file location
+        mkdirs(cert.path)
+        self.log.info('Copying signed certificate from {} to {}'.format(
+            puppet_crt_file, cert.crt_file
+        ))
+        shutil.copyfile(puppet_crt_file, cert.crt_file)
+        if not os.path.exists(cert.crt_file):
+            raise RuntimeError(
+                'Copying signed certificate from {} to {} failed'.format(
+                    cert.csr_file, puppet_crt_file
+                ),
+                self
+            )
diff --git a/certpy/certificate.py b/certpy/certificate.py
new file mode 100644
index 0000000..c4f75ba
--- /dev/null
+++ b/certpy/certificate.py
@@ -0,0 +1,449 @@
+# -*- coding: utf-8 -*-
+
+from datetime import datetime
+import os
+import logging
+import shutil
+
+from .key import RSAKey
+from .util import openssl, keytool, run_command, mkdirs, get_class_logger, 
is_in_keystore
+
+subject_fields = ['C', 'ST', 'O', 'OU', 'DN', 'CN', 'L', 'SN', 'GN']
+
+__all__ = ('Certificate', 'Subject', 'SubjectKeyError')
+
+
+class SubjectKeyError(KeyError):
+    def __init__(self, key):
+        super().__init__('{} is not a valid x509 subject field, must be one of 
{}'.format(
+            key, ', '.join(subject_fields)
+        ))
+
+# TODO: support long names?  mehhhhhh. use named tuple?? or UserDict
+# TODO: This needs much cleanup.  WIP
+class Subject(dict):
+    @staticmethod
+    def factory(d):
+        subject_dict = {k.upper(): v for k,v in d.items()}
+        for k in subject_dict.keys():
+            if k not in subject_fields:
+                raise SubjectKeyError(k)
+
+        return Subject(subject_dict)
+
+    def openssl_string(self):
+        a = ['{}={}'.format(k.upper(), v) for k, v in self.items()]
+        return '/' + '/'.join(a) + '/'
+
+    def keytool_string(self):
+        a = ['{}={}'.format(k.lower(), v) for k, v in self.items()]
+        return ', '.join(a)
+
+
+
+# TODO: remove
+default_subject = Subject.factory({
+    'O': 'WMF',
+    'C': 'US',
+})
+
+
+
+csr_config_template = """
+[ req ]
+distinguished_name         = req_distinguished_name
+{req_extensions}
+
+[ req_distinguished_name ]
+countryName                = Country Name (2 letter code)
+stateOrProvinceName        = State or Province Name (full name)
+localityName               = Locality Name (eg, city)
+organizationName           = Organization Name (eg, company)
+commonName                 = Common Name (e.g. server FQDN or YOUR name)
+"""
+
+san_config_template = """
+[ v3_req ]
+subjectAltName             = @alt_names
+
+[ alt_names ]
+{alt_names}
+"""
+
+dns_alt_name_template = 'DNS.{i} = {alt_name}'
+
+def render_csr_config(dns_alt_names=None):
+    req_extensions = ''
+    alt_names = ''
+    if dns_alt_names:
+        req_extensions = 'req_extensions             = v3_req'
+        for i, name in enumerate(dns_alt_names):
+            alt_names += dns_alt_name_template.format(i=i+1, alt_name=name) + 
'\n'
+
+
+    content = csr_config_template.format(req_extensions=req_extensions)
+    if dns_alt_names:
+        content += san_config_template.format(alt_names=alt_names)
+
+    return content
+
+
+
+
+class Certificate(object):
+    """
+    Represents an OpenSSL certificate.  Handles generation of
+    subject is a dict mapping x509 subject  keys to values.
+    """
+    def __init__(
+        self,
+        name,
+        path,
+        password,
+        key=None,
+        subject=default_subject,
+        dns_alt_names=None,
+        expiry_days=None,
+        digest=None,
+        ca=None,
+        read_only=False
+    ):
+        self.name = name
+        self.path = os.path.abspath(path)
+        self.key = key
+
+        subject['CN'] = name # TODO: ???
+        self.subject = Subject.factory(subject)
+        self.dns_alt_names = dns_alt_names
+
+        self.expiry_days = expiry_days
+        self.password = password
+
+        # TODO validate that digest is supported (e.g. sha256)
+        self.digest = digest
+
+        # Verify that CA has required methods. ( duck typing :) )
+        if ca and (
+            not hasattr(ca, 'sign') or
+            not hasattr(ca, 'verify')
+            ):
+            raise RuntimeError(
+                'Cannot instante new Certificate. ca {} should implement '
+                'both sign and verify methods.'.format(ca),
+                self
+            )
+
+        self.ca = ca
+
+        self.read_only = read_only
+
+        # If not give a key, then create a new RSA key by default.  TODO: keep 
this?
+        if key:
+            self.key = key
+        else:
+            self.key = RSAKey(name, path, password)
+
+        # Private Key in .pem format
+        self.key_file = self.key.key_file
+        # Certificate Signing Request
+        self.csr_file = os.path.join(self.path, '{}.csr'.format(self.name))
+        # CSR config file.  Needed to support SANs.
+        self.csr_conf_file = os.path.join(self.path, 
'{}.csr.cnf'.format(self.name))
+        # Public Signed Certificate in .pem format
+        self.crt_file = os.path.join(self.path, '{}.crt'.format(self.name))
+
+        # PKCS#12 'keystore' file
+        self.p12_file = os.path.join(self.path, '{}.p12'.format(self.name))
+        # Java Keystore
+        self.jks_file = os.path.join(self.path, '{}.jks'.format(self.name))
+
+        self.log = get_class_logger(self)
+
+    # TODO: rename this since we are removing paths, and clean up conditional 
logic.
+    def should_generate(self, path=None, force=False):
+        should_generate = True
+        if self.read_only:
+            self.log.warn(
+                'Cannot call any generate method on a read_only Certificate.  
Skipping generation.'
+            )
+            should_generate = False
+        elif path and os.path.exists(path):
+            if force:
+                self.log.warn(
+                    '{} exists, but force is True.  Removing before '
+                    'continuing with generation.'.format(path)
+                )
+                if os.path.isdir(path):
+                    shutil.rmtree(path)
+                else:
+                    os.remove(path)
+                should_generate = True
+            else:
+                self.log.warn(
+                    '{} exists, skipping generation.'.format(path)
+                )
+                should_generate = False
+        else:
+            should_generate = True
+
+        return should_generate
+
+    def generate(self, force=False):
+        # This top level should_generate check looks for the existence of 
self.path.
+        # It will not even attempt to recreate the files if self.path exists 
and force=False.
+        if not self.should_generate(self.path, force):
+            return False
+
+        self.log.info('Generating all files...')
+        mkdirs(self.path)
+
+        self.key.generate(force=force)
+        self.generate_crt(force=force)
+        self.generate_p12(force=force)
+        self.generate_keystore(force=force)
+
+        return True
+
+    def generate_crt(self, force=False):
+        if not self.should_generate(self.crt_file, force):
+            return False
+
+        # If we are going to include DNS alt names in the cert,
+        # we'll need a CSR conf file that specifies them.  For consistency,
+        # generate this conf file even if there are no DNS alt names specified.
+        self._generate_csr_conf(force=force)
+
+        # If no ca was provided, then generate a self signed certificate
+        if not self.ca:
+            self._self_generate_crt(force=force)
+        else:
+            self._ca_generate_crt(force=force)
+
+
+    def _self_generate_crt(self, force=False):
+        if not self.should_generate(self.crt_file, force):
+            return False
+
+        # Generate the certificate without a ca
+        command = [
+            openssl,
+            'req',
+            '-x509',
+            '-new',
+            '-config', self.csr_conf_file,
+            '-subj', self.subject.openssl_string(),
+            '-key', self.key.key_file,
+            '-out', self.crt_file
+        ]
+        if self.digest:
+            command += ['-{}'.format(self.digest)]
+        if self.expiry_days:
+            command =+ ['-days', str(self.expiry_days)]
+        if self.key.password:
+            command += ['-passin', 'pass:{}'.format(self.key.password)]
+        # If we need to instruct the x509 cert to use our custom SAN
+        # extensions section in the conf file.
+        if self.dns_alt_names:
+            command += ['-extensions', 'v3_req']
+
+        self.log.info('Generating self signed certificate')
+        if not run_command(command, creates=self.crt_file):
+            raise RuntimeError('Certificate generation failed', self)
+
+        return True
+
+    def _ca_generate_crt(self, force=False):
+        if not self.should_generate(self.crt_file, force):
+            return False
+
+        self.generate_csr(force=force)
+        self.log.info('Sending CSR to {}'.format(self.ca))
+        self.ca.sign(self)
+
+        # Verify that crt_file was created by the CA when it signed CSR.
+        if not os.path.exists(self.crt_file):
+            raise RuntimeError(
+                '{} does not exist even though {} signed and generated a '
+                'certificate.  This should not happen'.format(self.crt_file, 
self.ca)
+            )
+
+        self.log.info('Verifying signed certificate with {}'.format(self.ca))
+        if not self.ca.verify(self):
+            raise RuntimeError('Certificate {} verification failed with 
{}'.format(
+                self.crt_file, self.ca
+            ))
+
+        return True
+
+    def generate_csr(self, force=False):
+        if not self.should_generate(self.csr_file, force):
+            return False
+
+        # In order to support adding SANs to the CSR,
+        # we need to use a config file.  To be consistent, we generate
+        # and use this config file, event if we don't have any SANS
+        self._generate_csr_conf(force=force)
+
+        command = [
+            openssl,
+            'req',
+            '-new',
+            '-config', self.csr_conf_file,
+            '-subj', self.subject.openssl_string(),
+            '-key', self.key.key_file,
+            '-out', self.csr_file
+        ]
+        if self.digest:
+            command += ['-{}'.format(self.digest)]
+        if self.key.password:
+            command += ['-passin', 'pass:{}'.format(self.key.password)]
+
+        self.log.info('Generating CSR')
+        if not run_command(command, creates=self.csr_file):
+            raise RuntimeError('CSR generation failed', self)
+
+        return True
+
+    def _generate_csr_conf(self, force=False):
+        if not self.should_generate(self.csr_conf_file, force):
+            return False
+
+        csr_config_content = render_csr_config(self.dns_alt_names)
+        with open(self.csr_conf_file, 'w') as f:
+            f.write(csr_config_content)
+            f.flush()
+        if not os.path.exists(self.csr_conf_file):
+            raise RuntimeError(
+                'Attempted to write CSR conf file {}, but it does not exist. '
+                'This should not happen.'.format(self.csr_conf_file),
+                self
+            )
+        return True
+
+
+    def generate_p12(self, force=False):
+        if not self.should_generate(self.p12_file, force):
+            return False
+
+        command = [
+            openssl,
+            'pkcs12',
+            '-export',
+            '-name', self.name,
+            # private key
+            '-inkey', self.key.key_file,
+            #  Public certificate
+            '-in', self.crt_file,
+            # output p12 keystore with password
+            '-passout', 'pass:{}'.format(self.password),
+            '-out', self.p12_file,
+        ]
+        if self.key.password:
+            command += ['-passin', 'pass:{}'.format(self.key.password)]
+
+        if self.ca:
+            #  TODO: This will not work with PuppetCA!
+            command += ['-CAfile', self.ca.ca_cert.crt_file]
+
+        self.log.info('Generating PKCS12 keystore')
+        if not run_command(command, creates=self.p12_file):
+            raise RuntimeError('PKCS12 file generation failed', self)
+
+        # Verify that the cert is in the P12 file.
+        if not is_in_keystore(self.name, self.p12_file, self.password):
+            raise RuntimeError(
+                'Generation of PKS12 keystore succeeded, but a key for '
+                '{} is not in {}. This should not happen'.format(
+                    self.name, self.jks_file
+                )
+            )
+
+        # TODO: do we need to import the ca_cert into the PKS12 keystore too?
+
+        return True
+
+    def generate_keystore(self, force=False):
+        if not self.should_generate(self.jks_file, force):
+            return False
+
+        command = [
+            keytool,
+            '-importkeystore',
+            '-noprompt',
+            '-alias', self.name,
+            '-srcstoretype', 'PKCS12',
+            '-srcstorepass', self.password,
+            '-srckeystore', self.p12_file,
+            '-deststorepass', self.password,
+            '-destkeystore', self.jks_file
+        ]
+        if self.key.password:
+            command += ['-srckeypass', self.key.password, '-destkeypass', 
self.key.password]
+
+        self.log.info('Generating Java keystore')
+        if not run_command(command, creates=self.jks_file):
+            raise RuntimeError(
+                'Java Keystore generation and import of certificate failed', 
self
+            )
+
+        # Verify that the cert is in the Java Keystore.
+        if not is_in_keystore(self.name, self.jks_file, self.password):
+            raise RuntimeError(
+                'Java Keystore generation and import of certificate '
+                'succeeded, but a key for {} is not in {}.  This should not 
happen'.format(
+                    self.name, self.jks_file
+                )
+            )
+
+        # If this certificate was signed by a CA, then also
+        # import the CA certificate into the keystore.
+        if self.ca:
+            command = [
+                keytool,
+                '-importcert',
+                '-noprompt',
+                "-alias",     self.ca.ca_cert.name,
+                '-file', self.ca.ca_cert.crt_file,
+                '-storepass', self.password,
+                '-keystore', self.jks_file
+            ]
+            self.log.info('Importing {} cert into Java 
keystore'.format(self.ca))
+            if not run_command(command):
+                raise RuntimeError(
+                    'Import of {} cert into Java Keystore 
failed'.format(self.ca), self
+                )
+            # Verify that the ca_cert is in the Java Keystore.
+            if not is_in_keystore(self.ca.ca_cert.name, self.jks_file, 
self.password):
+                raise RuntimeError(
+                    'Import of {} certificate into Java Keystore succeeded, 
but a key for '
+                    '{} is not in {}. This should not happen'.format(
+                        self.ca, self.ca.cert.name, self.jks_file
+                    )
+                )
+
+        return True
+
+    def __repr__(self):
+        if self.ca:
+            return '{}(name={}, keytype={}, ca={})'.format(
+                self.__class__.__name__, self.name,
+                self.key.__class__.__name__, self.ca.name
+            )
+        else:
+            return '{}(name={}, keytype={})'.format(
+                self.__class__.__name__, self.name,
+                self.key.__class__.__name__
+            )
+
+
+    def status_string(self):
+        file_statuses = []
+        for p in [self.key_file, self.crt_file, self.p12_file, self.jks_file]:
+            if os.path.exists(p):
+                mtime = datetime.fromtimestamp(os.path.getmtime(p)).isoformat()
+                file_statuses += ['\t{}: PRESENT (mtime: {})'.format(p, mtime)]
+
+            else:
+                file_statuses += ['\t{}: ABSENT'.format(p)]
+
+        return '{}:\n{}'.format(self, '\n'.join(file_statuses))
diff --git a/certpy/config.py b/certpy/config.py
new file mode 100644
index 0000000..d09e3bf
--- /dev/null
+++ b/certpy/config.py
@@ -0,0 +1,221 @@
+# -*- coding: utf-8 -*-
+
+import os
+import logging
+import importlib
+from yamlreader import yaml_load
+
+#  TODO make new classes loadable from plugins
+from .certificate import *
+from .key import *
+from .ca import *
+
+__all__ = ('load_manifests', 'instantiate_manifest') # TODO: validate
+
+# TODO: use relative paths in config???
+
+default_config = {
+    'authorities': {},
+    'certs': {}
+}
+
+log = logging.getLogger('config')
+
+def load_manifests(path, glob='*.certs.yaml'):
+    """
+    Given a directory, this will load all files matching config_file_glob
+    as YAML and then recursively merge them into a single config hash
+    using the yamlreader library.
+
+    :param glob default: '*.certs.yaml'
+    :return config dict
+    """
+    path = os.path.abspath(path)
+
+    # If this is a single file, then no need to use the glob.
+    if os.path.isfile(path):
+        log.info('Loading certificate and authority manifest from 
{}'.format(path))
+        return yaml_load(path, default_config)
+    # Else it is a directory, probably containing multiple manifest files.
+    # Load any file that matches path glob.
+    else:
+        path_glob = os.path.join(path, glob)
+        log.info('Loading all certificate and authority manifests in 
{}'.format(path_glob))
+        return yaml_load(path_glob, default_config)
+
+
+    # TODO: validate loaded manifest config against jsonschema
+
+
+def instantiate_manifest(manifest):
+    """
+    Given a certificate maniest dict object (usually loaded from a yaml file), 
containing
+    definitions of certificates and CAs to manage, instantiate them into 
objects.
+
+    :param manifest manifest of all authority and certificate configs.  CAs, 
Certificate, and Key
+                    classes will all be instantiated based on this 
configuration and returned.
+                    This can also be a string path to manifests that will be 
loaded
+                    using load_manifests.
+
+    :return dict    of the form { 'authorities': {...}, 'certificates': {...} 
}, where each
+                    declared CA and Certificate will be keyed by name in the 
appropriate dict.
+    """
+
+    if isinstance(manifest, str):
+        manifest = load_manifests(manifest)
+
+    authorities = instantiate_authorities(manifest['authorities'])
+    certificates = instantiate_certs(manifest['certs'], authorities)
+
+    return {
+        'authorities': authorities,
+        'certificates': certificates
+    }
+
+
+
+def instantiate_cert(cert_config, authorities={}):
+    """
+    Given a cert config manifest, instantiate the Certificate class and return 
it.
+    If this cert has a CA configured, that CA should already be instantiated in
+    the authorities hash, keyed by the CA named in the cert_config.
+
+    :param certs_manifest   cert manifest config keyed by name to instantiate.
+                            Make sure that cert_config['name'] is set.
+    :param authorities      dict of CA instances keyed by ca name.
+
+
+    :return Certificate
+    """
+    # If we have a special config for this key (that is not the default RSAKey)
+    # that Certificate will generate if not given a key, then we need to
+    # instantiate a new key now.
+    if 'key' in cert_config:
+        key_config = cert_config['key']
+        key_config.setdefault('name', cert_config['name'])
+        key_config.setdefault('path', cert_config['path'])
+        key_config.setdefault('password', cert_config['password'])
+
+        cert_config['key'] = instantiate(key_config['type'], **key_config)
+
+    if 'ca' in cert_config:
+        ca_name = cert_config['ca']
+
+        if ca_name not in authorities:
+            raise RuntimeError(
+                '{name} cert\'s CA is set to {ca}, but authority with '
+                'name {ca} is not declared in authorities.'.format(
+                    name=name, ca=ca_name
+                ))
+
+        cert_config['ca'] = authorities[ca_name]
+
+    return Certificate(**cert_config)
+
+def instantiate_certs(certs_manifest, authorities):
+    """
+    This will return a dict of Certficate instances keyed by name
+    representing all of the certs declared in certs_manifest.
+    If a cert declares a ca, make sure that a CA instance exists
+    in the authorities dict, keyed by the same name as the ca
+    that the cert config specifies.
+
+    :param certs_manifest   dict of cert manifest config keyed by name to 
instantiate.
+    :param authorities      dict of CA instances keyed by ca name.
+
+    :return dict            dict of Certificate instances keyed by name.
+    """
+    certs = {}
+    for name, cert_config in certs_manifest.items():
+        # Set cert_config['name'] to this manifest cert key name.
+        cert_config['name'] = name
+
+        cert = instantiate_cert(cert_config, authorities)
+        certs[name] = cert
+    return certs
+
+
+#  TODO change param to authority_config
+def instantiate_authorities(authorities_manifest):
+    """
+    Given a manifest of authority config, this will instantiate each as a CA 
instance.
+
+    :param authorities_manifest dict of CA config keyed by CA name.
+    :return dict                dict of CA instances keyed by name.
+    """
+    authorities = {}
+    # TODO: s/ca_config/authority_config?
+    for name, ca_config in authorities_manifest.items():
+        ca_config['name'] = name
+
+        print(ca_config)
+        # Instantiate the CA cert for this CA IF it has one
+        if 'cert' in ca_config:
+            ca_cert_config = ca_config['cert']
+            ca_cert_config['name'] = name
+            ca_cert = instantiate_cert(ca_cert_config)
+            ca_config['cert'] = ca_cert
+
+        ca = instantiate(ca_config['type'], **ca_config)
+        authorities[name] = ca
+
+    return authorities
+
+
+def validate(config):
+    pass
+    # TODO: json schema? Maybe!
+
+
+
+def get_class(module_class_name):
+    """
+    Returns module_class_name as a class.  This is useful for instantiating
+    a class by its dotted string name.
+
+    :param module_class_name Fully qualified name, e.g. 'my.module.ClassName'. 
 This must
+                             either be in globals(), or be importable via 
importlib.import_module
+
+    :return Class
+    """
+    if module_class_name in globals():
+        return globals()[module_class_name]
+    elif module_class_name in locals():
+        return locals()[module_class_name]
+    elif '.' in module_class_name:
+        module_name, class_name = module_class_name.rsplit('.', 1)
+        module = importlib.import_module(module_name)
+        return getattr(module, class_name)
+    else:
+        raise RuntimeError(
+            'Cannot dynamically import {}, '
+            'it is not in globals() or importable'.format(module_class_name)
+        )
+
+
+def instantiate(class_name, **kwargs):
+    """
+    Given a fully qualified module and class name, this returns a new
+    instance of the class with kwargs passed to the constructor.
+
+    Make sure that the fully qualified class name that you pass is in
+    PYTHONPATH.  Example:
+
+        $ tree /path/to/certpy_ext
+         /path/to/certpy_ext/
+        └── ext
+            └── key.py
+
+        $ head -n 2 /path/to/certpy_ext/ext/key.py
+        from certpy import Key
+        class DSAKey(Key):
+
+        $ export PYTHONPATH=/path/to/certpy_ext
+
+        ...
+        instantiate('ext.key.DSAKey')
+
+    :param class_name fully qualified class name
+    :return instance of class
+    """
+    return get_class(class_name)(**kwargs)
diff --git a/certpy/key.py b/certpy/key.py
new file mode 100644
index 0000000..37e3a10
--- /dev/null
+++ b/certpy/key.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+
+"""
+Classes for OpenSSL private key generation.  Extend the base Key class if you 
need
+to generate a key of a type not yet supported here.
+"""
+
+import os.path
+
+from .util import mkdirs, run_command, openssl, get_class_logger
+
+__all__ = ('Key', 'RSAKey', 'ECKey')
+
+
+class Key(object):
+    """
+    Base class for Key objects.  This just sets up common instance variables
+    and includes some convenience methods.
+    """
+    def __init__(self, name, path, password=None, **kwargs):
+        """
+        :param name
+        :param path path in which to look for and generate files
+        :param password key password
+        """
+
+        self.name = name
+        self.path = os.path.abspath(path)
+        self.password = password
+        # Private Key file in .pem format
+        self.key_file = os.path.join(self.path, '{}.key'.format(self.name))
+
+        self.log = get_class_logger(self)
+
+    def exists(self):
+        """
+        Checks that this Key exists at the expected key file path.
+        """
+        return os.path.exists(self.key_file)
+
+    def check_force_generate(self, force):
+        """
+        DRY method for checking if generate should be allowed even if the key 
file already exists.
+        :param force If True, this returns True
+        :return True or False if generation should be allowed
+        """
+        if self.exists() and not force:
+            self.log.warn(
+                '{} already exists, skipping key 
generation...'.format(self.key_file)
+            )
+            return False
+        else:
+            return True
+
+    def generate(self, force=False):
+        """
+        Implement this method to support generation of your Key subclass.
+        """
+        raise NotImplementedError(
+            'Cannot generate Key of unknown algorithm type.  Use a subclass.', 
self
+        )
+
+    def __repr__(self):
+        return '{}({})'.format(self.__class__.__name__, self.name)
+
+
+
+class RSAKey(Key):
+
+    def __init__(self, name, path, password=None, key_size=2048, **kwargs):
+        """
+        Helps with generation of RSA key files.
+        :param name
+        :param path path in which to look for and generate files
+        :param password key password
+        :param key_size RSA key size
+        """
+        self.key_size = key_size
+        super().__init__(name, path, password)
+
+    def generate(self, force=False):
+        """
+        Generates the key file.
+        :param force if True, the key will be re-generated even if the key 
file exists.
+        """
+        if not self.check_force_generate(force):
+            return False
+
+        mkdirs(self.path)
+
+        command = [openssl, 'genrsa', '-out', self.key_file]
+        if self.password:
+            command += ['-passout', 'pass:{}'.format(self.password)]
+        command += [str(self.key_size)]
+
+        self.log.info('Generating RSA key')
+        if not run_command(command):
+            raise RuntimeError('RSA key generation failed')
+
+        if not self.exists():
+            raise RuntimeError(
+                'Key generation succeeded but key file does not exist. '
+                'This should not happen', self
+            )
+
+
+class ECKey(Key):
+    """
+    Helps with generation of Eliptic Curve key files.
+    :param name
+    :param path path in which to look for and generate files
+    :param password key password
+    :param asn1_oid
+    """
+    def __init__(self, name, path, password=None, asn1_oid='prime256v1', 
**kwargs):
+        self.asn1_oid = asn1_oid
+        super().__init__(name, path, password)
+
+    def generate(self, force=False):
+        """
+        Generates the key file.
+        :param force if True, the key will be re-generated even if the key 
file exists.
+        """
+        if not self.check_force_generate(force):
+            return False
+
+        mkdirs(self.path)
+
+        command = [openssl, 'ecparam', '-genkey', '-name', self.asn1_oid, 
'-out', self.key_file]
+
+        self.log.info('Generating EC key')
+        # Generate the keyfile with no password
+        if not run_command(command):
+            raise RuntimeError('EC key generation failed', self)
+
+        # Now encrypt the key with a password, overwriting the original
+        # passwordless key.
+        if self.password:
+            command = [
+                openssl, 'ec',
+                '-in', self.key_file,
+                '-out', self.key_file,
+                '-des3', '-passout', 'pass:{}'.format(self.password)
+            ]
+            self.log.info('Encrypting key with password')
+
+            if not run_command(command):
+                raise RuntimeError('EC key file password encryption failed')
+
+        if not self.exists():
+            raise RuntimeError(
+                'Key generation succeeded but key file does not exist. '
+                'This should not happen', self
+            )
diff --git a/certpy/main.py b/certpy/main.py
new file mode 100755
index 0000000..46e1106
--- /dev/null
+++ b/certpy/main.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+Reads in Certificate and CA manifest configuration and manages
+OpenSSL keys, certificates, and authorities in various formats and stores.
+
+Usage: certpy [options] <manifest_path>
+
+    <manifest_path> is the path to the certificate and authority manifest 
config file(s).
+                    If this is a directory, then all files that match 
--manifest-glob
+                    (default '*.certs.yaml') will be loaded as manifests.
+
+Options:
+    -h --help                   Show this help message and exit.
+    -d --working-dir            cd to this directory before generating 
anything.
+                                This allows relative file paths in the 
manifest to
+                                be generated in a different location than the 
current cwd.
+    -G --generate-certs         Generate all certificate (excluding CA certs).
+    -A --generate-authorities   Generate all CA certficiate files.
+    -F --force                  If given a generate option without --force, 
any existing files will not
+                                be overwritten.  If want to overwrite files, 
provide --force.
+    -v --verbose                Turn on verbose debug logging.
+"""
+
+from docopt import docopt
+
+from pprint import pprint # TODO: remove
+
+from certpy import instantiate_manifest, setup_logging
+
+
+import logging
+
+log = logging.getLogger('certpy')
+
+
+def authorities_status_string(authorities):
+    s = '--- Authorities ---\n'
+    for name, ca in authorities.items():
+        s += '{}({}):\n'.format(ca.__class__.__name__, ca.name)
+        s += '{}\n'.format(ca.ca_cert.status_string())
+    return s
+
+
+def certificates_status_string(certificates):
+    s = '--- Certificates ---\n'
+    for name, cert in certificates.items():
+        s += '{}\n'.format(cert.status_string())
+    return s
+
+
+def generate_authorities(authorities, force=False):
+    # generate all authorities
+    for name, ca in authorities.items():
+        if hasattr(ca, 'generate'):
+            try:
+                ca.generate(force=force)
+            except NotImplementedError as e:
+                logging.warn('{} does not support generation, 
skipping.'.format(ca))
+
+def generate_certificates(certificates, force=False):
+    for name, cert in certificates.items():
+        cert.generate(force=force)
+
+def main():
+    # parse arguments with docopt
+    args = docopt(__doc__)
+
+    setup_logging()
+
+    loaded_manifest = instantiate_manifest(args['<manifest_path>'])
+    authorities = loaded_manifest['authorities']
+    certificates = loaded_manifest['certificates']
+
+
+    # TODO: implement --working-dir
+
+    if args['--generate-authorities']:
+        log.info('Generating all authorities declared in {} with 
force={}'.format(
+            args['<manifest_path>'], args['--force'])
+        )
+        generate_authorities(authorities, force=args['--force'])
+
+    print('\n' + authorities_status_string(authorities))
+
+    if args['--generate-certs']:
+        log.info('Generating all certificates declared in {} with 
force={}'.format(
+            args['<manifest_path>'], args['--force'])
+        )
+        generate_certificates(certificates, force=args['--force'])
+
+    print(certificates_status_string(certificates))
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file
diff --git a/certpy/puppet_sign_cert.rb b/certpy/puppet_sign_cert.rb
new file mode 100755
index 0000000..b52236f
--- /dev/null
+++ b/certpy/puppet_sign_cert.rb
@@ -0,0 +1,168 @@
+#!/usr/bin/env ruby
+# Copyright (c) 2016 Giuseppe Lavagetto, Wikimedia Foundation
+# Loosely based on 
https://github.com/ripienaar/mcollective-choria/blob/master/lib/mcollective/util/choria.rb
+
+# TODO THIS HAS NOT BEEN TESTED!
+
+require 'json'
+require 'logger'
+require 'net/http'
+require 'openssl'
+require 'optparse'
+require 'yaml'
+
+require 'puppet'
+require 'puppet/ssl/certificate_authority'
+require 'puppet/util/command_line'
+
+OpenSSL::PKey::EC.send(:alias_method, :private?, :private_key?)
+
+class PuppetCertifcateSignError < StandardError
+end
+
+args = {
+  configfile: nil,
+  cert_dir: '/var/lib/puppet/ssl/certs',
+  key_dir: '/var/lib/puppet/ssl/private_keys',
+  organization: 'Wikimedia Foundation, Inc.',
+  country: 'US',
+  state: 'California',
+  locality: 'San Francisco',
+  puppetca: 'puppet',
+  altnames: [],
+  asn1_oid: 'prime256v1'
+}
+
+
+Log = Logger.new(STDOUT)
+
+Log.level = Logger::INFO
+Log.formatter = proc do |severity, datetime, _, msg|
+  date_format = datetime.strftime("%Y-%m-%d %H:%M:%S")
+  format("%s %-5s (puppet-sign-cert): %s\n", date_format, severity, msg)
+end
+
+
+
+module Puppet
+  module SSL
+    # Extend the signing checks
+    module CertificateAuthorityExtensions
+      def check_internal_signing_policies(hostname, csr, _allow_dns_alt_names)
+        super(hostname, csr, true)
+      rescue Puppet::SSL::CertificateAuthority::CertificateSigningError => e
+        if e.message.start_with?("CSR '#{csr.name}' subjectAltName contains a 
wildcard")
+          true
+        else
+          raise
+        end
+      end
+    end
+    # Extend the base class
+    class CertificateAuthority
+      prepend Puppet::SSL::CertificateAuthorityExtensions
+    end
+  end
+end
+
+
+
+
+def read_csr_file(csr_path)
+  OpenSSL::X509::Request.new(File.read(csr_path))
+end
+
+def common_name_from_csr(csr)
+  # csr.subject.to_a returns something like:
+  # [["C", "US", 19], ["ST", "CA", 12], ["O", "WMF", 12], ["CN", "testrsa1", 
12]]
+  # We want the entry with "CN":
+  csr.subject.to_a.select{|name, _, _| name == 'CN' }.first[1]
+end
+
+def send_csr(csr, hostname: 'puppet', port: 8140)
+  common_name = common_name_from_csr(csr)
+
+  https = Net::HTTP.new(hostname, port)
+  https.use_ssl = true
+  # TODO: fix this
+  https.verify_mode = OpenSSL::SSL::VERIFY_NONE
+
+
+  req = Net::HTTP::Put.new("/production/certificate_request/#{common_name}",
+                           'Content-Type' => 'text/plain')
+  req.body = csr.to_s
+
+  Log.info "Submitting CSR for #{common_name} to #{hostname}:#{port}"
+  resp, _ = https.request(req)
+  fail(PuppetCertifcateSignError,
+       format('CSR for %s to %s:%s failed with code %s: %s',
+              common_name, hostname, port, resp.code, resp.body)
+      ) unless resp.code == '200'
+  Log.info "CSR succeeded"
+end
+
+
+def cert_exists(csr)
+  #TODO 
https://docs.puppet.com/puppet/3.8/http_api/http_certificate_status.html
+  # GET /:environment/certificate_status/:certname but gives forbidden???
+  false
+end
+
+def sign_csr(csr)
+  common_name = common_name_from_csr(csr)
+  Log.info "Now signing the certificate for #{common_name}"
+  # Now sign the cert using puppet's own commandline interpreter
+  Puppet::Util::CommandLine.new('cert', ['sign', common_name]).execute
+end
+
+
+def send_and_sign_csr(csr_path, hostname: 'puppet', port: 8140)
+  csr = read_csr_file(csr_path)
+
+  # TODO:
+  # if cert_exists
+
+
+  send_csr(csr, hostname: hostname, port: port)
+  sign_csr(csr)
+end
+
+
+
+
+# Ruby main :)
+if __FILE__ == $0;
+
+  args = {
+    ca_hostname: 'puppet',
+    ca_port: 8140,
+  }
+
+  OptionParser.new do |opts|
+    opts.banner = "Usage: puppet-sign-cert [-H <puppet-ca-hostname>] [-P 
<puppet-ca-port>]  <csr-path>"
+
+    opts.on('-H', '--ca-hostname HOSTNAME', 'Puppet CA hostname') do |hostname|
+      args[:ca_hostname] = hostname
+    end
+
+    opts.on('-P', '--ca-port', 'Puppet CA port') do |port|
+      args[:ca_port] = port
+    end
+
+    opts.on('-d', '--debug', 'Show debug information') do
+      Log.level = Logger::DEBUG
+    end
+  end.parse!
+
+  csr_path = ARGV.shift || ''
+
+  fail(PuppetCertifcateSignError, 'You must provide a path to a .csr file in 
.pem format') unless csr_path != ''
+
+  begin
+    send_and_sign_csr(csr_path, hostname: args[:ca_hostname], port: 
args[:ca_port])
+  rescue PuppetCertifcateSignError => e
+    Log.error "#{e.message}"
+    exit 1
+  end
+
+end
diff --git a/certpy/util.py b/certpy/util.py
new file mode 100644
index 0000000..bcf0c21
--- /dev/null
+++ b/certpy/util.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import logging
+import os
+import os.path
+import subprocess
+import tempfile
+import yaml    # PyYAML (python-yaml)
+
+from .ca import *
+
+openssl = os.getenv('OPENSSL_BIN', 'openssl')
+keytool = os.getenv('KEYTOOL_BIN', 'keytool')
+
+__all__ = (
+    'setup_logging', 'get_class_logger', 'run_command', 'mkdirs', 
'is_in_keystore'
+)
+
+
+def setup_logging(level=None):
+    """
+    Conigures basic logging defaults.
+    If level is not given, but the environment variable LOG_LEVEL
+    is set, it will be used as the level.  Otherwise INFO is the default level.
+
+    :param level
+    """
+    if not level:
+        level = getattr(
+            logging, os.environ.get('LOG_LEVEL', 'INFO')
+        )
+
+    logging.basicConfig(
+        level=level,
+        format='%(asctime)s %(levelname)-8s %(name)-22s %(message)s'
+    )
+
+
+def get_class_logger(obj):
+    """
+    Returns a new logging instance for an class object instance.
+    If the obj instance has a name attribute, it will be included
+    in the logger name.  This is useful for using %(name)s in
+    logging your formatter.
+
+    :param obj an instance of any class
+    """
+    class_name = obj.__class__.__name__
+    if hasattr(obj, 'name'):
+        logger_name = '{}({})'.format(class_name, obj.name)
+    else:
+        logger_name = class_name
+    return logging.getLogger(logger_name)
+
+
+def run_command(command, creates=None):
+    """
+    Executes a command in a subshell and logs the output.
+
+    :param command a list of command args to pass to subprocess.check_output
+    :return True if the command exited with 0, else False
+    """
+    logger = logging.getLogger('shell')
+
+    if isinstance(command, str):
+        command = command.split()
+    try:
+        logger.debug("Running command: " + " ".join(command))
+        output = subprocess.check_output(command, stderr=subprocess.STDOUT)
+        for ln in output.splitlines(): logger.debug(ln)
+
+        # Ensure that any files that this command should have created exist.
+        if creates:
+            if isinstance(creates, str):
+                creates = [creates]
+            for f in creates:
+                if not os.path.exists(f):
+                    logger.error(
+                        'command succeeded, but was expected to create file {} 
'
+                        'and it does not exist. command: {}'.format(f, ' 
'.join(command))
+                    )
+                    return False
+
+    except subprocess.CalledProcessError as e:
+        for ln in e.output.splitlines(): logging.error(ln)
+        logger.error("command returned status %d: %s", e.returncode, " 
".join(command))
+        return False
+
+
+    logger.debug("command succeeded: %s", " ".join(command))
+    return True
+
+
+def mkdirs(directory):
+    """
+    Equivalent to mkdir -p
+
+    :param directory path
+    """
+    if not os.path.exists(directory):
+        os.makedirs(directory)
+
+def is_in_keystore(alias, jks_file, password):
+    command = [
+        keytool,
+        '-list',
+        '-alias', alias,
+        '-storepass', password,
+        '-keystore', jks_file
+    ]
+    return run_command(command)
diff --git a/examples/example.certs.yaml b/examples/example.certs.yaml
new file mode 100644
index 0000000..4eee53a
--- /dev/null
+++ b/examples/example.certs.yaml
@@ -0,0 +1,35 @@
+authorities:
+  rootCa:
+    type: SelfSigningCA
+    cert:
+      path: certificates/rootCa
+      subject:
+        C: US
+        ST: CA
+      password: qwerty
+      key:
+        type: RSAKey
+        password: qwerty
+
+certs:
+  hostname1.example.org:
+    path: certificates/hostname1.example.org
+    ca: rootCa
+    subject:
+      C: US
+      ST: CA
+    dns_alt_names: [me.we.you, fine.com]
+    password: qwerty
+    key:
+      type: ECKey
+      password: qwerty
+
+  clientA:
+    path: certificates/clientA
+    ca: rootCa
+    subject:
+      C: US
+      ST: CA
+    password: qwerty
+    key:
+      type: ECKey
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..c13b703
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+from setuptools import setup, find_packages
+
+try:
+    long_description = open('README.md').read()
+except IOError:
+    long_description = ''
+
+setup(
+    name='certpy',
+    version='0.1.0',
+    description='OpenSSL Certificate Manager',
+    license='Apache',
+    author='Andrew Otto',
+    packages=find_packages(),
+    install_requires=[
+        'docopt',
+        'yamlreader',
+    ],
+    long_description=long_description,
+    entry_points={'console_scripts': ['certpy = certpy.main:main']},
+    classifiers=[
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 3.3',
+    ]
+)

-- 
To view, visit https://gerrit.wikimedia.org/r/359960
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ie1a251caa6eaafc3d53120eff85ddb5e7b77369c
Gerrit-PatchSet: 1
Gerrit-Project: operations/software/certpy
Gerrit-Branch: master
Gerrit-Owner: Ottomata <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to