diff --git a/functional_tests/test_key_specification.py b/functional_tests/test_key_specification.py index 43f143575ccba66791ebd4a40e593ee02d022f44..69ebf588237acf89a23c87370a6b4c266a9dd08e 100644 --- a/functional_tests/test_key_specification.py +++ b/functional_tests/test_key_specification.py @@ -320,3 +320,67 @@ def test_renew_command_key_specification(tmpdir): # checks-out for it as well. stdout, _, _ = run_command('openssl', 'rsa', '-noout', '-text', '-in', '.gimmecert/client/myclient2.key.pem') assert "Private-Key: (3072 bit)" in stdout + + +def test_initialisation_with_ecdsa_key_specification(tmpdir): + # John is looking into using ECDSA keys in his latest project. He + # is already aware that Gimmecert supports use of RSA keys, but he + # hasn't tried using it with ECDSA yet. + + # He checks the help for the init command first to see if he can + # somehow request ECDSA keys to be used instead of RSA. + stdout, _, _ = run_command('gimmecert', 'init', '-h') + + # John noticies there is an option to provide a custom key + # specification to the tool, 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 "Supported 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 switches to his project directory. + tmpdir.chdir() + + # After a short deliberation, he opts to use the secp256r1 curve, + # and initialises his CA hierarchy. + stdout, stderr, exit_code = run_command('gimmecert', 'init', '--key-specification', 'ecdsa:secp256r1') + + # Command finishes execution with success, and John notices that + # the tool has informed him of what the private key algorithm is + # in use for the CA hierarchy. + assert exit_code == 0 + assert stderr == "" + assert "CA hierarchy initialised using secp256r1 ECDSA keys." in stdout + + # John goes ahead and inspects the CA private key to ensure his + # private key specification has been accepted. + stdout, stderr, exit_code = run_command('openssl', 'ec', '-noout', '-text', '-in', '.gimmecert/ca/level1.key.pem') + + assert exit_code == 0 + assert stderr == "read EC key\n" # OpenSSL print this out to stderr no matter what. + + # He notices that although he requested secp256r1, the output from + # OpenSSL tool uses its older name from RFC3279 - + # prime256v1. However, he understands this is just an alternate + # name for the curve. + assert "ASN1 OID: prime256v1" in stdout + + # John also does a quick check on the generated certificate's + # signing and public key algorithm. + stdout, stderr, exit_code = run_command('openssl', 'x509', '-noout', '-text', '-in', '.gimmecert/ca/level1.cert.pem') + + assert exit_code == 0 + assert stderr == "" + assert "Signature Algorithm: ecdsa-with-SHA256" in stdout + assert "Public Key Algorithm: id-ecPublicKey" in stdout + assert "ASN1 OID: prime256v1" in stdout diff --git a/gimmecert/cli.py b/gimmecert/cli.py index a0ccbfdaddc45d312fcb8fd8068dce524d435d00..3fc5ab4fd4fc4c927b2bf865c38c7ba9243c6a46 100644 --- a/gimmecert/cli.py +++ b/gimmecert/cli.py @@ -23,6 +23,8 @@ import argparse import os import sys +from cryptography.hazmat.primitives.asymmetric import ec + from .decorators import subcommand_parser, get_subcommand_parser_setup_functions from .commands import client, help_, init, renew, server, status, usage, ExitCode @@ -82,24 +84,37 @@ def key_specification(specification): Verifies and parses the passed-in key specification. This is a small utility function for use with the Python argument parser. - :param specification: Key specification. Currently supported formats are: "rsa:KEY_SIZE". + :param specification: Key specification. Currently supported formats are: "rsa:KEY_SIZE" and "ecdsa:CURVE_NAME". :type specification: str :returns: Parsed key algorithm and parameter(s) for the algorithm. For RSA, parameter is the RSA key size. - :rtype: tuple(str, int) + :rtype: tuple(str, int or cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve) :raises ValueError: If passed-in specification is invalid. """ + available_curves = { + "secp192r1": ec.SECP192R1, + "secp224r1": ec.SECP224R1, + "secp256k1": ec.SECP256K1, + "secp256r1": ec.SECP256R1, + "secp384r1": ec.SECP384R1, + "secp521r1": ec.SECP521R1, + } + try: algorithm, parameters = specification.split(":", 2) + algorithm = algorithm.lower() if algorithm == "rsa": parameters = int(parameters) + elif algorithm == "ecdsa": + parameters = str(parameters).lower() + parameters = available_curves[parameters] else: raise ValueError() - except ValueError: + except (ValueError, KeyError): raise ValueError("Invalid key specification: '%s'" % specification) return algorithm, parameters @@ -112,7 +127,9 @@ def setup_init_subcommand_parser(parser, subparsers): subparser.add_argument('--ca-hierarchy-depth', '-d', type=int, help="Depth of CA hierarchy to generate. Default is 1", default=1) subparser.add_argument('--key-specification', '-k', type=key_specification, help='''Default specification/parameters to use for private key generation. \ - For RSA keys, use format rsa:BIT_LENGTH. Default is rsa:2048.''', default="rsa:2048") + 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="rsa:2048") def init_wrapper(args): project_directory = os.getcwd() diff --git a/gimmecert/crypto.py b/gimmecert/crypto.py index 319362db4858c9104e10b11c3066dc41a5238dcc..6fab559d2299cd4c928a635e92130c093a3a8576 100644 --- a/gimmecert/crypto.py +++ b/gimmecert/crypto.py @@ -40,11 +40,12 @@ class KeyGenerator: """ Initialises an instance. - :param algorithm: Algorithm to use. Supported algorithms: 'rsa'. + :param algorithm: Algorithm to use. Supported algorithms: 'rsa', 'ecdsa'. :type algorithm: str :param parameters: Parameters for generating the keys using the specified algorithm. For RSA keys this is key size. - :type parameters: int + For ECDSA, this is an instance of cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve. + :type parameters: int or cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve """ self._algorithm = algorithm @@ -59,24 +60,38 @@ class KeyGenerator: :rtype: str """ - return "%d-bit RSA" % self._parameters + if self._algorithm == "rsa": + + return "%d-bit RSA" % self._parameters + + elif self._algorithm == "ecdsa": + + return "%s ECDSA" % self._parameters.name def __call__(self): """ - Generates RSA private key. Key size is deterimened by instance's - key specification (passed-in during instance creation). + Generates private key. Key algorithm and parameters are + deterimened by instance's key specification (passed-in during + instance creation). - :returns: RSA private key. - :rtype: cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey + :returns: Private key. + :rtype: cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey or cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey """ - rsa_public_exponent = 65537 + if self._algorithm == "rsa": + + rsa_public_exponent = 65537 - private_key = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key( - public_exponent=rsa_public_exponent, - key_size=self._parameters, - backend=cryptography.hazmat.backends.default_backend() - ) + private_key = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key( + public_exponent=rsa_public_exponent, + key_size=self._parameters, + backend=cryptography.hazmat.backends.default_backend() + ) + else: + private_key = cryptography.hazmat.primitives.asymmetric.ec.generate_private_key( + curve=self._parameters, + backend=cryptography.hazmat.backends.default_backend() + ) return private_key @@ -204,7 +219,8 @@ def generate_ca_hierarchy(base_name, depth, key_generator): :type key_generator: callable[[], cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey] :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)] + :rtype: list[(cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey or + cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey, cryptography.x509.Certificate)] """ hierarchy = [] diff --git a/tests/test_cli.py b/tests/test_cli.py index 12992016ef5609cc5c6399d281d8471e7e7b6dc8..6f7172dee1009827cc38d8800f42b44d1c79797c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -25,6 +25,8 @@ import sys import gimmecert.cli import gimmecert.decorators +import cryptography.hazmat.primitives.asymmetric.ec + import pytest from unittest import mock @@ -224,10 +226,24 @@ VALID_CLI_INVOCATIONS = [ ("gimmecert.cli.init", ["gimmecert", "init", "--ca-hierarchy-depth", "3"]), ("gimmecert.cli.init", ["gimmecert", "init", "-d", "3"]), - # init, key specification long and short option + # init, RSA key specification long and short option ("gimmecert.cli.init", ["gimmecert", "init", "--key-specification", "rsa:4096"]), ("gimmecert.cli.init", ["gimmecert", "init", "-k", "rsa:4096"]), + # init, ECDSA key specification long and short option + ("gimmecert.cli.init", ["gimmecert", "init", "--key-specification", "ecdsa:secp192r1"]), + ("gimmecert.cli.init", ["gimmecert", "init", "-k", "ecdsa:secp192r1"]), + ("gimmecert.cli.init", ["gimmecert", "init", "--key-specification", "ecdsa:secp224r1"]), + ("gimmecert.cli.init", ["gimmecert", "init", "-k", "ecdsa:secp224r1"]), + ("gimmecert.cli.init", ["gimmecert", "init", "--key-specification", "ecdsa:secp256k1"]), + ("gimmecert.cli.init", ["gimmecert", "init", "-k", "ecdsa:secp256k1"]), + ("gimmecert.cli.init", ["gimmecert", "init", "--key-specification", "ecdsa:secp256r1"]), + ("gimmecert.cli.init", ["gimmecert", "init", "-k", "ecdsa:secp256r1"]), + ("gimmecert.cli.init", ["gimmecert", "init", "--key-specification", "ecdsa:secp384r1"]), + ("gimmecert.cli.init", ["gimmecert", "init", "-k", "ecdsa:secp384r1"]), + ("gimmecert.cli.init", ["gimmecert", "init", "--key-specification", "ecdsa:secp521r1"]), + ("gimmecert.cli.init", ["gimmecert", "init", "-k", "ecdsa:secp521r1"]), + # server, no options ("gimmecert.cli.server", ["gimmecert", "server", "myserver"]), @@ -330,7 +346,9 @@ INVALID_CLI_INVOCATIONS = [ # init, invalid key specification ("gimmecert.cli.init", ["gimmecert", "init", "-k", "rsa"]), ("gimmecert.cli.init", ["gimmecert", "init", "-k", "rsa:not_a_number"]), - ("gimmecert.cli.init", ["gimmecert", "init", "-k", "unsupported:algorithm"]), + ("gimmecert.cli.init", ["gimmecert", "init", "-k", "ecdsa"]), + ("gimmecert.cli.init", ["gimmecert", "init", "-k", "ecdsa:not_a_valid_curve"]), + ("gimmecert.cli.init", ["gimmecert", "init", "-k", "ecdsa:BrainpoolP256R1"]), # Not supported by Gimmecert in spite of being available in Cryptography. # server, invalid key specification ("gimmecert.cli.server", ["gimmecert", "server", "-k", "rsa", "myserver"]), @@ -739,6 +757,9 @@ def test_renew_command_fails_if_both_new_private_key_and_csr_options_are_specifi "rsa", "rsa:not_a_number", "unsupported:algorithm", + "ecdsa", + "ecdsa:not_a_valid_curve", + "ecdsa:BrainpoolP256R1", ]) def test_key_specification_raises_exception_for_invalid_specification(key_specification): @@ -752,6 +773,15 @@ def test_key_specification_raises_exception_for_invalid_specification(key_specif ("rsa:1024", ("rsa", 1024)), ("rsa:2048", ("rsa", 2048)), ("rsa:4096", ("rsa", 4096)), + ("RSA:4096", ("rsa", 4096)), # Should ignore case. + ("RSa:4096", ("rsa", 4096)), # Should ignore case. + ("ecdsa:secp192r1", ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP192R1)), + ("ecdsa:secp224r1", ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP224R1)), + ("ecdsa:secp256k1", ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP256K1)), + ("ecdsa:secp384r1", ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP384R1)), + ("ecdsa:secp521r1", ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP521R1)), + ("EcDSa:secp521r1", ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP521R1)), # Should ignore case. + ("EcDSa:sEcP521R1", ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP521R1)), # Should ignore case. ]) def test_key_specification_returns_algorithm_and_parameters_for_valid_specification(key_specification, expected_return_value): diff --git a/tests/test_crypto.py b/tests/test_crypto.py index bffda8631c860a618627f28795e9ae9ea1cc3b1b..16e69e9e6393467c82ba526bea899614f7ef8c8b 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -622,6 +622,12 @@ def test_generate_csr_returns_csr_with_passed_in_name(): ("rsa", 1024, "1024-bit RSA"), ("rsa", 2048, "2048-bit RSA"), ("rsa", 4096, "4096-bit RSA"), + ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP192R1, "secp192r1 ECDSA"), + ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP224R1, "secp224r1 ECDSA"), + ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP256K1, "secp256k1 ECDSA"), + ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP256R1, "secp256r1 ECDSA"), + ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP384R1, "secp384r1 ECDSA"), + ("ecdsa", cryptography.hazmat.primitives.asymmetric.ec.SECP521R1, "secp521r1 ECDSA"), ]) def test_KeyGenerator_string_representation(algorithm, parameters, string_representation): @@ -640,6 +646,24 @@ def test_KeyGenerator_instance_returns_rsa_private_key_of_correct_size(key_size) assert private_key.key_size == key_size +@pytest.mark.parametrize("curve", [ + cryptography.hazmat.primitives.asymmetric.ec.SECP192R1, + cryptography.hazmat.primitives.asymmetric.ec.SECP224R1, + cryptography.hazmat.primitives.asymmetric.ec.SECP256K1, + cryptography.hazmat.primitives.asymmetric.ec.SECP256R1, + cryptography.hazmat.primitives.asymmetric.ec.SECP384R1, + cryptography.hazmat.primitives.asymmetric.ec.SECP521R1, +]) +def test_KeyGenerator_instance_returns_ecdsa_private_with_correct_curve(curve): + + key_generator = gimmecert.crypto.KeyGenerator("ecdsa", curve) + + private_key = key_generator() + + assert isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey) + assert isinstance(private_key.curve, curve) + + @pytest.mark.parametrize("key_generator, expected_bit_size", [ (gimmecert.crypto.KeyGenerator("rsa", 1024), 1024), (gimmecert.crypto.KeyGenerator("rsa", 2048), 2048),