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
                if err is not None:
61
                    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 fe647b46 Stanislav Král
    def make_csr(self, subject, subject_key, subject_key_pass=""):
119 6c098d6e Stanislav Král
        """
120
        Makes a CSR (Certificate Signing Request)
121
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
        :param csr: a string containing the CSR to be signed
142
        :param issuer_pem: string containing the certificate of the issuer that will sign this CSR in PEM format
143
        :param issuer_key: string containing the private key of the issuer's certificate in PEM format
144 9dbbcdae Stanislav Král
        :param issuer_key_pass: string containing the passphrase of the private key of the issuer's certificate in PEM
145
        format
146 fe647b46 Stanislav Král
        :param config: TODO NOT USED
147
        :param extensions: extensions to be applied when signing the CSR
148 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.
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
                   config="",
173 5fdd01a6 Stanislav Král
                   extensions="",
174
                   days=30):
175 9dbbcdae Stanislav Král
        """
176
        Signs the given CSR by the given issuer CA
177
        :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 config: TODO NOT USED
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 18588728 Stanislav Král
        csr = self.make_csr(subject, subject_key, subject_key_pass=subject_key_pass)
192 5fdd01a6 Stanislav Král
        return self.sign_csr(csr, issuer_pem, issuer_key, issuer_key_pass=issuer_key_pass, extensions=extensions,
193
                             days=days)
194
195
    @staticmethod
196
    def verify_ca(certificate, key, key_pass=None):
197
        # TODO could be renamed to "verify_certificate"? This method can verify all certificates, not just CAs.
198
        # call openssl to check whether the certificate is valid to this date
199
        args = [OPENSSL_EXECUTABLE, "x509", "-checkend", "0", "-noout", "-text", "-in", "-"]
200
201
        # create a new process
202
        proc = subprocess.Popen(args, stdin=subprocess.PIPE,
203
                                stdout=subprocess.PIPE,
204
                                stderr=subprocess.PIPE)
205
206
        out, err = proc.communicate(bytes(certificate, encoding="utf-8"))
207
208
        # zero return code means that the certificate is valid
209
        if proc.returncode == 0:
210
            return True
211
        elif proc.returncode == 1 and "Certificate will expire" in out.decode():
212
            # 1 return code means that the certificate is invalid but such message has to be present in the proc output
213
            return False
214
        else:
215
            # the process failed because of some other reason (incorrect cert format)
216
            raise CryptographyException(OPENSSL_EXECUTABLE, args, err.decode())
217 9dbbcdae Stanislav Král
218 c0aed2f5 Stanislav Král
219
class CryptographyException(Exception):
220
221
    def __init__(self, executable, args, message):
222
        self.executable = executable
223
        self.args = args
224
        self.message = message
225
226
    def __str__(self):
227
        return f"""
228
        EXECUTABLE: {self.executable}
229
        ARGS: {self.args}
230
        MESSAGE: {self.message}
231
        """