Projekt

Obecné

Profil

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