diff --git a/README.md b/README.md index c3b627a..9b1393b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,17 @@ You'll notice there is a csrgen and csrgen35. This corresponds to their respecti - csrgen uses Python 2.7 - csrgen34 uses Python 3.5 +## Installation / Dependencies +The following modules are required: +- OpenSSL (pyopenssl) +- Argparse (argparse) +- YAML (pyyaml) + +I've included a setup.py that will install these dependencies if you run: +``` +python setup.py install` +``` + ## Usage csrgen [fqdn] diff --git a/csr_tools/csrgen.py b/csr_tools/csrgen.py new file mode 100755 index 0000000..9a1cc49 --- /dev/null +++ b/csr_tools/csrgen.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# +# Generate a key, self-signed certificate, and certificate request. +# Usage: csrgen -n +# +# When more than one hostname is provided, a SAN (Subject Alternate Name) +# certificate and request are generated. This can be acheived by adding -s. +# Usage: csrgen -n -s +# +# If you want to generate multiple CSRs, you can use the -f command to +# feed in a .yaml file via the CLI. See the example sample.yaml in this +# repository for examples. +# +# Author: Courtney Cotton 06-25-2014, Updated 8-9-2017 + +# Libraries/Modules +from OpenSSL import crypto, SSL +import argparse +import yaml + + +# Generate Certificate Signing Request (CSR) +def generateCSR(nodename, C, ST, L, O, OU, sans = []): + # These variables will be used to create the host.csr and host.key files. + csrfile = nodename + '.csr' + keyfile = nodename + '.key' + # OpenSSL Key Type Variable, passed in later. + TYPE_RSA = crypto.TYPE_RSA + + # Appends SAN to have 'DNS:' + ss = [] + for i in sans: + ss.append("DNS: %s" % i) + ss = ", ".join(ss) + + req = crypto.X509Req() + req.get_subject().CN = nodename + req.get_subject().countryName = C + req.get_subject().stateOrProvinceName = ST + req.get_subject().localityName = L + req.get_subject().organizationName = O + req.get_subject().organizationalUnitName = OU + # Add in extensions + base_constraints = ([ + crypto.X509Extension("keyUsage", False, "Digital Signature, Non Repudiation, Key Encipherment"), + crypto.X509Extension("basicConstraints", False, "CA:FALSE"), + ]) + x509_extensions = base_constraints + # If there are SAN entries, append the base_constraints to include them. + if ss: + san_constraint = crypto.X509Extension("subjectAltName", False, ss) + x509_extensions.append(san_constraint) + req.add_extensions(x509_extensions) + # Utilizes generateKey function to kick off key generation. + key = generateKey(TYPE_RSA, 2048) + req.set_pubkey(key) + req.sign(key, "sha256") + + generateFiles(csrfile, req) + generateFiles(keyfile, key) + + return req + +def getCSRSubjects(): + while True: + C = raw_input("Enter your Country Name (2 letter code) [US]: ") + if len(C) != 2: + print "You must enter two letters. You entered %r" % (C) + continue + ST = raw_input("Enter your State or Province []:California: ") + if len(ST) == 0: + print "Please enter your State or Province." + continue + L = raw_input("Enter your (Locality Name (eg, city) []:San Francisco: ") + if len(L) == 0: + print "Please enter your City." + continue + O = raw_input("Enter your Organization Name (eg, company) []:FTW Enterprise: ") + if len(L) == 0: + print "Please enter your Organization Name." + continue + OU = raw_input("Enter your Organizational Unit (eg, section) []:IT: ") + if len(OU) == 0: + print "Please enter your OU." + continue + break + return C, ST, L, O, OU + + # Allows you to permanently set values required for CSR + # To use, comment raw_input and uncomment this section. + # C = 'US' + # ST = 'New York' + # L = 'Location' + # O = 'Organization' + # OU = 'Organizational Unit' + +# Reading in from the FILE +def generateFromFile(config_file, C, ST, L, O, OU): + print "Reading file: %s" % config_file + parseYAML(config_file, C, ST, L, O, OU) + +# Parse the contents of the YAML file and then +# generate a CSR for each of them. +def parseYAML(config_file, C, ST, L, O, OU): + with open(config_file, 'r') as stream: + cfg = yaml.load(stream) + for k,v in cfg.items(): + hostname = cfg[k]['hostname'] + if cfg[k]['sans']: + sans = cfg[k]['sans'] + else: + sans = '' + print "host: %s, sans: %s" % (hostname, sans) + generateCSR(hostname, C, ST, L, O, OU, sans) + exit() + +# Generate Private Key +def generateKey(type, bits): + key = crypto.PKey() + key.generate_key(type, bits) + return key + +# Generate .csr/key files. +def generateFiles(mkFile, request): + if ".csr" in mkFile: + f = open(mkFile, "w") + f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, request)) + f.close() + print crypto.dump_certificate_request(crypto.FILETYPE_PEM, request) + elif ".key" in mkFile: + f = open(mkFile, "w") + f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, request)) + f.close() + else: + print "Failed to create CSR/Key files" + exit() + + +# Run Portion +# This section will parse the flags available via command line. +parser = argparse.ArgumentParser() +parser.add_argument("-n", "--name", help="Provide the FQDN", action="store", default="") +parser.add_argument("-f", "--file", help="Configuration file", action="store", default="") +parser.add_argument("-s", "--san", help="SANS", action="store", nargs='*', default="") +args = parser.parse_args() + +# Run the primary function. +# Checks to see if the -f was given. If it wasn't, skip directly +# to the generateCSR, otherwise it'll need to parse the YAML file +# first via the functio parseYAML called via generateFromFile. +C, ST, L, O, OU = getCSRSubjects() + +if args.file: + generateFromFile(args.file, C, ST, L, O, OU) +else: + # TODO: If name is not given (minimum required), throw alert and exit. + generateCSR(args.name, C, ST, L, O, OU, args.san) diff --git a/csrgen35.py b/csr_tools/csrgen35.py similarity index 91% rename from csrgen35.py rename to csr_tools/csrgen35.py index f2b15f5..2df48ae 100755 --- a/csrgen35.py +++ b/csr_tools/csrgen35.py @@ -15,6 +15,7 @@ # Libraries/Modules from OpenSSL import crypto, SSL import argparse +import yaml # Generate Certificate Signing Request (CSR) @@ -122,12 +123,20 @@ def generateFiles(mkFile, request): # Run Portion - +# This section will parse the flags available via command line. parser = argparse.ArgumentParser() parser.add_argument("name", help="Provide the FQDN", action="store") +parser.add_argument("-f", "--file", help="Configuration file", action="store", nargs='*', default="") parser.add_argument("-s", "--san", help="SANS", action="store", nargs='*', default="") args = parser.parse_args() +# Variables from CLI Parser (Argparse) hostname = args.name sans = args.san -generateCSR(hostname, sans) +config_file = args.file + +# Run the primary function(s) based on input. +if config_file is None: + generateFromFile(config_file) +else: + generateCSR(hostname,sans) diff --git a/csrgen.py b/csrgen.py deleted file mode 100755 index da069d7..0000000 --- a/csrgen.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python -# -# Generate a key, self-signed certificate, and certificate request. -# Usage: csrgen -# -# When more than one hostname is provided, a SAN (Subject Alternate Name) -# certificate and request are generated. This can be acheived by adding -s. -# Usage: csrgen -s -# -# Author: Courtney Cotton 06-25-2014 - -# Libraries/Modules - -import argparse -from OpenSSL import crypto - - -# Generate Certificate Signing Request (CSR) -def generateCSR(nodename, sans = []): - - while True: - C = raw_input("Enter your Country Name (2 letter code) [US]: ") - if len(C) != 2: - print "You must enter two letters. You entered %r" % (C) - continue - ST = raw_input("Enter your State or Province []:California: ") - if len(ST) == 0: - print "Please enter your State or Province." - continue - L = raw_input("Enter your (Locality Name (eg, city) []:San Francisco: ") - if len(L) == 0: - print "Please enter your City." - continue - O = raw_input("Enter your Organization Name (eg, company) []:FTW Enterprise: ") - if len(L) == 0: - print "Please enter your Organization Name." - continue - OU = raw_input("Enter your Organizational Unit (eg, section) []:IT: ") - if len(OU) == 0: - print "Please enter your OU." - continue - - # Allows you to permanently set values required for CSR - # To use, comment raw_input and uncomment this section. - # C = 'US' - # ST = 'New York' - # L = 'Location' - # O = 'Organization' - # OU = 'Organizational Unit' - - csrfile = 'host.csr' - keyfile = 'host.key' - TYPE_RSA = crypto.TYPE_RSA - # Appends SAN to have 'DNS:' - ss = [] - for i in sans: - ss.append("DNS: %s" % i) - ss = ", ".join(ss) - - req = crypto.X509Req() - req.get_subject().CN = nodename - req.get_subject().countryName = C - req.get_subject().stateOrProvinceName = ST - req.get_subject().localityName = L - req.get_subject().organizationName = O - req.get_subject().organizationalUnitName = OU - # Add in extensions - base_constraints = ([ - crypto.X509Extension("keyUsage", False, "Digital Signature, Non Repudiation, Key Encipherment"), - crypto.X509Extension("basicConstraints", False, "CA:FALSE"), - ]) - x509_extensions = base_constraints - # If there are SAN entries, append the base_constraints to include them. - if ss: - san_constraint = crypto.X509Extension("subjectAltName", False, ss) - x509_extensions.append(san_constraint) - req.add_extensions(x509_extensions) - # Utilizes generateKey function to kick off key generation. - key = generateKey(TYPE_RSA, 2048) - req.set_pubkey(key) - - #update sha? - #req.sign(key, "sha1") - req.sign(key, "sha256") - - generateFiles(csrfile, req) - generateFiles(keyfile, key) - - return req - -# Generate Private Key -def generateKey(type, bits): - - key = crypto.PKey() - key.generate_key(type, bits) - return key - -# Generate .csr/key files. -def generateFiles(mkFile, request): - - if mkFile == 'host.csr': - f = open(mkFile, "w") - f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, request)) - f.close() - print crypto.dump_certificate_request(crypto.FILETYPE_PEM, request) - elif mkFile == 'host.key': - f = open(mkFile, "w") - f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, request)) - f.close() - else: - print "Failed." - exit() - - -# Run Portion -parser = argparse.ArgumentParser() -parser.add_argument("name", help="Provide the FQDN", action="store") -parser.add_argument("-s", "--san", help="SANS", action="store", nargs='*', default="") -args = parser.parse_args() - -hostname = args.name -sans = args.san - -generateCSR(hostname, sans) diff --git a/sample.yaml b/sample.yaml new file mode 100644 index 0000000..694573d --- /dev/null +++ b/sample.yaml @@ -0,0 +1,15 @@ +--- + host1: + hostname: 'cookies.com' + sans: + - 'sugar.cookies.com' + - 'pb.cookies.com' + host2: + hostname: 'muffins.com' + sans: + - 'bran.muffins.com' + - 'blueberry.muffins.com' + - 'rasberry.muffins.com' + host3: + hostname: 'cake.com' + sans: diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..77bab5d --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +from setuptools import setup, find_packages + +# Lets makes ure we have the correct modules installed before continuing. +# Had issues with people not having OpenSSL not installed, just +# wanted to run a check. +setup( + name="CSR Generator", + version="1.0", + packages=find_packages(), + install_requires=[ 'pyopenssl', 'argparse', 'pyyaml' ] +)