Projekt

Obecné

Profil

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