warcprox/warcprox/certauth.py

286 lines
9.1 KiB
Python
Raw Normal View History

import logging
import os
import random
from argparse import ArgumentParser
from datetime import datetime, timedelta
import threading
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
# =================================================================
# Valid for 3 years from now
# Max validity is 39 months:
# https://casecurity.org/2015/02/19/ssl-certificate-validity-periods-limited-to-39-months-starting-in-april/
CERT_NOT_AFTER = 3 * 365 * 24 * 60 * 60
CERTS_DIR = './ca/certs/'
CERT_NAME = 'certauth sample CA'
DEF_HASH_FUNC = hashes.SHA256()
# =================================================================
class CertificateAuthority(object):
"""
Utility class for signing individual certificate
with a root cert.
Static generate_ca_root() method for creating the root cert
All certs saved on filesystem. Individual certs are stored
in specified certs_dir and reused if previously created.
"""
def __init__(self, ca_file, certs_dir, ca_name,
overwrite=False,
cert_not_before=0,
cert_not_after=CERT_NOT_AFTER):
assert(ca_file)
self.ca_file = ca_file
assert(certs_dir)
self.certs_dir = certs_dir
assert(ca_name)
self.ca_name = ca_name
self._file_created = False
self.cert_not_before = cert_not_before
self.cert_not_after = cert_not_after
if not os.path.exists(certs_dir):
os.makedirs(certs_dir)
# if file doesn't exist or overwrite is true
# create new root cert
if (overwrite or not os.path.isfile(ca_file)):
self.cert, self.key = self.generate_ca_root(ca_file, ca_name)
self._file_created = True
# read previously created root cert
else:
self.cert, self.key = self.read_pem(ca_file)
self._lock = threading.Lock()
def cert_for_host(self, host, overwrite=False, wildcard=False):
with self._lock:
host_filename = os.path.join(self.certs_dir, host) + '.pem'
if not overwrite and os.path.exists(host_filename):
self._file_created = False
return host_filename
self.generate_host_cert(host, self.cert, self.key, host_filename,
wildcard)
self._file_created = True
return host_filename
def get_wildcard_cert(self, cert_host):
host_parts = cert_host.split('.', 1)
if len(host_parts) == 2 and '.' in host_parts[1]:
cert_host = host_parts[1]
certfile = self.cert_for_host(cert_host,
wildcard=True)
return certfile
def get_root_PKCS12(self):
return serialization.pkcs12.serialize_key_and_certificates(
name=b"root",
key=self.key,
cert=self.cert,
cas=None,
encryption_algorithm=serialization.NoEncryption()
)
def _make_cert(self, certname):
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, certname),
])
cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
self.key.public_key()
).serial_number(
random.randint(0, 2**64 - 1)
).not_valid_before(
datetime.utcnow()
).not_valid_after(
datetime.utcnow() + timedelta(seconds=self.cert_not_after)
).add_extension(
x509.BasicConstraints(ca=True, path_length=0), critical=True,
).add_extension(
x509.KeyUsage(key_cert_sign=True, crl_sign=True, digital_signature=False,
content_commitment=False, key_encipherment=False,
data_encipherment=False, key_agreement=False, encipher_only=False,
decipher_only=False), critical=True
).add_extension(
x509.SubjectKeyIdentifier.from_public_key(self.key.public_key()), critical=False
).sign(self.key, DEF_HASH_FUNC, default_backend())
return cert
def generate_ca_root(self, ca_file, ca_name, hash_func=DEF_HASH_FUNC):
# Generate key
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
# Generate cert
self.key = key
cert = self._make_cert(ca_name)
# Write cert + key
self.write_pem(ca_file, cert, key)
return cert, key
def generate_host_cert(self, host, root_cert, root_key, host_filename,
wildcard=False, hash_func=DEF_HASH_FUNC):
host = host.encode('utf-8')
# Generate key
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
# Generate CSR
csr = x509.CertificateSigningRequestBuilder().subject_name(
x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, host.decode('utf-8')),
])
).sign(key, hash_func, default_backend())
# Generate Cert
cert_builder = x509.CertificateBuilder().subject_name(
csr.subject
).issuer_name(
root_cert.subject
).public_key(
csr.public_key()
).serial_number(
random.randint(0, 2**64 - 1)
).not_valid_before(
datetime.utcnow()
).not_valid_after(
datetime.utcnow() + timedelta(seconds=self.cert_not_after)
)
if wildcard:
cert_builder = cert_builder.add_extension(
x509.SubjectAlternativeName([
x509.DNSName(host.decode('utf-8')),
x509.DNSName('*.' + host.decode('utf-8')),
]),
critical=False,
)
cert = cert_builder.sign(root_key, hash_func, default_backend())
# Write cert + key
self.write_pem(host_filename, cert, key)
return cert, key
def write_pem(self, filename, cert, key):
with open(filename, 'wb+') as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
))
f.write(cert.public_bytes(serialization.Encoding.PEM))
def read_pem(self, filename):
with open(filename, 'rb') as f:
cert = x509.load_pem_x509_certificate(f.read(), default_backend())
f.seek(0)
key = serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())
return cert, key
# =================================================================
def main(args=None):
parser = ArgumentParser(description='Certificate Authority Cert Maker Tools')
parser.add_argument('root_ca_cert',
help='Path to existing or new root CA file')
parser.add_argument('-c', '--certname', action='store', default=CERT_NAME,
help='Name for root certificate')
parser.add_argument('-n', '--hostname',
help='Hostname certificate to create')
parser.add_argument('-d', '--certs-dir', default=CERTS_DIR,
help='Directory for host certificates')
parser.add_argument('-f', '--force', action='store_true',
help='Overwrite certificates if they already exist')
parser.add_argument('-w', '--wildcard_cert', action='store_true',
help='add wildcard SAN to host: *.<host>, <host>')
r = parser.parse_args(args=args)
certs_dir = r.certs_dir
wildcard = r.wildcard_cert
root_cert = r.root_ca_cert
hostname = r.hostname
if not hostname:
overwrite = r.force
else:
overwrite = False
ca = CertificateAuthority(ca_file=root_cert,
certs_dir=r.certs_dir,
ca_name=r.certname,
overwrite=overwrite)
# Just creating the root cert
if not hostname:
if ca._file_created:
print('Created new root cert: "' + root_cert + '"')
return 0
else:
print('Root cert "' + root_cert +
'" already exists,' + ' use -f to overwrite')
return 1
# Sign a certificate for a given host
overwrite = r.force
host_filename = ca.cert_for_host(hostname,
overwrite, wildcard)
if ca._file_created:
print('Created new cert "' + hostname +
'" signed by root cert ' +
root_cert)
return 0
else:
print('Cert for "' + hostname + '" already exists,' +
' use -f to overwrite')
return 1
if __name__ == "__main__": #pragma: no cover
main()