commit 5fdd01a6f7476e9cbdb0e68f68ee18b9dca563c1
Author: Stanislav Král <stanislav.kral3@gmail.com>
Date:   Tue Mar 30 23:51:17 2021 +0200

    Re #8472 - Added verify_ca method that verifies whether a certificate has expired
    
    Added 3 unit tests that verify the added method.
    Added "days" parameter to some methods in order to be able to specify the number of days after which the generated certificate will expire.

diff --git a/proj/services/cryptography.py b/proj/services/cryptography.py
index a13146e..505cd22 100644
--- a/proj/services/cryptography.py
+++ b/proj/services/cryptography.py
@@ -135,7 +135,7 @@ class CryptographyService:
 
         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=""):
+    def sign_csr(self, csr, issuer_pem, issuer_key, issuer_key_pass=None, config="", extensions="", days=30):
         """
         Signs the given CSR by the given issuer CA
         :param csr: a string containing the CSR to be signed
@@ -145,6 +145,7 @@ class CryptographyService:
         format
         :param config: TODO NOT USED
         :param extensions: extensions to be applied when signing the CSR
+        :param days: number of days for which the certificate will be valid TODO this parameter is not in the SW arch.
         :return: string containing the generated and signed certificate in PEM format
         """
 
@@ -153,7 +154,7 @@ class CryptographyService:
 
         # 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"]
+        params = ["x509", "-req", "-in", "-", "-CA", "-", "-CAkey", "-", "-CAcreateserial", "-days", str(days)]
 
         # TODO delete created -.srl file
 
@@ -169,7 +170,8 @@ class CryptographyService:
 
     def create_crt(self, subject, subject_key, issuer_pem, issuer_key, subject_key_pass=None, issuer_key_pass=None,
                    config="",
-                   extensions=""):
+                   extensions="",
+                   days=30):
         """
         Signs the given CSR by the given issuer CA
         :param subject: subject to be added to the created certificate
@@ -183,10 +185,35 @@ class CryptographyService:
         format
         :param config: TODO NOT USED
         :param extensions: extensions to be applied when creating the certificate
+        :param days: number of days for which the certificate will be valid
         :return: string containing the generated certificate in PEM format
         """
         csr = self.make_csr(subject, subject_key, subject_key_pass=subject_key_pass)
-        return self.sign_csr(csr, issuer_pem, issuer_key, issuer_key_pass=issuer_key_pass, extensions=extensions)
+        return self.sign_csr(csr, issuer_pem, issuer_key, issuer_key_pass=issuer_key_pass, extensions=extensions,
+                             days=days)
+
+    @staticmethod
+    def verify_ca(certificate, key, key_pass=None):
+        # TODO could be renamed to "verify_certificate"? This method can verify all certificates, not just CAs.
+        # call openssl to check whether the certificate is valid to this date
+        args = [OPENSSL_EXECUTABLE, "x509", "-checkend", "0", "-noout", "-text", "-in", "-"]
+
+        # create a new process
+        proc = subprocess.Popen(args, stdin=subprocess.PIPE,
+                                stdout=subprocess.PIPE,
+                                stderr=subprocess.PIPE)
+
+        out, err = proc.communicate(bytes(certificate, encoding="utf-8"))
+
+        # zero return code means that the certificate is valid
+        if proc.returncode == 0:
+            return True
+        elif proc.returncode == 1 and "Certificate will expire" in out.decode():
+            # 1 return code means that the certificate is invalid but such message has to be present in the proc output
+            return False
+        else:
+            # the process failed because of some other reason (incorrect cert format)
+            raise CryptographyException(OPENSSL_EXECUTABLE, args, err.decode())
 
 
 class CryptographyException(Exception):
diff --git a/proj/tests/services/cryptography/verify_ca_test.py b/proj/tests/services/cryptography/verify_ca_test.py
new file mode 100644
index 0000000..9678500
--- /dev/null
+++ b/proj/tests/services/cryptography/verify_ca_test.py
@@ -0,0 +1,32 @@
+import pytest
+
+from proj.model.subject import Subject
+from proj.services.cryptography import CryptographyException
+
+
+def test_verify_valid_ca(service):
+    # verify validation of valid certificates
+    private_key = service.create_private_key()
+    root_cert = service.create_sscrt(private_key, Subject(common_name="foo"))
+    child_cert = service.create_crt(Subject(common_name="Expired Foo"), private_key, root_cert, private_key, days=1)
+
+    assert service.verify_ca(root_cert, private_key)
+    assert service.verify_ca(child_cert, private_key)
+
+
+def test_verify_invalid_ca(service):
+    # test whether expired certificate will fail to get verified
+    private_key = service.create_private_key()
+    root_cert = service.create_sscrt(private_key, Subject(common_name="foo"))
+
+    expired_cert = service.create_crt(Subject(common_name="Expired Foo"), private_key, root_cert, private_key, days=0)
+
+    assert not service.verify_ca(expired_cert, private_key)
+
+
+def test_verify_invalid_cert_format(service):
+    # verify that certificate in invalid format raises <CryptographyException>
+    private_key = service.create_private_key()
+
+    with pytest.raises(CryptographyException):
+        service.verify_ca("invalid cert", private_key)
