From d2f60d2550da741c17827ec45050fe5ee27314d1 Mon Sep 17 00:00:00 2001 From: Markus Teufelberger Date: Tue, 28 Jul 2020 21:05:18 +0200 Subject: [PATCH] rewrite and split into 2 modules instead --- plugins/modules/openssl_signature.py | 216 ++++------- plugins/modules/openssl_signature_info.py | 353 ++++++++++++++++++ .../targets/openssl_signature/tasks/loop.yml | 11 +- .../targets/openssl_signature_info/aliases | 2 + .../openssl_signature_info/meta/main.yml | 2 + .../openssl_signature_info/tasks/loop.yml | 28 ++ .../openssl_signature_info/tasks/main.yml | 108 ++++++ 7 files changed, 577 insertions(+), 143 deletions(-) create mode 100644 plugins/modules/openssl_signature_info.py create mode 100644 tests/integration/targets/openssl_signature_info/aliases create mode 100644 tests/integration/targets/openssl_signature_info/meta/main.yml create mode 100644 tests/integration/targets/openssl_signature_info/tasks/loop.yml create mode 100644 tests/integration/targets/openssl_signature_info/tasks/main.yml diff --git a/plugins/modules/openssl_signature.py b/plugins/modules/openssl_signature.py index 7fdc85d94..ad1c4a742 100644 --- a/plugins/modules/openssl_signature.py +++ b/plugins/modules/openssl_signature.py @@ -11,13 +11,13 @@ DOCUMENTATION = r''' --- module: openssl_signature -short_description: Sign and verify data with openssl +short_description: Sign data with openssl description: - - This module allows one to sign and verify data via a certificate and a private key + - This module allows one to sign data using a private key. - The module can use the cryptography Python library, or the pyOpenSSL Python library. By default, it tries to detect which one is available. This can be overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL backend - was deprecated in Ansible 2.9 and will be removed in Ansible 2.13. + was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0. requirements: - Either cryptography >= 1.4 (some key types require newer versions) - Or pyOpenSSL >= 0.11 (Ed25519 and Ed448 keys are not supported with this backend) @@ -25,30 +25,27 @@ - Patrick Pichler (@aveexy) - Markus Teufelberger (@MarkusTeufelberger) options: - action: - description: Action to be executed - type: str - required: true - choices: [ sign, verify ] privatekey_path: description: - - The path to the private key to use when signing + - The path to the private key to use when signing. + - Either I(privatekey_path) or I(privatekey_content) must be specified, but not both. type: path + privatekey_content: + description: + - The content of the private key to use when signing the certificate signing request. + - Either I(privatekey_path) or I(privatekey_content) must be specified, but not both. + type: str privatekey_passphrase: description: - The passphrase for the private key. - This is required if the private key is password protected. type: str path: - description: File to sign/verify + description: + - The file to sign. + - This file will only be read and not modified. type: path required: true - certificate: - description: Certificate required for verify action - type: path - signature: - description: Base64 encoded signature required for verify action - type: str select_crypto_backend: description: - Determines which crypto backend to use. @@ -65,30 +62,34 @@ DSA and ECDSA keys: C(cryptography) >= 1.5 ed448 and ed25519 keys: C(cryptography) >= 2.6 seealso: - - module: x509_certificate + - module: openssl_signature_info - module: openssl_privatekey ''' EXAMPLES = r''' - name: Sign example file - openssl_signature: - action: sign + community.crypto.openssl_signature: privatekey_path: private.key path: /tmp/example_file register: sig - name: Verify signature of example file - openssl_signature: - action: verify - certificate: cert.pem + community.crypto.openssl_signature_info: + certificate_path: cert.pem path: /tmp/example_file signature: "{{ sig.signature }}" + register: verify + +- name: Make sure the signature is valid + assert: + that: + - verify.valid ''' RETURN = r''' signature: - description: Base64 encoded signature - returned: changed or success + description: Base64 encoded signature. + returned: success type: str ''' @@ -134,7 +135,6 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( OpenSSLObject, - load_certificate, load_privatekey, ) @@ -154,11 +154,11 @@ def __init__(self, module, backend): self.backend = backend - self.action = module.params['action'] - self.signature = module.params['signature'] - self.passphrase = module.params['privatekey_passphrase'] - self.private_key = module.params['privatekey_path'] - self.certificate = module.params['certificate'] + self.privatekey_path = module.params['privatekey_path'] + self.privatekey_content = module.params['privatekey_content'] + if self.privatekey_content is not None: + self.privatekey_content = self.privatekey_content.encode('utf-8') + self.privatekey_passphrase = module.params['privatekey_passphrase'] def generate(self): # Empty method because OpenSSLObject wants this @@ -176,33 +176,22 @@ def __init__(self, module, backend): super(SignaturePyOpenSSL, self).__init__(module, backend) def run(self): - try: - result = dict() + result = dict() + + try: with open(self.path, "rb") as f: _in = f.read() - if self.action == "verify": - _signature = base64.b64decode(self.signature) - certificate = load_certificate(self.certificate, backend=self.backend) - - try: - OpenSSL.crypto.verify(certificate, _signature, _in, 'sha256') - except Exception: - self.module.fail_json( - msg="Verification failed" - ) - - elif self.action == "sign": - private_key = load_privatekey( - self.private_key, - None if self.passphrase is None else to_bytes(self.passphrase), - backend=self.backend - ) - - out = OpenSSL.crypto.sign(private_key, _in, "sha256") - result['signature'] = base64.b64encode(out) + private_key = load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend=self.backend, + ) + signature = OpenSSL.crypto.sign(private_key, _in, "sha256") + result['signature'] = base64.b64encode(signature) return result except Exception as e: raise OpenSSLObjectError(e) @@ -224,88 +213,41 @@ def run(self): with open(self.path, "rb") as f: _in = f.read() - if self.action == "verify": - _signature = base64.b64decode(self.signature) - public_key = load_certificate(self.certificate, backend=self.backend).public_key() - verified = False - - if CRYPTOGRAPHY_HAS_DSA_SIGN: - try: - if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey): - public_key.verify(_signature, _in, _hash) - verified = True - except cryptography.exceptions.InvalidSignature: - self.module.fail_json( - msg="DSA signature verification failed" - ) - if CRYPTOGRAPHY_HAS_EC_SIGN: - try: - if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey): - public_key.verify(_signature, _in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash)) - verified = True - except cryptography.exceptions.InvalidSignature: - self.module.fail_json( - msg="ECDSA signature verification failed" - ) - if CRYPTOGRAPHY_HAS_ED25519_SIGN: - try: - if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey): - public_key.verify(_signature, _in) - verified = True - except cryptography.exceptions.InvalidSignature: - self.module.fail_json( - msg="Ed25519 signature verification failed" - ) - if CRYPTOGRAPHY_HAS_ED448_SIGN: - try: - if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey): - public_key.verify(_signature, _in) - verified = True - except cryptography.exceptions.InvalidSignature: - self.module.fail_json( - msg="Ed448 signature verification failed" - ) - if CRYPTOGRAPHY_HAS_RSA_SIGN: - try: - if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey): - public_key.verify(_signature, _in, _padding, _hash) - verified = True - except cryptography.exceptions.InvalidSignature: - self.module.fail_json( - msg="RSA signature verification failed" - ) - if not verified: - self.module.fail_json( - msg="Unsupported key type" - ) - - elif self.action == "sign": - private_key = load_privatekey( - self.private_key, - None if self.passphrase is None else to_bytes(self.passphrase), - backend=self.backend - ) + private_key = load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend=self.backend, + ) - if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): - out = private_key.sign(_in, _padding, _hash) + signature = None - elif isinstance(private_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): - out = private_key.sign(_in, _hash) + if CRYPTOGRAPHY_HAS_DSA_SIGN: + if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): + signature = private_key.sign(_in, _hash) - elif isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): - out = private_key.sign(_in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash)) + if CRYPTOGRAPHY_HAS_EC_SIGN: + if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): + signature = private_key.sign(_in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash)) - elif (isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey) or - isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey)): - out = private_key.sign(_in) + if CRYPTOGRAPHY_HAS_ED25519_SIGN: + if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): + signature = private_key.sign(_in) - else: - self.module.fail_json( - msg="Unsupported key type" - ) + if CRYPTOGRAPHY_HAS_ED448_SIGN: + if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): + signature = private_key.sign(_in) - result['signature'] = base64.b64encode(out) + if CRYPTOGRAPHY_HAS_RSA_SIGN: + if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): + signature = private_key.sign(_in, _padding, _hash) + + if signature is None: + self.module.fail_json( + msg="Unsupported key type. Your cryptography version is {0}".format(CRYPTOGRAPHY_VERSION) + ) + result['signature'] = base64.b64encode(signature) return result except Exception as e: @@ -315,23 +257,19 @@ def run(self): def main(): module = AnsibleModule( argument_spec=dict( - action=dict(type='str', choices=['sign', 'verify'], required=True), privatekey_path=dict(type='path'), - certificate=dict(type='path'), + privatekey_content=dict(type='str'), privatekey_passphrase=dict(type='str', no_log=True), path=dict(type='path', required=True), - signature=dict(type='str'), select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'), ), + mutually_exclusive=( + ['privatekey_path', 'privatekey_content'], + ), + required_one_of=( + ['privatekey_path', 'privatekey_content'], + ), supports_check_mode=True, - required_if=[ - ['action', 'sign', ['privatekey_path']], - ['action', 'verify', ['certificate']], - ['action', 'verify', ['signature']], - ], - mutually_exclusive=[ - ['privatekey_path', 'certificate'] - ], ) if not os.path.isfile(module.params['path']): @@ -364,7 +302,7 @@ def main(): 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='2.13') + version='2.0.0', collection_name='community.crypto') _sign = SignaturePyOpenSSL(module, backend) elif backend == 'cryptography': if not CRYPTOGRAPHY_FOUND: diff --git a/plugins/modules/openssl_signature_info.py b/plugins/modules/openssl_signature_info.py new file mode 100644 index 000000000..ddaf24222 --- /dev/null +++ b/plugins/modules/openssl_signature_info.py @@ -0,0 +1,353 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Patrick Pichler +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: openssl_signature_info +short_description: Verify signatures with openssl +description: + - This module allows one to verify a signature for a file via a certificate. + - The module can use the cryptography Python library, or the pyOpenSSL Python + library. By default, it tries to detect which one is available. This can be + overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL backend + was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0. +requirements: + - Either cryptography >= 1.4 (some key types require newer versions) + - Or pyOpenSSL >= 0.11 (Ed25519 and Ed448 keys are not supported with this backend) +author: + - Patrick Pichler (@aveexy) + - Markus Teufelberger (@MarkusTeufelberger) +options: + path: + description: + - The signed file to verify. + - This file will only be read and not modified. + type: path + required: true + certificate_path: + description: + - The path to the certificate used to verify the signature. + - Either I(certificate_path) or I(certificate_content) must be specified, but not both. + type: path + certificate_content: + description: + - The content of the certificate used to verify the signature. + - Either I(certificate_path) or I(certificate_content) must be specified, but not both. + type: str + signature: + description: Base64 encoded signature. + type: str + required: true + 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 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. + type: str + default: auto + choices: [ auto, cryptography, pyopenssl ] +notes: + - | + When using the C(cryptography) backend, the following key types require at least the following C(cryptography) version: + RSA keys: C(cryptography) >= 1.4 + DSA and ECDSA keys: C(cryptography) >= 1.5 + ed448 and ed25519 keys: C(cryptography) >= 2.6 +seealso: + - module: openssl_signature + - module: x509_certificate +''' + +EXAMPLES = r''' +- name: Sign example file + community.crypto.openssl_signature: + privatekey_path: private.key + path: /tmp/example_file + register: sig + +- name: Verify signature of example file + community.crypto.openssl_signature_info: + certificate_path: cert.pem + path: /tmp/example_file + signature: "{{ sig.signature }}" + register: verify + +- name: Make sure the signature is valid + assert: + that: + - verify.valid +''' + +RETURN = r''' +valid: + description: True means the signature was valid for the given file, False means it wasn't. + returned: success + type: bool +''' + +import os +import traceback +from distutils.version import LooseVersion +import base64 + +MINIMAL_PYOPENSSL_VERSION = '0.11' +MINIMAL_CRYPTOGRAPHY_VERSION = '1.4' + +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 +else: + PYOPENSSL_FOUND = True + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + import cryptography.hazmat.primitives.asymmetric.padding + import cryptography.hazmat.primitives.hashes + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + CRYPTOGRAPHY_HAS_DSA_SIGN, + CRYPTOGRAPHY_HAS_EC_SIGN, + CRYPTOGRAPHY_HAS_ED25519_SIGN, + CRYPTOGRAPHY_HAS_ED448_SIGN, + CRYPTOGRAPHY_HAS_RSA_SIGN, + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_certificate, +) + +from ansible.module_utils._text import to_native, to_bytes +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + + +class SignatureInfoBase(OpenSSLObject): + + def __init__(self, module, backend): + super(SignatureInfoBase, self).__init__( + path=module.params['path'], + state='present', + force=False, + check_mode=module.check_mode + ) + + self.backend = backend + + self.signature = module.params['signature'] + self.certificate_path = module.params['certificate_path'] + self.certificate_content = module.params['certificate_content'] + if self.certificate_content is not None: + self.certificate_content = self.certificate_content.encode('utf-8') + + def generate(self): + # Empty method because OpenSSLObject wants this + pass + + def dump(self): + # Empty method because OpenSSLObject wants this + pass + + +# Implementation with using pyOpenSSL +class SignatureInfoPyOpenSSL(SignatureInfoBase): + + def __init__(self, module, backend): + super(SignatureInfoPyOpenSSL, self).__init__(module, backend) + + def run(self): + + result = dict() + + try: + with open(self.path, "rb") as f: + _in = f.read() + + _signature = base64.b64decode(self.signature) + certificate = load_certificate( + path=self.certificate_path, + content=self.certificate_content, + backend=self.backend, + ) + + try: + OpenSSL.crypto.verify(certificate, _signature, _in, 'sha256') + result['valid'] = True + except Exception: + result['valid'] = False + return result + except Exception as e: + raise OpenSSLObjectError(e) + + +# Implementation with using cryptography +class SignatureInfoCryptography(SignatureInfoBase): + + def __init__(self, module, backend): + super(SignatureInfoCryptography, self).__init__(module, backend) + + def run(self): + _padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15() + _hash = cryptography.hazmat.primitives.hashes.SHA256() + + result = dict() + + try: + with open(self.path, "rb") as f: + _in = f.read() + + _signature = base64.b64decode(self.signature) + certificate = load_certificate( + path=self.certificate_path, + content=self.certificate_content, + backend=self.backend, + ) + public_key = certificate.public_key() + verified = False + valid = False + + if CRYPTOGRAPHY_HAS_DSA_SIGN: + try: + if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey): + public_key.verify(_signature, _in, _hash) + verified = True + valid = True + except cryptography.exceptions.InvalidSignature: + verified = True + valid = False + + if CRYPTOGRAPHY_HAS_EC_SIGN: + try: + if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey): + public_key.verify(_signature, _in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash)) + verified = True + valid = True + except cryptography.exceptions.InvalidSignature: + verified = True + valid = False + + if CRYPTOGRAPHY_HAS_ED25519_SIGN: + try: + if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey): + public_key.verify(_signature, _in) + verified = True + valid = True + except cryptography.exceptions.InvalidSignature: + verified = True + valid = False + + if CRYPTOGRAPHY_HAS_ED448_SIGN: + try: + if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey): + public_key.verify(_signature, _in) + verified = True + valid = True + except cryptography.exceptions.InvalidSignature: + verified = True + valid = False + + if CRYPTOGRAPHY_HAS_RSA_SIGN: + try: + if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey): + public_key.verify(_signature, _in, _padding, _hash) + verified = True + valid = True + except cryptography.exceptions.InvalidSignature: + verified = True + valid = False + + if not verified: + self.module.fail_json( + msg="Unsupported key type. Your cryptography version is {0}".format(CRYPTOGRAPHY_VERSION) + ) + result['valid'] = valid + return result + + except Exception as e: + raise OpenSSLObjectError(e) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + certificate_path=dict(type='path'), + certificate_content=dict(type='str'), + path=dict(type='path', required=True), + signature=dict(type='str', required=True), + select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'), + ), + mutually_exclusive=( + ['certificate_path', 'certificate_content'], + ), + required_one_of=( + ['certificate_path', 'certificate_content'], + ), + supports_check_mode=True, + ) + + if not os.path.isfile(module.params['path']): + module.fail_json( + name=module.params['path'], + msg='The file {0} does not exist'.format(module.params['path']) + ) + + backend = module.params['select_crypto_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) + + # Decision + if 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)) + try: + 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='2.0.0', collection_name='community.crypto') + _sign = SignatureInfoPyOpenSSL(module, backend) + elif backend == 'cryptography': + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + _sign = SignatureInfoCryptography(module, backend) + + result = _sign.run() + + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/openssl_signature/tasks/loop.yml b/tests/integration/targets/openssl_signature/tasks/loop.yml index b5e6c80bb..c33a60911 100644 --- a/tests/integration/targets/openssl_signature/tasks/loop.yml +++ b/tests/integration/targets/openssl_signature/tasks/loop.yml @@ -2,7 +2,6 @@ # This file is intended to be included in a loop statement - name: Sign statement with {{ item.type }} key - {{ item.passwd }} using {{ item.backend }} openssl_signature: - action: sign privatekey_path: '{{ output_dir }}/{{item.backend}}_privatekey_{{ item.type }}_{{ item.passwd }}.pem' privatekey_passphrase: '{{ item.privatekey_passphrase | default(omit) }}' path: '{{ output_dir }}/statement.txt' @@ -13,13 +12,17 @@ var: sign_result - name: Verify {{ item.type }} signature - {{ item.passwd }} using {{ item.backend }} - openssl_signature: - action: verify - certificate: '{{ output_dir }}/{{item.backend}}_certificate_{{ item.type }}_{{ item.passwd }}.pem' + openssl_signature_info: + certificate_path: '{{ output_dir }}/{{item.backend}}_certificate_{{ item.type }}_{{ item.passwd }}.pem' path: '{{ output_dir }}/statement.txt' signature: '{{ sign_result.signature }}' select_crypto_backend: '{{ item.backend }}' register: verify_result +- name: Make sure the signature is valid + assert: + that: + - verify_result.valid + - debug: var: verify_result diff --git a/tests/integration/targets/openssl_signature_info/aliases b/tests/integration/targets/openssl_signature_info/aliases new file mode 100644 index 000000000..6eae8bd8d --- /dev/null +++ b/tests/integration/targets/openssl_signature_info/aliases @@ -0,0 +1,2 @@ +shippable/posix/group1 +destructive diff --git a/tests/integration/targets/openssl_signature_info/meta/main.yml b/tests/integration/targets/openssl_signature_info/meta/main.yml new file mode 100644 index 000000000..800aff642 --- /dev/null +++ b/tests/integration/targets/openssl_signature_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_openssl diff --git a/tests/integration/targets/openssl_signature_info/tasks/loop.yml b/tests/integration/targets/openssl_signature_info/tasks/loop.yml new file mode 100644 index 000000000..c33a60911 --- /dev/null +++ b/tests/integration/targets/openssl_signature_info/tasks/loop.yml @@ -0,0 +1,28 @@ +--- +# This file is intended to be included in a loop statement +- name: Sign statement with {{ item.type }} key - {{ item.passwd }} using {{ item.backend }} + openssl_signature: + privatekey_path: '{{ output_dir }}/{{item.backend}}_privatekey_{{ item.type }}_{{ item.passwd }}.pem' + privatekey_passphrase: '{{ item.privatekey_passphrase | default(omit) }}' + path: '{{ output_dir }}/statement.txt' + select_crypto_backend: '{{ item.backend }}' + register: sign_result + +- debug: + var: sign_result + +- name: Verify {{ item.type }} signature - {{ item.passwd }} using {{ item.backend }} + openssl_signature_info: + certificate_path: '{{ output_dir }}/{{item.backend}}_certificate_{{ item.type }}_{{ item.passwd }}.pem' + path: '{{ output_dir }}/statement.txt' + signature: '{{ sign_result.signature }}' + select_crypto_backend: '{{ item.backend }}' + register: verify_result + +- name: Make sure the signature is valid + assert: + that: + - verify_result.valid + +- debug: + var: verify_result diff --git a/tests/integration/targets/openssl_signature_info/tasks/main.yml b/tests/integration/targets/openssl_signature_info/tasks/main.yml new file mode 100644 index 000000000..b39ae986b --- /dev/null +++ b/tests/integration/targets/openssl_signature_info/tasks/main.yml @@ -0,0 +1,108 @@ +--- +# Test matrix: +# * pyopenssl or cryptography +# * DSA or ECC or ... +# * password protected private key or not + +- name: Set up test combinations + set_fact: + all_tests: [] + backends: [] + key_types: [] + key_password: + - passwd: nopasswd + - passwd: passwd + privatekey_passphrase: hunter2 + privatekey_cipher: auto + +- name: Add cryptography backend + set_fact: + backends: "{{ backends + [ { 'backend': 'cryptography' } ] }}" + when: cryptography_version.stdout is version('1.4', '>=') + +- name: Add pyopenssl backend + set_fact: + backends: "{{ backends + [ { 'backend': 'pyopenssl' } ] }}" + when: pyopenssl_version.stdout is version('0.11', '>=') + +- name: Add RSA tests + set_fact: + key_types: "{{ key_types + [ { 'type': 'RSA' } ] }}" + when: cryptography_version.stdout is version('1.4', '>=') + +- name: Add DSA + ECDSA tests + set_fact: + key_types: "{{ key_types + [ { 'type': 'DSA', 'size': 2048 }, { 'type': 'ECC', 'curve': 'secp256r1' } ] }}" + when: + - cryptography_version.stdout is version('1.5', '>=') + # FreeBSD 11 fails on secp256r1 keys + - not ansible_os_family == 'FreeBSD' + +- name: Add Ed25519 + Ed448 tests + set_fact: + key_types: "{{ key_types + [ { 'type': 'Ed25519' }, { 'type': 'Ed448' } ] }}" + when: + # The module under tests works with >= 2.6, but we also need to be able to create a certificate which requires 2.8 + - cryptography_version.stdout is version('2.8', '>=') + # FreeBSD doesn't have support for Ed448/25519 + - not ansible_os_family == 'FreeBSD' + +- name: Create all test combinations + set_fact: + # Explanation: see https://serverfault.com/a/1004124 + all_tests: >- + [ + {% for b in backends %} + {% for kt in key_types %} + {% for kp in key_password %} + {# Exclude Ed25519 and Ed448 tests on pyopenssl #} + {% if not (b.backend == 'pyopenssl' and (kt.type == 'Ed25519' or kt.type == 'Ed448')) %} + {{ b | combine (kt) | combine(kp) }}, + {% endif %} + {% endfor %} + {% endfor %} + {% endfor %} + ] + +- name: Generate private keys + openssl_privatekey: + path: '{{ output_dir }}/{{item.backend}}_privatekey_{{ item.type }}_{{ item.passwd }}.pem' + type: '{{ item.type }}' + curve: '{{ item.curve | default(omit) }}' + size: '{{ item.size | default(omit) }}' + passphrase: '{{ item.privatekey_passphrase | default(omit) }}' + cipher: '{{ item.privatekey_cipher | default(omit) }}' + select_crypto_backend: cryptography + loop: '{{ all_tests }}' + +- name: Generate public keys + openssl_publickey: + path: '{{ output_dir }}/{{item.backend}}_publickey_{{ item.type }}_{{ item.passwd }}.pem' + privatekey_path: '{{ output_dir }}/{{item.backend}}_privatekey_{{ item.type }}_{{ item.passwd }}.pem' + privatekey_passphrase: '{{ item.privatekey_passphrase | default(omit) }}' + loop: '{{ all_tests }}' + +- name: Generate CSRs + openssl_csr: + path: '{{ output_dir }}/{{item.backend}}_{{ item.type }}_{{ item.passwd }}.csr' + privatekey_path: '{{ output_dir }}/{{item.backend}}_privatekey_{{ item.type }}_{{ item.passwd }}.pem' + privatekey_passphrase: '{{ item.privatekey_passphrase | default(omit) }}' + loop: '{{ all_tests }}' + +- name: Generate selfsigned certificates + x509_certificate: + provider: selfsigned + path: '{{ output_dir }}/{{item.backend}}_certificate_{{ item.type }}_{{ item.passwd }}.pem' + privatekey_path: '{{ output_dir }}/{{item.backend}}_privatekey_{{ item.type }}_{{ item.passwd }}.pem' + privatekey_passphrase: '{{ item.privatekey_passphrase | default(omit) }}' + csr_path: '{{ output_dir }}/{{item.backend}}_{{ item.type }}_{{ item.passwd }}.csr' + loop: '{{ all_tests }}' + +- name: Create statement to be signed + copy: + content: "Erst wenn der Subwoofer die Katze inhaliert, fickt der Bass richtig übel. -- W.A. Mozart" + dest: '{{ output_dir }}/statement.txt' + +- name: Loop over all variants + include_tasks: loop.yml + loop: '{{ all_tests }}'