Changeset - 272ebabc4062
[Not reviewed]
0 5 0
Branko Majic (branko) - 4 years ago 2020-06-15 15:33:03
branko@majic.rs
GC-37: Added ECDSA support for initialising CA hierarchy:

- Added functional test.
- Added unit tests.
- Updated key specification parsing to support ECDSA specification
using curve name.
- Updated KeyGenerator to handle ECDSA private keys generation.
- Updated inline documentation.
5 files changed with 171 insertions and 20 deletions:
0 comments (0 inline, 0 general)
functional_tests/test_key_specification.py
Show inline comments
 
@@ -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
gimmecert/cli.py
Show inline comments
 
@@ -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()
gimmecert/crypto.py
Show inline comments
 
@@ -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 = []
tests/test_cli.py
Show inline comments
 
@@ -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):
 

	
tests/test_crypto.py
Show inline comments
 
@@ -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),
0 comments (0 inline, 0 general)