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; + } +}