1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2019, Felix Fontein <felix@fontein.de>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11DOCUMENTATION = r'''
12---
13module: x509_crl
14version_added: '1.0.0'
15short_description: Generate Certificate Revocation Lists (CRLs)
16description:
17    - This module allows one to (re)generate or update Certificate Revocation Lists (CRLs).
18    - Certificates on the revocation list can be either specified by serial number and (optionally) their issuer,
19      or as a path to a certificate file in PEM format.
20requirements:
21    - cryptography >= 1.2
22author:
23    - Felix Fontein (@felixfontein)
24options:
25    state:
26        description:
27            - Whether the CRL file should exist or not, taking action if the state is different from what is stated.
28        type: str
29        default: present
30        choices: [ absent, present ]
31
32    mode:
33        description:
34            - Defines how to process entries of existing CRLs.
35            - If set to C(generate), makes sure that the CRL has the exact set of revoked certificates
36              as specified in I(revoked_certificates).
37            - If set to C(update), makes sure that the CRL contains the revoked certificates from
38              I(revoked_certificates), but can also contain other revoked certificates. If the CRL file
39              already exists, all entries from the existing CRL will also be included in the new CRL.
40              When using C(update), you might be interested in setting I(ignore_timestamps) to C(yes).
41        type: str
42        default: generate
43        choices: [ generate, update ]
44
45    force:
46        description:
47            - Should the CRL be forced to be regenerated.
48        type: bool
49        default: no
50
51    backup:
52        description:
53            - Create a backup file including a timestamp so you can get the original
54              CRL back if you overwrote it with a new one by accident.
55        type: bool
56        default: no
57
58    path:
59        description:
60            - Remote absolute path where the generated CRL file should be created or is already located.
61        type: path
62        required: yes
63
64    format:
65        description:
66            - Whether the CRL file should be in PEM or DER format.
67            - If an existing CRL file does match everything but I(format), it will be converted to the correct format
68              instead of regenerated.
69        type: str
70        choices: [pem, der]
71        default: pem
72
73    privatekey_path:
74        description:
75            - Path to the CA's private key to use when signing the CRL.
76            - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
77        type: path
78
79    privatekey_content:
80        description:
81            - The content of the CA's private key to use when signing the CRL.
82            - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
83        type: str
84
85    privatekey_passphrase:
86        description:
87            - The passphrase for the I(privatekey_path).
88            - This is required if the private key is password protected.
89        type: str
90
91    issuer:
92        description:
93            - Key/value pairs that will be present in the issuer name field of the CRL.
94            - If you need to specify more than one value with the same key, use a list as value.
95            - Required if I(state) is C(present).
96        type: dict
97
98    last_update:
99        description:
100            - The point in time from which this CRL can be trusted.
101            - Time can be specified either as relative time or as absolute timestamp.
102            - Time will always be interpreted as UTC.
103            - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
104              + C([w | d | h | m | s]) (e.g. C(+32w1d2h).
105            - Note that if using relative time this module is NOT idempotent, except when
106              I(ignore_timestamps) is set to C(yes).
107        type: str
108        default: "+0s"
109
110    next_update:
111        description:
112            - "The absolute latest point in time by which this I(issuer) is expected to have issued
113               another CRL. Many clients will treat a CRL as expired once I(next_update) occurs."
114            - Time can be specified either as relative time or as absolute timestamp.
115            - Time will always be interpreted as UTC.
116            - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
117              + C([w | d | h | m | s]) (e.g. C(+32w1d2h).
118            - Note that if using relative time this module is NOT idempotent, except when
119              I(ignore_timestamps) is set to C(yes).
120            - Required if I(state) is C(present).
121        type: str
122
123    digest:
124        description:
125            - Digest algorithm to be used when signing the CRL.
126        type: str
127        default: sha256
128
129    revoked_certificates:
130        description:
131            - List of certificates to be revoked.
132            - Required if I(state) is C(present).
133        type: list
134        elements: dict
135        suboptions:
136            path:
137                description:
138                    - Path to a certificate in PEM format.
139                    - The serial number and issuer will be extracted from the certificate.
140                    - Mutually exclusive with I(content) and I(serial_number). One of these three options
141                      must be specified.
142                type: path
143            content:
144                description:
145                    - Content of a certificate in PEM format.
146                    - The serial number and issuer will be extracted from the certificate.
147                    - Mutually exclusive with I(path) and I(serial_number). One of these three options
148                      must be specified.
149                type: str
150            serial_number:
151                description:
152                    - Serial number of the certificate.
153                    - Mutually exclusive with I(path) and I(content). One of these three options must
154                      be specified.
155                type: int
156            revocation_date:
157                description:
158                    - The point in time the certificate was revoked.
159                    - Time can be specified either as relative time or as absolute timestamp.
160                    - Time will always be interpreted as UTC.
161                    - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
162                      + C([w | d | h | m | s]) (e.g. C(+32w1d2h).
163                    - Note that if using relative time this module is NOT idempotent, except when
164                      I(ignore_timestamps) is set to C(yes).
165                type: str
166                default: "+0s"
167            issuer:
168                description:
169                    - The certificate's issuer.
170                    - "Example: C(DNS:ca.example.org)"
171                type: list
172                elements: str
173            issuer_critical:
174                description:
175                    - Whether the certificate issuer extension should be critical.
176                type: bool
177                default: no
178            reason:
179                description:
180                    - The value for the revocation reason extension.
181                type: str
182                choices:
183                    - unspecified
184                    - key_compromise
185                    - ca_compromise
186                    - affiliation_changed
187                    - superseded
188                    - cessation_of_operation
189                    - certificate_hold
190                    - privilege_withdrawn
191                    - aa_compromise
192                    - remove_from_crl
193            reason_critical:
194                description:
195                    - Whether the revocation reason extension should be critical.
196                type: bool
197                default: no
198            invalidity_date:
199                description:
200                    - The point in time it was known/suspected that the private key was compromised
201                      or that the certificate otherwise became invalid.
202                    - Time can be specified either as relative time or as absolute timestamp.
203                    - Time will always be interpreted as UTC.
204                    - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
205                      + C([w | d | h | m | s]) (e.g. C(+32w1d2h).
206                    - Note that if using relative time this module is NOT idempotent. This will NOT
207                      change when I(ignore_timestamps) is set to C(yes).
208                type: str
209            invalidity_date_critical:
210                description:
211                    - Whether the invalidity date extension should be critical.
212                type: bool
213                default: no
214
215    ignore_timestamps:
216        description:
217            - Whether the timestamps I(last_update), I(next_update) and I(revocation_date) (in
218              I(revoked_certificates)) should be ignored for idempotency checks. The timestamp
219              I(invalidity_date) in I(revoked_certificates) will never be ignored.
220            - Use this in combination with relative timestamps for these values to get idempotency.
221        type: bool
222        default: no
223
224    return_content:
225        description:
226            - If set to C(yes), will return the (current or generated) CRL's content as I(crl).
227        type: bool
228        default: no
229
230extends_documentation_fragment:
231    - files
232
233notes:
234    - All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
235    - Date specified should be UTC. Minutes and seconds are mandatory.
236    - Supports C(check_mode).
237'''
238
239EXAMPLES = r'''
240- name: Generate a CRL
241  community.crypto.x509_crl:
242    path: /etc/ssl/my-ca.crl
243    privatekey_path: /etc/ssl/private/my-ca.pem
244    issuer:
245      CN: My CA
246    last_update: "+0s"
247    next_update: "+7d"
248    revoked_certificates:
249      - serial_number: 1234
250        revocation_date: 20190331202428Z
251        issuer:
252          CN: My CA
253      - serial_number: 2345
254        revocation_date: 20191013152910Z
255        reason: affiliation_changed
256        invalidity_date: 20191001000000Z
257      - path: /etc/ssl/crt/revoked-cert.pem
258        revocation_date: 20191010010203Z
259'''
260
261RETURN = r'''
262filename:
263    description: Path to the generated CRL.
264    returned: changed or success
265    type: str
266    sample: /path/to/my-ca.crl
267backup_file:
268    description: Name of backup file created.
269    returned: changed and if I(backup) is C(yes)
270    type: str
271    sample: /path/to/my-ca.crl.2019-03-09@11:22~
272privatekey:
273    description: Path to the private CA key.
274    returned: changed or success
275    type: str
276    sample: /path/to/my-ca.pem
277format:
278    description:
279        - Whether the CRL is in PEM format (C(pem)) or in DER format (C(der)).
280    returned: success
281    type: str
282    sample: pem
283issuer:
284    description:
285        - The CRL's issuer.
286        - Note that for repeated values, only the last one will be returned.
287    returned: success
288    type: dict
289    sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}'
290issuer_ordered:
291    description: The CRL's issuer as an ordered list of tuples.
292    returned: success
293    type: list
294    elements: list
295    sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]'
296last_update:
297    description: The point in time from which this CRL can be trusted as ASN.1 TIME.
298    returned: success
299    type: str
300    sample: 20190413202428Z
301next_update:
302    description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME.
303    returned: success
304    type: str
305    sample: 20190413202428Z
306digest:
307    description: The signature algorithm used to sign the CRL.
308    returned: success
309    type: str
310    sample: sha256WithRSAEncryption
311revoked_certificates:
312    description: List of certificates to be revoked.
313    returned: success
314    type: list
315    elements: dict
316    contains:
317        serial_number:
318            description: Serial number of the certificate.
319            type: int
320            sample: 1234
321        revocation_date:
322            description: The point in time the certificate was revoked as ASN.1 TIME.
323            type: str
324            sample: 20190413202428Z
325        issuer:
326            description: The certificate's issuer.
327            type: list
328            elements: str
329            sample: '["DNS:ca.example.org"]'
330        issuer_critical:
331            description: Whether the certificate issuer extension is critical.
332            type: bool
333            sample: no
334        reason:
335            description:
336                - The value for the revocation reason extension.
337                - One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded),
338                  C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and
339                  C(remove_from_crl).
340            type: str
341            sample: key_compromise
342        reason_critical:
343            description: Whether the revocation reason extension is critical.
344            type: bool
345            sample: no
346        invalidity_date:
347            description: |
348                The point in time it was known/suspected that the private key was compromised
349                or that the certificate otherwise became invalid as ASN.1 TIME.
350            type: str
351            sample: 20190413202428Z
352        invalidity_date_critical:
353            description: Whether the invalidity date extension is critical.
354            type: bool
355            sample: no
356crl:
357    description:
358        - The (current or generated) CRL's content.
359        - Will be the CRL itself if I(format) is C(pem), and Base64 of the
360          CRL if I(format) is C(der).
361    returned: if I(state) is C(present) and I(return_content) is C(yes)
362    type: str
363'''
364
365
366import base64
367import os
368import traceback
369
370from distutils.version import LooseVersion
371
372from ansible.module_utils.basic import AnsibleModule, missing_required_lib
373from ansible.module_utils.common.text.converters import to_native, to_text
374
375from ansible_collections.community.crypto.plugins.module_utils.io import (
376    write_file,
377)
378
379from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
380    OpenSSLObjectError,
381    OpenSSLBadPassphraseError,
382)
383
384from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
385    OpenSSLObject,
386    load_privatekey,
387    load_certificate,
388    parse_name_field,
389    get_relative_time_option,
390    select_message_digest,
391)
392
393from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
394    cryptography_get_name,
395    cryptography_name_to_oid,
396    cryptography_oid_to_name,
397    cryptography_serial_number_of_cert,
398)
399
400from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import (
401    REVOCATION_REASON_MAP,
402    TIMESTAMP_FORMAT,
403    cryptography_decode_revoked_certificate,
404    cryptography_dump_revoked,
405    cryptography_get_signature_algorithm_oid_from_crl,
406)
407
408from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
409    identify_pem_format,
410)
411
412from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import (
413    get_crl_info,
414)
415
416MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
417
418CRYPTOGRAPHY_IMP_ERR = None
419try:
420    import cryptography
421    from cryptography import x509
422    from cryptography.hazmat.backends import default_backend
423    from cryptography.hazmat.primitives.serialization import Encoding
424    from cryptography.x509 import (
425        CertificateRevocationListBuilder,
426        RevokedCertificateBuilder,
427        NameAttribute,
428        Name,
429    )
430    CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
431except ImportError:
432    CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
433    CRYPTOGRAPHY_FOUND = False
434else:
435    CRYPTOGRAPHY_FOUND = True
436
437
438class CRLError(OpenSSLObjectError):
439    pass
440
441
442class CRL(OpenSSLObject):
443
444    def __init__(self, module):
445        super(CRL, self).__init__(
446            module.params['path'],
447            module.params['state'],
448            module.params['force'],
449            module.check_mode
450        )
451
452        self.format = module.params['format']
453
454        self.update = module.params['mode'] == 'update'
455        self.ignore_timestamps = module.params['ignore_timestamps']
456        self.return_content = module.params['return_content']
457        self.crl_content = None
458
459        self.privatekey_path = module.params['privatekey_path']
460        self.privatekey_content = module.params['privatekey_content']
461        if self.privatekey_content is not None:
462            self.privatekey_content = self.privatekey_content.encode('utf-8')
463        self.privatekey_passphrase = module.params['privatekey_passphrase']
464
465        self.issuer = parse_name_field(module.params['issuer'])
466        self.issuer = [(entry[0], entry[1]) for entry in self.issuer if entry[1]]
467
468        self.last_update = get_relative_time_option(module.params['last_update'], 'last_update')
469        self.next_update = get_relative_time_option(module.params['next_update'], 'next_update')
470
471        self.digest = select_message_digest(module.params['digest'])
472        if self.digest is None:
473            raise CRLError('The digest "{0}" is not supported'.format(module.params['digest']))
474
475        self.revoked_certificates = []
476        for i, rc in enumerate(module.params['revoked_certificates']):
477            result = {
478                'serial_number': None,
479                'revocation_date': None,
480                'issuer': None,
481                'issuer_critical': False,
482                'reason': None,
483                'reason_critical': False,
484                'invalidity_date': None,
485                'invalidity_date_critical': False,
486            }
487            path_prefix = 'revoked_certificates[{0}].'.format(i)
488            if rc['path'] is not None or rc['content'] is not None:
489                # Load certificate from file or content
490                try:
491                    if rc['content'] is not None:
492                        rc['content'] = rc['content'].encode('utf-8')
493                    cert = load_certificate(rc['path'], content=rc['content'], backend='cryptography')
494                    result['serial_number'] = cryptography_serial_number_of_cert(cert)
495                except OpenSSLObjectError as e:
496                    if rc['content'] is not None:
497                        module.fail_json(
498                            msg='Cannot parse certificate from {0}content: {1}'.format(path_prefix, to_native(e))
499                        )
500                    else:
501                        module.fail_json(
502                            msg='Cannot read certificate "{1}" from {0}path: {2}'.format(path_prefix, rc['path'], to_native(e))
503                        )
504            else:
505                # Specify serial_number (and potentially issuer) directly
506                result['serial_number'] = rc['serial_number']
507            # All other options
508            if rc['issuer']:
509                result['issuer'] = [cryptography_get_name(issuer, 'issuer') for issuer in rc['issuer']]
510                result['issuer_critical'] = rc['issuer_critical']
511            result['revocation_date'] = get_relative_time_option(
512                rc['revocation_date'],
513                path_prefix + 'revocation_date'
514            )
515            if rc['reason']:
516                result['reason'] = REVOCATION_REASON_MAP[rc['reason']]
517                result['reason_critical'] = rc['reason_critical']
518            if rc['invalidity_date']:
519                result['invalidity_date'] = get_relative_time_option(
520                    rc['invalidity_date'],
521                    path_prefix + 'invalidity_date'
522                )
523                result['invalidity_date_critical'] = rc['invalidity_date_critical']
524            self.revoked_certificates.append(result)
525
526        self.module = module
527
528        self.backup = module.params['backup']
529        self.backup_file = None
530
531        try:
532            self.privatekey = load_privatekey(
533                path=self.privatekey_path,
534                content=self.privatekey_content,
535                passphrase=self.privatekey_passphrase,
536                backend='cryptography'
537            )
538        except OpenSSLBadPassphraseError as exc:
539            raise CRLError(exc)
540
541        self.crl = None
542        try:
543            with open(self.path, 'rb') as f:
544                data = f.read()
545            self.actual_format = 'pem' if identify_pem_format(data) else 'der'
546            if self.actual_format == 'pem':
547                self.crl = x509.load_pem_x509_crl(data, default_backend())
548                if self.return_content:
549                    self.crl_content = data
550            else:
551                self.crl = x509.load_der_x509_crl(data, default_backend())
552                if self.return_content:
553                    self.crl_content = base64.b64encode(data)
554        except Exception as dummy:
555            self.crl_content = None
556            self.actual_format = self.format
557            data = None
558
559        self.diff_after = self.diff_before = self._get_info(data)
560
561    def _get_info(self, data):
562        if data is None:
563            return dict()
564        try:
565            result = get_crl_info(self.module, data)
566            result['can_parse_crl'] = True
567            return result
568        except Exception as exc:
569            return dict(can_parse_crl=False)
570
571    def remove(self):
572        if self.backup:
573            self.backup_file = self.module.backup_local(self.path)
574        super(CRL, self).remove(self.module)
575
576    def _compress_entry(self, entry):
577        if self.ignore_timestamps:
578            # Throw out revocation_date
579            return (
580                entry['serial_number'],
581                tuple(entry['issuer']) if entry['issuer'] is not None else None,
582                entry['issuer_critical'],
583                entry['reason'],
584                entry['reason_critical'],
585                entry['invalidity_date'],
586                entry['invalidity_date_critical'],
587            )
588        else:
589            return (
590                entry['serial_number'],
591                entry['revocation_date'],
592                tuple(entry['issuer']) if entry['issuer'] is not None else None,
593                entry['issuer_critical'],
594                entry['reason'],
595                entry['reason_critical'],
596                entry['invalidity_date'],
597                entry['invalidity_date_critical'],
598            )
599
600    def check(self, module, perms_required=True, ignore_conversion=True):
601        """Ensure the resource is in its desired state."""
602
603        state_and_perms = super(CRL, self).check(self.module, perms_required)
604
605        if not state_and_perms:
606            return False
607
608        if self.crl is None:
609            return False
610
611        if self.last_update != self.crl.last_update and not self.ignore_timestamps:
612            return False
613        if self.next_update != self.crl.next_update and not self.ignore_timestamps:
614            return False
615        if self.digest.name != self.crl.signature_hash_algorithm.name:
616            return False
617
618        want_issuer = [(cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer]
619        if want_issuer != [(sub.oid, sub.value) for sub in self.crl.issuer]:
620            return False
621
622        old_entries = [self._compress_entry(cryptography_decode_revoked_certificate(cert)) for cert in self.crl]
623        new_entries = [self._compress_entry(cert) for cert in self.revoked_certificates]
624        if self.update:
625            # We don't simply use a set so that duplicate entries are treated correctly
626            for entry in new_entries:
627                try:
628                    old_entries.remove(entry)
629                except ValueError:
630                    return False
631        else:
632            if old_entries != new_entries:
633                return False
634
635        if self.format != self.actual_format and not ignore_conversion:
636            return False
637
638        return True
639
640    def _generate_crl(self):
641        backend = default_backend()
642        crl = CertificateRevocationListBuilder()
643
644        try:
645            crl = crl.issuer_name(Name([
646                NameAttribute(cryptography_name_to_oid(entry[0]), to_text(entry[1]))
647                for entry in self.issuer
648            ]))
649        except ValueError as e:
650            raise CRLError(e)
651
652        crl = crl.last_update(self.last_update)
653        crl = crl.next_update(self.next_update)
654
655        if self.update and self.crl:
656            new_entries = set([self._compress_entry(entry) for entry in self.revoked_certificates])
657            for entry in self.crl:
658                decoded_entry = self._compress_entry(cryptography_decode_revoked_certificate(entry))
659                if decoded_entry not in new_entries:
660                    crl = crl.add_revoked_certificate(entry)
661        for entry in self.revoked_certificates:
662            revoked_cert = RevokedCertificateBuilder()
663            revoked_cert = revoked_cert.serial_number(entry['serial_number'])
664            revoked_cert = revoked_cert.revocation_date(entry['revocation_date'])
665            if entry['issuer'] is not None:
666                revoked_cert = revoked_cert.add_extension(
667                    x509.CertificateIssuer([
668                        cryptography_get_name(name, 'issuer') for name in entry['issuer']
669                    ]),
670                    entry['issuer_critical']
671                )
672            if entry['reason'] is not None:
673                revoked_cert = revoked_cert.add_extension(
674                    x509.CRLReason(entry['reason']),
675                    entry['reason_critical']
676                )
677            if entry['invalidity_date'] is not None:
678                revoked_cert = revoked_cert.add_extension(
679                    x509.InvalidityDate(entry['invalidity_date']),
680                    entry['invalidity_date_critical']
681                )
682            crl = crl.add_revoked_certificate(revoked_cert.build(backend))
683
684        self.crl = crl.sign(self.privatekey, self.digest, backend=backend)
685        if self.format == 'pem':
686            return self.crl.public_bytes(Encoding.PEM)
687        else:
688            return self.crl.public_bytes(Encoding.DER)
689
690    def generate(self):
691        result = None
692        if not self.check(self.module, perms_required=False, ignore_conversion=True) or self.force:
693            result = self._generate_crl()
694        elif not self.check(self.module, perms_required=False, ignore_conversion=False) and self.crl:
695            if self.format == 'pem':
696                result = self.crl.public_bytes(Encoding.PEM)
697            else:
698                result = self.crl.public_bytes(Encoding.DER)
699
700        if result is not None:
701            self.diff_after = self._get_info(result)
702            if self.return_content:
703                if self.format == 'pem':
704                    self.crl_content = result
705                else:
706                    self.crl_content = base64.b64encode(result)
707            if self.backup:
708                self.backup_file = self.module.backup_local(self.path)
709            write_file(self.module, result)
710            self.changed = True
711
712        file_args = self.module.load_file_common_arguments(self.module.params)
713        if self.module.check_file_absent_if_check_mode(file_args['path']):
714            self.changed = True
715        elif self.module.set_fs_attributes_if_different(file_args, False):
716            self.changed = True
717
718    def dump(self, check_mode=False):
719        result = {
720            'changed': self.changed,
721            'filename': self.path,
722            'privatekey': self.privatekey_path,
723            'format': self.format,
724            'last_update': None,
725            'next_update': None,
726            'digest': None,
727            'issuer_ordered': None,
728            'issuer': None,
729            'revoked_certificates': [],
730        }
731        if self.backup_file:
732            result['backup_file'] = self.backup_file
733
734        if check_mode:
735            result['last_update'] = self.last_update.strftime(TIMESTAMP_FORMAT)
736            result['next_update'] = self.next_update.strftime(TIMESTAMP_FORMAT)
737            # result['digest'] = cryptography_oid_to_name(self.crl.signature_algorithm_oid)
738            result['digest'] = self.module.params['digest']
739            result['issuer_ordered'] = self.issuer
740            result['issuer'] = {}
741            for k, v in self.issuer:
742                result['issuer'][k] = v
743            result['revoked_certificates'] = []
744            for entry in self.revoked_certificates:
745                result['revoked_certificates'].append(cryptography_dump_revoked(entry))
746        elif self.crl:
747            result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
748            result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
749            result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl))
750            issuer = []
751            for attribute in self.crl.issuer:
752                issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value])
753            result['issuer_ordered'] = issuer
754            result['issuer'] = {}
755            for k, v in issuer:
756                result['issuer'][k] = v
757            result['revoked_certificates'] = []
758            for cert in self.crl:
759                entry = cryptography_decode_revoked_certificate(cert)
760                result['revoked_certificates'].append(cryptography_dump_revoked(entry))
761
762        if self.return_content:
763            result['crl'] = self.crl_content
764
765        result['diff'] = dict(
766            before=self.diff_before,
767            after=self.diff_after,
768        )
769        return result
770
771
772def main():
773    module = AnsibleModule(
774        argument_spec=dict(
775            state=dict(type='str', default='present', choices=['present', 'absent']),
776            mode=dict(type='str', default='generate', choices=['generate', 'update']),
777            force=dict(type='bool', default=False),
778            backup=dict(type='bool', default=False),
779            path=dict(type='path', required=True),
780            format=dict(type='str', default='pem', choices=['pem', 'der']),
781            privatekey_path=dict(type='path'),
782            privatekey_content=dict(type='str', no_log=True),
783            privatekey_passphrase=dict(type='str', no_log=True),
784            issuer=dict(type='dict'),
785            last_update=dict(type='str', default='+0s'),
786            next_update=dict(type='str'),
787            digest=dict(type='str', default='sha256'),
788            ignore_timestamps=dict(type='bool', default=False),
789            return_content=dict(type='bool', default=False),
790            revoked_certificates=dict(
791                type='list',
792                elements='dict',
793                options=dict(
794                    path=dict(type='path'),
795                    content=dict(type='str'),
796                    serial_number=dict(type='int'),
797                    revocation_date=dict(type='str', default='+0s'),
798                    issuer=dict(type='list', elements='str'),
799                    issuer_critical=dict(type='bool', default=False),
800                    reason=dict(
801                        type='str',
802                        choices=[
803                            'unspecified', 'key_compromise', 'ca_compromise', 'affiliation_changed',
804                            'superseded', 'cessation_of_operation', 'certificate_hold',
805                            'privilege_withdrawn', 'aa_compromise', 'remove_from_crl'
806                        ]
807                    ),
808                    reason_critical=dict(type='bool', default=False),
809                    invalidity_date=dict(type='str'),
810                    invalidity_date_critical=dict(type='bool', default=False),
811                ),
812                required_one_of=[['path', 'content', 'serial_number']],
813                mutually_exclusive=[['path', 'content', 'serial_number']],
814            ),
815        ),
816        required_if=[
817            ('state', 'present', ['privatekey_path', 'privatekey_content'], True),
818            ('state', 'present', ['issuer', 'next_update', 'revoked_certificates'], False),
819        ],
820        mutually_exclusive=(
821            ['privatekey_path', 'privatekey_content'],
822        ),
823        supports_check_mode=True,
824        add_file_common_args=True,
825    )
826
827    if not CRYPTOGRAPHY_FOUND:
828        module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
829                         exception=CRYPTOGRAPHY_IMP_ERR)
830
831    try:
832        crl = CRL(module)
833
834        if module.params['state'] == 'present':
835            if module.check_mode:
836                result = crl.dump(check_mode=True)
837                result['changed'] = module.params['force'] or not crl.check(module) or not crl.check(module, ignore_conversion=False)
838                module.exit_json(**result)
839
840            crl.generate()
841        else:
842            if module.check_mode:
843                result = crl.dump(check_mode=True)
844                result['changed'] = os.path.exists(module.params['path'])
845                module.exit_json(**result)
846
847            crl.remove()
848
849        result = crl.dump()
850        module.exit_json(**result)
851    except OpenSSLObjectError as exc:
852        module.fail_json(msg=to_native(exc))
853
854
855if __name__ == "__main__":
856    main()
857