Changeset - 21280594890a
[Not reviewed]
0 5 0
Branko Majic (branko) - 6 years ago 2018-04-27 23:05:52
branko@majic.rs
GC-22: Updated renew command to allow reading of CSR from stdin:

- Implemented functional test covering reading of CSR from stdin for
the renew command.
- Updated renew command CLI help.
- Updated renew command to read CSR from stdin if passed-in path is
set to '-'.
- Implemented relevant unit tests.
5 files changed with 194 insertions and 2 deletions:
0 comments (0 inline, 0 general)
functional_tests/base.py
Show inline comments
 
@@ -88,8 +88,10 @@ def run_interactive_command(prompt_answers, command, *args):
 
    # Spawn the process, use dedicated stream for capturin command
 
    # stdout/stderr.
 
    output_stream = io.StringIO()
 
    send_stream = io.StringIO()
 
    process = pexpect.spawnu(command, [*args], timeout=2)
 
    process.logfile_read = output_stream
 
    process.logfile_send = send_stream
 

	
 
    # Try to feed the interactive process with answers. Stop iteration
 
    # at first prompt that was not reached.
functional_tests/test_csr.py
Show inline comments
 
@@ -534,3 +534,124 @@ def test_client_command_accepts_csr_from_stdin(tmpdir):
 

	
 
    # To his delight, they are identical.
 
    assert certificate_public_key == public_key
 

	
 

	
 
def test_renew_command_accepts_csr_from_stdin(tmpdir):
 
    # John has an existing project where he has generated a server and
 
    # client private key with corresponding CSR.
 
    tmpdir.chdir()
 
    server_csr, _, server_csr_exit_code = run_command("openssl", "req", "-new", "-newkey", "rsa:2048", "-nodes", "-keyout", "myserver.key.pem",
 
                                                      "-subj", "/CN=myserver")
 
    client_csr, _, client_csr_exit_code = run_command("openssl", "req", "-new", "-newkey", "rsa:2048", "-nodes", "-keyout", "myclient.key.pem",
 
                                                      "-subj", "/CN=myclient")
 

	
 
    # John realises that although the CSR generation was successful, he
 
    # forgot to output them to a file.
 
    assert server_csr_exit_code == 0
 
    assert "BEGIN CERTIFICATE REQUEST" in server_csr
 
    assert "END CERTIFICATE REQUEST" in server_csr
 

	
 
    assert client_csr_exit_code == 0
 
    assert "BEGIN CERTIFICATE REQUEST" in client_csr
 
    assert "END CERTIFICATE REQUEST" in client_csr
 

	
 
    # He could output the CSR into a file, and feed that into
 
    # Gimmecert, but he feels a bit lazy. Instead, John tries to pass
 
    # in a dash ("-") as input, knowing that it is commonly used as
 
    # shorthand for reading from standard input.
 

	
 
    # He goes ahead and initalises the CA hierarchy first.
 
    tmpdir.chdir()
 
    run_command("gimmecert", "init")
 

	
 
    # He proceeds to issue a server and client certificate.
 
    run_command("gimmecert", "server", "myserver")
 
    run_command("gimmecert", "client", "myclient")
 

	
 
    # Very quickly John realises that he has mistakenly forgotten to
 
    # pass-in the relevant CSRs, and that Gimmecert has generated
 
    # private keys locally and issued certificates for them.
 
    assert tmpdir.join('.gimmecert', 'server', 'myserver.key.pem').check(file=1)
 
    assert not tmpdir.join('.gimmecert', 'server', 'myserver.csr.pem').check(file=1)
 
    assert tmpdir.join('.gimmecert', 'client', 'myclient.key.pem').check(file=1)
 
    assert not tmpdir.join('.gimmecert', 'client', 'myclient.csr.pem').check(file=1)
 

	
 
    # He has a look at the public key from the generated CSRs (that he
 
    # originally wanted to use).
 
    tmpfile = tmpdir.join('tempfile')
 

	
 
    tmpfile.write(server_csr)
 
    server_csr_public_key, _, _ = run_command("openssl", "req", "-noout", "-pubkey", "-in", tmpfile.strpath)
 

	
 
    tmpfile.write(client_csr)
 
    client_csr_public_key, _, _ = run_command("openssl", "req", "-noout", "-pubkey", "-in", tmpfile.strpath)
 

	
 
    # The renew command can accept a CSR to replace existing artifact
 
    # used for original issuance. He could output the CSRs into a
 
    # file, and feed that into Gimmecert, but he feels a bit
 
    # lazy. Instead, John tries to pass in a dash ("-") as input to
 
    # the renew command, knowing that it is commonly used as shorthand
 
    # for reading from standard input.
 
    renew_server_prompt_failure, renew_server_output, renew_server_exit_code = run_interactive_command(
 
        [],
 
        "gimmecert", "renew", "--csr", "-", "server", "myserver"
 
    )
 
    renew_client_prompt_failure, renew_client_output, renew_client_exit_code = run_interactive_command(
 
        [], "gimmecert", "renew", "--csr", "-", "client", "myclient"
 
    )
 

	
 
    # John sees that the application has prompted him to provide the
 
    # CSR interactively for both server and client certificate
 
    # renewal, and that it waits for his input.
 
    assert renew_server_exit_code is None, "Output was: %s" % renew_server_output
 
    assert renew_server_prompt_failure == "Command got stuck waiting for input.", "Output was: %s" % renew_server_output
 
    assert "Please enter the CSR (finish with Ctrl-D on an empty line):" in renew_server_output
 

	
 
    assert renew_server_exit_code is None, "Output was: %s" % renew_client_output
 
    assert renew_client_prompt_failure == "Command got stuck waiting for input.", "Output was: %s" % renew_client_output
 
    assert "Please enter the CSR (finish with Ctrl-D on an empty line):" in renew_client_output
 

	
 
    # John reruns renewal commands, this time passing-in the CSR and
 
    # ending the input with Ctrl-D.
 
    renew_server_prompt_failure, renew_server_output, renew_server_exit_code = run_interactive_command(
 
        [('Please enter the CSR \(finish with Ctrl-D on an empty line\):', server_csr + '\n\004')],
 
        "gimmecert", "renew", "--csr", "-", "server", "myserver"
 
    )
 

	
 
    renew_client_prompt_failure, renew_client_output, renew_client_exit_code = run_interactive_command(
 
        [('Please enter the CSR \(finish with Ctrl-D on an empty line\):', client_csr + '\n\004')],
 
        "gimmecert", "renew", "--csr", "-", "client", "myclient"
 
    )
 

	
 
    # The operation is successful, and he is presented with
 
    # information about generated artefacts.
 
    assert renew_server_prompt_failure is None
 
    assert renew_server_exit_code == 0
 
    assert ".gimmecert/server/myserver.cert.pem" in renew_server_output
 
    assert ".gimmecert/server/myserver.csr.pem" in renew_server_output
 

	
 
    assert renew_client_prompt_failure is None
 
    assert renew_client_exit_code == 0
 
    assert ".gimmecert/client/myclient.cert.pem" in renew_client_output
 
    assert ".gimmecert/client/myclient.csr.pem" in renew_client_output
 

	
 
    # John also notices that there is no mention of a private key.
 
    assert ".gimmecert/server/myserver.key.pem" not in renew_server_output
 
    assert ".gimmecert/client/myclient.key.pem" not in renew_client_output
 

	
 
    # John notices that the content of stored CSRs is identical to the
 
    # ones he provided.
 
    server_stored_csr = tmpdir.join(".gimmecert", "server", "myserver.csr.pem").read()
 
    assert server_stored_csr == server_csr
 

	
 
    client_stored_csr = tmpdir.join(".gimmecert", "client", "myclient.csr.pem").read()
 
    assert client_stored_csr == client_csr
 

	
 
    # John then quickly has a look at the public key from passed-in
 
    # CSR and compares it to the one stored in certificate.
 
    server_certificate_public_key, _, _ = run_command("openssl", "x509", "-pubkey", "-noout", "-in", ".gimmecert/server/myserver.cert.pem")
 
    client_certificate_public_key, _, _ = run_command("openssl", "x509", "-pubkey", "-noout", "-in", ".gimmecert/client/myclient.cert.pem")
 

	
 
    # To his delight, they are identical.
 
    assert server_certificate_public_key == server_csr_public_key
 
    assert client_certificate_public_key == client_csr_public_key
gimmecert/cli.py
Show inline comments
 
@@ -149,8 +149,8 @@ def setup_renew_subcommand_parser(parser, subparsers):
 
    new_private_key_or_csr_group.add_argument('--new-private-key', '-p', action='store_true', help='''Generate new private key for renewal. \
 
    Default is to keep the existing key. Mutually exclusive with the --csr option.''')
 
    new_private_key_or_csr_group.add_argument('--csr', '-c', type=str, default=None, help='''Do not use local private key and public key information from \
 
    existing certificate, and use the passed-in certificate signing request (CSR) instead. If private key exists, it will be removed. \
 
    Mutually exclusive with the --new-private-key option.''')
 
    existing certificate, and use the passed-in certificate signing request (CSR) instead. Use dash (-) to read from standard input. \
 
    If private key exists, it will be removed. Mutually exclusive with the --new-private-key option.''')
 

	
 
    def renew_wrapper(args):
 
        project_directory = os.getcwd()
gimmecert/commands.py
Show inline comments
 
@@ -424,6 +424,11 @@ def renew(stdout, stderr, project_directory, entity_type, entity_name, generate_
 
        private_key = gimmecert.crypto.generate_private_key()
 
        gimmecert.storage.write_private_key(private_key, private_key_path)
 
        public_key = private_key.public_key()
 
    elif custom_csr_path == '-':
 
        csr_pem = gimmecert.utils.read_input(sys.stdin, stderr, "Please enter the CSR")
 
        csr = gimmecert.utils.csr_from_pem(csr_pem)
 
        gimmecert.storage.write_csr(csr, csr_path)
 
        public_key = csr.public_key()
 
    elif custom_csr_path:
 
        csr = gimmecert.storage.read_csr(custom_csr_path)
 
        gimmecert.storage.write_csr(csr, csr_path)
tests/test_commands.py
Show inline comments
 
@@ -1527,3 +1527,67 @@ def test_client_reads_csr_from_stdin(mock_read_input, sample_project_directory,
 
    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
 

	
 

	
 
@mock.patch('gimmecert.utils.read_input')
 
def test_renew_server_reads_csr_from_stdin(mock_read_input, sample_project_directory, key_with_csr):
 
    entity_name = 'myserver'
 
    stored_csr_file = sample_project_directory.join('.gimmecert', 'server', '%s.csr.pem' % entity_name)
 
    certificate_file = sample_project_directory.join('.gimmecert', 'server', '%s.cert.pem' % entity_name)
 

	
 
    # Generate server certificate that will be renewed.
 
    gimmecert.commands.server(io.StringIO(), io.StringIO(), sample_project_directory.strpath, entity_name, None, False, None)
 

	
 
    # Mock our util for reading input from user.
 
    mock_read_input.return_value = key_with_csr.csr_pem
 

	
 
    stdout_stream = io.StringIO()
 
    stderr_stream = io.StringIO()
 

	
 
    status_code = gimmecert.commands.renew(stdout_stream, stderr_stream, sample_project_directory.strpath, "server", entity_name, False, '-')
 
    assert status_code == 0
 

	
 
    # Read stored/generated artefacts.
 
    stored_csr = gimmecert.storage.read_csr(stored_csr_file.strpath)
 
    certificate = gimmecert.storage.read_certificate(certificate_file.strpath)
 

	
 
    custom_csr_public_numbers = key_with_csr.csr.public_key().public_numbers()
 
    stored_csr_public_numbers = stored_csr.public_key().public_numbers()
 
    certificate_public_numbers = certificate.public_key().public_numbers()
 

	
 
    mock_read_input.assert_called_once_with(sys.stdin, stderr_stream, "Please enter the CSR")
 
    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
 

	
 

	
 
@mock.patch('gimmecert.utils.read_input')
 
def test_renew_client_reads_csr_from_stdin(mock_read_input, sample_project_directory, key_with_csr):
 
    entity_name = 'myclient'
 
    stored_csr_file = sample_project_directory.join('.gimmecert', 'client', '%s.csr.pem' % entity_name)
 
    certificate_file = sample_project_directory.join('.gimmecert', 'client', '%s.cert.pem' % entity_name)
 

	
 
    # Generate client certificate that will be renewed.
 
    gimmecert.commands.client(io.StringIO(), io.StringIO(), sample_project_directory.strpath, entity_name, None)
 

	
 
    # Mock our util for reading input from user.
 
    mock_read_input.return_value = key_with_csr.csr_pem
 

	
 
    stdout_stream = io.StringIO()
 
    stderr_stream = io.StringIO()
 

	
 
    status_code = gimmecert.commands.renew(stdout_stream, stderr_stream, sample_project_directory.strpath, "client", entity_name, False, '-')
 
    assert status_code == 0
 

	
 
    # Read stored/generated artefacts.
 
    stored_csr = gimmecert.storage.read_csr(stored_csr_file.strpath)
 
    certificate = gimmecert.storage.read_certificate(certificate_file.strpath)
 

	
 
    custom_csr_public_numbers = key_with_csr.csr.public_key().public_numbers()
 
    stored_csr_public_numbers = stored_csr.public_key().public_numbers()
 
    certificate_public_numbers = certificate.public_key().public_numbers()
 

	
 
    mock_read_input.assert_called_once_with(sys.stdin, stderr_stream, "Please enter the CSR")
 
    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
0 comments (0 inline, 0 general)