Eevans has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/236389

Change subject: WIP: certificate/keystore generation script
......................................................................

WIP: certificate/keystore generation script

cassandra-ca-mgr accepts a yaml-formatted manifest file as its only argument
and generates a root CA and corresponding Java truststore, and an arbitrary
number of Java keystores, signed by the root CA.

The script is idempotent, so subsequent invocations with an identical
manifest should effect no change; Invocations with additional keystore entity
definitions will result only on the creation of the new keystores.

Try `pydoc ./cassandra-ca-mgr' to see an example of how to format a manifest.

Bug: T108953
Change-Id: I497ff7de13cb87e82cd7f6562f7abfdb7c97fcd2
---
A modules/cassandra/files/cassandra-ca-mgr
1 file changed, 364 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/operations/puppet 
refs/changes/89/236389/1

diff --git a/modules/cassandra/files/cassandra-ca-mgr 
b/modules/cassandra/files/cassandra-ca-mgr
new file mode 100755
index 0000000..189bda6
--- /dev/null
+++ b/modules/cassandra/files/cassandra-ca-mgr
@@ -0,0 +1,364 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Cassandra certificate management
+
+First, you need a manifest that specifies the Certificate Authority, and
+each of the keystores.  For example:
+
+    # The top-level working directory
+    base_directory: /path/to/base/directory
+    
+    # The Certificate Authority
+    authority:
+      key:
+        size: 2048
+      cert:
+        subject:
+          organization: WMF
+          country: US
+          unit: Services
+        valid: 365
+      password: qwerty
+    
+    # Java keystores
+    keystores:
+      - name: restbase1001-a
+        key:
+          size: 2048
+        cert:
+          subject:
+            organization: WMF
+            country: US
+            unit: Services
+          valid: 365
+        password: qwerty
+    
+      - name: restbase1001-b
+        key:
+          size: 2048
+        cert:
+          subject:
+            organization: WMF
+            country: US
+            unit: Services
+          valid: 365
+        password: qwerty
+    
+      - name: restbase1002-a
+        key:
+          size: 2048
+        cert:
+          subject:
+            organization: WMF
+            country: US
+            unit: Services
+          valid: 365
+        password: qwerty
+
+Next, run the script with the manifest as its only argument:
+
+    $ cassandra-ca manifest.yaml
+    $ tree /path/to/base/directory
+    /path/to/base/directory
+    ├── restbase1001-a
+    │   ├── restbase1001-a.crt
+    │   └── restbase1001-a.csr
+    │   └── restbase1001-a.kst
+    ├── restbase1001-b
+    │   ├── restbase1001-b.crt
+    │   └── restbase1001-b.csr
+    │   └── restbase1001-b.kst
+    ├── restbase1002-a
+    │   ├── restbase1002-a.crt
+    │   └── restbase1002-a.csr
+    │   └── restbase1002-a.kst
+    ├── rootCa.crt
+    ├── rootCa.key
+    ├── rootCa.srl
+    └── truststore
+    
+    3 directories, 13 files
+
+    
+"""
+
+import logging
+import os
+import os.path
+import subprocess
+import sys
+import yaml    # PyYAML (python-yaml)
+
+
+logging.basicConfig(level=logging.DEBUG)
+
+
+class Subject(object):
+    def __init__(self, common_name, **kwargs):
+        self.common_name = common_name
+        self.organization = kwargs.get("organization", "WMF")
+        self.country = kwargs.get("country", "US")
+        self.unit = kwargs.get("unit", "Services")
+
+    def __repr__(self):
+        return "%s(cn=%s, o=%s, c=%s, u=%s)" \
+            % (self.__class__.__name__, self.common_name, self.organization, 
self.country, self.unit)
+
+class KeytoolSubject(Subject):
+    def __str__(self):
+        return "cn=%s, ou=%s, o=%s, c=%s" % (self.common_name, self.unit, 
self.organization, self.country)
+
+class Keystore(object):
+    def __init__(self, path, authority, **kwargs):
+        name = kwargs.get("name")
+        password = kwargs.get("password")
+        
+        if name is None:
+            raise RuntimeError("corrupt keystore entry; missing keystore name")
+        if password is None:
+            raise RuntimeError("corrupt keystore entry; missing keystore 
password")
+
+        key = kwargs.get("key", dict(size=2048))
+        size = int(key.get("size", 2048))
+        cert = kwargs.get("cert", dict(valid=365))
+        
+        self.base = os.path.abspath(path)
+        self.name = name
+        self.authority = authority
+        self.filename = os.path.join(self.base, name, "%s.kst" % self.name)
+        self.csr = os.path.join(self.base, name, "%s.csr" % name)
+        self.crt = os.path.join(self.base, name, "%s.crt" % name)
+        self.password = password
+        self.size = size
+        self.subject = KeytoolSubject(self.name, **cert)
+        self.valid = int(cert.get("valid", 365))
+
+        mkdirs(os.path.join(self.base, name))
+
+    def generate(self):
+        if not os.path.exists(self.filename):
+            # Generate the node key
+           #
+           # It looks as though a key password is required (if you do not pass 
the
+           # argument, then keytool prompts for the password on STDIN).  
Cassandra
+           # it seems, depends upon the key and store passwords being 
identical, (and
+           # indeed, keytool itself will attempt to use the -storepass when 
-keypass
+           # is omitted).  So much WTF.
+            command = [
+                "keytool",
+                "-genkeypair",
+                "-dname",     str(self.subject),
+                "-keyalg",    "RSA",
+                "-alias",     self.name,
+                "-validity",  str(self.valid),
+                "-storepass", self.password,
+                "-keypass",   self.password,
+                "-keystore",  self.filename
+            ]
+            if not run_command(command):
+                raise RuntimeError("CA key generation failed")
+            
+           # Generate a certificate signing request.
+            command = [
+                "keytool",
+                "-certreq",
+                "-dname",     str(self.subject),
+                "-alias",     self.name,
+                "-file",      self.csr,
+                "-keypass",   self.password,
+                "-storepass", self.password,
+                "-keystore",  self.filename
+            ]
+            if not run_command(command):
+                raise RuntimeError("signing request generation failed")
+
+            # Sign (and verify).
+            command = [
+                "openssl",
+                "x509",
+                "-req",
+                "-CAcreateserial",
+                "-in",    self.csr,
+                "-CA",    self.authority.certificate.filename,
+                "-CAkey", self.authority.key.filename,
+                "-days",  str(self.valid),
+                "-out",   self.crt
+            ]
+            if not run_command(command):
+                raise RuntimeError("certificate signing failed")
+
+            command = [
+                "openssl",
+                "verify",
+                "-CAfile", self.authority.certificate.filename,
+                self.crt
+            ]
+            if not run_command(command):
+                raise RuntimeError("certificate verification failed")
+
+            # Before we can import the signed certificate, the signer must be 
trusted,
+           # either with a trust entry in this keystore, or with one in the 
system
+            # truststore, aka 'cacerts', (provided -trustcacerts is passed).
+            command = [
+                "keytool",
+                "-importcert",
+                "-noprompt",
+                "-file",      self.authority.certificate.filename,
+                "-storepass", self.password,
+                "-keystore",  self.filename
+            ]
+            if not run_command(command):
+                raise RuntimeError("import of CA cert failed")
+            
+           # Import the CA signed certificate.
+            command = [
+                "keytool",
+                "-importcert",
+                "-noprompt",
+                "-file",      self.crt,
+                "-alias",     self.name,
+                "-storepass", self.password,
+                "-keystore",  self.filename
+            ]
+            if not run_command(command):
+                raise RuntimeError("import of CA-signed cert failed")
+        else:
+            logging.warn("%s already exists, skipping keystore generation...", 
self.filename)
+    
+    def __repr__(self):
+        return "%s(name=%s, filename=%s, size=%s, subject=%s)" \
+            % (self.__class__.__name__, self.name, self.filename, self.size, 
self.subject)
+
+class OpensslSubject(Subject):
+    def __str__(self):
+        return "/CN=%s/OU=%s/O=%s/C=%s/" % (self.common_name, self.unit, 
self.organization, self.country)
+    
+class OpensslCertificate(object):
+    def __init__(self, name, path, key, password, **kwargs):
+        self.name = name
+        self.base = os.path.abspath(path)
+        self.filename = os.path.join(self.base, "%s.crt" % self.name)
+        self.truststore = os.path.join(self.base, "truststore")
+        self.key = key
+        self.password = password
+        self.subject = OpensslSubject(name, **(kwargs.get("subject")))
+        self.valid = int(kwargs.get("valid", 365))
+
+    def generate(self):
+        # Generate the CA certificate
+        if not os.path.exists(self.filename):
+            command = [
+                "openssl",
+                "req",
+                "-x509",
+                "-new",
+                "-nodes",
+                "-subj", str(self.subject),
+                "-days", str(self.valid),
+                "-key", self.key.filename,
+                "-out", self.filename
+            ]
+            if not run_command(command):
+                raise RuntimeError("CA certificate generation failed")
+        else:
+            logging.warn("%s already exists, skipping certificate 
generation...", self.filename)
+
+        # Import the CA certificate to a Java truststore
+        if not os.path.exists(self.truststore):
+            # FIXME: -storepass should use :file or :env specifier to avoid 
exposing password to process list
+            command = [
+                "keytool",
+                "-importcert",
+                "-v",
+                "-noprompt",
+                "-trustcacerts",
+                "-alias", "rootCa",
+                "-file", self.filename,
+                "-storepass", self.password,
+                "-keystore", self.truststore
+            ]
+            if not run_command(command):
+                raise RuntimeError("CA truststore generation failed")
+        else :
+            logging.warn("%s already exists, skipping truststore 
generation...", self.filename)
+
+    def __repr__(self):
+        return "%s(name=%s, filename=%s, subject=%s, valid=%d)" \
+            % (self.__class__.__name__, self.name, self.filename, 
self.subject, self.valid)
+
+class OpensslKey(object):
+    def __init__(self, name, path, **kwargs):
+        self.name = name
+        self.base = os.path.abspath(path)
+        self.filename = os.path.join(self.base, "%s.key" % self.name)
+        self.size = kwargs.get("size", 2048)
+
+        mkdirs(self.base)
+
+    def generate(self):
+        if not os.path.exists(self.filename):
+            if not run_command(["openssl", "genrsa", "-out", self.filename, 
str(self.size)]):
+                raise RuntimeError("CA key generation failed")
+        else:
+            logging.warn("%s already exists, skipping key generation...", 
self.filename)
+
+    def __repr__(self):
+        return "%s(name=%s, filename=%s, size=%s)" % (self.__class__.__name__, 
self.name, self.filename, self.size)
+
+class Authority(object):
+    def __init__(self, base_directory, **kwargs):
+        self.password = kwargs.get("password")
+        if self.password is None:
+            raise RuntimeError("authority is missing mandatory password entry")
+
+        self.base_directory = base_directory
+        self.key = OpensslKey("rootCa", self.base_directory, 
**(kwargs.get("key")))
+        self.certificate = OpensslCertificate("rootCa", self.base_directory, 
self.key, self.password, **(kwargs.get("cert")))
+
+    def generate(self):
+        self.key.generate()
+        self.certificate.generate()
+        
+    def __repr__(self):
+        return "%s(key=%s, certifcate=%s)" % (self.__class__.__name__, 
self.key, self.certificate)
+
+def read_manifest(manifest):
+    with open(manifest, 'r') as f:
+        return yaml.load(f.read())
+
+def run_command(command):
+    try:
+        output = subprocess.check_output(command, stderr=subprocess.STDOUT)
+        for ln in output.split("\n"): logging.debug(ln)
+        logging.debug("command succeeded: %s", " ".join(command))
+    except subprocess.CalledProcessError, e:
+        for ln in e.output.split("\n"): logging.error(ln)
+        logging.error("command returned status %d: %s", e.returncode, " 
".join(command))
+        return False
+    return True
+
+def mkdirs(directory):
+    if not os.path.exists(directory):
+        os.makedirs(directory)
+
+if __name__ == '__main__':    
+    import argparse
+
+    parser = argparse.ArgumentParser(description='Manage a certificate 
authority')
+    parser.add_argument("manifest", type=str,
+                        help="YAML specification of managed keys and 
certificates")
+    args = parser.parse_args()
+
+    manifest = read_manifest(args.manifest)
+
+    base_directory = manifest.get("base_directory", os.path.abspath(os.curdir))
+    authority = Authority(base_directory, **(manifest.get("authority")))
+
+    authority.generate()
+    
+    entities = manifest.get("keystores")
+
+    for entity in entities:
+        Keystore(base_directory, authority, **entity).generate()

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I497ff7de13cb87e82cd7f6562f7abfdb7c97fcd2
Gerrit-PatchSet: 1
Gerrit-Project: operations/puppet
Gerrit-Branch: production
Gerrit-Owner: Eevans <eev...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to