Correct TODO's ;)

pull/7/head
Ben Mz 2018-06-15 20:28:40 +02:00
parent 33ad7bbd5c
commit 33eedb39a2
4 changed files with 437 additions and 276 deletions

View File

@ -3,9 +3,7 @@
Generate a key, self-signed certificate, and certificate request.
## Information
You'll notice there is a csrgen and csrgen35. This corresponds to their respective Python versions.
- csrgen uses Python 2.7
- csrgen34 uses Python 3.5
You'll notice there is only one version of python scripts. This can be used with both python(2.7) and python(3.5).
## Installation / Dependencies
The following modules are required:
@ -14,14 +12,42 @@ The following modules are required:
- YAML (pyyaml)
I've included a setup.py that will install these dependencies if you run:
```
```bash
python setup.py install
```
## Usage
csrgen [fqdn]
```bash
csrgen -n [fqdn]
```
Note: you could always use '-h' in order to get some informations ;)
```bash
user@host> ./csrgen.py -h
usage: csrgen.py [-h] [-v] [-d] [-l LOG] [-n NAME] [-s [SAN [SAN ...]]]
[-k KEYSIZE] [-u UNATTENDED] [-f FILE] [-a] [-c]
optional arguments:
-h, --help show this help message and exit
-v, --verbose Output more infos
-d, --debug Enable debug mode
-l LOG, --log LOG Define log file (default: /var/log/certGen.log
-n NAME, --name NAME Provide the FQDN
-s [SAN [SAN ...]], --san [SAN [SAN ...]]
SANS, define alternative names
-k KEYSIZE, --keysize KEYSIZE
Provide the key size
-u UNATTENDED, --unattended UNATTENDED
Load CSR predefined options
-f FILE, --file FILE Load hosts file (CN and optional Alternate Names) list
-a, --authority Generate Authority certificate (Default is server)
-c, --client Generate client certificate (Default is server)
```
Basic usage would be
```python
python csrgen -n test.test.com
```
@ -30,13 +56,33 @@ certificate and request are generated. This can be acheived by adding a -s.
csrgen <hostname> -s <san0> <san1>
```
```bash
python csrgen -n test.test.com -s mushu.test.com pushu.test.com
```
You can pass a yaml file as arguments to pre-fill your CSR values (C, ST, L, O, OU). Basically any attribute defined in the YAML file will be set in the certificate. On exception: if you force the hostname with -n parameter, it will override the 'Hostname' set in YAML file.
```python
python csrgen -f sample.yaml -u csr.yaml
```
## Debug options
A debug option (-d) and a verbose flag (-v) are available. If in any case you want to check the content of generated files, here is a quick cheat-sheet...
### To read a CSR
```bash
openssl req -in test.test.com.csr -noout -text
```
### To read a Certificate (CER)
```bash
openssl x509 -in cerfile.cer -noout -text
```
### To read a Certificate (PEM)
```bash
openssl x509 -inform pem -in cerfile.cer -noout -text
```
# TODO
- Consolidate Python 2.7 & 3.5
- For CLI (not -f), ensure a -n is provided or "fail" gracefully.
- Have the C, ST, L, O, OU stored in a .conf file and ask if
these are the settings the user wants to use before running.
- Turn this into a Class.
- Implement Unit Tests

6
csr.yaml Normal file
View File

@ -0,0 +1,6 @@
---
C: 'FR'
ST: 'PACA'
L: 'Gap'
O: 'ProHacktive SAS'
OU: 'prohacktive.io'

View File

@ -11,147 +11,398 @@
# feed in a .yaml file via the CLI. See the example sample.yaml in this
# repository for examples.
#
# If you want to predefine some of your CSR attributes, you can use the -u command
# to feed in a .yaml file via the CLI. See the example csr.yaml in this repository
# for examples.
#
# Author: Courtney Cotton <cotton@cottoncourtney.com> 06-25-2014, Updated 8-9-2017
# Author: Ben Mz <bmz@prohacktive.io> Updated 06-15-2018
# Libraries/Modules
import sys, platform, yaml
import argparse, logging, logging.handlers
from OpenSSL import crypto, SSL
import argparse
import yaml
__version__ = '1.0.1'
# 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
class Certificate:
def __init__(self, logger, opts={}):
self._logger = logger
self.allowed = ["Digital Signature", "Non Repudiation", "Key Encipherment"]
# Set default usage
self._level = logging.WARNING
self._key_size = 2048
self._ca = False
self._verbose = True
self.usage = ','.join(self.allowed)
# Appends SAN to have 'DNS:'
ss = []
for i in sans:
ss.append("DNS: %s" % i)
ss = ", ".join(ss)
try:
self._verbose = opts['verbose']
del opts['verbose']
except KeyError:
pass
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")
self._header()
generateFiles(csrfile, req)
generateFiles(keyfile, key)
# Set default log level
try:
self._level = opts['level']
del opts['level']
except KeyError:
pass
return req
# Set key size
try:
if int(opts['size']) in [1024,2048,4096]:
self._key_size = int(opts['size'])
del opts['size']
except KeyError:
pass
except ValueError:
pass
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 <full name> []: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
try:
for usage in opts['usage']:
if usage not in self.allowed:
raise Exception('Invalid key usage: {u}'.format(u=usage))
self.usage = opts['usage']
del opts['usage']
except KeyError:
# Keep server default if no usage is set
pass
# 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'
self.opts = opts
self.output('[*] We have already set options:',level=logging.DEBUG)
self.output('{o}'.format(o=self.opts),level=logging.DEBUG)
def _header(self):
self.output('\t\t..:: Certificate Signing Request (CSR) Generator ::..\n')
# 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)
def _isCA(self):
return "TRUE" if self._ca else "FALSE"
# 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']
def _ask(self, msg, country=False, default=None):
while True:
rep = raw_input(msg)
if country and (len(rep)) and (len(rep) != 2):
self.output('[!] Sorry this value is invalid (should be two letters only).')
continue
if len(rep) is 0:
if default is None:
self.output('[!] Sorry this value is mandatory.')
continue
rep = default
break
return rep
# Generate Certificate Signing Request (CSR)
def generateCSR(self):
try:
nodename = self.opts['hostname']
except KeyError:
raise Exception('Could not generate certificate with empty hostname')
# 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 = []
try:
for entry in self.opts['sans']:
ss.append("DNS: {e}".format(e=entry))
except KeyError:
pass
ss = ", ".join(ss)
req = crypto.X509Req()
req.get_subject().CN = nodename
try:
req.get_subject().countryName = self.opts['C']
req.get_subject().stateOrProvinceName = self.opts['ST']
req.get_subject().localityName = self.opts['L']
req.get_subject().organizationName = self.opts['O']
req.get_subject().organizationalUnitName = self.opts['OU']
except KeyError:
raise Exception('Missing mandatory certificate value!')
# Email Address is not mandatory
try:
req.get_subject().emailAddress = self.opts['emailAddress']
except KeyError:
pass
# Add in extensions
base_constraints = ([
crypto.X509Extension("keyUsage", False, self.usage),
crypto.X509Extension("basicConstraints", False, "CA:{c}".format(c=self._isCA())),
])
x509_extensions = base_constraints
# If there are SAN entries, append the base_constraints to include them.
if len(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 = self.generateKey(TYPE_RSA, self._key_size)
req.set_pubkey(key)
req.sign(key, "sha256")
self.output('[+] Generate CSR file: {f}'.format(f=csrfile))
self.generateFiles(csrfile, req)
self.output('[+] Generate Key file: {f}'.format(f=keyfile))
self.generateFiles(keyfile, key)
self.output("\n[+] Your CSR and certificate ({s} bits) are now generated with:".format(s=self._key_size))
for k,v in self.opts.items():
if k is 'hostname':
self.output("\t[{k}]\t-> {v}".format(k=k,v=v))
else:
self.output("\t[{k}]\t\t-> {v}".format(k=k,v=v))
return req
def getCSRSubjects(self):
fields = ['C','ST','L','O','OU','hostname']
for field in fields:
try:
# Check if field is already setup
if self.opts[field]:
self.output('[*] Field {n} is set'.format(n=field), level=logging.DEBUG)
continue
except KeyError:
self.output('[*] Field {n} is NOT set'.format(n=field), level=logging.DEBUG)
pass
if field is 'C':
self.opts['C'] = self._ask("Enter your Country Name (2 letter code) [US]: ", default='US', country=True)
elif field is 'ST':
self.opts['ST'] = self._ask("Enter your State or Province <full name> [California]: ", default='California')
elif field is 'L':
self.opts['L'] = self._ask("Enter your (Locality Name (eg, city) [San Francisco]: ", default='San Francisco')
elif field is 'O':
self.opts['O'] = self._ask("Enter your Organization Name (eg, company) [FTW Enterprise]: ", default='FTW Enterprise')
elif field is 'OU':
self.opts['OU'] = self._ask("Enter your Organizational Unit (eg, section) [IT]: ", default='IT')
elif field is 'hostname':
self.opts['hostname'] = self._ask("Enter your Common Name (eg, DNS name) [{n}]:".format(n=platform.node()), default=platform.node())
# 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'
# Parse the contents of the YAML file and then
# auto setup values.
def loadDefaults(self, csr_file):
try:
self.output("[+] Reading default values file: {f}".format(f=csr_file), level=logging.DEBUG)
cfg = self._parseYAML(csr_file)
except Exception as err:
raise Exception(err)
for k,v in cfg.items():
if (k is 'C') and len(v) != 2:
continue
if len(v) is 0:
continue
try:
self.opts[k] = str(v)
except Exception:
pass
# Parse the contents of the YAML file and then
# generate a CSR for each of them.
def loadNodes(self, nodes_file):
try:
self.output("[+] Reading nodes file: {f}".format(f=nodes_file), level=logging.DEBUG)
cfg = self._parseYAML(nodes_file)
except Exception as err:
raise Exception(err)
self.output('[+] Generate certificates for:')
for k,v in cfg.items():
self.opts['hostname'] = cfg[k]['hostname']
if cfg[k]['sans']:
self.opts['sans'] = cfg[k]['sans']
else:
self.opts['sans'] = ''
self.output("[+] host: {h}, alternate names: {s}".format(h=self.opts['hostname'], s=self.opts['sans']))
self.generateCSR()
def _parseYAML(self, yaml_file):
"""Parse YAML file and return object generated
"""
with open(yaml_file, 'r') as stream:
cfg = yaml.load(stream)
return cfg
def generateKey(self, type, bits):
"""Generate Private Key
"""
self.output('[+] Generate certificate seed Key...')
key = crypto.PKey()
key.generate_key(type, bits)
return key
def generateFiles(self, mkFile, request):
"""Generate .csr/key files.
"""
with open(mkFile, "w") as f:
if ".csr" in mkFile:
f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, request))
elif ".key" in mkFile:
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, request))
else:
self.output("[!] Failed to create CSR/Key files", level=logging.ERROR)
def output(self, msg, level=logging.WARNING):
"""Generate output to CLI and log file
"""
# Output to log
if level == logging.DEBUG:
self._logger.debug(msg)
elif level == logging.INFO:
self._logger.info(msg)
elif level == logging.WARNING:
self._logger.warning(msg)
elif level == logging.ERROR:
self._logger.error(msg)
elif level == logging.CRITICAL:
self._logger.critical(msg)
# Misconfigured level are high notifications
else:
sans = ''
print "host: %s, sans: %s" % (hostname, sans)
generateCSR(hostname, C, ST, L, O, OU, sans)
exit()
self._logger.error("[!] Invalid level for message: {m}".format(m=msg))
# Generate Private Key
def generateKey(type, bits):
key = crypto.PKey()
key.generate_key(type, bits)
return key
# Output to CLI if needed
if self._verbose and (level >= self._level):
sys.stdout.write("{m}\n".format(m=msg))
# 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()
class Authority(Certificate):
def __init__(self,logger, opts):
# Init certificate
try:
super(Authority, self).__init__(logger,opts)
except Exception as err:
raise Exception("Error at {n} initialization: {e}".format(n=self._name, e=err))
self._ca = True
def initialize(self):
self.generateCSR()
# 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()
def main(argv):
# Define default values
VERBOSE = False
LOG_FILE = "/var/log/certGen.log"
LOG_LEVEL = logging.WARNING
opts = {}
# 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()
# Run Portion
# This section will parse the flags available via command line.
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", help="Output more infos", action="store_true")
parser.add_argument("-d", "--debug", help="Enable debug mode", action="store_true")
parser.add_argument("-l", "--log", help="Define log file (default: {f}".format(f=LOG_FILE))
parser.add_argument("-n", "--name", help="Provide the FQDN", action="store", default="")
parser.add_argument("-s", "--san", help="SANS, define alternative names", action="store", nargs='*', default="")
parser.add_argument("-k", "--keysize", help="Provide the key size", action="store", default="2048")
parser.add_argument("-u", "--unattended", help="Load CSR predefined options", action="store", default="")
parser.add_argument("-f", "--file", help="Load hosts file (CN and optional Alternate Names) list", action="store", default="")
parser.add_argument("-a", "--authority", help="Generate Authority certificate (Default is server)", action="store_true")
parser.add_argument("-c", "--client", help="Generate client certificate (Default is server)", action="store_true")
args = parser.parse_args()
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)
# 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.
if args.log:
LOG_FILE = args.log
if args.verbose:
VERBOSE = True
opts['verbose'] = VERBOSE
if args.debug:
opts['level'] = logging.DEBUG
# Define logger
try:
logger = logging.getLogger('certgen')
hdlr = logging.handlers.TimedRotatingFileHandler(LOG_FILE, when="midnight", backupCount=3)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
hdlr.setFormatter(formatter)
logger.addHandler(hdlr)
logger.setLevel(LOG_LEVEL)
except AttributeError as err:
sys.stdout.write("[!] Unable to open log file {f}: {e}\n".format(f=LOG_FILE, e=err))
sys.exit(1)
except IOError as err:
sys.stdout.write("[!] Unable to open log file {f}: {e}\n".format(f=LOG_FILE, e=err))
sys.exit(1)
if args.keysize:
opts['size'] = args.keysize
if args.authority:
if args.client:
sys.stdout.write('[!] You can generate multiple certificate type at one time.')
sys.exit(2)
if args.san:
sys.stdout.write('[!] You can not specify alternative names with authority certificates')
sys.exit(1)
opts['usage'] = ['Certificate signing','CRL signing']
if args.client:
if args.san:
sys.stdout.write('[!] You can not specify alternative names with client certificates')
sys.exit(1)
opts['usage'] = ["digitalSignature"]
# Store infos if set
if args.name:
opts['hostname'] = args.name
if args.san:
opts['sans'] = args.san
try:
# Initialize certificate object
cert = Certificate(logger, opts)
if args.unattended:
cert.loadDefaults(args.unattended)
# Run interactively if needed for C, ST, L, O, OU values
cert.getCSRSubjects()
if args.file:
cert.generateFromFile(args.file)
else:
cert.generateCSR()
except KeyboardInterrupt:
sys.stdout.write('\n[!] Exit requested.')
except SystemExit:
sys.stdout.write('\n[!] Software aborted.')
sys.stdout.write('\nBye! ;)\n')
if __name__ == '__main__':
main(sys.argv)

View File

@ -1,142 +0,0 @@
#!/usr/bin/env python3
#
# Generate a key, self-signed certificate, and certificate request.
# Usage: csrgen <fqdn>
#
# 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 <hostname> -s <san0> <san1>
#
# Author: Courtney Cotton <cotton@cottoncourtney.com> 06-25-2014
# mod'd for python 3.5
# Libraries/Modules
from OpenSSL import crypto, SSL
import argparse
import yaml
# Generate Certificate Signing Request (CSR)
def generateCSR(nodename, sans=[]):
while True:
C = 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 = input("Enter your State or Province <full name> []:California: ")
if len(ST) == 0:
print("Please enter your State or Province.")
continue
L = input("Enter your (Locality Name (eg, city) []:San Francisco: ")
if len(L) == 0:
print("Please enter your City.")
continue
O = input("Enter your Organization Name (eg, company) []:FTW Enterprise: ")
if len(L) == 0:
print("Please enter your Organization Name.")
continue
OU = 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
# added bytearray to string
# before -> "keyUsage"
# after -> b"keyUsage"
base_constraints = ([
crypto.X509Extension(b"keyUsage", False, b"Digital Signature, Non Repudiation, Key Encipherment"),
crypto.X509Extension(b"basicConstraints", False, b"CA:FALSE"),
])
x509_extensions = base_constraints
# If there are SAN entries, append the base_constraints to include them.
if ss:
san_constraint = crypto.X509Extension(b"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)
# change to sha 256?
# 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, "wb")
f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, request))
f.close()
# print test
#print(crypto.dump_certificate_request(crypto.FILETYPE_PEM, request))
elif mkFile == 'host.key':
f = open(mkFile, "wb")
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, request))
f.close()
else:
print("Failed.")
exit()
# 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
config_file = args.file
# Run the primary function(s) based on input.
if config_file is None:
generateFromFile(config_file)
else:
generateCSR(hostname,sans)