diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml
index 1ea4c69..e6f9eaf 100644
--- a/.gitea/workflows/build.yml
+++ b/.gitea/workflows/build.yml
@@ -31,7 +31,7 @@ jobs:
--output dist/build \
-p:EnableWindowsTargeting=true \
-p:PublishSingleFile=true \
- -p:SelfContained=false
+ -p:SelfContained=true
cp dist/build/MrTrust.exe dist/MrTrust.exe
- name: Build release ZIP
@@ -41,15 +41,9 @@ jobs:
version="0.1.1"
package_root="dist/MrTrust-${version}"
rm -rf "$package_root" "dist/MrTrust-${version}.zip"
- mkdir -p "$package_root/scripts" "$package_root/assets/certificates" "$package_root/docs"
+ mkdir -p "$package_root"
cp dist/MrTrust.exe "$package_root/"
- cp MrTrust.ps1 README.md "$package_root/"
- cp assets/MrTrust.ico "$package_root/assets/"
- cp scripts/Install-MrTrust.ps1 scripts/Uninstall-MrTrust.ps1 scripts/Start-MrTrustGui.ps1 "$package_root/scripts/"
- cp assets/certificates/MrSphay-LocalTrust-Root.cer "$package_root/assets/certificates/"
- cp assets/certificates/MrSphay-CodeSigning.cer "$package_root/assets/certificates/"
- cp assets/certificates/thumbprints.txt "$package_root/assets/certificates/"
- cp docs/security-model.md "$package_root/docs/"
+ cp README.md "$package_root/"
(cd dist && zip -r "MrTrust-${version}.zip" "MrTrust-${version}")
- name: Show package contents
diff --git a/README.md b/README.md
index 321b351..2c1d7cd 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ MrTrust does not bypass Microsoft Defender or SmartScreen. Windows can still sca
- `scripts/Sign-MrTrustProject.ps1` signs `.exe`, `.msi`, `.ps1`, and other Authenticode-compatible files.
- `scripts/New-MrTrustRelease.ps1` builds a distributable ZIP package.
- `docs/integration-prompt.md` is a prompt you can paste into other Windows projects.
+- `MrTrust.exe` is standalone for normal users. It embeds the public certificates and runtime scripts.
## Quick Start For MrSphay
@@ -72,18 +73,7 @@ The Gitea workflow `.gitea/workflows/build.yml` builds the Windows launcher EXE
## User Installation
-For normal users, distribute MrTrust with the public certificate file:
-
-```text
-assets\certificates\MrSphay-LocalTrust-Root.cer
-assets\certificates\MrSphay-CodeSigning.cer
-```
-
-The user runs:
-
-```powershell
-.\MrTrust.ps1 gui
-```
+For normal users, distribute `MrTrust.exe`. The executable embeds the public certificate files and opens the GUI by default.
By default, MrTrust installs trust only for the current Windows user:
diff --git a/scripts/Build-MrTrustExe.ps1 b/scripts/Build-MrTrustExe.ps1
index d479496..380e46a 100644
--- a/scripts/Build-MrTrustExe.ps1
+++ b/scripts/Build-MrTrustExe.ps1
@@ -16,6 +16,21 @@ $sourcePath = Join-Path $root "src\MrTrustLauncher.cs"
$iconPath = Join-Path $root "assets\MrTrust.ico"
$resolvedOutputPath = Resolve-FullPath $OutputPath
$outputDirectory = Split-Path -Parent $resolvedOutputPath
+$payloadFiles = @(
+ @{ Path = "MrTrust.ps1"; ResourceName = "MrTrust.Payload.MrTrust.ps1" },
+ @{ Path = "scripts\Build-MrTrustExe.ps1"; ResourceName = "MrTrust.Payload.scripts.Build-MrTrustExe.ps1" },
+ @{ Path = "scripts\Install-MrTrust.ps1"; ResourceName = "MrTrust.Payload.scripts.Install-MrTrust.ps1" },
+ @{ Path = "scripts\New-MrTrustCertificate.ps1"; ResourceName = "MrTrust.Payload.scripts.New-MrTrustCertificate.ps1" },
+ @{ Path = "scripts\New-MrTrustIcon.ps1"; ResourceName = "MrTrust.Payload.scripts.New-MrTrustIcon.ps1" },
+ @{ Path = "scripts\New-MrTrustRelease.ps1"; ResourceName = "MrTrust.Payload.scripts.New-MrTrustRelease.ps1" },
+ @{ Path = "scripts\Sign-MrTrustProject.ps1"; ResourceName = "MrTrust.Payload.scripts.Sign-MrTrustProject.ps1" },
+ @{ Path = "scripts\Start-MrTrustGui.ps1"; ResourceName = "MrTrust.Payload.scripts.Start-MrTrustGui.ps1" },
+ @{ Path = "scripts\Uninstall-MrTrust.ps1"; ResourceName = "MrTrust.Payload.scripts.Uninstall-MrTrust.ps1" },
+ @{ Path = "assets\MrTrust.ico"; ResourceName = "MrTrust.Payload.assets.MrTrust.ico" },
+ @{ Path = "assets\certificates\MrSphay-LocalTrust-Root.cer"; ResourceName = "MrTrust.Payload.assets.certificates.MrSphay-LocalTrust-Root.cer" },
+ @{ Path = "assets\certificates\MrSphay-CodeSigning.cer"; ResourceName = "MrTrust.Payload.assets.certificates.MrSphay-CodeSigning.cer" },
+ @{ Path = "assets\certificates\thumbprints.txt"; ResourceName = "MrTrust.Payload.assets.certificates.thumbprints.txt" }
+)
if (-not (Test-Path -LiteralPath $sourcePath)) {
throw "Launcher source not found: $sourcePath"
@@ -25,6 +40,13 @@ if (-not (Test-Path -LiteralPath $iconPath)) {
& (Join-Path $root "scripts\New-MrTrustIcon.ps1") -OutputPath $iconPath
}
+foreach ($payloadFile in $payloadFiles) {
+ $payloadPath = Join-Path $root $payloadFile.Path
+ if (-not (Test-Path -LiteralPath $payloadPath)) {
+ throw "Payload file not found: $payloadPath"
+ }
+}
+
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
$compilerCandidates = @(
@@ -37,20 +59,29 @@ if (-not $compiler) {
throw "csc.exe was not found. Run this build on a Windows Gitea runner with .NET Framework installed."
}
-& $compiler `
- /nologo `
- /target:winexe `
- /optimize+ `
- /platform:anycpu `
- /out:$resolvedOutputPath `
- /win32icon:$iconPath `
- /reference:System.Windows.Forms.dll `
- /reference:System.Drawing.dll `
- $sourcePath
+$compilerArguments = @(
+ "/nologo",
+ "/target:winexe",
+ "/optimize+",
+ "/platform:anycpu",
+ "/out:$resolvedOutputPath",
+ "/win32icon:$iconPath",
+ "/reference:System.Windows.Forms.dll",
+ "/reference:System.Drawing.dll"
+)
+
+foreach ($payloadFile in $payloadFiles) {
+ $payloadPath = Join-Path $root $payloadFile.Path
+ $compilerArguments += "/resource:$payloadPath,$($payloadFile.ResourceName)"
+}
+
+$compilerArguments += $sourcePath
+
+& $compiler @compilerArguments
if ($LASTEXITCODE -ne 0) {
throw "csc.exe failed with exit code $LASTEXITCODE."
}
-Write-Host "Created EXE:"
+Write-Host "Created standalone EXE:"
Write-Host " $resolvedOutputPath"
diff --git a/scripts/New-MrTrustRelease.ps1 b/scripts/New-MrTrustRelease.ps1
index 6ee3a3a..9c7d41d 100644
--- a/scripts/New-MrTrustRelease.ps1
+++ b/scripts/New-MrTrustRelease.ps1
@@ -27,9 +27,6 @@ if (Test-Path -LiteralPath $packageRoot) {
}
New-Item -ItemType Directory -Force -Path $packageRoot | Out-Null
-New-Item -ItemType Directory -Force -Path (Join-Path $packageRoot "scripts") | Out-Null
-New-Item -ItemType Directory -Force -Path (Join-Path $packageRoot "assets\certificates") | Out-Null
-New-Item -ItemType Directory -Force -Path (Join-Path $packageRoot "docs") | Out-Null
if (-not (Test-Path -LiteralPath $iconPath)) {
& (Join-Path $root "scripts\New-MrTrustIcon.ps1") -OutputPath $iconPath
@@ -55,16 +52,7 @@ if ($SigningThumbprint) {
}
Copy-Item -LiteralPath $exePath -Destination $packageRoot
-Copy-Item -LiteralPath (Join-Path $root "MrTrust.ps1") -Destination $packageRoot
Copy-Item -LiteralPath (Join-Path $root "README.md") -Destination $packageRoot
-Copy-Item -LiteralPath $iconPath -Destination (Join-Path $packageRoot "assets")
-Copy-Item -LiteralPath (Join-Path $root "scripts\Install-MrTrust.ps1") -Destination (Join-Path $packageRoot "scripts")
-Copy-Item -LiteralPath (Join-Path $root "scripts\Uninstall-MrTrust.ps1") -Destination (Join-Path $packageRoot "scripts")
-Copy-Item -LiteralPath (Join-Path $root "scripts\Start-MrTrustGui.ps1") -Destination (Join-Path $packageRoot "scripts")
-Copy-Item -LiteralPath (Join-Path $root "assets\certificates\MrSphay-LocalTrust-Root.cer") -Destination (Join-Path $packageRoot "assets\certificates")
-Copy-Item -LiteralPath (Join-Path $root "assets\certificates\MrSphay-CodeSigning.cer") -Destination (Join-Path $packageRoot "assets\certificates")
-Copy-Item -LiteralPath (Join-Path $root "assets\certificates\thumbprints.txt") -Destination (Join-Path $packageRoot "assets\certificates")
-Copy-Item -LiteralPath (Join-Path $root "docs\security-model.md") -Destination (Join-Path $packageRoot "docs")
if (Test-Path -LiteralPath $zipPath) {
Remove-Item -LiteralPath $zipPath -Force
diff --git a/src/MrTrustLauncher.cs b/src/MrTrustLauncher.cs
index f8ad4d4..6908e9b 100644
--- a/src/MrTrustLauncher.cs
+++ b/src/MrTrustLauncher.cs
@@ -1,34 +1,49 @@
using System;
using System.Diagnostics;
using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
using System.Windows.Forms;
namespace MrTrust
{
internal static class MrTrustLauncher
{
- [STAThread]
- private static int Main()
- {
- string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
- string scriptPath = Path.Combine(baseDirectory, "MrTrust.ps1");
+ private const string PayloadResourcePrefix = "MrTrust.Payload.";
- if (!File.Exists(scriptPath))
- {
- MessageBox.Show(
- "MrTrust.ps1 was not found next to MrTrust.exe.",
- "MrTrust",
- MessageBoxButtons.OK,
- MessageBoxIcon.Error);
- return 1;
- }
+ private static readonly PayloadFile[] PayloadFiles =
+ {
+ new PayloadFile("MrTrust.ps1", "MrTrust.ps1"),
+ new PayloadFile("scripts.Build-MrTrustExe.ps1", Path.Combine("scripts", "Build-MrTrustExe.ps1")),
+ new PayloadFile("scripts.Install-MrTrust.ps1", Path.Combine("scripts", "Install-MrTrust.ps1")),
+ new PayloadFile("scripts.New-MrTrustCertificate.ps1", Path.Combine("scripts", "New-MrTrustCertificate.ps1")),
+ new PayloadFile("scripts.New-MrTrustIcon.ps1", Path.Combine("scripts", "New-MrTrustIcon.ps1")),
+ new PayloadFile("scripts.New-MrTrustRelease.ps1", Path.Combine("scripts", "New-MrTrustRelease.ps1")),
+ new PayloadFile("scripts.Sign-MrTrustProject.ps1", Path.Combine("scripts", "Sign-MrTrustProject.ps1")),
+ new PayloadFile("scripts.Start-MrTrustGui.ps1", Path.Combine("scripts", "Start-MrTrustGui.ps1")),
+ new PayloadFile("scripts.Uninstall-MrTrust.ps1", Path.Combine("scripts", "Uninstall-MrTrust.ps1")),
+ new PayloadFile("assets.MrTrust.ico", Path.Combine("assets", "MrTrust.ico")),
+ new PayloadFile("assets.certificates.MrSphay-LocalTrust-Root.cer", Path.Combine("assets", "certificates", "MrSphay-LocalTrust-Root.cer")),
+ new PayloadFile("assets.certificates.MrSphay-CodeSigning.cer", Path.Combine("assets", "certificates", "MrSphay-CodeSigning.cer")),
+ new PayloadFile("assets.certificates.thumbprints.txt", Path.Combine("assets", "certificates", "thumbprints.txt"))
+ };
+
+ [STAThread]
+ private static int Main(string[] args)
+ {
+ string baseDirectory = string.Empty;
try
{
+ baseDirectory = ExtractPayload();
+ string scriptPath = Path.Combine(baseDirectory, "MrTrust.ps1");
+ string commandArguments = BuildCommandArguments(args);
+
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "powershell.exe",
- Arguments = "-NoProfile -ExecutionPolicy Bypass -File \"" + scriptPath + "\" gui",
+ Arguments = "-NoProfile -ExecutionPolicy Bypass -File " + QuoteArgument(scriptPath) + " " + commandArguments,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = baseDirectory
@@ -40,9 +55,10 @@ namespace MrTrust
{
throw new InvalidOperationException("PowerShell could not be started.");
}
- }
- return 0;
+ process.WaitForExit();
+ return process.ExitCode;
+ }
}
catch (Exception ex)
{
@@ -53,6 +69,119 @@ namespace MrTrust
MessageBoxIcon.Error);
return 1;
}
+ finally
+ {
+ TryDeleteDirectory(baseDirectory);
+ }
+ }
+
+ private static string ExtractPayload()
+ {
+ Assembly assembly = Assembly.GetExecutingAssembly();
+ string versionKey = GetPayloadVersionKey(assembly);
+ string targetDirectory = Path.Combine(
+ Path.GetTempPath(),
+ "MrTrust",
+ "standalone",
+ versionKey,
+ Guid.NewGuid().ToString("N"));
+
+ foreach (PayloadFile payloadFile in PayloadFiles)
+ {
+ string targetPath = Path.Combine(targetDirectory, payloadFile.RelativePath);
+ Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
+
+ using (Stream stream = assembly.GetManifestResourceStream(PayloadResourcePrefix + payloadFile.ResourceName))
+ {
+ if (stream == null)
+ {
+ throw new FileNotFoundException("Embedded MrTrust payload file was not found.", payloadFile.RelativePath);
+ }
+
+ using (FileStream file = File.Create(targetPath))
+ {
+ stream.CopyTo(file);
+ }
+ }
+ }
+
+ return targetDirectory;
+ }
+
+ private static void TryDeleteDirectory(string directory)
+ {
+ if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory))
+ {
+ return;
+ }
+
+ try
+ {
+ Directory.Delete(directory, true);
+ }
+ catch
+ {
+ // Best-effort cleanup only. A locked icon or antivirus scan should not mask the command result.
+ }
+ }
+
+ private static string GetPayloadVersionKey(Assembly assembly)
+ {
+ string location = assembly.Location;
+ if (File.Exists(location))
+ {
+ FileInfo fileInfo = new FileInfo(location);
+ return fileInfo.Length.ToString("x") + "-" + fileInfo.LastWriteTimeUtc.Ticks.ToString("x");
+ }
+
+ return assembly.GetName().Version.ToString();
+ }
+
+ private static string BuildCommandArguments(string[] args)
+ {
+ string[] effectiveArgs = args.Length == 0 ? new[] { "gui" } : args;
+ return string.Join(" ", effectiveArgs.Select(QuoteArgument).ToArray());
+ }
+
+ private static string QuoteArgument(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return "\"\"";
+ }
+
+ if (value.IndexOfAny(new[] { ' ', '\t', '\n', '\r', '"' }) < 0)
+ {
+ return value;
+ }
+
+ StringBuilder builder = new StringBuilder();
+ builder.Append('"');
+ foreach (char character in value)
+ {
+ if (character == '"')
+ {
+ builder.Append('\\');
+ }
+
+ builder.Append(character);
+ }
+
+ builder.Append('"');
+ return builder.ToString();
+ }
+
+ private sealed class PayloadFile
+ {
+ public PayloadFile(string resourceName, string relativePath)
+ {
+ ResourceName = resourceName;
+ RelativePath = relativePath;
+ }
+
+ public string ResourceName { get; private set; }
+
+ public string RelativePath { get; private set; }
}
}
}
diff --git a/src/MrTrustLauncher.csproj b/src/MrTrustLauncher.csproj
index 14e922f..6e0d021 100644
--- a/src/MrTrustLauncher.csproj
+++ b/src/MrTrustLauncher.csproj
@@ -9,7 +9,23 @@
enable
enable
true
- false
+ true
+ win-x64
..\assets\MrTrust.ico
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+