Projekt

Obecné

Profil

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