Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(254)

Unified Diff: net/data/verify_certificate_chain_unittest/common.py

Issue 1414393008: Add scripts to generate simple test data for certificate verification. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@key_usages
Patch Set: rebase Created 5 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: net/data/verify_certificate_chain_unittest/common.py
diff --git a/net/data/verify_certificate_chain_unittest/common.py b/net/data/verify_certificate_chain_unittest/common.py
new file mode 100755
index 0000000000000000000000000000000000000000..c7e98d1981b3f025a95494cd1e264af0634156ee
--- /dev/null
+++ b/net/data/verify_certificate_chain_unittest/common.py
@@ -0,0 +1,417 @@
+#!/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 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
+
+# January 1st, 2015 midnight UTC
+JANUARY_1_2015_UTC = '150101120000Z'
+
+# January 1st, 2016 midnight UTC
+JANUARY_1_2016_UTC = '160101120000Z'
+
+# March 2nd, 2015 midnight UTC
+DEFAULT_TIME = '150302120000Z'
+
+# Counter used to generate unique (but readable) path names.
+g_cur_path_id = 0
+
+# Output paths used:
+# - g_out_dir: where any temporary files (keys, cert req, signing db etc) are
+# saved to.
+# - g_out_pem: the path to the final output (which is a .pem file)
+#
+# See init() for how these are assigned, based on the name of the calling
+# script.
+g_out_dir = None
+g_out_pem = None
+
+
+def GetUniquePathId(name):
+ """Returns a base filename that contains 'name', but is unique to the output
+ directory"""
+ global g_cur_path_id
+
+ path_id = g_cur_path_id
+ g_cur_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)
+
+
+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 = GetUniquePathId(name)
+
+ # 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 predictabl results each time
+ # the certificates are re-generated.
+ self.set_validity_range(JANUARY_1_2015_UTC, JANUARY_1_2016_UTC)
+
+ # 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. Starts off serial numbers at 1, and will
+ # increment them for each signed certificate.
+ write_string_to_file('01\n', self.get_serial_path())
+ write_string_to_file('', self.get_database_path())
+
+
+ def generate_rsa_key(self, size_bits):
+ """Generates an RSA private key for the certificate."""
+ subprocess.check_call(
+ ['openssl', 'genrsa', '-out', self.get_key_path(), str(size_bits)])
+
+
+ def generate_ec_key(self, named_curve):
+ """Generates an EC private key for the certificate. |named_curve| can be
+ something like secp384r1"""
+ subprocess.check_call(
+ ['openssl', 'ecparam', '-out', self.get_key_path(),
+ '-name', named_curve, '-genkey'])
+
+
+ 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_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_out_dir, '%s%s' % (self.path_id, suffix))
+
+
+ def get_key_path(self):
+ return self.get_path('.key')
+
+
+ def get_cert_path(self):
+ return self.get_path('.pem')
+
+
+ def get_serial_path(self):
+ return self.get_path('.serial')
+
+
+ def get_csr_path(self):
+ return self.get_path('.csr')
+
+
+ def get_database_path(self):
+ return self.get_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.
+ with open(self.get_cert_path(), 'r') as f:
+ return f.read()
+
+
+ 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. Callers have the option to generate a
+ # different type of key, but if that was not done default to a new 2048-bit
+ # RSA key.
+ if not os.path.isfile(self.get_key_path()):
+ self.generate_rsa_key(2048)
+
+ # 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.get_key_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('private_key', self.get_key_path())
+ section.set_property('new_certs_dir', g_out_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 data_to_pem(block_header, block_data):
+ return '-----BEGIN %s-----\n%s\n-----END %s-----\n' % (block_header,
+ base64.b64encode(block_data), block_header)
+
+
+def write_test_file(description, chain, trusted_certs, utc_time, verify_result):
+ """Writes a test file that contains all the inputs necessary to run a
+ verification on a certificate chain"""
+
+ # 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 the trust store.
+ for cert in trusted_certs:
+ cert_data = cert.get_cert_pem()
+ # Use a different block type in the .pem file.
+ cert_data = cert_data.replace('CERTIFICATE', 'TRUSTED_CERTIFICATE')
+ test_data += '\n' + cert_data
+
+ test_data += '\n' + data_to_pem('TIME', utc_time)
+
+ verify_result_string = 'SUCCESS' if verify_result else 'FAIL'
+ test_data += '\n' + data_to_pem('VERIFY_RESULT', verify_result_string)
+
+ write_string_to_file(test_data, g_out_pem)
+
+
+def write_string_to_file(data, path):
+ with open(path, 'w') as f:
+ f.write(data)
+
+
+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_out_dir
+ global g_out_pem
+
+ # Base the output name off of the invoking script's name.
+ out_name = os.path.splitext(os.path.basename(invoking_script_path))[0]
+
+ # Strip the leading 'generate-'
+ if out_name.startswith('generate-'):
+ out_name = out_name[9:]
+
+ # Use an output directory with the same name as the invoking script.
+ g_out_dir = os.path.join('out', out_name)
+
+ # Ensure the output directory exists and is empty.
+ sys.stdout.write('Creating output directory: %s\n' % (g_out_dir))
+ shutil.rmtree(g_out_dir, True)
+ os.makedirs(g_out_dir)
+
+ g_out_pem = os.path.join('%s.pem' % (out_name))
+
+
+def create_self_signed_root_certificate(name):
+ return Certificate(name, TYPE_CA, None)
+
+
+def create_intermediary_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])

Powered by Google App Engine
This is Rietveld 408576698