Projekt

Obecné

Profil

Stáhnout (16.6 KB) Statistiky
| Větev: | Tag: | Revize:
1 ef65f488 Stanislav Král
from typing import List
2
3 151e7604 Jan Pašek
from injector import inject
4
5 20b33bd4 Jan Pašek
from src.config.configuration import Configuration
6
from src.constants import ROOT_CA_ID, INTERMEDIATE_CA_ID, CA_ID, CERTIFICATE_ID, CERTIFICATE_STATES, \
7
    CERTIFICATE_REVOCATION_REASONS
8 4a40b0d2 Stanislav Král
from src.dao.certificate_repository import CertificateRepository
9
from src.model.certificate import Certificate
10 313b647b Stanislav Král
from src.model.private_key import PrivateKey
11 4a40b0d2 Stanislav Král
from src.model.subject import Subject
12
from src.services.cryptography import CryptographyService
13
14 313b647b Stanislav Král
import time
15
16
DATE_FORMAT = "%d.%m.%Y %H:%M:%S"
17 bbcb7c89 Stanislav Král
CA_EXTENSIONS = "basicConstraints=critical,CA:TRUE"
18 20b33bd4 Jan Pašek
CRL_EXTENSION = "crlDistributionPoints=URI:"
19
OCSP_EXTENSION = "authorityInfoAccess=OCSP;URI:"
20
STATUS_REVOKED = "revoked"
21
STATUS_VALID = "valid"
22 313b647b Stanislav Král
23 4a40b0d2 Stanislav Král
24
class CertificateService:
25
26 151e7604 Jan Pašek
    @inject
27 20b33bd4 Jan Pašek
    def __init__(self, cryptography_service: CryptographyService,
28
                 certificate_repository: CertificateRepository,
29
                 configuration: Configuration):
30 4a40b0d2 Stanislav Král
        self.cryptography_service = cryptography_service
31
        self.certificate_repository = certificate_repository
32 20b33bd4 Jan Pašek
        self.configuration = configuration
33 4a40b0d2 Stanislav Král
34 bbcb7c89 Stanislav Král
    # TODO usages present in method parameters but not in class diagram
35 ca3ac7c0 Stanislav Král
    def create_root_ca(self, key: PrivateKey, subject: Subject, extensions: str = "", config: str = "",
36 2f5101f1 Stanislav Král
                       usages=None, days=30):
37 a6727aa9 Stanislav Král
        """
38
        Creates a root CA certificate based on the given parameters.
39
        :param key: Private key to be used when generating the certificate
40
        :param subject: Subject to be used put into the certificate
41
        :param config: String containing the configuration to be used
42
        :param extensions: Name of the section in the configuration representing extensions
43
        :param usages: A dictionary containing usages of the certificate to be generated (see constants.py)
44 2f5101f1 Stanislav Král
        :param days: Number of days for which the generated cert. will be considered valid
45 a6727aa9 Stanislav Král
        :return: An instance of Certificate class representing the generated root CA cert
46
        """
47 ca3ac7c0 Stanislav Král
        if usages is None:
48
            usages = {}
49
50 20b33bd4 Jan Pašek
        cert_id = self.certificate_repository.get_next_id()
51
        extensions = extensions + "\n" + CRL_EXTENSION + " " + self.__get_crl_endpoint(cert_id)
52
        extensions = extensions + "\n" + OCSP_EXTENSION + " " + self.__get_ocsp_endpoint(cert_id)
53
54 313b647b Stanislav Král
        # create a new self signed  certificate
55
        cert_pem = self.cryptography_service.create_sscrt(subject, key.private_key, key_pass=key.password,
56 2f5101f1 Stanislav Král
                                                          extensions=extensions, config=config, days=days)
57 ca3ac7c0 Stanislav Král
        # specify CA usage
58
        usages[CA_ID] = True
59
60 4c19a9b1 Stanislav Král
        # wrap into Certificate class
61 a6727aa9 Stanislav Král
        certificate = self.__create_wrapper(cert_pem, key.private_key_id, usages, 0,
62 4c19a9b1 Stanislav Král
                                            ROOT_CA_ID)
63 313b647b Stanislav Král
64
        # store the wrapper into the repository
65
        created_id = self.certificate_repository.create(certificate)
66
67
        # assign the generated ID to the inserted certificate
68
        certificate.certificate_id = created_id
69 4a40b0d2 Stanislav Král
70 313b647b Stanislav Král
        return certificate
71 10fab051 Stanislav Král
72 a6727aa9 Stanislav Král
    def __create_wrapper(self, cert_pem, private_key_id, usages, parent_id, cert_type):
73
        """
74 a4e818dc Jan Pašek
        Wraps the given parameters using the Certificate class. Uses CryptographyService to find out the notBefore and
75 a6727aa9 Stanislav Král
        notAfter fields.
76
        :param cert_pem: PEM of the cert. to be wrapped
77
        :param private_key_id: ID of the private key used to create the given certificate
78
        :param usages: A dictionary containing usages of the generated certificate generated (see constants.py)
79
        :param parent_id: ID of the CA that issued this certificate
80
        :param cert_type: Type of this certificate (see constants.py)
81
        :return: An instance of the Certificate class wrapping the values passed  via method parameters
82
        """
83 4c19a9b1 Stanislav Král
        # parse the generated pem for subject and notBefore/notAfter fields
84 a6727aa9 Stanislav Král
        # TODO this could be improved in the future in such way that calling openssl is not required to parse the dates
85 4c19a9b1 Stanislav Král
        subj, not_before, not_after = self.cryptography_service.parse_cert_pem(cert_pem)
86
        # format the parsed date
87
        not_before_formatted = time.strftime(DATE_FORMAT, not_before)
88
        not_after_formatted = time.strftime(DATE_FORMAT, not_after)
89
90
        # create a certificate wrapper
91 a6727aa9 Stanislav Král
        certificate = Certificate(-1, subj.common_name, not_before_formatted, not_after_formatted, cert_pem,
92 4c19a9b1 Stanislav Král
                                  private_key_id, cert_type, parent_id, usages)
93
94
        return certificate
95
96 bbcb7c89 Stanislav Král
    # TODO config parameter present in class diagram but not here (unused)
97
    def create_ca(self, subject_key: PrivateKey, subject: Subject, issuer_cert: Certificate, issuer_key: PrivateKey,
98 ca3ac7c0 Stanislav Král
                  extensions: str = "", days: int = 30, usages=None):
99 a6727aa9 Stanislav Král
        """
100
        Creates an intermediate CA certificate issued by the given parent CA.
101
        :param subject_key: Private key to be used when generating the certificate
102
        :param subject: Subject to be used put into the certificate
103
        :param issuer_cert: Issuer certificate that will sign the CSR required to create an intermediate CA
104
        :param issuer_key: PK used to generate the issuer certificate
105
        :param extensions: Extensions to be used when generating the certificate
106
        :param usages: A dictionary containing usages of the certificate to be generated (see constants.py)
107
        :param days: Number of days for which the generated cert. will be considered valid
108
        :return: An instance of Certificate class representing the generated intermediate CA cert
109
        """
110 ca3ac7c0 Stanislav Král
        if usages is None:
111
            usages = {}
112
113 bbcb7c89 Stanislav Král
        extensions = extensions + "\n" + CA_EXTENSIONS
114 20b33bd4 Jan Pašek
        # Add CRL and OCSP distribution point to certificate extensions
115
        cert_id = self.certificate_repository.get_next_id()
116
        extensions = extensions + "\n" + CRL_EXTENSION + " " + self.__get_crl_endpoint(cert_id)
117
        extensions = extensions + "\n" + OCSP_EXTENSION + " " + self.__get_ocsp_endpoint(cert_id)
118
119 bbcb7c89 Stanislav Král
        # TODO implement AIA URI via extensions
120
        cert_pem = self.cryptography_service.create_crt(subject, subject_key.private_key, issuer_cert.pem_data,
121
                                                        issuer_key.private_key,
122
                                                        subject_key_pass=subject_key.password,
123
                                                        issuer_key_pass=issuer_key.password, extensions=extensions,
124
                                                        days=days)
125
126 4c19a9b1 Stanislav Král
        # specify CA usage
127
        usages[CA_ID] = True
128
129
        # wrap into Certificate class
130 a6727aa9 Stanislav Král
        self.__create_wrapper(cert_pem, subject_key.private_key_id, usages,
131 4c19a9b1 Stanislav Král
                              issuer_cert.certificate_id, INTERMEDIATE_CA_ID)
132
133 bbcb7c89 Stanislav Král
        # parse the generated pem for subject and notBefore/notAfter fields
134
        subj, not_before, not_after = self.cryptography_service.parse_cert_pem(cert_pem)
135
136
        # format the parsed date
137
        not_before_formatted = time.strftime(DATE_FORMAT, not_before)
138
        not_after_formatted = time.strftime(DATE_FORMAT, not_after)
139
140 ca3ac7c0 Stanislav Král
        # specify CA usage
141
        usages[CA_ID] = True
142
143 bbcb7c89 Stanislav Král
        # create a certificate wrapper
144
        certificate = Certificate(-1, subject.common_name, not_before_formatted, not_after_formatted, cert_pem,
145 ca3ac7c0 Stanislav Král
                                  subject_key.private_key_id, INTERMEDIATE_CA_ID, issuer_cert.certificate_id, usages)
146 bbcb7c89 Stanislav Král
147
        # store the wrapper into the repository
148
        created_id = self.certificate_repository.create(certificate)
149
150
        # assign the generated ID to the inserted certificate
151
        certificate.certificate_id = created_id
152
153
        return certificate
154
155 4c19a9b1 Stanislav Král
    def create_end_cert(self, subject_key: PrivateKey, subject: Subject, issuer_cert: Certificate,
156
                        issuer_key: PrivateKey,
157
                        extensions: str = "", days: int = 30, usages=None):
158 a6727aa9 Stanislav Král
        """
159
        Creates an end certificate issued by the given parent CA.
160
        :param subject_key: Private key to be used when generating the certificate
161
        :param subject: Subject to be used put into the certificate
162
        :param issuer_cert: Issuer certificate that will sign the CSR required to create an intermediate CA
163
        :param issuer_key: PK used to generate the issuer certificate
164
        :param extensions: Extensions to be used when generating the certificate
165
        :param usages: A dictionary containing usages of the certificate to be generated (see constants.py)
166
        :param days: Number of days for which the generated cert. will be considered valid
167
        :return: An instance of Certificate class representing the generated cert
168
        """
169 4c19a9b1 Stanislav Král
        if usages is None:
170
            usages = {}
171
172
        # generate a new certificate
173
        cert_pem = self.cryptography_service.create_crt(subject, subject_key.private_key, issuer_cert.pem_data,
174
                                                        issuer_key.private_key,
175
                                                        subject_key_pass=subject_key.password,
176
                                                        issuer_key_pass=issuer_key.password, extensions=extensions,
177
                                                        days=days)
178
179
        # wrap the generated certificate using Certificate class
180 a6727aa9 Stanislav Král
        certificate = self.__create_wrapper(cert_pem, subject_key.private_key_id, usages,
181 4c19a9b1 Stanislav Král
                                            issuer_cert.certificate_id, CERTIFICATE_ID)
182
183
        created_id = self.certificate_repository.create(certificate)
184
185
        certificate.certificate_id = created_id
186
187
        return certificate
188
189 10fab051 Stanislav Král
    def get_certificate(self, unique_id: int) -> Certificate:
190 a6727aa9 Stanislav Král
        """
191
        Tries to fetch a certificate from the certificate repository using a given id.
192
        :param unique_id: ID of the certificate to be fetched
193
        :return: Instance of the Certificate class containing a certificate with the given id or `None` if such
194
        certificate is not found
195
        """
196 10fab051 Stanislav Král
        return self.certificate_repository.read(unique_id)
197 2a90f4fd Stanislav Král
198 ef65f488 Stanislav Král
    def get_certificates(self, cert_type=None) -> List[Certificate]:
199 a6727aa9 Stanislav Král
        """
200
        Tries to fetch a list of all certificates from the certificate repository. Using the `cert_type` parameter only
201
        certificates of the given type can be returned.
202
        :param cert_type: Type of certificates to be returned
203
        :return: List of instances of the Certificate class representing all certificates present in the certificate
204
        repository. An empty list is returned when no certificates are found.
205
        """
206 2a90f4fd Stanislav Král
        return self.certificate_repository.read_all(cert_type)
207 ef65f488 Stanislav Král
208
    def get_chain_of_trust(self, from_id: int, to_id: int = -1, exclude_root=True) -> List[Certificate]:
209 4e70d22a Stanislav Král
        """
210
        Traverses the certificate hierarchy tree upwards till a certificate with the `to_id` ID is found or till a
211
        root CA certificate is found. Root certificates are excluded from the chain by default.
212
        :param from_id: ID of the first certificate to be included in the chain of trust
213
        :param to_id: ID of the last certificate to be included in the chain of trust
214
        :param exclude_root: a flag indicating whether root CA certificate should be excluded
215
        :return: a list of certificates representing the chain of trust starting with the certificate given by `from_id`
216
        ID
217
        """
218
        # read the first certificate of the chain
219 ef65f488 Stanislav Král
        start_cert = self.certificate_repository.read(from_id)
220
221 4e70d22a Stanislav Král
        # if no cert is found or the current cert is root CA and root CAs should be excluded, then return an empty list
222 ef65f488 Stanislav Král
        if start_cert is None or (start_cert.type_id == ROOT_CA_ID and exclude_root):
223
            return []
224
225
        current_cert = start_cert
226
        chain_of_trust = [current_cert]
227
228
        # TODO could possibly be simplified
229
        if start_cert.type_id == ROOT_CA_ID:
230 4e70d22a Stanislav Král
            # the first cert found is a root ca
231 ef65f488 Stanislav Král
            return chain_of_trust
232
233
        while True:
234
            parent_cert = self.certificate_repository.read(current_cert.parent_id)
235
236 4e70d22a Stanislav Král
            # check whether parent certificate exists
237
            if parent_cert is None:
238
                break
239
240
            # check whether the found certificate is a root certificate
241
            if parent_cert.type_id == ROOT_CA_ID:
242 ef65f488 Stanislav Král
                if not exclude_root:
243 4e70d22a Stanislav Král
                    # append the found root cert only if root certificates should not be excluded from the CoT
244 ef65f488 Stanislav Král
                    chain_of_trust.append(parent_cert)
245
                break
246
247 4e70d22a Stanislav Král
            # append the certificate
248 ef65f488 Stanislav Král
            chain_of_trust.append(parent_cert)
249
250 4e70d22a Stanislav Král
            # stop iterating over certificates if the id of the found certificate matches `to_id` method parameter
251 ef65f488 Stanislav Král
            if parent_cert.certificate_id == to_id:
252
                break
253
254
            current_cert = parent_cert
255
256
        return chain_of_trust
257 3d639744 Stanislav Král
258 a6727aa9 Stanislav Král
    def delete_certificate(self, unique_id) -> bool:
259 3d639744 Stanislav Král
        """
260
        Deletes a certificate
261
262
        :param unique_id: ID of specific certificate
263
264 a6727aa9 Stanislav Král
        :return: `True` when the deletion was successful. `False` in other case
265 3d639744 Stanislav Král
        """
266
        # TODO delete children?
267
        return self.certificate_repository.delete(unique_id)
268 c4ba6bb7 Jan Pašek
269 20b33bd4 Jan Pašek
    def set_certificate_revocation_status(self, id, status, reason="unspecified"):
270
        """
271
        Set certificate status to 'valid' or 'revoked'.
272
        If the new status is revoked a reason can be provided -> default is unspecified
273
        :param reason: reason for revocation
274
        :param id: identifier of the certificate whose status is to be changed
275
        :param status: new status of the certificate
276
        """
277
        if status not in CERTIFICATE_STATES:
278
            raise CertificateStatusInvalidException(status)
279
        if reason not in CERTIFICATE_REVOCATION_REASONS:
280
            raise RevocationReasonInvalidException(reason)
281
282 9c704fb1 Jan Pašek
        updated = False
283 20b33bd4 Jan Pašek
        if status == STATUS_VALID:
284 9c704fb1 Jan Pašek
            updated = self.certificate_repository.clear_certificate_revocation(id)
285 20b33bd4 Jan Pašek
        elif status == STATUS_REVOKED:
286
            revocation_timestamp = int(time.time())
287 9c704fb1 Jan Pašek
            updated = self.certificate_repository.set_certificate_revoked(id, str(revocation_timestamp), reason)
288
289
        if not updated:
290
            raise CertificateNotFoundException(id)
291 20b33bd4 Jan Pašek
292 c4ba6bb7 Jan Pašek
    def get_subject_from_certificate(self, certificate: Certificate) -> Subject:
293
        """
294
        Get Subject distinguished name from a Certificate
295
        :param certificate: certificate instance whose Subject shall be parsed
296
        :return: instance of Subject class containing resulting distinguished name
297
        """
298
        (subject, _, _) = self.cryptography_service.parse_cert_pem(certificate.pem_data)
299
        return subject
300 d3bfacfc Stanislav Král
301
    def get_public_key_from_certificate(self, certificate: Certificate):
302
        """
303
        Extracts a public key from the given certificate
304
        :param certificate: an instance of the Certificate class containing the certificate from which a public key
305
        should be extracted.
306
        :return: a string containing the extracted public key in PEM format
307
        """
308
        return self.cryptography_service.extract_public_key_from_certificate(certificate.pem_data)
309 20b33bd4 Jan Pašek
310
    def __get_crl_endpoint(self, ca_identifier: int) -> str:
311
        """
312
        Get URL address of CRL distribution endpoint based on
313
        issuer's ID
314
315
        :param ca_identifier: ID of issuing authority
316
        :return: CRL endpoint for the given CA
317
        """
318
        return self.configuration.base_server_url + "/api/crl/" + str(ca_identifier)
319
320
    def __get_ocsp_endpoint(self, ca_identifier: int) -> str:
321
        """
322
        Get URL address of OCSP distribution endpoint based on
323
        issuer's ID
324
325
        :param ca_identifier: ID of issuing authority
326
        :return: OCSP endpoint for the given CA
327
        """
328
        return self.configuration.base_server_url + "/api/ocsp/" + str(ca_identifier)
329
330
331
class RevocationReasonInvalidException(Exception):
332
    """
333
    Exception that denotes that the caller was trying to revoke
334
    a certificate using an invalid revocation reason
335
    """
336
337
    def __init__(self, reason):
338
        self.reason = reason
339
340
    def __str__(self):
341
        return f"Revocation reason '{self.reason}' is not valid."
342
343
344
class CertificateStatusInvalidException(Exception):
345
    """
346
    Exception that denotes that the caller was trying to set
347
    a certificate to an invalid state
348
    """
349
350
    def __init__(self, status):
351
        self.status = status
352
353
    def __str__(self):
354
        return f"Certificate status '{self.status}' is not valid."
355 9c704fb1 Jan Pašek
356
357
class CertificateNotFoundException(Exception):
358
    """
359
    Exception that denotes that the caller was trying to set
360
    a certificate to an invalid state
361
    """
362
363
    def __init__(self, id):
364
        self.id = id
365
366
    def __str__(self):
367
        return f"Certificate id '{self.id}' does not exist."