diff --git a/functional_tests/test_init.py b/functional_tests/test_init.py index ae9676805f454b2b57db5854fb34b1a2cf65e658..7ab4bb2b925683edf3e6de5540351c42bfcec97f 100644 --- a/functional_tests/test_init.py +++ b/functional_tests/test_init.py @@ -136,7 +136,7 @@ def test_initialisation_with_custom_base_name(tmpdir): # draws his attention. The option seems to be usable for # specifying the base name for the CAs - exactly what he needed. assert "--ca-base-name" in stdout - assert "-b" in stdout + assert " -b " in stdout # John switches to his project directory. tmpdir.chdir() @@ -165,3 +165,122 @@ def test_initialisation_with_custom_base_name(tmpdir): # project name. assert issuer_dn.rstrip() == subject_dn.rstrip() == "CN = My Project Level 1" assert tmpdir.basename not in issuer_dn + + +def test_initialisation_with_custom_hierarchy_depth(tmpdir): + # John is now involved in a project where the CA hierarchy used + # for issuing certificates is supposed to be deeper than 1. In + # other words, he needs Level 1 CA -> Level 2 CA -> Level 3 CA -> + # end entity certificates. + + # He hopes that the Gimmecert tool can still help him with this + # scenario as well. At first, he runs the tool with a help flag. + stdout, _, _ = run_command('gimmecert', 'init', '-h') + + # John notices there is an option to specify hierarchy depth. + assert "--ca-hierarchy-depth" in stdout + assert " -d " in stdout + + # John switches to his project directory. + tmpdir.chdir() + + # He runs the command, specifying this time around the desired CA + # hierarchy depth. + stdout, stderr, exit_code = run_command('gimmecert', 'init', '--ca-hierarchy-depth', '3') + + # Command finishes execution with success, and he is informed that + # his CA hierarchy has been initialised. He notices there are many + # more CA artifacts listed now. + assert exit_code == 0 + assert stderr == "" + assert "CA hierarchy initialised." in stdout + assert ".gimmecert/ca/level1.key.pem" in stdout + assert ".gimmecert/ca/level1.cert.pem" in stdout + assert ".gimmecert/ca/level2.key.pem" in stdout + assert ".gimmecert/ca/level2.cert.pem" in stdout + assert ".gimmecert/ca/level3.key.pem" in stdout + assert ".gimmecert/ca/level3.cert.pem" in stdout + assert ".gimmecert/ca/chain-full.cert.pem" in stdout + + # John goes ahead and inspects the CA keys first. + stdout1, stderr1, exit_code1 = run_command('openssl', 'rsa', '-noout', '-text', '-in', '.gimmecert/ca/level1.key.pem') + stdout2, stderr2, exit_code2 = run_command('openssl', 'rsa', '-noout', '-text', '-in', '.gimmecert/ca/level2.key.pem') + stdout3, stderr3, exit_code3 = run_command('openssl', 'rsa', '-noout', '-text', '-in', '.gimmecert/ca/level3.key.pem') + + assert exit_code1 == 0 + assert stderr1 == "" + assert "Private-Key: (2048 bit)" in stdout1 + + assert exit_code2 == 0 + assert stderr2 == "" + assert "Private-Key: (2048 bit)" in stdout2 + + assert exit_code3 == 0 + assert stderr3 == "" + assert "Private-Key: (2048 bit)" in stdout3 + + # John then has a look at the generated CA certificate files. + stdout1, stderr1, exit_code1 = run_command('openssl', 'x509', '-noout', '-text', '-in', '.gimmecert/ca/level1.cert.pem') + stdout2, stderr2, exit_code2 = run_command('openssl', 'x509', '-noout', '-text', '-in', '.gimmecert/ca/level2.cert.pem') + stdout3, stderr3, exit_code3 = run_command('openssl', 'x509', '-noout', '-text', '-in', '.gimmecert/ca/level3.cert.pem') + + # John observes that there are no errors, and that certificate + # details are being shown. + assert 'Certificate:' in stdout1 + assert 'Certificate:' in stdout2 + assert 'Certificate:' in stdout3 + + # John then runs a bunch of commands to get the subject and issuer + # DNs of certificates. + issuer_dn1, _, _ = run_command('openssl', 'x509', '-noout', '-issuer', '-in', '.gimmecert/ca/level1.cert.pem') + subject_dn1, _, _ = run_command('openssl', 'x509', '-noout', '-subject', '-in', '.gimmecert/ca/level1.cert.pem') + issuer_dn1 = issuer_dn1.replace('issuer=', '', 1).rstrip().replace(' /CN=', 'CN = ', 1) # OpenSSL 1.0 vs 1.1 formatting + subject_dn1 = subject_dn1.replace('subject=', '', 1).rstrip().replace(' /CN=', 'CN = ', 1) # OpenSSL 1.0 vs 1.1 formatting + + issuer_dn2, _, _ = run_command('openssl', 'x509', '-noout', '-issuer', '-in', '.gimmecert/ca/level2.cert.pem') + subject_dn2, _, _ = run_command('openssl', 'x509', '-noout', '-subject', '-in', '.gimmecert/ca/level2.cert.pem') + issuer_dn2 = issuer_dn2.replace('issuer=', '', 2).rstrip().replace(' /CN=', 'CN = ', 2) # OpenSSL 1.0 vs 1.2 formatting + subject_dn2 = subject_dn2.replace('subject=', '', 2).rstrip().replace(' /CN=', 'CN = ', 2) # OpenSSL 1.0 vs 1.2 formatting + + issuer_dn3, _, _ = run_command('openssl', 'x509', '-noout', '-issuer', '-in', '.gimmecert/ca/level3.cert.pem') + subject_dn3, _, _ = run_command('openssl', 'x509', '-noout', '-subject', '-in', '.gimmecert/ca/level3.cert.pem') + issuer_dn3 = issuer_dn3.replace('issuer=', '', 1).rstrip().replace(' /CN=', 'CN = ', 1) # OpenSSL 1.0 vs 1.1 formatting + subject_dn3 = subject_dn3.replace('subject=', '', 1).rstrip().replace(' /CN=', 'CN = ', 1) # OpenSSL 1.0 vs 1.1 formatting + + # He notices that the all certificates seem to be have been issued + # by correct entity. + assert issuer_dn1 == subject_dn1 + assert issuer_dn2 == subject_dn1 + assert issuer_dn3 == subject_dn2 + assert subject_dn1 == 'CN = %s Level 1' % tmpdir.basename + assert subject_dn2 == 'CN = %s Level 2' % tmpdir.basename + assert subject_dn3 == 'CN = %s Level 3' % tmpdir.basename + + # John opens-up the chain file, and observes that all certificates + # seem to be contained within. + with open(".gimmecert/ca/level1.cert.pem", "r") as level1_cert_file: + level1_cert = level1_cert_file.read() + with open(".gimmecert/ca/level2.cert.pem", "r") as level2_cert_file: + level2_cert = level2_cert_file.read() + with open(".gimmecert/ca/level3.cert.pem", "r") as level3_cert_file: + level3_cert = level3_cert_file.read() + with open(".gimmecert/ca/chain-full.cert.pem", "r") as chain_full_file: + chain_full = chain_full_file.read() + + assert level1_cert in chain_full + assert level2_cert in chain_full + assert level3_cert in chain_full + + # Just to make sure, John goes ahead and runs a command that + # should verify the certificate signatures. + _, _, error_code = run_command( + "openssl", "verify", + "-CAfile", + ".gimmecert/ca/chain-full.cert.pem", + ".gimmecert/ca/level1.cert.pem", + ".gimmecert/ca/level2.cert.pem", + ".gimmecert/ca/level3.cert.pem" + ) + + # He is happy to see that verification succeeds. + assert error_code == 0 diff --git a/gimmecert/cli.py b/gimmecert/cli.py index 4c35471b6ea9e57707bd56bc89ae007cdf1390ac..1d3a358fa427540e8463ffbc1da5e6782b0422c0 100644 --- a/gimmecert/cli.py +++ b/gimmecert/cli.py @@ -44,16 +44,18 @@ Examples: def setup_init_subcommand_parser(parser, subparsers): subparser = subparsers.add_parser('init', description='Initialise CA hierarchy.') subparser.add_argument('--ca-base-name', '-b', help="Base name to use for CA naming. Default is to use the working directory base name.") + subparser.add_argument('--ca-hierarchy-depth', '-d', type=int, help="Depth of CA hierarchy to generate. Default is 1", default=1) def init_wrapper(args): project_directory = os.getcwd() if args.ca_base_name is None: args.ca_base_name = os.path.basename(project_directory) - if init(project_directory, args.ca_base_name): + if init(project_directory, args.ca_base_name, args.ca_hierarchy_depth): print("CA hierarchy initialised. Generated artefacts:") - print(" CA Level 1 private key: .gimmecert/ca/level1.key.pem") - print(" CA Level 1 certificate: .gimmecert/ca/level1.cert.pem") + for level in range(1, args.ca_hierarchy_depth+1): + print(" CA Level %d private key: .gimmecert/ca/level%d.key.pem" % (level, level)) + print(" CA Level %d certificate: .gimmecert/ca/level%d.cert.pem" % (level, level)) print(" Full certificate chain: .gimmecert/ca/chain-full.cert.pem") else: print("CA hierarchy has already been initialised.") diff --git a/gimmecert/commands.py b/gimmecert/commands.py index 50fe4b36b14fa1a3d0f14911f83c172d688ebba1..411a440ab1d750566694c8d4fd7007318fff36c8 100644 --- a/gimmecert/commands.py +++ b/gimmecert/commands.py @@ -24,7 +24,7 @@ import gimmecert.crypto import gimmecert.storage -def init(project_directory, ca_base_name): +def init(project_directory, ca_base_name, ca_hierarchy_depth): """ Initialises the necessary directory and CA hierarchies for use in the specified directory. @@ -35,6 +35,9 @@ def init(project_directory, ca_base_name): :param ca_base_name: Base name to use for constructing CA subject DNs. :type ca_base_name: str + :param ca_hierarchy_depth: Length/depths of CA hierarchy that should be initialised. E.g. total number of CAs in chain. + :type ca_hierarchy_depth: int + :returns: False, if directory has been initialised in previous run, True if project has been initialised in this run. :rtype: bool """ @@ -42,9 +45,6 @@ def init(project_directory, ca_base_name): # Set-up various paths. base_directory = os.path.join(project_directory, '.gimmecert') ca_directory = os.path.join(base_directory, 'ca') - level1_private_key_path = os.path.join(ca_directory, 'level1.key.pem') - level1_certificate_path = os.path.join(ca_directory, 'level1.cert.pem') - full_chain_path = os.path.join(ca_directory, 'chain-full.cert.pem') if os.path.exists(base_directory): return False @@ -52,18 +52,19 @@ def init(project_directory, ca_base_name): # Initialise the directory. gimmecert.storage.initialise_storage(project_directory) - # Generate private key and certificate. - level1_dn = gimmecert.crypto.get_dn("%s Level 1" % ca_base_name) - not_before, not_after = gimmecert.crypto.get_validity_range() - level1_private_key = gimmecert.crypto.generate_private_key() - level1_certificate = gimmecert.crypto.issue_certificate(level1_dn, level1_dn, level1_private_key, level1_private_key.public_key(), not_before, not_after) + # Generate the CA hierarchy. + ca_hierarchy = gimmecert.crypto.generate_ca_hierarchy(ca_base_name, ca_hierarchy_depth) - # Grab the full CA certificate chain. - full_chain = level1_certificate + # Output the CA private keys and certificates. + for level, (private_key, certificate) in enumerate(ca_hierarchy, 1): + private_key_path = os.path.join(ca_directory, 'level%d.key.pem' % level) + certificate_path = os.path.join(ca_directory, 'level%d.cert.pem' % level) + gimmecert.storage.write_private_key(private_key, private_key_path) + gimmecert.storage.write_certificate(certificate, certificate_path) - # Write-out the artifacts. - gimmecert.storage.write_private_key(level1_private_key, level1_private_key_path) - gimmecert.storage.write_certificate(level1_certificate, level1_certificate_path) - gimmecert.storage.write_certificate(full_chain, full_chain_path) + # Output the certificate chain. + full_chain = [certificate for _, certificate in ca_hierarchy] + full_chain_path = os.path.join(ca_directory, 'chain-full.cert.pem') + gimmecert.storage.write_certificate_chain(full_chain, full_chain_path) return True diff --git a/gimmecert/crypto.py b/gimmecert/crypto.py index 8e582ae7b66f3e0c7429052907e648c3ee9cce93..580a2cc924faa06d30e7dfcf69ff5e0d1cc1c63e 100644 --- a/gimmecert/crypto.py +++ b/gimmecert/crypto.py @@ -84,7 +84,7 @@ def get_validity_range(): return not_before, not_after -def issue_certificate(issuer_dn, subject_dn, signing_key, public_key, not_before, not_after): +def issue_certificate(issuer_dn, subject_dn, signing_key, public_key, not_before, not_after, extensions=None): """ Issues a certificate using the passed-in data. @@ -105,8 +105,17 @@ def issue_certificate(issuer_dn, subject_dn, signing_key, public_key, not_before :param not_after: End of certificate validity. :type datetime.datetime: + + :param extensions: List of certificate extensions with their criticality to add to resulting certificate object. List of (extension, criticality) pairs. + :type extensions: list[(cryptography.x509.Extension, bool)] + + :returns: Issued certificate with requested content. + :rtype: cryptography.x509.Certificate """ + if extensions is None: + extensions = [] + builder = cryptography.x509.CertificateBuilder() builder = builder.subject_name(cryptography.x509.Name(subject_dn)) builder = builder.issuer_name(cryptography.x509.Name(issuer_dn)) @@ -115,6 +124,9 @@ def issue_certificate(issuer_dn, subject_dn, signing_key, public_key, not_before builder = builder.serial_number(cryptography.x509.random_serial_number()) builder = builder.public_key(public_key) + for extension in extensions: + builder = builder.add_extension(extension[0], critical=extension[1]) + certificate = builder.sign( private_key=signing_key, algorithm=cryptography.hazmat.primitives.hashes.SHA256(), @@ -125,10 +137,25 @@ def issue_certificate(issuer_dn, subject_dn, signing_key, public_key, not_before def generate_ca_hierarchy(base_name, depth): + """ + Generates CA hierarchy with specified depth, using the provided + naming as basis for the DNs. + + :param base_name: Base name for constructing the CA DNs. Resulting DNs are of format 'BASE Level N'. + :type base_name: str + + :returns: List of CA private key and certificate pairs, starting with the level 1 (root) CA, and ending with the leaf CA. + :rtype: list[(cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey, cryptography.x509.Certificate)] + """ + hierarchy = [] not_before, not_after = get_validity_range() + extensions = [ + (cryptography.x509.BasicConstraints(ca=True, path_length=None), True) + ] + # We have not issued yet any certificate. issuer_dn = None issuer_private_key = None @@ -142,7 +169,7 @@ def generate_ca_hierarchy(base_name, depth): issuer_dn = issuer_dn or dn issuer_private_key = issuer_private_key or private_key - certificate = issue_certificate(issuer_dn, dn, issuer_private_key, private_key.public_key(), not_before, not_after) + certificate = issue_certificate(issuer_dn, dn, issuer_private_key, private_key.public_key(), not_before, not_after, extensions) hierarchy.append((private_key, certificate)) # Current entity becomes issuer for next one in chain. diff --git a/tests/test_cli.py b/tests/test_cli.py index 70911e4c0fefef7a5a13851117ff2ae037b4710f..485c5d2042d4fe77049c1a6966da1b4eb127fd0c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -163,21 +163,25 @@ def test_setup_init_subcommand_sets_function_callback(): @mock.patch('sys.argv', ['gimmecert', 'init']) @mock.patch('gimmecert.cli.init') def test_init_command_invoked_with_correct_parameters_no_options(mock_init, tmpdir): + default_depth = 1 + tmpdir.chdir() gimmecert.cli.main() - mock_init.assert_called_once_with(tmpdir.strpath, tmpdir.basename) + mock_init.assert_called_once_with(tmpdir.strpath, tmpdir.basename, default_depth) @mock.patch('sys.argv', ['gimmecert', 'init', '-b', 'My Project']) @mock.patch('gimmecert.cli.init') def test_init_command_invoked_with_correct_parameters_with_options(mock_init, tmpdir): + default_depth = 1 + tmpdir.chdir() gimmecert.cli.main() - mock_init.assert_called_once_with(tmpdir.strpath, 'My Project') + mock_init.assert_called_once_with(tmpdir.strpath, 'My Project', default_depth) @mock.patch('sys.argv', ['gimmecert', 'init', '--ca-base-name', 'My Project']) @@ -190,3 +194,15 @@ def test_init_command_accepts_ca_base_name_option_long_form(): def test_init_command_accepts_ca_base_name_option_short_form(): gimmecert.cli.main() # Should not raise + + +@mock.patch('sys.argv', ['gimmecert', 'init', '--ca-hierarchy-depth', '3']) +def test_init_command_accepts_ca_hierarchy_depth_option_long_form(): + + gimmecert.cli.main() # Should not raise + + +@mock.patch('sys.argv', ['gimmecert', 'init', '-d', '3']) +def test_init_command_accepts_ca_hierarchy_depth_option_short_form(): + + gimmecert.cli.main() # Should not raise diff --git a/tests/test_commands.py b/tests/test_commands.py index 89e5f57dfa96be6341cf831553f419f3a4794c46..87c2d56cee23ad48b726d7b5214bb2012c36a847 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -26,52 +26,107 @@ import gimmecert.commands def test_init_sets_up_directory_structure(tmpdir): base_dir = tmpdir.join('.gimmecert') ca_dir = tmpdir.join('.gimmecert') + depth = 1 tmpdir.chdir() - gimmecert.commands.init(tmpdir.strpath, tmpdir.basename) + gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth) assert os.path.exists(base_dir.strpath) assert os.path.exists(ca_dir.strpath) -def test_init_generates_ca_artifacts(tmpdir): +def test_init_generates_single_ca_artifact_for_depth_1(tmpdir): + depth = 1 + tmpdir.chdir() - gimmecert.commands.init(tmpdir.strpath, tmpdir.basename) + gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth) assert os.path.exists(tmpdir.join('.gimmecert', 'ca', 'level1.key.pem').strpath) assert os.path.exists(tmpdir.join('.gimmecert', 'ca', 'level1.cert.pem').strpath) assert os.path.exists(tmpdir.join('.gimmecert', 'ca', 'chain-full.cert.pem').strpath) +def test_init_generates_three_ca_artifacts_for_depth_3(tmpdir): + depth = 3 + + tmpdir.chdir() + + gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth) + + assert os.path.exists(tmpdir.join('.gimmecert', 'ca', 'level1.key.pem').strpath) + assert os.path.exists(tmpdir.join('.gimmecert', 'ca', 'level1.cert.pem').strpath) + assert os.path.exists(tmpdir.join('.gimmecert', 'ca', 'level2.key.pem').strpath) + assert os.path.exists(tmpdir.join('.gimmecert', 'ca', 'level2.cert.pem').strpath) + assert os.path.exists(tmpdir.join('.gimmecert', 'ca', 'level3.key.pem').strpath) + assert os.path.exists(tmpdir.join('.gimmecert', 'ca', 'level3.cert.pem').strpath) + assert os.path.exists(tmpdir.join('.gimmecert', 'ca', 'chain-full.cert.pem').strpath) + + +def test_init_outputs_full_chain_for_depth_1(tmpdir): + depth = 1 + + tmpdir.chdir() + + gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth) + + level1_certificate = tmpdir.join('.gimmecert', 'ca', 'level1.cert.pem').read() + full_chain = tmpdir.join('.gimmecert', 'ca', 'chain-full.cert.pem').read() + assert level1_certificate == full_chain + assert full_chain.replace(level1_certificate, '') == '' + + +def test_init_outputs_full_chain_for_depth_3(tmpdir): + depth = 3 + + tmpdir.chdir() + + gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth) + + level1_certificate = tmpdir.join('.gimmecert', 'ca', 'level1.cert.pem').read() + level2_certificate = tmpdir.join('.gimmecert', 'ca', 'level2.cert.pem').read() + level3_certificate = tmpdir.join('.gimmecert', 'ca', 'level3.cert.pem').read() + full_chain = tmpdir.join('.gimmecert', 'ca', 'chain-full.cert.pem').read() + assert level1_certificate in full_chain + assert level2_certificate in full_chain + assert level3_certificate in full_chain + assert full_chain == "%s\n%s\n%s" % (level1_certificate, level2_certificate, level3_certificate) + + def test_init_returns_true_if_directory_has_not_been_previously_initialised(tmpdir): + depth = 1 + tmpdir.chdir() - initialised = gimmecert.commands.init(tmpdir.strpath, tmpdir.basename) + initialised = gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth) assert initialised is True def test_init_returns_false_if_directory_has_been_previously_initialised(tmpdir): + depth = 1 + tmpdir.chdir() - gimmecert.commands.init(tmpdir.strpath, tmpdir.basename) - initialised = gimmecert.commands.init(tmpdir.strpath, tmpdir.basename) + gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth) + initialised = gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth) assert initialised is False def test_init_does_not_overwrite_artifcats_if_already_initialised(tmpdir): + depth = 1 + tmpdir.chdir() - gimmecert.commands.init(tmpdir.strpath, tmpdir.basename) + gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth) level1_private_key_before = tmpdir.join('.gimmecert', 'ca', 'level1.key.pem').read() level1_certificate_before = tmpdir.join('.gimmecert', 'ca', 'level1.cert.pem').read() full_chain_before = tmpdir.join('.gimmecert', 'ca', 'chain-full.cert.pem').read() - gimmecert.commands.init(tmpdir.strpath, tmpdir.basename) + gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth) level1_private_key_after = tmpdir.join('.gimmecert', 'ca', 'level1.key.pem').read() level1_certificate_after = tmpdir.join('.gimmecert', 'ca', 'level1.cert.pem').read() diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 4126b472af41a6223d66c5b0a2be23713d576f2e..f472bad8bf97a770c81d7a02316c5f2e3f215c31 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -22,6 +22,7 @@ import datetime import cryptography.hazmat.primitives.asymmetric.rsa +import cryptography.x509 from dateutil.relativedelta import relativedelta import gimmecert.crypto @@ -205,3 +206,55 @@ def test_generate_ca_hierarchy_certificates_have_same_validity(): assert level1_certificate.not_valid_before == level2_certificate.not_valid_before == level3_certificate.not_valid_before assert level1_certificate.not_valid_after == level2_certificate.not_valid_after == level3_certificate.not_valid_after + + +def test_issue_certificate_sets_extensions(): + dn = gimmecert.crypto.get_dn('My test 1') + private_key = gimmecert.crypto.generate_private_key() + not_before, not_after = gimmecert.crypto.get_validity_range() + basic_constraints = cryptography.x509.BasicConstraints(ca=True, path_length=None) + ocsp_no_check = cryptography.x509.OCSPNoCheck() + extensions = [ + (basic_constraints, True), + (ocsp_no_check, False), + ] + + certificate = gimmecert.crypto.issue_certificate(dn, dn, private_key, private_key.public_key(), not_before, not_after, extensions) + + assert len(certificate.extensions) == 2 + + stored_extension = certificate.extensions.get_extension_for_class(cryptography.x509.BasicConstraints) + assert stored_extension.value == basic_constraints + assert stored_extension.critical is True + + stored_extension = certificate.extensions.get_extension_for_class(cryptography.x509.OCSPNoCheck) + assert stored_extension.critical is False + assert isinstance(stored_extension.value, cryptography.x509.OCSPNoCheck) + + +def test_issue_certificate_sets_no_extensions_if_none_are_passed(): + dn = gimmecert.crypto.get_dn('My test 1') + private_key = gimmecert.crypto.generate_private_key() + not_before, not_after = gimmecert.crypto.get_validity_range() + + certificate1 = gimmecert.crypto.issue_certificate(dn, dn, private_key, private_key.public_key(), not_before, not_after, None) + certificate2 = gimmecert.crypto.issue_certificate(dn, dn, private_key, private_key.public_key(), not_before, not_after, []) + + assert len(certificate1.extensions) == 0 + assert len(certificate2.extensions) == 0 + + +def test_generate_ca_hierarchy_produces_certificates_with_ca_basic_constraints(): + base_name = 'My test' + depth = 3 + + hierarchy = gimmecert.crypto.generate_ca_hierarchy(base_name, depth) + + for _, certificate in hierarchy: + stored_extension = certificate.extensions.get_extension_for_class(cryptography.x509.BasicConstraints) + value, critical = stored_extension.value, stored_extension.critical + + assert isinstance(value, cryptography.x509.BasicConstraints) + assert critical is True + assert value.ca is True + assert value.path_length is None