| OLD | NEW |
| (Empty) | |
| 1 #!/usr/bin/python |
| 2 # Copyright (c) 2015 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """Set of helpers to generate signed X.509v3 certificates. |
| 7 |
| 8 This works by shelling out calls to the 'openssl req' and 'openssl ca' |
| 9 commands, and passing the appropriate command line flags and configuration file |
| 10 (.cnf). |
| 11 """ |
| 12 |
| 13 import base64 |
| 14 import os |
| 15 import shutil |
| 16 import subprocess |
| 17 import sys |
| 18 |
| 19 import openssl_conf |
| 20 |
| 21 # Enum for the "type" of certificate that is to be created. This is used to |
| 22 # select sane defaults for the .cnf file and command line flags, but they can |
| 23 # all be overridden. |
| 24 TYPE_CA = 2 |
| 25 TYPE_END_ENTITY = 3 |
| 26 |
| 27 # January 1st, 2015 midnight UTC |
| 28 JANUARY_1_2015_UTC = '150101120000Z' |
| 29 |
| 30 # January 1st, 2016 midnight UTC |
| 31 JANUARY_1_2016_UTC = '160101120000Z' |
| 32 |
| 33 # March 2nd, 2015 midnight UTC |
| 34 DEFAULT_TIME = '150302120000Z' |
| 35 |
| 36 # Counter used to generate unique (but readable) path names. |
| 37 g_cur_path_id = 0 |
| 38 |
| 39 # Output paths used: |
| 40 # - g_out_dir: where any temporary files (keys, cert req, signing db etc) are |
| 41 # saved to. |
| 42 # - g_out_pem: the path to the final output (which is a .pem file) |
| 43 # |
| 44 # See init() for how these are assigned, based on the name of the calling |
| 45 # script. |
| 46 g_out_dir = None |
| 47 g_out_pem = None |
| 48 |
| 49 |
| 50 def GetUniquePathId(name): |
| 51 """Returns a base filename that contains 'name', but is unique to the output |
| 52 directory""" |
| 53 global g_cur_path_id |
| 54 |
| 55 path_id = g_cur_path_id |
| 56 g_cur_path_id += 1 |
| 57 |
| 58 # Use a short and clean name for the first use of this name. |
| 59 if path_id == 0: |
| 60 return name |
| 61 |
| 62 # Otherwise append the count to make it unique. |
| 63 return '%s_%d' % (name, path_id) |
| 64 |
| 65 |
| 66 class Certificate(object): |
| 67 """Helper for building an X.509 certificate.""" |
| 68 |
| 69 def __init__(self, name, cert_type, issuer): |
| 70 # The name will be used for the subject's CN, and also as a component of |
| 71 # the temporary filenames to help with debugging. |
| 72 self.name = name |
| 73 self.path_id = GetUniquePathId(name) |
| 74 |
| 75 # The issuer is also a Certificate object. Passing |None| means it is a |
| 76 # self-signed certificate. |
| 77 self.issuer = issuer |
| 78 if issuer is None: |
| 79 self.issuer = self |
| 80 |
| 81 # The config contains all the OpenSSL options that will be passed via a |
| 82 # .cnf file. Set up defaults. |
| 83 self.config = openssl_conf.Config() |
| 84 self.init_config() |
| 85 |
| 86 # Some settings need to be passed as flags rather than in the .cnf file. |
| 87 # Technically these can be set though a .cnf, however doing so makes it |
| 88 # sticky to the issuing certificate, rather than selecting it per |
| 89 # subordinate certificate. |
| 90 self.validity_flags = [] |
| 91 self.md_flags = [] |
| 92 |
| 93 # By default OpenSSL will use the current time for the start time. Instead |
| 94 # default to using a fixed timestamp for more predictabl results each time |
| 95 # the certificates are re-generated. |
| 96 self.set_validity_range(JANUARY_1_2015_UTC, JANUARY_1_2016_UTC) |
| 97 |
| 98 # Use SHA-256 when THIS certificate is signed (setting it in the |
| 99 # configuration would instead set the hash to use when signing other |
| 100 # certificates with this one). |
| 101 self.set_signature_hash('sha256') |
| 102 |
| 103 # Set appropriate key usages and basic constraints. For flexibility in |
| 104 # testing (since want to generate some flawed certificates) these are set |
| 105 # on a per-certificate basis rather than automatically when signing. |
| 106 if cert_type == TYPE_END_ENTITY: |
| 107 self.get_extensions().set_property('keyUsage', |
| 108 'critical,digitalSignature,keyEncipherment') |
| 109 self.get_extensions().set_property('extendedKeyUsage', |
| 110 'serverAuth,clientAuth') |
| 111 else: |
| 112 self.get_extensions().set_property('keyUsage', |
| 113 'critical,keyCertSign,cRLSign') |
| 114 self.get_extensions().set_property('basicConstraints', 'critical,CA:true') |
| 115 |
| 116 # Tracks whether the PEM file for this certificate has been written (since |
| 117 # generation is done lazily). |
| 118 self.finalized = False |
| 119 |
| 120 # Initialize any files that will be needed if this certificate is used to |
| 121 # sign other certificates. Starts off serial numbers at 1, and will |
| 122 # increment them for each signed certificate. |
| 123 write_string_to_file('01\n', self.get_serial_path()) |
| 124 write_string_to_file('', self.get_database_path()) |
| 125 |
| 126 |
| 127 def generate_rsa_key(self, size_bits): |
| 128 """Generates an RSA private key for the certificate.""" |
| 129 subprocess.check_call( |
| 130 ['openssl', 'genrsa', '-out', self.get_key_path(), str(size_bits)]) |
| 131 |
| 132 |
| 133 def generate_ec_key(self, named_curve): |
| 134 """Generates an EC private key for the certificate. |named_curve| can be |
| 135 something like secp384r1""" |
| 136 subprocess.check_call( |
| 137 ['openssl', 'ecparam', '-out', self.get_key_path(), |
| 138 '-name', named_curve, '-genkey']) |
| 139 |
| 140 |
| 141 def set_validity_range(self, start_date, end_date): |
| 142 """Sets the Validity notBefore and notAfter properties for the |
| 143 certificate""" |
| 144 self.validity_flags = ['-startdate', start_date, '-enddate', end_date] |
| 145 |
| 146 |
| 147 def set_signature_hash(self, md): |
| 148 """Sets the hash function that will be used when signing this certificate. |
| 149 Can be sha1, sha256, sha512, md5, etc.""" |
| 150 self.md_flags = ['-md', md] |
| 151 |
| 152 |
| 153 def get_extensions(self): |
| 154 return self.config.get_section('req_ext') |
| 155 |
| 156 |
| 157 def get_path(self, suffix): |
| 158 """Forms a path to an output file for this certificate, containing the |
| 159 indicated suffix. The certificate's name will be used as its basis.""" |
| 160 return os.path.join(g_out_dir, '%s%s' % (self.path_id, suffix)) |
| 161 |
| 162 |
| 163 def get_key_path(self): |
| 164 return self.get_path('.key') |
| 165 |
| 166 |
| 167 def get_cert_path(self): |
| 168 return self.get_path('.pem') |
| 169 |
| 170 |
| 171 def get_serial_path(self): |
| 172 return self.get_path('.serial') |
| 173 |
| 174 |
| 175 def get_csr_path(self): |
| 176 return self.get_path('.csr') |
| 177 |
| 178 |
| 179 def get_database_path(self): |
| 180 return self.get_path('.db') |
| 181 |
| 182 |
| 183 def get_config_path(self): |
| 184 return self.get_path('.cnf') |
| 185 |
| 186 |
| 187 def get_cert_pem(self): |
| 188 # Finish generating a .pem file for the certificate. |
| 189 self.finalize() |
| 190 |
| 191 # Read the certificate data. |
| 192 with open(self.get_cert_path(), 'r') as f: |
| 193 return f.read() |
| 194 |
| 195 |
| 196 def finalize(self): |
| 197 """Finishes the certificate creation process. This generates any needed |
| 198 key, creates and signs the CSR. On completion the resulting PEM file can be |
| 199 found at self.get_cert_path()""" |
| 200 |
| 201 if self.finalized: |
| 202 return # Already finalized, no work needed. |
| 203 |
| 204 self.finalized = True |
| 205 |
| 206 # Ensure that the issuer has been "finalized", since its outputs need to be |
| 207 # accessible. Note that self.issuer could be the same as self. |
| 208 self.issuer.finalize() |
| 209 |
| 210 # Ensure the certificate has a key. Callers have the option to generate a |
| 211 # different type of key, but if that was not done default to a new 2048-bit |
| 212 # RSA key. |
| 213 if not os.path.isfile(self.get_key_path()): |
| 214 self.generate_rsa_key(2048) |
| 215 |
| 216 # Serialize the config to a file. |
| 217 self.config.write_to_file(self.get_config_path()) |
| 218 |
| 219 # Create a CSR. |
| 220 subprocess.check_call( |
| 221 ['openssl', 'req', '-new', |
| 222 '-key', self.get_key_path(), |
| 223 '-out', self.get_csr_path(), |
| 224 '-config', self.get_config_path()]) |
| 225 |
| 226 cmd = ['openssl', 'ca', '-batch', '-in', |
| 227 self.get_csr_path(), '-out', self.get_cert_path(), '-config', |
| 228 self.issuer.get_config_path()] |
| 229 |
| 230 if self.issuer == self: |
| 231 cmd.append('-selfsign') |
| 232 |
| 233 # Add in any extra flags. |
| 234 cmd.extend(self.validity_flags) |
| 235 cmd.extend(self.md_flags) |
| 236 |
| 237 # Run the 'openssl ca' command. |
| 238 subprocess.check_call(cmd) |
| 239 |
| 240 |
| 241 def init_config(self): |
| 242 """Initializes default properties in the certificate .cnf file that are |
| 243 generic enough to work for all certificates (but can be overridden later). |
| 244 """ |
| 245 |
| 246 # -------------------------------------- |
| 247 # 'req' section |
| 248 # -------------------------------------- |
| 249 |
| 250 section = self.config.get_section('req') |
| 251 |
| 252 section.set_property('encrypt_key', 'no') |
| 253 section.set_property('utf8', 'yes') |
| 254 section.set_property('string_mask', 'utf8only') |
| 255 section.set_property('prompt', 'no') |
| 256 section.set_property('distinguished_name', 'req_dn') |
| 257 section.set_property('req_extensions', 'req_ext') |
| 258 |
| 259 # -------------------------------------- |
| 260 # 'req_dn' section |
| 261 # -------------------------------------- |
| 262 |
| 263 # This section describes the certificate subject's distinguished name. |
| 264 |
| 265 section = self.config.get_section('req_dn') |
| 266 section.set_property('commonName', '"%s"' % (self.name)) |
| 267 |
| 268 # -------------------------------------- |
| 269 # 'req_ext' section |
| 270 # -------------------------------------- |
| 271 |
| 272 # This section describes the certificate's extensions. |
| 273 |
| 274 section = self.config.get_section('req_ext') |
| 275 section.set_property('subjectKeyIdentifier', 'hash') |
| 276 |
| 277 # -------------------------------------- |
| 278 # SECTIONS FOR CAs |
| 279 # -------------------------------------- |
| 280 |
| 281 # The following sections are used by the 'openssl ca' and relate to the |
| 282 # signing operation. They are not needed for end-entity certificate |
| 283 # configurations, but only if this certifiate will be used to sign other |
| 284 # certificates. |
| 285 |
| 286 # -------------------------------------- |
| 287 # 'ca' section |
| 288 # -------------------------------------- |
| 289 |
| 290 section = self.config.get_section('ca') |
| 291 section.set_property('default_ca', 'root_ca') |
| 292 |
| 293 section = self.config.get_section('root_ca') |
| 294 section.set_property('certificate', self.get_cert_path()) |
| 295 section.set_property('private_key', self.get_key_path()) |
| 296 section.set_property('new_certs_dir', g_out_dir) |
| 297 section.set_property('serial', self.get_serial_path()) |
| 298 section.set_property('database', self.get_database_path()) |
| 299 section.set_property('unique_subject', 'no') |
| 300 |
| 301 # These will get overridden via command line flags. |
| 302 section.set_property('default_days', '365') |
| 303 section.set_property('default_md', 'sha256') |
| 304 |
| 305 section.set_property('policy', 'policy_anything') |
| 306 section.set_property('email_in_dn', 'no') |
| 307 section.set_property('preserve', 'yes') |
| 308 section.set_property('name_opt', 'multiline,-esc_msb,utf8') |
| 309 section.set_property('cert_opt', 'ca_default') |
| 310 section.set_property('copy_extensions', 'copy') |
| 311 section.set_property('x509_extensions', 'signing_ca_ext') |
| 312 section.set_property('default_crl_days', '30') |
| 313 section.set_property('crl_extensions', 'crl_ext') |
| 314 |
| 315 section = self.config.get_section('policy_anything') |
| 316 section.set_property('domainComponent', 'optional') |
| 317 section.set_property('countryName', 'optional') |
| 318 section.set_property('stateOrProvinceName', 'optional') |
| 319 section.set_property('localityName', 'optional') |
| 320 section.set_property('organizationName', 'optional') |
| 321 section.set_property('organizationalUnitName', 'optional') |
| 322 section.set_property('commonName', 'optional') |
| 323 section.set_property('emailAddress', 'optional') |
| 324 |
| 325 section = self.config.get_section('signing_ca_ext') |
| 326 section.set_property('subjectKeyIdentifier', 'hash') |
| 327 section.set_property('authorityKeyIdentifier', 'keyid:always') |
| 328 section.set_property('authorityInfoAccess', '@issuer_info') |
| 329 section.set_property('crlDistributionPoints', '@crl_info') |
| 330 |
| 331 section = self.config.get_section('issuer_info') |
| 332 section.set_property('caIssuers;URI.0', |
| 333 'http://url-for-aia/%s.cer' % (self.name)) |
| 334 |
| 335 section = self.config.get_section('crl_info') |
| 336 section.set_property('URI.0', 'http://url-for-crl/%s.crl' % (self.name)) |
| 337 |
| 338 section = self.config.get_section('crl_ext') |
| 339 section.set_property('authorityKeyIdentifier', 'keyid:always') |
| 340 section.set_property('authorityInfoAccess', '@issuer_info') |
| 341 |
| 342 |
| 343 def data_to_pem(block_header, block_data): |
| 344 return '-----BEGIN %s-----\n%s\n-----END %s-----\n' % (block_header, |
| 345 base64.b64encode(block_data), block_header) |
| 346 |
| 347 |
| 348 def write_test_file(description, chain, trusted_certs, utc_time, verify_result): |
| 349 """Writes a test file that contains all the inputs necessary to run a |
| 350 verification on a certificate chain""" |
| 351 |
| 352 # Prepend the script name that generated the file to the description. |
| 353 test_data = '[Created by: %s]\n\n%s\n' % (sys.argv[0], description) |
| 354 |
| 355 # Write the certificate chain to the output file. |
| 356 for cert in chain: |
| 357 test_data += '\n' + cert.get_cert_pem() |
| 358 |
| 359 # Write the trust store. |
| 360 for cert in trusted_certs: |
| 361 cert_data = cert.get_cert_pem() |
| 362 # Use a different block type in the .pem file. |
| 363 cert_data = cert_data.replace('CERTIFICATE', 'TRUSTED_CERTIFICATE') |
| 364 test_data += '\n' + cert_data |
| 365 |
| 366 test_data += '\n' + data_to_pem('TIME', utc_time) |
| 367 |
| 368 verify_result_string = 'SUCCESS' if verify_result else 'FAIL' |
| 369 test_data += '\n' + data_to_pem('VERIFY_RESULT', verify_result_string) |
| 370 |
| 371 write_string_to_file(test_data, g_out_pem) |
| 372 |
| 373 |
| 374 def write_string_to_file(data, path): |
| 375 with open(path, 'w') as f: |
| 376 f.write(data) |
| 377 |
| 378 |
| 379 def init(invoking_script_path): |
| 380 """Creates an output directory to contain all the temporary files that may be |
| 381 created, as well as determining the path for the final output. These paths |
| 382 are all based off of the name of the calling script. |
| 383 """ |
| 384 |
| 385 global g_out_dir |
| 386 global g_out_pem |
| 387 |
| 388 # Base the output name off of the invoking script's name. |
| 389 out_name = os.path.splitext(os.path.basename(invoking_script_path))[0] |
| 390 |
| 391 # Strip the leading 'generate-' |
| 392 if out_name.startswith('generate-'): |
| 393 out_name = out_name[9:] |
| 394 |
| 395 # Use an output directory with the same name as the invoking script. |
| 396 g_out_dir = os.path.join('out', out_name) |
| 397 |
| 398 # Ensure the output directory exists and is empty. |
| 399 sys.stdout.write('Creating output directory: %s\n' % (g_out_dir)) |
| 400 shutil.rmtree(g_out_dir, True) |
| 401 os.makedirs(g_out_dir) |
| 402 |
| 403 g_out_pem = os.path.join('%s.pem' % (out_name)) |
| 404 |
| 405 |
| 406 def create_self_signed_root_certificate(name): |
| 407 return Certificate(name, TYPE_CA, None) |
| 408 |
| 409 |
| 410 def create_intermediary_certificate(name, issuer): |
| 411 return Certificate(name, TYPE_CA, issuer) |
| 412 |
| 413 |
| 414 def create_end_entity_certificate(name, issuer): |
| 415 return Certificate(name, TYPE_END_ENTITY, issuer) |
| 416 |
| 417 init(sys.argv[0]) |
| OLD | NEW |