Skip to content

Instantly share code, notes, and snippets.

@Somrlik
Created July 14, 2018 02:50
Show Gist options
  • Save Somrlik/f8eb4ce69c097d2a4e72873d47525600 to your computer and use it in GitHub Desktop.
Save Somrlik/f8eb4ce69c097d2a4e72873d47525600 to your computer and use it in GitHub Desktop.

SiDeSeG - Simple development SSL certificate generator

This is a simple utility. Do not ever use this to generate anything going live!

I am a small utility hacked together in Python in about 3 hours, suitable for creation of development certificates, if you want to use service workers or test out http/2 features locally.

I do not use any python crypto packages, I am is just a simple wrapper for OpenSSL, because the creator found the whole concept of CAs, server certificates and signing very difficult to grasp and the process felt long and arduous; and thus he created me to help make the process less painful.

If you are just looking to get started with https quickly, install me.

Installation

  1. Install OpenSSL > 1.0.3
  2. Clone the repo
  3. Make a virtualenv python > 3.5
  4. Run python main.py

Configuration

You can configure your certificate authority by editing the openssl-ca.conf file.

You can configure your server certificate by editing the openssl-server-template.conf file. It is a template with simple replacements suitable for more customization, should you choose so.

Generating keys

When you run me for the first time, a new certificate authority will be created in the storage directory.

The most important file that gets created is storage/cacert.pem, which you should add to your trusted certificates, either by using a keychain, utilities from your operating system or a browser interface. If you do not know how to add a certificate to your software - well, google it :)

I will then prompt tou to enter domains that you want to sign. The specification is not clear about the limit, but I would advise against going over 50 entries.

All entries will have their subdomains registered automatically, so if you enter foo.bar, you will get *.foo.bar too.

In the end, you will get a brand new certificate in storage/certs directory that you can plug into your server. It contains the certificate AND the private key. This practice is discouraged, but who cares when it's only running locally.

Upon subsequent runs the authority is already created and you can generate as many certificates as you like.

Web server configuration

nginx

server {
    listen   443;
    ssl    on;
    ssl_certificate    >>path to certificate<<;
}

apache2

<VirtualHost *:433>
    SSLEngine on
    SSLCertificateFile >>path to certificate<<
</VirtualHost>

TODO

  • Do not call openssl, but use a python crypto package

Contributing

Yes, please.

License

MIT

from runner import Runner
if __name__ == '__main__':
try:
Runner().run()
except KeyboardInterrupt:
print('\nQuitting...')
HOME = .
#RANDFILE = $ENV::HOME/.rnd
####################################################################
[ ca ]
default_ca = CA_default # The default ca section
[ CA_default ]
base_dir = .
certificate = $base_dir/cacert.pem # The CA certifcate
private_key = $base_dir/cakey.key # The CA private key
new_certs_dir = $base_dir/certs # wot
database = $base_dir/index.txt # Database index file
serial = $base_dir/serial.txt # The current serial number
unique_subject = no # Set to 'no' to allow creation of
# several certificates with same subject.
default_days = 1825 # how long to certify for
default_crl_days = 30 # how long before next CRL
default_md = sha256 # use public key default MD
preserve = no # keep passed DN ordering
x509_extensions = ca_extensions # The extensions to add to the cert
email_in_dn = no # Don't concat the email in the DN
copy_extensions = copy # Required to copy SANs from CSR to cert
####################################################################
[ req ]
default_bits = 4096
default_keyfile = cakey.key
distinguished_name = ca_distinguished_name
x509_extensions = ca_extensions
string_mask = utf8only
####################################################################
[ ca_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = US
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = Colorado
localityName = Locality Name (eg, city)
localityName_default = South Park
organizationName = Organization Name (eg, company)
organizationName_default = ZZZ Localhost CA, Limited
organizationalUnitName = Organizational Unit (eg, division)
organizationalUnitName_default = Server Research Department
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_default = ZZZ Localhost Test CA
emailAddress = Email Address
emailAddress_default = catch.all@https.localhost
####################################################################
[ ca_extensions ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
basicConstraints = critical, CA:true
keyUsage = keyCertSign, cRLSign
####################################################################
[ signing_policy ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
####################################################################
[ signing_req ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
HOME = .
#RANDFILE = $ENV::HOME/.rnd
####################################################################
[ req ]
default_bits = 2048
default_keyfile = serverkey.key
distinguished_name = server_distinguished_name
req_extensions = server_req_extensions
string_mask = utf8only
####################################################################
[ server_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = {{ country_name }}
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = {{ province_name }}
localityName = Locality Name (eg, city)
localityName_default = {{ locality_name }}
organizationName = Organization Name (eg, company)
organizationName_default = {{ organization_name }}
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_default = {{ common_name }}
emailAddress = Email Address
emailAddress_default = catch-all@https.localhost
####################################################################
[ server_req_extensions ]
subjectKeyIdentifier = hash
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
subjectAltName = @alternate_names
nsComment = "OpenSSL Generated Certificate"
####################################################################
[ alternate_names ]
{{ alternate_names }}
import sys
import os
import subprocess
import shutil
import readline
import time
CA_STORAGE_DIRECTORY = os.path.join(os.path.dirname(__file__), 'storage')
CERTIFICATE_STORAGE_DIRECTORY = os.path.join(CA_STORAGE_DIRECTORY, 'certs')
CA_CERT = os.path.join(CA_STORAGE_DIRECTORY, 'cacert.pem')
CA_KEY = os.path.join(CA_STORAGE_DIRECTORY, 'cakey.key')
CA_INDEX = os.path.join(CA_STORAGE_DIRECTORY, 'index.txt')
CA_SERIAL = os.path.join(CA_STORAGE_DIRECTORY, 'serial.txt')
CA_OPENSSL_CONFIG = os.path.join(CA_STORAGE_DIRECTORY, '../openssl-ca.conf')
SERVER_CERT = os.path.join(CA_STORAGE_DIRECTORY, 'servercert.pem')
SERVER_KEY = os.path.join(CA_STORAGE_DIRECTORY, 'serverkey.key')
SERVER_SIGN_REQUEST = os.path.join(CA_STORAGE_DIRECTORY, 'servercert.csr')
SERVER_OPENSSL_CONFIG = os.path.join(CA_STORAGE_DIRECTORY, 'openssl-server.conf')
SERVER_OPENSSL_TEMPLATE = os.path.join(CA_STORAGE_DIRECTORY, '../openssl-server-template.conf')
GENERATE_CA_COMMAND =\
'openssl req -x509 -config openssl-ca.conf -newkey rsa:4096 -sha256 -nodes -out cacert.pem -outform PEM -batch'
GENERATE_CSR_COMMAND =\
'openssl req -config openssl-server.conf -newkey rsa:2048 -sha256 -nodes -out servercert.csr -outform PEM -batch'
SIGN_NEW_CERTIFICATE =\
'openssl ca -config openssl-ca.conf -policy signing_policy -extensions signing_req -out servercert.pem ' \
'-infiles servercert.csr'
class Runner:
def __init__(self):
if not os.path.exists(CA_STORAGE_DIRECTORY):
os.makedirs(CA_STORAGE_DIRECTORY)
if not os.path.exists(CERTIFICATE_STORAGE_DIRECTORY):
os.makedirs(CERTIFICATE_STORAGE_DIRECTORY)
if not os.path.exists(CA_OPENSSL_CONFIG):
print('The file %s does not exist, cannot continue.' % CA_OPENSSL_CONFIG)
sys.exit(1)
if not os.path.exists(SERVER_OPENSSL_TEMPLATE):
print('The file %s does not exist, cannot continue.' % SERVER_OPENSSL_TEMPLATE)
sys.exit(1)
os.chdir(CA_STORAGE_DIRECTORY)
@staticmethod
def already_has_ca():
return all(os.path.exists(x) for x in [CA_CERT, CA_KEY, CA_INDEX, CA_SERIAL])
def run(self):
if not self.already_has_ca():
self.generate_certificate_authority()
else:
print('Found a certificate authority named %s' % self.get_certificate_authority_name())
names = self.ask_for_qualified_names()
names = self.broaden_inputs(names)
self.request_certificate_for_names(names)
self.sign_new_certificate()
print('Finished!')
def generate_certificate_authority(self):
print('Generating certificate authority...')
shutil.copyfile(CA_OPENSSL_CONFIG, os.path.join(CA_STORAGE_DIRECTORY, 'openssl-ca.conf'))
stdout, stderr, return_code = self.execute_command_whole_output(GENERATE_CA_COMMAND.split(' '))
print(stdout)
print(stderr)
if return_code != 0:
print('Error while creating CA')
sys.exit(1)
open(CA_INDEX, 'a').close()
open(CA_INDEX + '.attr', 'a').close()
with open(CA_SERIAL, 'w') as f:
f.write('01')
print('Generated certificate authority. Be sure to insert ' + CA_CERT + ' to your keychain/OS/whatever.')
@staticmethod
def execute_command_whole_output(cmd: list, _input='') -> (str, str, int):
process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=_input, encoding='ascii')
return process.stdout, process.stderr, int(process.returncode)
@staticmethod
def ask_for_qualified_names():
print('For which domains would you like to generate SSL certificates?')
print('(subdomains are generated automatically with a wildcard)')
inputs = []
while True:
try:
this_input = input('Insert a domain name (or EOF to end) : ')
if '' == this_input:
continue
if '.' not in this_input:
this_input += '.localhost'
inputs.append(this_input)
except EOFError:
print()
if len(inputs) == 0:
print('No names specified, exiting...')
sys.exit(1)
break
return inputs
@staticmethod
def broaden_inputs(inputs):
final_inputs = {}
i = 1
for name in inputs:
final_inputs['DNS.' + str(i)] = name
i += 1
final_inputs['DNS.' + str(i)] = '*.' + name
i += 1
i = 1
for ip in ['127.0.0.1', '::1']:
final_inputs['IP.' + str(i)] = ip
i += 1
return final_inputs
def request_certificate_for_names(self, names):
print('Generating request for certificate...')
mappings = {
'country_name': 'US',
'province_name': 'Colorado',
'locality_name': 'South Park',
'organization_name': 'Localhost Server Inc.',
'common_name': 'For example ' + names.get('DNS.1'),
'alternate_names': '\n'.join('%s = %s' % (k, v) for k, v in names.items())
}
with open(SERVER_OPENSSL_TEMPLATE, 'r') as t:
final_config = t.read()
for key, value in mappings.items():
final_config = final_config.replace('{{ %s }}' % key, value)
pass
with open(SERVER_OPENSSL_CONFIG, 'w') as c:
c.write(final_config)
stdout, stderr, return_code = self.execute_command_whole_output(GENERATE_CSR_COMMAND.split(' '))
print(stdout)
print(stderr)
if return_code != 0:
print('Error while creating signing request')
sys.exit(1)
print('Request generated...')
pass
def sign_new_certificate(self):
stdout, stderr, return_code = self.execute_command_whole_output(SIGN_NEW_CERTIFICATE.split(' '), 'y\ny\n')
print(stdout)
print(stderr)
if return_code != 0:
print('Error while signing request')
sys.exit(1)
with open(CA_SERIAL + '.old', 'r') as s:
serial = s.read().strip()
pem_path = os.path.join(CERTIFICATE_STORAGE_DIRECTORY, '%s.pem' % serial)
with open(pem_path, 'a') as pem:
with open(SERVER_KEY, 'r') as key:
pem.write(key.read())
print('Wrote the private key into %s' % pem_path)
@staticmethod
def get_certificate_authority_name():
with open(CA_OPENSSL_CONFIG, 'r') as f:
for line in f:
if 'commonName_default' in line:
return line.split('=')[1].strip()
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment