Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

Commit

Permalink
Allow skip DLL sign & add signtool.exe parallelism (closes #90)
Browse files Browse the repository at this point in the history
  • Loading branch information
caesay committed Aug 24, 2022
1 parent 5883aef commit 5f9f594
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 61 deletions.
9 changes: 5 additions & 4 deletions src/Squirrel.CommandLine/ValidatedOptionSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,13 @@ protected virtual void IsValidUrl(string propertyName)
throw new OptionValidationException(propertyName, "Must start with http or https and be a valid URI.");
}

protected virtual int ParseIntArg(string propertyName, string propertyValue)
protected virtual int ParseIntArg(string propertyName, string v, int min = Int32.MinValue, int max = Int32.MaxValue)
{
if (int.TryParse(propertyValue, out var value))
return value;
if (!int.TryParse(v, out var i) || i < min || i > max) {
throw new OptionValidationException(propertyName, $"Must be an integer between {min} and {max}.");
}

throw new OptionValidationException(propertyName, "Must be a valid integer.");
return i;
}

public abstract void Validate();
Expand Down
27 changes: 16 additions & 11 deletions src/Squirrel.CommandLine/Windows/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,6 @@ static void Releasify(ReleasifyOptions options)
if (!DotnetUtil.IsSingleFileBundle(updatePath))
throw new InvalidOperationException("Update.exe is corrupt. Broken Squirrel install?");

// Sign Update.exe so that virus scanners don't think we're pulling one over on them
options.SignPEFile(updatePath);

// copy input package to target output directory
File.Copy(package, Path.Combine(targetDir.FullName, Path.GetFileName(package)), true);

Expand Down Expand Up @@ -174,20 +171,25 @@ RuntimeCpu parseMachine(PeNet.Header.Pe.MachineType machine)
var exesToCreateStubFor = new DirectoryInfo(pkgPath).GetAllFilesRecursively()
.Where(x => x.Name.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
.Where(x => !x.Name.Equals("squirrel.exe", StringComparison.InvariantCultureIgnoreCase))
.Where(x => !x.Name.Equals("createdump.exe", StringComparison.InvariantCultureIgnoreCase))
.Where(x => Utility.IsFileTopLevelInPackage(x.FullName, pkgPath))
.ToArray(); // materialize the IEnumerable so we never end up creating stubs for stubs
Log.Info($"Creating {exesToCreateStubFor.Length} stub executables");
exesToCreateStubFor.ForEach(x => createExecutableStubForExe(x.FullName));
// sign all exe's in this package
new DirectoryInfo(pkgPath).GetAllFilesRecursively()
.Where(x => Utility.FileIsLikelyPEImage(x.Name))
.ForEachAsync(x => options.SignPEFile(x.FullName))
.Wait();
// copy Update.exe into package, so it can also be updated in both full/delta packages
// and do it before signing so that Update.exe will also be signed. It is renamed to
// 'Squirrel.exe' only because Squirrel.Windows expects it to be called this.
File.Copy(updatePath, Path.Combine(libDir, "Squirrel.exe"), true);
// sign all exe's in this package
var filesToSign = new DirectoryInfo(libDir).GetAllFilesRecursively()
.Where(x => options.signSkipDll ? Utility.PathPartEndsWith(x.Name, ".exe") : Utility.FileIsLikelyPEImage(x.Name))
.Select(x => x.FullName)
.ToArray();
options.SignFiles(libDir, filesToSign);
// copy app icon to 'lib/fx/app.ico'
var iconTarget = Path.Combine(libDir, "app.ico");
Expand Down Expand Up @@ -261,8 +263,9 @@ RuntimeCpu parseMachine(PeNet.Header.Pe.MachineType machine)
Log.Info($"Creating Setup bundle");
var bundleOffset = SetupBundle.CreatePackageBundle(targetSetupExe, newestReleasePath);
Log.Info("Bundle package offset is " + bundleOffset);
options.SignPEFile(targetSetupExe);

List<string> setupFilesToSign = new() { targetSetupExe };

Log.Info($"Setup bundle created at '{targetSetupExe}'.");

// this option is used for debugging a local Setup.exe
Expand All @@ -275,11 +278,13 @@ RuntimeCpu parseMachine(PeNet.Header.Pe.MachineType machine)
if (SquirrelRuntimeInfo.IsWindows) {
bool x64 = options.msi.Equals("x64");
var msiPath = createMsiPackage(targetSetupExe, bundledzp, x64);
options.SignPEFile(msiPath);
setupFilesToSign.Add(msiPath);
} else {
Log.Warn("Unable to create MSI (only supported on windows).");
}
}

options.SignFiles(targetDir.FullName, setupFilesToSign.ToArray());

Log.Info("Done");
}
Expand Down
83 changes: 56 additions & 27 deletions src/Squirrel.CommandLine/Windows/HelperExe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ namespace Squirrel.CommandLine.Windows
internal class HelperExe : HelperFile
{
public static string SetupPath => FindHelperFile("Setup.exe");

public static string UpdatePath
=> FindHelperFile("Update.exe", p => Microsoft.NET.HostModel.AppHost.HostWriter.IsBundle(p, out var _));

public static string StubExecutablePath => FindHelperFile("StubExecutable.exe");

// private so we don't expose paths to internal tools. these should be exposed as a helper function
Expand Down Expand Up @@ -48,46 +50,73 @@ private static bool CheckIsAlreadySigned(string filePath)
}

[SupportedOSPlatform("windows")]
public static void SignPEFilesWithSignTool(string filePath, string signArguments)
public static void SignPEFilesWithSignTool(string rootDir, string[] filePaths, string signArguments, int parallelism)
{
if (CheckIsAlreadySigned(filePath)) return;

List<string> args = new List<string>();
args.Add("sign");
args.AddRange(PlatformUtil.CommandLineToArgvW(signArguments));
args.Add(filePath);
Queue<string> pendingSign = new Queue<string>();

foreach (var f in filePaths) {
if (!CheckIsAlreadySigned(f)) {
// try to find the path relative to rootDir
if (String.IsNullOrEmpty(rootDir)) {
pendingSign.Enqueue(f);
} else {
var partialPath = Utility.NormalizePath(f).Substring(Utility.NormalizePath(rootDir).Length).Trim('/', '\\');
pendingSign.Enqueue(partialPath);
}
} else {
Log.Debug($"'{f}' is already signed, and will not be signed again.");
}
}

var result = PlatformUtil.InvokeProcess(SignToolPath, args, null, CancellationToken.None);
if (result.ExitCode != 0) {
var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********");
throw new Exception(
$"Command failed:\n{cmdWithPasswordHidden}\n\n" +
$"Output was:\n" + result.StdOutput);
} else {
Log.Info("Sign successful: " + result.StdOutput);
if (filePaths.Length != pendingSign.Count) {
var diff = filePaths.Length - pendingSign.Count;
Log.Info($"{pendingSign.Count} files will be signed, {diff} will be skipped because they are already signed.");
}

var totalToSign = pendingSign.Count;
var baseSignArgs = PlatformUtil.CommandLineToArgvW(signArguments);

do {
List<string> args = new List<string>();
args.Add("sign");
args.AddRange(baseSignArgs);
for (int i = Math.Min(pendingSign.Count, parallelism); i > 0; i--) {
args.Add(pendingSign.Dequeue());
}

var result = PlatformUtil.InvokeProcess(SignToolPath, args, rootDir, CancellationToken.None);
if (result.ExitCode != 0) {
var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********");
Log.Debug($"Signing command failed: {cmdWithPasswordHidden}");
throw new Exception(
$"Signing command failed. Specify --verbose argument to print signing command.\n\n" +
$"Output was:\n" + result.StdOutput);
}

Log.Info($"Signed {totalToSign - pendingSign.Count}/{totalToSign} successfully.\r\n" + result.StdOutput);

} while (pendingSign.Count > 0);
}

[SupportedOSPlatform("windows")]
public static void SignPEFilesWithTemplate(string filePath, string signTemplate)
public static void SignPEFileWithTemplate(string filePath, string signTemplate)
{
if (CheckIsAlreadySigned(filePath)) return;
if (SquirrelRuntimeInfo.IsWindows && CheckIsAlreadySigned(filePath)) {
Log.Debug($"'{filePath}' is already signed, and will not be signed again.");
return;
}

var command = signTemplate.Replace("\"{{file}}\"", "{{file}}").Replace("{{file}}", $"\"{filePath}\"");
var args = PlatformUtil.CommandLineToArgvW(command);

if (args.Length < 2)
throw new OptionValidationException("Invalid signing template");

var result = PlatformUtil.InvokeProcess(args[0], args.Skip(1), null, CancellationToken.None);
var result = PlatformUtil.InvokeProcess(command, null, null, CancellationToken.None);
if (result.ExitCode != 0) {
var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********");
Log.Debug($"Signing command failed: {cmdWithPasswordHidden}");
throw new Exception(
$"Command failed:\n{cmdWithPasswordHidden}\n\n" +
$"Signing command failed. Specify --verbose argument to print signing command.\n\n" +
$"Output was:\n" + result.StdOutput);
} else {
Log.Info("Sign successful: " + result.StdOutput);
}

Log.Info("Sign successful: " + result.StdOutput);
}

[SupportedOSPlatform("windows")]
Expand Down Expand Up @@ -151,4 +180,4 @@ public static void SetPEVersionBlockFromPackageInfo(string exePath, NuGet.IPacka
Utility.Retry(() => InvokeAndThrowIfNonZero(RceditPath, args, null));
}
}
}
}
50 changes: 35 additions & 15 deletions src/Squirrel.CommandLine/Windows/Options.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Versioning;
Expand All @@ -12,27 +12,43 @@ internal class SigningOptions : BaseOptions
{
public string signParams { get; private set; }
public string signTemplate { get; private set; }
public bool signSkipDll { get; private set; }
public int signParallel { get; private set; } = 10;

public SigningOptions()
{
if (SquirrelRuntimeInfo.IsWindows) {
Add("n=|signParams=", "Sign files via SignTool.exe using these {PARAMETERS}",
Add("n=|signParams=", "Sign files via signtool.exe using these {PARAMETERS}",
v => signParams = v);
Add("signTemplate=", "Use a custom signing {COMMAND}. '{{{{file}}}}' will be replaced by the path of the file to sign.",
v => signTemplate = v);
Add("signSkipDll", "Only signs EXE files, and skips signing DLL files.", v => signSkipDll = true);
Add("signParallel=", "The number of files to sign in each call to signtool.exe",
v => signParallel = ParseIntArg(nameof(signParallel), v, 1, 1000));
}

Add("signTemplate=", "Use a custom signing {COMMAND}. '{{{{file}}}}' will be replaced by the path of the file to sign.", v => signTemplate = v);
}

public void SignPEFile(string filePath)
public void SignFiles(string rootDir, params string[] filePaths)
{
if (String.IsNullOrEmpty(signParams) && String.IsNullOrEmpty(signTemplate)) {
Log.Debug($"No signing paramaters provided, {filePaths.Length} file(s) will not be signed.");
return;
}

if (!String.IsNullOrEmpty(signTemplate)) {
Log.Info($"Preparing to sign {filePaths.Length} files with custom signing template");
foreach (var f in filePaths) {
HelperExe.SignPEFileWithTemplate(f, signTemplate);
}
return;
}

// signtool.exe does not work if we're not on windows.
if (!SquirrelRuntimeInfo.IsWindows) return;

if (!String.IsNullOrEmpty(signParams)) {
HelperExe.SignPEFilesWithSignTool(filePath, signParams);
} else if (!String.IsNullOrEmpty(signTemplate)) {
HelperExe.SignPEFilesWithTemplate(filePath, signTemplate);
} else {
Log.Debug($"No signing paramaters, file will not be signed: '{filePath}'.");
Log.Info($"Preparing to sign {filePaths.Length} files with embedded signtool.exe with parallelism of {signParallel}");
HelperExe.SignPEFilesWithSignTool(rootDir, filePaths, signParams, signParallel);
}
}

Expand All @@ -43,7 +59,8 @@ public override void Validate()
}

if (!String.IsNullOrEmpty(signTemplate) && !signTemplate.Contains("{{file}}")) {
throw new OptionValidationException($"Argument 'signTemplate': Must contain '{{{{file}}}}' in template string (replaced with the file to sign). Current value is '{signTemplate}'");
throw new OptionValidationException(
$"Argument 'signTemplate': Must contain '{{{{file}}}}' in template string (replaced with the file to sign). Current value is '{signTemplate}'");
}
}
}
Expand All @@ -70,7 +87,7 @@ public ReleasifyOptions()
Add("addSearchPath=", "Add additional search directories when looking for helper exe's such as Setup.exe, Update.exe, etc",
HelperExe.AddSearchPath, true);
Add("debugSetupExe=", "Uses the Setup.exe at this {PATH} to create the bundle, and then replaces it with the bundle. " +
"Used for locally debugging Setup.exe with a real bundle attached.", v => debugSetupExe = v, true);
"Used for locally debugging Setup.exe with a real bundle attached.", v => debugSetupExe = v, true);

// public arguments
InsertAt(1, "p=|package=", "{PATH} to a '.nupkg' package to releasify", v => package = v);
Expand Down Expand Up @@ -128,7 +145,10 @@ public PackOptions()

// hidden arguments
Add("packName=", "The name of the package to create",
v => { packId = v; Log.Warn("--packName is deprecated. Use --packId instead."); }, true);
v => {
packId = v;
Log.Warn("--packName is deprecated. Use --packId instead.");
}, true);
Add("packDirectory=", "", v => packDirectory = v, true);

// public arguments, with indexes so they appear before ReleasifyOptions
Expand All @@ -151,4 +171,4 @@ public override void Validate()
base.ValidateInternal(false);
}
}
}
}
10 changes: 6 additions & 4 deletions src/Squirrel/Internal/PlatformUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -435,14 +435,16 @@ private static (ProcessStartInfo StartInfo, string CommandDisplayString) CreateP
{
var psi = CreateProcessStartInfo(fileName, workingDirectory);

string displayArgs;
string displayArgs = "";

if (args != null) {
#if NET5_0_OR_GREATER
foreach (var a in args) psi.ArgumentList.Add(a);
displayArgs = $"['{String.Join("', '", args)}']";
foreach (var a in args) psi.ArgumentList.Add(a);
displayArgs = $"['{String.Join("', '", args)}']";
#else
psi.Arguments = displayArgs = SquirrelRuntimeInfo.IsWindows ? ArgsToCommandLine(args) : ArgsToCommandLineUnix(args);
psi.Arguments = displayArgs = SquirrelRuntimeInfo.IsWindows ? ArgsToCommandLine(args) : ArgsToCommandLineUnix(args);
#endif
}

return (psi, fileName + " " + displayArgs);
}
Expand Down

0 comments on commit 5f9f594

Please sign in to comment.