From d52b62b9a9db8380bd91291070b848bdf9de8ef2 2020-06-07 22:18:20 From: Branko Majic Date: 2020-06-07 22:18:20 Subject: [PATCH] GC-37: Use CA hierarchy issuer's key size when generating RSA private keys. --- diff --git a/functional_tests/test_server.py b/functional_tests/test_server.py index 8e95f4bb5eaccdb92b20871c618e8fca9d1c1a4e..a3ad83358a6dd14103c513c4e183238b963a3f4b 100644 --- a/functional_tests/test_server.py +++ b/functional_tests/test_server.py @@ -202,3 +202,37 @@ def test_server_command_does_not_overwrite_existing_artifacts(tmpdir): # unchanged. assert tmpdir.join(".gimmecert", "server", "myserver.key.pem").read() == private_key assert tmpdir.join(".gimmecert", "server", "myserver.cert.pem").read() == certificate + + +def test_server_command_uses_same_rsa_key_size_as_ca_hierarchy(tmpdir): + # John is setting-up a quick and dirty project to test some + # functionality revolving around X.509 certificates. Since he does + # not care much about the strength of private keys for it, he + # wants to use 1024-bit RSA keys. + + # He switches to his project directory, and initialises the CA + # hierarchy, requesting that 1024-bit RSA keys should be used. + tmpdir.chdir() + run_command("gimmecert", "init", "--key-specification", "rsa:1024") + + # John issues a server certificates. + stdout, stderr, exit_code = run_command('gimmecert', 'server', 'myserver') + + # John observes that the process was completed successfully. + assert exit_code == 0 + assert stderr == "" + + # He runs a command to see details about the generated private + # key. + stdout, _, _ = run_command('openssl', 'rsa', '-noout', '-text', '-in', '.gimmecert/server/myserver.key.pem') + + # And indeed, the generated private key uses the same size as the + # one he specified for the CA hierarchy. + assert "Private-Key: (1024 bit)" in stdout + + # He then has a look at the certificate. + stdout, _, _ = run_command('openssl', 'x509', '-noout', '-text', '-in', '.gimmecert/server/myserver.cert.pem') + + # Likewise with the private key, the certificate is also using the + # 1024-bit RSA key. + assert "Public-Key: (1024 bit)" in stdout diff --git a/gimmecert/commands.py b/gimmecert/commands.py index 495274720ffc054528022ceaaf2646f6e5b623b9..82c406cf50586e78fdaac025df710fc46d6800fe 100644 --- a/gimmecert/commands.py +++ b/gimmecert/commands.py @@ -160,6 +160,10 @@ def server(stdout, stderr, project_directory, entity_name, extra_dns_names, cust print("Refusing to overwrite existing data. Certificate has already been issued for server %s." % entity_name, file=stderr) return ExitCode.ERROR_CERTIFICATE_ALREADY_ISSUED + # Grab the issuing CA private key and certificate. + ca_hierarchy = gimmecert.storage.read_ca_hierarchy(os.path.join(project_directory, '.gimmecert', 'ca')) + issuer_private_key, issuer_certificate = ca_hierarchy[-1] + # Grab the private key or CSR, and extract public key. if custom_csr_path == "-": csr_pem = gimmecert.utils.read_input(sys.stdin, stderr, "Please enter the CSR") @@ -171,14 +175,12 @@ def server(stdout, stderr, project_directory, entity_name, extra_dns_names, cust public_key = csr.public_key() private_key = None else: - private_key = gimmecert.crypto.generate_private_key() + key_specification = gimmecert.crypto.key_specification_from_public_key(issuer_private_key.public_key()) + key_generator = gimmecert.crypto.KeyGenerator(key_specification[0], key_specification[1]) + private_key = key_generator() public_key = private_key.public_key() csr = None - # Grab the issuing CA private key and certificate. - ca_hierarchy = gimmecert.storage.read_ca_hierarchy(os.path.join(project_directory, '.gimmecert', 'ca')) - issuer_private_key, issuer_certificate = ca_hierarchy[-1] - # Issue the certificate. certificate = gimmecert.crypto.issue_server_certificate(entity_name, public_key, issuer_private_key, issuer_certificate, extra_dns_names) diff --git a/gimmecert/crypto.py b/gimmecert/crypto.py index 71343f81e7ad58aafcf0bc4d492dabad800a0530..319362db4858c9104e10b11c3066dc41a5238dcc 100644 --- a/gimmecert/crypto.py +++ b/gimmecert/crypto.py @@ -432,3 +432,24 @@ def generate_csr(name, private_key): ) return csr + + +def key_specification_from_public_key(public_key): + """ + Derives key specification (algorithm and associated parameters) + from the passed-in public key. Key specification can be used for + generating the private keys via KeyGenerator instances. + + :param public_key: Public + :type public_key: cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey + + :returns: Key algorithm and parameter(s) for generating same type of keys as the passed-in public key. + :rtype: tuple(str, int) + + :raises ValueError: If algorithm/parameters could not be derived from the passed-in public key. + """ + + if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey): + return "rsa", public_key.key_size + + raise ValueError("Unsupported public key instance passed-in: \"%s\" (%s)" % (str(public_key), type(public_key))) diff --git a/tests/test_commands.py b/tests/test_commands.py index 2adcb34e0a42ffaadd56fe08675c77ddbf36fae4..11959686230cdb91c5d1073b82af7b6045ba13e4 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1433,3 +1433,15 @@ def test_renew_client_reads_csr_from_stdin(mock_read_input, sample_project_direc assert stored_csr_public_numbers == custom_csr_public_numbers assert certificate_public_numbers == custom_csr_public_numbers assert certificate.subject != key_with_csr.csr.subject + + +def test_server_uses_same_private_key_algorithm_and_parameters_as_issuer_when_generating_private_key(tmpdir): + + private_key_file = tmpdir.join('.gimmecert', 'server', 'myserver.key.pem') + + gimmecert.commands.init(io.StringIO(), io.StringIO(), tmpdir.strpath, tmpdir.basename, 1, ("rsa", 1024)) + gimmecert.commands.server(io.StringIO(), io.StringIO(), tmpdir.strpath, 'myserver', None, None) + + private_key = gimmecert.storage.read_private_key(private_key_file.strpath) + + assert private_key.key_size == 1024 diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 28eeeaec21772e09c26a8d5c18ea9e1aed43bd72..bffda8631c860a618627f28795e9ae9ea1cc3b1b 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -652,3 +652,25 @@ def test_generate_ca_hierarchy_uses_correct_rsa_bit_size(key_generator, expected for private_key, _ in hierarchy: assert private_key.key_size == expected_bit_size + + +@pytest.mark.parametrize("specification", [ + ("rsa", 1024), + ("rsa", 2048) +]) +def test_key_specification_from_public_key_returns_correct_algorithm_and_parameters(specification): + key_generator = gimmecert.crypto.KeyGenerator(specification[0], specification[1]) + public_key = key_generator().public_key() + + algorithm, parameters = gimmecert.crypto.key_specification_from_public_key(public_key) + + assert (algorithm, parameters) == specification + + +def test_key_specification_raises_exception_for_invalid_public_key(): + public_key = "not_a_public_key_instance" + + with pytest.raises(ValueError) as e_info: + gimmecert.crypto.key_specification_from_public_key(public_key) + + assert str(e_info.value) == "Unsupported public key instance passed-in: \"not_a_public_key_instance\" ()"