Projekt

Obecné

Profil

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