Projekt

Obecné

Profil

Stáhnout (25.7 KB) Statistiky
| Větev: | Tag: | Revize:
1
from typing import List
2

    
3
from injector import inject
4

    
5
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, SSL_ID, SIGNATURE_ID, AUTHENTICATION_ID, CERTIFICATE_VALID, CERTIFICATE_EXPIRED, \
8
    CERTIFICATE_REVOKED,\
9
    CERTIFICATE_REVOCATION_REASON_HOLD
10
from src.dao.certificate_repository import CertificateRepository
11
from src.exceptions.certificate_not_found_exception import CertificateNotFoundException
12
from src.exceptions.database_exception import DatabaseException
13
from src.exceptions.unknown_exception import UnknownException
14
from src.model.certificate import Certificate
15
from src.model.private_key import PrivateKey
16
from src.model.subject import Subject
17
from src.services.cryptography import CryptographyService
18

    
19
import time
20

    
21
from src.utils.usages_to_extensions import usages_to_extension_lines, ExtensionFieldFlags, CRITICAL, KEY_CERT_SIGN, \
22
    CRL_SIGN, CA, DIGITAL_SIGNATURE, KEY_ENCIPHERMENT, KEY_AGREEMENT, SERVER_AUTH, NON_REPUDIATION, TIME_STAMPING, \
23
    CLIENT_AUTH
24

    
25
from src.utils.logger import Logger
26

    
27
VALID_FROM_TO_DATE_FORMAT = "%d.%m.%Y %H:%M:%S"
28
CA_EXTENSIONS = "basicConstraints=critical,CA:TRUE"
29
CRL_EXTENSION = "crlDistributionPoints=URI:"
30
OCSP_EXTENSION = "authorityInfoAccess=OCSP;URI:"
31
STATUS_REVOKED = "revoked"
32
STATUS_VALID = "valid"
33

    
34
# define which flags are required for various usages
35
REQUIRED_USAGE_EXTENSION_FLAGS = {
36
    CA_ID: ExtensionFieldFlags({CRITICAL, KEY_CERT_SIGN, CRL_SIGN}, {}, {CRITICAL, CA}),
37
    SSL_ID: ExtensionFieldFlags({DIGITAL_SIGNATURE, KEY_ENCIPHERMENT, KEY_AGREEMENT}, {SERVER_AUTH}, {}),
38
    SIGNATURE_ID: ExtensionFieldFlags({DIGITAL_SIGNATURE, NON_REPUDIATION}, {}, {}),
39
    AUTHENTICATION_ID: ExtensionFieldFlags({DIGITAL_SIGNATURE}, {CLIENT_AUTH}, {})}
40

    
41

    
42
class CertificateService:
43

    
44
    @inject
45
    def __init__(self, cryptography_service: CryptographyService,
46
                 certificate_repository: CertificateRepository,
47
                 configuration: Configuration):
48
        self.cryptography_service = cryptography_service
49
        self.certificate_repository = certificate_repository
50
        self.configuration = configuration
51

    
52
    # TODO usages present in method parameters but not in class diagram
53
    def create_root_ca(self, key: PrivateKey, subject: Subject, extensions: str = "", config: str = "",
54
                       usages=None, days=30):
55
        """
56
        Creates a root CA certificate based on the given parameters.
57
        :param key: Private key to be used when generating the certificate
58
        :param subject: Subject to be used put into the certificate
59
        :param config: String containing the configuration to be used
60
        :param extensions: Name of the section in the configuration representing extensions
61
        :param usages: A dictionary containing usages of the certificate to be generated (see constants.py)
62
        :param days: Number of days for which the generated cert. will be considered valid
63
        :return: An instance of Certificate class representing the generated root CA cert
64
        """
65

    
66
        Logger.debug("Function launched.")
67

    
68
        if usages is None:
69
            usages = {}
70

    
71
        cert_id = self.certificate_repository.get_next_id()
72

    
73
        # specify CA usage
74
        usages[CA_ID] = True
75

    
76
        # generate extension configuration lines based on the specified usages
77
        extensions = extensions + "\n" + "\n".join(usages_to_extension_lines(usages, REQUIRED_USAGE_EXTENSION_FLAGS))
78

    
79
        # create a new self signed  certificate
80
        cert_pem = self.cryptography_service.create_sscrt(subject, key.private_key, key_pass=key.password,
81
                                                          extensions=extensions, config=config, days=days, sn=cert_id)
82

    
83
        # wrap into Certificate class
84
        certificate = self.__create_wrapper(cert_pem, key.private_key_id, usages, 0,
85
                                            ROOT_CA_ID)
86

    
87
        # store the wrapper into the repository
88
        created_id = self.certificate_repository.create(certificate)
89

    
90
        # assign the generated ID to the inserted certificate
91
        certificate.certificate_id = created_id
92

    
93
        return certificate
94

    
95
    def __create_wrapper(self, cert_pem, private_key_id, usages, parent_id, cert_type):
96
        """
97
        Wraps the given parameters using the Certificate class. Uses CryptographyService to find out the notBefore and
98
        notAfter fields.
99
        :param cert_pem: PEM of the cert. to be wrapped
100
        :param private_key_id: ID of the private key used to create the given certificate
101
        :param usages: A dictionary containing usages of the generated certificate generated (see constants.py)
102
        :param parent_id: ID of the CA that issued this certificate
103
        :param cert_type: Type of this certificate (see constants.py)
104
        :return: An instance of the Certificate class wrapping the values passed  via method parameters
105
        """
106

    
107
        Logger.debug("Function launched.")
108

    
109
        # parse the generated pem for subject and notBefore/notAfter fields
110
        # TODO this could be improved in the future in such way that calling openssl is not required to parse the dates
111
        subj, not_before, not_after = self.cryptography_service.parse_cert_pem(cert_pem)
112
        # format the parsed date
113
        not_before_formatted = time.strftime(VALID_FROM_TO_DATE_FORMAT, not_before)
114
        not_after_formatted = time.strftime(VALID_FROM_TO_DATE_FORMAT, not_after)
115

    
116
        # create a certificate wrapper
117
        certificate = Certificate(-1, subj.common_name, not_before_formatted, not_after_formatted, cert_pem,
118
                                  private_key_id, cert_type, parent_id, usages)
119

    
120
        return certificate
121

    
122
    # TODO config parameter present in class diagram but not here (unused)
123
    def create_ca(self, subject_key: PrivateKey, subject: Subject, issuer_cert: Certificate, issuer_key: PrivateKey,
124
                  extensions: str = "", days: int = 30, usages=None):
125
        """
126
        Creates an intermediate CA certificate issued by the given parent CA.
127
        :param subject_key: Private key to be used when generating the certificate
128
        :param subject: Subject to be used put into the certificate
129
        :param issuer_cert: Issuer certificate that will sign the CSR required to create an intermediate CA
130
        :param issuer_key: PK used to generate the issuer certificate
131
        :param extensions: Extensions to be used when generating the certificate
132
        :param usages: A dictionary containing usages of the certificate to be generated (see constants.py)
133
        :param days: Number of days for which the generated cert. will be considered valid
134
        :return: An instance of Certificate class representing the generated intermediate CA cert
135
        """
136

    
137
        Logger.debug("Function launched.")
138

    
139
        if usages is None:
140
            usages = {}
141

    
142
        # specify CA usage
143
        usages[CA_ID] = True
144

    
145
        # generate extension configuration lines based on the specified usages
146
        extensions = extensions + "\n" + "\n".join(usages_to_extension_lines(usages, REQUIRED_USAGE_EXTENSION_FLAGS))
147

    
148
        # Add CRL and OCSP distribution point to certificate extensions
149
        cert_id = self.certificate_repository.get_next_id()
150
        extensions = extensions + "\n" + CRL_EXTENSION + " " + self.__get_crl_endpoint(issuer_cert.certificate_id)
151
        extensions = extensions + "\n" + OCSP_EXTENSION + " " + self.__get_ocsp_endpoint(issuer_cert.certificate_id)
152

    
153
        # TODO implement AIA URI via extensions
154
        cert_pem = self.cryptography_service.create_crt(subject, subject_key.private_key, issuer_cert.pem_data,
155
                                                        issuer_key.private_key,
156
                                                        subject_key_pass=subject_key.password,
157
                                                        issuer_key_pass=issuer_key.password, extensions=extensions,
158
                                                        days=days,
159
                                                        sn=cert_id)
160

    
161
        # wrap into Certificate class
162
        self.__create_wrapper(cert_pem, subject_key.private_key_id, usages,
163
                              issuer_cert.certificate_id, INTERMEDIATE_CA_ID)
164

    
165
        # parse the generated pem for subject and notBefore/notAfter fields
166
        subj, not_before, not_after = self.cryptography_service.parse_cert_pem(cert_pem)
167

    
168
        # format the parsed date
169
        not_before_formatted = time.strftime(VALID_FROM_TO_DATE_FORMAT, not_before)
170
        not_after_formatted = time.strftime(VALID_FROM_TO_DATE_FORMAT, not_after)
171

    
172
        # create a certificate wrapper
173
        certificate = Certificate(-1, subject.common_name, not_before_formatted, not_after_formatted, cert_pem,
174
                                  subject_key.private_key_id, INTERMEDIATE_CA_ID, issuer_cert.certificate_id, usages)
175

    
176
        # store the wrapper into the repository
177
        created_id = self.certificate_repository.create(certificate)
178

    
179
        # assign the generated ID to the inserted certificate
180
        certificate.certificate_id = created_id
181

    
182
        return certificate
183

    
184
    def create_end_cert(self, subject_key: PrivateKey, subject: Subject, issuer_cert: Certificate,
185
                        issuer_key: PrivateKey,
186
                        extensions: str = "", days: int = 30, usages=None):
187
        """
188
        Creates an end certificate issued by the given parent CA.
189
        :param subject_key: Private key to be used when generating the certificate
190
        :param subject: Subject to be used put into the certificate
191
        :param issuer_cert: Issuer certificate that will sign the CSR required to create an intermediate CA
192
        :param issuer_key: PK used to generate the issuer certificate
193
        :param extensions: Extensions to be used when generating the certificate
194
        :param usages: A dictionary containing usages of the certificate to be generated (see constants.py)
195
        :param days: Number of days for which the generated cert. will be considered valid
196
        :return: An instance of Certificate class representing the generated cert
197
        """
198

    
199
        Logger.debug("Function launched.")
200

    
201
        if usages is None:
202
            usages = {}
203

    
204
        # get the next certificate ID in order to be able to specify the serial number
205
        cert_id = self.certificate_repository.get_next_id()
206

    
207
        # generate extension configuration lines based on the specified usages
208
        extensions = extensions + "\n" + "\n".join(usages_to_extension_lines(usages, REQUIRED_USAGE_EXTENSION_FLAGS))
209

    
210
        # Add CRL and OCSP distribution point to certificate extensions
211
        extensions = extensions + "\n" + CRL_EXTENSION + " " + self.__get_crl_endpoint(issuer_cert.certificate_id)
212
        extensions = extensions + "\n" + OCSP_EXTENSION + " " + self.__get_ocsp_endpoint(issuer_cert.certificate_id)
213

    
214
        # generate a new certificate
215
        cert_pem = self.cryptography_service.create_crt(subject, subject_key.private_key, issuer_cert.pem_data,
216
                                                        issuer_key.private_key,
217
                                                        subject_key_pass=subject_key.password,
218
                                                        issuer_key_pass=issuer_key.password, extensions=extensions,
219
                                                        days=days,
220
                                                        sn=cert_id
221
                                                        )
222

    
223
        # wrap the generated certificate using Certificate class
224
        certificate = self.__create_wrapper(cert_pem, subject_key.private_key_id, usages,
225
                                            issuer_cert.certificate_id, CERTIFICATE_ID)
226

    
227
        created_id = self.certificate_repository.create(certificate)
228

    
229
        certificate.certificate_id = created_id
230

    
231
        return certificate
232

    
233
    def get_certificate(self, unique_id: int) -> Certificate:
234
        """
235
        Tries to fetch a certificate from the certificate repository using a given id.
236
        :param unique_id: ID of the certificate to be fetched
237
        :return: Instance of the Certificate class containing a certificate with the given id or `None` if such
238
        certificate is not found
239
        """
240

    
241
        Logger.debug("Function launched.")
242

    
243
        return self.certificate_repository.read(unique_id)
244

    
245
    def get_certificates(self, cert_type=None) -> List[Certificate]:
246
        """
247
        Tries to fetch a list of all certificates from the certificate repository. Using the `cert_type` parameter only
248
        certificates of the given type can be returned.
249
        :param cert_type: Type of certificates to be returned
250
        :return: List of instances of the Certificate class representing all certificates present in the certificate
251
        repository. An empty list is returned when no certificates are found.
252
        """
253

    
254
        Logger.debug("Function launched.")
255

    
256
        return self.certificate_repository.read_all(cert_type)
257

    
258
    def get_chain_of_trust(self, from_id: int, to_id: int = -1, exclude_root=True) -> List[Certificate]:
259
        """
260
        Traverses the certificate hierarchy tree upwards till a certificate with the `to_id` ID is found or till a
261
        root CA certificate is found. Root certificates are excluded from the chain by default.
262
        :param from_id: ID of the first certificate to be included in the chain of trust
263
        :param to_id: ID of the last certificate to be included in the chain of trust
264
        :param exclude_root: a flag indicating whether root CA certificate should be excluded
265
        :return: a list of certificates representing the chain of trust starting with the certificate given by `from_id`
266
        ID
267
        """
268

    
269
        Logger.debug("Function launched.")
270

    
271
        # read the first certificate of the chain
272
        start_cert = self.certificate_repository.read(from_id)
273

    
274
        # if no cert is found or the current cert is root CA and root CAs should be excluded, then return an empty list
275
        if start_cert is None or (start_cert.type_id == ROOT_CA_ID and exclude_root):
276
            return []
277

    
278
        current_cert = start_cert
279
        chain_of_trust = [current_cert]
280

    
281
        # TODO could possibly be simplified
282
        if start_cert.type_id == ROOT_CA_ID:
283
            # the first cert found is a root ca
284
            return chain_of_trust
285

    
286
        while True:
287
            parent_cert = self.certificate_repository.read(current_cert.parent_id)
288

    
289
            # check whether parent certificate exists
290
            if parent_cert is None:
291
                break
292

    
293
            # check whether the found certificate is a root certificate
294
            if parent_cert.type_id == ROOT_CA_ID:
295
                if not exclude_root:
296
                    # append the found root cert only if root certificates should not be excluded from the CoT
297
                    chain_of_trust.append(parent_cert)
298
                break
299

    
300
            # append the certificate
301
            chain_of_trust.append(parent_cert)
302

    
303
            # stop iterating over certificates if the id of the found certificate matches `to_id` method parameter
304
            if parent_cert.certificate_id == to_id:
305
                break
306

    
307
            current_cert = parent_cert
308

    
309
        return chain_of_trust
310

    
311
    def delete_certificate(self, unique_id):
312
        """
313
        Deletes a certificate. Raises an Exception should any unexpected behavior occur.
314

    
315
        :param unique_id: ID of specific certificate
316
        """
317

    
318
        Logger.debug("Function launched.")
319

    
320
        to_delete = self.certificate_repository.get_all_descendants_of(unique_id)
321
        if to_delete is None:
322
            Logger.error(f"No such certificate found 'ID = {unique_id}'.")
323
            raise CertificateNotFoundException(unique_id)
324

    
325
        for cert in to_delete:
326
            try:
327
                self.set_certificate_revocation_status(cert.certificate_id, STATUS_REVOKED)
328
            except CertificateAlreadyRevokedException:
329
                Logger.info(f"Certificate already revoked 'ID = {unique_id}'.")
330
                # TODO log as an info/debug, not warning or above <-- perfectly legal
331
                pass
332

    
333
            if not self.certificate_repository.delete(cert.certificate_id):
334
                Logger.error(f"The certificate has not been deleted 'ID = {cert.certificate_id}'.")
335

    
336
    def get_certificates_issued_by(self, issuer_id):
337
        """
338
        Returns a list of all children of a certificate identified by an unique_id.
339
        Raises a DatabaseException should any unexpected behavior occur.
340
        :param issuer_id: target certificate ID
341
        :return: children of unique_id
342
        """
343

    
344
        Logger.debug("Function launched.")
345

    
346
        try:
347
            if self.certificate_repository.read(issuer_id) is None:
348
                Logger.error(f"No such certificate found 'ID = {issuer_id}'.")
349
                raise CertificateNotFoundException(issuer_id)
350
        except DatabaseException:
351
            Logger.error(f"No such certificate found 'ID = {issuer_id}'.")
352
            raise CertificateNotFoundException(issuer_id)
353

    
354
        return self.certificate_repository.get_all_issued_by(issuer_id)
355

    
356
    def get_certificates_issued_by_filter(self, issuer_id, target_types, target_usages, target_cn_substring, page,
357
                                          per_page):
358
        """
359
        Returns a list of all children of a certificate identified by an unique_id.
360
        Filters the results according to target types, usages, cn substring and pagination.
361
        Raises a DatabaseException should any unexpected behavior occur.
362
        :param issuer_id: target certificate ID
363
        :param target_types: filter of types
364
        :param target_usages: filter of usages
365
        :param target_cn_substring: CN substring
366
        :param page: target page or None
367
        :param per_page: certs per page or None
368
        :return: list of certificates (a page if specified)
369
        """
370

    
371
        Logger.debug("Function launched.")
372

    
373
        try:
374
            if self.certificate_repository.read(issuer_id) is None:
375
                Logger.error(f"No such certificate found 'ID = {issuer_id}'.")
376
                raise CertificateNotFoundException(issuer_id)
377
        except DatabaseException:
378
            Logger.error(f"No such certificate found 'ID = {issuer_id}'.")
379
            raise CertificateNotFoundException(issuer_id)
380

    
381
    def set_certificate_revocation_status(self, id, status, reason="unspecified"):
382
        """
383
        Set certificate status to 'valid' or 'revoked'.
384
        If the new status is revoked a reason can be provided -> default is unspecified
385
        :param reason: reason for revocation
386
        :param id: identifier of the certificate whose status is to be changed
387
        :param status: new status of the certificate
388
        :raises CertificateStatusInvalidException: if status is not valid
389
        :raises RevocationReasonInvalidException: if reason is not valid
390
        :raises CertificateNotFoundException: if certificate with given id cannot be found
391
        :raises CertificateCannotBeSetToValid: if certificate was already revoked and not on hold,
392
                it cannot be set revalidated
393
        :raises CertificateAlreadyRevokedException: if caller tries to revoke a certificate that is already revoked
394
        :raises UnknownException: if the database is corrupted
395
        """
396

    
397
        Logger.debug("Function launched.")
398

    
399
        if status not in CERTIFICATE_STATES:
400
            Logger.error(f"Wrong parameter, invalid status '{status}'.")
401
            raise CertificateStatusInvalidException(status)
402
        if reason not in CERTIFICATE_REVOCATION_REASONS:
403
            Logger.error(f"Wrong parameter, invalid reason '{reason}'.")
404
            raise RevocationReasonInvalidException(reason)
405

    
406
        # check whether the certificate exists
407
        certificate = self.certificate_repository.read(id)
408
        if certificate is None:
409
            Logger.error(f"No such certificate found 'ID = {id}'.")
410
            raise CertificateNotFoundException(id)
411

    
412
        updated = False
413
        if status == STATUS_VALID:
414
            # if the certificate is revoked but the reason is not certificateHold, it cannot be re-validated
415
            #    -> throw an exception
416
            if certificate.revocation_reason != "" and \
417
               certificate.revocation_reason != CERTIFICATE_REVOCATION_REASON_HOLD:
418
                raise CertificateCannotBeSetToValid(certificate.revocation_reason)
419
            updated = self.certificate_repository.clear_certificate_revocation(id)
420
        elif status == STATUS_REVOKED:
421
            # check if the certificate is not revoked already
422
            revoked = self.certificate_repository.get_all_revoked_by(certificate.parent_id)
423
            if certificate.certificate_id in [x.certificate_id for x in revoked]:
424
                Logger.error(f"Certificate already revoked 'ID = {id}'.")
425
                raise CertificateAlreadyRevokedException(id)
426

    
427
            revocation_timestamp = int(time.time())
428
            updated = self.certificate_repository.set_certificate_revoked(id, str(revocation_timestamp), reason)
429

    
430
        if not updated:
431
            Logger.error(f"Repository returned 'false' from clear_certificate_revocation() "
432
                         f"or set_certificate_revoked().")
433
            raise UnknownException("Repository returned 'false' from clear_certificate_revocation() "
434
                                   "or set_certificate_revoked().")
435

    
436
    def get_subject_from_certificate(self, certificate: Certificate) -> Subject:
437
        """
438
        Get Subject distinguished name from a Certificate
439
        :param certificate: certificate instance whose Subject shall be parsed
440
        :return: instance of Subject class containing resulting distinguished name
441
        """
442

    
443
        Logger.debug("Function launched.")
444

    
445
        (subject, _, _) = self.cryptography_service.parse_cert_pem(certificate.pem_data)
446
        return subject
447

    
448
    def get_public_key_from_certificate(self, certificate: Certificate):
449
        """
450
        Extracts a public key from the given certificate
451
        :param certificate: an instance of the Certificate class containing the certificate from which a public key
452
        should be extracted.
453
        :return: a string containing the extracted public key in PEM format
454
        """
455

    
456
        Logger.debug("Function launched.")
457

    
458
        return self.cryptography_service.extract_public_key_from_certificate(certificate.pem_data)
459

    
460
    def get_certificate_state(self, id: int) -> str:
461
        """
462
        Check whether the certificate is expired, valid or revoked.
463
            - in case it's revoked and expired, revoked is returned
464
        :param id: identifier of the certificate
465
        :return: certificates state from {valid, revoked, expired}
466
        :raises CertificateNotFoundException: in case id of non-existing certificate is entered
467
        """
468
        Logger.debug("Function launched.")
469
        status = CERTIFICATE_VALID
470

    
471
        # Read the selected certificate from the repository
472
        certificate = self.certificate_repository.read(id)
473
        if certificate is None:
474
            Logger.error("Certificate whose details were requested does not exists.")
475
            raise CertificateNotFoundException(id)
476

    
477
        # check the expiration date using OpenSSL
478
        if not self.cryptography_service.verify_cert(certificate.pem_data):
479
            status = CERTIFICATE_EXPIRED
480

    
481
        # check certificate revocation
482
        all_revoked_by_parent = self.certificate_repository.get_all_revoked_by(certificate.parent_id)
483
        all_revoked_by_parent_ids = [cert.certificate_id for cert in all_revoked_by_parent]
484

    
485
        if id in all_revoked_by_parent_ids:
486
            status = CERTIFICATE_REVOKED
487

    
488
        return status
489

    
490
    def __get_crl_endpoint(self, ca_identifier: int) -> str:
491
        """
492
        Get URL address of CRL distribution endpoint based on
493
        issuer's ID
494

    
495
        :param ca_identifier: ID of issuing authority
496
        :return: CRL endpoint for the given CA
497
        """
498

    
499
        Logger.debug("Function launched.")
500

    
501
        return self.configuration.base_server_url + "/api/crl/" + str(ca_identifier)
502

    
503
    def __get_ocsp_endpoint(self, ca_identifier: int) -> str:
504
        """
505
        Get URL address of OCSP distribution endpoint based on
506
        issuer's ID
507

    
508
        :param ca_identifier: ID of issuing authority
509
        :return: OCSP endpoint for the given CA
510
        """
511

    
512
        Logger.debug("Function launched.")
513

    
514
        return self.configuration.base_server_url + "/api/ocsp/" + str(ca_identifier)
515

    
516
class RevocationReasonInvalidException(Exception):
517
    """
518
    Exception that denotes that the caller was trying to revoke
519
    a certificate using an invalid revocation reason
520
    """
521

    
522
    def __init__(self, reason):
523
        self.reason = reason
524

    
525
    def __str__(self):
526
        return f"Revocation reason '{self.reason}' is not valid."
527

    
528

    
529
class CertificateStatusInvalidException(Exception):
530
    """
531
    Exception that denotes that the caller was trying to set
532
    a certificate to an invalid state
533
    """
534

    
535
    def __init__(self, status):
536
        self.status = status
537

    
538
    def __str__(self):
539
        return f"Certificate status '{self.status}' is not valid."
540

    
541

    
542
class CertificateAlreadyRevokedException(Exception):
543
    """
544
    Exception that denotes that the caller was trying to revoke
545
    a certificate that is already revoked
546
    """
547

    
548
    def __init__(self, id):
549
        self.id = id
550

    
551
    def __str__(self):
552
        return f"Certificate id '{self.id}' is already revoked."
553

    
554

    
555
class CertificateCannotBeSetToValid(Exception):
556
    """
557
    Exception that denotes that the caller was trying to
558
    set certificate to valid if the certificate was already
559
    revoked but not certificateHold.
560
    """
561

    
562
    def __init__(self, old_reason):
563
        self.old_state = old_reason
564

    
565
    def __str__(self):
566
        return "Cannot set revoked certificate back to valid when the certificate revocation reason is not " \
567
               "certificateHold. " \
568
               f"The revocation reason of the certificate is {self.old_state}"
(2-2/4)