#!/usr/bin/python
# 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 annotations

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.
author:
  - Patrick Pichler (@aveexy)
  - Markus Teufelberger (@MarkusTeufelberger)
extends_documentation_fragment:
  - community.crypto._attributes
  - community.crypto._cryptography_dep.minimum
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.
      - Note that with community.crypto 3.0.0, all values behave the same.
        This option will be deprecated in a later version.
        We recommend to not set it explicitly.
    type: str
    default: auto
    choices: [auto, cryptography]
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 typing as t

from ansible_collections.community.crypto.plugins.module_utils._cryptography_dep import (
    COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION,
    assert_required_cryptography_version,
)
from ansible_collections.community.crypto.plugins.module_utils._version import (
    LooseVersion,
)

MINIMAL_CRYPTOGRAPHY_VERSION = COLLECTION_MINIMUM_CRYPTOGRAPHY_VERSION

try:
    import cryptography
    import cryptography.hazmat.primitives.asymmetric.padding
    import cryptography.hazmat.primitives.hashes

except ImportError:
    CRYPTOGRAPHY_VERSION = LooseVersion("0.0")  # pylint: disable=invalid-name
else:
    # pylint: disable-next=invalid-name
    CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)

from ansible.module_utils.basic import AnsibleModule

from ansible_collections.community.crypto.plugins.module_utils._crypto.basic import (
    OpenSSLObjectError,
)
from ansible_collections.community.crypto.plugins.module_utils._crypto.support import (
    OpenSSLObject,
    load_privatekey,
)


class SignatureBase(OpenSSLObject):
    def __init__(self, module: AnsibleModule) -> None:
        super().__init__(
            path=module.params["path"],
            state="present",
            force=False,
            check_mode=module.check_mode,
        )

        self.module = module
        self.privatekey_path: str | None = module.params["privatekey_path"]
        privatekey_content: str | None = module.params["privatekey_content"]
        if privatekey_content is not None:
            self.privatekey_content: bytes | None = privatekey_content.encode("utf-8")
        else:
            self.privatekey_content = None
        self.privatekey_passphrase: str | None = module.params["privatekey_passphrase"]

    def generate(self, module: AnsibleModule) -> None:
        # Empty method because OpenSSLObject wants this
        pass

    def dump(self) -> dict[str, t.Any]:
        # Empty method because OpenSSLObject wants this
        return {}


# Implementation with using cryptography
class SignatureCryptography(SignatureBase):
    def __init__(self, module: AnsibleModule) -> None:
        super().__init__(module)

    def run(self) -> dict[str, t.Any]:
        _padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15()
        _hash = cryptography.hazmat.primitives.hashes.SHA256()

        result: dict[str, t.Any] = {}

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

            signature = None

            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,
            ):
                signature = private_key.sign(
                    _in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash)
                )

            elif isinstance(
                private_key,
                (
                    cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey,
                    cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey,
                ),
            ):
                signature = private_key.sign(_in)

            elif 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=f"Unsupported key type. Your cryptography version is {CRYPTOGRAPHY_VERSION}"
                )

            result["signature"] = base64.b64encode(signature)
            return result

        except Exception as e:
            raise OpenSSLObjectError(e) from e


def main() -> t.NoReturn:
    module = AnsibleModule(
        argument_spec={
            "privatekey_path": {"type": "path"},
            "privatekey_content": {"type": "str", "no_log": True},
            "privatekey_passphrase": {"type": "str", "no_log": True},
            "path": {"type": "path", "required": True},
            "select_crypto_backend": {
                "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=f"The file {module.params['path']} does not exist",
        )

    assert_required_cryptography_version(
        module, minimum_cryptography_version=MINIMAL_CRYPTOGRAPHY_VERSION
    )

    try:
        _sign = SignatureCryptography(module)

        result = _sign.run()

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


if __name__ == "__main__":
    main()
