commit fe647b46cb8a9cd1ef41f9a3c0463ea42d8e8cb7
Author: Stanislav Král <stanislav.kral3@gmail.com>
Date:   Tue Mar 30 20:15:47 2021 +0200

    Re #8472 - Added sign_csr method that signs a CSR
    
    Added 3 unit tests testing the added method.

diff --git a/proj/services/cryptography.py b/proj/services/cryptography.py
index 2fa61ac..8bffa38 100644
--- a/proj/services/cryptography.py
+++ b/proj/services/cryptography.py
@@ -49,7 +49,8 @@ class CryptographyService:
             args.insert(0, executable)
 
             # create a new process
-            proc = subprocess.Popen(args, stdin=subprocess.PIPE if proc_input is not None else None, stdout=subprocess.PIPE,
+            proc = subprocess.Popen(args, stdin=subprocess.PIPE if proc_input is not None else None,
+                                    stdout=subprocess.PIPE,
                                     stderr=subprocess.PIPE)
 
             out, err = proc.communicate(proc_input)
@@ -108,12 +109,13 @@ class CryptographyService:
             # it would be best to not send the pass phrase at all, but for some reason pytest then prompts for
             # the pass phrase (this does not happen when run from pycharm)
 
-            # if key_passphrase is not None:
+            #  add the passphrase even when None is passed. Otherwise when running tests with pytest some tests freeze
+            # waiting for the passphrase to be typed in
             args.extend(["-passin", f"pass:{key_passphrase}"])
 
             return self._run_for_output(args, proc_input=bytes(key, encoding="utf-8")).decode()
 
-    def make_csr(self, subject, subject_key, subject_key_pass=None):
+    def make_csr(self, subject, subject_key, subject_key_pass=""):
         """
         Makes a CSR (Certificate Signing Request)
 
@@ -127,11 +129,43 @@ class CryptographyService:
 
         args = ["req", "-new", "-subj", subj_param, "-key", "-"]
 
-        if subject_key_pass is not None:
-            args.extend(["-passin", f"pass:{subject_key_pass}"])
+        # add the passphrase even when None is passed. Otherwise when running tests with pytest some tests freeze
+        # waiting for the passphrase to be typed in
+        args.extend(["-passin", f"pass:{subject_key_pass}"])
 
         return self._run_for_output(args, proc_input=bytes(subject_key, encoding="utf-8")).decode()
 
+    def sign_csr(self, csr, issuer_pem, issuer_key, issuer_key_pass=None, config="", extensions=""):
+        """
+        Signs the given CSR by the given issuer CA
+        :param csr: a string containing the CSR to be signed
+        :param issuer_pem: string containing the certificate of the issuer that will sign this CSR in PEM format
+        :param issuer_key: string containing the private key of the issuer's certificate in PEM format
+        :param issuer_key_pass: string containing the private key of the issuer's certificate in PEM format
+        :param config: TODO NOT USED
+        :param extensions: extensions to be applied when signing the CSR
+        :return: string containing the generated and signed certificate in PEM format
+        """
+
+        # concatenate CSR, issuer certificate and issuer's key (will be used in the openssl call)
+        proc_input = csr + issuer_pem + issuer_key
+
+        # prepare openssl parameters...
+        # CSR, CA and CA's private key will be passed via stdin (that's the meaning of the '-' symbol)
+        params = ["x509", "-req", "-in", "-", "-CA", "-", "-CAkey", "-", "-CAcreateserial"]
+
+        # TODO delete created -.srl file
+
+        with TemporaryFile("extensions.conf", extensions) as ext_path:
+            # add the passphrase even when None is passed. Otherwise when running tests with pytest some tests freeze
+            # waiting for the passphrase to be typed in
+            params.extend(["-passin", f"pass:{issuer_key_pass}"])
+
+            if len(extensions) > 0:
+                params.extend(["-extfile", ext_path])
+
+            return self._run_for_output(params, proc_input=(bytes(proc_input, encoding="utf-8"))).decode()
+
 
 class CryptographyException(Exception):
 
diff --git a/proj/tests/services/cryptography/sign_csr_test.py b/proj/tests/services/cryptography/sign_csr_test.py
new file mode 100644
index 0000000..ad04075
--- /dev/null
+++ b/proj/tests/services/cryptography/sign_csr_test.py
@@ -0,0 +1,93 @@
+import subprocess
+
+import pytest
+
+from proj.model.subject import Subject
+from proj.services.cryptography import CryptographyException
+
+
+def export_csr(csr):
+    return subprocess.check_output(["openssl", "x509", "-noout", "-text", "-in", "-"],
+                                   input=bytes(csr, encoding="utf-8"), stderr=subprocess.STDOUT).decode()
+
+
+def test_sign_csr(service):
+    # create root CA
+    root_key = service.create_private_key()
+    root_ca = service.create_sscrt(root_key, Subject(common_name="foo"))
+
+    # create a private key to be used to make a CSR for the intermediate CA
+    inter_key = service.create_private_key()
+    csr = service.make_csr(Subject(common_name="bar", country="CZ"), inter_key)
+
+    # sign the created CSR with root CA
+    inter_ca = service.sign_csr(csr, root_ca, root_key)
+
+    inter_ca_printed = export_csr(inter_ca)
+
+    # assert fields
+    assert "Issuer: CN = foo" in inter_ca_printed
+    assert "Subject: CN = bar, C = CZ" in inter_ca_printed
+
+
+def test_sign_csr_passphrase(service):
+    # create root CA and encrypt the private key of the root CA
+    root_key_passphrase = "barbaz"
+    root_key = service.create_private_key(passphrase=root_key_passphrase)
+    root_ca = service.create_sscrt(root_key, Subject(common_name="foo"), key_passphrase=root_key_passphrase)
+
+    # create a private key to be used to make a CSR for the intermediate CA
+    inter_key = service.create_private_key()
+    csr = service.make_csr(Subject(common_name="bar", country="CZ"), inter_key)
+
+    # sign the created CSR with root CA and specify root key passphrase
+    inter_ca = service.sign_csr(csr, root_ca, root_key, issuer_key_pass=root_key_passphrase)
+
+    inter_ca_printed = export_csr(inter_ca)
+
+    # assert fields
+    assert "Issuer: CN = foo" in inter_ca_printed
+    assert "Subject: CN = bar, C = CZ" in inter_ca_printed
+
+    # try to sign it using a wrong passphrase
+    with pytest.raises(CryptographyException) as e:
+        service.sign_csr(csr, root_ca, root_key,
+                         extensions="authorityInfoAccess = caIssuers;URI:bar.cz/baz.cert", issuer_key_pass="bazbaz")
+    assert "bad decrypt" in e.value.message
+
+    # try to sign it without specifying a passphrase
+    with pytest.raises(CryptographyException) as e:
+        service.sign_csr(csr, root_ca, root_key,
+                         extensions="authorityInfoAccess = caIssuers;URI:bar.cz/baz.cert")
+    assert "bad decrypt" in e.value.message
+
+
+def test_sign_csr_extensions(service):
+    # create root CA and encrypt the private key of the root CA
+    root_key_passphrase = "barbaz"
+    root_key = service.create_private_key(passphrase=root_key_passphrase)
+    root_ca = service.create_sscrt(root_key, Subject(common_name="foo"), key_passphrase=root_key_passphrase)
+
+    # create a private key to be used to make a CSR for the intermediate CA
+    inter_key = service.create_private_key()
+    csr = service.make_csr(Subject(common_name="bar", country="CZ"), inter_key)
+
+    # sign the created CSR with root CA and specify root key passphrase and specify extensions (AIA and CA)
+    inter_ca = service.sign_csr(csr, root_ca, root_key,
+                                extensions="authorityInfoAccess = caIssuers;URI:bar.cz/baz/cert\nbasicConstraints=critical,CA:TRUE",
+                                issuer_key_pass=root_key_passphrase)
+
+    inter_ca_printed = export_csr(inter_ca)
+
+    # assert fields
+    assert "Issuer: CN = foo" in inter_ca_printed
+    assert "Subject: CN = bar, C = CZ" in inter_ca_printed
+
+    # assert extensions
+    expected_extensions = """        X509v3 extensions:
+            Authority Information Access: 
+                CA Issuers - URI:bar.cz/baz/cert
+
+            X509v3 Basic Constraints: critical
+                CA:TRUE"""
+    assert expected_extensions in inter_ca_printed
