Filippo Giunchedi has submitted this change and it was merged.

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


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, 382 insertions(+), 0 deletions(-)

Approvals:
  Filippo Giunchedi: Verified; Looks good to me, approved



diff --git a/modules/cassandra/files/cassandra-ca-mgr 
b/modules/cassandra/files/cassandra-ca-mgr
new file mode 100755
index 0000000..b76f8fe
--- /dev/null
+++ b/modules/cassandra/files/cassandra-ca-mgr
@@ -0,0 +1,382 @@
+#!/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 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 os.path.exists(self.filename):
+            logging.warn("%s already exists, skipping keystore generation...", 
self.filename)
+            return
+
+        # 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")
+
+    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", dict())))
+        self.valid = int(kwargs.get("valid", 365))
+
+    def generate(self):
+        if os.path.exists(self.filename):
+            logging.warn("%s already exists, skipping certificate 
generation...", self.filename)
+            return
+
+        # Generate the CA certificate
+        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")
+
+        if os.path.exists(self.truststore):
+            logging.warn("%s already exists, skipping truststore 
generation...", self.filename)
+            return
+
+        # Import the CA certificate to a Java 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")
+
+    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 os.path.exists(self.filename):
+            logging.warn("%s already exists, skipping key generation...", 
self.filename)
+            return
+
+        if not run_command(["openssl", "genrsa", "-out", self.filename, 
str(self.size)]):
+            raise RuntimeError("CA key generation failed")
+
+    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", dict())))
+        self.certificate = OpensslCertificate("rootCa", self.base_directory, 
self.key, self.password, **(kwargs.get("cert", dict())))
+
+    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.splitlines(): logging.debug(ln)
+        logging.debug("command succeeded: %s", " ".join(command))
+    except subprocess.CalledProcessError, e:
+        for ln in e.output.splitlines(): 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")
+    parser.add_argument("--base_directory", type=str, default=None,
+                        help="Override base_directory from manifest")
+    args = parser.parse_args()
+
+    manifest = read_manifest(args.manifest)
+
+    base_directory = args.base_directory or manifest.get("base_directory")
+    if base_directory is None:
+        parser.error("base_directory not specified")
+
+    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: merged
Gerrit-Change-Id: I497ff7de13cb87e82cd7f6562f7abfdb7c97fcd2
Gerrit-PatchSet: 8
Gerrit-Project: operations/puppet
Gerrit-Branch: production
Gerrit-Owner: Eevans <eev...@wikimedia.org>
Gerrit-Reviewer: Eevans <eev...@wikimedia.org>
Gerrit-Reviewer: Filippo Giunchedi <fgiunch...@wikimedia.org>
Gerrit-Reviewer: GWicke <gwi...@wikimedia.org>
Gerrit-Reviewer: Mobrovac <mobro...@wikimedia.org>
Gerrit-Reviewer: Muehlenhoff <mmuhlenh...@wikimedia.org>
Gerrit-Reviewer: jenkins-bot <>

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

Reply via email to