From c3e3f7ebf69d78d3d17a110f77cf55b02a78464d 2020-07-07 16:05:14 From: Branko Majic Date: 2020-07-07 16:05:14 Subject: [PATCH] GC-37: Added ECDSA support for issuing server certificates via server command: - Added functional test. - Added unit tests. - Updated existing functional test that checks for avertising of curve support for key specification in the init command to be a bit less fragile in case the output gets broken-up into different lines in a slightly different location. - Implement ability to get public key specification out of ECDSA public key. - Expose ECDSA key specification in the server command. - Updated inline documentation. --- diff --git a/functional_tests/test_key_specification.py b/functional_tests/test_key_specification.py index a3f6992c96a44081f9be9854204a78d46e727aa1..a96aaec02ac76b2da81939c1116234b95309e4eb 100644 --- a/functional_tests/test_key_specification.py +++ b/functional_tests/test_key_specification.py @@ -346,7 +346,7 @@ def test_initialisation_with_ecdsa_key_specification(tmpdir): assert "ecdsa:CURVE_NAME" in stdout # John can see a number of curves listed as supported. - assert "Supported curves: " in stdout + assert "curves: " in stdout assert "secp192r1" in stdout assert "secp224r1" in stdout assert "secp256k1" in stdout @@ -390,3 +390,85 @@ def test_initialisation_with_ecdsa_key_specification(tmpdir): assert "Signature Algorithm: ecdsa-with-SHA256" in stdout assert "Public Key Algorithm: id-ecPublicKey" in stdout assert "ASN1 OID: prime256v1" in stdout + + +def test_server_command_default_key_specification_with_ecdsa(tmpdir): + # John is setting-up a project to test some functionality + # revolving around X.509 certificates. He has used RSA extensively + # before, but now he wants to switch to using ECDSA private keys + # instead. + + # He switches to his project directory, and initialises the CA + # hierarchy, requesting that secp256r1 ECDSA keys should be used. + tmpdir.chdir() + run_command("gimmecert", "init", "--key-specification", "ecdsa:secp384r1") + + # John issues a server certificate. + stdout, stderr, exit_code = run_command('gimmecert', 'server', 'myserver1') + + # 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', 'ec', '-noout', '-text', '-in', '.gimmecert/server/myserver1.key.pem') + + # And indeed, the generated private key uses the same algorithm as + # the one he specified for the CA hierarchy. + assert "ASN1 OID: secp384r1" in stdout + + +def test_server_command_key_specification_with_ecdsa(tmpdir): + # John is setting-up a project where he needs to test performance + # when using different ECDSA private key sizes. + + # He switches to his project directory, and initialises the CA + # hierarchy, requesting that secp192r1 ECDSA keys should be used. + tmpdir.chdir() + run_command("gimmecert", "init", "--key-specification", "ecdsa:secp192r1") + + # Very soon he realizes that he needs to test performance using + # different elliptic curve algorithms for proper comparison. He + # starts off by having a look at the help for the server command + # to see if there is an option that will satisfy his needs. + stdout, stderr, exit_code = run_command("gimmecert", "server", "-h") + + # John notices the option for passing-in a key specification, and + # that he can request ECDSA keys to be used with a specific curve. + assert " --key-specification" in stdout + assert " -k" in stdout + assert "rsa:BIT_LENGTH" in stdout + assert "ecdsa:CURVE_NAME" in stdout + + # John can see a number of curves listed as supported. + assert "curves: " in stdout + assert "secp192r1" in stdout + assert "secp224r1" in stdout + assert "secp256k1" in stdout + assert "secp256r1" in stdout + assert "secp384r1" in stdout + assert "secp521r1" in stdout + + # John goes ahead and tries to issue a server certificate using + # key specification option. + stdout, stderr, exit_code = run_command("gimmecert", "server", "--key-specification", "ecdsa:secp224r11", "myserver1") + + # Unfortunately, the command fails due to John's typo. + assert exit_code != 0 + assert "invalid key_specification" in stderr + + # John tries again, fixing his typo. + stdout, stderr, exit_code = run_command("gimmecert", "server", "--key-specification", "ecdsa:secp224r1", "myserver1") + + # This time around he succeeds. + assert exit_code == 0 + assert stderr == "" + + # He runs a command to see details about the generated private + # key. + stdout, _, _ = run_command('openssl', 'ec', '-noout', '-text', '-in', '.gimmecert/server/myserver1.key.pem') + + # He nods with his head, observing that the generated private key + # uses the same algorithm as he has specified. + assert "ASN1 OID: secp224r1" in stdout diff --git a/gimmecert/cli.py b/gimmecert/cli.py index 3fc5ab4fd4fc4c927b2bf865c38c7ba9243c6a46..3907a8c2619d34cdb0f9727139ddd29c3ada6bd4 100644 --- a/gimmecert/cli.py +++ b/gimmecert/cli.py @@ -164,7 +164,9 @@ def setup_server_subcommand_parser(parser, subparsers): certificate signing request (CSR) instead. Use dash (-) to read from standard input. Only the public key is taken from the CSR.''') subparser.add_argument('--key-specification', '-k', type=key_specification, help='''Specification/parameters to use for private key generation. \ - For RSA keys, use format rsa:BIT_LENGTH. Default is to use same algorithm/parameters as used by CA hierarchy.''', default=None) + For RSA keys, use format rsa:BIT_LENGTH. For ECDSA keys, use format ecdsa:CURVE_NAME. \ + Supported curves: secp192r1, secp224r1, secp256k1, secp256r1, secp384r1, secp521r1. \ + Default is rsa:2048. Default is to use same algorithm/parameters as used by CA hierarchy.''', default=None) def server_wrapper(args): project_directory = os.getcwd() diff --git a/gimmecert/crypto.py b/gimmecert/crypto.py index 6fab559d2299cd4c928a635e92130c093a3a8576..88862dfb76678da44f42f488fd42904ad2308830 100644 --- a/gimmecert/crypto.py +++ b/gimmecert/crypto.py @@ -460,12 +460,14 @@ def key_specification_from_public_key(public_key): :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) + :rtype: tuple(str, int or cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve) :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 + elif isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey): + return "ecdsa", type(public_key.curve) raise ValueError("Unsupported public key instance passed-in: \"%s\" (%s)" % (str(public_key), type(public_key))) diff --git a/tests/test_cli.py b/tests/test_cli.py index 6f7172dee1009827cc38d8800f42b44d1c79797c..7f04c15acc8fd7af831a9cefbfcd0e8597cc3794 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -258,10 +258,24 @@ VALID_CLI_INVOCATIONS = [ ("gimmecert.cli.server", ["gimmecert", "server", "--csr", "myserver.csr.pem", "myserver"]), ("gimmecert.cli.server", ["gimmecert", "server", "-c", "myserver.csr.pem", "myserver"]), - # server, key specification long and short option + # server, RSA key specification long and short option ("gimmecert.cli.server", ["gimmecert", "server", "--key-specification", "rsa:4096", "myserver"]), ("gimmecert.cli.server", ["gimmecert", "server", "-k", "rsa:1024", "myserver"]), + # server, ECDSA key specification long and short option + ("gimmecert.cli.server", ["gimmecert", "server", "--key-specification", "ecdsa:secp192r1", "myserver"]), + ("gimmecert.cli.server", ["gimmecert", "server", "-k", "ecdsa:secp192r1", "myserver"]), + ("gimmecert.cli.server", ["gimmecert", "server", "--key-specification", "ecdsa:secp224r1", "myserver"]), + ("gimmecert.cli.server", ["gimmecert", "server", "-k", "ecdsa:secp224r1", "myserver"]), + ("gimmecert.cli.server", ["gimmecert", "server", "--key-specification", "ecdsa:secp256k1", "myserver"]), + ("gimmecert.cli.server", ["gimmecert", "server", "-k", "ecdsa:secp256k1", "myserver"]), + ("gimmecert.cli.server", ["gimmecert", "server", "--key-specification", "ecdsa:secp256r1", "myserver"]), + ("gimmecert.cli.server", ["gimmecert", "server", "-k", "ecdsa:secp256r1", "myserver"]), + ("gimmecert.cli.server", ["gimmecert", "server", "--key-specification", "ecdsa:secp384r1", "myserver"]), + ("gimmecert.cli.server", ["gimmecert", "server", "-k", "ecdsa:secp384r1", "myserver"]), + ("gimmecert.cli.server", ["gimmecert", "server", "--key-specification", "ecdsa:secp521r1", "myserver"]), + ("gimmecert.cli.server", ["gimmecert", "server", "-k", "ecdsa:secp521r1", "myserver"]), + # client, no options ("gimmecert.cli.client", ["gimmecert", "client", "myclient"]), @@ -354,6 +368,7 @@ INVALID_CLI_INVOCATIONS = [ ("gimmecert.cli.server", ["gimmecert", "server", "-k", "rsa", "myserver"]), ("gimmecert.cli.server", ["gimmecert", "server", "-k", "rsa:not_a_number", "myserver"]), ("gimmecert.cli.server", ["gimmecert", "server", "-k", "unsupported:algorithm", "myserver"]), + ("gimmecert.cli.server", ["gimmecert", "server", "-k", "ecdsa:unsupported_curve", "myserver"]), # client, invalid key specification ("gimmecert.cli.client", ["gimmecert", "client", "-k", "rsa", "myclient"]), diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 16e69e9e6393467c82ba526bea899614f7ef8c8b..51f0087255c68a5e53ab09decd060d95de285d07 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -21,6 +21,7 @@ import datetime +import cryptography.hazmat.primitives.asymmetric.ec import cryptography.hazmat.primitives.asymmetric.rsa import cryptography.x509 from dateutil.relativedelta import relativedelta @@ -680,7 +681,13 @@ def test_generate_ca_hierarchy_uses_correct_rsa_bit_size(key_generator, expected @pytest.mark.parametrize("specification", [ ("rsa", 1024), - ("rsa", 2048) + ("rsa", 2048), + ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP192R1), + ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP224R1), + ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP256K1), + ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP256R1), + ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP384R1), + ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP521R1), ]) def test_key_specification_from_public_key_returns_correct_algorithm_and_parameters(specification): key_generator = gimmecert.crypto.KeyGenerator(specification[0], specification[1])