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 |