Projekt

Obecné

Profil

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