diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ba81862
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+bin/
+obj/
+.vs/
diff --git a/App.config b/App.config
new file mode 100644
index 0000000..5754728
--- /dev/null
+++ b/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/IAuthenticodeSignatureTraits.cs b/IAuthenticodeSignatureTraits.cs
new file mode 100644
index 0000000..3625a89
--- /dev/null
+++ b/IAuthenticodeSignatureTraits.cs
@@ -0,0 +1,30 @@
+namespace PowershellScriptTimestamp
+{
+ internal interface IAuthenticodeSignatureTraits
+ {
+ ///
+ /// Code point sequence that is found before every signature
+ ///
+ string SignatureBeginSequence { get; }
+
+ ///
+ /// Code point sequence that is found after every signature
+ ///
+ string SignatureEndSequence { get; }
+
+ ///
+ /// Code point sequence that is found at the beginning of each signature chunk
+ ///
+ string SignatureLineBeginning { get; }
+
+ ///
+ /// Code point sequence that is found at the end of each signature chunk, including the line terminator
+ ///
+ string SignatureLineEnding { get; }
+
+ ///
+ /// Number of base64 characters found on each signature chunk.
+ ///
+ int SignatureCharsPerLine { get; }
+ }
+}
diff --git a/LICENSE b/LICENSE.txt
similarity index 100%
rename from LICENSE
rename to LICENSE.txt
diff --git a/PowershellScriptTimestamp.csproj b/PowershellScriptTimestamp.csproj
new file mode 100644
index 0000000..8d06e7a
--- /dev/null
+++ b/PowershellScriptTimestamp.csproj
@@ -0,0 +1,58 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {DA9D348A-026B-4217-B507-8D6A11B73979}
+ Exe
+ PowershellScriptTimestamp
+ PowershellScriptTimestamp
+ v4.7.2
+ 512
+ true
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PowershellScriptTimestamp.sln b/PowershellScriptTimestamp.sln
new file mode 100644
index 0000000..71c6117
--- /dev/null
+++ b/PowershellScriptTimestamp.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.6.33829.357
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowershellScriptTimestamp", "PowershellScriptTimestamp.csproj", "{DA9D348A-026B-4217-B507-8D6A11B73979}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {DA9D348A-026B-4217-B507-8D6A11B73979}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DA9D348A-026B-4217-B507-8D6A11B73979}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DA9D348A-026B-4217-B507-8D6A11B73979}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DA9D348A-026B-4217-B507-8D6A11B73979}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {CA8E06A2-FA4E-4CAC-9DC2-79FCE76F2931}
+ EndGlobalSection
+EndGlobal
diff --git a/PowershellSignatureTraits.cs b/PowershellSignatureTraits.cs
new file mode 100644
index 0000000..102ad46
--- /dev/null
+++ b/PowershellSignatureTraits.cs
@@ -0,0 +1,30 @@
+namespace PowershellScriptTimestamp
+{
+ internal class PowershellSignatureTraits : IAuthenticodeSignatureTraits
+ {
+ ///
+ /// Code point sequence that is found before every signature
+ ///
+ string IAuthenticodeSignatureTraits.SignatureBeginSequence => "\r\n# SIG # Begin signature block\r\n";
+
+ ///
+ /// Code point sequence that is found after every signature
+ ///
+ string IAuthenticodeSignatureTraits.SignatureEndSequence => "\r\n# SIG # End signature block\r\n";
+
+ ///
+ /// Code point sequence that is found at the beginning of each signature chunk
+ ///
+ string IAuthenticodeSignatureTraits.SignatureLineBeginning => "# ";
+
+ ///
+ /// Code point sequence that is found at the end of each signature chunk, including the line terminator
+ ///
+ string IAuthenticodeSignatureTraits.SignatureLineEnding => "\r\n";
+
+ ///
+ /// Number of base64 characters found on each signature chunk.
+ ///
+ int IAuthenticodeSignatureTraits.SignatureCharsPerLine => 64;
+ }
+}
diff --git a/ProcessExecutionResult.cs b/ProcessExecutionResult.cs
new file mode 100644
index 0000000..301777e
--- /dev/null
+++ b/ProcessExecutionResult.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Diagnostics;
+using System.Text;
+using System.Threading;
+
+namespace PowershellScriptTimestamp
+{
+ internal class ProcessExecutionResult
+ {
+ public string ExecutablePath { get; }
+ public string CommandLineArguments { get; }
+
+ public bool Successful { get; }
+ public int ExitCode { get; }
+
+ public string Stdout { get; }
+ public string Stderr { get; }
+
+
+ public ProcessExecutionResult(string path, string arguments)
+ {
+
+ ExecutablePath = path;
+ CommandLineArguments = arguments;
+
+ var outStringBuilder = new StringBuilder();
+ var errStringBuilder = new StringBuilder();
+ using (var process = new Process())
+ try
+ {
+
+ process.StartInfo.CreateNoWindow = true;
+ process.StartInfo.Arguments = arguments;
+ process.StartInfo.FileName = path;
+ process.StartInfo.RedirectStandardError = true;
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.UseShellExecute = false;
+
+ using (AutoResetEvent stdoutConsumed = new AutoResetEvent(false))
+ using (AutoResetEvent stderrConsumed = new AutoResetEvent(false))
+ {
+ process.OutputDataReceived += (sender, evtArgs) =>
+ {
+ if (evtArgs.Data == null)
+ {
+ stdoutConsumed.Set();
+ }
+ else
+ {
+ outStringBuilder.AppendLine(evtArgs.Data);
+ }
+ };
+ process.ErrorDataReceived += (sender, evtArgs) =>
+ {
+ if (evtArgs.Data == null)
+ {
+ stderrConsumed.Set();
+ }
+ else
+ {
+ errStringBuilder.AppendLine(evtArgs.Data);
+ }
+ };
+
+ Console.WriteLine($"[INFO] About to run process {path} with arguments {arguments}.");
+
+ if (!process.Start())
+ {
+ Console.WriteLine($"[ERROR] Could not start process.");
+ return;
+ }
+
+ try
+ {
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+ process.WaitForExit();
+ stdoutConsumed.WaitOne();
+ stderrConsumed.WaitOne();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ERROR] Could not wait for end of process. Dumping exception.");
+ Console.WriteLine(ex);
+ return;
+ }
+ }
+
+ ExitCode = process.ExitCode;
+ Successful = true;
+
+ return;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ERROR] Could not run process. Dumping exception.");
+ Console.WriteLine(ex);
+ }
+ finally
+ {
+ Stdout = outStringBuilder.ToString();
+ Stderr = errStringBuilder.ToString();
+ }
+ }
+
+ }
+}
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..1d17a26
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PowershellScriptTimestamp
+{
+ internal class Program
+ {
+ private const string ARG_POWERSHELL = "/powershell";
+ private const string ARG_VBSCRIPT = "/vbscript";
+
+ private const string ARG_SIGNTOOL_PATH = "/signtool";
+
+ private const string ARG_SERVER_URI = "/tr";
+ private const string ARG_DIGESTMETHOD = "/td";
+
+ private static string[] ARGS_HELP = new string[] { "/?", "-h", "--help", "/h", "/help" };
+
+ private static void Usage()
+ {
+ Console.WriteLine("Usage:");
+ Console.WriteLine($"\t{AppDomain.CurrentDomain.FriendlyName} [] [] ");
+ Console.WriteLine("");
+ Console.WriteLine($"\t\tfile type : <{ARG_POWERSHELL} | {ARG_VBSCRIPT}>");
+ Console.WriteLine($"\t\tsigntool spec: {ARG_SIGNTOOL_PATH} ");
+ Console.WriteLine($"\t\tserver spec : {ARG_SERVER_URI} ");
+ Console.WriteLine($"\t\tdigest spec : {ARG_DIGESTMETHOD} ");
+ }
+
+ static int Main(string[] args)
+ {
+ TextFileTimestamp timestampObject = null;
+ List fileList = new List();
+ List successfulList = new List();
+ List failedList = new List();
+
+ string timestampServerUri = null;
+ string digestAlgorithm = "sha256";
+ string signToolPath = "signtool.exe";
+
+ int argIndex = 0;
+ while (argIndex < args.Length)
+ {
+ if (ARGS_HELP.Any(spec => args[argIndex].Equals(spec, StringComparison.OrdinalIgnoreCase)))
+ {
+ Usage();
+ return -1;
+ }
+ else if (args[argIndex].Equals(ARG_POWERSHELL, StringComparison.OrdinalIgnoreCase))
+ {
+ timestampObject = new TextFileTimestamp(traits: new PowershellSignatureTraits());
+ }
+ else if (args[argIndex].Equals(ARG_VBSCRIPT, StringComparison.OrdinalIgnoreCase))
+ {
+ timestampObject = new TextFileTimestamp(traits: new VbscriptSignatureTraits());
+ }
+ else if (args[argIndex].Equals(ARG_SERVER_URI, StringComparison.OrdinalIgnoreCase))
+ {
+ argIndex++;
+ if (argIndex >= args.Length)
+ {
+ Console.WriteLine($"[ERROR] Expecting argument for {ARG_SERVER_URI}\n----\n");
+ Usage();
+ return -1;
+ }
+ timestampServerUri = args[argIndex];
+ }
+ else if (args[argIndex].Equals(ARG_DIGESTMETHOD, StringComparison.OrdinalIgnoreCase))
+ {
+ argIndex++;
+ if (argIndex >= args.Length)
+ {
+ Console.WriteLine($"[ERROR] Expecting argument for {ARG_DIGESTMETHOD}\n----\n");
+ Usage();
+ return -1;
+ }
+ digestAlgorithm = args[argIndex];
+ }
+ else if (args[argIndex].Equals(ARG_SIGNTOOL_PATH, StringComparison.OrdinalIgnoreCase))
+ {
+ argIndex++;
+ if (argIndex >= args.Length)
+ {
+ Console.WriteLine($"[ERROR] Expecting argument for {ARG_SIGNTOOL_PATH}\n----\n");
+ Usage();
+ return -1;
+ }
+ signToolPath = args[argIndex];
+ }
+ else
+ {
+ if (!System.IO.File.Exists(args[argIndex]))
+ {
+ Console.WriteLine($"[ERROR] File {args[argIndex]} does not exist. Aborting.");
+ return -1;
+ }
+ fileList.Add(args[argIndex]);
+ }
+ argIndex++;
+ }
+
+ if (timestampObject == null)
+ {
+ Console.WriteLine($"[ERROR] File type unknown, expecting {ARG_POWERSHELL} or {ARG_VBSCRIPT}\n----\n");
+ Usage();
+ return -1;
+ }
+ else if (fileList.Count == 0)
+ {
+ Console.WriteLine($"[ERROR] No input file specified\n----\n");
+ Usage();
+ return -1;
+ }
+ else if(timestampServerUri == null)
+ {
+ Console.WriteLine("[ERROR] No timestamp server URI specified.\n----\n");
+ Usage();
+ return -1;
+ }
+
+ fileList.ForEach(filePath =>
+ {
+ Console.WriteLine($"[INFO] About to timestamp file {filePath}\n\tSigntool: {signToolPath}\n\tTimestamp URI: {timestampServerUri}\n\tDigest algorithm: {digestAlgorithm}");
+ if (timestampObject.TimestampFile(signToolPath, filePath, timestampServerUri, digestAlgorithm) == 0)
+ {
+ successfulList.Add(filePath);
+ }
+ else
+ {
+ Console.WriteLine($"[ERROR] file {filePath} could not be timestamped.");
+ failedList.Add(filePath);
+ }
+ });
+
+ if (successfulList.Count > 0)
+ {
+ Console.WriteLine("The following files were successfully timestamped:");
+ successfulList.ForEach(filePath => Console.WriteLine($"\t* [SUCCESS] {filePath}"));
+ }
+
+ if (failedList.Count > 0)
+ {
+ Console.WriteLine("[ERROR] The following files could not be timestamped:");
+ failedList.ForEach(filePath => Console.WriteLine($"\t* [ERROR] {filePath}"));
+ }
+
+ return 0;
+ }
+ }
+}
diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..e103f37
--- /dev/null
+++ b/Properties/AssemblyInfo.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("PowershellScriptTimestamp")]
+[assembly: AssemblyDescription("Utility to add a timestamp to a powershell script")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Stormshield SAS")]
+[assembly: AssemblyProduct("PowershellScriptTimestamp")]
+[assembly: AssemblyCopyright("Copyright © Stormshield 2023")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+[assembly: ComVisible(false)]
+
+[assembly: Guid("da9d348a-026b-4217-b507-8d6a11b73979")]
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..6063e5c
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,33 @@
+(C) 2023 Stormshield
+
+PowershellScriptTimestamp - Utility to timestamp the last Authenticode signature
+ on a PowerShell (or VBScript) file.
+
+USAGE
+-----
+
+(note: in this section the caret (^) is used as EOL escape character, as in batch files)
+
+For PowerShell files:
+
+PowershellScriptTimestamp.exe ^
+ /powershell ^
+ /uri ^
+ [/digest ] ^
+ [/signtool ] ^
+ file.ps1 [...]
+
+For VBScript files: replace /powershell with /vbscript
+
+Remarks:
+ * should be the URI of a RFC 3161 timestamp server
+ * If the path to signtool.exe is not specified, the program will run signtool.exe and
+ expect it to be resolved using the PATH environment variable
+ * The default digest algorithm value is sha256. Supported values are the same as for
+ signtool.
+
+FAQ
+---
+
+Q. Do you accept pull requests?
+A. They are welcome and will be reviewed.
diff --git a/TextFileTimestamp.cs b/TextFileTimestamp.cs
new file mode 100644
index 0000000..b170130
--- /dev/null
+++ b/TextFileTimestamp.cs
@@ -0,0 +1,326 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace PowershellScriptTimestamp
+{
+ internal class TextFileTimestamp
+ {
+ public TextFileTimestamp(IAuthenticodeSignatureTraits traits)
+ {
+ _traits = traits;
+ }
+
+ private readonly IAuthenticodeSignatureTraits _traits;
+
+ ///
+ /// Indicates the byte format of the signature.
+ ///
+ ///
+ /// Powershell and vbs signature ALWAYS uses CRLF as a line terminator.
+ /// It is advised to always use CRLF for Powershell and vbs scripts.
+ /// Moreover, their signature block is encoded either in pure ASCII, or in UTF-16le.
+ ///
+ private enum ETextFileEncodingType
+ {
+ ///
+ /// The signature is ASCII-encoded
+ ///
+ OneByte,
+ ///
+ /// The signature is UTF-16LE-encoded
+ ///
+ Utf16Le,
+ }
+
+ ///
+ /// Represents a text file of signable format, deassembled
+ ///
+ private class SignedFileParts
+ {
+ ///
+ /// Used when reassembling the script: indicates how to encode the signature
+ ///
+ public ETextFileEncodingType encodingType;
+
+ ///
+ /// Bytes before the signature block, raw (this also can include BOM)
+ ///
+ public byte[] rawBytesBeforeSignature;
+
+ ///
+ /// Binary representation of the signature. This is a byte sequence representing a DER-encoded PKCS#7 bundle.
+ ///
+ public byte[] rawPkcs7SignatureBytes;
+
+ ///
+ /// Bytes after the signature block, raw (always observed to be empty)
+ ///
+ public byte[] rawBytesAfterSignature;
+ }
+
+ ///
+ /// Extract the parts of a signed file such that the signature block can be amended.
+ ///
+ /// Path to the file
+ /// Deassembled file
+ private SignedFileParts DeassembleSignedFile(string filePath)
+ {
+ var deassembledScript = new SignedFileParts();
+ byte[] originalFileBytes = File.ReadAllBytes(filePath);
+
+ deassembledScript.encodingType = ETextFileEncodingType.OneByte;
+ int codeUnitByteLength = 1;
+ if (originalFileBytes.Length > 2 && originalFileBytes[0] == 0xFF && originalFileBytes[1] == 0xFE)
+ {
+ // We have a Byte order mark. The only possible encoding is UTF-16, Little Endian.
+ deassembledScript.encodingType = ETextFileEncodingType.Utf16Le;
+ codeUnitByteLength = 2;
+ }
+
+ byte[] SIGNATURE_BEGIN_SEQUENCE = deassembledScript.encodingType == ETextFileEncodingType.OneByte ?
+ System.Text.Encoding.UTF8.GetBytes(_traits.SignatureBeginSequence) :
+ System.Text.Encoding.Unicode.GetBytes(_traits.SignatureBeginSequence);
+
+ byte[] SIGNATURE_END_SEQUENCE = deassembledScript.encodingType == ETextFileEncodingType.OneByte ?
+ System.Text.Encoding.UTF8.GetBytes(_traits.SignatureEndSequence) :
+ System.Text.Encoding.Unicode.GetBytes(_traits.SignatureEndSequence);
+
+ int signatureBeginOffset = 0;
+ for (bool beginSignatureFound = false; !beginSignatureFound;)
+ {
+ // Non-optimized subarray search algorithm. Could be replaced by Boyer-Moore if performance is necessary.
+ for (int searchPosition = 0; searchPosition < originalFileBytes.Length; searchPosition += codeUnitByteLength)
+ {
+ if (originalFileBytes.Skip(searchPosition).Take(SIGNATURE_BEGIN_SEQUENCE.Length).SequenceEqual(SIGNATURE_BEGIN_SEQUENCE))
+ {
+ beginSignatureFound = true;
+ signatureBeginOffset = searchPosition + SIGNATURE_BEGIN_SEQUENCE.Length;
+ deassembledScript.rawBytesBeforeSignature = originalFileBytes.Take(searchPosition).ToArray();
+ break;
+ }
+ }
+ if (!beginSignatureFound)
+ {
+ if (deassembledScript.encodingType == ETextFileEncodingType.OneByte)
+ {
+ // Maybe the script is UTF-16LE encoded, but does not have a BOM. Retrying.
+ Console.WriteLine($"[WARN] Signature block not found in one-byte encoding. Retrying, assuming UTF-16LE without BOM.");
+ SIGNATURE_BEGIN_SEQUENCE = System.Text.Encoding.Unicode.GetBytes(_traits.SignatureBeginSequence);
+ SIGNATURE_END_SEQUENCE = System.Text.Encoding.Unicode.GetBytes(_traits.SignatureEndSequence);
+ codeUnitByteLength = 2;
+ deassembledScript.encodingType = ETextFileEncodingType.Utf16Le;
+ }
+ else
+ {
+ Console.WriteLine($"[ERROR] Signature block not found.");
+ return null;
+ }
+ }
+ }
+
+ int signatureEndOffset = 0;
+ bool endSignatureFound = false;
+ for (int searchPosition = signatureBeginOffset; searchPosition < originalFileBytes.Length; searchPosition += codeUnitByteLength)
+ {
+ if (originalFileBytes.Skip(searchPosition).Take(SIGNATURE_END_SEQUENCE.Length).SequenceEqual(SIGNATURE_END_SEQUENCE))
+ {
+ endSignatureFound = true;
+ signatureEndOffset = searchPosition;
+ deassembledScript.rawBytesAfterSignature = originalFileBytes.Skip(searchPosition + SIGNATURE_END_SEQUENCE.Length).ToArray();
+ break;
+ }
+ }
+ if (!endSignatureFound)
+ {
+ Console.WriteLine($"[ERROR] End of signature block not found.");
+ return null;
+ }
+
+ try
+ {
+ byte[] signatureSectionAsBytes = originalFileBytes.Skip(signatureBeginOffset).Take(signatureEndOffset - signatureBeginOffset).ToArray();
+
+ string signatureLines = deassembledScript.encodingType == ETextFileEncodingType.OneByte ?
+ System.Text.Encoding.UTF8.GetString(signatureSectionAsBytes) :
+ System.Text.Encoding.Unicode.GetString(signatureSectionAsBytes);
+
+ string wholeSignatureBase64 =
+ new string(signatureLines.Split(separator: new string[] { _traits.SignatureLineEnding }, options: StringSplitOptions.None)
+ .Select(line =>
+ {
+ if (line.StartsWith(_traits.SignatureLineBeginning, StringComparison.Ordinal))
+ {
+ return line.Substring(_traits.SignatureLineBeginning.Length);
+ }
+ return line;
+ })
+ .Aggregate("", (s, line) => s + line)
+ .Where(c =>
+ {
+ return (char.IsLetterOrDigit(c) && c <= 0x7F) // ASCII chiffers and letters
+ || c == '/'
+ || c == '+'
+ || c == '=';
+ })
+ .ToArray());
+
+ deassembledScript.rawPkcs7SignatureBytes = Convert.FromBase64String(wholeSignatureBase64);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("[ERROR] Could not convert signature lines from Base64. Dumping exception.");
+ Console.WriteLine(ex);
+ return null;
+ }
+
+ return deassembledScript;
+ }
+
+ ///
+ /// Split a string in portions of up to code units
+ ///
+ /// String to split
+ /// chunk size
+ /// enumeration of chunks
+ private static IEnumerable ChunkifyString(string stringToSplit, int chunkSize)
+ {
+ int offset = 0;
+ while (offset < stringToSplit.Length)
+ {
+ yield return stringToSplit.Substring(offset, Math.Min(chunkSize, stringToSplit.Length - offset));
+ offset += chunkSize;
+ }
+ yield break;
+ }
+
+ ///
+ /// Rewrite signed file with PKCS#7 block modified
+ ///
+ /// Deassembled file
+ /// Output file path
+ private bool ReassembleSignedFileFromParts(SignedFileParts fileParts, string outputPath)
+ {
+ var base64EncodedSignature = System.Convert.ToBase64String(fileParts.rawPkcs7SignatureBytes);
+
+ // signature lines consist of comment mark, space, and up to 64 base64 characters.
+ IEnumerable signatureLines = ChunkifyString(base64EncodedSignature, _traits.SignatureCharsPerLine).Select(line => _traits.SignatureLineBeginning + line);
+
+ string signatureBlock = _traits.SignatureBeginSequence +
+ String.Join(_traits.SignatureLineEnding, signatureLines) +
+ _traits.SignatureEndSequence;
+
+ byte[] signatureBytes = fileParts.encodingType == ETextFileEncodingType.OneByte ?
+ System.Text.Encoding.UTF8.GetBytes(signatureBlock) :
+ System.Text.Encoding.Unicode.GetBytes(signatureBlock);
+
+ try
+ {
+ File.WriteAllBytes(
+ outputPath,
+ fileParts.rawBytesBeforeSignature
+ .Concat(signatureBytes)
+ .Concat(fileParts.rawBytesAfterSignature)
+ .ToArray());
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ERROR] Could not overwrite script {outputPath}. Dumping exception.");
+ Console.WriteLine(ex);
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool TimestampPKCS7File(string signToolPath, string pkcs7FilePath, string timestampServerUri, string digestAlgorithm)
+ {
+ var signToolResult = new ProcessExecutionResult(
+ signToolPath,
+ string.Format("timestamp /v /tr \"{0}\" /td {1} /p7 \"{2}\"", timestampServerUri, digestAlgorithm, pkcs7FilePath)
+ );
+
+ if (!signToolResult.Successful)
+ {
+ Console.WriteLine($"[ERROR] Could not timestamp PKCS#7 file ${pkcs7FilePath}.");
+ return false;
+ }
+ return signToolResult.ExitCode == 0;
+ }
+
+ ///
+ /// Timestamp a signed text file.
+ ///
+ /// Path to signtool.exe
+ /// Path to the file to sign
+ /// URI of a RFC 3161 timestamp server
+ /// Method for digest, passed to signtool timestamp subcommand
+ ///
+ public int TimestampFile(string signToolPath, string filePath, string timestampServerUri, string digestAlgorithm)
+ {
+ string temporaryPkcs7SignatureBlockPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".p7");
+ try
+ {
+ SignedFileParts script = DeassembleSignedFile(filePath);
+ if (script == null)
+ {
+ Console.WriteLine($"[ERROR] Could not deassemble script ${filePath}.");
+ return -1;
+ }
+
+ try
+ {
+ File.WriteAllBytes(temporaryPkcs7SignatureBlockPath, script.rawPkcs7SignatureBytes);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ERROR] Could not extract signature PKCS#7 block from file ${filePath} to file {temporaryPkcs7SignatureBlockPath}. Dumping exception.");
+ Console.WriteLine(ex);
+ return -1;
+ }
+
+ if (!TimestampPKCS7File(signToolPath, temporaryPkcs7SignatureBlockPath, timestampServerUri, digestAlgorithm))
+ {
+ Console.WriteLine($"[ERROR] Could not timestamp PKCS#7 file ${temporaryPkcs7SignatureBlockPath} with URI {timestampServerUri}.");
+ return -1;
+ }
+
+ try
+ {
+ script.rawPkcs7SignatureBytes = File.ReadAllBytes(temporaryPkcs7SignatureBlockPath);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ERROR] Could not read file ${temporaryPkcs7SignatureBlockPath} after timestamp operation. Dumping exception.");
+ Console.WriteLine(ex);
+ return -1;
+ }
+
+ if (!ReassembleSignedFileFromParts(script, filePath))
+ {
+ Console.WriteLine($"[ERROR] Could not re-assemble file ${filePath} with signature from file {temporaryPkcs7SignatureBlockPath}.");
+ return -1;
+ }
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ERROR] An unhandle error occurred while timestamping file {filePath}. Dumping exception.");
+ Console.WriteLine(ex);
+ return -1;
+ }
+ finally
+ {
+ try
+ {
+ File.Delete(temporaryPkcs7SignatureBlockPath);
+ }
+ catch
+ {
+ Console.WriteLine($"[WARN] The temporary file {temporaryPkcs7SignatureBlockPath} could not be deleted.");
+ }
+ }
+ }
+ }
+}
diff --git a/VbscriptSignatureTraits.cs b/VbscriptSignatureTraits.cs
new file mode 100644
index 0000000..6ea8e41
--- /dev/null
+++ b/VbscriptSignatureTraits.cs
@@ -0,0 +1,30 @@
+namespace PowershellScriptTimestamp
+{
+ internal class VbscriptSignatureTraits : IAuthenticodeSignatureTraits
+ {
+ ///
+ /// Code point sequence that is found before every signature
+ ///
+ string IAuthenticodeSignatureTraits.SignatureBeginSequence => "\r\n'' SIG '' Begin signature block\r\n";
+
+ ///
+ /// Code point sequence that is found after every signature
+ ///
+ string IAuthenticodeSignatureTraits.SignatureEndSequence => "\r\n'' SIG '' End signature block\r\n";
+
+ ///
+ /// Code point sequence that is found at the beginning of each signature chunk
+ ///
+ string IAuthenticodeSignatureTraits.SignatureLineBeginning => "'' SIG '' ";
+
+ ///
+ /// Code point sequence that is found at the end of each signature chunk, including the line terminator
+ ///
+ string IAuthenticodeSignatureTraits.SignatureLineEnding => "\r\n";
+
+ ///
+ /// Number of base64 characters found on each signature chunk.
+ ///
+ int IAuthenticodeSignatureTraits.SignatureCharsPerLine => 44;
+ }
+}