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 # Use case-insensitive matching for counting duplicates, since some |
82 g_cur_path_id[name] = path_id + 1 | 85 # filesystems are case insensitive, but case preserving. |
| 86 lowercase_name = name.lower() |
| 87 path_id = g_cur_path_id.get(lowercase_name, 0) |
| 88 g_cur_path_id[lowercase_name] = path_id + 1 |
83 | 89 |
84 # Use a short and clean name for the first use of this name. | 90 # Use a short and clean name for the first use of this name. |
85 if path_id == 0: | 91 if path_id == 0: |
86 return name | 92 return name |
87 | 93 |
88 # Otherwise append the count to make it unique. | 94 # Otherwise append the count to make it unique. |
89 return '%s_%d' % (name, path_id) | 95 return '%s_%d' % (name, path_id) |
90 | 96 |
91 | 97 |
92 def get_path_in_output_dir(name, suffix): | 98 def get_path_in_output_dir(name, suffix): |
93 return os.path.join(g_out_dir, '%s%s' % (name, suffix)) | 99 return os.path.join(g_out_dir, '%s%s' % (name, suffix)) |
94 | 100 |
95 | 101 |
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): | 102 class Key(object): |
101 """Describes a public + private key pair. It is a dumb wrapper around an | 103 """Describes a public + private key pair. It is a dumb wrapper around an |
102 on-disk key.""" | 104 on-disk key.""" |
103 | 105 |
104 def __init__(self, path): | 106 def __init__(self, path): |
105 self.path = path | 107 self.path = path |
106 | 108 |
107 | 109 |
108 def get_path(self): | 110 def get_path(self): |
109 """Returns the path to a file that contains the key contents.""" | 111 """Returns the path to a file that contains the key contents.""" |
110 return self.path | 112 return self.path |
111 | 113 |
112 | 114 |
113 def generate_rsa_key(size_bits, path=None): | 115 def get_or_generate_key(generation_arguments, path): |
114 """Generates an RSA private key and returns it as a Key object. If |path| is | 116 """Helper function to either retrieve a key from an existing file |path|, or |
115 specified the resulting key will be saved at that location.""" | 117 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 | 118 |
119 # Ensure the path doesn't already exists (otherwise will be overwriting | 119 generation_arguments_str = ' '.join(generation_arguments) |
120 # something). | |
121 assert not os.path.isfile(path) | |
122 | 120 |
123 subprocess.check_call( | 121 # If the file doesn't already exist, generate a new key using the generation |
124 ['openssl', 'genrsa', '-out', path, str(size_bits)]) | 122 # parameters. |
| 123 if not os.path.isfile(path): |
| 124 key_contents = subprocess.check_output(generation_arguments) |
| 125 |
| 126 # Prepend the generation parameters to the key file. |
| 127 write_string_to_file(generation_arguments_str + '\n' + key_contents, |
| 128 path) |
| 129 else: |
| 130 # If the path already exists, confirm that it is for the expected key type. |
| 131 first_line = read_file_to_string(path).splitlines()[0] |
| 132 if first_line != generation_arguments_str: |
| 133 sys.stderr.write(('\nERROR: The existing key file:\n %s\nis not ' |
| 134 'compatible with the requested parameters:\n "%s" vs "%s".\n' |
| 135 'Delete the file if you want to re-generate it with the new ' |
| 136 'parameters, otherwise pick a new filename\n') % ( |
| 137 path, first_line, generation_arguments_str)) |
| 138 sys.exit(1) |
125 | 139 |
126 return Key(path) | 140 return Key(path) |
127 | 141 |
128 | 142 |
129 def generate_ec_key(named_curve, path=None): | 143 def get_or_generate_rsa_key(size_bits, path): |
130 """Generates an EC private key for the certificate and returns it as a Key | 144 """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 | 145 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.""" | 146 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 | 147 |
136 # Ensure the path doesn't already exists (otherwise will be overwriting | |
137 # something). | |
138 assert not os.path.isfile(path) | |
139 | 148 |
140 subprocess.check_call( | 149 def get_or_generate_ec_key(named_curve, path): |
141 ['openssl', 'ecparam', '-out', path, | 150 """Retrieves an existing key from a file if the path exists. Otherwise |
142 '-name', named_curve, '-genkey']) | 151 generates an EC key with the specified named curve and saves it to the |
| 152 path.""" |
| 153 return get_or_generate_key(['openssl', 'ecparam', '-name', named_curve, |
| 154 '-genkey'], path) |
143 | 155 |
144 return Key(path) | 156 |
| 157 def create_key_path(base_name): |
| 158 """Generates a name that contains |base_name| in it, and is relative to the |
| 159 "keys/" directory. If create_key_path(xxx) is called more than once during |
| 160 the script run, a suffix will be added.""" |
| 161 |
| 162 # Save keys to CWD/keys/<generate-script-name>/*.key |
| 163 # Hack: if the script name was generate-certs.py, then just save to |
| 164 # 'keys/*.key' (used by external consumers of common.py) |
| 165 keys_dir = 'keys' |
| 166 if g_script_name != 'certs': |
| 167 keys_dir = os.path.join(keys_dir, g_script_name) |
| 168 |
| 169 # Create the keys directory if it doesn't exist |
| 170 if not os.path.exists(keys_dir): |
| 171 os.makedirs(keys_dir) |
| 172 |
| 173 return get_unique_path_id(os.path.join(keys_dir, base_name)) + '.key' |
145 | 174 |
146 | 175 |
147 class Certificate(object): | 176 class Certificate(object): |
148 """Helper for building an X.509 certificate.""" | 177 """Helper for building an X.509 certificate.""" |
149 | 178 |
150 def __init__(self, name, cert_type, issuer): | 179 def __init__(self, name, cert_type, issuer): |
151 # The name will be used for the subject's CN, and also as a component of | 180 # The name will be used for the subject's CN, and also as a component of |
152 # the temporary filenames to help with debugging. | 181 # the temporary filenames to help with debugging. |
153 self.name = name | 182 self.name = name |
154 self.path_id = get_unique_path_id(name) | 183 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): | 277 def set_key_internal(self, key): |
249 self.key = key | 278 self.key = key |
250 | 279 |
251 # Associate the private key with the certificate. | 280 # Associate the private key with the certificate. |
252 section = self.config.get_section('root_ca') | 281 section = self.config.get_section('root_ca') |
253 section.set_property('private_key', self.key.get_path()) | 282 section.set_property('private_key', self.key.get_path()) |
254 | 283 |
255 | 284 |
256 def get_key(self): | 285 def get_key(self): |
257 if self.key is None: | 286 if self.key is None: |
258 self.set_key_internal(generate_rsa_key(2048, path=self.get_path(".key"))) | 287 self.set_key_internal( |
| 288 get_or_generate_rsa_key(2048, create_key_path(self.name))) |
259 return self.key | 289 return self.key |
260 | 290 |
261 | 291 |
262 def get_cert_path(self): | 292 def get_cert_path(self): |
263 return self.get_path('.pem') | 293 return self.get_path('.pem') |
264 | 294 |
265 | 295 |
266 def get_serial_path(self): | 296 def get_serial_path(self): |
267 return self.get_name_path('.serial') | 297 return self.get_name_path('.serial') |
268 | 298 |
269 | 299 |
270 def get_csr_path(self): | 300 def get_csr_path(self): |
271 return self.get_path('.csr') | 301 return self.get_path('.csr') |
272 | 302 |
273 | 303 |
274 def get_database_path(self): | 304 def get_database_path(self): |
275 return self.get_name_path('.db') | 305 return self.get_name_path('.db') |
276 | 306 |
277 | 307 |
278 def get_config_path(self): | 308 def get_config_path(self): |
279 return self.get_path('.cnf') | 309 return self.get_path('.cnf') |
280 | 310 |
281 | 311 |
282 def get_cert_pem(self): | 312 def get_cert_pem(self): |
283 # Finish generating a .pem file for the certificate. | 313 # Finish generating a .pem file for the certificate. |
284 self.finalize() | 314 self.finalize() |
285 | 315 |
286 # Read the certificate data. | 316 # Read the certificate data. |
287 with open(self.get_cert_path(), 'r') as f: | 317 return read_file_to_string(self.get_cert_path()) |
288 return f.read() | |
289 | 318 |
290 | 319 |
291 def finalize(self): | 320 def finalize(self): |
292 """Finishes the certificate creation process. This generates any needed | 321 """Finishes the certificate creation process. This generates any needed |
293 key, creates and signs the CSR. On completion the resulting PEM file can be | 322 key, creates and signs the CSR. On completion the resulting PEM file can be |
294 found at self.get_cert_path()""" | 323 found at self.get_cert_path()""" |
295 | 324 |
296 if self.finalized: | 325 if self.finalized: |
297 return # Already finalized, no work needed. | 326 return # Already finalized, no work needed. |
298 | 327 |
(...skipping 174 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
473 test_data += '\n' + text_data_to_pem('TIME', utc_time) | 502 test_data += '\n' + text_data_to_pem('TIME', utc_time) |
474 | 503 |
475 verify_result_string = 'SUCCESS' if verify_result else 'FAIL' | 504 verify_result_string = 'SUCCESS' if verify_result else 'FAIL' |
476 test_data += '\n' + text_data_to_pem('VERIFY_RESULT', verify_result_string) | 505 test_data += '\n' + text_data_to_pem('VERIFY_RESULT', verify_result_string) |
477 | 506 |
478 test_data += '\n' + text_data_to_pem('KEY_PURPOSE', key_purpose) | 507 test_data += '\n' + text_data_to_pem('KEY_PURPOSE', key_purpose) |
479 | 508 |
480 if errors is not None: | 509 if errors is not None: |
481 test_data += '\n' + text_data_to_pem('ERRORS', errors) | 510 test_data += '\n' + text_data_to_pem('ERRORS', errors) |
482 | 511 |
483 write_string_to_file(test_data, out_pem if out_pem else g_out_pem) | 512 if not out_pem: |
| 513 out_pem = g_script_name + '.pem' |
| 514 write_string_to_file(test_data, out_pem) |
484 | 515 |
485 | 516 |
486 def write_string_to_file(data, path): | 517 def write_string_to_file(data, path): |
487 with open(path, 'w') as f: | 518 with open(path, 'w') as f: |
488 f.write(data) | 519 f.write(data) |
489 | 520 |
490 | 521 |
| 522 def read_file_to_string(path): |
| 523 with open(path, 'r') as f: |
| 524 return f.read() |
| 525 |
| 526 |
491 def init(invoking_script_path): | 527 def init(invoking_script_path): |
492 """Creates an output directory to contain all the temporary files that may be | 528 """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 | 529 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. | 530 are all based off of the name of the calling script. |
495 """ | 531 """ |
496 | 532 |
497 global g_out_dir | 533 global g_out_dir |
498 global g_out_pem | 534 global g_script_name |
| 535 |
| 536 # The scripts assume to be run from within their containing directory (paths |
| 537 # to things like "keys/" are written relative). |
| 538 expected_cwd = os.path.realpath(os.path.dirname(invoking_script_path)) |
| 539 actual_cwd = os.path.realpath(os.getcwd()) |
| 540 if actual_cwd != expected_cwd: |
| 541 sys.stderr.write( |
| 542 ('Your current working directory must be that containing the python ' |
| 543 'scripts:\n%s\nas the script may reference paths relative to this\n') |
| 544 % (expected_cwd)) |
| 545 sys.exit(1) |
499 | 546 |
500 # Base the output name off of the invoking script's name. | 547 # Base the output name off of the invoking script's name. |
501 out_name = os.path.splitext(os.path.basename(invoking_script_path))[0] | 548 out_name = os.path.splitext(os.path.basename(invoking_script_path))[0] |
502 | 549 |
503 # Strip the leading 'generate-' | 550 # Strip the leading 'generate-' |
504 if out_name.startswith('generate-'): | 551 if out_name.startswith('generate-'): |
505 out_name = out_name[9:] | 552 out_name = out_name[9:] |
506 | 553 |
507 # Use an output directory with the same name as the invoking script. | 554 # Use an output directory with the same name as the invoking script. |
508 g_out_dir = os.path.join('out', out_name) | 555 g_out_dir = os.path.join('out', out_name) |
509 | 556 |
510 # Ensure the output directory exists and is empty. | 557 # Ensure the output directory exists and is empty. |
511 sys.stdout.write('Creating output directory: %s\n' % (g_out_dir)) | 558 sys.stdout.write('Creating output directory: %s\n' % (g_out_dir)) |
512 shutil.rmtree(g_out_dir, True) | 559 shutil.rmtree(g_out_dir, True) |
513 os.makedirs(g_out_dir) | 560 os.makedirs(g_out_dir) |
514 | 561 |
515 g_out_pem = os.path.join('%s.pem' % (out_name)) | 562 g_script_name = out_name |
516 | 563 |
517 | 564 |
518 def create_self_signed_root_certificate(name): | 565 def create_self_signed_root_certificate(name): |
519 return Certificate(name, TYPE_CA, None) | 566 return Certificate(name, TYPE_CA, None) |
520 | 567 |
521 | 568 |
522 def create_intermediate_certificate(name, issuer): | 569 def create_intermediate_certificate(name, issuer): |
523 return Certificate(name, TYPE_CA, issuer) | 570 return Certificate(name, TYPE_CA, issuer) |
524 | 571 |
525 | 572 |
526 def create_end_entity_certificate(name, issuer): | 573 def create_end_entity_certificate(name, issuer): |
527 return Certificate(name, TYPE_END_ENTITY, issuer) | 574 return Certificate(name, TYPE_END_ENTITY, issuer) |
528 | 575 |
529 init(sys.argv[0]) | 576 init(sys.argv[0]) |
OLD | NEW |