| #!/usr/bin/python | |
| # Copyright (c) 2015 The Chromium Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Set of helpers to generate signed X.509v3 certificates. | |
| This works by shelling out calls to the 'openssl req' and 'openssl ca' | |
| commands, and passing the appropriate command line flags and configuration file | |
| (.cnf). | |
| """ | |
| import base64 | |
| import hashlib | |
| import os | |
| import shutil | |
| import subprocess | |
| import sys | |
| import openssl_conf | |
| # Enum for the "type" of certificate that is to be created. This is used to | |
| # select sane defaults for the .cnf file and command line flags, but they can | |
| # all be overridden. | |
| TYPE_CA = 2 | |
| TYPE_END_ENTITY = 3 | |
| # March 1st, 2015 12:00 UTC | |
| MARCH_1_2015_UTC = '150301120000Z' | |
| # March 2nd, 2015 12:00 UTC | |
| MARCH_2_2015_UTC = '150302120000Z' | |
| # January 1st, 2015 12:00 UTC | |
| JANUARY_1_2015_UTC = '150101120000Z' | |
| # September 1st, 2015 12:00 UTC | |
| SEPTEMBER_1_2015_UTC = '150901120000Z' | |
| # January 1st, 2016 12:00 UTC | |
| JANUARY_1_2016_UTC = '160101120000Z' | |
| # March 10, 2018 12:00 UTC | |
| MARCH_10_2018_UTC = '180310120000Z' | |
| # January 1st, 2021 12:00 UTC | |
| JANUARY_1_2021_UTC = '210101120000Z' | |
| KEY_PURPOSE_ANY = 'anyExtendedKeyUsage' | |
| KEY_PURPOSE_SERVER_AUTH = 'serverAuth' | |
| KEY_PURPOSE_CLIENT_AUTH = 'clientAuth' | |
| DEFAULT_KEY_PURPOSE = KEY_PURPOSE_SERVER_AUTH | |
| # Counters used to generate unique (but readable) path names. | |
| g_cur_path_id = {} | |
| # Output paths used: | |
| # - g_tmp_dir: where any temporary files (cert req, signing db etc) are | |
| # saved to. | |
| # See init() for how these are assigned. | |
| g_tmp_dir = None | |
| g_invoking_script_path = None | |
| # The default validity range of generated certificates. Can be modified with | |
| # set_default_validity_range(). | |
| g_default_start_date = MARCH_10_2018_UTC | |
| g_default_end_date = JANUARY_1_2021_UTC | |
| def set_default_validity_range(start_date, end_date): | |
| """Sets the validity range that will be used for certificates created with | |
| Certificate""" | |
| global g_default_start_date | |
| global g_default_end_date | |
| g_default_start_date = start_date | |
| g_default_end_date = end_date | |
| def get_unique_path_id(name): | |
| """Returns a base filename that contains 'name', but is unique to the output | |
| directory""" | |
| # Use case-insensitive matching for counting duplicates, since some | |
| # filesystems are case insensitive, but case preserving. | |
| lowercase_name = name.lower() | |
| path_id = g_cur_path_id.get(lowercase_name, 0) | |
| g_cur_path_id[lowercase_name] = path_id + 1 | |
| # Use a short and clean name for the first use of this name. | |
| if path_id == 0: | |
| return name | |
| # Otherwise append the count to make it unique. | |
| return '%s_%d' % (name, path_id) | |
| def get_path_in_tmp_dir(name, suffix): | |
| return os.path.join(g_tmp_dir, '%s%s' % (name, suffix)) | |
| class Key(object): | |
| """Describes a public + private key pair. It is a dumb wrapper around an | |
| on-disk key.""" | |
| def __init__(self, path): | |
| self.path = path | |
| def get_path(self): | |
| """Returns the path to a file that contains the key contents.""" | |
| return self.path | |
| def get_or_generate_key(generation_arguments, path): | |
| """Helper function to either retrieve a key from an existing file |path|, or | |
| generate a new one using the command line |generation_arguments|.""" | |
| generation_arguments_str = ' '.join(generation_arguments) | |
| # If the file doesn't already exist, generate a new key using the generation | |
| # parameters. | |
| if not os.path.isfile(path): | |
| key_contents = subprocess.check_output(generation_arguments) | |
| # Prepend the generation parameters to the key file. | |
| write_string_to_file(generation_arguments_str + '\n' + key_contents, | |
| path) | |
| else: | |
| # If the path already exists, confirm that it is for the expected key type. | |
| first_line = read_file_to_string(path).splitlines()[0] | |
| if first_line != generation_arguments_str: | |
| sys.stderr.write(('\nERROR: The existing key file:\n %s\nis not ' | |
| 'compatible with the requested parameters:\n "%s" vs "%s".\n' | |
| 'Delete the file if you want to re-generate it with the new ' | |
| 'parameters, otherwise pick a new filename\n') % ( | |
| path, first_line, generation_arguments_str)) | |
| sys.exit(1) | |
| return Key(path) | |
| def get_or_generate_rsa_key(size_bits, path): | |
| """Retrieves an existing key from a file if the path exists. Otherwise | |
| generates an RSA key with the specified bit size and saves it to the path.""" | |
| return get_or_generate_key(['openssl', 'genrsa', str(size_bits)], path) | |
| def get_or_generate_ec_key(named_curve, path): | |
| """Retrieves an existing key from a file if the path exists. Otherwise | |
| generates an EC key with the specified named curve and saves it to the | |
| path.""" | |
| return get_or_generate_key(['openssl', 'ecparam', '-name', named_curve, | |
| '-genkey'], path) | |
| def create_key_path(base_name): | |
| """Generates a name that contains |base_name| in it, and is relative to the | |
| "keys/" directory. If create_key_path(xxx) is called more than once during | |
| the script run, a suffix will be added.""" | |
| # Save keys to CWD/keys/*.key | |
| keys_dir = 'keys' | |
| # Create the keys directory if it doesn't exist | |
| if not os.path.exists(keys_dir): | |
| os.makedirs(keys_dir) | |
| return get_unique_path_id(os.path.join(keys_dir, base_name)) + '.key' | |
| class Certificate(object): | |
| """Helper for building an X.509 certificate.""" | |
| def __init__(self, name, cert_type, issuer): | |
| # The name will be used for the subject's CN, and also as a component of | |
| # the temporary filenames to help with debugging. | |
| self.name = name | |
| self.path_id = get_unique_path_id(name) | |
| # Allow the caller to override the key later. If no key was set will | |
| # auto-generate one. | |
| self.key = None | |
| # The issuer is also a Certificate object. Passing |None| means it is a | |
| # self-signed certificate. | |
| self.issuer = issuer | |
| if issuer is None: | |
| self.issuer = self | |
| # The config contains all the OpenSSL options that will be passed via a | |
| # .cnf file. Set up defaults. | |
| self.config = openssl_conf.Config() | |
| self.init_config() | |
| # Some settings need to be passed as flags rather than in the .cnf file. | |
| # Technically these can be set though a .cnf, however doing so makes it | |
| # sticky to the issuing certificate, rather than selecting it per | |
| # subordinate certificate. | |
| self.validity_flags = [] | |
| self.md_flags = [] | |
| # By default OpenSSL will use the current time for the start time. Instead | |
| # default to using a fixed timestamp for more predictable results each time | |
| # the certificates are re-generated. | |
| self.set_validity_range(g_default_start_date, g_default_end_date) | |
| # Use SHA-256 when THIS certificate is signed (setting it in the | |
| # configuration would instead set the hash to use when signing other | |
| # certificates with this one). | |
| self.set_signature_hash('sha256') | |
| # Set appropriate key usages and basic constraints. For flexibility in | |
| # testing (since want to generate some flawed certificates) these are set | |
| # on a per-certificate basis rather than automatically when signing. | |
| if cert_type == TYPE_END_ENTITY: | |
| self.get_extensions().set_property('keyUsage', | |
| 'critical,digitalSignature,keyEncipherment') | |
| self.get_extensions().set_property('extendedKeyUsage', | |
| 'serverAuth,clientAuth') | |
| else: | |
| self.get_extensions().set_property('keyUsage', | |
| 'critical,keyCertSign,cRLSign') | |
| self.get_extensions().set_property('basicConstraints', 'critical,CA:true') | |
| # Tracks whether the PEM file for this certificate has been written (since | |
| # generation is done lazily). | |
| self.finalized = False | |
| # Initialize any files that will be needed if this certificate is used to | |
| # sign other certificates. Picks a pseudo-random starting serial number | |
| # based on the file system path, and will increment this for each signed | |
| # certificate. | |
| if not os.path.exists(self.get_serial_path()): | |
| write_string_to_file('%s\n' % self.make_serial_number(), | |
| self.get_serial_path()) | |
| if not os.path.exists(self.get_database_path()): | |
| write_string_to_file('', self.get_database_path()) | |
| def set_validity_range(self, start_date, end_date): | |
| """Sets the Validity notBefore and notAfter properties for the | |
| certificate""" | |
| self.validity_flags = ['-startdate', start_date, '-enddate', end_date] | |
| def set_signature_hash(self, md): | |
| """Sets the hash function that will be used when signing this certificate. | |
| Can be sha1, sha256, sha512, md5, etc.""" | |
| self.md_flags = ['-md', md] | |
| def get_extensions(self): | |
| return self.config.get_section('req_ext') | |
| def get_subject(self): | |
| """Returns the configuration section responsible for the subject of the | |
| certificate. This can be used to alter the subject to be more complex.""" | |
| return self.config.get_section('req_dn') | |
| def get_path(self, suffix): | |
| """Forms a path to an output file for this certificate, containing the | |
| indicated suffix. The certificate's name will be used as its basis.""" | |
| return os.path.join(g_tmp_dir, '%s%s' % (self.path_id, suffix)) | |
| def get_name_path(self, suffix): | |
| """Forms a path to an output file for this CA, containing the indicated | |
| suffix. If multiple certificates have the same name, they will use the same | |
| path.""" | |
| return get_path_in_tmp_dir(self.name, suffix) | |
| def set_key(self, key): | |
| assert self.finalized is False | |
| self.set_key_internal(key) | |
| def set_key_internal(self, key): | |
| self.key = key | |
| # Associate the private key with the certificate. | |
| section = self.config.get_section('root_ca') | |
| section.set_property('private_key', self.key.get_path()) | |
| def get_key(self): | |
| if self.key is None: | |
| self.set_key_internal( | |
| get_or_generate_rsa_key(2048, create_key_path(self.name))) | |
| return self.key | |
| def get_cert_path(self): | |
| return self.get_path('.pem') | |
| def get_serial_path(self): | |
| return self.get_name_path('.serial') | |
| def make_serial_number(self): | |
| """Returns a hex number that is generated based on the certificate file | |
| path. This serial number will likely be globally unique, which makes it | |
| easier to use the certificates with NSS (which assumes certificate | |
| equivalence based on issuer and serial number).""" | |
| # Hash some predictable values together to get the serial number. The | |
| # predictability is so that re-generating certificate chains is | |
| # a no-op, however each certificate ends up with a unique serial number. | |
| m = hashlib.sha1() | |
| # Mix in up to the last 3 components of the path for the generating script. | |
| # For example, | |
| # "verify_certificate_chain_unittest/my_test/generate_chains.py" | |
| script_path = os.path.realpath(g_invoking_script_path) | |
| script_path = "/".join(script_path.split(os.sep)[-3:]) | |
| m.update(script_path) | |
| # Mix in the path_id, which corresponds to a unique path for the | |
| # certificate under out/ (and accounts for non-unique certificate names). | |
| m.update(self.path_id) | |
| serial_bytes = m.digest() | |
| # SHA1 digest is 20 bytes long, which is appropriate for a serial number. | |
| # However, need to also make sure the most significant bit is 0 so it is | |
| # not a "negative" number. | |
| serial_bytes = chr(ord(serial_bytes[0]) & 0x7F) + serial_bytes[1:] | |
| return serial_bytes.encode("hex") | |
| def get_csr_path(self): | |
| return self.get_path('.csr') | |
| def get_database_path(self): | |
| return self.get_name_path('.db') | |
| def get_config_path(self): | |
| return self.get_path('.cnf') | |
| def get_cert_pem(self): | |
| # Finish generating a .pem file for the certificate. | |
| self.finalize() | |
| # Read the certificate data. | |
| return read_file_to_string(self.get_cert_path()) | |
| def finalize(self): | |
| """Finishes the certificate creation process. This generates any needed | |
| key, creates and signs the CSR. On completion the resulting PEM file can be | |
| found at self.get_cert_path()""" | |
| if self.finalized: | |
| return # Already finalized, no work needed. | |
| self.finalized = True | |
| # Ensure that the issuer has been "finalized", since its outputs need to be | |
| # accessible. Note that self.issuer could be the same as self. | |
| self.issuer.finalize() | |
| # Ensure the certificate has a key (gets lazily created by this call if | |
| # missing). | |
| self.get_key() | |
| # Serialize the config to a file. | |
| self.config.write_to_file(self.get_config_path()) | |
| # Create a CSR. | |
| subprocess.check_call( | |
| ['openssl', 'req', '-new', | |
| '-key', self.key.get_path(), | |
| '-out', self.get_csr_path(), | |
| '-config', self.get_config_path()]) | |
| cmd = ['openssl', 'ca', '-batch', '-in', | |
| self.get_csr_path(), '-out', self.get_cert_path(), '-config', | |
| self.issuer.get_config_path()] | |
| if self.issuer == self: | |
| cmd.append('-selfsign') | |
| # Add in any extra flags. | |
| cmd.extend(self.validity_flags) | |
| cmd.extend(self.md_flags) | |
| # Run the 'openssl ca' command. | |
| subprocess.check_call(cmd) | |
| def init_config(self): | |
| """Initializes default properties in the certificate .cnf file that are | |
| generic enough to work for all certificates (but can be overridden later). | |
| """ | |
| # -------------------------------------- | |
| # 'req' section | |
| # -------------------------------------- | |
| section = self.config.get_section('req') | |
| section.set_property('encrypt_key', 'no') | |
| section.set_property('utf8', 'yes') | |
| section.set_property('string_mask', 'utf8only') | |
| section.set_property('prompt', 'no') | |
| section.set_property('distinguished_name', 'req_dn') | |
| section.set_property('req_extensions', 'req_ext') | |
| # -------------------------------------- | |
| # 'req_dn' section | |
| # -------------------------------------- | |
| # This section describes the certificate subject's distinguished name. | |
| section = self.config.get_section('req_dn') | |
| section.set_property('commonName', '"%s"' % (self.name)) | |
| # -------------------------------------- | |
| # 'req_ext' section | |
| # -------------------------------------- | |
| # This section describes the certificate's extensions. | |
| section = self.config.get_section('req_ext') | |
| section.set_property('subjectKeyIdentifier', 'hash') | |
| # -------------------------------------- | |
| # SECTIONS FOR CAs | |
| # -------------------------------------- | |
| # The following sections are used by the 'openssl ca' and relate to the | |
| # signing operation. They are not needed for end-entity certificate | |
| # configurations, but only if this certifiate will be used to sign other | |
| # certificates. | |
| # -------------------------------------- | |
| # 'ca' section | |
| # -------------------------------------- | |
| section = self.config.get_section('ca') | |
| section.set_property('default_ca', 'root_ca') | |
| section = self.config.get_section('root_ca') | |
| section.set_property('certificate', self.get_cert_path()) | |
| section.set_property('new_certs_dir', g_tmp_dir) | |
| section.set_property('serial', self.get_serial_path()) | |
| section.set_property('database', self.get_database_path()) | |
| section.set_property('unique_subject', 'no') | |
| # These will get overridden via command line flags. | |
| section.set_property('default_days', '365') | |
| section.set_property('default_md', 'sha256') | |
| section.set_property('policy', 'policy_anything') | |
| section.set_property('email_in_dn', 'no') | |
| section.set_property('preserve', 'yes') | |
| section.set_property('name_opt', 'multiline,-esc_msb,utf8') | |
| section.set_property('cert_opt', 'ca_default') | |
| section.set_property('copy_extensions', 'copy') | |
| section.set_property('x509_extensions', 'signing_ca_ext') | |
| section.set_property('default_crl_days', '30') | |
| section.set_property('crl_extensions', 'crl_ext') | |
| section = self.config.get_section('policy_anything') | |
| section.set_property('domainComponent', 'optional') | |
| section.set_property('countryName', 'optional') | |
| section.set_property('stateOrProvinceName', 'optional') | |
| section.set_property('localityName', 'optional') | |
| section.set_property('organizationName', 'optional') | |
| section.set_property('organizationalUnitName', 'optional') | |
| section.set_property('commonName', 'optional') | |
| section.set_property('emailAddress', 'optional') | |
| section = self.config.get_section('signing_ca_ext') | |
| section.set_property('subjectKeyIdentifier', 'hash') | |
| section.set_property('authorityKeyIdentifier', 'keyid:always') | |
| section.set_property('authorityInfoAccess', '@issuer_info') | |
| section.set_property('crlDistributionPoints', '@crl_info') | |
| section = self.config.get_section('issuer_info') | |
| section.set_property('caIssuers;URI.0', | |
| 'http://url-for-aia/%s.cer' % (self.name)) | |
| section = self.config.get_section('crl_info') | |
| section.set_property('URI.0', 'http://url-for-crl/%s.crl' % (self.name)) | |
| section = self.config.get_section('crl_ext') | |
| section.set_property('authorityKeyIdentifier', 'keyid:always') | |
| section.set_property('authorityInfoAccess', '@issuer_info') | |
| def text_data_to_pem(block_header, text_data): | |
| return '%s\n-----BEGIN %s-----\n%s\n-----END %s-----\n' % (text_data, | |
| block_header, base64.b64encode(text_data), block_header) | |
| def write_chain(description, chain, out_pem): | |
| """Writes the chain to a .pem file as a series of CERTIFICATE blocks""" | |
| # Prepend the script name that generated the file to the description. | |
| test_data = '[Created by: %s]\n\n%s\n' % (sys.argv[0], description) | |
| # Write the certificate chain to the output file. | |
| for cert in chain: | |
| test_data += '\n' + cert.get_cert_pem() | |
| write_string_to_file(test_data, out_pem) | |
| def write_string_to_file(data, path): | |
| with open(path, 'w') as f: | |
| f.write(data) | |
| def read_file_to_string(path): | |
| with open(path, 'r') as f: | |
| return f.read() | |
| def init(invoking_script_path): | |
| """Creates an output directory to contain all the temporary files that may be | |
| created, as well as determining the path for the final output. These paths | |
| are all based off of the name of the calling script. | |
| """ | |
| global g_tmp_dir | |
| global g_invoking_script_path | |
| g_invoking_script_path = invoking_script_path | |
| # The scripts assume to be run from within their containing directory (paths | |
| # to things like "keys/" are written relative). | |
| expected_cwd = os.path.realpath(os.path.dirname(invoking_script_path)) | |
| actual_cwd = os.path.realpath(os.getcwd()) | |
| if actual_cwd != expected_cwd: | |
| sys.stderr.write( | |
| ('Your current working directory must be that containing the python ' | |
| 'scripts:\n%s\nas the script may reference paths relative to this\n') | |
| % (expected_cwd)) | |
| sys.exit(1) | |
| # Use an output directory that is a sibling of the invoking script. | |
| g_tmp_dir = 'out' | |
| # Ensure the output directory exists and is empty. | |
| sys.stdout.write('Creating output directory: %s\n' % (g_tmp_dir)) | |
| shutil.rmtree(g_tmp_dir, True) | |
| os.makedirs(g_tmp_dir) | |
| def create_self_signed_root_certificate(name): | |
| return Certificate(name, TYPE_CA, None) | |
| def create_intermediate_certificate(name, issuer): | |
| return Certificate(name, TYPE_CA, issuer) | |
| def create_end_entity_certificate(name, issuer): | |
| return Certificate(name, TYPE_END_ENTITY, issuer) | |
| init(sys.argv[0]) |