xref: /aosp_15_r20/external/cronet/net/data/gencerts/__init__.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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