1#!/usr/bin/env python 2# Copyright 2015 The Chromium Authors 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 8This works by shelling out calls to the 'openssl req' and 'openssl ca' 9commands, and passing the appropriate command line flags and configuration file 10(.cnf). 11""" 12 13import base64 14import hashlib 15import os 16import shutil 17import subprocess 18import sys 19 20from . import openssl_conf 21 22# Enum for the "type" of certificate that is to be created. This is used to 23# select sane defaults for the .cnf file and command line flags, but they can 24# all be overridden. 25TYPE_CA = 2 26TYPE_END_ENTITY = 3 27 28# March 1st, 2015 12:00 UTC 29MARCH_1_2015_UTC = '150301120000Z' 30 31# March 2nd, 2015 12:00 UTC 32MARCH_2_2015_UTC = '150302120000Z' 33 34# January 1st, 2015 12:00 UTC 35JANUARY_1_2015_UTC = '150101120000Z' 36 37# September 1st, 2015 12:00 UTC 38SEPTEMBER_1_2015_UTC = '150901120000Z' 39 40# January 1st, 2016 12:00 UTC 41JANUARY_1_2016_UTC = '160101120000Z' 42 43# October 5th, 2021 12:00 UTC 44OCTOBER_5_2021_UTC = '211005120000Z' 45 46# October 5th, 2022 12:00 UTC 47OCTOBER_5_2022_UTC = '221005120000Z' 48 49KEY_PURPOSE_ANY = 'anyExtendedKeyUsage' 50KEY_PURPOSE_SERVER_AUTH = 'serverAuth' 51KEY_PURPOSE_CLIENT_AUTH = 'clientAuth' 52 53DEFAULT_KEY_PURPOSE = KEY_PURPOSE_SERVER_AUTH 54 55# Counters used to generate unique (but readable) path names. 56g_cur_path_id = {} 57 58# Output paths used: 59# - g_tmp_dir: where any temporary files (cert req, signing db etc) are 60# saved to. 61 62# See init() for how these are assigned. 63g_tmp_dir = None 64g_invoking_script_path = None 65 66# The default validity range of generated certificates. Can be modified with 67# set_default_validity_range(). This range is intentionally already expired to 68# avoid tests being added which depend on the certs being valid at the current 69# time rather than specifying the time as an input of the test. 70g_default_start_date = OCTOBER_5_2021_UTC 71g_default_end_date = OCTOBER_5_2022_UTC 72 73 74def set_default_validity_range(start_date, end_date): 75 """Sets the validity range that will be used for certificates created with 76 Certificate""" 77 global g_default_start_date 78 global g_default_end_date 79 g_default_start_date = start_date 80 g_default_end_date = end_date 81 82 83def get_unique_path_id(name): 84 """Returns a base filename that contains 'name', but is unique to the output 85 directory""" 86 # Use case-insensitive matching for counting duplicates, since some 87 # filesystems are case insensitive, but case preserving. 88 lowercase_name = name.lower() 89 path_id = g_cur_path_id.get(lowercase_name, 0) 90 g_cur_path_id[lowercase_name] = path_id + 1 91 92 # Use a short and clean name for the first use of this name. 93 if path_id == 0: 94 return name 95 96 # Otherwise append the count to make it unique. 97 return '%s_%d' % (name, path_id) 98 99 100def get_path_in_tmp_dir(name, suffix): 101 return os.path.join(g_tmp_dir, '%s%s' % (name, suffix)) 102 103 104class Key(object): 105 """Describes a public + private key pair. It is a dumb wrapper around an 106 on-disk key.""" 107 108 def __init__(self, path): 109 self.path = path 110 111 112 def get_path(self): 113 """Returns the path to a file that contains the key contents.""" 114 return self.path 115 116 117def get_or_generate_key(generation_arguments, path): 118 """Helper function to either retrieve a key from an existing file |path|, or 119 generate a new one using the command line |generation_arguments|.""" 120 121 generation_arguments_str = ' '.join(generation_arguments) 122 123 # If the file doesn't already exist, generate a new key using the generation 124 # parameters. 125 if not os.path.isfile(path): 126 key_contents = subprocess.check_output(generation_arguments, text=True) 127 128 # Prepend the generation parameters to the key file. 129 write_string_to_file(generation_arguments_str + '\n' + key_contents, 130 path) 131 else: 132 # If the path already exists, confirm that it is for the expected key type. 133 first_line = read_file_to_string(path).splitlines()[0] 134 if first_line != generation_arguments_str: 135 sys.stderr.write(('\nERROR: The existing key file:\n %s\nis not ' 136 'compatible with the requested parameters:\n "%s" vs "%s".\n' 137 'Delete the file if you want to re-generate it with the new ' 138 'parameters, otherwise pick a new filename\n') % ( 139 path, first_line, generation_arguments_str)) 140 sys.exit(1) 141 142 return Key(path) 143 144 145def get_or_generate_rsa_key(size_bits, path): 146 """Retrieves an existing key from a file if the path exists. Otherwise 147 generates an RSA key with the specified bit size and saves it to the path.""" 148 return get_or_generate_key(['openssl', 'genrsa', str(size_bits)], path) 149 150 151def get_or_generate_ec_key(named_curve, path): 152 """Retrieves an existing key from a file if the path exists. Otherwise 153 generates an EC key with the specified named curve and saves it to the 154 path.""" 155 return get_or_generate_key(['openssl', 'ecparam', '-name', named_curve, 156 '-genkey'], path) 157 158 159def create_key_path(base_name): 160 """Generates a name that contains |base_name| in it, and is relative to the 161 "keys/" directory. If create_key_path(xxx) is called more than once during 162 the script run, a suffix will be added.""" 163 164 # Save keys to CWD/keys/*.key 165 keys_dir = 'keys' 166 167 # Create the keys directory if it doesn't exist 168 if not os.path.exists(keys_dir): 169 os.makedirs(keys_dir) 170 171 return get_unique_path_id(os.path.join(keys_dir, base_name)) + '.key' 172 173 174class Certificate(object): 175 """Helper for building an X.509 certificate.""" 176 177 def __init__(self, name, cert_type, issuer): 178 # The name will be used for the subject's CN, and also as a component of 179 # the temporary filenames to help with debugging. 180 self.name = name 181 self.path_id = get_unique_path_id(name) 182 183 # Allow the caller to override the key later. If no key was set will 184 # auto-generate one. 185 self.key = None 186 187 # The issuer is also a Certificate object. Passing |None| means it is a 188 # self-signed certificate. 189 self.issuer = issuer 190 if issuer is None: 191 self.issuer = self 192 193 # The config contains all the OpenSSL options that will be passed via a 194 # .cnf file. Set up defaults. 195 self.config = openssl_conf.Config() 196 self.init_config() 197 198 # Some settings need to be passed as flags rather than in the .cnf file. 199 # Technically these can be set though a .cnf, however doing so makes it 200 # sticky to the issuing certificate, rather than selecting it per 201 # subordinate certificate. 202 self.validity_flags = [] 203 self.md_flags = [] 204 205 # By default OpenSSL will use the current time for the start time. Instead 206 # default to using a fixed timestamp for more predictable results each time 207 # the certificates are re-generated. 208 self.set_validity_range(g_default_start_date, g_default_end_date) 209 210 # Use SHA-256 when THIS certificate is signed (setting it in the 211 # configuration would instead set the hash to use when signing other 212 # certificates with this one). 213 self.set_signature_hash('sha256') 214 215 # Set appropriate key usages and basic constraints. For flexibility in 216 # testing (since want to generate some flawed certificates) these are set 217 # on a per-certificate basis rather than automatically when signing. 218 if cert_type == TYPE_END_ENTITY: 219 self.get_extensions().set_property('keyUsage', 220 'critical,digitalSignature,keyEncipherment') 221 self.get_extensions().set_property('extendedKeyUsage', 222 'serverAuth,clientAuth') 223 else: 224 self.get_extensions().set_property('keyUsage', 225 'critical,keyCertSign,cRLSign') 226 self.get_extensions().set_property('basicConstraints', 'critical,CA:true') 227 228 # Tracks whether the PEM file for this certificate has been written (since 229 # generation is done lazily). 230 self.finalized = False 231 232 # Initialize any files that will be needed if this certificate is used to 233 # sign other certificates. Picks a pseudo-random starting serial number 234 # based on the file system path, and will increment this for each signed 235 # certificate. 236 if not os.path.exists(self.get_serial_path()): 237 write_string_to_file('%s\n' % self.make_serial_number(), 238 self.get_serial_path()) 239 if not os.path.exists(self.get_database_path()): 240 write_string_to_file('', self.get_database_path()) 241 242 243 def set_validity_range(self, start_date, end_date): 244 """Sets the Validity notBefore and notAfter properties for the 245 certificate""" 246 self.validity_flags = ['-startdate', start_date, '-enddate', end_date] 247 248 249 def set_signature_hash(self, md): 250 """Sets the hash function that will be used when signing this certificate. 251 Can be sha1, sha256, sha512, md5, etc.""" 252 self.md_flags = ['-md', md] 253 254 255 def get_extensions(self): 256 return self.config.get_section('req_ext') 257 258 259 def get_subject(self): 260 """Returns the configuration section responsible for the subject of the 261 certificate. This can be used to alter the subject to be more complex.""" 262 return self.config.get_section('req_dn') 263 264 265 def get_path(self, suffix): 266 """Forms a path to an output file for this certificate, containing the 267 indicated suffix. The certificate's name will be used as its basis.""" 268 return os.path.join(g_tmp_dir, '%s%s' % (self.path_id, suffix)) 269 270 271 def get_name_path(self, suffix): 272 """Forms a path to an output file for this CA, containing the indicated 273 suffix. If multiple certificates have the same name, they will use the same 274 path.""" 275 return get_path_in_tmp_dir(self.name, suffix) 276 277 278 def set_key(self, key): 279 assert self.finalized is False 280 self.set_key_internal(key) 281 282 283 def set_key_internal(self, key): 284 self.key = key 285 286 # Associate the private key with the certificate. 287 section = self.config.get_section('root_ca') 288 section.set_property('private_key', self.key.get_path()) 289 290 291 def get_key(self): 292 if self.key is None: 293 self.set_key_internal( 294 get_or_generate_rsa_key(2048, create_key_path(self.name))) 295 return self.key 296 297 298 def get_cert_path(self): 299 return self.get_path('.pem') 300 301 302 def get_serial_path(self): 303 return self.get_name_path('.serial') 304 305 306 def make_serial_number(self): 307 """Returns a hex number that is generated based on the certificate file 308 path. This serial number will likely be globally unique, which makes it 309 easier to use the certificates with NSS (which assumes certificate 310 equivalence based on issuer and serial number).""" 311 312 # Hash some predictable values together to get the serial number. The 313 # predictability is so that re-generating certificate chains is 314 # a no-op, however each certificate ends up with a unique serial number. 315 m = hashlib.sha1() 316 317 # Mix in up to the last 3 components of the path for the generating script. 318 # For example, 319 # "verify_certificate_chain_unittest/my_test/generate_chains.py" 320 script_path = os.path.realpath(g_invoking_script_path) 321 script_path = "/".join(script_path.split(os.sep)[-3:]) 322 m.update(script_path.encode('utf-8')) 323 324 # Mix in the path_id, which corresponds to a unique path for the 325 # certificate under out/ (and accounts for non-unique certificate names). 326 m.update(self.path_id.encode('utf-8')) 327 328 serial_bytes = bytearray(m.digest()) 329 330 # SHA1 digest is 20 bytes long, which is appropriate for a serial number. 331 # However, need to also make sure the most significant bit is 0 so it is 332 # not a "negative" number. 333 serial_bytes[0] = serial_bytes[0] & 0x7F 334 335 return serial_bytes.hex() 336 337 338 def get_csr_path(self): 339 return self.get_path('.csr') 340 341 342 def get_database_path(self): 343 return self.get_name_path('.db') 344 345 346 def get_config_path(self): 347 return self.get_path('.cnf') 348 349 350 def get_cert_pem(self): 351 # Finish generating a .pem file for the certificate. 352 self.finalize() 353 354 # Read the certificate data. 355 return read_file_to_string(self.get_cert_path()) 356 357 358 def finalize(self): 359 """Finishes the certificate creation process. This generates any needed 360 key, creates and signs the CSR. On completion the resulting PEM file can be 361 found at self.get_cert_path()""" 362 363 if self.finalized: 364 return # Already finalized, no work needed. 365 366 self.finalized = True 367 368 # Ensure that the issuer has been "finalized", since its outputs need to be 369 # accessible. Note that self.issuer could be the same as self. 370 self.issuer.finalize() 371 372 # Ensure the certificate has a key (gets lazily created by this call if 373 # missing). 374 self.get_key() 375 376 # Serialize the config to a file. 377 self.config.write_to_file(self.get_config_path()) 378 379 # Create a CSR. 380 subprocess.check_call( 381 ['openssl', 'req', '-new', 382 '-key', self.key.get_path(), 383 '-out', self.get_csr_path(), 384 '-config', self.get_config_path()]) 385 386 cmd = ['openssl', 'ca', '-batch', '-in', 387 self.get_csr_path(), '-out', self.get_cert_path(), '-config', 388 self.issuer.get_config_path()] 389 390 if self.issuer == self: 391 cmd.append('-selfsign') 392 393 # Add in any extra flags. 394 cmd.extend(self.validity_flags) 395 cmd.extend(self.md_flags) 396 397 # Run the 'openssl ca' command. 398 subprocess.check_call(cmd) 399 400 401 def init_config(self): 402 """Initializes default properties in the certificate .cnf file that are 403 generic enough to work for all certificates (but can be overridden later). 404 """ 405 406 # -------------------------------------- 407 # 'req' section 408 # -------------------------------------- 409 410 section = self.config.get_section('req') 411 412 section.set_property('encrypt_key', 'no') 413 section.set_property('utf8', 'yes') 414 section.set_property('string_mask', 'utf8only') 415 section.set_property('prompt', 'no') 416 section.set_property('distinguished_name', 'req_dn') 417 section.set_property('req_extensions', 'req_ext') 418 419 # -------------------------------------- 420 # 'req_dn' section 421 # -------------------------------------- 422 423 # This section describes the certificate subject's distinguished name. 424 425 section = self.config.get_section('req_dn') 426 section.set_property('commonName', '"%s"' % (self.name)) 427 428 # -------------------------------------- 429 # 'req_ext' section 430 # -------------------------------------- 431 432 # This section describes the certificate's extensions. 433 434 section = self.config.get_section('req_ext') 435 section.set_property('subjectKeyIdentifier', 'hash') 436 437 # -------------------------------------- 438 # SECTIONS FOR CAs 439 # -------------------------------------- 440 441 # The following sections are used by the 'openssl ca' and relate to the 442 # signing operation. They are not needed for end-entity certificate 443 # configurations, but only if this certifiate will be used to sign other 444 # certificates. 445 446 # -------------------------------------- 447 # 'ca' section 448 # -------------------------------------- 449 450 section = self.config.get_section('ca') 451 section.set_property('default_ca', 'root_ca') 452 453 section = self.config.get_section('root_ca') 454 section.set_property('certificate', self.get_cert_path()) 455 section.set_property('new_certs_dir', g_tmp_dir) 456 section.set_property('serial', self.get_serial_path()) 457 section.set_property('database', self.get_database_path()) 458 section.set_property('unique_subject', 'no') 459 460 # These will get overridden via command line flags. 461 section.set_property('default_days', '365') 462 section.set_property('default_md', 'sha256') 463 464 section.set_property('policy', 'policy_anything') 465 section.set_property('email_in_dn', 'no') 466 section.set_property('preserve', 'yes') 467 section.set_property('name_opt', 'multiline,-esc_msb,utf8') 468 section.set_property('cert_opt', 'ca_default') 469 section.set_property('copy_extensions', 'copy') 470 section.set_property('x509_extensions', 'signing_ca_ext') 471 section.set_property('default_crl_days', '30') 472 section.set_property('crl_extensions', 'crl_ext') 473 474 section = self.config.get_section('policy_anything') 475 section.set_property('domainComponent', 'optional') 476 section.set_property('countryName', 'optional') 477 section.set_property('stateOrProvinceName', 'optional') 478 section.set_property('localityName', 'optional') 479 section.set_property('organizationName', 'optional') 480 section.set_property('organizationalUnitName', 'optional') 481 section.set_property('commonName', 'optional') 482 section.set_property('emailAddress', 'optional') 483 484 section = self.config.get_section('signing_ca_ext') 485 section.set_property('subjectKeyIdentifier', 'hash') 486 section.set_property('authorityKeyIdentifier', 'keyid:always') 487 section.set_property('authorityInfoAccess', '@issuer_info') 488 section.set_property('crlDistributionPoints', '@crl_info') 489 490 section = self.config.get_section('issuer_info') 491 section.set_property('caIssuers;URI.0', 492 'http://url-for-aia/%s.cer' % (self.name)) 493 494 section = self.config.get_section('crl_info') 495 section.set_property('URI.0', 'http://url-for-crl/%s.crl' % (self.name)) 496 497 section = self.config.get_section('crl_ext') 498 section.set_property('authorityKeyIdentifier', 'keyid:always') 499 section.set_property('authorityInfoAccess', '@issuer_info') 500 501 502def text_data_to_pem(block_header, text_data): 503 # b64encode takes in bytes and returns bytes. 504 pem_data = base64.b64encode(text_data.encode('utf8')).decode('utf8') 505 return '%s\n-----BEGIN %s-----\n%s\n-----END %s-----\n' % ( 506 text_data, block_header, pem_data, block_header) 507 508 509def write_chain(description, chain, out_pem): 510 """Writes the chain to a .pem file as a series of CERTIFICATE blocks""" 511 512 # Prepend the script name that generated the file to the description. 513 test_data = '[Created by: %s]\n\n%s\n' % (sys.argv[0], description) 514 515 # Write the certificate chain to the output file. 516 for cert in chain: 517 test_data += '\n' + cert.get_cert_pem() 518 519 write_string_to_file(test_data, out_pem) 520 521 522def write_string_to_file(data, path): 523 with open(path, 'w') as f: 524 f.write(data) 525 526 527def read_file_to_string(path): 528 with open(path, 'r') as f: 529 return f.read() 530 531 532def init(invoking_script_path): 533 """Creates an output directory to contain all the temporary files that may be 534 created, as well as determining the path for the final output. These paths 535 are all based off of the name of the calling script. 536 """ 537 538 global g_tmp_dir 539 global g_invoking_script_path 540 541 g_invoking_script_path = invoking_script_path 542 543 # The scripts assume to be run from within their containing directory (paths 544 # to things like "keys/" are written relative). 545 expected_cwd = os.path.realpath(os.path.dirname(invoking_script_path)) 546 actual_cwd = os.path.realpath(os.getcwd()) 547 if actual_cwd != expected_cwd: 548 sys.stderr.write( 549 ('Your current working directory must be that containing the python ' 550 'scripts:\n%s\nas the script may reference paths relative to this\n') 551 % (expected_cwd)) 552 sys.exit(1) 553 554 # Use an output directory that is a sibling of the invoking script. 555 g_tmp_dir = 'out' 556 557 # Ensure the output directory exists and is empty. 558 sys.stdout.write('Creating output directory: %s\n' % (g_tmp_dir)) 559 shutil.rmtree(g_tmp_dir, True) 560 os.makedirs(g_tmp_dir) 561 562 563def create_self_signed_root_certificate(name): 564 return Certificate(name, TYPE_CA, None) 565 566 567def create_intermediate_certificate(name, issuer): 568 return Certificate(name, TYPE_CA, issuer) 569 570 571def create_self_signed_end_entity_certificate(name): 572 return Certificate(name, TYPE_END_ENTITY, None) 573 574 575def create_end_entity_certificate(name, issuer): 576 return Certificate(name, TYPE_END_ENTITY, issuer) 577 578init(sys.argv[0]) 579