Changeset - 11712c273c16
[Not reviewed]
0 7 0
Branko Majic (branko) - 7 years ago 2018-03-01 14:24:54
branko@majic.rs
GC-3: Implement option for specifying CA hierarchy depth during initialisation:

- Minor fix for option checks to make the more reliable (for short
flags).
- Added functional test for initialising a CA hierarchy with custom
depth.
- Added option for specifying the CA hierarchy depth (defaults to 1).
- Updated the init command to accept and process the CA hierarchy
depth option correctly.
- Updated function used for issuing certificates to accept list of
extensions to be added to certificate.
- Updated function used for generating the CA hierarchy to add the CA
basic constraint extension.
7 files changed with 304 insertions and 31 deletions:
0 comments (0 inline, 0 general)
functional_tests/test_init.py
Show inline comments
 
@@ -127,25 +127,25 @@ def test_initialisation_with_custom_base_name(tmpdir):
 
    # certificates coming from different envioronments where the
 
    # project directories have the same name. What he would like to do
 
    # is to be able to specify the base name explicitly, instead of
 
    # letting the tool pick it for him.
 

	
 
    # John decides to check the command help from CLI.
 
    stdout, _, _ = run_command('gimmecert', 'init', '-h')
 

	
 
    # Amongst the different options, he notices one in particular that
 
    # 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()
 

	
 
    # This time around he runs the command using the newly-found
 
    # option.
 
    stdout, stderr, exit_code = run_command('gimmecert', 'init', '--ca-base-name', 'My Project')
 

	
 
    # Command finishes execution with success, and he is informed that
 
    # his CA hierarchy has been initialised..
 
    assert exit_code == 0
 
    assert stderr == ""
 
@@ -156,12 +156,131 @@ def test_initialisation_with_custom_base_name(tmpdir):
 
    # commands to get the issuer and subject DN from generated
 
    # certificate.
 
    issuer_dn, _, _ = run_command('openssl', 'x509', '-noout', '-issuer', '-in', '.gimmecert/ca/level1.cert.pem')
 
    subject_dn, _, _ = run_command('openssl', 'x509', '-noout', '-subject', '-in', '.gimmecert/ca/level1.cert.pem')
 
    issuer_dn = issuer_dn.replace('issuer=', '', 1)
 
    subject_dn = subject_dn.replace('subject=', '', 1)
 

	
 
    # To his delight, both the issuer and subject DN are identical,
 
    # and now they are based on his custom-provided name instead of
 
    # 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
gimmecert/cli.py
Show inline comments
 
@@ -35,34 +35,36 @@ Examples:
 
    mkdir myproject/
 
    cd myproject/
 

	
 
    # Initialise the local CA hierarchy and all the necessary directories.
 
    gimmecert init
 
"""
 

	
 

	
 
@subcommand_parser
 
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.")
 

	
 
    subparser.set_defaults(func=init_wrapper)
 

	
 
    return subparser
 

	
 

	
 
@subcommand_parser
 
def setup_help_subcommand_parser(parser, subparsers):
 
    subparser = subparsers.add_parser('help', description='shows help')
gimmecert/commands.py
Show inline comments
 
@@ -15,55 +15,56 @@
 
# details.
 
#
 
# You should have received a copy of the GNU General Public License along with
 
# Gimmecert.  If not, see <http://www.gnu.org/licenses/>.
 
#
 

	
 
import os
 

	
 
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.
 

	
 
    :param project_directory: Path to directory where the structure should be initialised. Should be top-level project directory normally.
 
    :type project_directory: str
 

	
 
    :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
 
    """
 

	
 
    # 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
 

	
 
    # 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
gimmecert/crypto.py
Show inline comments
 
@@ -75,77 +75,104 @@ def get_validity_range():
 

	
 
    :returns: (not_before, not_after) -- Tuple defining the time range.
 
    :rtype: (datetime.datetime, datetime.datetime)
 
    """
 

	
 
    now = datetime.datetime.utcnow().replace(microsecond=0)
 
    not_before = now - datetime.timedelta(minutes=15)
 
    not_after = now + relativedelta(years=1)
 

	
 
    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.
 

	
 
    :param issuer_dn: Issuer DN to use in issued certificate.
 
    :type issuer_dn: cryptography.x509.Name
 

	
 
    :param subject_dn: Subject DN to use in issued certificate.
 
    :type subject_dn: cryptography.x509.Name
 

	
 
    :param signing_key: Private key belonging to entity associated with passed-in issuer_dn. Used for signing the certificate data.
 
    :type signing_key: cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey
 

	
 
    :param public_key: Public key belonging to entity associated with passed-in subject_dn. Used as part of certificate to denote its owner.
 
    :type cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey:
 

	
 
    :param not_before: Beginning of certifiate validity.
 
    :type datetime.datetime.:
 

	
 
    :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))
 
    builder = builder.not_valid_before(not_before)
 
    builder = builder.not_valid_after(not_after)
 
    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(),
 
        backend=cryptography.hazmat.backends.default_backend()
 
    )
 

	
 
    return certificate
 

	
 

	
 
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
 

	
 
    for level in range(1, depth+1):
 
        # Generate info for the new CA.
 
        dn = get_dn("%s Level %d" % (base_name, level))
 
        private_key = generate_private_key()
 

	
 
        # First certificate issued needs to be self-signed.
 
        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.
 
        issuer_dn, issuer_private_key = dn, private_key
 

	
 
    return hierarchy
tests/test_cli.py
Show inline comments
 
@@ -154,39 +154,55 @@ def test_setup_init_subcommand_returns_parser():
 
def test_setup_init_subcommand_sets_function_callback():
 
    parser = argparse.ArgumentParser()
 
    subparsers = parser.add_subparsers()
 

	
 
    subparser = gimmecert.cli.setup_init_subcommand_parser(parser, subparsers)
 

	
 
    assert callable(subparser.get_default('func'))
 

	
 

	
 
@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'])
 
def test_init_command_accepts_ca_base_name_option_long_form():
 

	
 
    gimmecert.cli.main()  # Should not raise
 

	
 

	
 
@mock.patch('sys.argv', ['gimmecert', 'init', '-b', 'My Project'])
 
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
tests/test_commands.py
Show inline comments
 
@@ -17,66 +17,121 @@
 
# You should have received a copy of the GNU General Public License along with
 
# Gimmecert.  If not, see <http://www.gnu.org/licenses/>.
 
#
 

	
 
import os
 

	
 
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()
 
    full_chain_after = tmpdir.join('.gimmecert', 'ca', 'chain-full.cert.pem').read()
 

	
 
    assert level1_private_key_before == level1_private_key_after
 
    assert level1_certificate_before == level1_certificate_after
 
    assert full_chain_before == full_chain_after
tests/test_crypto.py
Show inline comments
 
@@ -13,24 +13,25 @@
 
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 
# details.
 
#
 
# You should have received a copy of the GNU General Public License along with
 
# Gimmecert.  If not, see <http://www.gnu.org/licenses/>.
 
#
 

	
 

	
 
import datetime
 

	
 
import cryptography.hazmat.primitives.asymmetric.rsa
 
import cryptography.x509
 
from dateutil.relativedelta import relativedelta
 

	
 
import gimmecert.crypto
 

	
 
from freezegun import freeze_time
 

	
 

	
 
def test_generate_private_key_returns_private_key():
 
    private_key = gimmecert.crypto.generate_private_key()
 

	
 
    assert isinstance(private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey)
 

	
 
@@ -196,12 +197,64 @@ def test_generate_ca_hierarchy_cas_have_differing_keys():
 
def test_generate_ca_hierarchy_certificates_have_same_validity():
 
    base_name = 'My Project'
 
    depth = 3
 

	
 
    hierarchy = gimmecert.crypto.generate_ca_hierarchy(base_name, depth)
 

	
 
    _, level1_certificate = hierarchy[0]
 
    _, level2_certificate = hierarchy[1]
 
    _, level3_certificate = hierarchy[2]
 

	
 
    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
0 comments (0 inline, 0 general)