commit 5f4e6c2f295ff157ae29915ce8d9e966625e0613
Author: Stanislav Král <stanislav.kral3@gmail.com>
Date:   Mon Apr 19 18:22:01 2021 +0200

    Re #8575 - Added a file that defines methods used for generating CA index file lines
    
    Added unit tests verifying the validity of added methods

diff --git a/src/services/certificate_service.py b/src/services/certificate_service.py
index 4e5bcb6..80dc62d 100644
--- a/src/services/certificate_service.py
+++ b/src/services/certificate_service.py
@@ -15,7 +15,7 @@ from src.services.cryptography import CryptographyService
 
 import time
 
-DATE_FORMAT = "%d.%m.%Y %H:%M:%S"
+NOT_BEFORE_AFTER_DATE_FORMAT = "%d.%m.%Y %H:%M:%S"
 CA_EXTENSIONS = "basicConstraints=critical,CA:TRUE"
 CRL_EXTENSION = "crlDistributionPoints=URI:"
 OCSP_EXTENSION = "authorityInfoAccess=OCSP;URI:"
@@ -86,8 +86,8 @@ class CertificateService:
         # TODO this could be improved in the future in such way that calling openssl is not required to parse the dates
         subj, not_before, not_after = self.cryptography_service.parse_cert_pem(cert_pem)
         # format the parsed date
-        not_before_formatted = time.strftime(DATE_FORMAT, not_before)
-        not_after_formatted = time.strftime(DATE_FORMAT, not_after)
+        not_before_formatted = time.strftime(NOT_BEFORE_AFTER_DATE_FORMAT, not_before)
+        not_after_formatted = time.strftime(NOT_BEFORE_AFTER_DATE_FORMAT, not_after)
 
         # create a certificate wrapper
         certificate = Certificate(-1, subj.common_name, not_before_formatted, not_after_formatted, cert_pem,
@@ -136,8 +136,8 @@ class CertificateService:
         subj, not_before, not_after = self.cryptography_service.parse_cert_pem(cert_pem)
 
         # format the parsed date
-        not_before_formatted = time.strftime(DATE_FORMAT, not_before)
-        not_after_formatted = time.strftime(DATE_FORMAT, not_after)
+        not_before_formatted = time.strftime(NOT_BEFORE_AFTER_DATE_FORMAT, not_before)
+        not_after_formatted = time.strftime(NOT_BEFORE_AFTER_DATE_FORMAT, not_after)
 
         # specify CA usage
         usages[CA_ID] = True
diff --git a/src/services/crl/__init__.py b/src/services/crl/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/services/crl/ca_index_file_line_generator.py b/src/services/crl/ca_index_file_line_generator.py
new file mode 100644
index 0000000..59c3935
--- /dev/null
+++ b/src/services/crl/ca_index_file_line_generator.py
@@ -0,0 +1,40 @@
+import time
+from time import struct_time
+
+from src.model.certificate import Certificate
+from src.model.subject import Subject
+
+TAB_CHAR = "\t"
+INDEX_FILE_DATE_ENTRY_FORMAT = "%y%m%d%H%M%SZ"
+
+
+def get_index_file_time_entry(date: struct_time):
+    # convert the time to the format of openssl CA index file
+    return time.strftime(INDEX_FILE_DATE_ENTRY_FORMAT, date)
+
+
+def get_distinguished_name(subject: Subject):
+    # convert subject class instance to the distinguished name in the openssl CA index file format
+    return "".join([f"/{key}={value}" if value is not None else "" for key, value in subject.to_dict().items()])
+
+
+def create_index_file_revoked_line(revoked_certificate: Certificate, subject: Subject, revocation_date: struct_time,
+                                   valid_to: struct_time) -> str:
+    # converts the given certificate as well as the subject and revocation / valid_to dates to a line of openssl CA
+    # index file format
+    items = [
+        # certificate status flag (R stands for revoked)
+        "R",
+        # followed by the expiration date field
+        f"{get_index_file_time_entry(valid_to)}",
+        # followed by the revocation date field
+        f"{get_index_file_time_entry(revocation_date)},{revoked_certificate.revocation_reason}",
+        # followed by the serial number of the certificate in hex format
+        hex(revoked_certificate.certificate_id).replace("x", ""),
+        # certificate filename ("unknown" literal used for unknown file names)
+        "unknown",
+        # certificate distinguished name
+        get_distinguished_name(subject)
+    ]
+
+    return TAB_CHAR.join(items)
diff --git a/tests/unit_tests/services/crl/__init__.py b/tests/unit_tests/services/crl/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/unit_tests/services/crl/ca_index_file_line_generator_test.py b/tests/unit_tests/services/crl/ca_index_file_line_generator_test.py
new file mode 100644
index 0000000..76637ef
--- /dev/null
+++ b/tests/unit_tests/services/crl/ca_index_file_line_generator_test.py
@@ -0,0 +1,66 @@
+import time
+
+from src.model.certificate import Certificate
+from src.model.subject import Subject
+from src.services.crl.ca_index_file_line_generator import create_index_file_revoked_line
+
+
+def test_get_index_file_time_entry_valid():
+    cert = Certificate(
+        1, "Foo", "21.03.2020", "21.03.2021", "", -1, -1, -1, {}, "02.02.2021", "keyCompromise"
+    )
+
+    date_format = "%d.%m.%Y"
+
+    expiration_date = time.strptime(cert.valid_to, date_format)
+    revocation_date = time.strptime(cert.revocation_date, date_format)
+
+    index_line = create_index_file_revoked_line(cert, Subject(
+        "Foo", "CZ", "Pilsen", email_address="bar@foo.cz"
+    ), revocation_date, expiration_date)
+
+    assert "R	210321000000Z	210202000000Z,keyCompromise	01	unknown	" \
+           "/CN=Foo/C=CZ/L=Pilsen/emailAddress=bar@foo.cz" \
+           == index_line
+
+
+def test_get_index_file_time_entry_valid_2():
+    cert = Certificate(
+        1024, "Bar", "01.01.2019", "06.10.2021", "", -1, -1, -1, {}, "03.09.2021", "affiliationChanged"
+    )
+
+    date_format = "%d.%m.%Y"
+
+    expiration_date = time.strptime(cert.valid_to, date_format)
+    revocation_date = time.strptime(cert.revocation_date, date_format)
+
+    index_line = create_index_file_revoked_line(cert, Subject(
+        "Bar", "SK", "Foosen", organization="Bar Org."
+    ), revocation_date, expiration_date)
+
+    assert "R	211006000000Z	210903000000Z,affiliationChanged	0400	unknown	" \
+           "/CN=Bar/C=SK/L=Foosen/O=Bar Org." \
+           == index_line
+
+
+def test_get_index_file_time_entry_valid_3():
+    cert = Certificate(
+        1, "IA CA", "", "01.04.2023 15:01:11", "", -1, -1, -1, {}, "01.04.2021 15:12:00", "keyCompromise"
+    )
+
+    date_format = "%d.%m.%Y %H:%M:%S"
+
+    expiration_date = time.strptime(cert.valid_to, date_format)
+    revocation_date = time.strptime(cert.revocation_date, date_format)
+
+    index_line = create_index_file_revoked_line(cert, Subject(
+        common_name=cert.common_name,
+        state="Some-State",
+        country="AU",
+        organization="Internet Widgits Pty Ltd "
+    ), revocation_date, expiration_date)
+
+    expected = "R	230401150111Z	210401151200Z,keyCompromise	01	unknown	/CN=IA CA/C=AU/ST=Some-State/O=Internet " \
+               "Widgits Pty Ltd "
+
+    assert expected == index_line
