Projekt

Obecné

Profil

Stáhnout (23.9 KB) Statistiky
| Větev: | Tag: | Revize:
1 4a40b0d2 Stanislav Král
import re
2 36409852 Stanislav Král
import subprocess
3
import time
4 9a55ea8a Stanislav Král
import random
5 2c8b7911 Stanislav Král
from typing import List
6 c0aed2f5 Stanislav Král
7 64cfca84 Jan Pašek
from src.constants import CRL_CONFIG
8
from src.model.certificate import Certificate
9
from src.model.private_key import PrivateKey
10 cc51ca2c Stanislav Král
from src.model.subject import Subject
11 b3c80ccb David Friesecký
from src.utils.logger import Logger
12 181e1196 Jan Pašek
from src.utils.temporary_file import TemporaryFile
13 c0aed2f5 Stanislav Král
14 76648193 Captain_Trojan
# the prefix of an rsa key
15
KEY_PREFIX = b"-----BEGIN RSA PRIVATE KEY-----"
16
17
# what every encrypted key contains
18
GUARANTEED_SUBSTRING_OF_ENCRYPTED_KEYS = "ENCRYPTED"
19
20 36409852 Stanislav Král
# encryption method to be used when generating private keys
21 c0aed2f5 Stanislav Král
PRIVATE_KEY_ENCRYPTION_METHOD = "-aes256"
22
23
# openssl executable name
24
OPENSSL_EXECUTABLE = "openssl"
25
26 36409852 Stanislav Král
# format of NOT_BEFORE NOT_AFTER date fields
27
NOT_AFTER_BEFORE_DATE_FORMAT = "%b %d %H:%M:%S %Y %Z"
28
29 3e770afd Jan Pašek
# minimal configuration file to be used for openssl req command
30
# specifies distinguished_name that references empty section only
31
# openssl requires this option to be present
32
MINIMAL_CONFIG_FILE = "[req]\ndistinguished_name = req_distinguished_name\n[req_distinguished_name]\n\n"
33
34
# section to be used to specify extensions when creating a SSCRT
35
SSCRT_SECTION = "sscrt_ext"
36
37
CA_EXTENSIONS = "basicConstraints=critical,CA:TRUE"
38
39 9a55ea8a Stanislav Král
# upper bound of the range of random serial numbers to be generated
40
MAX_SN = 4294967296
41
42 c0aed2f5 Stanislav Král
43
class CryptographyService:
44
45
    @staticmethod
46 57898b2f Stanislav Král
    def __subject_to_param_format(subject):
47
        """
48
        Converts the given subject to a dictionary containing openssl field names mapped to subject's fields
49
        :param subject: subject to be converted
50
        :return: a dictionary containing openssl field names mapped to subject's fields
51
        """
52 b3c80ccb David Friesecký
53
        Logger.debug("Function launched.")
54
55 6c098d6e Stanislav Král
        subj_dict = {}
56
        if subject.common_name is not None:
57
            subj_dict["CN"] = subject.common_name
58
        if subject.country is not None:
59
            subj_dict["C"] = subject.country
60
        if subject.locality is not None:
61
            subj_dict["L"] = subject.locality
62
        if subject.state is not None:
63
            subj_dict["ST"] = subject.state
64
        if subject.organization is not None:
65
            subj_dict["O"] = subject.organization
66
        if subject.organization_unit is not None:
67
            subj_dict["OU"] = subject.organization_unit
68
        if subject.email_address is not None:
69
            subj_dict["emailAddress"] = subject.email_address
70
71
        # merge the subject into a "subj" parameter format
72
        return "".join([f"/{key}={value}" for key, value in subj_dict.items()])
73
74
    @staticmethod
75 7444d4cb Stanislav Král
    def __run_for_output(args=None, proc_input=None, executable=OPENSSL_EXECUTABLE):
76 c0aed2f5 Stanislav Král
        """
77
        Launches a new process in which the given executable is run. STDIN and process arguments can be set.
78
        If the process ends with a non-zero then <CryptographyException> is raised.
79 4691a56f Stanislav Král
80 c0aed2f5 Stanislav Král
        :param args: Arguments to be passed to the program.
81 6c098d6e Stanislav Král
        :param proc_input: String input to be passed to the stdin of the created process.
82 c0aed2f5 Stanislav Král
        :param executable: Executable to be run (defaults to openssl)
83
        :return: If the process ends with a zero return code then the STDOUT of the process is returned as a byte array.
84
        """
85 b3c80ccb David Friesecký
86
        Logger.debug("Function launched.")
87
88 c0aed2f5 Stanislav Král
        if args is None:
89
            args = []
90
        try:
91
            # prepend the name of the executable
92
            args.insert(0, executable)
93
94
            # create a new process
95 fe647b46 Stanislav Král
            proc = subprocess.Popen(args, stdin=subprocess.PIPE if proc_input is not None else None,
96
                                    stdout=subprocess.PIPE,
97 c0aed2f5 Stanislav Král
                                    stderr=subprocess.PIPE)
98
99 6c098d6e Stanislav Král
            out, err = proc.communicate(proc_input)
100 c0aed2f5 Stanislav Král
101
            if proc.returncode != 0:
102
                # if the process did not result in zero result code, then raise an exception
103 7d0aa304 Stanislav Král
                if err is not None and len(err) > 0:
104 b3c80ccb David Friesecký
                    Logger.error("CryptographyException")
105 c0aed2f5 Stanislav Král
                    raise CryptographyException(executable, args, err.decode())
106
                else:
107 b3c80ccb David Friesecký
                    Logger.error("CryptographyException")
108 c0aed2f5 Stanislav Král
                    raise CryptographyException(executable, args,
109
                                                f""""Execution resulted in non-zero argument""")
110
111
            return out
112
        except FileNotFoundError:
113 b3c80ccb David Friesecký
            Logger.error("CryptographyException")
114 c0aed2f5 Stanislav Král
            raise CryptographyException(executable, args, f""""{executable}" not found in the current PATH.""")
115
116
    def create_private_key(self, passphrase=None):
117
        """
118
        Creates a private key with the option to encrypt it using a passphrase.
119
        :param passphrase: A passphrase to be used when encrypting the key (if none is passed then the key is not
120
        encrypted at all). Empty passphrase ("") also results in a key that is not encrypted.
121 18588728 Stanislav Král
        :return: string containing the generated private key in PEM format
122 c0aed2f5 Stanislav Král
        """
123 b3c80ccb David Friesecký
124
        Logger.debug("Function launched.")
125
126 c0aed2f5 Stanislav Král
        if passphrase is None or len(passphrase) == 0:
127 7444d4cb Stanislav Král
            return self.__run_for_output(["genrsa", "2048"]).decode()
128 c0aed2f5 Stanislav Král
        else:
129 7444d4cb Stanislav Král
            return self.__run_for_output(
130 c0aed2f5 Stanislav Král
                ["genrsa", PRIVATE_KEY_ENCRYPTION_METHOD, "-passout", f"pass:{passphrase}", "2048"]).decode()
131
132 be2df9b7 Stanislav Král
    def create_sscrt(self, subject, key, config="", extensions="", key_pass=None, days=30, sn: int = None):
133 c0aed2f5 Stanislav Král
        """
134 329216fe Stanislav Král
        Creates a self signed certificate
135 c0aed2f5 Stanislav Král
136
        :param subject: an instance of <Subject> representing the subject to be added to the certificate
137 02f63b07 Stanislav Král
        :param key: private key of the CA to be used
138 c0aed2f5 Stanislav Král
        :param config: string containing the configuration to be used
139
        :param extensions: name of the section in the configuration representing extensions
140 18588728 Stanislav Král
        :param key_pass: passphrase of the private key
141 2f5101f1 Stanislav Král
        :param days: number of days for which the certificate will be valid
142 9a55ea8a Stanislav Král
        :param sn: serial number to be set, when "None" is set a random serial number is generated
143 c0aed2f5 Stanislav Král
144 18588728 Stanislav Král
        :return: string containing the generated certificate in PEM format
145 c0aed2f5 Stanislav Král
        """
146 b3c80ccb David Friesecký
147
        Logger.debug("Function launched.")
148
149 c0aed2f5 Stanislav Král
        assert key is not None
150
        assert subject is not None
151
152 57898b2f Stanislav Král
        subj = self.__subject_to_param_format(subject)
153 c0aed2f5 Stanislav Král
154 3e770afd Jan Pašek
        # To specify extension for creating a SSCRT, one has to use a configuration
155
        # file instead of an extension file. Therefore the following code creates
156
        # the most basic configuration file with sscrt_ext section, that is later
157
        # reference in openssl req command using -extensions option.
158
        if len(config) == 0:
159 9e6f791a Jan Pašek
            config += MINIMAL_CONFIG_FILE
160 3e770afd Jan Pašek
        config += "\n[ " + SSCRT_SECTION + " ]" + "\n" + extensions
161
162 c0aed2f5 Stanislav Král
        with TemporaryFile("openssl.conf", config) as conf_path:
163 2f5101f1 Stanislav Král
            args = ["req", "-x509", "-new", "-subj", subj, "-days", f"{days}",
164 c0aed2f5 Stanislav Král
                    "-key", "-"]
165 3e770afd Jan Pašek
166 9a55ea8a Stanislav Král
            # serial number passed, use it when generating the certificate,
167
            # without passing it openssl generates a random one
168
            if sn is not None:
169
                args.extend(["-set_serial", str(sn)])
170
171 c0aed2f5 Stanislav Král
            if len(config) > 0:
172
                args.extend(["-config", conf_path])
173
            if len(extensions) > 0:
174 64cfca84 Jan Pašek
                args.extend(["-extensions", SSCRT_SECTION])  # when creating SSCRT, section references section in config
175 c0aed2f5 Stanislav Král
176
            # it would be best to not send the pass phrase at all, but for some reason pytest then prompts for
177
            # the pass phrase (this does not happen when run from pycharm)
178
179 fe647b46 Stanislav Král
            #  add the passphrase even when None is passed. Otherwise when running tests with pytest some tests freeze
180
            # waiting for the passphrase to be typed in
181 18588728 Stanislav Král
            args.extend(["-passin", f"pass:{key_pass}"])
182 c0aed2f5 Stanislav Král
183 7444d4cb Stanislav Král
            return self.__run_for_output(args, proc_input=bytes(key, encoding="utf-8")).decode()
184 6c098d6e Stanislav Král
185 87fd5afc Stanislav Král
    def __create_csr(self, subject, key, key_pass=""):
186 6c098d6e Stanislav Král
        """
187 bdf9a46c Stanislav Král
        Creates a CSR (Certificate Signing Request)
188 6c098d6e Stanislav Král
189
        :param subject: an instance of <Subject> representing the subject to be added to the CSR
190 87fd5afc Stanislav Král
        :param key: the private key of the subject to be used to generate the CSR
191
        :param key_pass: passphrase of the subject's private key
192 18588728 Stanislav Král
        :return: string containing the generated certificate signing request in PEM format
193 6c098d6e Stanislav Král
        """
194
195 b3c80ccb David Friesecký
        Logger.debug("Function launched.")
196
197 57898b2f Stanislav Král
        subj_param = self.__subject_to_param_format(subject)
198 6c098d6e Stanislav Král
199
        args = ["req", "-new", "-subj", subj_param, "-key", "-"]
200
201 fe647b46 Stanislav Král
        # add the passphrase even when None is passed. Otherwise when running tests with pytest some tests freeze
202
        # waiting for the passphrase to be typed in
203 87fd5afc Stanislav Král
        args.extend(["-passin", f"pass:{key_pass}"])
204 6c098d6e Stanislav Král
205 87fd5afc Stanislav Král
        return self.__run_for_output(args, proc_input=bytes(key, encoding="utf-8")).decode()
206 c0aed2f5 Stanislav Král
207 9a55ea8a Stanislav Král
    def __sign_csr(self, csr, issuer_pem, issuer_key, issuer_key_pass=None, extensions="", days=30, sn: int = None):
208 fe647b46 Stanislav Král
        """
209
        Signs the given CSR by the given issuer CA
210 ad068f9d Stanislav Král
211 fe647b46 Stanislav Král
        :param csr: a string containing the CSR to be signed
212
        :param issuer_pem: string containing the certificate of the issuer that will sign this CSR in PEM format
213
        :param issuer_key: string containing the private key of the issuer's certificate in PEM format
214 9dbbcdae Stanislav Král
        :param issuer_key_pass: string containing the passphrase of the private key of the issuer's certificate in PEM
215
        format
216 fe647b46 Stanislav Král
        :param extensions: extensions to be applied when signing the CSR
217 c4b2f4d2 Stanislav Král
        :param days: number of days for which the certificate will be valid
218 9a55ea8a Stanislav Král
        :param sn: serial number to be set, when "None" is set a random serial number is generated
219 fe647b46 Stanislav Král
        :return: string containing the generated and signed certificate in PEM format
220
        """
221
222 b3c80ccb David Friesecký
        Logger.debug("Function launched.")
223
224 fe647b46 Stanislav Král
        # concatenate CSR, issuer certificate and issuer's key (will be used in the openssl call)
225
        proc_input = csr + issuer_pem + issuer_key
226
227 9a55ea8a Stanislav Král
        # TODO find a better way to generate a random serial number or let openssl generate a .srl file
228
        # when serial number is not passed generate a random one
229
        if sn is None:
230
            sn = random.randint(0, MAX_SN)
231
232 fe647b46 Stanislav Král
        # prepare openssl parameters...
233
        # CSR, CA and CA's private key will be passed via stdin (that's the meaning of the '-' symbol)
234 2510f01a Stanislav Král
        params = ["x509", "-req", "-in", "-", "-CA", "-", "-CAkey", "-", "-CAcreateserial", "-days", str(days),
235
                  "-set_serial", str(sn)]
236 fe647b46 Stanislav Král
237
        with TemporaryFile("extensions.conf", extensions) as ext_path:
238
            # add the passphrase even when None is passed. Otherwise when running tests with pytest some tests freeze
239
            # waiting for the passphrase to be typed in
240
            params.extend(["-passin", f"pass:{issuer_key_pass}"])
241
242
            if len(extensions) > 0:
243
                params.extend(["-extfile", ext_path])
244
245 7444d4cb Stanislav Král
            return self.__run_for_output(params, proc_input=(bytes(proc_input, encoding="utf-8"))).decode()
246 fe647b46 Stanislav Král
247 18588728 Stanislav Král
    def create_crt(self, subject, subject_key, issuer_pem, issuer_key, subject_key_pass=None, issuer_key_pass=None,
248 5fdd01a6 Stanislav Král
                   extensions="",
249 9a55ea8a Stanislav Král
                   days=30,
250
                   sn: int = None):
251 9dbbcdae Stanislav Král
        """
252 61a42455 Stanislav Král
        Creates a certificate by using the given subject, subject's key, issuer and its key.
253 ad068f9d Stanislav Král
254 9dbbcdae Stanislav Král
        :param subject: subject to be added to the created certificate
255 18588728 Stanislav Král
        :param subject_key: string containing the private key to be used when creating the certificate in PEM format
256 9dbbcdae Stanislav Král
        :param issuer_key: string containing the private key of the issuer's certificate in PEM format
257
        :param issuer_pem: string containing the certificate of the issuer that will sign this CSR in PEM format
258 36409852 Stanislav Král
        :param subject_key_pass: string containing the passphrase of the private key used when creating the certificate
259
        in PEM format
260 9dbbcdae Stanislav Král
        :param issuer_key_pass: string containing the passphrase of the private key of the issuer's certificate in PEM
261
        format
262
        :param extensions: extensions to be applied when creating the certificate
263 5fdd01a6 Stanislav Král
        :param days: number of days for which the certificate will be valid
264 9a55ea8a Stanislav Král
        :param sn: serial number to be set, when "None" is set a random serial number is generated
265 18588728 Stanislav Král
        :return: string containing the generated certificate in PEM format
266 9dbbcdae Stanislav Král
        """
267 b3c80ccb David Friesecký
268
        Logger.debug("Function launched.")
269
270 87fd5afc Stanislav Král
        csr = self.__create_csr(subject, subject_key, key_pass=subject_key_pass)
271 87a7a4a5 Stanislav Král
        return self.__sign_csr(csr, issuer_pem, issuer_key, issuer_key_pass=issuer_key_pass, extensions=extensions,
272 9a55ea8a Stanislav Král
                               days=days, sn=sn)
273 5fdd01a6 Stanislav Král
274
    @staticmethod
275 61a42455 Stanislav Král
    def verify_cert(certificate):
276
        """
277
        Verifies whether the given certificate is not expired.
278
279
        :param certificate: certificate to be verified in PEM format
280
        :return: Returns `true` if the certificate is not expired, `false` when expired.
281
        """
282 b3c80ccb David Friesecký
283
        Logger.debug("Function launched.")
284
285 5fdd01a6 Stanislav Král
        # call openssl to check whether the certificate is valid to this date
286
        args = [OPENSSL_EXECUTABLE, "x509", "-checkend", "0", "-noout", "-text", "-in", "-"]
287
288
        # create a new process
289
        proc = subprocess.Popen(args, stdin=subprocess.PIPE,
290
                                stdout=subprocess.PIPE,
291
                                stderr=subprocess.PIPE)
292
293
        out, err = proc.communicate(bytes(certificate, encoding="utf-8"))
294
295
        # zero return code means that the certificate is valid
296
        if proc.returncode == 0:
297
            return True
298
        elif proc.returncode == 1 and "Certificate will expire" in out.decode():
299
            # 1 return code means that the certificate is invalid but such message has to be present in the proc output
300
            return False
301
        else:
302
            # the process failed because of some other reason (incorrect cert format)
303 b3c80ccb David Friesecký
            Logger.error("CryptographyException")
304 5fdd01a6 Stanislav Král
            raise CryptographyException(OPENSSL_EXECUTABLE, args, err.decode())
305 9dbbcdae Stanislav Král
306 19e5260d Stanislav Král
    def extract_public_key_from_private_key(self, private_key_pem: str, passphrase=None) -> str:
307 5c748d51 Stanislav Král
        """
308 e8face67 Stanislav Král
        Extracts a public key from the given private key passed in PEM format
309
        :param private_key_pem: PEM data representing the private key from which a public key should be extracted
310
        :param passphrase: passphrase to be provided when the supplied private key is encrypted
311 5c748d51 Stanislav Král
        :return: a string containing the extracted public key in PEM format
312
        """
313 b3c80ccb David Friesecký
314
        Logger.debug("Function launched.")
315
316 e8face67 Stanislav Král
        args = ["rsa", "-in", "-", "-pubout"]
317
        if passphrase is not None:
318
            args.extend(["-passin", f"pass:{passphrase}"])
319
        return self.__run_for_output(args, proc_input=bytes(private_key_pem, encoding="utf-8")).decode()
320 5c748d51 Stanislav Král
321 19e5260d Stanislav Král
    def extract_public_key_from_certificate(self, cert_pem: str) -> str:
322
        """
323
        Extracts a public key from the given certificate passed in PEM format
324
        :param cert_pem: PEM data representing a certificate from which a public key should be extracted
325
        :return: a string containing the extracted public key in PEM format
326
        """
327 b3c80ccb David Friesecký
328
        Logger.debug("Function launched.")
329
330 19e5260d Stanislav Král
        # extracting public key from a certificate does not seem to require a passphrase even when
331
        # signed using an encrypted PK
332
        args = ["x509", "-in", "-", "-noout", "-pubkey"]
333
        return self.__run_for_output(args, proc_input=bytes(cert_pem, encoding="utf-8")).decode()
334
335 4a40b0d2 Stanislav Král
    def parse_cert_pem(self, cert_pem):
336 cc51ca2c Stanislav Král
        """
337 36409852 Stanislav Král
        Parses the given certificate in PEM format and returns the subject of the certificate and it's NOT_BEFORE
338
        and NOT_AFTER field
339 cc51ca2c Stanislav Král
        :param cert_pem: a certificated in a PEM format to be parsed
340 36409852 Stanislav Král
        :return: a tuple containing a subject, NOT_BEFORE and NOT_AFTER dates
341 cc51ca2c Stanislav Král
        """
342 b3c80ccb David Friesecký
343
        Logger.debug("Function launched.")
344
345 cc51ca2c Stanislav Král
        # run openssl x509 to view certificate content
346 36409852 Stanislav Král
        args = ["x509", "-noout", "-subject", "-startdate", "-enddate", "-in", "-"]
347 cc51ca2c Stanislav Král
348 36409852 Stanislav Král
        cert_info_raw = self.__run_for_output(args, proc_input=bytes(cert_pem, encoding="utf-8")).decode()
349
350
        # split lines
351
        results = re.split("\n", cert_info_raw)
352 3e770afd Jan Pašek
        subj_line = results[0].strip()
353
        not_before_line = results[1].strip()
354
        not_after_line = results[2].strip()
355 36409852 Stanislav Král
356
        # attempt to extract subject via regex
357
        match = re.search(r"subject=(.*)", subj_line)
358 4a40b0d2 Stanislav Král
        if match is None:
359 cc51ca2c Stanislav Král
            # TODO use logger
360 36409852 Stanislav Král
            print(f"Could not find subject to parse: {subj_line}")
361 cc51ca2c Stanislav Král
            return None
362 4a40b0d2 Stanislav Král
        else:
363 36409852 Stanislav Král
            # find all attributes (key = value)
364
            found = re.findall(r"\s?([^c=\s]+)\s?=\s?([^,\n]+)", match.group(1))
365 cc51ca2c Stanislav Král
            subj = Subject()
366
            for key, value in found:
367
                if key == "C":
368 3e770afd Jan Pašek
                    subj.country = value.strip()
369 cc51ca2c Stanislav Král
                elif key == "ST":
370 3e770afd Jan Pašek
                    subj.state = value.strip()
371 cc51ca2c Stanislav Král
                elif key == "L":
372 3e770afd Jan Pašek
                    subj.locality = value.strip()
373 cc51ca2c Stanislav Král
                elif key == "O":
374 3e770afd Jan Pašek
                    subj.organization = value.strip()
375 cc51ca2c Stanislav Král
                elif key == "OU":
376 3e770afd Jan Pašek
                    subj.organization_unit = value.strip()
377 cc51ca2c Stanislav Král
                elif key == "CN":
378 3e770afd Jan Pašek
                    subj.common_name = value.strip()
379 cc51ca2c Stanislav Král
                elif key == "emailAddress":
380 3e770afd Jan Pašek
                    subj.email_address = value.strip()
381 36409852 Stanislav Král
382
        # extract notBefore and notAfter date fields
383
        not_before = re.search(r"notBefore=(.*)", not_before_line)
384
        not_after = re.search(r"notAfter=(.*)", not_after_line)
385
386
        # if date fields are found parse them into date objects
387
        if not_before is not None:
388 4faab824 Jan Pašek
            not_before = time.strptime(not_before.group(1).strip(), NOT_AFTER_BEFORE_DATE_FORMAT)
389 36409852 Stanislav Král
        if not_after is not None:
390 4faab824 Jan Pašek
            not_after = time.strptime(not_after.group(1).strip(), NOT_AFTER_BEFORE_DATE_FORMAT)
391 36409852 Stanislav Král
392
        # TODO wrapper class?
393
        # return it as a tuple
394
        return subj, not_before, not_after
395 4a40b0d2 Stanislav Král
396 81dbb479 Jan Pašek
    def get_openssl_version(self) -> str:
397
        """
398
        Get version of the OpenSSL installed on the system
399
        :return: version of the OpenSSL as returned from the process
400
        """
401 b3c80ccb David Friesecký
402
        Logger.debug("Function launched.")
403
404 81dbb479 Jan Pašek
        return self.__run_for_output(["version"]).decode("utf-8")
405
406 64cfca84 Jan Pašek
    def generate_crl(self, cert: Certificate, key: PrivateKey, index_file_path: str) -> str:
407 0fd6d825 Jan Pašek
        """
408
        Generate a CertificateRevocationList for a specified
409
        certificate authority.
410
411 64cfca84 Jan Pašek
        :param key: key that is used to sign the CRL (must belong to the given certificate)
412
        :param cert: Certificate of the certificate authority that issue the CRL
413 0fd6d825 Jan Pašek
        :param index_file_path: path to a file that contains the openssl index with all revoked certificates
414
        :return: CRL encoded in PEM format string
415
        """
416 b3c80ccb David Friesecký
417
        Logger.debug("Function launched.")
418
419 dd91fb7a Jan Pašek
        # openssl ca requires the .srl file to exists, therefore a dummy, unused file is created
420 64cfca84 Jan Pašek
        with TemporaryFile("serial.srl", "0") as serial_file, \
421 76648193 Captain_Trojan
                TemporaryFile("crl.conf", CRL_CONFIG % (index_file_path, serial_file)) as config_file, \
422
                TemporaryFile("certificate.pem", cert.pem_data) as cert_file, \
423
                TemporaryFile("private_key.pem", key.private_key) as key_file:
424 64cfca84 Jan Pašek
            args = ["ca", "-config", config_file, "-gencrl", "-keyfile", key_file, "-cert", cert_file, "-outdir", "."]
425 94e89bb1 Jan Pašek
426
            if key.password is not None and key.password != "":
427
                args.extend(["-passin", f"pass:{key.password}"])
428
429 64cfca84 Jan Pašek
            return self.__run_for_output(args).decode("utf-8")
430
431 7f9b2c58 Captain_Trojan
    def generate_ocsp(self, cert, key, index_path, der_ocsp_request):
432
        """
433
        Generate an OCSP Response from an OCSP Request given the issuer cert, issuer cert key and the index file.
434
        The OSCP Response is signed by the CA itself (recommended way according to multiple sources).
435
436
        :param cert: issuer certificate
437
        :param key: corresponding key
438
        :param index_path: path/to/the/generated/index/file
439
        :param der_ocsp_request: DER encoded OCSP Request
440
        :return: DER encoded OCSP Response
441
        """
442 b3c80ccb David Friesecký
443
        Logger.debug("Function launched.")
444
445 7f9b2c58 Captain_Trojan
        with TemporaryFile("certificate.pem", cert.pem_data) as ca_certificate, \
446 76648193 Captain_Trojan
                TemporaryFile("private_key.pem", key.private_key) as key_file, \
447
                TemporaryFile("request.der", der_ocsp_request) as request_file:
448 7f9b2c58 Captain_Trojan
            args = ["ocsp", "-index", index_path, "-CA", ca_certificate, "-rsigner", ca_certificate, "-rkey", key_file,
449
                    "-reqin", request_file, "-respout", "-"]
450
451
            if key.password is not None and key.password != "":
452
                args.extend(["-passin", f"pass:{key.password}"])
453
454
            return self.__run_for_output(args)
455
456 76648193 Captain_Trojan
    def verify_key(self, key, passphrase):
457
        """
458
        Verifies whether the provided key is encrypted by the provided passphrase. If passphrase is none, verifies
459
        that the provided key is unencrypted.
460
        :param key: target key
461
        :param passphrase: target passphrase or None
462
        :return: True if the condition is fulfilled, else False
463
        """
464
        if passphrase is None:
465 9a466331 Captain_Trojan
            if re.search(GUARANTEED_SUBSTRING_OF_ENCRYPTED_KEYS, key) is not None:
466
                return False
467
            else:
468
                try:
469
                    with TemporaryFile("tested_key.pem", key) as f:
470
                        ret = self.__run_for_output(["rsa", "-in", f, "-inform", "PEM"])
471
                    return ret.startswith(KEY_PREFIX)
472
                except CryptographyException:
473
                    return False
474 76648193 Captain_Trojan
        else:
475
            if re.search(GUARANTEED_SUBSTRING_OF_ENCRYPTED_KEYS, key) is None:
476
                return False
477
            else:
478
                try:
479
                    with TemporaryFile("tested_key.pem", key) as f:
480
                        ret = self.__run_for_output(["rsa", "-in", f, "-passin", f"pass:{passphrase}"])
481
                    return ret.startswith(KEY_PREFIX)
482
                except CryptographyException:
483
                    return False
484
485 2c8b7911 Stanislav Král
    def generate_pkcs_identity(self, cert_pem: str, cert_key_pem: str, identity_name: str, identity_passphrase: str,
486
                               chain_of_trust_pems: List[str], cert_key_passphrase: str = None):
487
        """
488
        Generates a PKCS12 identity of the given child certificate while including the given chain of trust.
489
490
        :param cert_pem: PEM of the certificate whose identity should be created
491
        :param cert_key_pem: PEM of the private key used to sign the certificate whose identity should be created
492
        :param identity_name: the name to be given to the identity created
493
        :param chain_of_trust_pems: list of PEMs representing certificates present in the chain of trust of the certificate
494
        whose identity should be created
495
        :param identity_passphrase: passphrase to be used when encrypting the identity
496
        :param cert_key_passphrase: passphrase of the key used to sign the certificate whose identity should be created
497
        :return: byte array containing the generated identity
498
        """
499
        with TemporaryFile("cert_key.pem", cert_key_pem) as cert_key_pem_file:
500 dea7f12f Stanislav Král
            if identity_passphrase is None:
501
                identity_passphrase = ""
502
503 df7f5fda Stanislav Král
            args = ["pkcs12", "-export", "-name", identity_name, "-in", "-", "-inkey", cert_key_pem_file, "-passout", f"pass:{identity_passphrase}", "-passin", f"pass:{cert_key_passphrase}"]
504
            proc_input = cert_pem
505
            # when the chain of trust is not empty append the -CAfile argument and the concatenated list of CoT PEMs
506
            # to the input of the process to be launched
507
            if len(chain_of_trust_pems) > 0:
508
                args.extend(["-CAfile", "-", ])
509
                proc_input += "".join(chain_of_trust_pems)
510 2c8b7911 Stanislav Král
            return self.__run_for_output(args,
511 df7f5fda Stanislav Král
                                         proc_input=bytes(proc_input, encoding="utf-8"))
512 2c8b7911 Stanislav Král
513 c0aed2f5 Stanislav Král
514
class CryptographyException(Exception):
515
516
    def __init__(self, executable, args, message):
517
        self.executable = executable
518
        self.args = args
519
        self.message = message
520
521
    def __str__(self):
522 b3c80ccb David Friesecký
        # TODO check log is valid here
523 da0fc952 Stanislav Král
        # TODO Standa does not think so...
524 b3c80ccb David Friesecký
        msg = f"""
525 c0aed2f5 Stanislav Král
        EXECUTABLE: {self.executable}
526
        ARGS: {self.args}
527
        MESSAGE: {self.message}
528
        """
529 b3c80ccb David Friesecký
530
        Logger.error(msg)
531
        return msg