@joebigelow / wix-1 / commits / 5fd80416

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".

Rob Mensching committed Feb 25, 2022 at 22:24 UTC 5fd804165587b3b6d2bd6b9844dcb3fa55a4a305
35 files changed +1033 -247
src/api/wix/WixToolset.Data/Symbols/WixBundlePayloadSymbol.cs
+16
@@ -26,6 +26,8 @@ namespace WixToolset.Data
26 new IntermediateFieldDefinition(nameof(WixBundlePayloadSymbolFields.LayoutOnly), IntermediateFieldType.Bool),
27 new IntermediateFieldDefinition(nameof(WixBundlePayloadSymbolFields.Packaging), IntermediateFieldType.Number),
28 new IntermediateFieldDefinition(nameof(WixBundlePayloadSymbolFields.ParentPackagePayloadRef), IntermediateFieldType.String),
29 + new IntermediateFieldDefinition(nameof(WixBundlePayloadSymbolFields.CertificatePublicKey), IntermediateFieldType.String),
30 + new IntermediateFieldDefinition(nameof(WixBundlePayloadSymbolFields.CertificateThumbprint), IntermediateFieldType.String),
31 },
32 typeof(WixBundlePayloadSymbol));
33 }
@@ -53,6 +55,8 @@ namespace WixToolset.Data.Symbols
55 LayoutOnly,
56 Packaging,
57 ParentPackagePayloadRef,
58 + CertificatePublicKey,
59 + CertificateThumbprint,
60 }
61
62 public class WixBundlePayloadSymbol : IntermediateSymbol
@@ -162,5 +166,17 @@ namespace WixToolset.Data.Symbols
166 get => (string)this.Fields[(int)WixBundlePayloadSymbolFields.ParentPackagePayloadRef];
167 set => this.Set((int)WixBundlePayloadSymbolFields.ParentPackagePayloadRef, value);
168 }
169 +
170 + public string CertificatePublicKey
171 + {
172 + get => (string)this.Fields[(int)WixBundlePayloadSymbolFields.CertificatePublicKey];
173 + set => this.Set((int)WixBundlePayloadSymbolFields.CertificatePublicKey, value);
174 + }
175 +
176 + public string CertificateThumbprint
177 + {
178 + get => (string)this.Fields[(int)WixBundlePayloadSymbolFields.CertificateThumbprint];
179 + set => this.Set((int)WixBundlePayloadSymbolFields.CertificateThumbprint, value);
180 + }
181 }
182 }
src/wix/WixToolset.Converters.Symbolizer/WixToolset.Converters.Symbolizer.v3.ncrunchproject new
+7
@@ -0,0 +1,7 @@
1 +<ProjectConfiguration>
2 + <Settings>
3 + <HiddenComponentWarnings>
4 + <Value>LostReference</Value>
5 + </HiddenComponentWarnings>
6 + </Settings>
7 +</ProjectConfiguration>
\ No newline at end of file
src/wix/WixToolset.Core.Burn/Bundles/CreateBurnManifestCommand.cs
+10 -1
@@ -672,7 +672,16 @@ namespace WixToolset.Core.Burn.Bundles
672 writer.WriteAttributeString("Id", payload.Id.Id);
673 writer.WriteAttributeString("FilePath", payload.Name);
674 writer.WriteAttributeString("FileSize", payload.FileSize.Value.ToString(CultureInfo.InvariantCulture));
675 - writer.WriteAttributeString("Hash", payload.Hash);
675 +
676 + if (!String.IsNullOrEmpty(payload.CertificatePublicKey) && !String.IsNullOrEmpty(payload.CertificateThumbprint))
677 + {
678 + writer.WriteAttributeString("CertificateRootPublicKeyIdentifier", payload.CertificatePublicKey);
679 + writer.WriteAttributeString("CertificateRootThumbprint", payload.CertificateThumbprint);
680 + }
681 + else
682 + {
683 + writer.WriteAttributeString("Hash", payload.Hash);
684 + }
685
686 if (payload.LayoutOnly)
687 {
src/wix/WixToolset.Core.Burn/BurnBackendErrors.cs
+6
@@ -56,6 +56,11 @@ namespace WixToolset.Core.Burn
56 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);
57 }
58
59 + public static Message UnsupportedRemotePackagePayload(string extension, string path)
60 + {
61 + 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);
62 + }
63 +
64 private static Message Message(SourceLineNumber sourceLineNumber, Ids id, string format, params object[] args)
65 {
66 return new Message(sourceLineNumber, MessageLevel.Error, (int)id, format, args);
@@ -73,6 +78,7 @@ namespace WixToolset.Core.Burn
78 PackageCachePayloadCollision2 = 8007,
79 TooManyAttachedContainers = 8008,
80 IncompatibleWixBurnSection = 8009,
81 + UnsupportedRemotePackagePayload = 8010,
82 } // last available is 8499. 8500 is BurnBackendWarnings.
83 }
84 }
src/wix/WixToolset.Core.Burn/CommandLine/BurnCommand.cs
+4
@@ -52,6 +52,10 @@ namespace WixToolset.Core.Burn.CommandLine
52 case "reattach":
53 this.Subcommand = new ReattachSubcommand(this.ServiceProvider);
54 return true;
55 +
56 + case "remotepayload":
57 + this.Subcommand = new RemotePayloadSubcommand(this.ServiceProvider);
58 + return true;
59 }
60
61 return false;
src/wix/WixToolset.Core.Burn/CommandLine/RemotePayloadSubcommand.cs new
+272
@@ -0,0 +1,272 @@
1 +// 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.
2 +
3 +namespace WixToolset.Core.Burn.CommandLine
4 +{
5 + using System;
6 + using System.Collections.Generic;
7 + using System.IO;
8 + using System.Linq;
9 + using System.Threading;
10 + using System.Threading.Tasks;
11 + using System.Xml.Linq;
12 + using WixToolset.Core.Burn.Interfaces;
13 + using WixToolset.Core.Native;
14 + using WixToolset.Data;
15 + using WixToolset.Data.Symbols;
16 + using WixToolset.Extensibility.Services;
17 +
18 + internal class RemotePayloadSubcommand : BurnSubcommandBase
19 + {
20 + private static readonly XName ExePackagePayloadName = "ExePackagePayload";
21 + private static readonly XName MsuPackagePayloadName = "MsuPackagePayload";
22 + private static readonly XName PayloadName = "Payload";
23 +
24 + public RemotePayloadSubcommand(IServiceProvider serviceProvider)
25 + {
26 + this.Messaging = serviceProvider.GetService<IMessaging>();
27 + this.PayloadHarvester = serviceProvider.GetService<IPayloadHarvester>();
28 + }
29 +
30 + private IMessaging Messaging { get; }
31 +
32 + private IPayloadHarvester PayloadHarvester { get; }
33 +
34 + private List<string> BasePaths { get; } = new List<string>();
35 +
36 + private string DownloadUrl { get; set; }
37 +
38 + private List<string> InputPaths { get; } = new List<string>();
39 +
40 + private string OutputPath { get; set; }
41 +
42 + private WixBundlePackageType? PackageType { get; set; }
43 +
44 + private bool Recurse { get; set; }
45 +
46 + private bool UseCertificate { get; set; }
47 +
48 + public override Task<int> ExecuteAsync(CancellationToken cancellationToken)
49 + {
50 + var inputPaths = this.ExpandInputPaths();
51 + if (inputPaths.Count == 0)
52 + {
53 + Console.Error.WriteLine("Path to a remote payload is required");
54 + return Task.FromResult(-1);
55 + }
56 +
57 + // Reverse sort to ensure longest paths are matched first.
58 + this.BasePaths.Sort();
59 + this.BasePaths.Reverse();
60 +
61 + var elements = this.HarvestRemotePayloads(inputPaths);
62 +
63 + if (!this.Messaging.EncounteredError)
64 + {
65 + if (!String.IsNullOrEmpty(this.OutputPath))
66 + {
67 + var outputFolder = Path.GetDirectoryName(this.OutputPath);
68 + Directory.CreateDirectory(outputFolder);
69 +
70 + File.WriteAllLines(this.OutputPath, elements.Select(e => e.ToString()));
71 + }
72 + else
73 + {
74 + foreach (var element in elements)
75 + {
76 + Console.WriteLine(element);
77 + }
78 + }
79 + }
80 +
81 + return Task.FromResult(this.Messaging.LastErrorNumber);
82 + }
83 +
84 + public override bool TryParseArgument(ICommandLineParser parser, string argument)
85 + {
86 + if (parser.IsSwitch(argument))
87 + {
88 + var parameter = argument.Substring(1);
89 + switch (parameter.ToLowerInvariant())
90 + {
91 + case "bp":
92 + case "basepath":
93 + this.BasePaths.Add(parser.GetNextArgumentAsDirectoryOrError(argument));
94 + return true;
95 +
96 + case "du":
97 + case "downloadurl":
98 + this.DownloadUrl = parser.GetNextArgumentOrError(argument);
99 + return true;
100 +
101 + case "packagetype":
102 + var packageTypeValue = parser.GetNextArgumentOrError(argument);
103 + if (Enum.TryParse<WixBundlePackageType>(packageTypeValue, ignoreCase: true, out var packageType))
104 + {
105 + this.PackageType = packageType;
106 + return true;
107 + }
108 + break;
109 +
110 + case "o":
111 + case "out":
112 + this.OutputPath = parser.GetNextArgumentAsFilePathOrError(argument);
113 + return true;
114 +
115 + case "r":
116 + case "recurse":
117 + this.Recurse = true;
118 + return true;
119 +
120 + case "usecertificate":
121 + this.UseCertificate = true;
122 + return true;
123 + }
124 + }
125 + else
126 + {
127 + this.InputPaths.Add(argument);
128 + return true;
129 + }
130 +
131 + return false;
132 + }
133 +
134 + private IReadOnlyCollection<string> ExpandInputPaths()
135 + {
136 + var result = new List<string>();
137 +
138 + foreach (var inputPath in this.InputPaths)
139 + {
140 + var filename = Path.GetFileName(inputPath);
141 + var folder = Path.GetDirectoryName(inputPath);
142 +
143 + if (String.IsNullOrEmpty(folder))
144 + {
145 + folder = ".";
146 + }
147 +
148 + foreach (var path in Directory.EnumerateFiles(folder, filename, this.Recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
149 + {
150 + result.Add(path);
151 + }
152 + }
153 +
154 + return result.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
155 + }
156 +
157 + private IEnumerable<XElement> HarvestRemotePayloads(IEnumerable<string> paths)
158 + {
159 + var first = true;
160 + var hashes = new Dictionary<string, CertificateHashes>();
161 +
162 + if (this.UseCertificate)
163 + {
164 + hashes = CertificateHashes.Read(paths).Where(c => !String.IsNullOrEmpty(c.PublicKey) && !String.IsNullOrEmpty(c.Thumbprint) && c.Exception is null).ToDictionary(c => c.Path);
165 + }
166 +
167 + foreach (var path in paths)
168 + {
169 + var element = this.CreateRemotePayloadElement(path, first);
170 + first = false;
171 +
172 + if (element == null)
173 + {
174 + continue;
175 + }
176 +
177 + var payloadSymbol = new WixBundlePayloadSymbol
178 + {
179 + SourceFile = new IntermediateFieldPathValue { Path = path },
180 + };
181 +
182 + this.PayloadHarvester.HarvestStandardInformation(payloadSymbol);
183 +
184 + element.Add(new XAttribute("Name", Path.GetFileName(path)));
185 +
186 + if (!String.IsNullOrEmpty(payloadSymbol.DisplayName))
187 + {
188 + element.Add(new XAttribute("ProductName", payloadSymbol.DisplayName));
189 + }
190 +
191 + if (!String.IsNullOrEmpty(payloadSymbol.Description))
192 + {
193 + element.Add(new XAttribute("Description", payloadSymbol.Description));
194 + }
195 +
196 + if (!String.IsNullOrEmpty(this.DownloadUrl))
197 + {
198 + var filename = this.GetRelativeFileName(payloadSymbol.SourceFile.Path);
199 + var formattedUrl = String.Format(this.DownloadUrl, filename);
200 +
201 + if (Uri.TryCreate(formattedUrl, UriKind.Absolute, out var url))
202 + {
203 + element.Add(new XAttribute("DownloadUrl", url.AbsoluteUri));
204 + }
205 + }
206 +
207 + if (hashes.TryGetValue(path, out var certificateHashes))
208 + {
209 + element.Add(new XAttribute("CertificatePublicKey", certificateHashes.PublicKey));
210 + element.Add(new XAttribute("CertificateThumbprint", certificateHashes.Thumbprint));
211 + }
212 +
213 + if (!String.IsNullOrEmpty(payloadSymbol.Hash))
214 + {
215 + element.Add(new XAttribute("Hash", payloadSymbol.Hash));
216 + }
217 +
218 + if (payloadSymbol.FileSize.HasValue)
219 + {
220 + element.Add(new XAttribute("Size", payloadSymbol.FileSize.Value));
221 + }
222 +
223 + if (!String.IsNullOrEmpty(payloadSymbol.Version))
224 + {
225 + element.Add(new XAttribute("Version", payloadSymbol.Version));
226 + }
227 +
228 + yield return element;
229 + }
230 + }
231 +
232 + private XElement CreateRemotePayloadElement(string path, bool firstHarvest)
233 + {
234 + if (firstHarvest)
235 + {
236 + var extension = this.PackageType.HasValue ? this.PackageType.ToString() : Path.GetExtension(path);
237 +
238 + switch (extension.ToUpperInvariant())
239 + {
240 + case "EXE":
241 + case ".EXE":
242 + return new XElement(ExePackagePayloadName);
243 +
244 + case "MSU":
245 + case ".MSU":
246 + return new XElement(MsuPackagePayloadName);
247 +
248 + default:
249 + this.Messaging.Write(BurnBackendErrors.UnsupportedRemotePackagePayload(extension, path));
250 + return null;
251 + }
252 + }
253 + else
254 + {
255 + return new XElement(PayloadName);
256 + }
257 + }
258 +
259 + private string GetRelativeFileName(string path)
260 + {
261 + foreach (var basePath in this.BasePaths)
262 + {
263 + if (path.StartsWith(basePath, StringComparison.OrdinalIgnoreCase))
264 + {
265 + return path.Substring(basePath.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
266 + }
267 + }
268 +
269 + return Path.GetFileName(path);
270 + }
271 + }
272 +}
src/wix/WixToolset.Core.Native/CertificateHashes.cs new
+83
@@ -0,0 +1,83 @@
1 +// 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.
2 +
3 +namespace WixToolset.Core.Native
4 +{
5 + using System;
6 + using System.Collections.Generic;
7 + using System.ComponentModel;
8 + using System.Globalization;
9 + using System.Linq;
10 +
11 + /// <summary>
12 + /// Read certificates' public key and thumbprint hashes.
13 + /// </summary>
14 + public sealed class CertificateHashes
15 + {
16 + private static readonly char[] TextLineSplitter = new[] { '\t' };
17 +
18 + private CertificateHashes(string path, string publicKey, string thumbprint, Exception exception)
19 + {
20 + this.Path = path;
21 + this.PublicKey = publicKey;
22 + this.Thumbprint = thumbprint;
23 + this.Exception = exception;
24 + }
25 +
26 + /// <summary>
27 + /// Path to the file read.
28 + /// </summary>
29 + public string Path { get; }
30 +
31 + /// <summary>
32 + /// Hash of the certificate's public key.
33 + /// </summary>
34 + public string PublicKey { get; }
35 +
36 + /// <summary>
37 + /// Hash of the certificate's thumbprint.
38 + /// </summary>
39 + public string Thumbprint { get; }
40 +
41 + /// <summary>
42 + /// Exception encountered while trying to read certificate's hash.
43 + /// </summary>
44 + public Exception Exception { get; }
45 +
46 + /// <summary>
47 + /// Read the certificate hashes from the provided paths.
48 + /// </summary>
49 + /// <param name="paths">Paths to read for certificates.</param>
50 + /// <returns>Certificate hashes for the provided paths.</returns>
51 + public static IReadOnlyList<CertificateHashes> Read(IEnumerable<string> paths)
52 + {
53 + var result = new List<CertificateHashes>();
54 +
55 + var wixnative = new WixNativeExe("certhashes");
56 +
57 + foreach (var path in paths)
58 + {
59 + wixnative.AddStdinLine(path);
60 + }
61 +
62 + try
63 + {
64 + var outputLines = wixnative.Run();
65 + foreach (var line in outputLines.Where(l => !String.IsNullOrEmpty(l)))
66 + {
67 + var data = line.Split(TextLineSplitter, StringSplitOptions.None);
68 +
69 + var error = Int32.Parse(data[3].Substring(2), NumberStyles.HexNumber);
70 + var exception = error != 0 ? new Win32Exception(error) : null;
71 +
72 + result.Add(new CertificateHashes(data[0], data[1], data[2], exception));
73 + }
74 + }
75 + catch (Exception e)
76 + {
77 + result.Add(new CertificateHashes(null, null, null, e));
78 + }
79 +
80 + return result;
81 + }
82 + }
83 +}
src/wix/WixToolset.Core.Native/WixNativeExe.cs
+1 -1
@@ -30,7 +30,7 @@ namespace WixToolset.Core.Native
30 this.stdinLines.AddRange(lines);
31 }
32
33 - public IEnumerable<string> Run()
33 + public IReadOnlyCollection<string> Run()
34 {
35 EnsurePathToWixNativeExeSet();
36
src/wix/WixToolset.Core/Compile/CompilerPayload.cs
+67 -22
@@ -11,12 +11,31 @@ namespace WixToolset.Core
11
12 internal class CompilerPayload
13 {
14 + public string Version { get; set; }
15 +
16 + public CompilerPayload(CompilerCore core, SourceLineNumber sourceLineNumbers, XElement element)
17 + {
18 + this.Core = core;
19 + this.Element = element;
20 + this.SourceLineNumbers = sourceLineNumbers;
21 + }
22 +
23 + private CompilerCore Core { get; }
24 +
25 + private XElement Element { get; }
26 +
27 + private SourceLineNumber SourceLineNumbers { get; }
28 +
29 public YesNoDefaultType Compressed { get; set; } = YesNoDefaultType.Default;
30
31 public string Description { get; set; }
32
33 public string DownloadUrl { get; set; }
34
35 + public string CertificatePublicKey { get; set; }
36 +
37 + public string CertificateThumbprint { get; set; }
38 +
39 public string Hash { get; set; }
40
41 public Identifier Id { get; set; }
@@ -33,24 +52,9 @@ namespace WixToolset.Core
52
53 public string SourceFile { get; set; }
54
36 - public string Version { get; set; }
37 -
38 - public CompilerPayload(CompilerCore core, SourceLineNumber sourceLineNumbers, XElement element)
39 - {
40 - this.Core = core;
41 - this.Element = element;
42 - this.SourceLineNumbers = sourceLineNumbers;
43 - }
44 -
45 - private CompilerCore Core { get; }
46 -
47 - private XElement Element { get; }
48 -
49 - private SourceLineNumber SourceLineNumbers { get; }
50 -
55 private void CalculateAndVerifyFields()
56 {
53 - var isRemote = this.IsRemoteAllowed && !String.IsNullOrEmpty(this.Hash);
57 + var isRemote = this.IsRemoteAllowed && (!String.IsNullOrEmpty(this.CertificatePublicKey) || !String.IsNullOrEmpty(this.CertificateThumbprint) || !String.IsNullOrEmpty(this.Hash));
58
59 if (String.IsNullOrEmpty(this.SourceFile))
60 {
@@ -81,7 +85,7 @@ namespace WixToolset.Core
85 }
86 else
87 {
84 - this.Core.Write(ErrorMessages.ExpectedAttributes(this.SourceLineNumbers, this.Element.Name.LocalName, "SourceFile", "Hash"));
88 + this.Core.Write(ErrorMessages.ExpectedAttributes(this.SourceLineNumbers, this.Element.Name.LocalName, "SourceFile", "CertificatePublicKey", "Hash"));
89 }
90 }
91 }
@@ -93,7 +97,20 @@ namespace WixToolset.Core
97 {
98 if (isRemote)
99 {
96 - this.Core.Write(ErrorMessages.IllegalAttributeWithOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Hash", "SourceFile"));
100 + if (!String.IsNullOrEmpty(this.Hash))
101 + {
102 + this.Core.Write(ErrorMessages.IllegalAttributeWithOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Hash", "SourceFile"));
103 + }
104 +
105 + if (!String.IsNullOrEmpty(this.CertificatePublicKey))
106 + {
107 + this.Core.Write(ErrorMessages.IllegalAttributeWithOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "CertificatePublicKey", "SourceFile"));
108 + }
109 +
110 + if (!String.IsNullOrEmpty(this.CertificateThumbprint))
111 + {
112 + this.Core.Write(ErrorMessages.IllegalAttributeWithOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "CertificateThumbprint", "SourceFile"));
113 + }
114 }
115
116 if (!String.IsNullOrEmpty(this.Description))
@@ -120,17 +137,34 @@ namespace WixToolset.Core
137 {
138 if (String.IsNullOrEmpty(this.DownloadUrl))
139 {
123 - this.Core.Write(ErrorMessages.ExpectedAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "DownloadUrl", "Hash"));
140 + this.Core.Write(ErrorMessages.ExpectedAttributeWithoutOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "DownloadUrl", "SourceFile"));
141 }
142
143 if (String.IsNullOrEmpty(this.Name))
144 {
128 - this.Core.Write(ErrorMessages.ExpectedAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Name", "Hash"));
145 + this.Core.Write(ErrorMessages.ExpectedAttributeWithoutOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Name", "SourceFile"));
146 }
147
148 if (!this.Size.HasValue)
149 {
133 - this.Core.Write(ErrorMessages.ExpectedAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Size", "Hash"));
150 + this.Core.Write(ErrorMessages.ExpectedAttributeWithoutOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Size", "SourceFile"));
151 + }
152 +
153 + if (String.IsNullOrEmpty(this.Hash))
154 + {
155 + this.Core.Write(ErrorMessages.ExpectedAttributeWithoutOtherAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "Hash", "SourceFile"));
156 + }
157 +
158 + if (!String.IsNullOrEmpty(this.CertificatePublicKey) || !String.IsNullOrEmpty(this.CertificateThumbprint))
159 + {
160 + if (String.IsNullOrEmpty(this.CertificateThumbprint))
161 + {
162 + this.Core.Write(ErrorMessages.ExpectedAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "CertificateThumbprint", "CertificatePublicKey"));
163 + }
164 + else if (String.IsNullOrEmpty(this.CertificatePublicKey))
165 + {
166 + this.Core.Write(ErrorMessages.ExpectedAttribute(this.SourceLineNumbers, this.Element.Name.LocalName, "CertificatePublicKey", "CertificateThumbprint"));
167 + }
168 }
169
170 if (YesNoDefaultType.Yes == this.Compressed)
@@ -177,6 +211,8 @@ namespace WixToolset.Core
211 Hash = this.Hash,
212 FileSize = this.Size,
213 Version = this.Version,
214 + CertificatePublicKey = this.CertificatePublicKey,
215 + CertificateThumbprint = this.CertificateThumbprint
216 });
217
218 this.Core.CreateGroupAndOrderingRows(this.SourceLineNumbers, parentType, parentId, ComplexReferenceChildType.Payload, symbol.Id.Id, ComplexReferenceChildType.Unknown, null);
@@ -248,6 +284,16 @@ namespace WixToolset.Core
284 this.DownloadUrl = this.Core.GetAttributeValue(this.SourceLineNumbers, attrib);
285 }
286
287 + public void ParseCertificatePublicKey(XAttribute attrib)
288 + {
289 + this.CertificatePublicKey = this.Core.GetAttributeValue(this.SourceLineNumbers, attrib);
290 + }
291 +
292 + public void ParseCertificateThumbprint(XAttribute attrib)
293 + {
294 + this.CertificateThumbprint = this.Core.GetAttributeValue(this.SourceLineNumbers, attrib);
295 + }
296 +
297 public void ParseHash(XAttribute attrib)
298 {
299 this.Hash = this.Core.GetAttributeValue(this.SourceLineNumbers, attrib);
@@ -286,6 +332,5 @@ namespace WixToolset.Core
332 {
333 this.Version = this.Core.GetAttributeValue(this.SourceLineNumbers, attrib);
334 }
289 -
335 }
336 }
src/wix/WixToolset.Core/Compiler_Bundle.cs
+14
@@ -2459,6 +2459,20 @@ namespace WixToolset.Core
2459 case "SourceFile":
2460 compilerPayload.ParseSourceFile(attrib);
2461 break;
2462 + case "CertificatePublicKey":
2463 + allowed = compilerPayload.IsRemoteAllowed;
2464 + if (allowed)
2465 + {
2466 + compilerPayload.ParseCertificatePublicKey(attrib);
2467 + }
2468 + break;
2469 + case "CertificateThumbprint":
2470 + allowed = compilerPayload.IsRemoteAllowed;
2471 + if (allowed)
2472 + {
2473 + compilerPayload.ParseCertificateThumbprint(attrib);
2474 + }
2475 + break;
2476 case "DownloadUrl":
2477 compilerPayload.ParseDownloadUrl(attrib);
2478 break;
src/wix/heat/PayloadHarvester.cs deleted
-129
@@ -1,129 +0,0 @@
1 -// 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.
2 -
3 -namespace WixToolset.Harvesters
4 -{
5 - using System;
6 - using System.IO;
7 - using WixToolset.Core.Burn.Interfaces;
8 - using WixToolset.Data;
9 - using WixToolset.Data.Symbols;
10 - using WixToolset.Harvesters.Data;
11 - using WixToolset.Harvesters.Extensibility;
12 - using Wix = WixToolset.Harvesters.Serialize;
13 -
14 - /// <summary>
15 - /// Harvest WiX authoring for a payload from the file system.
16 - /// </summary>
17 - public sealed class PayloadHarvester : BaseHarvesterExtension
18 - {
19 - private bool setUniqueIdentifiers;
20 - private WixBundlePackageType packageType;
21 -
22 - private IPayloadHarvester payloadHarvester;
23 -
24 - /// <summary>
25 - /// Instantiate a new PayloadHarvester.
26 - /// </summary>
27 - public PayloadHarvester(IPayloadHarvester payloadHarvester, WixBundlePackageType packageType)
28 - {
29 - this.payloadHarvester = payloadHarvester;
30 -
31 - this.packageType = packageType;
32 - this.setUniqueIdentifiers = true;
33 - }
34 -
35 - /// <summary>
36 - /// Gets of sets the option to set unique identifiers.
37 - /// </summary>
38 - /// <value>The option to set unique identifiers.</value>
39 - public bool SetUniqueIdentifiers
40 - {
41 - get { return this.setUniqueIdentifiers; }
42 - set { this.setUniqueIdentifiers = value; }
43 - }
44 -
45 - /// <summary>
46 - /// Harvest a payload.
47 - /// </summary>
48 - /// <param name="argument">The path of the payload.</param>
49 - /// <returns>A harvested payload.</returns>
50 - public override Wix.Fragment[] Harvest(string argument)
51 - {
52 - if (null == argument)
53 - {
54 - throw new ArgumentNullException("argument");
55 - }
56 -
57 - string fullPath = Path.GetFullPath(argument);
58 -
59 - var remotePayload = this.HarvestRemotePayload(fullPath);
60 -
61 - var fragment = new Wix.Fragment();
62 - fragment.AddChild(remotePayload);
63 -
64 - return new Wix.Fragment[] { fragment };
65 - }
66 -
67 - /// <summary>
68 - /// Harvest a payload.
69 - /// </summary>
70 - /// <param name="path">The path of the payload.</param>
71 - /// <returns>A harvested payload.</returns>
72 - public Wix.RemotePayload HarvestRemotePayload(string path)
73 - {
74 - if (null == path)
75 - {
76 - throw new ArgumentNullException("path");
77 - }
78 -
79 - if (!File.Exists(path))
80 - {
81 - throw new WixException(HarvesterErrors.FileNotFound(path));
82 - }
83 -
84 - Wix.RemotePayload remotePayload;
85 -
86 - switch (this.packageType)
87 - {
88 - case WixBundlePackageType.Exe:
89 - remotePayload = new Wix.ExePackagePayload();
90 - break;
91 - case WixBundlePackageType.Msu:
92 - remotePayload = new Wix.MsuPackagePayload();
93 - break;
94 - default:
95 - throw new NotImplementedException();
96 - }
97 -
98 - var payloadSymbol = new WixBundlePayloadSymbol
99 - {
100 - SourceFile = new IntermediateFieldPathValue { Path = path },
101 - };
102 -
103 - this.payloadHarvester.HarvestStandardInformation(payloadSymbol);
104 -
105 - if (payloadSymbol.FileSize.HasValue)
106 - {
107 - remotePayload.Size = payloadSymbol.FileSize.Value;
108 - }
109 - remotePayload.Hash = payloadSymbol.Hash;
110 -
111 - if (!String.IsNullOrEmpty(payloadSymbol.Version))
112 - {
113 - remotePayload.Version = payloadSymbol.Version;
114 - }
115 -
116 - if (!String.IsNullOrEmpty(payloadSymbol.Description))
117 - {
118 - remotePayload.Description = payloadSymbol.Description;
119 - }
120 -
121 - if (!String.IsNullOrEmpty(payloadSymbol.DisplayName))
122 - {
123 - remotePayload.ProductName = payloadSymbol.DisplayName;
124 - }
125 -
126 - return remotePayload;
127 - }
128 - }
129 -}
src/wix/heat/UtilHeatExtension.cs
-8
@@ -87,14 +87,6 @@ namespace WixToolset.Harvesters
87 harvesterExtension = new FileHarvester();
88 active = true;
89 break;
90 - case "exepackagepayload":
91 - harvesterExtension = new PayloadHarvester(this.PayloadHarvester, WixBundlePackageType.Exe);
92 - active = true;
93 - break;
94 - case "msupackagepayload":
95 - harvesterExtension = new PayloadHarvester(this.PayloadHarvester, WixBundlePackageType.Msu);
96 - active = true;
97 - break;
90 case "perf":
91 harvesterExtension = new PerformanceCategoryHarvester();
92 active = true;
src/wix/test/WixToolsetTest.Converters.Symbolizer/WixToolsetTest.Converters.Symbolizer.v3.ncrunchproject
+3
@@ -1,5 +1,8 @@
1 <ProjectConfiguration>
2 <Settings>
3 + <HiddenComponentWarnings>
4 + <Value>LostReference</Value>
5 + </HiddenComponentWarnings>
6 <IgnoredTests>
7 <NamedTestSelector>
8 <TestName>WixToolsetTest.Converters.Symbolizer.ConvertSymbolsFixture.CanLoadWixoutAndConvertToIntermediate</TestName>
src/wix/test/WixToolsetTest.Core.Native/CertificateHashesFixture.cs new
+58
@@ -0,0 +1,58 @@
1 +// 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.
2 +
3 +namespace WixToolsetTest.CoreNative
4 +{
5 + using System.ComponentModel;
6 + using System.Linq;
7 + using WixToolset.Core.Native;
8 + using WixToolsetTest.CoreNative.Utility;
9 + using Xunit;
10 +
11 + public class CertificateHashesFixture
12 + {
13 + [Fact]
14 + public void CanGetHashesFromSignedFile()
15 + {
16 + var cabFile = TestData.Get(@"TestData\test.cab");
17 +
18 + var hashes = CertificateHashes.Read(new[] { cabFile });
19 +
20 + var hash = hashes.Single();
21 + Assert.Equal(cabFile, hash.Path);
22 + Assert.Equal("7EC90B3FC3D580EB571210011F1095E149DCC6BB", hash.PublicKey);
23 + Assert.Equal("0B13494DB50BC185A34389BBBAA01EDD1CF56350", hash.Thumbprint);
24 + Assert.Null(hash.Exception);
25 + }
26 +
27 + [Fact]
28 + public void CannotGetHashesFromUnsignedFile()
29 + {
30 + var txtFile = TestData.Get(@"TestData\test.txt");
31 +
32 + var hashes = CertificateHashes.Read(new[] { txtFile });
33 +
34 + var hash = hashes.Single();
35 + Assert.Equal(txtFile, hash.Path);
36 + Assert.Null(hash.Exception);
37 + }
38 +
39 + [Fact]
40 + public void CanGetMultipleHashes()
41 + {
42 + var cabFile = TestData.Get(@"TestData\test.cab");
43 + var txtFile = TestData.Get(@"TestData\test.txt");
44 +
45 + var hashes = CertificateHashes.Read(new[] { cabFile, txtFile });
46 +
47 + Assert.Equal(cabFile, hashes[0].Path);
48 + Assert.Equal("7EC90B3FC3D580EB571210011F1095E149DCC6BB", hashes[0].PublicKey);
49 + Assert.Equal("0B13494DB50BC185A34389BBBAA01EDD1CF56350", hashes[0].Thumbprint);
50 + Assert.Null(hashes[0].Exception);
51 +
52 + Assert.Equal(txtFile, hashes[1].Path);
53 + Assert.Empty(hashes[1].PublicKey);
54 + Assert.Empty(hashes[1].Thumbprint);
55 + Assert.Null(hashes[1].Exception);
56 + }
57 + }
58 +}
src/wix/test/WixToolsetTest.Core.Native/TestData/test.cab
Binary files a/src/wix/test/WixToolsetTest.Core.Native/TestData/test.cab and b/src/wix/test/WixToolsetTest.Core.Native/TestData/test.cab differ
src/wix/test/WixToolsetTest.CoreIntegration/PackagePayloadFixture.cs
+119 -3
@@ -56,6 +56,26 @@ namespace WixToolsetTest.CoreIntegration
56 }
57 }
58
59 + [Fact]
60 + public void CanSpecifyPackagePayloadWithCertificate()
61 + {
62 + var folder = TestData.Get(@"TestData", "PackagePayload");
63 +
64 + using (var fs = new DisposableFileSystem())
65 + {
66 + var baseFolder = fs.GetFolder();
67 +
68 + var result = WixRunner.Execute(new[]
69 + {
70 + "build",
71 + Path.Combine(folder, "SpecifiedCertificate.wxs"),
72 + "-o", Path.Combine(baseFolder, "test.wixlib")
73 + });
74 +
75 + result.AssertSuccess();
76 + }
77 + }
78 +
79 [Fact]
80 public void ErrorWhenMissingSourceFileAndHash()
81 {
@@ -75,7 +95,7 @@ namespace WixToolsetTest.CoreIntegration
95 Assert.Equal(44, result.ExitCode);
96 WixAssert.CompareLineByLine(new[]
97 {
78 - "The MsuPackagePayload element's SourceFile or Hash attribute was not found; one of these is required.",
98 + "The MsuPackagePayload element's SourceFile, CertificatePublicKey, or Hash attribute was not found; one of these is required.",
99 }, result.Messages.Select(m => m.ToString()).ToArray());
100 }
101 }
@@ -144,10 +164,10 @@ namespace WixToolsetTest.CoreIntegration
164 "-o", Path.Combine(baseFolder, "test.wixlib")
165 });
166
147 - Assert.Equal(10, result.ExitCode);
167 + Assert.Equal(408, result.ExitCode);
168 WixAssert.CompareLineByLine(new[]
169 {
150 - "The MsuPackagePayload/@DownloadUrl attribute was not found; it is required when attribute Hash is specified.",
170 + "The MsuPackagePayload element's DownloadUrl attribute was not found; it is required without attribute SourceFile present.",
171 }, result.Messages.Select(m => m.ToString()).ToArray());
172 }
173 }
@@ -176,6 +196,102 @@ namespace WixToolsetTest.CoreIntegration
196 }
197 }
198
199 + [Fact]
200 + public void ErrorWhenSpecifiedSourceFileAndCertificatePublicKey()
201 + {
202 + var folder = TestData.Get(@"TestData", "PackagePayload");
203 +
204 + using (var fs = new DisposableFileSystem())
205 + {
206 + var baseFolder = fs.GetFolder();
207 +
208 + var result = WixRunner.Execute(new[]
209 + {
210 + "build",
211 + Path.Combine(folder, "SpecifiedSourceFileAndCertificatePublicKey.wxs"),
212 + "-o", Path.Combine(baseFolder, "test.wixlib")
213 + });
214 +
215 + WixAssert.CompareLineByLine(new[]
216 + {
217 + "The ExePackagePayload/@CertificatePublicKey attribute cannot be specified when attribute SourceFile is present.",
218 + }, result.Messages.Select(m => m.ToString()).ToArray());
219 + Assert.Equal(35, result.ExitCode);
220 + }
221 + }
222 +
223 + [Fact]
224 + public void ErrorWhenSpecifiedSourceFileAndCertificateThumprint()
225 + {
226 + var folder = TestData.Get(@"TestData", "PackagePayload");
227 +
228 + using (var fs = new DisposableFileSystem())
229 + {
230 + var baseFolder = fs.GetFolder();
231 +
232 + var result = WixRunner.Execute(new[]
233 + {
234 + "build",
235 + Path.Combine(folder, "SpecifiedSourceFileAndCertificateThumbprint.wxs"),
236 + "-o", Path.Combine(baseFolder, "test.wixlib")
237 + });
238 +
239 + WixAssert.CompareLineByLine(new[]
240 + {
241 + "The ExePackagePayload/@CertificateThumbprint attribute cannot be specified when attribute SourceFile is present.",
242 + }, result.Messages.Select(m => m.ToString()).ToArray());
243 + Assert.Equal(35, result.ExitCode);
244 + }
245 + }
246 +
247 + [Fact]
248 + public void ErrorWhenSpecifiedCertificateThumbprintWithoutPublicKey()
249 + {
250 + var folder = TestData.Get(@"TestData", "PackagePayload");
251 +
252 + using (var fs = new DisposableFileSystem())
253 + {
254 + var baseFolder = fs.GetFolder();
255 +
256 + var result = WixRunner.Execute(new[]
257 + {
258 + "build",
259 + Path.Combine(folder, "SpecifiedHashAndCertificatePublicKey.wxs"),
260 + "-o", Path.Combine(baseFolder, "test.wixlib")
261 + });
262 +
263 + WixAssert.CompareLineByLine(new[]
264 + {
265 + "The ExePackagePayload/@CertificatePublicKey attribute was not found; it is required when attribute CertificateThumbprint is specified.",
266 + }, result.Messages.Select(m => m.ToString()).ToArray());
267 + Assert.Equal(10, result.ExitCode);
268 + }
269 + }
270 +
271 + [Fact]
272 + public void ErrorWhenSpecifiedCertificatePublicKeyWithoutThumbprint()
273 + {
274 + var folder = TestData.Get(@"TestData", "PackagePayload");
275 +
276 + using (var fs = new DisposableFileSystem())
277 + {
278 + var baseFolder = fs.GetFolder();
279 +
280 + var result = WixRunner.Execute(new[]
281 + {
282 + "build",
283 + Path.Combine(folder, "SpecifiedCertificatePublicKeyWithoutThumbprint.wxs"),
284 + "-o", Path.Combine(baseFolder, "test.wixlib")
285 + });
286 +
287 + WixAssert.CompareLineByLine(new[]
288 + {
289 + "The ExePackagePayload/@CertificateThumbprint attribute was not found; it is required when attribute CertificatePublicKey is specified.",
290 + }, result.Messages.Select(m => m.ToString()).ToArray());
291 + Assert.Equal(10, result.ExitCode);
292 + }
293 + }
294 +
295 [Fact]
296 public void ErrorWhenWrongPackagePayloadInPayloadGroup()
297 {
src/wix/test/WixToolsetTest.CoreIntegration/RemotePayloadFixture.cs new
+172
@@ -0,0 +1,172 @@
1 +// 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.
2 +
3 +namespace WixToolsetTest.CoreIntegration
4 +{
5 + using System.IO;
6 + using System.Linq;
7 + using WixBuildTools.TestSupport;
8 + using WixToolset.Core.TestPackage;
9 + using Xunit;
10 +
11 + public class RemotePayloadFixture
12 + {
13 + [Fact]
14 + public void CanGetRemotePayload()
15 + {
16 + var folder = TestData.Get(@"TestData");
17 +
18 + using (var fs = new DisposableFileSystem())
19 + {
20 + var outputFolder = fs.GetFolder();
21 + var outFile = Path.Combine(outputFolder, "out.xml");
22 +
23 + var result = WixRunner.Execute(new[]
24 + {
25 + "burn", "remotepayload",
26 + Path.Combine(folder, ".Data", "burn.exe"),
27 + "-downloadurl", "https://www.example.com/files/{0}",
28 + "-o", outFile
29 + });
30 +
31 + result.AssertSuccess();
32 +
33 + var elements = File.ReadAllLines(outFile);
34 + elements = elements.Select(s => s.Replace("\"", "'")).ToArray();
35 +
36 + WixAssert.CompareLineByLine(new[]
37 + {
38 + @"<ExePackagePayload Name='burn.exe' ProductName='Windows Installer XML Toolset' Description='WiX Toolset Bootstrapper' DownloadUrl='https://www.example.com/files/burn.exe' Hash='F6E722518AC3AB7E31C70099368D5770788C179AA23226110DCF07319B1E1964E246A1E8AE72E2CF23E0138AFC281BAFDE45969204405E114EB20C8195DA7E5E' Size='463360' Version='3.14.1703.0' />",
39 + }, elements);
40 + }
41 + }
42 +
43 + [Fact]
44 + public void CanGetRemoteMsuPayload()
45 + {
46 + var folder = TestData.Get(@"TestData");
47 +
48 + using (var fs = new DisposableFileSystem())
49 + {
50 + var outputFolder = fs.GetFolder();
51 + var outFile = Path.Combine(outputFolder, "out.xml");
52 +
53 + var result = WixRunner.Execute(new[]
54 + {
55 + "burn", "remotepayload",
56 + Path.Combine(folder, ".Data", "Windows8.1-KB2937592-x86.msu"),
57 + "-o", outFile
58 + });
59 +
60 + result.AssertSuccess();
61 +
62 + var elements = File.ReadAllLines(outFile);
63 + elements = elements.Select(s => s.Replace("\"", "'")).ToArray();
64 +
65 + WixAssert.CompareLineByLine(new[]
66 + {
67 + @"<MsuPackagePayload Name='Windows8.1-KB2937592-x86.msu' Hash='904ADEA6AB675ACE16483138BF3F5850FD56ACB6E3A13AFA7263ED49C68CCE6CF84D6AAD6F99AAF175A95EE1A56C787C5AD968019056490B1073E7DBB7B9B7BE' Size='309544' />",
68 + }, elements);
69 + }
70 + }
71 +
72 + [Fact]
73 + public void CanGetRemotePayloadWithCertificate()
74 + {
75 + var folder = TestData.Get(@"TestData");
76 +
77 + using (var fs = new DisposableFileSystem())
78 + {
79 + var outputFolder = fs.GetFolder();
80 + var outFile = Path.Combine(outputFolder, "out.xml");
81 +
82 + var result = WixRunner.Execute(new[]
83 + {
84 + "burn", "remotepayload",
85 + "-usecertificate",
86 + Path.Combine(folder, ".Data", "burn.exe"),
87 + Path.Combine(folder, ".Data", "signed_cab1.cab"),
88 + "-o", outFile
89 + });
90 +
91 + result.AssertSuccess();
92 +
93 + var elements = File.ReadAllLines(outFile);
94 + elements = elements.Select(s => s.Replace("\"", "'")).ToArray();
95 +
96 + WixAssert.CompareLineByLine(new[]
97 + {
98 + @"<ExePackagePayload Name='burn.exe' ProductName='Windows Installer XML Toolset' Description='WiX Toolset Bootstrapper' Hash='F6E722518AC3AB7E31C70099368D5770788C179AA23226110DCF07319B1E1964E246A1E8AE72E2CF23E0138AFC281BAFDE45969204405E114EB20C8195DA7E5E' Size='463360' Version='3.14.1703.0' />",
99 + @"<Payload Name='signed_cab1.cab' CertificatePublicKey='BBD1B48A37503767C71F455624967D406A5D66C3' CertificateThumbprint='DE13B4CE635E3F63AA2394E66F95C460267BC82F' Hash='D8D3842403710E1F6036A62543224855CADF546853933C2B17BA99D789D4347B36717687C022678A9D3DE749DFC1482DAAB92B997B62BB32A8A6828B9D04C414' Size='1585' />",
100 + }, elements);
101 + }
102 + }
103 +
104 + [Fact]
105 + public void CanGetRemotePayloadWithoutCertificate()
106 + {
107 + var folder = TestData.Get(@"TestData");
108 +
109 + using (var fs = new DisposableFileSystem())
110 + {
111 + var outputFolder = fs.GetFolder();
112 + var outFile = Path.Combine(outputFolder, "out.xml");
113 +
114 + var result = WixRunner.Execute(new[]
115 + {
116 + "burn", "remotepayload",
117 + Path.Combine(folder, ".Data", "burn.exe"),
118 + Path.Combine(folder, ".Data", "signed_cab1.cab"),
119 + "-o", outFile
120 + });
121 +
122 + result.AssertSuccess();
123 +
124 + var elements = File.ReadAllLines(outFile);
125 + elements = elements.Select(s => s.Replace("\"", "'")).ToArray();
126 +
127 + WixAssert.CompareLineByLine(new[]
128 + {
129 + @"<ExePackagePayload Name='burn.exe' ProductName='Windows Installer XML Toolset' Description='WiX Toolset Bootstrapper' Hash='F6E722518AC3AB7E31C70099368D5770788C179AA23226110DCF07319B1E1964E246A1E8AE72E2CF23E0138AFC281BAFDE45969204405E114EB20C8195DA7E5E' Size='463360' Version='3.14.1703.0' />",
130 + @"<Payload Name='signed_cab1.cab' Hash='D8D3842403710E1F6036A62543224855CADF546853933C2B17BA99D789D4347B36717687C022678A9D3DE749DFC1482DAAB92B997B62BB32A8A6828B9D04C414' Size='1585' />",
131 + }, elements);
132 + }
133 + }
134 +
135 + [Fact]
136 + public void CanGetRemotePayloadsRecursive()
137 + {
138 + var folder = TestData.Get(@"TestData");
139 +
140 + using (var fs = new DisposableFileSystem())
141 + {
142 + var outputFolder = fs.GetFolder();
143 + var outFile = Path.Combine(outputFolder, "out.xml");
144 +
145 + var result = WixRunner.Execute(new[]
146 + {
147 + "burn", "remotepayload",
148 + "-recurse",
149 + "-du", "https://www.example.com/files/{0}",
150 + Path.Combine(folder, ".Data", "burn.exe"),
151 + Path.Combine(folder, "RemotePayload", "*"),
152 + "-basepath", folder,
153 + "-bp", Path.Combine(folder, ".Data"),
154 + "-o", outFile
155 + });
156 +
157 + result.AssertSuccess();
158 +
159 + var elements = File.ReadAllLines(outFile);
160 + elements = elements.Select(s => s.Replace("\"", "'")).ToArray();
161 +
162 + WixAssert.CompareLineByLine(new[]
163 + {
164 + @"<ExePackagePayload Name='burn.exe' ProductName='Windows Installer XML Toolset' Description='WiX Toolset Bootstrapper' DownloadUrl='https://www.example.com/files/burn.exe' Hash='F6E722518AC3AB7E31C70099368D5770788C179AA23226110DCF07319B1E1964E246A1E8AE72E2CF23E0138AFC281BAFDE45969204405E114EB20C8195DA7E5E' Size='463360' Version='3.14.1703.0' />",
165 + @"<Payload Name='a.dat' DownloadUrl='https://www.example.com/files/RemotePayload/a.dat' Hash='D13926E5CBE5ED8B46133F9199FAF2FF25B25981C67A31AE2BC3F6C20390FACBFADCD89BD22D3445D95B989C8EACFB1E68DB634BECB5C9624865BA453BCE362A' Size='16' />",
166 + @"<Payload Name='b.dat' DownloadUrl='https://www.example.com/files/RemotePayload/subfolder/b.dat' Hash='5F94707BC29ADFE3B9615E6753388707FD0B8F5FD9EEEC2B17E21E72F1635FF7D7A101E7D14F614E111F263CB9AC4D0940BE1247881A7844F226D6C400293D8E' Size='37' />",
167 + @"<Payload Name='c.dat' DownloadUrl='https://www.example.com/files/RemotePayload/subfolder/c.dat' Hash='97D6209A5571E05E4F72F9C6BF0987651FA03E63F971F9B53C2B3D798A666D9864F232D4E2D6442E47D9D72B282309B6EEFF4EE017B43B706FA92A0F5EF74734' Size='42' />",
168 + }, elements);
169 + }
170 + }
171 + }
172 +}
src/wix/test/WixToolsetTest.CoreIntegration/TestData/.Data/Windows8.1-KB2937592-x86.msu renamed
src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedCertificate.wxs new
+10
@@ -0,0 +1,10 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
3 + <Fragment>
4 + <PackageGroup Id="BundlePackages">
5 + <ExePackage Id="SpecifiedSourceFileAndHash" Permanent="yes" DetectCondition="none">
6 + <ExePackagePayload CertificatePublicKey="abcd" CertificateThumbprint="abcd" Hash="1234" DownloadUrl="https://example.com/" Name="fake.exe" Size="100" />
7 + </ExePackage>
8 + </PackageGroup>
9 + </Fragment>
10 +</Wix>
src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedCertificatePublicKeyWithoutThumbprint.wxs new
+10
@@ -0,0 +1,10 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
3 + <Fragment>
4 + <PackageGroup Id="BundlePackages">
5 + <ExePackage Id="SpecifiedSourceFileAndHash" Permanent="yes" DetectCondition="none">
6 + <ExePackagePayload CertificatePublicKey="abcd" Hash="123" DownloadUrl="https://example.com/" Name="fake.exe" Size="100" />
7 + </ExePackage>
8 + </PackageGroup>
9 + </Fragment>
10 +</Wix>
src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedHashAndCertificatePublicKey.wxs new
+10
@@ -0,0 +1,10 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
3 + <Fragment>
4 + <PackageGroup Id="BundlePackages">
5 + <ExePackage Id="SpecifiedSourceFileAndHash" Permanent="yes" DetectCondition="none">
6 + <ExePackagePayload Hash="abcd" CertificateThumbprint="abcd" DownloadUrl="https://example.com/" Name="fake.exe" Size="100" />
7 + </ExePackage>
8 + </PackageGroup>
9 + </Fragment>
10 +</Wix>
src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedSourceFileAndCertificatePublicKey.wxs new
+10
@@ -0,0 +1,10 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
3 + <Fragment>
4 + <PackageGroup Id="BundlePackages">
5 + <ExePackage Id="SpecifiedSourceFileAndHash" Permanent="yes" DetectCondition="none">
6 + <ExePackagePayload SourceFile="example.exe" CertificatePublicKey="abcd" />
7 + </ExePackage>
8 + </PackageGroup>
9 + </Fragment>
10 +</Wix>
src/wix/test/WixToolsetTest.CoreIntegration/TestData/PackagePayload/SpecifiedSourceFileAndCertificateThumbprint.wxs new
+10
@@ -0,0 +1,10 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
3 + <Fragment>
4 + <PackageGroup Id="BundlePackages">
5 + <ExePackage Id="SpecifiedSourceFileAndHash" Permanent="yes" DetectCondition="none">
6 + <ExePackagePayload SourceFile="example.exe" CertificateThumbprint="abcd" />
7 + </ExePackage>
8 + </PackageGroup>
9 + </Fragment>
10 +</Wix>
src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/a.dat new
+1
@@ -0,0 +1 @@
1 +This is a.dat.
src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/subfolder/b.dat new
+1
@@ -0,0 +1 @@
1 +This is b.dat. A little bit longer.
src/wix/test/WixToolsetTest.CoreIntegration/TestData/RemotePayload/subfolder/c.dat new
+1
@@ -0,0 +1 @@
1 +This is c.dat. A little bit longer, now!
src/wix/test/WixToolsetTest.Heat/PayloadTests.cs deleted
-66
@@ -1,66 +0,0 @@
1 -// 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.
2 -
3 -namespace WixToolsetTest.Harvesters
4 -{
5 - using System;
6 - using System.IO;
7 - using WixBuildTools.TestSupport;
8 - using Xunit;
9 -
10 - public class PayloadTests
11 - {
12 - [Fact]
13 - public void CanHarvestExePackagePayload()
14 - {
15 - var folder = TestData.Get(@"TestData");
16 -
17 - using (var fs = new DisposableFileSystem())
18 - {
19 - var baseFolder = fs.GetFolder();
20 - var outputFilePath = Path.Combine(baseFolder, "test.wxs");
21 -
22 - var result = HeatRunner.Execute(new[]
23 - {
24 - "exepackagepayload",
25 - Path.Combine(folder, ".Data", "burn.exe"),
26 - "-o", outputFilePath,
27 - });
28 -
29 - result.AssertSuccess();
30 -
31 - Assert.True(File.Exists(outputFilePath));
32 -
33 - var expected = File.ReadAllText(Path.Combine(folder, "Payload", "HarvestedExePackagePayload.wxs")).Replace("\r\n", "\n");
34 - var actual = File.ReadAllText(outputFilePath).Replace("\r\n", "\n");
35 - Assert.Equal(expected, actual);
36 - }
37 - }
38 -
39 - [Fact]
40 - public void CanHarvestMsuPackagePayload()
41 - {
42 - var folder = TestData.Get(@"TestData");
43 -
44 - using (var fs = new DisposableFileSystem())
45 - {
46 - var baseFolder = fs.GetFolder();
47 - var outputFilePath = Path.Combine(baseFolder, "test.wxs");
48 -
49 - var result = HeatRunner.Execute(new[]
50 - {
51 - "msupackagepayload",
52 - Path.Combine(folder, ".Data", "Windows8.1-KB2937592-x86.msu"),
53 - "-o", outputFilePath,
54 - });
55 -
56 - result.AssertSuccess();
57 -
58 - Assert.True(File.Exists(outputFilePath));
59 -
60 - var expected = File.ReadAllText(Path.Combine(folder, "Payload", "HarvestedMsuPackagePayload.wxs")).Replace("\r\n", "\n");
61 - var actual = File.ReadAllText(outputFilePath).Replace("\r\n", "\n");
62 - Assert.Equal(expected, actual);
63 - }
64 - }
65 - }
66 -}
src/wix/test/WixToolsetTest.Heat/TestData/.Data/burn.exe
Binary files a/src/wix/test/WixToolsetTest.Heat/TestData/.Data/burn.exe and /dev/null differ
src/wix/test/WixToolsetTest.Heat/TestData/Payload/HarvestedExePackagePayload.wxs deleted
-6
@@ -1,6 +0,0 @@
1 -<?xml version="1.0" encoding="utf-8"?>
2 -<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
3 - <Fragment>
4 - <ExePackagePayload Description="WiX Toolset Bootstrapper" Hash="F6E722518AC3AB7E31C70099368D5770788C179AA23226110DCF07319B1E1964E246A1E8AE72E2CF23E0138AFC281BAFDE45969204405E114EB20C8195DA7E5E" ProductName="Windows Installer XML Toolset" Size="463360" Version="3.14.1703.0" />
5 - </Fragment>
6 -</Wix>
\ No newline at end of file
src/wix/test/WixToolsetTest.Heat/TestData/Payload/HarvestedMsuPackagePayload.wxs deleted
-6
@@ -1,6 +0,0 @@
1 -<?xml version="1.0" encoding="utf-8"?>
2 -<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
3 - <Fragment>
4 - <MsuPackagePayload Hash="904ADEA6AB675ACE16483138BF3F5850FD56ACB6E3A13AFA7263ED49C68CCE6CF84D6AAD6F99AAF175A95EE1A56C787C5AD968019056490B1073E7DBB7B9B7BE" Size="309544" />
5 - </Fragment>
6 -</Wix>
\ No newline at end of file
src/wix/test/WixToolsetTest.Heat/WixToolsetTest.Heat.csproj
-4
@@ -8,10 +8,6 @@
8 <SignOutput>false</SignOutput>
9 </PropertyGroup>
10
11 - <ItemGroup>
12 - <Content Include="TestData\**" CopyToOutputDirectory="PreserveNewest" />
13 - </ItemGroup>
14 -
11 <ItemGroup>
12 <ProjectReference Include="..\..\heat\heat.csproj" />
13 <ProjectReference Include="..\..\WixToolset.Core.TestPackage\WixToolset.Core.TestPackage.csproj" />
src/wix/wixnative/certhashes.cpp new
+138
@@ -0,0 +1,138 @@
1 +// 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.
2 +
3 +#include "precomp.h"
4 +
5 +#define SHA1_HASH_LEN 20
6 +
7 +static HRESULT GetPublicKeyIdentifierAndThumbprint(
8 + __in_z LPCWSTR wzPath,
9 + __inout_z LPWSTR* psczPublicKeyIdentifier,
10 + __inout_z LPWSTR* psczThumbprint);
11 +
12 +static HRESULT GetChainContext(
13 + __in_z LPCWSTR wzPath,
14 + __out PCCERT_CHAIN_CONTEXT* ppChainContext);
15 +
16 +
17 +HRESULT CertificateHashesCommand(
18 + __in int argc,
19 + __in_ecount(argc) LPWSTR argv[])
20 +{
21 + Unused(argc);
22 + Unused(argv);
23 +
24 + HRESULT hr = S_OK;
25 +
26 + LPWSTR sczFilePath = NULL;
27 + LPWSTR sczPublicKeyIdentifier = NULL;
28 + LPWSTR sczThumbprint = NULL;
29 +
30 + hr = WixNativeReadStdinPreamble();
31 + ExitOnFailure(hr, "Failed to read stdin preamble before reading paths to get certificate hashes");
32 +
33 + // Get the hash for each provided file.
34 + for (;;)
35 + {
36 + hr = ConsoleReadW(&sczFilePath);
37 + ConsoleExitOnFailure(hr, CONSOLE_COLOR_RED, "Failed to read file path to signed file from stdin");
38 +
39 + if (!*sczFilePath)
40 + {
41 + break;
42 + }
43 +
44 + hr = GetPublicKeyIdentifierAndThumbprint(sczFilePath, &sczPublicKeyIdentifier, &sczThumbprint);
45 + if (FAILED(hr))
46 + {
47 + // Treat no signature as success without finding certificate hashes.
48 + ConsoleWriteLine(CONSOLE_COLOR_NORMAL, "%ls\t\t\t0x%x", sczFilePath, TRUST_E_NOSIGNATURE == hr ? 0 : hr);
49 + }
50 + else
51 + {
52 + ConsoleWriteLine(CONSOLE_COLOR_NORMAL, "%ls\t%ls\t%ls\t0x%x", sczFilePath, sczPublicKeyIdentifier, sczThumbprint, hr);
53 + }
54 + }
55 +
56 +LExit:
57 + ReleaseStr(sczThumbprint);
58 + ReleaseStr(sczPublicKeyIdentifier);
59 + ReleaseStr(sczFilePath);
60 +
61 + return hr;
62 +}
63 +
64 +static HRESULT GetPublicKeyIdentifierAndThumbprint(
65 + __in_z LPCWSTR wzPath,
66 + __inout_z LPWSTR* psczPublicKeyIdentifier,
67 + __inout_z LPWSTR* psczThumbprint)
68 +{
69 + HRESULT hr = S_OK;
70 + PCCERT_CHAIN_CONTEXT pChainContext = NULL;
71 + PCCERT_CONTEXT pCertContext = NULL;
72 + BYTE rgbPublicKeyIdentifier[SHA1_HASH_LEN] = { };
73 + DWORD cbPublicKeyIdentifier = sizeof(rgbPublicKeyIdentifier);
74 + BYTE* pbThumbprint = NULL;
75 + DWORD cbThumbprint = 0;
76 +
77 + hr = GetChainContext(wzPath, &pChainContext);
78 + ExitOnFailure(hr, "Failed to get chain context for file: %ls", wzPath);
79 +
80 + pCertContext = pChainContext->rgpChain[0]->rgpElement[0]->pCertContext;
81 +
82 + // Get the certificate's public key identifier and thumbprint.
83 + if (!::CryptHashPublicKeyInfo(NULL, CALG_SHA1, 0, X509_ASN_ENCODING, &pCertContext->pCertInfo->SubjectPublicKeyInfo, rgbPublicKeyIdentifier, &cbPublicKeyIdentifier))
84 + {
85 + ExitWithLastError(hr, "Failed to get certificate public key identifier from file: %ls", wzPath);
86 + }
87 +
88 + hr = CertReadProperty(pCertContext, CERT_SHA1_HASH_PROP_ID, &pbThumbprint, &cbThumbprint);
89 + ExitOnFailure(hr, "Failed to read certificate thumbprint from file: %ls", wzPath);
90 +
91 + // Get the public key indentifier and thumbprint in hex.
92 + hr = StrAllocHexEncode(rgbPublicKeyIdentifier, cbPublicKeyIdentifier, psczPublicKeyIdentifier);
93 + ExitOnFailure(hr, "Failed to convert certificate public key to hex for file: %ls", wzPath);
94 +
95 + hr = StrAllocHexEncode(pbThumbprint, cbThumbprint, psczThumbprint);
96 + ExitOnFailure(hr, "Failed to convert certificate thumbprint to hex for file: %ls", wzPath);
97 +
98 +LExit:
99 + ReleaseMem(pbThumbprint);
100 + return hr;
101 +}
102 +
103 +static HRESULT GetChainContext(
104 + __in_z LPCWSTR wzPath,
105 + __out PCCERT_CHAIN_CONTEXT* ppChainContext)
106 +{
107 + HRESULT hr = S_OK;
108 +
109 + GUID guidAuthenticode = WINTRUST_ACTION_GENERIC_VERIFY_V2;
110 + WINTRUST_FILE_INFO wfi = { };
111 + WINTRUST_DATA wtd = { };
112 + CRYPT_PROVIDER_DATA* pProviderData = NULL;
113 + CRYPT_PROVIDER_SGNR* pSigner = NULL;
114 +
115 + wfi.cbStruct = sizeof(wfi);
116 + wfi.pcwszFilePath = wzPath;
117 +
118 + wtd.cbStruct = sizeof(wtd);
119 + wtd.dwUnionChoice = WTD_CHOICE_FILE;
120 + wtd.pFile = &wfi;
121 + wtd.dwStateAction = WTD_STATEACTION_VERIFY;
122 + wtd.dwProvFlags = WTD_REVOCATION_CHECK_NONE | WTD_HASH_ONLY_FLAG | WTD_CACHE_ONLY_URL_RETRIEVAL;
123 + wtd.dwUIChoice = WTD_UI_NONE;
124 +
125 + hr = ::WinVerifyTrust(static_cast<HWND>(INVALID_HANDLE_VALUE), &guidAuthenticode, &wtd);
126 + ExitOnFailure(hr, "Failed to verify certificate on file: %ls", wzPath);
127 +
128 + pProviderData = ::WTHelperProvDataFromStateData(wtd.hWVTStateData);
129 + ExitOnNullWithLastError(pProviderData, hr, "Failed to get provider state from authenticode certificate on file: %ls", wzPath);
130 +
131 + pSigner = ::WTHelperGetProvSignerFromChain(pProviderData, 0, FALSE, 0);
132 + ExitOnNullWithLastError(pSigner, hr, "Failed to get signer chain from authenticode certificate on file: %ls", wzPath);
133 +
134 + *ppChainContext = pSigner->pChainContext;
135 +
136 +LExit:
137 + return hr;
138 +}
src/wix/wixnative/precomp.h
+4
@@ -4,9 +4,12 @@
4 #include <windows.h>
5 #include <aclapi.h>
6 #include <mergemod.h>
7 +#include <softpub.h>
8 #include <strsafe.h>
9 +#include <wintrust.h>
10
11 #include "dutil.h"
12 +#include "certutil.h"
13 #include "conutil.h"
14 #include "memutil.h"
15 #include "pathutil.h"
@@ -15,6 +18,7 @@
18 #include "cabutil.h"
19
20 HRESULT WixNativeReadStdinPreamble();
21 +HRESULT CertificateHashesCommand(__in int argc, __in_ecount(argc) LPWSTR argv[]);
22 HRESULT SmartCabCommand(__in int argc, __in_ecount(argc) LPWSTR argv[]);
23 HRESULT ResetAclsCommand(__in int argc, __in_ecount(argc) LPWSTR argv[]);
24 HRESULT EnumCabCommand(__in int argc, __in_ecount(argc) LPWSTR argv[]);
src/wix/wixnative/wixnative.cpp
+4
@@ -24,6 +24,10 @@ int __cdecl wmain(int argc, LPWSTR argv[])
24 {
25 hr = EnumCabCommand(argc - 2, argv + 2);
26 }
27 + else if (CSTR_EQUAL == ::CompareString(LOCALE_INVARIANT, NORM_IGNORECASE, argv[1], -1, L"certhashes", -1))
28 + {
29 + hr = CertificateHashesCommand(argc - 2, argv + 2);
30 + }
31 else if (CSTR_EQUAL == ::CompareString(LOCALE_INVARIANT, NORM_IGNORECASE, argv[1], -1, L"resetacls", -1))
32 {
33 hr = ResetAclsCommand(argc - 2, argv + 2);
src/wix/wixnative/wixnative.vcxproj
+2 -1
@@ -42,7 +42,7 @@
42 <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
43
44 <PropertyGroup>
45 - <ProjectAdditionalLinkLibraries>crypt32.lib;cabinet.lib;msi.lib</ProjectAdditionalLinkLibraries>
45 + <ProjectAdditionalLinkLibraries>crypt32.lib;cabinet.lib;msi.lib;wintrust.lib</ProjectAdditionalLinkLibraries>
46 </PropertyGroup>
47
48 <ItemGroup>
@@ -50,6 +50,7 @@
50 <ClCompile Include="precomp.cpp">
51 <PrecompiledHeader>Create</PrecompiledHeader>
52 </ClCompile>
53 + <ClCompile Include="certhashes.cpp" />
54 <ClCompile Include="enumcab.cpp" />
55 <ClCompile Include="extractcab.cpp" />
56 <ClCompile Include="resetacls.cpp" />