From e9bc7c7163ed752e3b73e8cb8f9c135cda3a69d7 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Thu, 20 May 2021 19:36:07 +0200 Subject: [PATCH] openssl_pkcs12: add cryptography backend (#234) * Began refactoring. * Continue. * Factor PyOpenSSL backend out. * Add basic cryptography backend. * Update plugins/modules/openssl_pkcs12.py Co-authored-by: Ajpantuso * Only run tests when new enough pyOpenSSL or cryptography is around. * Reduce required pyOpenSSL version from 17.1.0 to 0.15. I have no idea why 17.1.0 was there (in the tests), and not something smaller. The module itself did not mention any version. * Linting. * Linting. * Increase compatibility by selecting pyopenssl backend when iter_size or maciter_size is used. * Improve docs, add changelog fragment. * Move hackish code to cryptography_support. * Update plugins/modules/openssl_pkcs12.py Co-authored-by: Ajpantuso * Update plugins/modules/openssl_pkcs12.py Co-authored-by: Ajpantuso * Streamline cert creation. * Convert range to list. Co-authored-by: Ajpantuso --- .../234-openssl_pkcs12-cryptography.yml | 4 + .../crypto/cryptography_support.py | 27 ++ plugins/modules/openssl_pkcs12.py | 353 ++++++++++++++---- .../targets/openssl_pkcs12/tasks/impl.yml | 212 +++++------ .../targets/openssl_pkcs12/tasks/main.yml | 69 +++- .../targets/openssl_pkcs12/tests/validate.yml | 26 +- 6 files changed, 504 insertions(+), 187 deletions(-) create mode 100644 changelogs/fragments/234-openssl_pkcs12-cryptography.yml diff --git a/changelogs/fragments/234-openssl_pkcs12-cryptography.yml b/changelogs/fragments/234-openssl_pkcs12-cryptography.yml new file mode 100644 index 00000000..d6bf9da7 --- /dev/null +++ b/changelogs/fragments/234-openssl_pkcs12-cryptography.yml @@ -0,0 +1,4 @@ +minor_changes: +- "openssl_pkcs12 - added option ``select_crypto_backend`` and a ``cryptography`` backend. + This requires cryptography 3.0 or newer, and does not support the ``iter_size`` and ``maciter_size`` options + (https://github.com/ansible-collections/community.crypto/pull/234)." diff --git a/plugins/module_utils/crypto/cryptography_support.py b/plugins/module_utils/crypto/cryptography_support.py index 67ec34f4..8a1580da 100644 --- a/plugins/module_utils/crypto/cryptography_support.py +++ b/plugins/module_utils/crypto/cryptography_support.py @@ -35,6 +35,15 @@ except ImportError: # Error handled in the calling module. pass +try: + # This is a separate try/except since this is only present in cryptography 2.5 or newer + from cryptography.hazmat.primitives.serialization.pkcs12 import ( + load_key_and_certificates as _load_key_and_certificates, + ) +except ImportError: + # Error handled in the calling module. + _load_key_and_certificates = None + from .basic import ( CRYPTOGRAPHY_HAS_ED25519, CRYPTOGRAPHY_HAS_ED448, @@ -428,3 +437,21 @@ def cryptography_serial_number_of_cert(cert): except AttributeError: # The property was called "serial" before cryptography 1.4 return cert.serial + + +def parse_pkcs12(pkcs12_bytes, passphrase=None): + '''Returns a tuple (private_key, certificate, additional_certificates, friendly_name). + ''' + if _load_key_and_certificates is None: + raise ValueError('load_key_and_certificates() not present in the current cryptography version') + private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase) + + friendly_name = None + if certificate: + # See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238 + maybe_name = certificate._backend._lib.X509_alias_get0( + certificate._x509, certificate._backend._ffi.NULL) + if maybe_name != certificate._backend._ffi.NULL: + friendly_name = certificate._backend._ffi.string(maybe_name) + + return private_key, certificate, additional_certificates, friendly_name diff --git a/plugins/modules/openssl_pkcs12.py b/plugins/modules/openssl_pkcs12.py index 16939887..aee76ca7 100644 --- a/plugins/modules/openssl_pkcs12.py +++ b/plugins/modules/openssl_pkcs12.py @@ -16,8 +16,14 @@ author: short_description: Generate OpenSSL PKCS#12 archive description: - This module allows one to (re-)generate PKCS#12. + - The module can use the cryptography Python library, or the pyOpenSSL Python + library. By default, it tries to detect which one is available, assuming none of the + I(iter_size) and I(maciter_size) options are used. This can be overridden with the + I(select_crypto_backend) option. + # Please note that the C(pyopenssl) backend has been deprecated in community.crypto x.y.0, + # and will be removed in community.crypto (x+1).0.0. requirements: - - python-pyOpenSSL + - PyOpenSSL >= 0.15 or cryptography >= 3.0 options: action: description: @@ -58,16 +64,21 @@ options: iter_size: description: - Number of times to repeat the encryption step. + - This is not considered during idempotency checks. + - This is only used by the C(pyopenssl) backend. When using it, the default is C(2048). type: int - default: 2048 maciter_size: description: - Number of times to repeat the MAC step. + - This is not considered during idempotency checks. + - This is only used by the C(pyopenssl) backend. When using it, the default is C(1). type: int - default: 1 passphrase: description: - The PKCS#12 password. + - "B(Note:) PKCS12 encryption is not secure and should not be used as a security mechanism. + If you need to store or send a PKCS12 file safely, you should additionally encrypt it + with something else." type: str path: description: @@ -105,6 +116,21 @@ options: type: bool default: no version_added: "1.0.0" + select_crypto_backend: + description: + - Determines which crypto backend to use. + - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl). + If one of I(iter_size) or I(maciter_size) is used, C(auto) will always result in C(pyopenssl) to be chosen + for backwards compatibility. + - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. + - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. + # - Please note that the C(pyopenssl) backend has been deprecated in community.crypto x.y.0, and will be + # removed in community.crypto (x+1).0.0. + # From that point on, only the C(cryptography) backend will be available. + type: str + default: auto + choices: [ auto, cryptography, pyopenssl ] + version_added: 1.7.0 extends_documentation_fragment: - files seealso: @@ -207,11 +233,14 @@ pkcs12: version_added: "1.0.0" ''' +import abc import base64 import os import stat import traceback +from distutils.version import LooseVersion + from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_bytes, to_native @@ -225,6 +254,10 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.basic impo OpenSSLBadPassphraseError, ) +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + parse_pkcs12, +) + from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( OpenSSLObject, load_privatekey, @@ -235,23 +268,40 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import split_pem_list, ) +MINIMAL_CRYPTOGRAPHY_VERSION = '3.0' +MINIMAL_PYOPENSSL_VERSION = '0.15' + PYOPENSSL_IMP_ERR = None try: + import OpenSSL from OpenSSL import crypto + PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) except ImportError: PYOPENSSL_IMP_ERR = traceback.format_exc() - pyopenssl_found = False + PYOPENSSL_FOUND = False else: - pyopenssl_found = True + PYOPENSSL_FOUND = True + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True -def load_certificate_set(filename): +def load_certificate_set(filename, backend): ''' Load list of concatenated PEM files, and return a list of parsed certificates. ''' with open(filename, 'rb') as f: data = f.read().decode('utf-8') - return [load_certificate(None, content=cert) for cert in split_pem_list(data)] + return [load_certificate(None, content=cert.encode('utf-8'), backend=backend) for cert in split_pem_list(data)] class PkcsError(OpenSSLObjectError): @@ -259,21 +309,21 @@ class PkcsError(OpenSSLObjectError): class Pkcs(OpenSSLObject): - - def __init__(self, module): + def __init__(self, module, backend): super(Pkcs, self).__init__( module.params['path'], module.params['state'], module.params['force'], module.check_mode ) + self.backend = backend self.action = module.params['action'] self.other_certificates = module.params['other_certificates'] self.other_certificates_parse_all = module.params['other_certificates_parse_all'] self.certificate_path = module.params['certificate_path'] self.friendly_name = module.params['friendly_name'] - self.iter_size = module.params['iter_size'] - self.maciter_size = module.params['maciter_size'] + self.iter_size = module.params['iter_size'] or 2048 + self.maciter_size = module.params['maciter_size'] or 1 self.passphrase = module.params['passphrase'] self.pkcs12 = None self.privatekey_passphrase = module.params['privatekey_passphrase'] @@ -293,12 +343,37 @@ class Pkcs(OpenSSLObject): filenames = list(self.other_certificates) self.other_certificates = [] for other_cert_bundle in filenames: - self.other_certificates.extend(load_certificate_set(other_cert_bundle)) + self.other_certificates.extend(load_certificate_set(other_cert_bundle, self.backend)) else: self.other_certificates = [ - load_certificate(other_cert) for other_cert in self.other_certificates + load_certificate(other_cert, backend=self.backend) for other_cert in self.other_certificates ] + @abc.abstractmethod + def generate_bytes(self, module): + """Generate PKCS#12 file archive.""" + pass + + @abc.abstractmethod + def parse_bytes(self, pkcs12_content): + pass + + @abc.abstractmethod + def _dump_privatekey(self, pkcs12): + pass + + @abc.abstractmethod + def _dump_certificate(self, pkcs12): + pass + + @abc.abstractmethod + def _dump_other_certificates(self, pkcs12): + pass + + @abc.abstractmethod + def _get_friendly_name(self, pkcs12): + pass + def check(self, module, perms_required=True): """Ensure the resource is in its desired state.""" @@ -307,10 +382,8 @@ class Pkcs(OpenSSLObject): def _check_pkey_passphrase(): if self.privatekey_passphrase: try: - load_privatekey(self.privatekey_path, self.privatekey_passphrase) - except crypto.Error: - return False - except OpenSSLBadPassphraseError: + load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend) + except OpenSSLObjectError: return False return True @@ -318,32 +391,28 @@ class Pkcs(OpenSSLObject): return state_and_perms if os.path.exists(self.path) and module.params['action'] == 'export': - dummy = self.generate(module) + dummy = self.generate_bytes(module) self.src = self.path try: pkcs12_privatekey, pkcs12_certificate, pkcs12_other_certificates, pkcs12_friendly_name = self.parse() - except crypto.Error: + except OpenSSLObjectError: return False if (pkcs12_privatekey is not None) and (self.privatekey_path is not None): - expected_pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, - self.pkcs12.get_privatekey()) + expected_pkey = self._dump_privatekey(self.pkcs12) if pkcs12_privatekey != expected_pkey: return False elif bool(pkcs12_privatekey) != bool(self.privatekey_path): return False if (pkcs12_certificate is not None) and (self.certificate_path is not None): - - expected_cert = crypto.dump_certificate(crypto.FILETYPE_PEM, - self.pkcs12.get_certificate()) + expected_cert = self._dump_certificate(self.pkcs12) if pkcs12_certificate != expected_cert: return False elif bool(pkcs12_certificate) != bool(self.certificate_path): return False if (pkcs12_other_certificates is not None) and (self.other_certificates is not None): - expected_other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM, - other_cert) for other_cert in self.pkcs12.get_ca_certificates()] + expected_other_certs = self._dump_other_certificates(self.pkcs12) if set(pkcs12_other_certificates) != set(expected_other_certs): return False elif bool(pkcs12_other_certificates) != bool(self.other_certificates): @@ -352,15 +421,16 @@ class Pkcs(OpenSSLObject): if pkcs12_privatekey: # This check is required because pyOpenSSL will not return a friendly name # if the private key is not set in the file - if ((self.pkcs12.get_friendlyname() is not None) and (pkcs12_friendly_name is not None)): - if self.pkcs12.get_friendlyname() != pkcs12_friendly_name: + friendly_name = self._get_friendly_name(self.pkcs12) + if ((friendly_name is not None) and (pkcs12_friendly_name is not None)): + if friendly_name != pkcs12_friendly_name: return False - elif bool(self.pkcs12.get_friendlyname()) != bool(pkcs12_friendly_name): + elif bool(friendly_name) != bool(pkcs12_friendly_name): return False elif module.params['action'] == 'parse' and os.path.exists(self.src) and os.path.exists(self.path): try: pkey, cert, other_certs, friendly_name = self.parse() - except crypto.Error: + except OpenSSLObjectError: return False expected_content = to_bytes( ''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None]) @@ -390,27 +460,6 @@ class Pkcs(OpenSSLObject): return result - def generate(self, module): - """Generate PKCS#12 file archive.""" - self.pkcs12 = crypto.PKCS12() - - if self.other_certificates: - self.pkcs12.set_ca_certificates(self.other_certificates) - - if self.certificate_path: - self.pkcs12.set_certificate(load_certificate(self.certificate_path)) - - if self.friendly_name: - self.pkcs12.set_friendlyname(to_bytes(self.friendly_name)) - - if self.privatekey_path: - try: - self.pkcs12.set_privatekey(load_privatekey(self.privatekey_path, self.privatekey_passphrase)) - except OpenSSLBadPassphraseError as exc: - raise PkcsError(exc) - - return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size) - def remove(self, module): if self.backup: self.backup_file = module.backup_local(self.path) @@ -422,8 +471,51 @@ class Pkcs(OpenSSLObject): try: with open(self.src, 'rb') as pkcs12_fh: pkcs12_content = pkcs12_fh.read() - p12 = crypto.load_pkcs12(pkcs12_content, - self.passphrase) + return self.parse_bytes(pkcs12_content) + except IOError as exc: + raise PkcsError(exc) + + def generate(self): + pass + + def write(self, module, content, mode=None): + """Write the PKCS#12 file.""" + if self.backup: + self.backup_file = module.backup_local(self.path) + write_file(module, content, mode) + if self.return_content: + self.pkcs12_bytes = content + + +class PkcsPyOpenSSL(Pkcs): + def __init__(self, module): + super(PkcsPyOpenSSL, self).__init__(module, 'pyopenssl') + + def generate_bytes(self, module): + """Generate PKCS#12 file archive.""" + self.pkcs12 = crypto.PKCS12() + + if self.other_certificates: + self.pkcs12.set_ca_certificates(self.other_certificates) + + if self.certificate_path: + self.pkcs12.set_certificate(load_certificate(self.certificate_path, backend=self.backend)) + + if self.friendly_name: + self.pkcs12.set_friendlyname(to_bytes(self.friendly_name)) + + if self.privatekey_path: + try: + self.pkcs12.set_privatekey( + load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend)) + except OpenSSLBadPassphraseError as exc: + raise PkcsError(exc) + + return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size) + + def parse_bytes(self, pkcs12_content): + try: + p12 = crypto.load_pkcs12(pkcs12_content, self.passphrase) pkey = p12.get_privatekey() if pkey is not None: pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey) @@ -438,17 +530,143 @@ class Pkcs(OpenSSLObject): friendly_name = p12.get_friendlyname() return (pkey, crt, other_certs, friendly_name) - - except IOError as exc: + except crypto.Error as exc: raise PkcsError(exc) - def write(self, module, content, mode=None): - """Write the PKCS#12 file.""" - if self.backup: - self.backup_file = module.backup_local(self.path) - write_file(module, content, mode) - if self.return_content: - self.pkcs12_bytes = content + def _dump_privatekey(self, pkcs12): + pk = pkcs12.get_privatekey() + return crypto.dump_privatekey(crypto.FILETYPE_PEM, pk) if pk else None + + def _dump_certificate(self, pkcs12): + cert = pkcs12.get_certificate() + return crypto.dump_certificate(crypto.FILETYPE_PEM, cert) if cert else None + + def _dump_other_certificates(self, pkcs12): + return [ + crypto.dump_certificate(crypto.FILETYPE_PEM, other_cert) + for other_cert in pkcs12.get_ca_certificates() + ] + + def _get_friendly_name(self, pkcs12): + return pkcs12.get_friendlyname() + + +class PkcsCryptography(Pkcs): + def __init__(self, module): + super(PkcsCryptography, self).__init__(module, 'cryptography') + + def generate_bytes(self, module): + """Generate PKCS#12 file archive.""" + pkey = None + if self.privatekey_path: + try: + pkey = load_privatekey(self.privatekey_path, self.privatekey_passphrase, backend=self.backend) + except OpenSSLBadPassphraseError as exc: + raise PkcsError(exc) + + cert = None + if self.certificate_path: + cert = load_certificate(self.certificate_path, backend=self.backend) + + friendly_name = to_bytes(self.friendly_name) if self.friendly_name is not None else None + + # Store fake object which can be used to retrieve the components back + self.pkcs12 = (pkey, cert, self.other_certificates, friendly_name) + + return serialize_key_and_certificates( + friendly_name, + pkey, + cert, + self.other_certificates, + serialization.BestAvailableEncryption(to_bytes(self.passphrase)) + if self.passphrase else serialization.NoEncryption(), + ) + + def parse_bytes(self, pkcs12_content): + try: + private_key, certificate, additional_certificates, friendly_name = parse_pkcs12( + pkcs12_content, self.passphrase) + + pkey = None + if private_key is not None: + pkey = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + crt = None + if certificate is not None: + crt = certificate.public_bytes(serialization.Encoding.PEM) + + other_certs = [] + if additional_certificates is not None: + other_certs = [ + other_cert.public_bytes(serialization.Encoding.PEM) + for other_cert in additional_certificates + ] + + return (pkey, crt, other_certs, friendly_name) + except ValueError as exc: + raise PkcsError(exc) + + # The following methods will get self.pkcs12 passed, which is computed as: + # + # self.pkcs12 = (pkey, cert, self.other_certificates, self.friendly_name) + + def _dump_privatekey(self, pkcs12): + return pkcs12[0].private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) if pkcs12[0] else None + + def _dump_certificate(self, pkcs12): + return pkcs12[1].public_bytes(serialization.Encoding.PEM) if pkcs12[1] else None + + def _dump_other_certificates(self, pkcs12): + return [other_cert.public_bytes(serialization.Encoding.PEM) for other_cert in pkcs12[2]] + + def _get_friendly_name(self, pkcs12): + return pkcs12[3] + + +def select_backend(module, backend): + if backend == 'auto': + # Detection what is possible + can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) + can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) + + # If no restrictions are provided, first try cryptography, then pyOpenSSL + if module.params['iter_size'] is not None or module.params['maciter_size'] is not None: + # If iter_size or maciter_size is specified, use pyOpenSSL backend + backend = 'pyopenssl' + elif can_use_cryptography: + backend = 'cryptography' + elif can_use_pyopenssl: + backend = 'pyopenssl' + + # Success? + if backend == 'auto': + module.fail_json(msg=("Can't detect any of the required Python libraries " + "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( + MINIMAL_CRYPTOGRAPHY_VERSION, + MINIMAL_PYOPENSSL_VERSION)) + + if backend == 'pyopenssl': + if not PYOPENSSL_FOUND: + module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), + exception=PYOPENSSL_IMP_ERR) + # module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', + # version='x.0.0', collection_name='community.crypto') + return backend, PkcsPyOpenSSL(module) + elif backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + return backend, PkcsCryptography(module) + else: + raise ValueError('Unsupported value for backend: {0}'.format(backend)) def main(): @@ -459,8 +677,8 @@ def main(): certificate_path=dict(type='path'), force=dict(type='bool', default=False), friendly_name=dict(type='str', aliases=['name']), - iter_size=dict(type='int', default=2048), - maciter_size=dict(type='int', default=1), + iter_size=dict(type='int'), + maciter_size=dict(type='int'), passphrase=dict(type='str', no_log=True), path=dict(type='path', required=True), privatekey_passphrase=dict(type='str', no_log=True), @@ -469,6 +687,7 @@ def main(): src=dict(type='path'), backup=dict(type='bool', default=False), return_content=dict(type='bool', default=False), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), ) required_if = [ @@ -482,8 +701,7 @@ def main(): supports_check_mode=True, ) - if not pyopenssl_found: - module.fail_json(msg=missing_required_lib('pyOpenSSL'), exception=PYOPENSSL_IMP_ERR) + backend, pkcs12 = select_backend(module, module.params['select_crypto_backend']) base_dir = os.path.dirname(module.params['path']) or '.' if not os.path.isdir(base_dir): @@ -493,7 +711,6 @@ def main(): ) try: - pkcs12 = Pkcs(module) changed = False if module.params['state'] == 'present': @@ -506,7 +723,7 @@ def main(): if module.params['action'] == 'export': if not module.params['friendly_name']: module.fail_json(msg='Friendly_name is required') - pkcs12_content = pkcs12.generate(module) + pkcs12_content = pkcs12.generate_bytes(module) pkcs12.write(module, pkcs12_content, 0o600) changed = True else: diff --git a/tests/integration/targets/openssl_pkcs12/tasks/impl.yml b/tests/integration/targets/openssl_pkcs12/tasks/impl.yml index 9b36027f..bd878b8b 100644 --- a/tests/integration/targets/openssl_pkcs12/tasks/impl.yml +++ b/tests/integration/targets/openssl_pkcs12/tasks/impl.yml @@ -1,246 +1,237 @@ - block: - - name: Generate privatekey - openssl_privatekey: - path: '{{ output_dir }}/ansible_pkey.pem' - size: '{{ default_rsa_key_size_certifiates }}' - - name: Generate privatekey2 - openssl_privatekey: - path: '{{ output_dir }}/ansible_pkey2.pem' - size: '{{ default_rsa_key_size_certifiates }}' - - name: Generate privatekey3 - openssl_privatekey: - path: '{{ output_dir }}/ansible_pkey3.pem' - size: '{{ default_rsa_key_size_certifiates }}' - - name: Generate CSR - openssl_csr: - path: '{{ output_dir }}/ansible.csr' - privatekey_path: '{{ output_dir }}/ansible_pkey.pem' - commonName: www.ansible.com - - name: Generate CSR 2 - openssl_csr: - path: '{{ output_dir }}/ansible2.csr' - privatekey_path: '{{ output_dir }}/ansible_pkey2.pem' - commonName: www2.ansible.com - - name: Generate CSR 3 - openssl_csr: - path: '{{ output_dir }}/ansible3.csr' - privatekey_path: '{{ output_dir }}/ansible_pkey3.pem' - commonName: www3.ansible.com - - name: Generate certificate - x509_certificate: - path: '{{ output_dir }}/{{ item.name }}.crt' - privatekey_path: '{{ output_dir }}/{{ item.pkey }}' - csr_path: '{{ output_dir }}/{{ item.name }}.csr' - provider: selfsigned - loop: - - name: ansible - pkey: ansible_pkey.pem - - name: ansible2 - pkey: ansible_pkey2.pem - - name: ansible3 - pkey: ansible_pkey3.pem - - name: Generate concatenated PEM file - copy: - dest: '{{ output_dir }}/ansible23.crt' - content: | - {{ lookup("file", output_dir ~ "/ansible2.crt") }} - {{ lookup("file", output_dir ~ "/ansible3.crt") }} - - name: Generate PKCS#12 file + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible.p12' friendly_name: abracadabra - privatekey_path: '{{ output_dir }}/ansible_pkey.pem' - certificate_path: '{{ output_dir }}/ansible.crt' + privatekey_path: '{{ output_dir }}/ansible_pkey1.pem' + certificate_path: '{{ output_dir }}/ansible1.crt' state: present return_content: true register: p12_standard - - name: Generate PKCS#12 file again, idempotency + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file again, idempotency" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible.p12' friendly_name: abracadabra - privatekey_path: '{{ output_dir }}/ansible_pkey.pem' - certificate_path: '{{ output_dir }}/ansible.crt' + privatekey_path: '{{ output_dir }}/ansible_pkey1.pem' + certificate_path: '{{ output_dir }}/ansible1.crt' state: present return_content: true register: p12_standard_idempotency - - name: Read ansible.p12 + + - name: "({{ select_crypto_backend }}) Read ansible.p12" slurp: src: '{{ output_dir }}/ansible.p12' register: ansible_p12_content - - name: Validate PKCS#12 + + - name: "({{ select_crypto_backend }}) Validate PKCS#12" assert: that: - p12_standard.pkcs12 == ansible_p12_content.content - p12_standard_idempotency.pkcs12 == p12_standard.pkcs12 - - name: Generate PKCS#12 file (force) + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (force)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible.p12' friendly_name: abracadabra - privatekey_path: '{{ output_dir }}/ansible_pkey.pem' - certificate_path: '{{ output_dir }}/ansible.crt' + privatekey_path: '{{ output_dir }}/ansible_pkey1.pem' + certificate_path: '{{ output_dir }}/ansible1.crt' state: present force: true register: p12_force - - name: Generate PKCS#12 file (force + change mode) + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (force + change mode)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible.p12' friendly_name: abracadabra - privatekey_path: '{{ output_dir }}/ansible_pkey.pem' - certificate_path: '{{ output_dir }}/ansible.crt' + privatekey_path: '{{ output_dir }}/ansible_pkey1.pem' + certificate_path: '{{ output_dir }}/ansible1.crt' state: present force: true mode: '0644' register: p12_force_and_mode - - name: Dump PKCS#12 + + - name: "({{ select_crypto_backend }}) Dump PKCS#12" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' src: '{{ output_dir }}/ansible.p12' path: '{{ output_dir }}/ansible_parse.pem' action: parse state: present register: p12_dumped - - name: Dump PKCS#12 file again, idempotency + + - name: "({{ select_crypto_backend }}) Dump PKCS#12 file again, idempotency" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' src: '{{ output_dir }}/ansible.p12' path: '{{ output_dir }}/ansible_parse.pem' action: parse state: present register: p12_dumped_idempotency - - name: Dump PKCS#12, check mode + + - name: "({{ select_crypto_backend }}) Dump PKCS#12, check mode" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' src: '{{ output_dir }}/ansible.p12' path: '{{ output_dir }}/ansible_parse.pem' action: parse state: present check_mode: true register: p12_dumped_check_mode - - name: Generate PKCS#12 file with multiple certs + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file with multiple certs" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_multi_certs.p12' friendly_name: abracadabra - privatekey_path: '{{ output_dir }}/ansible_pkey.pem' - certificate_path: '{{ output_dir }}/ansible.crt' + privatekey_path: '{{ output_dir }}/ansible_pkey1.pem' + certificate_path: '{{ output_dir }}/ansible1.crt' other_certificates: - '{{ output_dir }}/ansible2.crt' - '{{ output_dir }}/ansible3.crt' state: present register: p12_multiple_certs - - name: Generate PKCS#12 file with multiple certs, again (idempotency) + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file with multiple certs, again (idempotency)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_multi_certs.p12' friendly_name: abracadabra - privatekey_path: '{{ output_dir }}/ansible_pkey.pem' - certificate_path: '{{ output_dir }}/ansible.crt' + privatekey_path: '{{ output_dir }}/ansible_pkey1.pem' + certificate_path: '{{ output_dir }}/ansible1.crt' other_certificates: - '{{ output_dir }}/ansible2.crt' - '{{ output_dir }}/ansible3.crt' state: present register: p12_multiple_certs_idempotency - - name: Dump PKCS#12 with multiple certs + + - name: "({{ select_crypto_backend }}) Dump PKCS#12 with multiple certs" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' src: '{{ output_dir }}/ansible_multi_certs.p12' path: '{{ output_dir }}/ansible_parse_multi_certs.pem' action: parse state: present - - name: Generate privatekey with password - openssl_privatekey: - path: '{{ output_dir }}/privatekeypw.pem' - passphrase: hunter2 - cipher: auto - size: '{{ default_rsa_key_size }}' - select_crypto_backend: cryptography - - name: Generate PKCS#12 file (password fail 1) + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (password fail 1)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_pw1.p12' friendly_name: abracadabra - privatekey_path: '{{ output_dir }}/ansible_pkey.pem' + privatekey_path: '{{ output_dir }}/ansible_pkey1.pem' privatekey_passphrase: hunter2 - certificate_path: '{{ output_dir }}/ansible.crt' + certificate_path: '{{ output_dir }}/ansible1.crt' state: present ignore_errors: true register: passphrase_error_1 - - name: Generate PKCS#12 file (password fail 2) + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (password fail 2)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_pw2.p12' friendly_name: abracadabra privatekey_path: '{{ output_dir }}/privatekeypw.pem' privatekey_passphrase: wrong_password - certificate_path: '{{ output_dir }}/ansible.crt' + certificate_path: '{{ output_dir }}/ansible1.crt' state: present ignore_errors: true register: passphrase_error_2 - - name: Generate PKCS#12 file (password fail 3) + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (password fail 3)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_pw3.p12' friendly_name: abracadabra privatekey_path: '{{ output_dir }}/privatekeypw.pem' - certificate_path: '{{ output_dir }}/ansible.crt' + certificate_path: '{{ output_dir }}/ansible1.crt' state: present ignore_errors: true register: passphrase_error_3 - - name: Generate PKCS#12 file, no privatekey + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file, no privatekey" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_no_pkey.p12' friendly_name: abracadabra - certificate_path: '{{ output_dir }}/ansible.crt' + certificate_path: '{{ output_dir }}/ansible1.crt' state: present register: p12_no_pkey - - name: Create broken PKCS#12 + + - name: "({{ select_crypto_backend }}) Create broken PKCS#12" copy: dest: '{{ output_dir }}/broken.p12' content: broken - - name: Regenerate broken PKCS#12 + + - name: "({{ select_crypto_backend }}) Regenerate broken PKCS#12" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/broken.p12' friendly_name: abracadabra - privatekey_path: '{{ output_dir }}/ansible_pkey.pem' - certificate_path: '{{ output_dir }}/ansible.crt' + privatekey_path: '{{ output_dir }}/ansible_pkey1.pem' + certificate_path: '{{ output_dir }}/ansible1.crt' state: present force: true mode: '0644' register: output_broken - - name: Generate PKCS#12 file + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_backup.p12' friendly_name: abracadabra - privatekey_path: '{{ output_dir }}/ansible_pkey.pem' - certificate_path: '{{ output_dir }}/ansible.crt' + privatekey_path: '{{ output_dir }}/ansible_pkey1.pem' + certificate_path: '{{ output_dir }}/ansible1.crt' state: present backup: true register: p12_backup_1 - - name: Generate PKCS#12 file (idempotent) + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (idempotent)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_backup.p12' friendly_name: abracadabra - privatekey_path: '{{ output_dir }}/ansible_pkey.pem' - certificate_path: '{{ output_dir }}/ansible.crt' + privatekey_path: '{{ output_dir }}/ansible_pkey1.pem' + certificate_path: '{{ output_dir }}/ansible1.crt' state: present backup: true register: p12_backup_2 - - name: Generate PKCS#12 file (change) + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (change)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_backup.p12' friendly_name: abra - privatekey_path: '{{ output_dir }}/ansible_pkey.pem' - certificate_path: '{{ output_dir }}/ansible.crt' + privatekey_path: '{{ output_dir }}/ansible_pkey1.pem' + certificate_path: '{{ output_dir }}/ansible1.crt' state: present force: true backup: true register: p12_backup_3 - - name: Generate PKCS#12 file (remove) + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (remove)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_backup.p12' state: absent backup: true return_content: true register: p12_backup_4 - - name: Generate PKCS#12 file (remove, idempotent) + + - name: "({{ select_crypto_backend }}) Generate PKCS#12 file (remove, idempotent)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_backup.p12' state: absent backup: true register: p12_backup_5 - - name: Generate 'empty' PKCS#12 file + + - name: "({{ select_crypto_backend }}) Generate 'empty' PKCS#12 file" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_empty.p12' friendly_name: abracadabra other_certificates: @@ -248,8 +239,11 @@ - '{{ output_dir }}/ansible3.crt' state: present register: p12_empty - - name: Generate 'empty' PKCS#12 file (idempotent) + + + - name: "({{ select_crypto_backend }}) Generate 'empty' PKCS#12 file (idempotent)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_empty.p12' friendly_name: abracadabra other_certificates: @@ -257,8 +251,10 @@ - '{{ output_dir }}/ansible2.crt' state: present register: p12_empty_idem - - name: Generate 'empty' PKCS#12 file (idempotent, concatenated other certificates) + + - name: "({{ select_crypto_backend }}) Generate 'empty' PKCS#12 file (idempotent, concatenated other certificates)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' path: '{{ output_dir }}/ansible_empty.p12' friendly_name: abracadabra other_certificates: @@ -266,14 +262,18 @@ other_certificates_parse_all: true state: present register: p12_empty_concat_idem - - name: Generate 'empty' PKCS#12 file (parse) + + - name: "({{ select_crypto_backend }}) Generate 'empty' PKCS#12 file (parse)" openssl_pkcs12: + select_crypto_backend: '{{ select_crypto_backend }}' src: '{{ output_dir }}/ansible_empty.p12' path: '{{ output_dir }}/ansible_empty.pem' action: parse + - import_tasks: ../tests/validate.yml + always: - - name: Delete PKCS#12 file + - name: "({{ select_crypto_backend }}) Delete PKCS#12 file" openssl_pkcs12: state: absent path: '{{ output_dir }}/{{ item }}.p12' diff --git a/tests/integration/targets/openssl_pkcs12/tasks/main.yml b/tests/integration/targets/openssl_pkcs12/tasks/main.yml index 8d57ff45..8ae05042 100644 --- a/tests/integration/targets/openssl_pkcs12/tasks/main.yml +++ b/tests/integration/targets/openssl_pkcs12/tasks/main.yml @@ -4,6 +4,69 @@ # and should not be used as examples of how to write Ansible roles # #################################################################### -- name: Run tests - include_tasks: impl.yml - when: pyopenssl_version.stdout is version('17.1.0', '>=') +- block: + - name: Generate private keys + openssl_privatekey: + path: '{{ output_dir }}/ansible_pkey{{ item }}.pem' + size: '{{ default_rsa_key_size_certifiates }}' + loop: "{{ range(1, 4) | list }}" + + - name: Generate privatekey with password + openssl_privatekey: + path: '{{ output_dir }}/privatekeypw.pem' + passphrase: hunter2 + cipher: auto + size: '{{ default_rsa_key_size }}' + + - name: Generate CSRs + openssl_csr: + path: '{{ output_dir }}/ansible{{ item }}.csr' + privatekey_path: '{{ output_dir }}/ansible_pkey{{ item }}.pem' + commonName: www{{ item }}.ansible.com + loop: "{{ range(1, 4) | list }}" + + - name: Generate certificate + x509_certificate: + path: '{{ output_dir }}/ansible{{ item }}.crt' + privatekey_path: '{{ output_dir }}/ansible_pkey{{ item }}.pem' + csr_path: '{{ output_dir }}/ansible{{ item }}.csr' + provider: selfsigned + loop: "{{ range(1, 4) | list }}" + + - name: Generate concatenated PEM file + copy: + dest: '{{ output_dir }}/ansible23.crt' + content: | + {{ lookup("file", output_dir ~ "/ansible2.crt") }} + {{ lookup("file", output_dir ~ "/ansible3.crt") }} + + - name: Generate PKCS#12 file with backend autodetection + openssl_pkcs12: + path: '{{ output_dir }}/ansible.p12' + friendly_name: abracadabra + privatekey_path: '{{ output_dir }}/ansible_pkey1.pem' + certificate_path: '{{ output_dir }}/ansible1.crt' + state: present + + - name: Delete result + file: + path: '{{ output_dir }}/ansible.p12' + state: absent + + - block: + - name: Running tests with pyOpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: pyopenssl + + when: pyopenssl_version.stdout is version('0.15', '>=') + + - block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('3.0', '>=') + + when: pyopenssl_version.stdout is version('0.15', '>=') or cryptography_version.stdout is version('3.0', '>=') diff --git a/tests/integration/targets/openssl_pkcs12/tests/validate.yml b/tests/integration/targets/openssl_pkcs12/tests/validate.yml index 86e4115d..f3c52482 100644 --- a/tests/integration/targets/openssl_pkcs12/tests/validate.yml +++ b/tests/integration/targets/openssl_pkcs12/tests/validate.yml @@ -1,17 +1,17 @@ --- -- name: 'Validate PKCS#12' +- name: '({{ select_crypto_backend }}) Validate PKCS#12' command: "{{ openssl_binary }} pkcs12 -info -in {{ output_dir }}/ansible.p12 -nodes -passin pass:''" register: p12 -- name: 'Validate PKCS#12 with no private key' +- name: '({{ select_crypto_backend }}) Validate PKCS#12 with no private key' command: "{{ openssl_binary }} pkcs12 -info -in {{ output_dir }}/ansible_no_pkey.p12 -nodes -passin pass:''" register: p12_validate_no_pkey -- name: 'Validate PKCS#12 with multiple certs' +- name: '({{ select_crypto_backend }}) Validate PKCS#12 with multiple certs' shell: "{{ openssl_binary }} pkcs12 -info -in {{ output_dir }}/ansible_multi_certs.p12 -nodes -passin pass:'' | grep subject" register: p12_validate_multi_certs -- name: 'Validate PKCS#12 (assert)' +- name: '({{ select_crypto_backend }}) Validate PKCS#12 (assert)' assert: that: - p12.stdout_lines[2].split(':')[-1].strip() == 'abracadabra' @@ -25,11 +25,11 @@ - not p12_multiple_certs_idempotency.changed - not p12_dumped_idempotency.changed - not p12_dumped_check_mode.changed - - "'www.' in p12_validate_multi_certs.stdout" + - "'www1.' in p12_validate_multi_certs.stdout" - "'www2.' in p12_validate_multi_certs.stdout" - "'www3.' in p12_validate_multi_certs.stdout" -- name: Check passphrase on private key +- name: '({{ select_crypto_backend }}) Check passphrase on private key' assert: that: - passphrase_error_1 is failed @@ -39,12 +39,12 @@ - passphrase_error_3 is failed - "'assphrase' in passphrase_error_3.msg or 'assword' in passphrase_error_3.msg or 'serializ' in passphrase_error_3.msg" -- name: "Verify that broken PKCS#12 will be regenerated" +- name: '({{ select_crypto_backend }}) Verify that broken PKCS#12 will be regenerated' assert: that: - output_broken is changed -- name: Check backup +- name: '({{ select_crypto_backend }}) Check backup' assert: that: - p12_backup_1 is changed @@ -59,10 +59,16 @@ - p12_backup_5.backup_file is undefined - p12_backup_4.pkcs12 is none -- name: Check 'empty' file +- name: '({{ select_crypto_backend }}) Load "empty" file' + set_fact: + empty_contents: "{{ lookup('file', output_dir ~ '/ansible_empty.pem') }}" + empty_expected_pyopenssl: "{{ lookup('file', output_dir ~ '/ansible3.crt') ~ '\n' ~ lookup('file', output_dir ~ '/ansible2.crt') }}" + empty_expected_cryptography: "{{ lookup('file', output_dir ~ '/ansible2.crt') ~ '\n' ~ lookup('file', output_dir ~ '/ansible3.crt') }}" + +- name: '({{ select_crypto_backend }}) Check "empty" file' assert: that: - p12_empty is changed - p12_empty_idem is not changed - p12_empty_concat_idem is not changed - - "lookup('file', output_dir ~ '/ansible_empty.pem') == lookup('file', output_dir ~ '/ansible3.crt') ~ '\n' ~ lookup('file', output_dir ~ '/ansible2.crt')" + - empty_contents == (empty_expected_pyopenssl if select_crypto_backend == 'pyopenssl' else empty_expected_cryptography)