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