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

Commit

Permalink
Support custom signing commands (see #29)
Browse files Browse the repository at this point in the history
  • Loading branch information
caesay committed Jan 17, 2022
1 parent a67ae3e commit edda9c8
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 52 deletions.
37 changes: 2 additions & 35 deletions src/Squirrel/Internal/HelperExe.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.IO;
using System.Linq;
Expand All @@ -22,10 +22,10 @@ internal static class HelperExe
public static string SingleFileHostPath => FindHelperFile("singlefilehost.exe");
public static string WixTemplatePath => FindHelperFile("template.wxs");
public static string SevenZipPath => FindHelperFile("7z.exe");
public static string SignToolPath => FindHelperFile("signtool.exe");

// private so we don't expose paths to internal tools. these should be exposed as a helper function
private static string RceditPath => FindHelperFile("rcedit.exe");
private static string SignToolPath => FindHelperFile("signtool.exe");
private static string WixCandlePath => FindHelperFile("candle.exe");
private static string WixLightPath => FindHelperFile("light.exe");

Expand Down Expand Up @@ -160,38 +160,5 @@ public static async Task SetPEVersionBlockFromPackageInfo(string exePath, NuGet.
Console.WriteLine(processResult.StdOutput);
}
}

public static async Task SignPEFile(string signPath, string exePath, string signingOpts)
{
if (String.IsNullOrEmpty(signingOpts)) {
Log.Debug("{0} was not signed.", exePath);
return;
}

try {
if (AuthenticodeTools.IsTrusted(exePath)) {
Log.Info("{0} is already signed, skipping...", exePath);
return;
}
} catch (Exception ex) {
Log.ErrorException("Failed to determine signing status for " + exePath, ex);
}

signPath = signPath ?? SignToolPath;

Log.Info("About to sign {0}", exePath);

var psi = Utility.CreateProcessStartInfo(signPath, $"sign {signingOpts} \"{exePath}\"");
var processResult = await Utility.InvokeProcessUnsafeAsync(psi, CancellationToken.None).ConfigureAwait(false);

if (processResult.ExitCode != 0) {
var optsWithPasswordHidden = new Regex(@"/p\s+\w+").Replace(signingOpts, "/p ********");
var msg = String.Format("Failed to sign, command invoked was: '{0} sign {1} {2}'\r\n{3}",
SignToolPath, optsWithPasswordHidden, exePath, processResult.StdOutput);
throw new Exception(msg);
} else {
Log.Info("Sign successful: " + processResult.StdOutput);
}
}
}
}
28 changes: 27 additions & 1 deletion src/Squirrel/Internal/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ public static T Retry<T>(this Func<T> block, int retries = 2, int retryDelay = 2
/// <remarks>
/// The string is only valid for passing directly to a process. If the target process is invoked by passing the
/// process name + arguments to cmd.exe then further escaping is required, to counteract cmd.exe's interpretation
/// of additional special characters. See CommandRunner.cs-EscapeCmdExeMetachars.</remarks>
/// of additional special characters. See <see cref="EscapeCmdExeMetachars"/>.</remarks>
public static string ArgsToCommandLine(IEnumerable<string> args)
{
var sb = new StringBuilder();
Expand Down Expand Up @@ -220,6 +220,32 @@ public static string ArgsToCommandLine(IEnumerable<string> args)
}
private static readonly char[] _cmdChars = new[] { ' ', '"', '\n', '\t', '\v' };

/// <summary>
/// Escapes all cmd.exe meta-characters by prefixing them with a ^. See <see cref="ArgsToCommandLine"/> for more
/// information.</summary>
public static string EscapeCmdExeMetachars(string command)
{
var result = new StringBuilder();
foreach (var ch in command) {
switch (ch) {
case '(':
case ')':
case '%':
case '!':
case '^':
case '"':
case '<':
case '>':
case '&':
case '|':
result.Append('^');
break;
}
result.Append(ch);
}
return result.ToString();
}

/// <summary>
/// This function will escape command line arguments such that CommandLineToArgvW is guarenteed to produce the same output as the 'args' parameter.
/// It also will automatically execute wine if trying to run an exe while not on windows.
Expand Down
78 changes: 69 additions & 9 deletions src/SquirrelCli/Options.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Squirrel;
Expand All @@ -22,12 +24,69 @@ public BaseOptions()
}
}

internal class ReleasifyOptions : BaseOptions
internal class SigningOptions : BaseOptions
{
public string signParams { get; private set; }
public string signTemplate { get; private set; }

public SigningOptions()
{
Add("n=|signParams=", "Sign files via SignTool.exe using these parameters",
v => signParams = v);
Add("signTemplate=", "Use an entirely custom signing command. '{{{{file}}}}' will be replaced by the path of the file to sign.",
v => signTemplate = v);
}

public override void Validate()
{
if (!String.IsNullOrEmpty(signParams) && !String.IsNullOrEmpty(signTemplate)) {
throw new OptionValidationException($"Cannot use 'signParams' and 'signTemplate' options together, please choose one or the other.");
}
}

public void SignPEFile(string filePath)
{
try {
if (AuthenticodeTools.IsTrusted(filePath)) {
Log.Debug("'{0}' is already signed, skipping...", filePath);
return;
}
} catch (Exception ex) {
Log.ErrorException("Failed to determine signing status for " + filePath, ex);
}

string cmd;
ProcessStartInfo psi;
if (!String.IsNullOrEmpty(signParams)) {
// use embedded signtool.exe with provided parameters
cmd = $"sign {signParams} \"{filePath}\"";
psi = Utility.CreateProcessStartInfo(HelperExe.SignToolPath, cmd);
cmd = "signtool.exe " + cmd;
} else if (!String.IsNullOrEmpty(signTemplate)) {
// escape custom sign command and pass it to cmd.exe
cmd = signTemplate.Replace("\"{{file}}\"", "{{file}}").Replace("{{file}}", $"\"{filePath}\"");
psi = Utility.CreateProcessStartInfo("cmd", $"/c {Utility.EscapeCmdExeMetachars(cmd)}");
} else {
Log.Debug("{0} was not signed. (skipped; no signing parameters)", filePath);
return;
}

var processResult = Utility.InvokeProcessUnsafeAsync(psi, CancellationToken.None)
.ConfigureAwait(false).GetAwaiter().GetResult();

if (processResult.ExitCode != 0) {
var cmdWithPasswordHidden = new Regex(@"/p\s+\w+").Replace(cmd, "/p ********");
throw new Exception("Signing command failed: \n > " + cmdWithPasswordHidden + "\n" + processResult.StdOutput);
} else {
Log.Info("Sign successful: " + processResult.StdOutput);
}
}
}

internal class ReleasifyOptions : SigningOptions
{
public string package { get; set; }
public string baseUrl { get; private set; }
public string signParams { get; private set; }
public string signToolPath { get; private set; }
public string framework { get; private set; }
public string splashImage { get; private set; }
public string updateIcon { get; private set; }
Expand All @@ -42,19 +101,18 @@ public ReleasifyOptions()
// hidden arguments
Add("b=|baseUrl=", "Provides a base URL to prefix the RELEASES file packages with", v => baseUrl = v, true);
Add("allowUnaware", "Allows building packages without a SquirrelAwareApp (disabled by default)", v => allowUnaware = true, true);
Add("addSearchPath=", "Add additional search directories when looking for helper exe's such as Setup.exe, Update.exe, etc", v => HelperExe.AddSearchPath(v), true);
Add("addSearchPath=", "Add additional search directories when looking for helper exe's such as Setup.exe, Update.exe, etc",
v => HelperExe.AddSearchPath(v), true);

// public arguments
Add("p=|package=", "Path to a '.nupkg' package to releasify", v => package = v);
InsertAt(1, "p=|package=", "Path to a '.nupkg' package to releasify", v => package = v);
Add("noDelta", "Skip the generation of delta packages", v => noDelta = true);
Add("f=|framework=", "List of required runtimes to install during setup -\nexample: 'net6,vcredist143'", v => framework = v);
Add("s=|splashImage=", "Splash image to be displayed during installation", v => splashImage = v);
Add("i=|icon=", ".ico to be used for Setup.exe and Update.exe",
(v) => { updateIcon = v; setupIcon = v; });
Add("appIcon=", ".ico to be used in the 'Apps and Features' list", v => appIcon = v);
Add("msi=", "Compiles a .msi machine-wide deployment tool.\nThis value must be either 'x86' 'x64'", v => msi = v.ToLower());
Add("n=|signParams=", "Sign files via SignTool.exe using these parameters", v => signParams = v);
Add("signToolPath=", "Use a custom signing binary (eg AzureSignTool.exe)", v => signToolPath = v);
Add("noDelta", "Skip the generation of delta packages", v => noDelta = true);
}

public override void Validate()
Expand All @@ -78,6 +136,8 @@ protected virtual void ValidateInternal(bool checkPackage)
if (!String.IsNullOrEmpty(msi))
if (!msi.Equals("x86") && !msi.Equals("x64"))
throw new OptionValidationException($"Argument 'msi': File must be either 'x86' or 'x64'. Actual value was '{msi}'.");

base.Validate();
}
}

Expand Down
11 changes: 4 additions & 7 deletions src/SquirrelCli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,6 @@ static void Releasify(ReleasifyOptions options)
Directory.CreateDirectory(targetDir);
}

var signingOpts = options.signParams;
var signToolPath = options.signToolPath;
var package = options.package;
var baseUrl = options.baseUrl;
var generateDeltas = !options.noDelta;
Expand Down Expand Up @@ -170,7 +168,7 @@ static void Releasify(ReleasifyOptions options)
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
HelperExe.SignPEFile(signToolPath, updatePath, signingOpts).Wait();
options.SignPEFile(updatePath);

// copy input package to target output directory
var di = new DirectoryInfo(targetDir);
Expand Down Expand Up @@ -219,7 +217,7 @@ static void Releasify(ReleasifyOptions options)
// sign all exe's in this package
new DirectoryInfo(pkgPath).GetAllFilesRecursively()
.Where(x => Utility.FileIsLikelyPEImage(x.Name))
.ForEachAsync(x => HelperExe.SignPEFile(signToolPath, x.FullName, signingOpts))
.ForEachAsync(x => options.SignPEFile(x.FullName))
.Wait();
// copy Update.exe into package, so it can also be updated in both full/delta packages
Expand Down Expand Up @@ -296,13 +294,12 @@ static void Releasify(ReleasifyOptions options)
if (backgroundGif != null) infosave.SplashImageBytes = File.ReadAllBytes(backgroundGif);

infosave.WriteToFile(targetSetupExe);

HelperExe.SignPEFile(signToolPath, targetSetupExe, signingOpts).Wait();
options.SignPEFile(targetSetupExe);

if (!String.IsNullOrEmpty(options.msi)) {
bool x64 = options.msi.Equals("x64");
var msiPath = createMsiPackage(targetSetupExe, new ZipPackage(package), x64).Result;
HelperExe.SignPEFile(signToolPath, msiPath, signingOpts).Wait();
options.SignPEFile(msiPath);
}

Log.Info("Done");
Expand Down

0 comments on commit edda9c8

Please sign in to comment.