Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2015 The Chromium Authors. All rights reserved. | 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 | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Set of helpers to generate signed X.509v3 certificates. | 6 """Set of helpers to generate signed X.509v3 certificates. |
| 7 | 7 |
| 8 This works by shelling out calls to the 'openssl req' and 'openssl ca' | 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 | 9 commands, and passing the appropriate command line flags and configuration file |
| 10 (.cnf). | 10 (.cnf). |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 45 KEY_PURPOSE_ANY = 'anyExtendedKeyUsage' | 45 KEY_PURPOSE_ANY = 'anyExtendedKeyUsage' |
| 46 KEY_PURPOSE_SERVER_AUTH = 'serverAuth' | 46 KEY_PURPOSE_SERVER_AUTH = 'serverAuth' |
| 47 KEY_PURPOSE_CLIENT_AUTH = 'clientAuth' | 47 KEY_PURPOSE_CLIENT_AUTH = 'clientAuth' |
| 48 | 48 |
| 49 DEFAULT_KEY_PURPOSE = KEY_PURPOSE_SERVER_AUTH | 49 DEFAULT_KEY_PURPOSE = KEY_PURPOSE_SERVER_AUTH |
| 50 | 50 |
| 51 # Counters used to generate unique (but readable) path names. | 51 # Counters used to generate unique (but readable) path names. |
| 52 g_cur_path_id = {} | 52 g_cur_path_id = {} |
| 53 | 53 |
| 54 # Output paths used: | 54 # Output paths used: |
| 55 # - g_out_dir: where any temporary files (keys, cert req, signing db etc) are | 55 # - g_out_dir: where any temporary files (cert req, signing db etc) are |
| 56 # saved to. | 56 # saved to. |
| 57 # - g_out_pem: the path to the final output (which is a .pem file) | 57 # - g_script_name: the name of the invoking script. For instance if this is |
| 58 # being run by generate-foo.py then g_script_name will be | |
| 59 # 'foo' | |
| 58 # | 60 # |
| 59 # See init() for how these are assigned, based on the name of the calling | 61 # See init() for how these are assigned, based on the name of the calling |
| 60 # script. | 62 # script. |
| 61 g_out_dir = None | 63 g_out_dir = None |
| 62 g_out_pem = None | 64 g_script_name = None |
| 63 | 65 |
| 64 # The default validity range of generated certificates. Can be modified with | 66 # The default validity range of generated certificates. Can be modified with |
| 65 # set_default_validity_range(). | 67 # set_default_validity_range(). |
| 66 g_default_start_date = JANUARY_1_2015_UTC | 68 g_default_start_date = JANUARY_1_2015_UTC |
| 67 g_default_end_date = JANUARY_1_2016_UTC | 69 g_default_end_date = JANUARY_1_2016_UTC |
| 68 | 70 |
| 69 | 71 |
| 70 def set_default_validity_range(start_date, end_date): | 72 def set_default_validity_range(start_date, end_date): |
| 71 """Sets the validity range that will be used for certificates created with | 73 """Sets the validity range that will be used for certificates created with |
| 72 Certificate""" | 74 Certificate""" |
| 73 global g_default_start_date | 75 global g_default_start_date |
| 74 global g_default_end_date | 76 global g_default_end_date |
| 75 g_default_start_date = start_date | 77 g_default_start_date = start_date |
| 76 g_default_end_date = end_date | 78 g_default_end_date = end_date |
| 77 | 79 |
| 80 | |
| 78 def get_unique_path_id(name): | 81 def get_unique_path_id(name): |
| 79 """Returns a base filename that contains 'name', but is unique to the output | 82 """Returns a base filename that contains 'name', but is unique to the output |
| 80 directory""" | 83 directory""" |
| 81 path_id = g_cur_path_id.get(name, 0) | 84 path_id = g_cur_path_id.get(name, 0) |
| 82 g_cur_path_id[name] = path_id + 1 | 85 g_cur_path_id[name] = path_id + 1 |
| 83 | 86 |
| 84 # Use a short and clean name for the first use of this name. | 87 # Use a short and clean name for the first use of this name. |
| 85 if path_id == 0: | 88 if path_id == 0: |
| 86 return name | 89 return name |
| 87 | 90 |
| 88 # Otherwise append the count to make it unique. | 91 # Otherwise append the count to make it unique. |
| 89 return '%s_%d' % (name, path_id) | 92 return '%s_%d' % (name, path_id) |
| 90 | 93 |
| 91 | 94 |
| 92 def get_path_in_output_dir(name, suffix): | 95 def get_path_in_output_dir(name, suffix): |
| 93 return os.path.join(g_out_dir, '%s%s' % (name, suffix)) | 96 return os.path.join(g_out_dir, '%s%s' % (name, suffix)) |
| 94 | 97 |
| 95 | 98 |
| 96 def get_unique_path_in_output_dir(name, suffix): | |
| 97 return get_path_in_output_dir(get_unique_path_id(name), suffix) | |
| 98 | |
| 99 | |
| 100 class Key(object): | 99 class Key(object): |
| 101 """Describes a public + private key pair. It is a dumb wrapper around an | 100 """Describes a public + private key pair. It is a dumb wrapper around an |
| 102 on-disk key.""" | 101 on-disk key.""" |
| 103 | 102 |
| 104 def __init__(self, path): | 103 def __init__(self, path): |
| 105 self.path = path | 104 self.path = path |
| 106 | 105 |
| 107 | 106 |
| 108 def get_path(self): | 107 def get_path(self): |
| 109 """Returns the path to a file that contains the key contents.""" | 108 """Returns the path to a file that contains the key contents.""" |
| 110 return self.path | 109 return self.path |
| 111 | 110 |
| 112 | 111 |
| 113 def generate_rsa_key(size_bits, path=None): | 112 def get_or_generate_key(generation_arguments, path): |
| 114 """Generates an RSA private key and returns it as a Key object. If |path| is | 113 """Helper function to either retrieve a key from an existing file |path|, or |
| 115 specified the resulting key will be saved at that location.""" | 114 generate a new one using the command line |generation_arguments|.""" |
| 116 if path is None: | |
| 117 path = get_unique_path_in_output_dir('RsaKey', 'key') | |
| 118 | 115 |
| 119 # Ensure the path doesn't already exists (otherwise will be overwriting | 116 generation_arguments_str = ' '.join(generation_arguments) |
| 120 # something). | |
| 121 assert not os.path.isfile(path) | |
| 122 | 117 |
| 123 subprocess.check_call( | 118 # If the file doesn't already exist, generate a new key using the generation |
| 124 ['openssl', 'genrsa', '-out', path, str(size_bits)]) | 119 # parameters. |
| 120 if not os.path.isfile(path): | |
| 121 key_contents = subprocess.check_output(generation_arguments) | |
| 122 | |
| 123 # Prepend the generation parameters to the key file. | |
| 124 write_string_to_file(generation_arguments_str + '\n' + key_contents, | |
| 125 path) | |
| 126 else: | |
| 127 # If the path already exists, confirm that it is for the expected key type. | |
| 128 file_contents = read_file_to_string(path) | |
| 129 if not file_contents.startswith(generation_arguments_str): | |
|
mattm
2017/04/07 22:01:27
This could just match part of the generation args
eroman
2017/04/07 23:36:33
Good point! Done.
| |
| 130 sys.stderr.write( | |
| 131 ('The existing key file %s is not compatible with the ' | |
| 132 'requested parameters:\n%s.\n') % (path, generation_arguments_str)) | |
|
mattm
2017/04/07 22:01:27
might be helpful to mention you can delete the key
eroman
2017/04/07 23:36:33
Done. I also print out the contradiction explicitl
| |
| 133 sys.exit(1) | |
| 125 | 134 |
| 126 return Key(path) | 135 return Key(path) |
| 127 | 136 |
| 128 | 137 |
| 129 def generate_ec_key(named_curve, path=None): | 138 def get_or_generate_rsa_key(size_bits, path): |
| 130 """Generates an EC private key for the certificate and returns it as a Key | 139 """Retrieves an existing key from a file if the path exists. Otherwise |
| 131 object. |named_curve| can be something like secp384r1. If |path| is specified | 140 generates an RSA key with the specified bit size and saves it to the path.""" |
| 132 the resulting key will be saved at that location.""" | 141 return get_or_generate_key(['openssl', 'genrsa', str(size_bits)], path) |
| 133 if path is None: | |
| 134 path = get_unique_path_in_output_dir('EcKey', 'key') | |
| 135 | 142 |
| 136 # Ensure the path doesn't already exists (otherwise will be overwriting | |
| 137 # something). | |
| 138 assert not os.path.isfile(path) | |
| 139 | 143 |
| 140 subprocess.check_call( | 144 def get_or_generate_ec_key(named_curve, path): |
| 141 ['openssl', 'ecparam', '-out', path, | 145 """Retrieves an existing key from a file if the path exists. Otherwise |
| 142 '-name', named_curve, '-genkey']) | 146 generates an EC key with the specified named curve and saves it to the |
| 147 path.""" | |
| 148 return get_or_generate_key(['openssl', 'ecparam', '-name', named_curve, | |
| 149 '-genkey'], path) | |
| 143 | 150 |
| 144 return Key(path) | 151 |
| 152 def create_key_path(base_name): | |
| 153 """Generates a name that contains |base_name| in it, and is relative to the | |
| 154 "keys/" directory""" | |
| 155 return get_unique_path_id('keys/' + g_script_name + '_' + base_name) + '.key' | |
|
mattm
2017/04/07 22:01:27
This may work a bit less well for other scripts th
eroman
2017/04/07 23:36:33
I made a few changes which hopefully address those
| |
| 145 | 156 |
| 146 | 157 |
| 147 class Certificate(object): | 158 class Certificate(object): |
| 148 """Helper for building an X.509 certificate.""" | 159 """Helper for building an X.509 certificate.""" |
| 149 | 160 |
| 150 def __init__(self, name, cert_type, issuer): | 161 def __init__(self, name, cert_type, issuer): |
| 151 # The name will be used for the subject's CN, and also as a component of | 162 # The name will be used for the subject's CN, and also as a component of |
| 152 # the temporary filenames to help with debugging. | 163 # the temporary filenames to help with debugging. |
| 153 self.name = name | 164 self.name = name |
| 154 self.path_id = get_unique_path_id(name) | 165 self.path_id = get_unique_path_id(name) |
| (...skipping 93 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 248 def set_key_internal(self, key): | 259 def set_key_internal(self, key): |
| 249 self.key = key | 260 self.key = key |
| 250 | 261 |
| 251 # Associate the private key with the certificate. | 262 # Associate the private key with the certificate. |
| 252 section = self.config.get_section('root_ca') | 263 section = self.config.get_section('root_ca') |
| 253 section.set_property('private_key', self.key.get_path()) | 264 section.set_property('private_key', self.key.get_path()) |
| 254 | 265 |
| 255 | 266 |
| 256 def get_key(self): | 267 def get_key(self): |
| 257 if self.key is None: | 268 if self.key is None: |
| 258 self.set_key_internal(generate_rsa_key(2048, path=self.get_path(".key"))) | 269 self.set_key_internal( |
| 270 get_or_generate_rsa_key(2048, create_key_path(self.name))) | |
| 259 return self.key | 271 return self.key |
| 260 | 272 |
| 261 | 273 |
| 262 def get_cert_path(self): | 274 def get_cert_path(self): |
| 263 return self.get_path('.pem') | 275 return self.get_path('.pem') |
| 264 | 276 |
| 265 | 277 |
| 266 def get_serial_path(self): | 278 def get_serial_path(self): |
| 267 return self.get_name_path('.serial') | 279 return self.get_name_path('.serial') |
| 268 | 280 |
| 269 | 281 |
| 270 def get_csr_path(self): | 282 def get_csr_path(self): |
| 271 return self.get_path('.csr') | 283 return self.get_path('.csr') |
| 272 | 284 |
| 273 | 285 |
| 274 def get_database_path(self): | 286 def get_database_path(self): |
| 275 return self.get_name_path('.db') | 287 return self.get_name_path('.db') |
| 276 | 288 |
| 277 | 289 |
| 278 def get_config_path(self): | 290 def get_config_path(self): |
| 279 return self.get_path('.cnf') | 291 return self.get_path('.cnf') |
| 280 | 292 |
| 281 | 293 |
| 282 def get_cert_pem(self): | 294 def get_cert_pem(self): |
| 283 # Finish generating a .pem file for the certificate. | 295 # Finish generating a .pem file for the certificate. |
| 284 self.finalize() | 296 self.finalize() |
| 285 | 297 |
| 286 # Read the certificate data. | 298 # Read the certificate data. |
| 287 with open(self.get_cert_path(), 'r') as f: | 299 return read_file_to_string(self.get_cert_path()) |
| 288 return f.read() | |
| 289 | 300 |
| 290 | 301 |
| 291 def finalize(self): | 302 def finalize(self): |
| 292 """Finishes the certificate creation process. This generates any needed | 303 """Finishes the certificate creation process. This generates any needed |
| 293 key, creates and signs the CSR. On completion the resulting PEM file can be | 304 key, creates and signs the CSR. On completion the resulting PEM file can be |
| 294 found at self.get_cert_path()""" | 305 found at self.get_cert_path()""" |
| 295 | 306 |
| 296 if self.finalized: | 307 if self.finalized: |
| 297 return # Already finalized, no work needed. | 308 return # Already finalized, no work needed. |
| 298 | 309 |
| (...skipping 174 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 473 test_data += '\n' + text_data_to_pem('TIME', utc_time) | 484 test_data += '\n' + text_data_to_pem('TIME', utc_time) |
| 474 | 485 |
| 475 verify_result_string = 'SUCCESS' if verify_result else 'FAIL' | 486 verify_result_string = 'SUCCESS' if verify_result else 'FAIL' |
| 476 test_data += '\n' + text_data_to_pem('VERIFY_RESULT', verify_result_string) | 487 test_data += '\n' + text_data_to_pem('VERIFY_RESULT', verify_result_string) |
| 477 | 488 |
| 478 test_data += '\n' + text_data_to_pem('KEY_PURPOSE', key_purpose) | 489 test_data += '\n' + text_data_to_pem('KEY_PURPOSE', key_purpose) |
| 479 | 490 |
| 480 if errors is not None: | 491 if errors is not None: |
| 481 test_data += '\n' + text_data_to_pem('ERRORS', errors) | 492 test_data += '\n' + text_data_to_pem('ERRORS', errors) |
| 482 | 493 |
| 483 write_string_to_file(test_data, out_pem if out_pem else g_out_pem) | 494 if not out_pem: |
| 495 out_pem = g_script_name + '.pem' | |
| 496 write_string_to_file(test_data, out_pem) | |
| 484 | 497 |
| 485 | 498 |
| 486 def write_string_to_file(data, path): | 499 def write_string_to_file(data, path): |
| 487 with open(path, 'w') as f: | 500 with open(path, 'w') as f: |
| 488 f.write(data) | 501 f.write(data) |
| 489 | 502 |
| 490 | 503 |
| 504 def read_file_to_string(path): | |
| 505 with open(path, 'r') as f: | |
| 506 return f.read() | |
| 507 | |
| 508 | |
| 491 def init(invoking_script_path): | 509 def init(invoking_script_path): |
| 492 """Creates an output directory to contain all the temporary files that may be | 510 """Creates an output directory to contain all the temporary files that may be |
| 493 created, as well as determining the path for the final output. These paths | 511 created, as well as determining the path for the final output. These paths |
| 494 are all based off of the name of the calling script. | 512 are all based off of the name of the calling script. |
| 495 """ | 513 """ |
| 496 | 514 |
| 497 global g_out_dir | 515 global g_out_dir |
| 498 global g_out_pem | 516 global g_script_name |
| 499 | 517 |
| 500 # Base the output name off of the invoking script's name. | 518 # Base the output name off of the invoking script's name. |
| 501 out_name = os.path.splitext(os.path.basename(invoking_script_path))[0] | 519 out_name = os.path.splitext(os.path.basename(invoking_script_path))[0] |
| 502 | 520 |
| 503 # Strip the leading 'generate-' | 521 # Strip the leading 'generate-' |
| 504 if out_name.startswith('generate-'): | 522 if out_name.startswith('generate-'): |
| 505 out_name = out_name[9:] | 523 out_name = out_name[9:] |
| 506 | 524 |
| 507 # Use an output directory with the same name as the invoking script. | 525 # Use an output directory with the same name as the invoking script. |
| 508 g_out_dir = os.path.join('out', out_name) | 526 g_out_dir = os.path.join('out', out_name) |
| 509 | 527 |
| 510 # Ensure the output directory exists and is empty. | 528 # Ensure the output directory exists and is empty. |
| 511 sys.stdout.write('Creating output directory: %s\n' % (g_out_dir)) | 529 sys.stdout.write('Creating output directory: %s\n' % (g_out_dir)) |
| 512 shutil.rmtree(g_out_dir, True) | 530 shutil.rmtree(g_out_dir, True) |
| 513 os.makedirs(g_out_dir) | 531 os.makedirs(g_out_dir) |
| 514 | 532 |
| 515 g_out_pem = os.path.join('%s.pem' % (out_name)) | 533 g_script_name = out_name |
| 516 | 534 |
| 517 | 535 |
| 518 def create_self_signed_root_certificate(name): | 536 def create_self_signed_root_certificate(name): |
| 519 return Certificate(name, TYPE_CA, None) | 537 return Certificate(name, TYPE_CA, None) |
| 520 | 538 |
| 521 | 539 |
| 522 def create_intermediate_certificate(name, issuer): | 540 def create_intermediate_certificate(name, issuer): |
| 523 return Certificate(name, TYPE_CA, issuer) | 541 return Certificate(name, TYPE_CA, issuer) |
| 524 | 542 |
| 525 | 543 |
| 526 def create_end_entity_certificate(name, issuer): | 544 def create_end_entity_certificate(name, issuer): |
| 527 return Certificate(name, TYPE_END_ENTITY, issuer) | 545 return Certificate(name, TYPE_END_ENTITY, issuer) |
| 528 | 546 |
| 529 init(sys.argv[0]) | 547 init(sys.argv[0]) |
| OLD | NEW |