From 5fd804165587b3b6d2bd6b9844dcb3fa55a4a305 Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Fri, 25 Feb 2022 22:24:42 -0800 Subject: Support certs on remote payloads and generate them from burn subcommand Bring back Authenticode certificate validation but only on Exe and Msu remote payloads. Move the generation of remote payload XML to a subcommand of the "burn command". --- .../Symbols/WixBundlePayloadSymbol.cs | 16 ++ ...Toolset.Converters.Symbolizer.v3.ncrunchproject | 7 + .../Bundles/CreateBurnManifestCommand.cs | 11 +- src/wix/WixToolset.Core.Burn/BurnBackendErrors.cs | 6 + .../CommandLine/BurnCommand.cs | 4 + .../CommandLine/RemotePayloadSubcommand.cs | 272 +++++++++++++++++++++ .../WixToolset.Core.Native/CertificateHashes.cs | 83 +++++++ src/wix/WixToolset.Core.Native/WixNativeExe.cs | 2 +- src/wix/WixToolset.Core/Compile/CompilerPayload.cs | 89 +++++-- src/wix/WixToolset.Core/Compiler_Bundle.cs | 14 ++ src/wix/heat/PayloadHarvester.cs | 129 ---------- src/wix/heat/UtilHeatExtension.cs | 8 - ...setTest.Converters.Symbolizer.v3.ncrunchproject | 3 + .../CertificateHashesFixture.cs | 58 +++++ .../WixToolsetTest.Core.Native/TestData/test.cab | Bin 115 -> 8027 bytes .../PackagePayloadFixture.cs | 122 ++++++++- .../RemotePayloadFixture.cs | 172 +++++++++++++ .../TestData/.Data/Windows8.1-KB2937592-x86.msu | Bin 0 -> 309544 bytes .../PackagePayload/SpecifiedCertificate.wxs | 10 + ...cifiedCertificatePublicKeyWithoutThumbprint.wxs | 10 + .../SpecifiedHashAndCertificatePublicKey.wxs | 10 + .../SpecifiedSourceFileAndCertificatePublicKey.wxs | 10 + ...SpecifiedSourceFileAndCertificateThumbprint.wxs | 10 + .../TestData/RemotePayload/a.dat | 1 + .../TestData/RemotePayload/subfolder/b.dat | 1 + .../TestData/RemotePayload/subfolder/c.dat | 1 + src/wix/test/WixToolsetTest.Heat/PayloadTests.cs | 66 ----- .../TestData/.Data/Windows8.1-KB2937592-x86.msu | Bin 309544 -> 0 bytes .../WixToolsetTest.Heat/TestData/.Data/burn.exe | Bin 463360 -> 0 bytes .../Payload/HarvestedExePackagePayload.wxs | 6 - .../Payload/HarvestedMsuPackagePayload.wxs | 6 - .../WixToolsetTest.Heat/WixToolsetTest.Heat.csproj | 4 - src/wix/wixnative/certhashes.cpp | 138 +++++++++++ src/wix/wixnative/precomp.h | 4 + src/wix/wixnative/wixnative.cpp | 4 + src/wix/wixnative/wixnative.vcxproj | 3 +- 36 files changed, 1033 insertions(+), 247 deletions(-) create mode 100644 src/wix/WixToolset.Converters.Symbolizer/WixToolset.Converters.Symbolizer.v3.ncrunchproject create mode 100644 src/wix/WixToolset.Core.Burn/CommandLine/RemotePayloadSubcommand.cs create mode 100644 src/wix/WixToolset.Core.Native/CertificateHashes.cs delete mode 100644 src/wix/heat/PayloadHarvester.cs create mode 100644 src/wix/test/WixToolsetTest.Core.Native/CertificateHashesFixture.cs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/RemotePayloadFixture.cs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/.Data/Windows8.1-KB2937592-x86.msu create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedCertificate.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedCertificatePublicKeyWithoutThumbprint.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedHashAndCertificatePublicKey.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedSourceFileAndCertificatePublicKey.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedSourceFileAndCertificateThumbprint.wxs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/a.dat create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/subfolder/b.dat create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/subfolder/c.dat delete mode 100644 src/wix/test/WixToolsetTest.Heat/PayloadTests.cs delete mode 100644 src/wix/test/WixToolsetTest.Heat/TestData/.Data/Windows8.1-KB2937592-x86.msu delete mode 100644 src/wix/test/WixToolsetTest.Heat/TestData/.Data/burn.exe delete mode 100644 src/wix/test/WixToolsetTest.Heat/TestData/Payload/HarvestedExePackagePayload.wxs delete mode 100644 src/wix/test/WixToolsetTest.Heat/TestData/Payload/HarvestedMsuPackagePayload.wxs create mode 100644 src/wix/wixnative/certhashes.cpp diff --git a/src/api/wix/WixToolset.Data/Symbols/WixBundlePayloadSymbol.cs b/src/api/wix/WixToolset.Data/Symbols/WixBundlePayloadSymbol.cs index 82b75285..be581fb3 100644 --- a/src/api/wix/WixToolset.Data/Symbols/WixBundlePayloadSymbol.cs +++ b/src/api/wix/WixToolset.Data/Symbols/WixBundlePayloadSymbol.cs @@ -26,6 +26,8 @@ namespace WixToolset.Data new IntermediateFieldDefinition(nameof(WixBundlePayloadSymbolFields.LayoutOnly), IntermediateFieldType.Bool), new IntermediateFieldDefinition(nameof(WixBundlePayloadSymbolFields.Packaging), IntermediateFieldType.Number), new IntermediateFieldDefinition(nameof(WixBundlePayloadSymbolFields.ParentPackagePayloadRef), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(WixBundlePayloadSymbolFields.CertificatePublicKey), IntermediateFieldType.String), + new IntermediateFieldDefinition(nameof(WixBundlePayloadSymbolFields.CertificateThumbprint), IntermediateFieldType.String), }, typeof(WixBundlePayloadSymbol)); } @@ -53,6 +55,8 @@ namespace WixToolset.Data.Symbols LayoutOnly, Packaging, ParentPackagePayloadRef, + CertificatePublicKey, + CertificateThumbprint, } public class WixBundlePayloadSymbol : IntermediateSymbol @@ -162,5 +166,17 @@ namespace WixToolset.Data.Symbols get => (string)this.Fields[(int)WixBundlePayloadSymbolFields.ParentPackagePayloadRef]; set => this.Set((int)WixBundlePayloadSymbolFields.ParentPackagePayloadRef, value); } + + public string CertificatePublicKey + { + get => (string)this.Fields[(int)WixBundlePayloadSymbolFields.CertificatePublicKey]; + set => this.Set((int)WixBundlePayloadSymbolFields.CertificatePublicKey, value); + } + + public string CertificateThumbprint + { + get => (string)this.Fields[(int)WixBundlePayloadSymbolFields.CertificateThumbprint]; + set => this.Set((int)WixBundlePayloadSymbolFields.CertificateThumbprint, value); + } } } diff --git a/src/wix/WixToolset.Converters.Symbolizer/WixToolset.Converters.Symbolizer.v3.ncrunchproject b/src/wix/WixToolset.Converters.Symbolizer/WixToolset.Converters.Symbolizer.v3.ncrunchproject new file mode 100644 index 00000000..cd54b69b --- /dev/null +++ b/src/wix/WixToolset.Converters.Symbolizer/WixToolset.Converters.Symbolizer.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + LostReference + + + \ No newline at end of file diff --git a/src/wix/WixToolset.Core.Burn/Bundles/CreateBurnManifestCommand.cs b/src/wix/WixToolset.Core.Burn/Bundles/CreateBurnManifestCommand.cs index c8f10a71..af45e736 100644 --- a/src/wix/WixToolset.Core.Burn/Bundles/CreateBurnManifestCommand.cs +++ b/src/wix/WixToolset.Core.Burn/Bundles/CreateBurnManifestCommand.cs @@ -672,7 +672,16 @@ namespace WixToolset.Core.Burn.Bundles writer.WriteAttributeString("Id", payload.Id.Id); writer.WriteAttributeString("FilePath", payload.Name); writer.WriteAttributeString("FileSize", payload.FileSize.Value.ToString(CultureInfo.InvariantCulture)); - writer.WriteAttributeString("Hash", payload.Hash); + + if (!String.IsNullOrEmpty(payload.CertificatePublicKey) && !String.IsNullOrEmpty(payload.CertificateThumbprint)) + { + writer.WriteAttributeString("CertificateRootPublicKeyIdentifier", payload.CertificatePublicKey); + writer.WriteAttributeString("CertificateRootThumbprint", payload.CertificateThumbprint); + } + else + { + writer.WriteAttributeString("Hash", payload.Hash); + } if (payload.LayoutOnly) { diff --git a/src/wix/WixToolset.Core.Burn/BurnBackendErrors.cs b/src/wix/WixToolset.Core.Burn/BurnBackendErrors.cs index fd6eb093..74dc4cd5 100644 --- a/src/wix/WixToolset.Core.Burn/BurnBackendErrors.cs +++ b/src/wix/WixToolset.Core.Burn/BurnBackendErrors.cs @@ -56,6 +56,11 @@ namespace WixToolset.Core.Burn return Message(null, Ids.IncompatibleWixBurnSection, "Unable to read bundle executable '{0}', because this bundle was created with a different version of WiX burn (.wixburn section version '{1}'). You must use the same version of Windows Installer XML in order to read this bundle.", bundleExecutable, bundleVersion); } + public static Message UnsupportedRemotePackagePayload(string extension, string path) + { + return Message(null, Ids.UnsupportedRemotePackagePayload, "The first remote payload must be a supported package type of .exe or .msu. Use the -packageType switch to override the inferred extension: {0} from file: {1}", extension, path); + } + private static Message Message(SourceLineNumber sourceLineNumber, Ids id, string format, params object[] args) { return new Message(sourceLineNumber, MessageLevel.Error, (int)id, format, args); @@ -73,6 +78,7 @@ namespace WixToolset.Core.Burn PackageCachePayloadCollision2 = 8007, TooManyAttachedContainers = 8008, IncompatibleWixBurnSection = 8009, + UnsupportedRemotePackagePayload = 8010, } // last available is 8499. 8500 is BurnBackendWarnings. } } diff --git a/src/wix/WixToolset.Core.Burn/CommandLine/BurnCommand.cs b/src/wix/WixToolset.Core.Burn/CommandLine/BurnCommand.cs index a50fb7cd..b552678f 100644 --- a/src/wix/WixToolset.Core.Burn/CommandLine/BurnCommand.cs +++ b/src/wix/WixToolset.Core.Burn/CommandLine/BurnCommand.cs @@ -52,6 +52,10 @@ namespace WixToolset.Core.Burn.CommandLine case "reattach": this.Subcommand = new ReattachSubcommand(this.ServiceProvider); return true; + + case "remotepayload": + this.Subcommand = new RemotePayloadSubcommand(this.ServiceProvider); + return true; } return false; diff --git a/src/wix/WixToolset.Core.Burn/CommandLine/RemotePayloadSubcommand.cs b/src/wix/WixToolset.Core.Burn/CommandLine/RemotePayloadSubcommand.cs new file mode 100644 index 00000000..7b362485 --- /dev/null +++ b/src/wix/WixToolset.Core.Burn/CommandLine/RemotePayloadSubcommand.cs @@ -0,0 +1,272 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Core.Burn.CommandLine +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using System.Xml.Linq; + using WixToolset.Core.Burn.Interfaces; + using WixToolset.Core.Native; + using WixToolset.Data; + using WixToolset.Data.Symbols; + using WixToolset.Extensibility.Services; + + internal class RemotePayloadSubcommand : BurnSubcommandBase + { + private static readonly XName ExePackagePayloadName = "ExePackagePayload"; + private static readonly XName MsuPackagePayloadName = "MsuPackagePayload"; + private static readonly XName PayloadName = "Payload"; + + public RemotePayloadSubcommand(IServiceProvider serviceProvider) + { + this.Messaging = serviceProvider.GetService(); + this.PayloadHarvester = serviceProvider.GetService(); + } + + private IMessaging Messaging { get; } + + private IPayloadHarvester PayloadHarvester { get; } + + private List BasePaths { get; } = new List(); + + private string DownloadUrl { get; set; } + + private List InputPaths { get; } = new List(); + + private string OutputPath { get; set; } + + private WixBundlePackageType? PackageType { get; set; } + + private bool Recurse { get; set; } + + private bool UseCertificate { get; set; } + + public override Task ExecuteAsync(CancellationToken cancellationToken) + { + var inputPaths = this.ExpandInputPaths(); + if (inputPaths.Count == 0) + { + Console.Error.WriteLine("Path to a remote payload is required"); + return Task.FromResult(-1); + } + + // Reverse sort to ensure longest paths are matched first. + this.BasePaths.Sort(); + this.BasePaths.Reverse(); + + var elements = this.HarvestRemotePayloads(inputPaths); + + if (!this.Messaging.EncounteredError) + { + if (!String.IsNullOrEmpty(this.OutputPath)) + { + var outputFolder = Path.GetDirectoryName(this.OutputPath); + Directory.CreateDirectory(outputFolder); + + File.WriteAllLines(this.OutputPath, elements.Select(e => e.ToString())); + } + else + { + foreach (var element in elements) + { + Console.WriteLine(element); + } + } + } + + return Task.FromResult(this.Messaging.LastErrorNumber); + } + + public override bool TryParseArgument(ICommandLineParser parser, string argument) + { + if (parser.IsSwitch(argument)) + { + var parameter = argument.Substring(1); + switch (parameter.ToLowerInvariant()) + { + case "bp": + case "basepath": + this.BasePaths.Add(parser.GetNextArgumentAsDirectoryOrError(argument)); + return true; + + case "du": + case "downloadurl": + this.DownloadUrl = parser.GetNextArgumentOrError(argument); + return true; + + case "packagetype": + var packageTypeValue = parser.GetNextArgumentOrError(argument); + if (Enum.TryParse(packageTypeValue, ignoreCase: true, out var packageType)) + { + this.PackageType = packageType; + return true; + } + break; + + case "o": + case "out": + this.OutputPath = parser.GetNextArgumentAsFilePathOrError(argument); + return true; + + case "r": + case "recurse": + this.Recurse = true; + return true; + + case "usecertificate": + this.UseCertificate = true; + return true; + } + } + else + { + this.InputPaths.Add(argument); + return true; + } + + return false; + } + + private IReadOnlyCollection ExpandInputPaths() + { + var result = new List(); + + foreach (var inputPath in this.InputPaths) + { + var filename = Path.GetFileName(inputPath); + var folder = Path.GetDirectoryName(inputPath); + + if (String.IsNullOrEmpty(folder)) + { + folder = "."; + } + + foreach (var path in Directory.EnumerateFiles(folder, filename, this.Recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)) + { + result.Add(path); + } + } + + return result.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + } + + private IEnumerable HarvestRemotePayloads(IEnumerable paths) + { + var first = true; + var hashes = new Dictionary(); + + if (this.UseCertificate) + { + hashes = CertificateHashes.Read(paths).Where(c => !String.IsNullOrEmpty(c.PublicKey) && !String.IsNullOrEmpty(c.Thumbprint) && c.Exception is null).ToDictionary(c => c.Path); + } + + foreach (var path in paths) + { + var element = this.CreateRemotePayloadElement(path, first); + first = false; + + if (element == null) + { + continue; + } + + var payloadSymbol = new WixBundlePayloadSymbol + { + SourceFile = new IntermediateFieldPathValue { Path = path }, + }; + + this.PayloadHarvester.HarvestStandardInformation(payloadSymbol); + + element.Add(new XAttribute("Name", Path.GetFileName(path))); + + if (!String.IsNullOrEmpty(payloadSymbol.DisplayName)) + { + element.Add(new XAttribute("ProductName", payloadSymbol.DisplayName)); + } + + if (!String.IsNullOrEmpty(payloadSymbol.Description)) + { + element.Add(new XAttribute("Description", payloadSymbol.Description)); + } + + if (!String.IsNullOrEmpty(this.DownloadUrl)) + { + var filename = this.GetRelativeFileName(payloadSymbol.SourceFile.Path); + var formattedUrl = String.Format(this.DownloadUrl, filename); + + if (Uri.TryCreate(formattedUrl, UriKind.Absolute, out var url)) + { + element.Add(new XAttribute("DownloadUrl", url.AbsoluteUri)); + } + } + + if (hashes.TryGetValue(path, out var certificateHashes)) + { + element.Add(new XAttribute("CertificatePublicKey", certificateHashes.PublicKey)); + element.Add(new XAttribute("CertificateThumbprint", certificateHashes.Thumbprint)); + } + + if (!String.IsNullOrEmpty(payloadSymbol.Hash)) + { + element.Add(new XAttribute("Hash", payloadSymbol.Hash)); + } + + if (payloadSymbol.FileSize.HasValue) + { + element.Add(new XAttribute("Size", payloadSymbol.FileSize.Value)); + } + + if (!String.IsNullOrEmpty(payloadSymbol.Version)) + { + element.Add(new XAttribute("Version", payloadSymbol.Version)); + } + + yield return element; + } + } + + private XElement CreateRemotePayloadElement(string path, bool firstHarvest) + { + if (firstHarvest) + { + var extension = this.PackageType.HasValue ? this.PackageType.ToString() : Path.GetExtension(path); + + switch (extension.ToUpperInvariant()) + { + case "EXE": + case ".EXE": + return new XElement(ExePackagePayloadName); + + case "MSU": + case ".MSU": + return new XElement(MsuPackagePayloadName); + + default: + this.Messaging.Write(BurnBackendErrors.UnsupportedRemotePackagePayload(extension, path)); + return null; + } + } + else + { + return new XElement(PayloadName); + } + } + + private string GetRelativeFileName(string path) + { + foreach (var basePath in this.BasePaths) + { + if (path.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) + { + return path.Substring(basePath.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + } + + return Path.GetFileName(path); + } + } +} diff --git a/src/wix/WixToolset.Core.Native/CertificateHashes.cs b/src/wix/WixToolset.Core.Native/CertificateHashes.cs new file mode 100644 index 00000000..a024c923 --- /dev/null +++ b/src/wix/WixToolset.Core.Native/CertificateHashes.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Core.Native +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Globalization; + using System.Linq; + + /// + /// Read certificates' public key and thumbprint hashes. + /// + public sealed class CertificateHashes + { + private static readonly char[] TextLineSplitter = new[] { '\t' }; + + private CertificateHashes(string path, string publicKey, string thumbprint, Exception exception) + { + this.Path = path; + this.PublicKey = publicKey; + this.Thumbprint = thumbprint; + this.Exception = exception; + } + + /// + /// Path to the file read. + /// + public string Path { get; } + + /// + /// Hash of the certificate's public key. + /// + public string PublicKey { get; } + + /// + /// Hash of the certificate's thumbprint. + /// + public string Thumbprint { get; } + + /// + /// Exception encountered while trying to read certificate's hash. + /// + public Exception Exception { get; } + + /// + /// Read the certificate hashes from the provided paths. + /// + /// Paths to read for certificates. + /// Certificate hashes for the provided paths. + public static IReadOnlyList Read(IEnumerable paths) + { + var result = new List(); + + var wixnative = new WixNativeExe("certhashes"); + + foreach (var path in paths) + { + wixnative.AddStdinLine(path); + } + + try + { + var outputLines = wixnative.Run(); + foreach (var line in outputLines.Where(l => !String.IsNullOrEmpty(l))) + { + var data = line.Split(TextLineSplitter, StringSplitOptions.None); + + var error = Int32.Parse(data[3].Substring(2), NumberStyles.HexNumber); + var exception = error != 0 ? new Win32Exception(error) : null; + + result.Add(new CertificateHashes(data[0], data[1], data[2], exception)); + } + } + catch (Exception e) + { + result.Add(new CertificateHashes(null, null, null, e)); + } + + return result; + } + } +} diff --git a/src/wix/WixToolset.Core.Native/WixNativeExe.cs b/src/wix/WixToolset.Core.Native/WixNativeExe.cs index 13d7a4ea..f5ff8bf0 100644 --- a/src/wix/WixToolset.Core.Native/WixNativeExe.cs +++ b/src/wix/WixToolset.Core.Native/WixNativeExe.cs @@ -30,7 +30,7 @@ namespace WixToolset.Core.Native this.stdinLines.AddRange(lines); } - public IEnumerable Run() + public IReadOnlyCollection Run() { EnsurePathToWixNativeExeSet(); diff --git a/src/wix/WixToolset.Core/Compile/CompilerPayload.cs b/src/wix/WixToolset.Core/Compile/CompilerPayload.cs index ea63ee8c..cee9b377 100644 --- a/src/wix/WixToolset.Core/Compile/CompilerPayload.cs +++ b/src/wix/WixToolset.Core/Compile/CompilerPayload.cs @@ -11,12 +11,31 @@ namespace WixToolset.Core internal class CompilerPayload { + public string Version { get; set; } + + public CompilerPayload(CompilerCore core, SourceLineNumber sourceLineNumbers, XElement element) + { + this.Core = core; + this.Element = element; + this.SourceLineNumbers = sourceLineNumbers; + } + + private CompilerCore Core { get; } + + private XElement Element { get; } + + private SourceLineNumber SourceLineNumbers { get; } + public YesNoDefaultType Compressed { get; set; } = YesNoDefaultType.Default; public string Description { get; set; } public string DownloadUrl { get; set; } + public string CertificatePublicKey { get; set; } + + public string CertificateThumbprint { get; set; } + public string Hash { get; set; } public Identifier Id { get; set; } @@ -33,24 +52,9 @@ namespace WixToolset.Core public string SourceFile { get; set; } - public string Version { get; set; } - - public CompilerPayload(CompilerCore core, SourceLineNumber sourceLineNumbers, XElement element) - { - this.Core = core; - this.Element = element; - this.SourceLineNumbers = sourceLineNumbers; - } - - private CompilerCore Core { get; } - - private XElement Element { get; } - - private SourceLineNumber SourceLineNumbers { get; } - private void CalculateAndVerifyFields() { - var isRemote = this.IsRemoteAllowed && !String.IsNullOrEmpty(this.Hash); + var isRemote = this.IsRemoteAllowed && (!String.IsNullOrEmpty(this.CertificatePublicKey) || !String.IsNullOrEmpty(this.CertificateThumbprint) || !String.IsNullOrEmpty(this.Hash)); if (String.IsNullOrEmpty(this.SourceFile)) { @@ -81,7 +85,7 @@ namespace WixToolset.Core } else { - this.Core.Write(ErrorMessages.ExpectedAttributes(this.SourceLineNumbers, this.Element.Name.LocalName, "SourceFile", "Hash")); + this.Core.Write(ErrorMessages.ExpectedAttributes(this.SourceLineNumbers, this.Element.Name.LocalName, "SourceFile", "CertificatePublicKey", "Hash")); } } } @@ -93,7 +97,20 @@ namespace WixToolset.Core { if (isRemote) { - this.Core.Write(ErrorMessages.IllegalAttributeWithOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Hash", "SourceFile")); + if (!String.IsNullOrEmpty(this.Hash)) + { + this.Core.Write(ErrorMessages.IllegalAttributeWithOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Hash", "SourceFile")); + } + + if (!String.IsNullOrEmpty(this.CertificatePublicKey)) + { + this.Core.Write(ErrorMessages.IllegalAttributeWithOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "CertificatePublicKey", "SourceFile")); + } + + if (!String.IsNullOrEmpty(this.CertificateThumbprint)) + { + this.Core.Write(ErrorMessages.IllegalAttributeWithOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "CertificateThumbprint", "SourceFile")); + } } if (!String.IsNullOrEmpty(this.Description)) @@ -120,17 +137,34 @@ namespace WixToolset.Core { if (String.IsNullOrEmpty(this.DownloadUrl)) { - this.Core.Write(ErrorMessages.ExpectedAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "DownloadUrl", "Hash")); + this.Core.Write(ErrorMessages.ExpectedAttributeWithoutOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "DownloadUrl", "SourceFile")); } if (String.IsNullOrEmpty(this.Name)) { - this.Core.Write(ErrorMessages.ExpectedAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Name", "Hash")); + this.Core.Write(ErrorMessages.ExpectedAttributeWithoutOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Name", "SourceFile")); } if (!this.Size.HasValue) { - this.Core.Write(ErrorMessages.ExpectedAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Size", "Hash")); + this.Core.Write(ErrorMessages.ExpectedAttributeWithoutOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Size", "SourceFile")); + } + + if (String.IsNullOrEmpty(this.Hash)) + { + this.Core.Write(ErrorMessages.ExpectedAttributeWithoutOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Hash", "SourceFile")); + } + + if (!String.IsNullOrEmpty(this.CertificatePublicKey) || !String.IsNullOrEmpty(this.CertificateThumbprint)) + { + if (String.IsNullOrEmpty(this.CertificateThumbprint)) + { + this.Core.Write(ErrorMessages.ExpectedAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "CertificateThumbprint", "CertificatePublicKey")); + } + else if (String.IsNullOrEmpty(this.CertificatePublicKey)) + { + this.Core.Write(ErrorMessages.ExpectedAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "CertificatePublicKey", "CertificateThumbprint")); + } } if (YesNoDefaultType.Yes == this.Compressed) @@ -177,6 +211,8 @@ namespace WixToolset.Core Hash = this.Hash, FileSize = this.Size, Version = this.Version, + CertificatePublicKey = this.CertificatePublicKey, + CertificateThumbprint = this.CertificateThumbprint }); this.Core.CreateGroupAndOrderingRows(this.SourceLineNumbers, parentType, parentId, ComplexReferenceChildType.Payload, symbol.Id.Id, ComplexReferenceChildType.Unknown, null); @@ -248,6 +284,16 @@ namespace WixToolset.Core this.DownloadUrl = this.Core.GetAttributeValue(this.SourceLineNumbers, attrib); } + public void ParseCertificatePublicKey(XAttribute attrib) + { + this.CertificatePublicKey = this.Core.GetAttributeValue(this.SourceLineNumbers, attrib); + } + + public void ParseCertificateThumbprint(XAttribute attrib) + { + this.CertificateThumbprint = this.Core.GetAttributeValue(this.SourceLineNumbers, attrib); + } + public void ParseHash(XAttribute attrib) { this.Hash = this.Core.GetAttributeValue(this.SourceLineNumbers, attrib); @@ -286,6 +332,5 @@ namespace WixToolset.Core { this.Version = this.Core.GetAttributeValue(this.SourceLineNumbers, attrib); } - } } diff --git a/src/wix/WixToolset.Core/Compiler_Bundle.cs b/src/wix/WixToolset.Core/Compiler_Bundle.cs index c8b78243..3fde990e 100644 --- a/src/wix/WixToolset.Core/Compiler_Bundle.cs +++ b/src/wix/WixToolset.Core/Compiler_Bundle.cs @@ -2459,6 +2459,20 @@ namespace WixToolset.Core case "SourceFile": compilerPayload.ParseSourceFile(attrib); break; + case "CertificatePublicKey": + allowed = compilerPayload.IsRemoteAllowed; + if (allowed) + { + compilerPayload.ParseCertificatePublicKey(attrib); + } + break; + case "CertificateThumbprint": + allowed = compilerPayload.IsRemoteAllowed; + if (allowed) + { + compilerPayload.ParseCertificateThumbprint(attrib); + } + break; case "DownloadUrl": compilerPayload.ParseDownloadUrl(attrib); break; diff --git a/src/wix/heat/PayloadHarvester.cs b/src/wix/heat/PayloadHarvester.cs deleted file mode 100644 index d2492512..00000000 --- a/src/wix/heat/PayloadHarvester.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. - -namespace WixToolset.Harvesters -{ - using System; - using System.IO; - using WixToolset.Core.Burn.Interfaces; - using WixToolset.Data; - using WixToolset.Data.Symbols; - using WixToolset.Harvesters.Data; - using WixToolset.Harvesters.Extensibility; - using Wix = WixToolset.Harvesters.Serialize; - - /// - /// Harvest WiX authoring for a payload from the file system. - /// - public sealed class PayloadHarvester : BaseHarvesterExtension - { - private bool setUniqueIdentifiers; - private WixBundlePackageType packageType; - - private IPayloadHarvester payloadHarvester; - - /// - /// Instantiate a new PayloadHarvester. - /// - public PayloadHarvester(IPayloadHarvester payloadHarvester, WixBundlePackageType packageType) - { - this.payloadHarvester = payloadHarvester; - - this.packageType = packageType; - this.setUniqueIdentifiers = true; - } - - /// - /// Gets of sets the option to set unique identifiers. - /// - /// The option to set unique identifiers. - public bool SetUniqueIdentifiers - { - get { return this.setUniqueIdentifiers; } - set { this.setUniqueIdentifiers = value; } - } - - /// - /// Harvest a payload. - /// - /// The path of the payload. - /// A harvested payload. - public override Wix.Fragment[] Harvest(string argument) - { - if (null == argument) - { - throw new ArgumentNullException("argument"); - } - - string fullPath = Path.GetFullPath(argument); - - var remotePayload = this.HarvestRemotePayload(fullPath); - - var fragment = new Wix.Fragment(); - fragment.AddChild(remotePayload); - - return new Wix.Fragment[] { fragment }; - } - - /// - /// Harvest a payload. - /// - /// The path of the payload. - /// A harvested payload. - public Wix.RemotePayload HarvestRemotePayload(string path) - { - if (null == path) - { - throw new ArgumentNullException("path"); - } - - if (!File.Exists(path)) - { - throw new WixException(HarvesterErrors.FileNotFound(path)); - } - - Wix.RemotePayload remotePayload; - - switch (this.packageType) - { - case WixBundlePackageType.Exe: - remotePayload = new Wix.ExePackagePayload(); - break; - case WixBundlePackageType.Msu: - remotePayload = new Wix.MsuPackagePayload(); - break; - default: - throw new NotImplementedException(); - } - - var payloadSymbol = new WixBundlePayloadSymbol - { - SourceFile = new IntermediateFieldPathValue { Path = path }, - }; - - this.payloadHarvester.HarvestStandardInformation(payloadSymbol); - - if (payloadSymbol.FileSize.HasValue) - { - remotePayload.Size = payloadSymbol.FileSize.Value; - } - remotePayload.Hash = payloadSymbol.Hash; - - if (!String.IsNullOrEmpty(payloadSymbol.Version)) - { - remotePayload.Version = payloadSymbol.Version; - } - - if (!String.IsNullOrEmpty(payloadSymbol.Description)) - { - remotePayload.Description = payloadSymbol.Description; - } - - if (!String.IsNullOrEmpty(payloadSymbol.DisplayName)) - { - remotePayload.ProductName = payloadSymbol.DisplayName; - } - - return remotePayload; - } - } -} diff --git a/src/wix/heat/UtilHeatExtension.cs b/src/wix/heat/UtilHeatExtension.cs index e5be9cac..5ad5ef8a 100644 --- a/src/wix/heat/UtilHeatExtension.cs +++ b/src/wix/heat/UtilHeatExtension.cs @@ -87,14 +87,6 @@ namespace WixToolset.Harvesters harvesterExtension = new FileHarvester(); active = true; break; - case "exepackagepayload": - harvesterExtension = new PayloadHarvester(this.PayloadHarvester, WixBundlePackageType.Exe); - active = true; - break; - case "msupackagepayload": - harvesterExtension = new PayloadHarvester(this.PayloadHarvester, WixBundlePackageType.Msu); - active = true; - break; case "perf": harvesterExtension = new PerformanceCategoryHarvester(); active = true; diff --git a/src/wix/test/WixToolsetTest.Converters.Symbolizer/WixToolsetTest.Converters.Symbolizer.v3.ncrunchproject b/src/wix/test/WixToolsetTest.Converters.Symbolizer/WixToolsetTest.Converters.Symbolizer.v3.ncrunchproject index 18ab4f79..b8c570ae 100644 --- a/src/wix/test/WixToolsetTest.Converters.Symbolizer/WixToolsetTest.Converters.Symbolizer.v3.ncrunchproject +++ b/src/wix/test/WixToolsetTest.Converters.Symbolizer/WixToolsetTest.Converters.Symbolizer.v3.ncrunchproject @@ -1,5 +1,8 @@  + + LostReference + WixToolsetTest.Converters.Symbolizer.ConvertSymbolsFixture.CanLoadWixoutAndConvertToIntermediate diff --git a/src/wix/test/WixToolsetTest.Core.Native/CertificateHashesFixture.cs b/src/wix/test/WixToolsetTest.Core.Native/CertificateHashesFixture.cs new file mode 100644 index 00000000..5783445b --- /dev/null +++ b/src/wix/test/WixToolsetTest.Core.Native/CertificateHashesFixture.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.CoreNative +{ + using System.ComponentModel; + using System.Linq; + using WixToolset.Core.Native; + using WixToolsetTest.CoreNative.Utility; + using Xunit; + + public class CertificateHashesFixture + { + [Fact] + public void CanGetHashesFromSignedFile() + { + var cabFile = TestData.Get(@"TestData\test.cab"); + + var hashes = CertificateHashes.Read(new[] { cabFile }); + + var hash = hashes.Single(); + Assert.Equal(cabFile, hash.Path); + Assert.Equal("7EC90B3FC3D580EB571210011F1095E149DCC6BB", hash.PublicKey); + Assert.Equal("0B13494DB50BC185A34389BBBAA01EDD1CF56350", hash.Thumbprint); + Assert.Null(hash.Exception); + } + + [Fact] + public void CannotGetHashesFromUnsignedFile() + { + var txtFile = TestData.Get(@"TestData\test.txt"); + + var hashes = CertificateHashes.Read(new[] { txtFile }); + + var hash = hashes.Single(); + Assert.Equal(txtFile, hash.Path); + Assert.Null(hash.Exception); + } + + [Fact] + public void CanGetMultipleHashes() + { + var cabFile = TestData.Get(@"TestData\test.cab"); + var txtFile = TestData.Get(@"TestData\test.txt"); + + var hashes = CertificateHashes.Read(new[] { cabFile, txtFile }); + + Assert.Equal(cabFile, hashes[0].Path); + Assert.Equal("7EC90B3FC3D580EB571210011F1095E149DCC6BB", hashes[0].PublicKey); + Assert.Equal("0B13494DB50BC185A34389BBBAA01EDD1CF56350", hashes[0].Thumbprint); + Assert.Null(hashes[0].Exception); + + Assert.Equal(txtFile, hashes[1].Path); + Assert.Empty(hashes[1].PublicKey); + Assert.Empty(hashes[1].Thumbprint); + Assert.Null(hashes[1].Exception); + } + } +} diff --git a/src/wix/test/WixToolsetTest.Core.Native/TestData/test.cab b/src/wix/test/WixToolsetTest.Core.Native/TestData/test.cab index ca78f632..9700cd64 100644 Binary files a/src/wix/test/WixToolsetTest.Core.Native/TestData/test.cab and b/src/wix/test/WixToolsetTest.Core.Native/TestData/test.cab differ diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/PackagePayloadFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/PackagePayloadFixture.cs index 6b2d8bfa..5d0c8561 100644 --- a/src/wix/test/WixToolsetTest.CoreIntegration/PackagePayloadFixture.cs +++ b/src/wix/test/WixToolsetTest.CoreIntegration/PackagePayloadFixture.cs @@ -56,6 +56,26 @@ namespace WixToolsetTest.CoreIntegration } } + [Fact] + public void CanSpecifyPackagePayloadWithCertificate() + { + var folder = TestData.Get(@"TestData", "PackagePayload"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "SpecifiedCertificate.wxs"), + "-o", Path.Combine(baseFolder, "test.wixlib") + }); + + result.AssertSuccess(); + } + } + [Fact] public void ErrorWhenMissingSourceFileAndHash() { @@ -75,7 +95,7 @@ namespace WixToolsetTest.CoreIntegration Assert.Equal(44, result.ExitCode); WixAssert.CompareLineByLine(new[] { - "The MsuPackagePayload element's SourceFile or Hash attribute was not found; one of these is required.", + "The MsuPackagePayload element's SourceFile, CertificatePublicKey, or Hash attribute was not found; one of these is required.", }, result.Messages.Select(m => m.ToString()).ToArray()); } } @@ -144,10 +164,10 @@ namespace WixToolsetTest.CoreIntegration "-o", Path.Combine(baseFolder, "test.wixlib") }); - Assert.Equal(10, result.ExitCode); + Assert.Equal(408, result.ExitCode); WixAssert.CompareLineByLine(new[] { - "The MsuPackagePayload/@DownloadUrl attribute was not found; it is required when attribute Hash is specified.", + "The MsuPackagePayload element's DownloadUrl attribute was not found; it is required without attribute SourceFile present.", }, result.Messages.Select(m => m.ToString()).ToArray()); } } @@ -176,6 +196,102 @@ namespace WixToolsetTest.CoreIntegration } } + [Fact] + public void ErrorWhenSpecifiedSourceFileAndCertificatePublicKey() + { + var folder = TestData.Get(@"TestData", "PackagePayload"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "SpecifiedSourceFileAndCertificatePublicKey.wxs"), + "-o", Path.Combine(baseFolder, "test.wixlib") + }); + + WixAssert.CompareLineByLine(new[] + { + "The ExePackagePayload/@CertificatePublicKey attribute cannot be specified when attribute SourceFile is present.", + }, result.Messages.Select(m => m.ToString()).ToArray()); + Assert.Equal(35, result.ExitCode); + } + } + + [Fact] + public void ErrorWhenSpecifiedSourceFileAndCertificateThumprint() + { + var folder = TestData.Get(@"TestData", "PackagePayload"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "SpecifiedSourceFileAndCertificateThumbprint.wxs"), + "-o", Path.Combine(baseFolder, "test.wixlib") + }); + + WixAssert.CompareLineByLine(new[] + { + "The ExePackagePayload/@CertificateThumbprint attribute cannot be specified when attribute SourceFile is present.", + }, result.Messages.Select(m => m.ToString()).ToArray()); + Assert.Equal(35, result.ExitCode); + } + } + + [Fact] + public void ErrorWhenSpecifiedCertificateThumbprintWithoutPublicKey() + { + var folder = TestData.Get(@"TestData", "PackagePayload"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "SpecifiedHashAndCertificatePublicKey.wxs"), + "-o", Path.Combine(baseFolder, "test.wixlib") + }); + + WixAssert.CompareLineByLine(new[] + { + "The ExePackagePayload/@CertificatePublicKey attribute was not found; it is required when attribute CertificateThumbprint is specified.", + }, result.Messages.Select(m => m.ToString()).ToArray()); + Assert.Equal(10, result.ExitCode); + } + } + + [Fact] + public void ErrorWhenSpecifiedCertificatePublicKeyWithoutThumbprint() + { + var folder = TestData.Get(@"TestData", "PackagePayload"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "SpecifiedCertificatePublicKeyWithoutThumbprint.wxs"), + "-o", Path.Combine(baseFolder, "test.wixlib") + }); + + WixAssert.CompareLineByLine(new[] + { + "The ExePackagePayload/@CertificateThumbprint attribute was not found; it is required when attribute CertificatePublicKey is specified.", + }, result.Messages.Select(m => m.ToString()).ToArray()); + Assert.Equal(10, result.ExitCode); + } + } + [Fact] public void ErrorWhenWrongPackagePayloadInPayloadGroup() { diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/RemotePayloadFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/RemotePayloadFixture.cs new file mode 100644 index 00000000..434b3621 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/RemotePayloadFixture.cs @@ -0,0 +1,172 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.CoreIntegration +{ + using System.IO; + using System.Linq; + using WixBuildTools.TestSupport; + using WixToolset.Core.TestPackage; + using Xunit; + + public class RemotePayloadFixture + { + [Fact] + public void CanGetRemotePayload() + { + var folder = TestData.Get(@"TestData"); + + using (var fs = new DisposableFileSystem()) + { + var outputFolder = fs.GetFolder(); + var outFile = Path.Combine(outputFolder, "out.xml"); + + var result = WixRunner.Execute(new[] + { + "burn", "remotepayload", + Path.Combine(folder, ".Data", "burn.exe"), + "-downloadurl", "https://www.example.com/files/{0}", + "-o", outFile + }); + + result.AssertSuccess(); + + var elements = File.ReadAllLines(outFile); + elements = elements.Select(s => s.Replace("\"", "'")).ToArray(); + + WixAssert.CompareLineByLine(new[] + { + @"", + }, elements); + } + } + + [Fact] + public void CanGetRemoteMsuPayload() + { + var folder = TestData.Get(@"TestData"); + + using (var fs = new DisposableFileSystem()) + { + var outputFolder = fs.GetFolder(); + var outFile = Path.Combine(outputFolder, "out.xml"); + + var result = WixRunner.Execute(new[] + { + "burn", "remotepayload", + Path.Combine(folder, ".Data", "Windows8.1-KB2937592-x86.msu"), + "-o", outFile + }); + + result.AssertSuccess(); + + var elements = File.ReadAllLines(outFile); + elements = elements.Select(s => s.Replace("\"", "'")).ToArray(); + + WixAssert.CompareLineByLine(new[] + { + @"", + }, elements); + } + } + + [Fact] + public void CanGetRemotePayloadWithCertificate() + { + var folder = TestData.Get(@"TestData"); + + using (var fs = new DisposableFileSystem()) + { + var outputFolder = fs.GetFolder(); + var outFile = Path.Combine(outputFolder, "out.xml"); + + var result = WixRunner.Execute(new[] + { + "burn", "remotepayload", + "-usecertificate", + Path.Combine(folder, ".Data", "burn.exe"), + Path.Combine(folder, ".Data", "signed_cab1.cab"), + "-o", outFile + }); + + result.AssertSuccess(); + + var elements = File.ReadAllLines(outFile); + elements = elements.Select(s => s.Replace("\"", "'")).ToArray(); + + WixAssert.CompareLineByLine(new[] + { + @"", + @"", + }, elements); + } + } + + [Fact] + public void CanGetRemotePayloadWithoutCertificate() + { + var folder = TestData.Get(@"TestData"); + + using (var fs = new DisposableFileSystem()) + { + var outputFolder = fs.GetFolder(); + var outFile = Path.Combine(outputFolder, "out.xml"); + + var result = WixRunner.Execute(new[] + { + "burn", "remotepayload", + Path.Combine(folder, ".Data", "burn.exe"), + Path.Combine(folder, ".Data", "signed_cab1.cab"), + "-o", outFile + }); + + result.AssertSuccess(); + + var elements = File.ReadAllLines(outFile); + elements = elements.Select(s => s.Replace("\"", "'")).ToArray(); + + WixAssert.CompareLineByLine(new[] + { + @"", + @"", + }, elements); + } + } + + [Fact] + public void CanGetRemotePayloadsRecursive() + { + var folder = TestData.Get(@"TestData"); + + using (var fs = new DisposableFileSystem()) + { + var outputFolder = fs.GetFolder(); + var outFile = Path.Combine(outputFolder, "out.xml"); + + var result = WixRunner.Execute(new[] + { + "burn", "remotepayload", + "-recurse", + "-du", "https://www.example.com/files/{0}", + Path.Combine(folder, ".Data", "burn.exe"), + Path.Combine(folder, "RemotePayload", "*"), + "-basepath", folder, + "-bp", Path.Combine(folder, ".Data"), + "-o", outFile + }); + + result.AssertSuccess(); + + var elements = File.ReadAllLines(outFile); + elements = elements.Select(s => s.Replace("\"", "'")).ToArray(); + + WixAssert.CompareLineByLine(new[] + { + @"", + @"", + @"", + @"", + }, elements); + } + } + } +} diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/.Data/Windows8.1-KB2937592-x86.msu b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/.Data/Windows8.1-KB2937592-x86.msu new file mode 100644 index 00000000..c39f53b0 Binary files /dev/null and b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/.Data/Windows8.1-KB2937592-x86.msu differ diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedCertificate.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedCertificate.wxs new file mode 100644 index 00000000..b5dec9a2 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedCertificate.wxs @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedCertificatePublicKeyWithoutThumbprint.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedCertificatePublicKeyWithoutThumbprint.wxs new file mode 100644 index 00000000..aa915a31 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedCertificatePublicKeyWithoutThumbprint.wxs @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedHashAndCertificatePublicKey.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedHashAndCertificatePublicKey.wxs new file mode 100644 index 00000000..5570017e --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedHashAndCertificatePublicKey.wxs @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedSourceFileAndCertificatePublicKey.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedSourceFileAndCertificatePublicKey.wxs new file mode 100644 index 00000000..b79b5596 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedSourceFileAndCertificatePublicKey.wxs @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedSourceFileAndCertificateThumbprint.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedSourceFileAndCertificateThumbprint.wxs new file mode 100644 index 00000000..ad2ab521 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedSourceFileAndCertificateThumbprint.wxs @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/a.dat b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/a.dat new file mode 100644 index 00000000..a96b19b3 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/a.dat @@ -0,0 +1 @@ +This is a.dat. diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/subfolder/b.dat b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/subfolder/b.dat new file mode 100644 index 00000000..35c4c043 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/subfolder/b.dat @@ -0,0 +1 @@ +This is b.dat. A little bit longer. diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/subfolder/c.dat b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/subfolder/c.dat new file mode 100644 index 00000000..937aa91f --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/subfolder/c.dat @@ -0,0 +1 @@ +This is c.dat. A little bit longer, now! diff --git a/src/wix/test/WixToolsetTest.Heat/PayloadTests.cs b/src/wix/test/WixToolsetTest.Heat/PayloadTests.cs deleted file mode 100644 index 8072f50d..00000000 --- a/src/wix/test/WixToolsetTest.Heat/PayloadTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. - -namespace WixToolsetTest.Harvesters -{ - using System; - using System.IO; - using WixBuildTools.TestSupport; - using Xunit; - - public class PayloadTests - { - [Fact] - public void CanHarvestExePackagePayload() - { - var folder = TestData.Get(@"TestData"); - - using (var fs = new DisposableFileSystem()) - { - var baseFolder = fs.GetFolder(); - var outputFilePath = Path.Combine(baseFolder, "test.wxs"); - - var result = HeatRunner.Execute(new[] - { - "exepackagepayload", - Path.Combine(folder, ".Data", "burn.exe"), - "-o", outputFilePath, - }); - - result.AssertSuccess(); - - Assert.True(File.Exists(outputFilePath)); - - var expected = File.ReadAllText(Path.Combine(folder, "Payload", "HarvestedExePackagePayload.wxs")).Replace("\r\n", "\n"); - var actual = File.ReadAllText(outputFilePath).Replace("\r\n", "\n"); - Assert.Equal(expected, actual); - } - } - - [Fact] - public void CanHarvestMsuPackagePayload() - { - var folder = TestData.Get(@"TestData"); - - using (var fs = new DisposableFileSystem()) - { - var baseFolder = fs.GetFolder(); - var outputFilePath = Path.Combine(baseFolder, "test.wxs"); - - var result = HeatRunner.Execute(new[] - { - "msupackagepayload", - Path.Combine(folder, ".Data", "Windows8.1-KB2937592-x86.msu"), - "-o", outputFilePath, - }); - - result.AssertSuccess(); - - Assert.True(File.Exists(outputFilePath)); - - var expected = File.ReadAllText(Path.Combine(folder, "Payload", "HarvestedMsuPackagePayload.wxs")).Replace("\r\n", "\n"); - var actual = File.ReadAllText(outputFilePath).Replace("\r\n", "\n"); - Assert.Equal(expected, actual); - } - } - } -} diff --git a/src/wix/test/WixToolsetTest.Heat/TestData/.Data/Windows8.1-KB2937592-x86.msu b/src/wix/test/WixToolsetTest.Heat/TestData/.Data/Windows8.1-KB2937592-x86.msu deleted file mode 100644 index c39f53b0..00000000 Binary files a/src/wix/test/WixToolsetTest.Heat/TestData/.Data/Windows8.1-KB2937592-x86.msu and /dev/null differ diff --git a/src/wix/test/WixToolsetTest.Heat/TestData/.Data/burn.exe b/src/wix/test/WixToolsetTest.Heat/TestData/.Data/burn.exe deleted file mode 100644 index 2a4f423f..00000000 Binary files a/src/wix/test/WixToolsetTest.Heat/TestData/.Data/burn.exe and /dev/null differ diff --git a/src/wix/test/WixToolsetTest.Heat/TestData/Payload/HarvestedExePackagePayload.wxs b/src/wix/test/WixToolsetTest.Heat/TestData/Payload/HarvestedExePackagePayload.wxs deleted file mode 100644 index 40100f22..00000000 --- a/src/wix/test/WixToolsetTest.Heat/TestData/Payload/HarvestedExePackagePayload.wxs +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/wix/test/WixToolsetTest.Heat/TestData/Payload/HarvestedMsuPackagePayload.wxs b/src/wix/test/WixToolsetTest.Heat/TestData/Payload/HarvestedMsuPackagePayload.wxs deleted file mode 100644 index f203fe27..00000000 --- a/src/wix/test/WixToolsetTest.Heat/TestData/Payload/HarvestedMsuPackagePayload.wxs +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/wix/test/WixToolsetTest.Heat/WixToolsetTest.Heat.csproj b/src/wix/test/WixToolsetTest.Heat/WixToolsetTest.Heat.csproj index 2a706aa0..1ba62393 100644 --- a/src/wix/test/WixToolsetTest.Heat/WixToolsetTest.Heat.csproj +++ b/src/wix/test/WixToolsetTest.Heat/WixToolsetTest.Heat.csproj @@ -8,10 +8,6 @@ false - - - - diff --git a/src/wix/wixnative/certhashes.cpp b/src/wix/wixnative/certhashes.cpp new file mode 100644 index 00000000..cbb548ba --- /dev/null +++ b/src/wix/wixnative/certhashes.cpp @@ -0,0 +1,138 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +#include "precomp.h" + +#define SHA1_HASH_LEN 20 + +static HRESULT GetPublicKeyIdentifierAndThumbprint( + __in_z LPCWSTR wzPath, + __inout_z LPWSTR* psczPublicKeyIdentifier, + __inout_z LPWSTR* psczThumbprint); + +static HRESULT GetChainContext( + __in_z LPCWSTR wzPath, + __out PCCERT_CHAIN_CONTEXT* ppChainContext); + + +HRESULT CertificateHashesCommand( + __in int argc, + __in_ecount(argc) LPWSTR argv[]) +{ + Unused(argc); + Unused(argv); + + HRESULT hr = S_OK; + + LPWSTR sczFilePath = NULL; + LPWSTR sczPublicKeyIdentifier = NULL; + LPWSTR sczThumbprint = NULL; + + hr = WixNativeReadStdinPreamble(); + ExitOnFailure(hr, "Failed to read stdin preamble before reading paths to get certificate hashes"); + + // Get the hash for each provided file. + for (;;) + { + hr = ConsoleReadW(&sczFilePath); + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "Failed to read file path to signed file from stdin"); + + if (!*sczFilePath) + { + break; + } + + hr = GetPublicKeyIdentifierAndThumbprint(sczFilePath, &sczPublicKeyIdentifier, &sczThumbprint); + if (FAILED(hr)) + { + // Treat no signature as success without finding certificate hashes. + ConsoleWriteLine(CONSOLE_COLOR_NORMAL, "%ls\t\t\t0x%x", sczFilePath, TRUST_E_NOSIGNATURE == hr ? 0 : hr); + } + else + { + ConsoleWriteLine(CONSOLE_COLOR_NORMAL, "%ls\t%ls\t%ls\t0x%x", sczFilePath, sczPublicKeyIdentifier, sczThumbprint, hr); + } + } + +LExit: + ReleaseStr(sczThumbprint); + ReleaseStr(sczPublicKeyIdentifier); + ReleaseStr(sczFilePath); + + return hr; +} + +static HRESULT GetPublicKeyIdentifierAndThumbprint( + __in_z LPCWSTR wzPath, + __inout_z LPWSTR* psczPublicKeyIdentifier, + __inout_z LPWSTR* psczThumbprint) +{ + HRESULT hr = S_OK; + PCCERT_CHAIN_CONTEXT pChainContext = NULL; + PCCERT_CONTEXT pCertContext = NULL; + BYTE rgbPublicKeyIdentifier[SHA1_HASH_LEN] = { }; + DWORD cbPublicKeyIdentifier = sizeof(rgbPublicKeyIdentifier); + BYTE* pbThumbprint = NULL; + DWORD cbThumbprint = 0; + + hr = GetChainContext(wzPath, &pChainContext); + ExitOnFailure(hr, "Failed to get chain context for file: %ls", wzPath); + + pCertContext = pChainContext->rgpChain[0]->rgpElement[0]->pCertContext; + + // Get the certificate's public key identifier and thumbprint. + if (!::CryptHashPublicKeyInfo(NULL, CALG_SHA1, 0, X509_ASN_ENCODING, &pCertContext->pCertInfo->SubjectPublicKeyInfo, rgbPublicKeyIdentifier, &cbPublicKeyIdentifier)) + { + ExitWithLastError(hr, "Failed to get certificate public key identifier from file: %ls", wzPath); + } + + hr = CertReadProperty(pCertContext, CERT_SHA1_HASH_PROP_ID, &pbThumbprint, &cbThumbprint); + ExitOnFailure(hr, "Failed to read certificate thumbprint from file: %ls", wzPath); + + // Get the public key indentifier and thumbprint in hex. + hr = StrAllocHexEncode(rgbPublicKeyIdentifier, cbPublicKeyIdentifier, psczPublicKeyIdentifier); + ExitOnFailure(hr, "Failed to convert certificate public key to hex for file: %ls", wzPath); + + hr = StrAllocHexEncode(pbThumbprint, cbThumbprint, psczThumbprint); + ExitOnFailure(hr, "Failed to convert certificate thumbprint to hex for file: %ls", wzPath); + +LExit: + ReleaseMem(pbThumbprint); + return hr; +} + +static HRESULT GetChainContext( + __in_z LPCWSTR wzPath, + __out PCCERT_CHAIN_CONTEXT* ppChainContext) +{ + HRESULT hr = S_OK; + + GUID guidAuthenticode = WINTRUST_ACTION_GENERIC_VERIFY_V2; + WINTRUST_FILE_INFO wfi = { }; + WINTRUST_DATA wtd = { }; + CRYPT_PROVIDER_DATA* pProviderData = NULL; + CRYPT_PROVIDER_SGNR* pSigner = NULL; + + wfi.cbStruct = sizeof(wfi); + wfi.pcwszFilePath = wzPath; + + wtd.cbStruct = sizeof(wtd); + wtd.dwUnionChoice = WTD_CHOICE_FILE; + wtd.pFile = &wfi; + wtd.dwStateAction = WTD_STATEACTION_VERIFY; + wtd.dwProvFlags = WTD_REVOCATION_CHECK_NONE | WTD_HASH_ONLY_FLAG | WTD_CACHE_ONLY_URL_RETRIEVAL; + wtd.dwUIChoice = WTD_UI_NONE; + + hr = ::WinVerifyTrust(static_cast(INVALID_HANDLE_VALUE), &guidAuthenticode, &wtd); + ExitOnFailure(hr, "Failed to verify certificate on file: %ls", wzPath); + + pProviderData = ::WTHelperProvDataFromStateData(wtd.hWVTStateData); + ExitOnNullWithLastError(pProviderData, hr, "Failed to get provider state from authenticode certificate on file: %ls", wzPath); + + pSigner = ::WTHelperGetProvSignerFromChain(pProviderData, 0, FALSE, 0); + ExitOnNullWithLastError(pSigner, hr, "Failed to get signer chain from authenticode certificate on file: %ls", wzPath); + + *ppChainContext = pSigner->pChainContext; + +LExit: + return hr; +} diff --git a/src/wix/wixnative/precomp.h b/src/wix/wixnative/precomp.h index 490bbdcf..0a458ca6 100644 --- a/src/wix/wixnative/precomp.h +++ b/src/wix/wixnative/precomp.h @@ -4,9 +4,12 @@ #include #include #include +#include #include +#include #include "dutil.h" +#include "certutil.h" #include "conutil.h" #include "memutil.h" #include "pathutil.h" @@ -15,6 +18,7 @@ #include "cabutil.h" HRESULT WixNativeReadStdinPreamble(); +HRESULT CertificateHashesCommand(__in int argc, __in_ecount(argc) LPWSTR argv[]); HRESULT SmartCabCommand(__in int argc, __in_ecount(argc) LPWSTR argv[]); HRESULT ResetAclsCommand(__in int argc, __in_ecount(argc) LPWSTR argv[]); HRESULT EnumCabCommand(__in int argc, __in_ecount(argc) LPWSTR argv[]); diff --git a/src/wix/wixnative/wixnative.cpp b/src/wix/wixnative/wixnative.cpp index d1236da1..8a24d5f1 100644 --- a/src/wix/wixnative/wixnative.cpp +++ b/src/wix/wixnative/wixnative.cpp @@ -24,6 +24,10 @@ int __cdecl wmain(int argc, LPWSTR argv[]) { hr = EnumCabCommand(argc - 2, argv + 2); } + else if (CSTR_EQUAL == ::CompareString(LOCALE_INVARIANT, NORM_IGNORECASE, argv[1], -1, L"certhashes", -1)) + { + hr = CertificateHashesCommand(argc - 2, argv + 2); + } else if (CSTR_EQUAL == ::CompareString(LOCALE_INVARIANT, NORM_IGNORECASE, argv[1], -1, L"resetacls", -1)) { hr = ResetAclsCommand(argc - 2, argv + 2); diff --git a/src/wix/wixnative/wixnative.vcxproj b/src/wix/wixnative/wixnative.vcxproj index 1dac36e2..2e90661c 100644 --- a/src/wix/wixnative/wixnative.vcxproj +++ b/src/wix/wixnative/wixnative.vcxproj @@ -42,7 +42,7 @@ - crypt32.lib;cabinet.lib;msi.lib + crypt32.lib;cabinet.lib;msi.lib;wintrust.lib @@ -50,6 +50,7 @@ Create + -- cgit v1.2.3-55-g6feb