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 <ajpantuso@gmail.com>

* 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 <ajpantuso@gmail.com>

* Update plugins/modules/openssl_pkcs12.py

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>

* Streamline cert creation.

* Convert range to list.

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>
pull/239/head
Felix Fontein 2021-05-20 19:36:07 +02:00 committed by GitHub
parent 0a0d0f2bdf
commit e9bc7c7163
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 504 additions and 187 deletions

View File

@ -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)."

View File

@ -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

View File

@ -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:

View File

@ -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'

View File

@ -4,6 +4,69 @@
# and should not be used as examples of how to write Ansible roles #
####################################################################
- name: Run tests
- 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
when: pyopenssl_version.stdout is version('17.1.0', '>=')
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', '>=')

View File

@ -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)