import { execFile } from "child_process";
import { readFileSync, writeFileSync } from "fs";
import { mkdtemp, rm, writeFile } from "fs/promises";
import forge from "node-forge";
import { tmpdir } from "os";
import { join } from "path";
import { promisify } from "util";
// Signature block markers
const SIGNATURE_HEADER = "MCPB_SIG_V1";
const SIGNATURE_FOOTER = "MCPB_SIG_END";
const execFileAsync = promisify(execFile);
/**
 * Signs a MCPB file with the given certificate and private key using PKCS#7
 *
 * @param mcpbPath Path to the MCPB file to sign
 * @param certPath Path to the certificate file (PEM format)
 * @param keyPath Path to the private key file (PEM format)
 * @param intermediates Optional array of intermediate certificate paths
 */
export function signMcpbFile(mcpbPath, certPath, keyPath, intermediates) {
    // Read the original MCPB file
    const mcpbContent = readFileSync(mcpbPath);
    // Read certificate and key
    const certificatePem = readFileSync(certPath, "utf-8");
    const privateKeyPem = readFileSync(keyPath, "utf-8");
    // Read intermediate certificates if provided
    const intermediatePems = intermediates?.map((path) => readFileSync(path, "utf-8"));
    // Create PKCS#7 signed data
    const p7 = forge.pkcs7.createSignedData();
    p7.content = forge.util.createBuffer(mcpbContent);
    // Parse and add certificates
    const signingCert = forge.pki.certificateFromPem(certificatePem);
    const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
    p7.addCertificate(signingCert);
    // Add intermediate certificates
    if (intermediatePems) {
        for (const pem of intermediatePems) {
            p7.addCertificate(forge.pki.certificateFromPem(pem));
        }
    }
    // Add signer
    p7.addSigner({
        key: privateKey,
        certificate: signingCert,
        digestAlgorithm: forge.pki.oids.sha256,
        authenticatedAttributes: [
            {
                type: forge.pki.oids.contentType,
                value: forge.pki.oids.data,
            },
            {
                type: forge.pki.oids.messageDigest,
                // Value will be auto-populated
            },
            {
                type: forge.pki.oids.signingTime,
                // Value will be auto-populated with current time
            },
        ],
    });
    // Sign with detached signature
    p7.sign({ detached: true });
    // Convert to DER format
    const asn1 = forge.asn1.toDer(p7.toAsn1());
    const pkcs7Signature = Buffer.from(asn1.getBytes(), "binary");
    // Create signature block with PKCS#7 data
    const signatureBlock = createSignatureBlock(pkcs7Signature);
    // Append signature block to MCPB file
    const signedContent = Buffer.concat([mcpbContent, signatureBlock]);
    writeFileSync(mcpbPath, signedContent);
}
/**
 * Verifies a signed MCPB file using OS certificate store
 *
 * @param mcpbPath Path to the signed MCPB file
 * @returns Signature information including verification status
 */
export async function verifyMcpbFile(mcpbPath) {
    try {
        const fileContent = readFileSync(mcpbPath);
        // Find and extract signature block
        const { originalContent, pkcs7Signature } = extractSignatureBlock(fileContent);
        if (!pkcs7Signature) {
            return { status: "unsigned" };
        }
        // Parse PKCS#7 signature
        const asn1 = forge.asn1.fromDer(pkcs7Signature.toString("binary"));
        const p7Message = forge.pkcs7.messageFromAsn1(asn1);
        // Verify it's signed data and cast to correct type
        if (!("type" in p7Message) ||
            p7Message.type !== forge.pki.oids.signedData) {
            return { status: "unsigned" };
        }
        // Now we know it's PkcsSignedData. The types are incorrect, so we'll
        // fix them there
        const p7 = p7Message;
        // Extract certificates from PKCS#7
        const certificates = p7.certificates || [];
        if (certificates.length === 0) {
            return { status: "unsigned" };
        }
        // Get the signing certificate (first one)
        const signingCert = certificates[0];
        // Verify PKCS#7 signature
        const contentBuf = forge.util.createBuffer(originalContent);
        try {
            p7.verify({ authenticatedAttributes: true });
            // Also verify the content matches
            const signerInfos = p7.signerInfos;
            const signerInfo = signerInfos?.[0];
            if (signerInfo) {
                const md = forge.md.sha256.create();
                md.update(contentBuf.getBytes());
                const digest = md.digest().getBytes();
                // Find the message digest attribute
                let messageDigest = null;
                for (const attr of signerInfo.authenticatedAttributes) {
                    if (attr.type === forge.pki.oids.messageDigest) {
                        messageDigest = attr.value;
                        break;
                    }
                }
                if (!messageDigest || messageDigest !== digest) {
                    return { status: "unsigned" };
                }
            }
        }
        catch (error) {
            return { status: "unsigned" };
        }
        // Convert forge certificate to PEM for OS verification
        const certPem = forge.pki.certificateToPem(signingCert);
        const intermediatePems = certificates
            .slice(1)
            .map((cert) => Buffer.from(forge.pki.certificateToPem(cert)));
        // Verify certificate chain against OS trust store
        const chainValid = await verifyCertificateChain(Buffer.from(certPem), intermediatePems);
        if (!chainValid) {
            // Signature is valid but certificate is not trusted
            return { status: "unsigned" };
        }
        // Extract certificate info
        const isSelfSigned = signingCert.issuer.getField("CN")?.value ===
            signingCert.subject.getField("CN")?.value;
        return {
            status: isSelfSigned ? "self-signed" : "signed",
            publisher: signingCert.subject.getField("CN")?.value || "Unknown",
            issuer: signingCert.issuer.getField("CN")?.value || "Unknown",
            valid_from: signingCert.validity.notBefore.toISOString(),
            valid_to: signingCert.validity.notAfter.toISOString(),
            fingerprint: forge.md.sha256
                .create()
                .update(forge.asn1.toDer(forge.pki.certificateToAsn1(signingCert)).getBytes())
                .digest()
                .toHex(),
        };
    }
    catch (error) {
        throw new Error(`Failed to verify MCPB file: ${error}`);
    }
}
/**
 * Creates a signature block buffer with PKCS#7 signature
 */
function createSignatureBlock(pkcs7Signature) {
    const parts = [];
    // Header
    parts.push(Buffer.from(SIGNATURE_HEADER, "utf-8"));
    // PKCS#7 signature length and data
    const sigLengthBuffer = Buffer.alloc(4);
    sigLengthBuffer.writeUInt32LE(pkcs7Signature.length, 0);
    parts.push(sigLengthBuffer);
    parts.push(pkcs7Signature);
    // Footer
    parts.push(Buffer.from(SIGNATURE_FOOTER, "utf-8"));
    return Buffer.concat(parts);
}
/**
 * Extracts the signature block from a signed MCPB file
 */
export function extractSignatureBlock(fileContent) {
    // Look for signature footer at the end
    const footerBytes = Buffer.from(SIGNATURE_FOOTER, "utf-8");
    const footerIndex = fileContent.lastIndexOf(footerBytes);
    if (footerIndex === -1) {
        return { originalContent: fileContent };
    }
    // Look for signature header before footer
    const headerBytes = Buffer.from(SIGNATURE_HEADER, "utf-8");
    let headerIndex = -1;
    // Search backwards from footer
    for (let i = footerIndex - 1; i >= 0; i--) {
        if (fileContent.slice(i, i + headerBytes.length).equals(headerBytes)) {
            headerIndex = i;
            break;
        }
    }
    if (headerIndex === -1) {
        return { originalContent: fileContent };
    }
    // Extract original content (everything before signature block)
    const originalContent = fileContent.slice(0, headerIndex);
    // Parse signature block
    let offset = headerIndex + headerBytes.length;
    try {
        // Read PKCS#7 signature length
        const sigLength = fileContent.readUInt32LE(offset);
        offset += 4;
        // Read PKCS#7 signature
        const pkcs7Signature = fileContent.slice(offset, offset + sigLength);
        return {
            originalContent,
            pkcs7Signature,
        };
    }
    catch {
        return { originalContent: fileContent };
    }
}
/**
 * Verifies certificate chain against OS trust store
 */
export async function verifyCertificateChain(certificate, intermediates) {
    let tempDir = null;
    try {
        tempDir = await mkdtemp(join(tmpdir(), "mcpb-verify-"));
        const certChainPath = join(tempDir, "chain.pem");
        const certChain = [certificate, ...(intermediates || [])].join("\n");
        await writeFile(certChainPath, certChain);
        // Platform-specific verification
        if (process.platform === "darwin") {
            try {
                await execFileAsync("security", [
                    "verify-cert",
                    "-c",
                    certChainPath,
                    "-p",
                    "codeSign",
                ]);
                return true;
            }
            catch (error) {
                return false;
            }
        }
        else if (process.platform === "win32") {
            const psCommand = `
        $ErrorActionPreference = 'Stop'
        $certCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
        $certCollection.Import('${certChainPath}')
        
        if ($certCollection.Count -eq 0) {
          Write-Error 'No certificates found'
          exit 1
        }
        
        $leafCert = $certCollection[0]
        $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
        
        # Enable revocation checking
        $chain.ChainPolicy.RevocationMode = 'Online'
        $chain.ChainPolicy.RevocationFlag = 'EntireChain'
        $chain.ChainPolicy.UrlRetrievalTimeout = New-TimeSpan -Seconds 30
        
        # Add code signing application policy
        $codeSignOid = New-Object System.Security.Cryptography.Oid '1.3.6.1.5.5.7.3.3'
        $chain.ChainPolicy.ApplicationPolicy.Add($codeSignOid)
        
        # Add intermediate certificates to extra store
        for ($i = 1; $i -lt $certCollection.Count; $i++) {
          [void]$chain.ChainPolicy.ExtraStore.Add($certCollection[$i])
        }
        
        # Build and validate chain
        $result = $chain.Build($leafCert)
        
        if ($result) { 
          'Valid' 
        } else { 
          $chain.ChainStatus | ForEach-Object { 
            Write-Error "$($_.Status): $($_.StatusInformation)"
          }
          exit 1 
        }
      `.trim();
            const { stdout } = await execFileAsync("powershell.exe", [
                "-NoProfile",
                "-NonInteractive",
                "-Command",
                psCommand,
            ]);
            return stdout.includes("Valid");
        }
        else {
            // Linux: Use openssl
            try {
                await execFileAsync("openssl", [
                    "verify",
                    "-purpose",
                    "codesigning",
                    "-CApath",
                    "/etc/ssl/certs",
                    certChainPath,
                ]);
                return true;
            }
            catch (error) {
                return false;
            }
        }
    }
    catch (error) {
        return false;
    }
    finally {
        if (tempDir) {
            try {
                await rm(tempDir, { recursive: true, force: true });
            }
            catch {
                // Ignore cleanup errors
            }
        }
    }
}
/**
 * Removes signature from a MCPB file
 */
export function unsignMcpbFile(mcpbPath) {
    const fileContent = readFileSync(mcpbPath);
    const { originalContent } = extractSignatureBlock(fileContent);
    writeFileSync(mcpbPath, originalContent);
}
