1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2017, Guillaume Delpierre <gde@llew.me>
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: openssl_pkcs12
14author:
15- Guillaume Delpierre (@gdelpierre)
16short_description: Generate OpenSSL PKCS#12 archive
17description:
18    - This module allows one to (re-)generate PKCS#12.
19    - The module can use the cryptography Python library, or the pyOpenSSL Python
20      library. By default, it tries to detect which one is available, assuming none of the
21      I(iter_size) and I(maciter_size) options are used. This can be overridden with the
22      I(select_crypto_backend) option.
23    # Please note that the C(pyopenssl) backend has been deprecated in community.crypto x.y.0,
24    # and will be removed in community.crypto (x+1).0.0.
25requirements:
26    - PyOpenSSL >= 0.15 or cryptography >= 3.0
27options:
28    action:
29        description:
30            - C(export) or C(parse) a PKCS#12.
31        type: str
32        default: export
33        choices: [ export, parse ]
34    other_certificates:
35        description:
36            - List of other certificates to include. Pre Ansible 2.8 this parameter was called I(ca_certificates).
37            - Assumes there is one PEM-encoded certificate per file. If a file contains multiple PEM certificates,
38              set I(other_certificates_parse_all) to C(true).
39        type: list
40        elements: path
41        aliases: [ ca_certificates ]
42    other_certificates_parse_all:
43        description:
44            - If set to C(true), assumes that the files mentioned in I(other_certificates) can contain more than one
45              certificate per file (or even none per file).
46        type: bool
47        default: false
48        version_added: 1.4.0
49    certificate_path:
50        description:
51            - The path to read certificates and private keys from.
52            - Must be in PEM format.
53        type: path
54    force:
55        description:
56            - Should the file be regenerated even if it already exists.
57        type: bool
58        default: no
59    friendly_name:
60        description:
61            - Specifies the friendly name for the certificate and private key.
62        type: str
63        aliases: [ name ]
64    iter_size:
65        description:
66            - Number of times to repeat the encryption step.
67            - This is not considered during idempotency checks.
68            - This is only used by the C(pyopenssl) backend. When using it, the default is C(2048).
69        type: int
70    maciter_size:
71        description:
72            - Number of times to repeat the MAC step.
73            - This is not considered during idempotency checks.
74            - This is only used by the C(pyopenssl) backend. When using it, the default is C(1).
75        type: int
76    passphrase:
77        description:
78            - The PKCS#12 password.
79            - "B(Note:) PKCS12 encryption is not secure and should not be used as a security mechanism.
80              If you need to store or send a PKCS12 file safely, you should additionally encrypt it
81              with something else."
82        type: str
83    path:
84        description:
85            - Filename to write the PKCS#12 file to.
86        type: path
87        required: true
88    privatekey_passphrase:
89        description:
90            - Passphrase source to decrypt any input private keys with.
91        type: str
92    privatekey_path:
93        description:
94            - File to read private key from.
95        type: path
96    state:
97        description:
98            - Whether the file should exist or not.
99              All parameters except C(path) are ignored when state is C(absent).
100        choices: [ absent, present ]
101        default: present
102        type: str
103    src:
104        description:
105            - PKCS#12 file path to parse.
106        type: path
107    backup:
108        description:
109            - Create a backup file including a timestamp so you can get the original
110              output file back if you overwrote it with a new one by accident.
111        type: bool
112        default: no
113    return_content:
114        description:
115            - If set to C(yes), will return the (current or generated) PKCS#12's content as I(pkcs12).
116        type: bool
117        default: no
118        version_added: "1.0.0"
119    select_crypto_backend:
120        description:
121            - Determines which crypto backend to use.
122            - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl).
123              If one of I(iter_size) or I(maciter_size) is used, C(auto) will always result in C(pyopenssl) to be chosen
124              for backwards compatibility.
125            - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
126            - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
127            # - Please note that the C(pyopenssl) backend has been deprecated in community.crypto x.y.0, and will be
128            #   removed in community.crypto (x+1).0.0.
129            #   From that point on, only the C(cryptography) backend will be available.
130        type: str
131        default: auto
132        choices: [ auto, cryptography, pyopenssl ]
133        version_added: 1.7.0
134extends_documentation_fragment:
135    - files
136seealso:
137- module: community.crypto.x509_certificate
138- module: community.crypto.openssl_csr
139- module: community.crypto.openssl_dhparam
140- module: community.crypto.openssl_privatekey
141- module: community.crypto.openssl_publickey
142'''
143
144EXAMPLES = r'''
145- name: Generate PKCS#12 file
146  community.crypto.openssl_pkcs12:
147    action: export
148    path: /opt/certs/ansible.p12
149    friendly_name: raclette
150    privatekey_path: /opt/certs/keys/key.pem
151    certificate_path: /opt/certs/cert.pem
152    other_certificates: /opt/certs/ca.pem
153    # Note that if /opt/certs/ca.pem contains multiple certificates,
154    # only the first one will be used. See the other_certificates_parse_all
155    # option for changing this behavior.
156    state: present
157
158- name: Generate PKCS#12 file
159  community.crypto.openssl_pkcs12:
160    action: export
161    path: /opt/certs/ansible.p12
162    friendly_name: raclette
163    privatekey_path: /opt/certs/keys/key.pem
164    certificate_path: /opt/certs/cert.pem
165    other_certificates_parse_all: true
166    other_certificates:
167      - /opt/certs/ca_bundle.pem
168        # Since we set other_certificates_parse_all to true, all
169        # certificates in the CA bundle are included and not just
170        # the first one.
171      - /opt/certs/intermediate.pem
172        # In case this file has multiple certificates in it,
173        # all will be included as well.
174    state: present
175
176- name: Change PKCS#12 file permission
177  community.crypto.openssl_pkcs12:
178    action: export
179    path: /opt/certs/ansible.p12
180    friendly_name: raclette
181    privatekey_path: /opt/certs/keys/key.pem
182    certificate_path: /opt/certs/cert.pem
183    other_certificates: /opt/certs/ca.pem
184    state: present
185    mode: '0600'
186
187- name: Regen PKCS#12 file
188  community.crypto.openssl_pkcs12:
189    action: export
190    src: /opt/certs/ansible.p12
191    path: /opt/certs/ansible.p12
192    friendly_name: raclette
193    privatekey_path: /opt/certs/keys/key.pem
194    certificate_path: /opt/certs/cert.pem
195    other_certificates: /opt/certs/ca.pem
196    state: present
197    mode: '0600'
198    force: yes
199
200- name: Dump/Parse PKCS#12 file
201  community.crypto.openssl_pkcs12:
202    action: parse
203    src: /opt/certs/ansible.p12
204    path: /opt/certs/ansible.pem
205    state: present
206
207- name: Remove PKCS#12 file
208  community.crypto.openssl_pkcs12:
209    path: /opt/certs/ansible.p12
210    state: absent
211'''
212
213RETURN = r'''
214filename:
215    description: Path to the generate PKCS#12 file.
216    returned: changed or success
217    type: str
218    sample: /opt/certs/ansible.p12
219privatekey:
220    description: Path to the TLS/SSL private key the public key was generated from.
221    returned: changed or success
222    type: str
223    sample: /etc/ssl/private/ansible.com.pem
224backup_file:
225    description: Name of backup file created.
226    returned: changed and if I(backup) is C(yes)
227    type: str
228    sample: /path/to/ansible.com.pem.2019-03-09@11:22~
229pkcs12:
230    description: The (current or generated) PKCS#12's content Base64 encoded.
231    returned: if I(state) is C(present) and I(return_content) is C(yes)
232    type: str
233    version_added: "1.0.0"
234'''
235
236import abc
237import base64
238import os
239import stat
240import traceback
241
242from distutils.version import LooseVersion
243
244from ansible.module_utils.basic import AnsibleModule, missing_required_lib
245from ansible.module_utils.common.text.converters import to_bytes, to_native
246
247from ansible_collections.community.crypto.plugins.module_utils.io import (
248    load_file_if_exists,
249    write_file,
250)
251
252from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
253    OpenSSLObjectError,
254    OpenSSLBadPassphraseError,
255)
256
257from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
258    parse_pkcs12,
259)
260
261from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
262    OpenSSLObject,
263    load_privatekey,
264    load_certificate,
265)
266
267from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
268    split_pem_list,
269)
270
271MINIMAL_CRYPTOGRAPHY_VERSION = '3.0'
272MINIMAL_PYOPENSSL_VERSION = '0.15'
273
274PYOPENSSL_IMP_ERR = None
275try:
276    import OpenSSL
277    from OpenSSL import crypto
278    PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
279except ImportError:
280    PYOPENSSL_IMP_ERR = traceback.format_exc()
281    PYOPENSSL_FOUND = False
282else:
283    PYOPENSSL_FOUND = True
284
285CRYPTOGRAPHY_IMP_ERR = None
286try:
287    import cryptography
288    from cryptography.hazmat.primitives import serialization
289    from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates
290    CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
291except ImportError:
292    CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
293    CRYPTOGRAPHY_FOUND = False
294else:
295    CRYPTOGRAPHY_FOUND = True
296
297
298def load_certificate_set(filename, backend):
299    '''
300    Load list of concatenated PEM files, and return a list of parsed certificates.
301    '''
302    with open(filename, 'rb') as f:
303        data = f.read().decode('utf-8')
304    return [load_certificate(None, content=cert.encode('utf-8'), backend=backend) for cert in split_pem_list(data)]
305
306
307class PkcsError(OpenSSLObjectError):
308    pass
309
310
311class Pkcs(OpenSSLObject):
312    def __init__(self, module, backend):
313        super(Pkcs, self).__init__(
314            module.params['path'],
315            module.params['state'],
316            module.params['force'],
317            module.check_mode
318        )
319        self.backend = backend
320        self.action = module.params['action']
321        self.other_certificates = module.params['other_certificates']
322        self.other_certificates_parse_all = module.params['other_certificates_parse_all']
323        self.certificate_path = module.params['certificate_path']
324        self.friendly_name = module.params['friendly_name']
325        self.iter_size = module.params['iter_size'] or 2048
326        self.maciter_size = module.params['maciter_size'] or 1
327        self.passphrase = module.params['passphrase']
328        self.pkcs12 = None
329        self.privatekey_passphrase = module.params['privatekey_passphrase']
330        self.privatekey_path = module.params['privatekey_path']
331        self.pkcs12_bytes = None
332        self.return_content = module.params['return_content']
333        self.src = module.params['src']
334
335        if module.params['mode'] is None:
336            module.params['mode'] = '0400'
337
338        self.backup = module.params['backup']
339        self.backup_file = None
340
341        if self.other_certificates:
342            if self.other_certificates_parse_all:
343                filenames = list(self.other_certificates)
344                self.other_certificates = []
345                for other_cert_bundle in filenames:
346                    self.other_certificates.extend(load_certificate_set(other_cert_bundle, self.backend))
347            else:
348                self.other_certificates = [
349                    load_certificate(other_cert, backend=self.backend) for other_cert in self.other_certificates
350                ]
351
352    @abc.abstractmethod
353    def generate_bytes(self, module):
354        """Generate PKCS#12 file archive."""
355        pass
356
357    @abc.abstractmethod
358    def parse_bytes(self, pkcs12_content):
359        pass
360
361    @abc.abstractmethod
362    def _dump_privatekey(self, pkcs12):
363        pass
364
365    @abc.abstractmethod
366    def _dump_certificate(self, pkcs12):
367        pass
368
369    @abc.abstractmethod
370    def _dump_other_certificates(self, pkcs12):
371        pass
372
373    @abc.abstractmethod
374    def _get_friendly_name(self, pkcs12):
375        pass
376
377    def check(self, module, perms_required=True):
378        """Ensure the resource is in its desired state."""
379
380        state_and_perms = super(Pkcs, self).check(module, perms_required)
381
382        def _check_pkey_passphrase():
383            if self.privatekey_passphrase:
384                try:
385                    load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend)
386                except OpenSSLObjectError:
387                    return False
388            return True
389
390        if not state_and_perms:
391            return state_and_perms
392
393        if os.path.exists(self.path) and module.params['action'] == 'export':
394            dummy = self.generate_bytes(module)
395            self.src = self.path
396            try:
397                pkcs12_privatekey, pkcs12_certificate, pkcs12_other_certificates, pkcs12_friendly_name = self.parse()
398            except OpenSSLObjectError:
399                return False
400            if (pkcs12_privatekey is not None) and (self.privatekey_path is not None):
401                expected_pkey = self._dump_privatekey(self.pkcs12)
402                if pkcs12_privatekey != expected_pkey:
403                    return False
404            elif bool(pkcs12_privatekey) != bool(self.privatekey_path):
405                return False
406
407            if (pkcs12_certificate is not None) and (self.certificate_path is not None):
408                expected_cert = self._dump_certificate(self.pkcs12)
409                if pkcs12_certificate != expected_cert:
410                    return False
411            elif bool(pkcs12_certificate) != bool(self.certificate_path):
412                return False
413
414            if (pkcs12_other_certificates is not None) and (self.other_certificates is not None):
415                expected_other_certs = self._dump_other_certificates(self.pkcs12)
416                if set(pkcs12_other_certificates) != set(expected_other_certs):
417                    return False
418            elif bool(pkcs12_other_certificates) != bool(self.other_certificates):
419                return False
420
421            if pkcs12_privatekey:
422                # This check is required because pyOpenSSL will not return a friendly name
423                # if the private key is not set in the file
424                friendly_name = self._get_friendly_name(self.pkcs12)
425                if ((friendly_name is not None) and (pkcs12_friendly_name is not None)):
426                    if friendly_name != pkcs12_friendly_name:
427                        return False
428                elif bool(friendly_name) != bool(pkcs12_friendly_name):
429                    return False
430        elif module.params['action'] == 'parse' and os.path.exists(self.src) and os.path.exists(self.path):
431            try:
432                pkey, cert, other_certs, friendly_name = self.parse()
433            except OpenSSLObjectError:
434                return False
435            expected_content = to_bytes(
436                ''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None])
437            )
438            dumped_content = load_file_if_exists(self.path, ignore_errors=True)
439            if expected_content != dumped_content:
440                return False
441        else:
442            return False
443
444        return _check_pkey_passphrase()
445
446    def dump(self):
447        """Serialize the object into a dictionary."""
448
449        result = {
450            'filename': self.path,
451        }
452        if self.privatekey_path:
453            result['privatekey_path'] = self.privatekey_path
454        if self.backup_file:
455            result['backup_file'] = self.backup_file
456        if self.return_content:
457            if self.pkcs12_bytes is None:
458                self.pkcs12_bytes = load_file_if_exists(self.path, ignore_errors=True)
459            result['pkcs12'] = base64.b64encode(self.pkcs12_bytes) if self.pkcs12_bytes else None
460
461        return result
462
463    def remove(self, module):
464        if self.backup:
465            self.backup_file = module.backup_local(self.path)
466        super(Pkcs, self).remove(module)
467
468    def parse(self):
469        """Read PKCS#12 file."""
470
471        try:
472            with open(self.src, 'rb') as pkcs12_fh:
473                pkcs12_content = pkcs12_fh.read()
474            return self.parse_bytes(pkcs12_content)
475        except IOError as exc:
476            raise PkcsError(exc)
477
478    def generate(self):
479        pass
480
481    def write(self, module, content, mode=None):
482        """Write the PKCS#12 file."""
483        if self.backup:
484            self.backup_file = module.backup_local(self.path)
485        write_file(module, content, mode)
486        if self.return_content:
487            self.pkcs12_bytes = content
488
489
490class PkcsPyOpenSSL(Pkcs):
491    def __init__(self, module):
492        super(PkcsPyOpenSSL, self).__init__(module, 'pyopenssl')
493
494    def generate_bytes(self, module):
495        """Generate PKCS#12 file archive."""
496        self.pkcs12 = crypto.PKCS12()
497
498        if self.other_certificates:
499            self.pkcs12.set_ca_certificates(self.other_certificates)
500
501        if self.certificate_path:
502            self.pkcs12.set_certificate(load_certificate(self.certificate_path, backend=self.backend))
503
504        if self.friendly_name:
505            self.pkcs12.set_friendlyname(to_bytes(self.friendly_name))
506
507        if self.privatekey_path:
508            try:
509                self.pkcs12.set_privatekey(
510                    load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend))
511            except OpenSSLBadPassphraseError as exc:
512                raise PkcsError(exc)
513
514        return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size)
515
516    def parse_bytes(self, pkcs12_content):
517        try:
518            p12 = crypto.load_pkcs12(pkcs12_content, self.passphrase)
519            pkey = p12.get_privatekey()
520            if pkey is not None:
521                pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
522            crt = p12.get_certificate()
523            if crt is not None:
524                crt = crypto.dump_certificate(crypto.FILETYPE_PEM, crt)
525            other_certs = []
526            if p12.get_ca_certificates() is not None:
527                other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM,
528                                                       other_cert) for other_cert in p12.get_ca_certificates()]
529
530            friendly_name = p12.get_friendlyname()
531
532            return (pkey, crt, other_certs, friendly_name)
533        except crypto.Error as exc:
534            raise PkcsError(exc)
535
536    def _dump_privatekey(self, pkcs12):
537        pk = pkcs12.get_privatekey()
538        return crypto.dump_privatekey(crypto.FILETYPE_PEM, pk) if pk else None
539
540    def _dump_certificate(self, pkcs12):
541        cert = pkcs12.get_certificate()
542        return crypto.dump_certificate(crypto.FILETYPE_PEM, cert) if cert else None
543
544    def _dump_other_certificates(self, pkcs12):
545        return [
546            crypto.dump_certificate(crypto.FILETYPE_PEM, other_cert)
547            for other_cert in pkcs12.get_ca_certificates()
548        ]
549
550    def _get_friendly_name(self, pkcs12):
551        return pkcs12.get_friendlyname()
552
553
554class PkcsCryptography(Pkcs):
555    def __init__(self, module):
556        super(PkcsCryptography, self).__init__(module, 'cryptography')
557
558    def generate_bytes(self, module):
559        """Generate PKCS#12 file archive."""
560        pkey = None
561        if self.privatekey_path:
562            try:
563                pkey = load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend)
564            except OpenSSLBadPassphraseError as exc:
565                raise PkcsError(exc)
566
567        cert = None
568        if self.certificate_path:
569            cert = load_certificate(self.certificate_path, backend=self.backend)
570
571        friendly_name = to_bytes(self.friendly_name) if self.friendly_name is not None else None
572
573        # Store fake object which can be used to retrieve the components back
574        self.pkcs12 = (pkey, cert, self.other_certificates, friendly_name)
575
576        return serialize_key_and_certificates(
577            friendly_name,
578            pkey,
579            cert,
580            self.other_certificates,
581            serialization.BestAvailableEncryption(to_bytes(self.passphrase))
582            if self.passphrase else serialization.NoEncryption(),
583        )
584
585    def parse_bytes(self, pkcs12_content):
586        try:
587            private_key, certificate, additional_certificates, friendly_name = parse_pkcs12(
588                pkcs12_content, self.passphrase)
589
590            pkey = None
591            if private_key is not None:
592                pkey = private_key.private_bytes(
593                    encoding=serialization.Encoding.PEM,
594                    format=serialization.PrivateFormat.TraditionalOpenSSL,
595                    encryption_algorithm=serialization.NoEncryption(),
596                )
597
598            crt = None
599            if certificate is not None:
600                crt = certificate.public_bytes(serialization.Encoding.PEM)
601
602            other_certs = []
603            if additional_certificates is not None:
604                other_certs = [
605                    other_cert.public_bytes(serialization.Encoding.PEM)
606                    for other_cert in additional_certificates
607                ]
608
609            return (pkey, crt, other_certs, friendly_name)
610        except ValueError as exc:
611            raise PkcsError(exc)
612
613    # The following methods will get self.pkcs12 passed, which is computed as:
614    #
615    #     self.pkcs12 = (pkey, cert, self.other_certificates, self.friendly_name)
616
617    def _dump_privatekey(self, pkcs12):
618        return pkcs12[0].private_bytes(
619            encoding=serialization.Encoding.PEM,
620            format=serialization.PrivateFormat.TraditionalOpenSSL,
621            encryption_algorithm=serialization.NoEncryption(),
622        ) if pkcs12[0] else None
623
624    def _dump_certificate(self, pkcs12):
625        return pkcs12[1].public_bytes(serialization.Encoding.PEM) if pkcs12[1] else None
626
627    def _dump_other_certificates(self, pkcs12):
628        return [other_cert.public_bytes(serialization.Encoding.PEM) for other_cert in pkcs12[2]]
629
630    def _get_friendly_name(self, pkcs12):
631        return pkcs12[3]
632
633
634def select_backend(module, backend):
635    if backend == 'auto':
636        # Detection what is possible
637        can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
638        can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
639
640        # If no restrictions are provided, first try cryptography, then pyOpenSSL
641        if module.params['iter_size'] is not None or module.params['maciter_size'] is not None:
642            # If iter_size or maciter_size is specified, use pyOpenSSL backend
643            backend = 'pyopenssl'
644        elif can_use_cryptography:
645            backend = 'cryptography'
646        elif can_use_pyopenssl:
647            backend = 'pyopenssl'
648
649        # Success?
650        if backend == 'auto':
651            module.fail_json(msg=("Can't detect any of the required Python libraries "
652                                  "cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
653                                      MINIMAL_CRYPTOGRAPHY_VERSION,
654                                      MINIMAL_PYOPENSSL_VERSION))
655
656    if backend == 'pyopenssl':
657        if not PYOPENSSL_FOUND:
658            module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
659                             exception=PYOPENSSL_IMP_ERR)
660        # module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
661        #                  version='x.0.0', collection_name='community.crypto')
662        return backend, PkcsPyOpenSSL(module)
663    elif backend == 'cryptography':
664        if not CRYPTOGRAPHY_FOUND:
665            module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
666                             exception=CRYPTOGRAPHY_IMP_ERR)
667        return backend, PkcsCryptography(module)
668    else:
669        raise ValueError('Unsupported value for backend: {0}'.format(backend))
670
671
672def main():
673    argument_spec = dict(
674        action=dict(type='str', default='export', choices=['export', 'parse']),
675        other_certificates=dict(type='list', elements='path', aliases=['ca_certificates']),
676        other_certificates_parse_all=dict(type='bool', default=False),
677        certificate_path=dict(type='path'),
678        force=dict(type='bool', default=False),
679        friendly_name=dict(type='str', aliases=['name']),
680        iter_size=dict(type='int'),
681        maciter_size=dict(type='int'),
682        passphrase=dict(type='str', no_log=True),
683        path=dict(type='path', required=True),
684        privatekey_passphrase=dict(type='str', no_log=True),
685        privatekey_path=dict(type='path'),
686        state=dict(type='str', default='present', choices=['absent', 'present']),
687        src=dict(type='path'),
688        backup=dict(type='bool', default=False),
689        return_content=dict(type='bool', default=False),
690        select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
691    )
692
693    required_if = [
694        ['action', 'parse', ['src']],
695    ]
696
697    module = AnsibleModule(
698        add_file_common_args=True,
699        argument_spec=argument_spec,
700        required_if=required_if,
701        supports_check_mode=True,
702    )
703
704    backend, pkcs12 = select_backend(module, module.params['select_crypto_backend'])
705
706    base_dir = os.path.dirname(module.params['path']) or '.'
707    if not os.path.isdir(base_dir):
708        module.fail_json(
709            name=base_dir,
710            msg="The directory '%s' does not exist or the path is not a directory" % base_dir
711        )
712
713    try:
714        changed = False
715
716        if module.params['state'] == 'present':
717            if module.check_mode:
718                result = pkcs12.dump()
719                result['changed'] = module.params['force'] or not pkcs12.check(module)
720                module.exit_json(**result)
721
722            if not pkcs12.check(module, perms_required=False) or module.params['force']:
723                if module.params['action'] == 'export':
724                    if not module.params['friendly_name']:
725                        module.fail_json(msg='Friendly_name is required')
726                    pkcs12_content = pkcs12.generate_bytes(module)
727                    pkcs12.write(module, pkcs12_content, 0o600)
728                    changed = True
729                else:
730                    pkey, cert, other_certs, friendly_name = pkcs12.parse()
731                    dump_content = ''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None])
732                    pkcs12.write(module, to_bytes(dump_content))
733                    changed = True
734
735            file_args = module.load_file_common_arguments(module.params)
736            if module.check_file_absent_if_check_mode(file_args['path']):
737                changed = True
738            elif module.set_fs_attributes_if_different(file_args, changed):
739                changed = True
740        else:
741            if module.check_mode:
742                result = pkcs12.dump()
743                result['changed'] = os.path.exists(module.params['path'])
744                module.exit_json(**result)
745
746            if os.path.exists(module.params['path']):
747                pkcs12.remove(module)
748                changed = True
749
750        result = pkcs12.dump()
751        result['changed'] = changed
752        if os.path.exists(module.params['path']):
753            file_mode = "%04o" % stat.S_IMODE(os.stat(module.params['path']).st_mode)
754            result['mode'] = file_mode
755
756        module.exit_json(**result)
757    except OpenSSLObjectError as exc:
758        module.fail_json(msg=to_native(exc))
759
760
761if __name__ == '__main__':
762    main()
763