From 1d67951da5af8387fc7c047889b4243338f0c49a 2018-03-04 14:55:59 From: Branko Majic Date: 2018-03-04 14:55:59 Subject: [PATCH] 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. --- diff --git a/functional_tests/test_server.py b/functional_tests/test_server.py index 637afa0f83854608d7180145d9e859e34949dcd6..5d96cbe90d5d81236ca1b269d1998994ff87e497 100644 --- a/functional_tests/test_server.py +++ b/functional_tests/test_server.py @@ -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 diff --git a/gimmecert/cli.py b/gimmecert/cli.py index 4523eec035798705f4fe27036e83dac2899574d8..c1748a3dff6c76179992cdce2763e9a54e98ade6 100644 --- a/gimmecert/cli.py +++ b/gimmecert/cli.py @@ -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) diff --git a/gimmecert/commands.py b/gimmecert/commands.py index ce42f071e3491661edbf9fb92509720c1e8b8975..80c9e378d6cbf2b77b7b52f74ca035c32fc01ea6 100644 --- a/gimmecert/commands.py +++ b/gimmecert/commands.py @@ -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) diff --git a/gimmecert/crypto.py b/gimmecert/crypto.py index 79b9671adb29756300f87bf30b7ca398d357142a..057f891fc273ce3c76f1f90aff87762c53939050 100644 --- a/gimmecert/crypto.py +++ b/gimmecert/crypto.py @@ -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: diff --git a/tests/test_cli.py b/tests/test_cli.py index da1a706e845f6a44d48d65f235c679ca8221b6e4..ac5ea30942356632b3f9d7e994f33bc62f26e622 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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']) diff --git a/tests/test_commands.py b/tests/test_commands.py index acc82365d4d1bc98bb4fa3e434293052ae774cb4..f895f04a466baf128332ab74549aa9744e619f2a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -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) diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 1fb8ed13477fcb1838b3c973dd4beb84c57435bd..e54b478995fdec94613c6fd3ac2f75d5efe0657e 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -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