# SPDX-License-Identifier: LGPL-2.1-or-later

# Copyright (c) 2019 Red Hat, Inc.
# Copyright (c) 2019 Tomáš Mráz <tmraz@fedoraproject.org>
#
# This generator targets GnuTLS with allowlisting support (newer than 3.7.2)

from subprocess import call, CalledProcessError
from tempfile import mkstemp

import os
import textwrap

from .configgenerator import ConfigGenerator


class GnuTLSGenerator(ConfigGenerator):
    CONFIG_NAME = 'gnutls'
    SCOPES = {'tls', 'ssl', 'gnutls'}

    mac_map = {
        'AEAD': 'AEAD',
        'HMAC-SHA1': 'SHA1',
        'HMAC-MD5': 'MD5',
        'HMAC-SHA2-256': None,  # not allowlisted over concerns that
        'HMAC-SHA2-384': None,  # implementation might be vulnerable to Lucky13
                                # https://gitlab.com/gnutls/gnutls/-/issues/503
        'HMAC-SHA2-512': 'SHA512',
    }

    hash_map = {
        'AEAD': 'AEAD',
        'SHA1': 'SHA1',
        'MD5': 'MD5',
        'SHA2-224': 'SHA224',
        'SHA2-256': 'SHA256',
        'SHA2-384': 'SHA384',
        'SHA2-512': 'SHA512',
        'SHA3-224': 'SHA3-224',
        'SHA3-256': 'SHA3-256',
        'SHA3-384': 'SHA3-384',
        'SHA3-512': 'SHA3-512',
        'SHAKE-128': 'SHAKE-128',
        'SHAKE-256': 'SHAKE-256',
    }

    group_map = {
        'X448': 'GROUP-X448',
        'X25519': 'GROUP-X25519',
        'SECP256R1': 'GROUP-SECP256R1',
        'SECP384R1': 'GROUP-SECP384R1',
        'SECP521R1': 'GROUP-SECP521R1',
        'FFDHE-6144': 'GROUP-FFDHE6144',
        'FFDHE-2048': 'GROUP-FFDHE2048',
        'FFDHE-3072': 'GROUP-FFDHE3072',
        'FFDHE-4096': 'GROUP-FFDHE4096',
        'FFDHE-8192': 'GROUP-FFDHE8192',
    }

    group_curve_map = {
        'X448': 'X448',
        'X25519': 'X25519',
        'SECP224R1': 'SECP224R1',
        'SECP256R1': 'SECP256R1',
        'SECP384R1': 'SECP384R1',
        'SECP521R1': 'SECP521R1',
    }

    sign_curve_map = {
        'EDDSA-ED448': 'Ed448',
        'EDDSA-ED25519': 'Ed25519',
    }

    sign_map = {
        'RSA-MD5': ['RSA-MD5'],
        'RSA-SHA1': ['RSA-SHA1'],
        'DSA-SHA1': ['DSA-SHA1'],
        'ECDSA-SHA1': ['ECDSA-SHA1'],
        'RSA-SHA2-224': ['RSA-SHA224'],
        'DSA-SHA2-224': ['DSA-SHA224'],
        'ECDSA-SHA2-224': ['ECDSA-SHA224'],
        'RSA-SHA2-256': ['RSA-SHA256'],
        'DSA-SHA2-256': ['DSA-SHA256'],
        'ECDSA-SHA2-256': ['ECDSA-SHA256', 'ECDSA-SECP256R1-SHA256'],
        'RSA-SHA2-384': ['RSA-SHA384'],
        'DSA-SHA2-384': ['DSA-SHA384'],
        'ECDSA-SHA2-384': ['ECDSA-SHA384', 'ECDSA-SECP384R1-SHA384'],
        'RSA-SHA2-512': ['RSA-SHA512'],
        'DSA-SHA2-512': ['DSA-SHA512'],
        'ECDSA-SHA2-512': ['ECDSA-SHA512', 'ECDSA-SECP521R1-SHA512'],

        # These are only available under 3.6.3+
        'RSA-PSS-SHA2-256': ['RSA-PSS-SHA256'],
        'RSA-PSS-SHA2-384': ['RSA-PSS-SHA384'],
        'RSA-PSS-SHA2-512': ['RSA-PSS-SHA512'],
        'RSA-PSS-RSAE-SHA2-256': ['RSA-PSS-RSAE-SHA256'],
        'RSA-PSS-RSAE-SHA2-384': ['RSA-PSS-RSAE-SHA384'],
        'RSA-PSS-RSAE-SHA2-512': ['RSA-PSS-RSAE-SHA512'],

        'RSA-SHA3-224': ['RSA-SHA3-224'],
        'DSA-SHA3-224': ['DSA-SHA3-224'],
        'ECDSA-SHA3-224': ['ECDSA-SHA3-224'],
        'RSA-SHA3-256': ['RSA-SHA3-256'],
        'DSA-SHA3-256': ['DSA-SHA3-256'],
        'ECDSA-SHA3-256': ['ECDSA-SHA3-256'],
        'RSA-SHA3-384': ['RSA-SHA3-384'],
        'DSA-SHA3-384': ['DSA-SHA3-384'],
        'ECDSA-SHA3-384': ['ECDSA-SHA3-384'],
        'RSA-SHA3-512': ['RSA-SHA3-512'],
        'DSA-SHA3-512': ['DSA-SHA3-512'],
        'ECDSA-SHA3-512': ['ECDSA-SHA3-512'],

        'EDDSA-ED448': ['EdDSA-Ed448'],
        'EDDSA-ED25519': ['EdDSA-Ed25519'],
    }

    cipher_map = {
        'AES-256-CTR': '',
        'AES-128-CTR': '',
        'AES-256-GCM': 'AES-256-GCM',
        'AES-128-GCM': 'AES-128-GCM',
        'AES-256-CCM': 'AES-256-CCM',
        'AES-128-CCM': 'AES-128-CCM',
        'AES-256-CBC': 'AES-256-CBC',
        'AES-128-CBC': 'AES-128-CBC',
        'CAMELLIA-256-GCM': 'CAMELLIA-256-GCM',
        'CAMELLIA-128-GCM': 'CAMELLIA-128-GCM',
        'CAMELLIA-256-CBC': 'CAMELLIA-256-CBC',
        'CAMELLIA-128-CBC': 'CAMELLIA-128-CBC',
        'CHACHA20-POLY1305': 'CHACHA20-POLY1305',
        '3DES-CBC': '3DES-CBC',
        'RC4-128': 'ARCFOUR-128',
    }

    key_exchange_map = {
        # ECDHE is handled separately as it splits to ECDHE-ECDSA
        # and ECDHE-RSA.
        'ECDHE': ('ECDHE-RSA', 'ECDHE-ECDSA'),
        'RSA': ('RSA',),
        'DHE-RSA': ('DHE-RSA',),
        'DHE-DSS': ('DHE-DSS',),
        # PSK kexes are not allowlisted because enabling them "forces them":
        # * RSA-PSK precludes using TLS 1.3
        # * others make gnutls-cli ask for PSK identity,
        #   users should enable them explicitly anyway
        #   (https://gitlab.com/gnutls/gnutls/-/issues/680)
        # 'PSK': 'PSK',
        # 'DHE-PSK': 'DHE-PSK',
        # 'ECDHE-PSK': 'ECDHE-PSK',
        # 'RSA-PSK': 'RSA-PSK',
    }

    protocol_map = {
        'SSL3.0': 'SSL3.0',
        'TLS1.0': 'TLS1.0',
        'TLS1.1': 'TLS1.1',
        'TLS1.2': 'TLS1.2',
        'TLS1.3': 'TLS1.3',

        # DTLS 0.9 is excluded specifically due to the fact that
        # gnutls hard-disables algorithms in F35
        # and openconnect is left with no sensible way to re-enable it.
        # this should be addressed with soft-disabling in F36
        # Luckily, it's not in NORMAL so it won't be on by default.

        # 'DTLS0.9': 'DTLS0.9',

        'DTLS1.0': 'DTLS1.0',
        'DTLS1.2': 'DTLS1.2'
    }

    @classmethod
    def generate_config(cls, policy):
        p = policy.enabled

        s = textwrap.dedent('''
            [global]
            override-mode = allowlist

            [overrides]
        ''').lstrip()

        if p['hash']:
            for i in p['hash']:
                try:
                    if cls.hash_map[i]:
                        s += 'secure-hash = '
                        s += cls.hash_map[i]
                        s += '\n'
                except KeyError:
                    pass

        if p['mac']:
            for i in p['mac']:
                try:
                    if cls.mac_map[i]:
                        s += 'tls-enabled-mac = '
                        s += cls.mac_map[i]
                        s += '\n'
                except KeyError:
                    pass

        for i in p['group']:
            if i in cls.group_map:
                s += f'tls-enabled-group = {cls.group_map[i]}\n'

        sigs = [cls.sign_map[i] for i in p['sign'] if i in cls.sign_map]
        for i in sigs:
            for j in i:
                s += f'secure-sig = {j}\n'
        for i in sigs:
            for j in i:
                s += f'secure-sig-for-cert = {j}\n'

        if policy.integers['sha1_in_certs']:
            s += 'secure-sig-for-cert = rsa-sha1\n'
            s += 'secure-sig-for-cert = dsa-sha1\n'
            s += 'secure-sig-for-cert = ecdsa-sha1\n'

        # with allowlisting, curves now need to be enabled separately
        for i in p['group']:
            if i in cls.group_curve_map:
                s += f'enabled-curve = {cls.group_curve_map[i]}\n'
        for i in p['sign']:
            if i in cls.sign_curve_map:
                s += f'enabled-curve = {cls.sign_curve_map[i]}\n'

        if p['cipher']:
            for i in p['cipher']:
                try:
                    if cls.cipher_map[i]:
                        s += 'tls-enabled-cipher = '
                        s += cls.cipher_map[i]
                        s += '\n'
                except KeyError:
                    pass

        for i in p['key_exchange']:
            if i in cls.key_exchange_map:
                for kx in cls.key_exchange_map[i]:
                    s += f'tls-enabled-kx = {kx}\n'

        for i in p['protocol']:
            if i in cls.protocol_map:
                s += f'enabled-version = {cls.protocol_map[i]}\n'

        # option not in Fedora yet, default to True
        no_tls_session_hash = (
            os.getenv('GNUTLS_NO_TLS_SESSION_HASH', '1') == '1'
        )
        if not no_tls_session_hash:
            if policy.enums['__ems'] == 'ENFORCE':
                s += 'tls-session-hash = require\n'
            elif policy.enums['__ems'] == 'RELAX':
                s += 'tls-session-hash = request\n'
            elif policy.enums['__ems'] == 'DEFAULT':
                pass  # let the library determine a fitting default

        # We cannot separate RSA strength from DH params.
        min_dh_size = policy.integers['min_dh_size']
        min_rsa_size = policy.integers['min_rsa_size']
        if min_dh_size <= 768 or min_rsa_size <= 768:
            s += 'min-verification-profile = very_weak'
        elif min_dh_size <= 1024 or min_rsa_size <= 1024:
            s += 'min-verification-profile = low'
        elif min_dh_size <= 2048 or min_rsa_size <= 2048:
            s += 'min-verification-profile = medium'
        elif min_dh_size <= 3072 or min_rsa_size <= 3072:
            s += 'min-verification-profile = high'
        elif min_dh_size <= 8192 or min_rsa_size <= 8192:
            s += 'min-verification-profile = ultra'
        else:
            s += 'min-verification-profile = future'

        s += '\n\n\n[priorities]\nSYSTEM=NONE\n'

        return s

    @classmethod
    def test_config(cls, config):
        if os.getenv('OLD_GNUTLS') == '1':
            return True
        if not os.access('/usr/bin/gnutls-cli', os.X_OK):
            return True

        fd, path = mkstemp()

        ret = 255
        try:
            with os.fdopen(fd, 'w') as f:
                f.write(config)
            try:
                os.environ['GNUTLS_SYSTEM_PRIORITY_FILE'] = path
                os.environ['GNUTLS_DEBUG_LEVEL'] = '3'
                os.environ['GNUTLS_SYSTEM_PRIORITY_FAIL_ON_INVALID'] = '1'
                ret = call('/usr/bin/gnutls-cli -l >/dev/null',
                           shell=True)
            except CalledProcessError:
                cls.eprint("/usr/bin/gnutls-cli: Execution failed")
        finally:
            del os.environ['GNUTLS_SYSTEM_PRIORITY_FILE']
            del os.environ['GNUTLS_DEBUG_LEVEL']
            del os.environ['GNUTLS_SYSTEM_PRIORITY_FAIL_ON_INVALID']
            os.unlink(path)

        if ret:
            cls.eprint('There is an error in gnutls generated policy')
            cls.eprint(f'Policy:\n{config}')
            return False
        return True
