#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2019, Patrick Pichler <ppichler+ansible@mgit.at>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function


__metaclass__ = type


DOCUMENTATION = r"""
module: openssl_signature
version_added: 1.1.0
short_description: Sign data with openssl
description:
  - This module allows one to sign data using a private key.
  - The module uses the cryptography Python library.
requirements:
  - cryptography >= 1.4 (some key types require newer versions)
author:
  - Patrick Pichler (@aveexy)
  - Markus Teufelberger (@MarkusTeufelberger)
extends_documentation_fragment:
  - community.crypto.attributes
attributes:
  check_mode:
    support: full
    details:
      - This action does not modify state.
  diff_mode:
    support: none
  idempotent:
    support: partial
    details:
      - Signature algorithms are generally not deterministic. Thus the generated signature
        can change from one invocation to the next.
options:
  privatekey_path:
    description:
      - The path to the private key to use when signing.
      - Either O(privatekey_path) or O(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 O(privatekey_path) or O(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:
      - The file to sign.
      - This file will only be read and not modified.
    type: path
    required: true
  select_crypto_backend:
    description:
      - Determines which crypto backend to use.
      - The default choice is V(auto), which tries to use C(cryptography) if available.
      - If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
    type: str
    default: auto
    choices: [auto, cryptography]
notes:
  - "When using the C(cryptography) backend, the following key types require at least the following C(cryptography) version:\n
    RSA keys: C(cryptography) >= 1.4\nDSA and ECDSA keys: C(cryptography) >= 1.5\ned448 and ed25519 keys: C(cryptography)
    >= 2.6."
seealso:
  - module: community.crypto.openssl_signature_info
  - module: community.crypto.openssl_privatekey
"""

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
  ansible.builtin.assert:
    that:
      - verify.valid
"""

RETURN = r"""
signature:
  description: Base64 encoded signature.
  returned: success
  type: str
"""

import base64
import os
import traceback

from ansible_collections.community.crypto.plugins.module_utils.version import (
    LooseVersion,
)


MINIMAL_CRYPTOGRAPHY_VERSION = "1.4"

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.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
    CRYPTOGRAPHY_HAS_DSA_SIGN,
    CRYPTOGRAPHY_HAS_EC_SIGN,
    CRYPTOGRAPHY_HAS_ED448_SIGN,
    CRYPTOGRAPHY_HAS_ED25519_SIGN,
    CRYPTOGRAPHY_HAS_RSA_SIGN,
    OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
    OpenSSLObject,
    load_privatekey,
)


class SignatureBase(OpenSSLObject):

    def __init__(self, module, backend):
        super(SignatureBase, self).__init__(
            path=module.params["path"],
            state="present",
            force=False,
            check_mode=module.check_mode,
        )

        self.backend = backend

        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
        pass

    def dump(self):
        # Empty method because OpenSSLObject wants this
        pass


# Implementation with using cryptography
class SignatureCryptography(SignatureBase):

    def __init__(self, module, backend):
        super(SignatureCryptography, 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()

            private_key = load_privatekey(
                path=self.privatekey_path,
                content=self.privatekey_content,
                passphrase=self.privatekey_passphrase,
                backend=self.backend,
            )

            signature = None

            if CRYPTOGRAPHY_HAS_DSA_SIGN:
                if isinstance(
                    private_key,
                    cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey,
                ):
                    signature = private_key.sign(_in, _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)
                    )

            if CRYPTOGRAPHY_HAS_ED25519_SIGN:
                if isinstance(
                    private_key,
                    cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey,
                ):
                    signature = private_key.sign(_in)

            if CRYPTOGRAPHY_HAS_ED448_SIGN:
                if isinstance(
                    private_key,
                    cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey,
                ):
                    signature = private_key.sign(_in)

            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:
            raise OpenSSLObjectError(e)


def main():
    module = AnsibleModule(
        argument_spec=dict(
            privatekey_path=dict(type="path"),
            privatekey_content=dict(type="str", no_log=True),
            privatekey_passphrase=dict(type="str", no_log=True),
            path=dict(type="path", required=True),
            select_crypto_backend=dict(
                type="str", choices=["auto", "cryptography"], default="auto"
            ),
        ),
        mutually_exclusive=(["privatekey_path", "privatekey_content"],),
        required_one_of=(["privatekey_path", "privatekey_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)
        )

        # Decision
        if can_use_cryptography:
            backend = "cryptography"

        # Success?
        if backend == "auto":
            module.fail_json(
                msg=(
                    "Cannot detect the required Python library " "cryptography (>= {0})"
                ).format(MINIMAL_CRYPTOGRAPHY_VERSION)
            )
    try:
        if backend == "cryptography":
            if not CRYPTOGRAPHY_FOUND:
                module.fail_json(
                    msg=missing_required_lib(
                        "cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
                    ),
                    exception=CRYPTOGRAPHY_IMP_ERR,
                )
            _sign = SignatureCryptography(module, backend)

        result = _sign.run()

        module.exit_json(**result)
    except OpenSSLObjectError as exc:
        module.fail_json(msg=to_native(exc))


if __name__ == "__main__":
    main()
