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 5fdd01a6 Stanislav Král
    def sign_csr(self, csr, issuer_pem, issuer_key, issuer_key_pass=None, config="", 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 config: TODO NOT USED
148
        :param extensions: extensions to be applied when signing the CSR
149 5fdd01a6 Stanislav Král
        :param days: number of days for which the certificate will be valid TODO this parameter is not in the SW arch.
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
                   config="",
174 5fdd01a6 Stanislav Král
                   extensions="",
175
                   days=30):
176 9dbbcdae Stanislav Král
        """
177 ad068f9d Stanislav Král
        Creates a certificate by using the given subject, subject's key, issuer and it's key.
178
179 9dbbcdae Stanislav Král
        :param subject: subject to be added to the created certificate
180 18588728 Stanislav Král
        :param subject_key: string containing the private key to be used when creating the certificate in PEM format
181 9dbbcdae Stanislav Král
        :param issuer_key: string containing the private key of the issuer's certificate in PEM format
182
        :param issuer_pem: string containing the certificate of the issuer that will sign this CSR in PEM format
183
        :param issuer_key: string containing the private key of the issuer's certificate in PEM format
184 18588728 Stanislav Král
        :param subject_key_pass: string containing the passphrase of the private key used when creating the certificate in PEM
185 9dbbcdae Stanislav Král
        format
186
        :param issuer_key_pass: string containing the passphrase of the private key of the issuer's certificate in PEM
187
        format
188
        :param config: TODO NOT USED
189
        :param extensions: extensions to be applied when creating the certificate
190 5fdd01a6 Stanislav Král
        :param days: number of days for which the certificate will be valid
191 18588728 Stanislav Král
        :return: string containing the generated certificate in PEM format
192 9dbbcdae Stanislav Král
        """
193 bdf9a46c Stanislav Král
        csr = self.create_csr(subject, subject_key, subject_key_pass=subject_key_pass)
194 5fdd01a6 Stanislav Král
        return self.sign_csr(csr, issuer_pem, issuer_key, issuer_key_pass=issuer_key_pass, extensions=extensions,
195
                             days=days)
196
197
    @staticmethod
198 ad068f9d Stanislav Král
    def verify_ca(certificate):
199 5fdd01a6 Stanislav Král
        # TODO could be renamed to "verify_certificate"? This method can verify all certificates, not just CAs.
200
        # call openssl to check whether the certificate is valid to this date
201
        args = [OPENSSL_EXECUTABLE, "x509", "-checkend", "0", "-noout", "-text", "-in", "-"]
202
203
        # create a new process
204
        proc = subprocess.Popen(args, stdin=subprocess.PIPE,
205
                                stdout=subprocess.PIPE,
206
                                stderr=subprocess.PIPE)
207
208
        out, err = proc.communicate(bytes(certificate, encoding="utf-8"))
209
210
        # zero return code means that the certificate is valid
211
        if proc.returncode == 0:
212
            return True
213
        elif proc.returncode == 1 and "Certificate will expire" in out.decode():
214
            # 1 return code means that the certificate is invalid but such message has to be present in the proc output
215
            return False
216
        else:
217
            # the process failed because of some other reason (incorrect cert format)
218
            raise CryptographyException(OPENSSL_EXECUTABLE, args, err.decode())
219 9dbbcdae Stanislav Král
220 c0aed2f5 Stanislav Král
221
class CryptographyException(Exception):
222
223
    def __init__(self, executable, args, message):
224
        self.executable = executable
225
        self.args = args
226
        self.message = message
227
228
    def __str__(self):
229
        return f"""
230
        EXECUTABLE: {self.executable}
231
        ARGS: {self.args}
232
        MESSAGE: {self.message}
233
        """