Update openssl_signature module (#63)
* Use module_utils from collection, clean up code a bit * add DSA keys, because why not... * sign/verify was added in pyOpenSSL 0.11 apparently * Add signing capability detection to module_utils.crypto.basic * Rework feature detection of signature types. * Rename parameters to match other modules * Add initial version of integration tests * fix whitespace in tests * More whitespace fixes * small fixes for issues in testing * Organize integration tests as test matrix * another indentation fix to make pep8 happy * use openssl pkeyutl when possible, otherwise fall back to openssl dgst * More linter fixes * openssl pkeyutl -help can apparently return 1 * ignore errors on openssl call and another try at formatting * Remove the OpenSSL calls in tests * Add collection name to deprecation notice and deprecate at version 2.0.0 * Exclude Ed448/25519 tests on pyopenssl * revert the collection name in the deprecation notice (breaks 2.9) * limit test platforms even more * disable FreeBSD DSA and ECC tests * Add module name to README * rewrite and split into 2 modules instead * add module to README and fix whitespace issue * remove duplicated tests * address review remarks * resolve another commentpull/106/head
parent
128991c3dc
commit
346c2f55ff
|
@ -24,6 +24,8 @@ Most modules require a recent enough version of [the Python cryptography library
|
||||||
- openssl_privatekey_info
|
- openssl_privatekey_info
|
||||||
- openssl_privatekey
|
- openssl_privatekey
|
||||||
- openssl_publickey
|
- openssl_publickey
|
||||||
|
- openssl_signature_info
|
||||||
|
- openssl_signature
|
||||||
- x509_certificate_info
|
- x509_certificate_info
|
||||||
- x509_certificate
|
- x509_certificate
|
||||||
- x509_crl_info
|
- x509_crl_info
|
||||||
|
|
|
@ -65,11 +65,78 @@ try:
|
||||||
x509.RFC822Name.__hash__ = simple_hash
|
x509.RFC822Name.__hash__ = simple_hash
|
||||||
x509.UniformResourceIdentifier.__hash__ = simple_hash
|
x509.UniformResourceIdentifier.__hash__ = simple_hash
|
||||||
|
|
||||||
# Test whether we have support for X25519, X448, Ed25519 and/or Ed448
|
# Test whether we have support for DSA, EC, Ed25519, Ed448, RSA, X25519 and/or X448
|
||||||
try:
|
try:
|
||||||
|
# added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/dsa/
|
||||||
|
import cryptography.hazmat.primitives.asymmetric.dsa
|
||||||
|
CRYPTOGRAPHY_HAS_DSA = True
|
||||||
|
try:
|
||||||
|
# added later in 1.5
|
||||||
|
cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey.sign
|
||||||
|
CRYPTOGRAPHY_HAS_DSA_SIGN = True
|
||||||
|
except AttributeError:
|
||||||
|
CRYPTOGRAPHY_HAS_DSA_SIGN = False
|
||||||
|
except ImportError:
|
||||||
|
CRYPTOGRAPHY_HAS_DSA = False
|
||||||
|
CRYPTOGRAPHY_HAS_DSA_SIGN = False
|
||||||
|
try:
|
||||||
|
# added in 2.6 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed25519/
|
||||||
|
import cryptography.hazmat.primitives.asymmetric.ed25519
|
||||||
|
CRYPTOGRAPHY_HAS_ED25519 = True
|
||||||
|
try:
|
||||||
|
# added with the primitive in 2.6
|
||||||
|
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.sign
|
||||||
|
CRYPTOGRAPHY_HAS_ED25519_SIGN = True
|
||||||
|
except AttributeError:
|
||||||
|
CRYPTOGRAPHY_HAS_ED25519_SIGN = False
|
||||||
|
except ImportError:
|
||||||
|
CRYPTOGRAPHY_HAS_ED25519 = False
|
||||||
|
CRYPTOGRAPHY_HAS_ED25519_SIGN = False
|
||||||
|
try:
|
||||||
|
# added in 2.6 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed448/
|
||||||
|
import cryptography.hazmat.primitives.asymmetric.ed448
|
||||||
|
CRYPTOGRAPHY_HAS_ED448 = True
|
||||||
|
try:
|
||||||
|
# added with the primitive in 2.6
|
||||||
|
cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.sign
|
||||||
|
CRYPTOGRAPHY_HAS_ED448_SIGN = True
|
||||||
|
except AttributeError:
|
||||||
|
CRYPTOGRAPHY_HAS_ED448_SIGN = False
|
||||||
|
except ImportError:
|
||||||
|
CRYPTOGRAPHY_HAS_ED448 = False
|
||||||
|
CRYPTOGRAPHY_HAS_ED448_SIGN = False
|
||||||
|
try:
|
||||||
|
# added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
|
||||||
|
import cryptography.hazmat.primitives.asymmetric.ec
|
||||||
|
CRYPTOGRAPHY_HAS_EC = True
|
||||||
|
try:
|
||||||
|
# added later in 1.5
|
||||||
|
cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey.sign
|
||||||
|
CRYPTOGRAPHY_HAS_EC_SIGN = True
|
||||||
|
except AttributeError:
|
||||||
|
CRYPTOGRAPHY_HAS_EC_SIGN = False
|
||||||
|
except ImportError:
|
||||||
|
CRYPTOGRAPHY_HAS_EC = False
|
||||||
|
CRYPTOGRAPHY_HAS_EC_SIGN = False
|
||||||
|
try:
|
||||||
|
# added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/
|
||||||
|
import cryptography.hazmat.primitives.asymmetric.rsa
|
||||||
|
CRYPTOGRAPHY_HAS_RSA = True
|
||||||
|
try:
|
||||||
|
# added later in 1.4
|
||||||
|
cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey.sign
|
||||||
|
CRYPTOGRAPHY_HAS_RSA_SIGN = True
|
||||||
|
except AttributeError:
|
||||||
|
CRYPTOGRAPHY_HAS_RSA_SIGN = False
|
||||||
|
except ImportError:
|
||||||
|
CRYPTOGRAPHY_HAS_RSA = False
|
||||||
|
CRYPTOGRAPHY_HAS_RSA_SIGN = False
|
||||||
|
try:
|
||||||
|
# added in 2.0 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x25519/
|
||||||
import cryptography.hazmat.primitives.asymmetric.x25519
|
import cryptography.hazmat.primitives.asymmetric.x25519
|
||||||
CRYPTOGRAPHY_HAS_X25519 = True
|
CRYPTOGRAPHY_HAS_X25519 = True
|
||||||
try:
|
try:
|
||||||
|
# added later in 2.5
|
||||||
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.private_bytes
|
cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.private_bytes
|
||||||
CRYPTOGRAPHY_HAS_X25519_FULL = True
|
CRYPTOGRAPHY_HAS_X25519_FULL = True
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -78,29 +145,28 @@ try:
|
||||||
CRYPTOGRAPHY_HAS_X25519 = False
|
CRYPTOGRAPHY_HAS_X25519 = False
|
||||||
CRYPTOGRAPHY_HAS_X25519_FULL = False
|
CRYPTOGRAPHY_HAS_X25519_FULL = False
|
||||||
try:
|
try:
|
||||||
|
# added in 2.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x448/
|
||||||
import cryptography.hazmat.primitives.asymmetric.x448
|
import cryptography.hazmat.primitives.asymmetric.x448
|
||||||
CRYPTOGRAPHY_HAS_X448 = True
|
CRYPTOGRAPHY_HAS_X448 = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
CRYPTOGRAPHY_HAS_X448 = False
|
CRYPTOGRAPHY_HAS_X448 = False
|
||||||
try:
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.ed25519
|
|
||||||
CRYPTOGRAPHY_HAS_ED25519 = True
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_HAS_ED25519 = False
|
|
||||||
try:
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.ed448
|
|
||||||
CRYPTOGRAPHY_HAS_ED448 = True
|
|
||||||
except ImportError:
|
|
||||||
CRYPTOGRAPHY_HAS_ED448 = False
|
|
||||||
|
|
||||||
HAS_CRYPTOGRAPHY = True
|
HAS_CRYPTOGRAPHY = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Error handled in the calling module.
|
# Error handled in the calling module.
|
||||||
|
CRYPTOGRAPHY_HAS_EC = False
|
||||||
|
CRYPTOGRAPHY_HAS_EC_SIGN = False
|
||||||
|
CRYPTOGRAPHY_HAS_ED25519 = False
|
||||||
|
CRYPTOGRAPHY_HAS_ED25519_SIGN = False
|
||||||
|
CRYPTOGRAPHY_HAS_ED448 = False
|
||||||
|
CRYPTOGRAPHY_HAS_ED448_SIGN = False
|
||||||
|
CRYPTOGRAPHY_HAS_DSA = False
|
||||||
|
CRYPTOGRAPHY_HAS_DSA_SIGN = False
|
||||||
|
CRYPTOGRAPHY_HAS_RSA = False
|
||||||
|
CRYPTOGRAPHY_HAS_RSA_SIGN = False
|
||||||
CRYPTOGRAPHY_HAS_X25519 = False
|
CRYPTOGRAPHY_HAS_X25519 = False
|
||||||
CRYPTOGRAPHY_HAS_X25519_FULL = False
|
CRYPTOGRAPHY_HAS_X25519_FULL = False
|
||||||
CRYPTOGRAPHY_HAS_X448 = False
|
CRYPTOGRAPHY_HAS_X448 = False
|
||||||
CRYPTOGRAPHY_HAS_ED25519 = False
|
|
||||||
CRYPTOGRAPHY_HAS_ED448 = False
|
|
||||||
HAS_CRYPTOGRAPHY = False
|
HAS_CRYPTOGRAPHY = False
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,54 +1,52 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright: (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
|
|
||||||
# Copyright: (c) 2019, Patrick Pichler <ppichler+ansible@mgit.at>
|
# Copyright: (c) 2019, Patrick Pichler <ppichler+ansible@mgit.at>
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# 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
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
ANSIBLE_METADATA = {'metadata_version': '1.1'}
|
|
||||||
|
|
||||||
DOCUMENTATION = r'''
|
DOCUMENTATION = r'''
|
||||||
---
|
---
|
||||||
module: openssl_signature
|
module: openssl_signature
|
||||||
short_description: Sign and verify data with openssl
|
version_added: 1.1.0
|
||||||
description: This module allows one to sign and verify data via certificate and private key
|
short_description: Sign data with openssl
|
||||||
|
description:
|
||||||
|
- 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 community.crypto 2.0.0.
|
||||||
requirements:
|
requirements:
|
||||||
- Either cryptography >= 1.2.3 (older versions might work as well)
|
- Either cryptography >= 1.4 (some key types require newer versions)
|
||||||
- Or pyOpenSSL
|
- Or pyOpenSSL >= 0.11 (Ed25519 and Ed448 keys are not supported with this backend)
|
||||||
author:
|
author:
|
||||||
- Patrick Pichler (@aveexy)
|
- Patrick Pichler (@aveexy)
|
||||||
|
- Markus Teufelberger (@MarkusTeufelberger)
|
||||||
options:
|
options:
|
||||||
action:
|
privatekey_path:
|
||||||
description: Action to be executed
|
description:
|
||||||
type: str
|
- The path to the private key to use when signing.
|
||||||
required: true
|
- Either I(privatekey_path) or I(privatekey_content) must be specified, but not both.
|
||||||
choices: [ sign, verify ]
|
|
||||||
|
|
||||||
private_key:
|
|
||||||
description: Private key required for sign action
|
|
||||||
type: path
|
type: path
|
||||||
|
privatekey_content:
|
||||||
certificate:
|
description:
|
||||||
description: Certificate required for verify action
|
- The content of the private key to use when signing the certificate signing request.
|
||||||
type: path
|
- Either I(privatekey_path) or I(privatekey_content) must be specified, but not both.
|
||||||
|
type: str
|
||||||
passphrase:
|
privatekey_passphrase:
|
||||||
description: Passphrase for private_key
|
description:
|
||||||
|
- The passphrase for the private key.
|
||||||
|
- This is required if the private key is password protected.
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
path:
|
path:
|
||||||
description: file to sign/verify
|
description:
|
||||||
|
- The file to sign.
|
||||||
|
- This file will only be read and not modified.
|
||||||
type: path
|
type: path
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
signature:
|
|
||||||
description: base64 encoded signature required for verify action
|
|
||||||
type: str
|
|
||||||
|
|
||||||
select_crypto_backend:
|
select_crypto_backend:
|
||||||
description:
|
description:
|
||||||
- Determines which crypto backend to use.
|
- Determines which crypto backend to use.
|
||||||
|
@ -58,28 +56,41 @@ options:
|
||||||
type: str
|
type: str
|
||||||
default: auto
|
default: auto
|
||||||
choices: [ auto, cryptography, pyopenssl ]
|
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: community.crypto.openssl_signature_info
|
||||||
|
- module: community.crypto.openssl_privatekey
|
||||||
'''
|
'''
|
||||||
|
|
||||||
EXAMPLES = r'''
|
EXAMPLES = r'''
|
||||||
- name: Sign example file
|
- name: Sign example file
|
||||||
openssl_signature:
|
community.crypto.openssl_signature:
|
||||||
action: sign
|
privatekey_path: private.key
|
||||||
private_key: private.key
|
|
||||||
path: /tmp/example_file
|
path: /tmp/example_file
|
||||||
register: sig
|
register: sig
|
||||||
|
|
||||||
- name: Verify signature of example file
|
- name: Verify signature of example file
|
||||||
openssl_signature:
|
community.crypto.openssl_signature_info:
|
||||||
action: verify
|
certificate_path: cert.pem
|
||||||
certificate: cert.pem
|
|
||||||
path: /tmp/example_file
|
path: /tmp/example_file
|
||||||
signature: sig.signature
|
signature: "{{ sig.signature }}"
|
||||||
|
register: verify
|
||||||
|
|
||||||
|
- name: Make sure the signature is valid
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- verify.valid
|
||||||
'''
|
'''
|
||||||
|
|
||||||
RETURN = r'''
|
RETURN = r'''
|
||||||
signature:
|
signature:
|
||||||
description: base64 encoded signature
|
description: Base64 encoded signature.
|
||||||
returned: changed or success
|
returned: success
|
||||||
type: str
|
type: str
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -88,14 +99,13 @@ import traceback
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
MINIMAL_PYOPENSSL_VERSION = '0.6'
|
MINIMAL_PYOPENSSL_VERSION = '0.11'
|
||||||
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
|
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
|
||||||
|
|
||||||
PYOPENSSL_IMP_ERR = None
|
PYOPENSSL_IMP_ERR = None
|
||||||
try:
|
try:
|
||||||
import OpenSSL
|
import OpenSSL
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
|
|
||||||
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
PYOPENSSL_IMP_ERR = traceback.format_exc()
|
||||||
|
@ -106,12 +116,8 @@ else:
|
||||||
CRYPTOGRAPHY_IMP_ERR = None
|
CRYPTOGRAPHY_IMP_ERR = None
|
||||||
try:
|
try:
|
||||||
import cryptography
|
import cryptography
|
||||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.dsa
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.ec
|
|
||||||
import cryptography.hazmat.primitives.asymmetric.padding
|
import cryptography.hazmat.primitives.asymmetric.padding
|
||||||
import cryptography.hazmat.primitives.hashes
|
import cryptography.hazmat.primitives.hashes
|
||||||
|
|
||||||
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||||
|
@ -119,35 +125,48 @@ except ImportError:
|
||||||
else:
|
else:
|
||||||
CRYPTOGRAPHY_FOUND = True
|
CRYPTOGRAPHY_FOUND = True
|
||||||
|
|
||||||
from ansible.module_utils import crypto as crypto_utils
|
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_privatekey,
|
||||||
|
)
|
||||||
|
|
||||||
from ansible.module_utils._text import to_native, to_bytes
|
from ansible.module_utils._text import to_native, to_bytes
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||||
|
|
||||||
|
|
||||||
class SignatureBase(crypto_utils.OpenSSLObject):
|
class SignatureBase(OpenSSLObject):
|
||||||
|
|
||||||
def __init__(self, module, backend):
|
def __init__(self, module, backend):
|
||||||
super(SignatureBase, self).__init__(
|
super(SignatureBase, self).__init__(
|
||||||
module.params['path'],
|
path=module.params['path'],
|
||||||
'present',
|
state='present',
|
||||||
False,
|
force=False,
|
||||||
module.check_mode
|
check_mode=module.check_mode
|
||||||
)
|
)
|
||||||
|
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
|
|
||||||
self.action = module.params['action']
|
self.privatekey_path = module.params['privatekey_path']
|
||||||
self.signature = module.params['signature']
|
self.privatekey_content = module.params['privatekey_content']
|
||||||
self.passphrase = module.params['passphrase']
|
if self.privatekey_content is not None:
|
||||||
self.private_key = module.params['private_key']
|
self.privatekey_content = self.privatekey_content.encode('utf-8')
|
||||||
self.certificate = module.params['certificate']
|
self.privatekey_passphrase = module.params['privatekey_passphrase']
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
# Empty method because crypto_utils.OpenSSLObject wants this
|
# Empty method because OpenSSLObject wants this
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def dump(self):
|
def dump(self):
|
||||||
# Empty method because crypto_utils.OpenSSLObject wants this
|
# Empty method because OpenSSLObject wants this
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,36 +177,25 @@ class SignaturePyOpenSSL(SignatureBase):
|
||||||
super(SignaturePyOpenSSL, self).__init__(module, backend)
|
super(SignaturePyOpenSSL, self).__init__(module, backend)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
|
||||||
result = dict()
|
|
||||||
|
|
||||||
|
result = dict()
|
||||||
|
|
||||||
|
try:
|
||||||
with open(self.path, "rb") as f:
|
with open(self.path, "rb") as f:
|
||||||
_in = f.read()
|
_in = f.read()
|
||||||
|
|
||||||
if self.action == "verify":
|
private_key = load_privatekey(
|
||||||
_signature = base64.b64decode(self.signature)
|
path=self.privatekey_path,
|
||||||
certificate = crypto_utils.load_certificate(self.certificate, backend=self.backend)
|
content=self.privatekey_content,
|
||||||
|
passphrase=self.privatekey_passphrase,
|
||||||
try:
|
backend=self.backend,
|
||||||
OpenSSL.crypto.verify(certificate, _signature, _in, 'sha256')
|
)
|
||||||
except Exception:
|
|
||||||
self.module.fail_json(
|
|
||||||
msg="Verification failed"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.action == "sign":
|
|
||||||
private_key = crypto_utils.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)
|
|
||||||
|
|
||||||
|
signature = OpenSSL.crypto.sign(private_key, _in, "sha256")
|
||||||
|
result['signature'] = base64.b64encode(signature)
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise crypto_utils.OpenSSLObjectError(e)
|
raise OpenSSLObjectError(e)
|
||||||
|
|
||||||
|
|
||||||
# Implementation with using cryptography
|
# Implementation with using cryptography
|
||||||
|
@ -206,97 +214,65 @@ class SignatureCryptography(SignatureBase):
|
||||||
with open(self.path, "rb") as f:
|
with open(self.path, "rb") as f:
|
||||||
_in = f.read()
|
_in = f.read()
|
||||||
|
|
||||||
if self.action == "verify":
|
private_key = load_privatekey(
|
||||||
_signature = base64.b64decode(self.signature)
|
path=self.privatekey_path,
|
||||||
public_key = crypto_utils.load_certificate(self.certificate, backend=self.backend).public_key()
|
content=self.privatekey_content,
|
||||||
|
passphrase=self.privatekey_passphrase,
|
||||||
|
backend=self.backend,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
signature = None
|
||||||
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
|
|
||||||
public_key.verify(_signature, _in, _padding, _hash)
|
|
||||||
|
|
||||||
elif isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
|
if CRYPTOGRAPHY_HAS_DSA_SIGN:
|
||||||
public_key.verify(_signature, _in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash))
|
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
|
||||||
|
signature = private_key.sign(_in, _hash)
|
||||||
|
|
||||||
elif (isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey) or
|
if CRYPTOGRAPHY_HAS_EC_SIGN:
|
||||||
isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey)):
|
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||||
public_key.verify(_signature, _in)
|
signature = private_key.sign(_in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash))
|
||||||
|
|
||||||
else:
|
if CRYPTOGRAPHY_HAS_ED25519_SIGN:
|
||||||
self.module.fail_json(
|
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
|
||||||
msg="Unsupported algorithm"
|
signature = private_key.sign(_in)
|
||||||
)
|
|
||||||
|
|
||||||
except Exception:
|
if CRYPTOGRAPHY_HAS_ED448_SIGN:
|
||||||
self.module.fail_json(
|
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
|
||||||
msg="Verification failed"
|
signature = private_key.sign(_in)
|
||||||
)
|
|
||||||
|
|
||||||
elif self.action == "sign":
|
if CRYPTOGRAPHY_HAS_RSA_SIGN:
|
||||||
private_key = crypto_utils.load_privatekey(
|
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||||
self.private_key,
|
signature = private_key.sign(_in, _padding, _hash)
|
||||||
None if self.passphrase is None else to_bytes(self.passphrase),
|
|
||||||
backend=self.backend
|
if signature is None:
|
||||||
|
self.module.fail_json(
|
||||||
|
msg="Unsupported key type. Your cryptography version is {0}".format(CRYPTOGRAPHY_VERSION)
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
result['signature'] = base64.b64encode(signature)
|
||||||
out = private_key.sign(_in, _padding, _hash)
|
|
||||||
|
|
||||||
elif isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
|
||||||
out = 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)
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.module.fail_json(
|
|
||||||
msg="Unsupported algorithm"
|
|
||||||
)
|
|
||||||
|
|
||||||
result['signature'] = base64.b64encode(out)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise crypto_utils.OpenSSLObjectError(e)
|
raise OpenSSLObjectError(e)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
argument_spec=dict(
|
argument_spec=dict(
|
||||||
action=dict(type='str', choices=[
|
privatekey_path=dict(type='path'),
|
||||||
'sign', 'verify'
|
privatekey_content=dict(type='str'),
|
||||||
]),
|
privatekey_passphrase=dict(type='str', no_log=True),
|
||||||
private_key=dict(type='path'),
|
|
||||||
certificate=dict(type='path'),
|
|
||||||
passphrase=dict(type='str', no_log=True),
|
|
||||||
path=dict(type='path', required=True),
|
path=dict(type='path', required=True),
|
||||||
signature=dict(type='path'),
|
|
||||||
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'),
|
||||||
),
|
),
|
||||||
supports_check_mode=False,
|
mutually_exclusive=(
|
||||||
|
['privatekey_path', 'privatekey_content'],
|
||||||
|
),
|
||||||
|
required_one_of=(
|
||||||
|
['privatekey_path', 'privatekey_content'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if module.params['private_key'] is not None and module.params['certificate'] is not None:
|
|
||||||
module.fail_json(
|
|
||||||
msg="private_key and certificate are mutually exclusive"
|
|
||||||
)
|
|
||||||
|
|
||||||
if module.params['private_key'] is None and module.params['action'] == "sign":
|
|
||||||
module.fail_json(
|
|
||||||
msg="Private key missing"
|
|
||||||
)
|
|
||||||
|
|
||||||
if module.params['certificate'] is None and module.params['action'] == "verify":
|
|
||||||
module.fail_json(
|
|
||||||
msg="Certificate missing"
|
|
||||||
)
|
|
||||||
|
|
||||||
if module.params['action'] == "verify" and module.params['signature'] is None:
|
|
||||||
module.fail_json(
|
|
||||||
msg="Can't verify without a signature"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not os.path.isfile(module.params['path']):
|
if not os.path.isfile(module.params['path']):
|
||||||
module.fail_json(
|
module.fail_json(
|
||||||
name=module.params['path'],
|
name=module.params['path'],
|
||||||
|
@ -327,7 +303,7 @@ def main():
|
||||||
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
|
||||||
exception=PYOPENSSL_IMP_ERR)
|
exception=PYOPENSSL_IMP_ERR)
|
||||||
module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
|
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)
|
_sign = SignaturePyOpenSSL(module, backend)
|
||||||
elif backend == 'cryptography':
|
elif backend == 'cryptography':
|
||||||
if not CRYPTOGRAPHY_FOUND:
|
if not CRYPTOGRAPHY_FOUND:
|
||||||
|
@ -338,7 +314,7 @@ def main():
|
||||||
result = _sign.run()
|
result = _sign.run()
|
||||||
|
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
except crypto_utils.OpenSSLObjectError as exc:
|
except OpenSSLObjectError as exc:
|
||||||
module.fail_json(msg=to_native(exc))
|
module.fail_json(msg=to_native(exc))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,354 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright: (c) 2019, Patrick Pichler <ppichler+ansible@mgit.at>
|
||||||
|
# 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
|
||||||
|
version_added: 1.1.0
|
||||||
|
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: community.crypto.openssl_signature
|
||||||
|
- module: community.crypto.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: C(true) means the signature was valid for the given file, C(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()
|
|
@ -0,0 +1,3 @@
|
||||||
|
shippable/posix/group1
|
||||||
|
openssl_signature_info
|
||||||
|
destructive
|
|
@ -0,0 +1,2 @@
|
||||||
|
dependencies:
|
||||||
|
- setup_openssl
|
|
@ -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
|
|
@ -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 }}'
|
Loading…
Reference in New Issue