Projekt

Obecné

Profil

Stáhnout (13.5 KB) Statistiky
| Větev: | Tag: | Revize:
1 4a40b0d2 Stanislav Král
import re
2 36409852 Stanislav Král
import subprocess
3
import time
4 c0aed2f5 Stanislav Král
5 cc51ca2c Stanislav Král
from src.model.subject import Subject
6 181e1196 Jan Pašek
from src.utils.temporary_file import TemporaryFile
7 c0aed2f5 Stanislav Král
8 36409852 Stanislav Král
# encryption method to be used when generating private keys
9 c0aed2f5 Stanislav Král
PRIVATE_KEY_ENCRYPTION_METHOD = "-aes256"
10
11
# openssl executable name
12
OPENSSL_EXECUTABLE = "openssl"
13
14 36409852 Stanislav Král
# format of NOT_BEFORE NOT_AFTER date fields
15
NOT_AFTER_BEFORE_DATE_FORMAT = "%b %d %H:%M:%S %Y %Z"
16
17 c0aed2f5 Stanislav Král
18
class CryptographyService:
19
20
    @staticmethod
21 57898b2f Stanislav Král
    def __subject_to_param_format(subject):
22
        """
23
        Converts the given subject to a dictionary containing openssl field names mapped to subject's fields
24
        :param subject: subject to be converted
25
        :return: a dictionary containing openssl field names mapped to subject's fields
26
        """
27 6c098d6e Stanislav Král
        subj_dict = {}
28
        if subject.common_name is not None:
29
            subj_dict["CN"] = subject.common_name
30
        if subject.country is not None:
31
            subj_dict["C"] = subject.country
32
        if subject.locality is not None:
33
            subj_dict["L"] = subject.locality
34
        if subject.state is not None:
35
            subj_dict["ST"] = subject.state
36
        if subject.organization is not None:
37
            subj_dict["O"] = subject.organization
38
        if subject.organization_unit is not None:
39
            subj_dict["OU"] = subject.organization_unit
40
        if subject.email_address is not None:
41
            subj_dict["emailAddress"] = subject.email_address
42
43
        # merge the subject into a "subj" parameter format
44
        return "".join([f"/{key}={value}" for key, value in subj_dict.items()])
45
46
    @staticmethod
47 7444d4cb Stanislav Král
    def __run_for_output(args=None, proc_input=None, executable=OPENSSL_EXECUTABLE):
48 c0aed2f5 Stanislav Král
        """
49
        Launches a new process in which the given executable is run. STDIN and process arguments can be set.
50
        If the process ends with a non-zero then <CryptographyException> is raised.
51 4691a56f Stanislav Král
52 c0aed2f5 Stanislav Král
        :param args: Arguments to be passed to the program.
53 6c098d6e Stanislav Král
        :param proc_input: String input to be passed to the stdin of the created process.
54 c0aed2f5 Stanislav Král
        :param executable: Executable to be run (defaults to openssl)
55
        :return: If the process ends with a zero return code then the STDOUT of the process is returned as a byte array.
56
        """
57
        if args is None:
58
            args = []
59
        try:
60
            # prepend the name of the executable
61
            args.insert(0, executable)
62
63
            # create a new process
64 fe647b46 Stanislav Král
            proc = subprocess.Popen(args, stdin=subprocess.PIPE if proc_input is not None else None,
65
                                    stdout=subprocess.PIPE,
66 c0aed2f5 Stanislav Král
                                    stderr=subprocess.PIPE)
67
68 6c098d6e Stanislav Král
            out, err = proc.communicate(proc_input)
69 c0aed2f5 Stanislav Král
70
            if proc.returncode != 0:
71
                # if the process did not result in zero result code, then raise an exception
72 7d0aa304 Stanislav Král
                if err is not None and len(err) > 0:
73 c0aed2f5 Stanislav Král
                    raise CryptographyException(executable, args, err.decode())
74
                else:
75
                    raise CryptographyException(executable, args,
76
                                                f""""Execution resulted in non-zero argument""")
77
78
            return out
79
        except FileNotFoundError:
80
            raise CryptographyException(executable, args, f""""{executable}" not found in the current PATH.""")
81
82
    def create_private_key(self, passphrase=None):
83
        """
84
        Creates a private key with the option to encrypt it using a passphrase.
85
        :param passphrase: A passphrase to be used when encrypting the key (if none is passed then the key is not
86
        encrypted at all). Empty passphrase ("") also results in a key that is not encrypted.
87 18588728 Stanislav Král
        :return: string containing the generated private key in PEM format
88 c0aed2f5 Stanislav Král
        """
89
        if passphrase is None or len(passphrase) == 0:
90 7444d4cb Stanislav Král
            return self.__run_for_output(["genrsa", "2048"]).decode()
91 c0aed2f5 Stanislav Král
        else:
92 7444d4cb Stanislav Král
            return self.__run_for_output(
93 c0aed2f5 Stanislav Král
                ["genrsa", PRIVATE_KEY_ENCRYPTION_METHOD, "-passout", f"pass:{passphrase}", "2048"]).decode()
94
95 2f5101f1 Stanislav Král
    def create_sscrt(self, subject, key, config="", extensions="", key_pass=None, days=30):
96 c0aed2f5 Stanislav Král
        """
97
        Creates a root CA
98
99
        :param subject: an instance of <Subject> representing the subject to be added to the certificate
100 02f63b07 Stanislav Král
        :param key: private key of the CA to be used
101 c0aed2f5 Stanislav Král
        :param config: string containing the configuration to be used
102
        :param extensions: name of the section in the configuration representing extensions
103 18588728 Stanislav Král
        :param key_pass: passphrase of the private key
104 2f5101f1 Stanislav Král
        :param days: number of days for which the certificate will be valid
105 c0aed2f5 Stanislav Král
106 18588728 Stanislav Král
        :return: string containing the generated certificate in PEM format
107 c0aed2f5 Stanislav Král
        """
108
        assert key is not None
109
        assert subject is not None
110
111 57898b2f Stanislav Král
        subj = self.__subject_to_param_format(subject)
112 c0aed2f5 Stanislav Král
113
        with TemporaryFile("openssl.conf", config) as conf_path:
114 2f5101f1 Stanislav Král
            args = ["req", "-x509", "-new", "-subj", subj, "-days", f"{days}",
115 c0aed2f5 Stanislav Král
                    "-key", "-"]
116
            if len(config) > 0:
117
                args.extend(["-config", conf_path])
118
119
            if len(extensions) > 0:
120
                args.extend(["-extensions", extensions])
121
122
            # it would be best to not send the pass phrase at all, but for some reason pytest then prompts for
123
            # the pass phrase (this does not happen when run from pycharm)
124
125 fe647b46 Stanislav Král
            #  add the passphrase even when None is passed. Otherwise when running tests with pytest some tests freeze
126
            # waiting for the passphrase to be typed in
127 18588728 Stanislav Král
            args.extend(["-passin", f"pass:{key_pass}"])
128 c0aed2f5 Stanislav Král
129 7444d4cb Stanislav Král
            return self.__run_for_output(args, proc_input=bytes(key, encoding="utf-8")).decode()
130 6c098d6e Stanislav Král
131 87fd5afc Stanislav Král
    def __create_csr(self, subject, key, key_pass=""):
132 6c098d6e Stanislav Král
        """
133 bdf9a46c Stanislav Král
        Creates a CSR (Certificate Signing Request)
134 6c098d6e Stanislav Král
135
        :param subject: an instance of <Subject> representing the subject to be added to the CSR
136 87fd5afc Stanislav Král
        :param key: the private key of the subject to be used to generate the CSR
137
        :param key_pass: passphrase of the subject's private key
138 18588728 Stanislav Král
        :return: string containing the generated certificate signing request in PEM format
139 6c098d6e Stanislav Král
        """
140
141 57898b2f Stanislav Král
        subj_param = self.__subject_to_param_format(subject)
142 6c098d6e Stanislav Král
143
        args = ["req", "-new", "-subj", subj_param, "-key", "-"]
144
145 fe647b46 Stanislav Král
        # add the passphrase even when None is passed. Otherwise when running tests with pytest some tests freeze
146
        # waiting for the passphrase to be typed in
147 87fd5afc Stanislav Král
        args.extend(["-passin", f"pass:{key_pass}"])
148 6c098d6e Stanislav Král
149 87fd5afc Stanislav Král
        return self.__run_for_output(args, proc_input=bytes(key, encoding="utf-8")).decode()
150 c0aed2f5 Stanislav Král
151 87a7a4a5 Stanislav Král
    def __sign_csr(self, csr, issuer_pem, issuer_key, issuer_key_pass=None, extensions="", days=30):
152 fe647b46 Stanislav Král
        """
153
        Signs the given CSR by the given issuer CA
154 ad068f9d Stanislav Král
155 fe647b46 Stanislav Král
        :param csr: a string containing the CSR to be signed
156
        :param issuer_pem: string containing the certificate of the issuer that will sign this CSR in PEM format
157
        :param issuer_key: string containing the private key of the issuer's certificate in PEM format
158 9dbbcdae Stanislav Král
        :param issuer_key_pass: string containing the passphrase of the private key of the issuer's certificate in PEM
159
        format
160 fe647b46 Stanislav Král
        :param extensions: extensions to be applied when signing the CSR
161 c4b2f4d2 Stanislav Král
        :param days: number of days for which the certificate will be valid
162 fe647b46 Stanislav Král
        :return: string containing the generated and signed certificate in PEM format
163
        """
164
165
        # concatenate CSR, issuer certificate and issuer's key (will be used in the openssl call)
166
        proc_input = csr + issuer_pem + issuer_key
167
168
        # prepare openssl parameters...
169
        # CSR, CA and CA's private key will be passed via stdin (that's the meaning of the '-' symbol)
170 5fdd01a6 Stanislav Král
        params = ["x509", "-req", "-in", "-", "-CA", "-", "-CAkey", "-", "-CAcreateserial", "-days", str(days)]
171 fe647b46 Stanislav Král
172
        # TODO delete created -.srl file
173
174
        with TemporaryFile("extensions.conf", extensions) as ext_path:
175
            # add the passphrase even when None is passed. Otherwise when running tests with pytest some tests freeze
176
            # waiting for the passphrase to be typed in
177
            params.extend(["-passin", f"pass:{issuer_key_pass}"])
178
179
            if len(extensions) > 0:
180
                params.extend(["-extfile", ext_path])
181
182 7444d4cb Stanislav Král
            return self.__run_for_output(params, proc_input=(bytes(proc_input, encoding="utf-8"))).decode()
183 fe647b46 Stanislav Král
184 18588728 Stanislav Král
    def create_crt(self, subject, subject_key, issuer_pem, issuer_key, subject_key_pass=None, issuer_key_pass=None,
185 5fdd01a6 Stanislav Král
                   extensions="",
186
                   days=30):
187 9dbbcdae Stanislav Král
        """
188 61a42455 Stanislav Král
        Creates a certificate by using the given subject, subject's key, issuer and its key.
189 ad068f9d Stanislav Král
190 9dbbcdae Stanislav Král
        :param subject: subject to be added to the created certificate
191 18588728 Stanislav Král
        :param subject_key: string containing the private key to be used when creating the certificate in PEM format
192 9dbbcdae Stanislav Král
        :param issuer_key: string containing the private key of the issuer's certificate in PEM format
193
        :param issuer_pem: string containing the certificate of the issuer that will sign this CSR in PEM format
194 36409852 Stanislav Král
        :param subject_key_pass: string containing the passphrase of the private key used when creating the certificate
195
        in PEM format
196 9dbbcdae Stanislav Král
        :param issuer_key_pass: string containing the passphrase of the private key of the issuer's certificate in PEM
197
        format
198
        :param extensions: extensions to be applied when creating the certificate
199 5fdd01a6 Stanislav Král
        :param days: number of days for which the certificate will be valid
200 18588728 Stanislav Král
        :return: string containing the generated certificate in PEM format
201 9dbbcdae Stanislav Král
        """
202 87fd5afc Stanislav Král
        csr = self.__create_csr(subject, subject_key, key_pass=subject_key_pass)
203 87a7a4a5 Stanislav Král
        return self.__sign_csr(csr, issuer_pem, issuer_key, issuer_key_pass=issuer_key_pass, extensions=extensions,
204
                               days=days)
205 5fdd01a6 Stanislav Král
206
    @staticmethod
207 61a42455 Stanislav Král
    def verify_cert(certificate):
208
        """
209
        Verifies whether the given certificate is not expired.
210
211
        :param certificate: certificate to be verified in PEM format
212
        :return: Returns `true` if the certificate is not expired, `false` when expired.
213
        """
214 5fdd01a6 Stanislav Král
        # call openssl to check whether the certificate is valid to this date
215
        args = [OPENSSL_EXECUTABLE, "x509", "-checkend", "0", "-noout", "-text", "-in", "-"]
216
217
        # create a new process
218
        proc = subprocess.Popen(args, stdin=subprocess.PIPE,
219
                                stdout=subprocess.PIPE,
220
                                stderr=subprocess.PIPE)
221
222
        out, err = proc.communicate(bytes(certificate, encoding="utf-8"))
223
224
        # zero return code means that the certificate is valid
225
        if proc.returncode == 0:
226
            return True
227
        elif proc.returncode == 1 and "Certificate will expire" in out.decode():
228
            # 1 return code means that the certificate is invalid but such message has to be present in the proc output
229
            return False
230
        else:
231
            # the process failed because of some other reason (incorrect cert format)
232
            raise CryptographyException(OPENSSL_EXECUTABLE, args, err.decode())
233 9dbbcdae Stanislav Král
234 4a40b0d2 Stanislav Král
    def parse_cert_pem(self, cert_pem):
235 cc51ca2c Stanislav Král
        """
236 36409852 Stanislav Král
        Parses the given certificate in PEM format and returns the subject of the certificate and it's NOT_BEFORE
237
        and NOT_AFTER field
238 cc51ca2c Stanislav Král
        :param cert_pem: a certificated in a PEM format to be parsed
239 36409852 Stanislav Král
        :return: a tuple containing a subject, NOT_BEFORE and NOT_AFTER dates
240 cc51ca2c Stanislav Král
        """
241
        # run openssl x509 to view certificate content
242 36409852 Stanislav Král
        args = ["x509", "-noout", "-subject", "-startdate", "-enddate", "-in", "-"]
243 cc51ca2c Stanislav Král
244 36409852 Stanislav Král
        cert_info_raw = self.__run_for_output(args, proc_input=bytes(cert_pem, encoding="utf-8")).decode()
245
246
        # split lines
247
        results = re.split("\n", cert_info_raw)
248
        subj_line = results[0]
249
        not_before_line = results[1]
250
        not_after_line = results[2]
251
252
        # attempt to extract subject via regex
253
        match = re.search(r"subject=(.*)", subj_line)
254 4a40b0d2 Stanislav Král
        if match is None:
255 cc51ca2c Stanislav Král
            # TODO use logger
256 36409852 Stanislav Král
            print(f"Could not find subject to parse: {subj_line}")
257 cc51ca2c Stanislav Král
            return None
258 4a40b0d2 Stanislav Král
        else:
259 36409852 Stanislav Král
            # find all attributes (key = value)
260
            found = re.findall(r"\s?([^c=\s]+)\s?=\s?([^,\n]+)", match.group(1))
261 cc51ca2c Stanislav Král
            subj = Subject()
262
            for key, value in found:
263
                if key == "C":
264
                    subj.country = value
265
                elif key == "ST":
266
                    subj.state = value
267
                elif key == "L":
268
                    subj.locality = value
269
                elif key == "O":
270
                    subj.organization = value
271
                elif key == "OU":
272
                    subj.organization_unit = value
273
                elif key == "CN":
274
                    subj.common_name = value
275
                elif key == "emailAddress":
276
                    subj.email_address = value
277 36409852 Stanislav Král
278
        # extract notBefore and notAfter date fields
279
        not_before = re.search(r"notBefore=(.*)", not_before_line)
280
        not_after = re.search(r"notAfter=(.*)", not_after_line)
281
282
        # if date fields are found parse them into date objects
283
        if not_before is not None:
284
            not_before = time.strptime(not_before.group(1), NOT_AFTER_BEFORE_DATE_FORMAT)
285
        if not_after is not None:
286
            not_after = time.strptime(not_after.group(1), NOT_AFTER_BEFORE_DATE_FORMAT)
287
288
        # TODO wrapper class?
289
        # return it as a tuple
290
        return subj, not_before, not_after
291 4a40b0d2 Stanislav Král
292 c0aed2f5 Stanislav Král
293
class CryptographyException(Exception):
294
295
    def __init__(self, executable, args, message):
296
        self.executable = executable
297
        self.args = args
298
        self.message = message
299
300
    def __str__(self):
301
        return f"""
302
        EXECUTABLE: {self.executable}
303
        ARGS: {self.args}
304
        MESSAGE: {self.message}
305
        """