Changeset - 1d67951da5af
[Not reviewed]
0 7 0
Branko Majic (branko) - 6 years ago 2018-03-04 14:55:59
branko@majic.rs
GC-15: Implemented functionality for including extra DNS names in server certificates:

- Added functional test covering the new scenario.
- Updated invocations of relevant commands in existing code to pass-in
the list of extra DNS names where appropriate.
- Updated server command and high-level function for issuing server
certificates to accept list of additional DNS subject alternative
names to include in certificate.
- Fixed existing unit tests.
- Added additional unit tests that cover the new function.
7 files changed with 86 insertions and 12 deletions:
0 comments (0 inline, 0 general)
functional_tests/test_server.py
Show inline comments
 
@@ -135,3 +135,33 @@ def test_server_command_issues_server_certificate(tmpdir):
 

	
 
    # He is happy to see that verification succeeds.
 
    assert error_code == 0
 

	
 

	
 
def test_server_command_issues_server_certificate_with_additional_subject_alternative_names(tmpdir):
 
    # John wants to issue a server certificate that will include a
 
    # number of additional DNS subject alternative names. He switches
 
    # to his project directory, and initialises the CA hierarchy
 
    # there.
 
    tmpdir.chdir()
 
    run_command("gimmecert", "init")
 

	
 
    # He then runs command for issuing a server certificate, providing
 
    # additional DNS subject alternative names..
 
    stdout, stderr, exit_code = run_command('gimmecert', 'server', 'myserver', 'myserver.local', 'myserver.example.com')
 

	
 
    # The command finishes without any errors being reported.
 
    assert stderr == ""
 
    assert exit_code == 0
 

	
 
    # John then a look at generated certificate file.
 
    stdout, stderr, exit_code = run_command('openssl', 'x509', '-noout', '-text', '-in', '.gimmecert/server/myserver.cert.pem')
 

	
 
    # No errors are reported, and he notices that the provided subject
 
    # alternative names have been included in the certificate in
 
    # addition to default one based on the server entity name.
 
    assert exit_code == 0
 
    assert stderr == ""
 

	
 
    assert "DNS:myserver," in stdout
 
    assert "DNS:myserver.local," in stdout
 
    assert "DNS:myserver.example.com\n" in stdout
gimmecert/cli.py
Show inline comments
 
@@ -87,7 +87,7 @@ def setup_server_subcommand_parser(parser, subparsers):
 
    def server_wrapper(args):
 
        project_directory = os.getcwd()
 

	
 
        status, message = server(project_directory, args.entity_name)
 
        status, message = server(project_directory, args.entity_name, args.dns_name)
 

	
 
        if status is False:
 
            print(message, file=sys.stderr)
gimmecert/commands.py
Show inline comments
 
@@ -70,7 +70,7 @@ def init(project_directory, ca_base_name, ca_hierarchy_depth):
 
    return True
 

	
 

	
 
def server(project_directory, entity_name):
 
def server(project_directory, entity_name, extra_dns_names):
 
    """
 
    Generates a server private key and issues a server certificate
 
    using the CA hierarchy initialised within the specified directory.
 
@@ -81,6 +81,9 @@ def server(project_directory, entity_name):
 
    :param entity_name: Name of the server entity. Name will be used in subject DN and DNS subject alternative name.
 
    :type entity_name: str
 

	
 
    :param extra_dns_names: List of additional DNS names to include in the subject alternative name.
 
    :type extra_dns_names: list[str]
 

	
 
    :returns: Tuple consisting out of status and message to show to user.
 
    :rtype: (bool, str)
 
    """
 
@@ -99,7 +102,7 @@ def server(project_directory, entity_name):
 
    ca_hierarchy = gimmecert.storage.read_ca_hierarchy(os.path.join(project_directory, '.gimmecert', 'ca'))
 
    issuer_private_key, issuer_certificate = ca_hierarchy[-1]
 
    private_key = gimmecert.crypto.generate_private_key()
 
    certificate = gimmecert.crypto.issue_server_certificate(entity_name, private_key.public_key(), issuer_private_key, issuer_certificate)
 
    certificate = gimmecert.crypto.issue_server_certificate(entity_name, private_key.public_key(), issuer_private_key, issuer_certificate, extra_dns_names)
 

	
 
    gimmecert.storage.write_private_key(private_key, private_key_path)
 
    gimmecert.storage.write_certificate(certificate, certificate_path)
gimmecert/crypto.py
Show inline comments
 
@@ -178,7 +178,7 @@ def generate_ca_hierarchy(base_name, depth):
 
    return hierarchy
 

	
 

	
 
def issue_server_certificate(name, public_key, issuer_private_key, issuer_certificate):
 
def issue_server_certificate(name, public_key, issuer_private_key, issuer_certificate, extra_dns_names=None):
 
    """
 
    Issues a server certificate. The resulting certificate will use
 
    the passed-in name for subject DN, as well as DNS subject
 
@@ -202,10 +202,18 @@ def issue_server_certificate(name, public_key, issuer_private_key, issuer_certif
 
    :param issuer_certificate: Certificate of certificate issuer. Naming and validity constraints will be applied based on its content.
 
    :type issuer_certificate: cryptography.x509.Certificate
 

	
 
    :param extra_dns_names: Additional DNS names to include in subject alternative name. Set to None (default) to not include anything.
 
    :type extra_dns_names: list[str] or None
 

	
 
    :returns: Server certificate issued by designated issuer.
 
    :rtype: cryptography.x509.Certificate
 
    """
 

	
 
    dns_names = [name]
 

	
 
    if extra_dns_names is not None:
 
        dns_names.extend(extra_dns_names)
 

	
 
    dn = get_dn(name)
 
    not_before, not_after = get_validity_range()
 
    extensions = [
 
@@ -224,7 +232,7 @@ def issue_server_certificate(name, public_key, issuer_private_key, issuer_certif
 
            ), True
 
        ),
 
        (cryptography.x509.ExtendedKeyUsage([cryptography.x509.oid.ExtendedKeyUsageOID.SERVER_AUTH]), True),
 
        (cryptography.x509.SubjectAlternativeName([cryptography.x509.DNSName('myserver')]), False)
 
        (cryptography.x509.SubjectAlternativeName([cryptography.x509.DNSName(dns_name) for dns_name in dns_names]), False)
 
    ]
 

	
 
    if not_before < issuer_certificate.not_valid_before:
tests/test_cli.py
Show inline comments
 
@@ -269,14 +269,26 @@ def test_setup_server_subcommand_sets_function_callback():
 

	
 
@mock.patch('sys.argv', ['gimmecert', 'server', 'myserver'])
 
@mock.patch('gimmecert.cli.server')
 
def test_server_command_invoked_with_correct_parameters(mock_server, tmpdir):
 
def test_server_command_invoked_with_correct_parameters_without_extra_dns_names(mock_server, tmpdir):
 
    mock_server.return_value = True, "Bogus"
 

	
 
    tmpdir.chdir()
 

	
 
    gimmecert.cli.main()
 

	
 
    mock_server.assert_called_once_with(tmpdir.strpath, 'myserver')
 
    mock_server.assert_called_once_with(tmpdir.strpath, 'myserver', [])
 

	
 

	
 
@mock.patch('sys.argv', ['gimmecert', 'server', 'myserver', 'service.local', 'service.example.com'])
 
@mock.patch('gimmecert.cli.server')
 
def test_server_command_invoked_with_correct_parameters_with_extra_dns_names(mock_server, tmpdir):
 
    mock_server.return_value = True, "Bogus"
 

	
 
    tmpdir.chdir()
 

	
 
    gimmecert.cli.main()
 

	
 
    mock_server.assert_called_once_with(tmpdir.strpath, 'myserver', ['service.local', 'service.example.com'])
 

	
 

	
 
@mock.patch('sys.argv', ['gimmecert', 'server', 'myserver'])
tests/test_commands.py
Show inline comments
 
@@ -142,7 +142,7 @@ def test_init_does_not_overwrite_artifcats_if_already_initialised(tmpdir):
 
def test_server_returns_status_and_message(tmpdir):
 
    tmpdir.chdir()
 

	
 
    status, message = gimmecert.commands.server(tmpdir.strpath, 'myserver')
 
    status, message = gimmecert.commands.server(tmpdir.strpath, 'myserver', None)
 

	
 
    assert isinstance(status, bool)
 
    assert isinstance(message, str)
 
@@ -151,7 +151,7 @@ def test_server_returns_status_and_message(tmpdir):
 
def test_server_reports_error_if_directory_is_not_initialised(tmpdir):
 
    tmpdir.chdir()
 

	
 
    status, message = gimmecert.commands.server(tmpdir.strpath, 'myserver')
 
    status, message = gimmecert.commands.server(tmpdir.strpath, 'myserver', None)
 

	
 
    assert status is False
 
    assert "must be initialised" in message
 
@@ -163,7 +163,7 @@ def test_server_reports_paths_to_generated_artifacts(tmpdir):
 
    tmpdir.chdir()
 
    gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth)
 

	
 
    status, message = gimmecert.commands.server(tmpdir.strpath, 'myserver')
 
    status, message = gimmecert.commands.server(tmpdir.strpath, 'myserver', None)
 

	
 
    assert status is True
 
    assert ".gimmecert/server/myserver.key.pem" in message
 
@@ -177,7 +177,7 @@ def test_server_outputs_private_key_to_file(tmpdir):
 
    tmpdir.chdir()
 
    gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth)
 

	
 
    gimmecert.commands.server(tmpdir.strpath, 'myserver')
 
    gimmecert.commands.server(tmpdir.strpath, 'myserver', None)
 

	
 
    assert private_key_file.check(file=1)
 

	
 
@@ -194,7 +194,7 @@ def test_server_outputs_certificate_to_file(tmpdir):
 
    tmpdir.chdir()
 
    gimmecert.commands.init(tmpdir.strpath, tmpdir.basename, depth)
 

	
 
    gimmecert.commands.server(tmpdir.strpath, 'myserver')
 
    gimmecert.commands.server(tmpdir.strpath, 'myserver', None)
 

	
 
    assert certificate_file.check(file=1)
 

	
tests/test_crypto.py
Show inline comments
 
@@ -377,3 +377,24 @@ def test_issue_server_certificate_not_after_does_not_exceed_ca_validity():
 
        certificate1 = gimmecert.crypto.issue_server_certificate('myserver', private_key.public_key(), issuer_private_key, issuer_certificate)
 

	
 
    assert certificate1.not_valid_after == issuer_certificate.not_valid_after
 

	
 

	
 
def test_issue_server_certificate_incorporates_additional_dns_subject_alternative_names():
 
    ca_hierarchy = gimmecert.crypto.generate_ca_hierarchy('My Project', 1)
 
    issuer_private_key, issuer_certificate = ca_hierarchy[0]
 

	
 
    private_key = gimmecert.crypto.generate_private_key()
 

	
 
    expected_subject_alternative_name = cryptography.x509.SubjectAlternativeName(
 
        [
 
            cryptography.x509.DNSName('myserver'),
 
            cryptography.x509.DNSName('service.local'),
 
            cryptography.x509.DNSName('service.example.com')
 
        ]
 
    )
 

	
 
    extra_dns_names = ['service.local', 'service.example.com']
 
    certificate = gimmecert.crypto.issue_server_certificate('myserver', private_key.public_key(), issuer_private_key, issuer_certificate, extra_dns_names)
 

	
 
    assert certificate.extensions.get_extension_for_class(cryptography.x509.SubjectAlternativeName).critical is False
 
    assert certificate.extensions.get_extension_for_class(cryptography.x509.SubjectAlternativeName).value == expected_subject_alternative_name
0 comments (0 inline, 0 general)