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