aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core/Inscriber.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core/Inscriber.cs')
-rw-r--r--src/WixToolset.Core/Inscriber.cs457
1 files changed, 457 insertions, 0 deletions
diff --git a/src/WixToolset.Core/Inscriber.cs b/src/WixToolset.Core/Inscriber.cs
new file mode 100644
index 00000000..5b467ec1
--- /dev/null
+++ b/src/WixToolset.Core/Inscriber.cs
@@ -0,0 +1,457 @@
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
3namespace WixToolset
4{
5 using System;
6 using System.CodeDom.Compiler;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.IO;
10 using System.Runtime.InteropServices;
11 using System.Security.Cryptography.X509Certificates;
12 using WixToolset.Bind.Bundles;
13 using WixToolset.Data;
14 using WixToolset.Msi;
15 using WixToolset.Core.Native;
16
17 /// <summary>
18 /// Converts a wixout representation of an MSM database into a ComponentGroup the form of WiX source.
19 /// </summary>
20 public sealed class Inscriber : IMessageHandler
21 {
22 // private TempFileCollection tempFiles;
23 private TableDefinitionCollection tableDefinitions;
24
25 public Inscriber()
26 {
27 this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerStandard.GetTableDefinitions());
28 }
29
30 /// <summary>
31 /// Gets or sets the temp files collection.
32 /// </summary>
33 /// <value>The temp files collection.</value>
34 // public TempFileCollection TempFiles
35 // {
36 // get { return this.tempFiles; }
37 // set { this.tempFiles = value; }
38 // }
39
40 /// <summary>
41 /// Gets or sets the path to the temp files location.
42 /// </summary>
43 /// <value>The path to the temp files location.</value>
44 public string TempFilesLocation
45 {
46 get
47 {
48 // if (null == this.tempFiles)
49 // {
50 // return null;
51 // }
52 // else
53 // {
54 // return this.tempFiles.BasePath;
55 // }
56 return Path.GetTempPath();
57 }
58 // set
59 // {
60 // this.DeleteTempFiles();
61
62 // if (null == value)
63 // {
64 // this.tempFiles = new TempFileCollection();
65 // }
66 // else
67 // {
68 // this.tempFiles = new TempFileCollection(value);
69 // }
70
71 // // ensure the base path exists
72 // Directory.CreateDirectory(this.tempFiles.BasePath);
73 // }
74 }
75
76 /// <summary>
77 /// Extracts engine from attached container and updates engine with detached container signatures.
78 /// </summary>
79 /// <param name="bundleFile">Bundle with attached container.</param>
80 /// <param name="outputFile">Bundle engine only.</param>
81 /// <returns>True if bundle was updated.</returns>
82 public bool InscribeBundleEngine(string bundleFile, string outputFile)
83 {
84 string tempFile = Path.Combine(this.TempFilesLocation, "bundle_engine_unsigned.exe");
85
86 using (BurnReader reader = BurnReader.Open(bundleFile))
87 using (FileStream writer = File.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read | FileShare.Delete))
88 {
89 reader.Stream.Seek(0, SeekOrigin.Begin);
90
91 byte[] buffer = new byte[4 * 1024];
92 int total = 0;
93 int read = 0;
94 do
95 {
96 read = Math.Min(buffer.Length, (int)reader.EngineSize - total);
97
98 read = reader.Stream.Read(buffer, 0, read);
99 writer.Write(buffer, 0, read);
100
101 total += read;
102 } while (total < reader.EngineSize && 0 < read);
103
104 if (total != reader.EngineSize)
105 {
106 throw new InvalidOperationException("Failed to copy engine out of bundle.");
107 }
108
109 // TODO: update writer with detached container signatures.
110 }
111
112 Directory.CreateDirectory(Path.GetDirectoryName(outputFile));
113 if (File.Exists(outputFile))
114 {
115 File.Delete(outputFile);
116 }
117 File.Move(tempFile, outputFile);
118 WixToolset.Core.Native.NativeMethods.ResetAcls(new string[] { outputFile }, 1);
119
120 return true;
121 }
122
123 /// <summary>
124 /// Updates engine with attached container information and adds attached container again.
125 /// </summary>
126 /// <param name="bundleFile">Bundle with attached container.</param>
127 /// <param name="signedEngineFile">Signed bundle engine.</param>
128 /// <param name="outputFile">Signed engine with attached container.</param>
129 /// <returns>True if bundle was updated.</returns>
130 public bool InscribeBundle(string bundleFile, string signedEngineFile, string outputFile)
131 {
132 bool inscribed = false;
133 string tempFile = Path.Combine(this.TempFilesLocation, "bundle_engine_signed.exe");
134
135 using (BurnReader reader = BurnReader.Open(bundleFile))
136 {
137 File.Copy(signedEngineFile, tempFile, true);
138
139 // If there was an attached container on the original (unsigned) bundle, put it back.
140 if (reader.AttachedContainerSize > 0)
141 {
142 reader.Stream.Seek(reader.AttachedContainerAddress, SeekOrigin.Begin);
143
144 using (BurnWriter writer = BurnWriter.Open(tempFile))
145 {
146 writer.RememberThenResetSignature();
147 writer.AppendContainer(reader.Stream, reader.AttachedContainerSize, BurnCommon.Container.Attached);
148 inscribed = true;
149 }
150 }
151 }
152
153 Directory.CreateDirectory(Path.GetDirectoryName(outputFile));
154 if (File.Exists(outputFile))
155 {
156 File.Delete(outputFile);
157 }
158 File.Move(tempFile, outputFile);
159 WixToolset.Core.Native.NativeMethods.ResetAcls(new string[] { outputFile }, 1);
160
161 return inscribed;
162 }
163
164 /// <summary>
165 /// Updates database with signatures from external cabinets.
166 /// </summary>
167 /// <param name="databaseFile">Path to MSI database.</param>
168 /// <param name="outputFile">Ouput for updated MSI database.</param>
169 /// <param name="tidy">Clean up files.</param>
170 /// <returns>True if database is updated.</returns>
171 public bool InscribeDatabase(string databaseFile, string outputFile, bool tidy)
172 {
173 // Keeps track of whether we've encountered at least one signed cab or not - we'll throw a warning if no signed cabs were encountered
174 bool foundUnsignedExternals = false;
175 bool shouldCommit = false;
176
177 FileAttributes attributes = File.GetAttributes(databaseFile);
178 if (FileAttributes.ReadOnly == (attributes & FileAttributes.ReadOnly))
179 {
180 this.OnMessage(WixErrors.ReadOnlyOutputFile(databaseFile));
181 return shouldCommit;
182 }
183
184 using (Database database = new Database(databaseFile, OpenDatabase.Transact))
185 {
186 // Just use the English codepage, because the tables we're importing only have binary streams / MSI identifiers / other non-localizable content
187 int codepage = 1252;
188
189 // list of certificates for this database (hash/identifier)
190 Dictionary<string, string> certificates = new Dictionary<string, string>();
191
192 // Reset the in-memory tables for this new database
193 Table digitalSignatureTable = new Table(null, this.tableDefinitions["MsiDigitalSignature"]);
194 Table digitalCertificateTable = new Table(null, this.tableDefinitions["MsiDigitalCertificate"]);
195
196 // If any digital signature records exist that are not of the media type, preserve them
197 if (database.TableExists("MsiDigitalSignature"))
198 {
199 using (View digitalSignatureView = database.OpenExecuteView("SELECT `Table`, `SignObject`, `DigitalCertificate_`, `Hash` FROM `MsiDigitalSignature` WHERE `Table` <> 'Media'"))
200 {
201 while (true)
202 {
203 using (Record digitalSignatureRecord = digitalSignatureView.Fetch())
204 {
205 if (null == digitalSignatureRecord)
206 {
207 break;
208 }
209
210 Row digitalSignatureRow = null;
211 digitalSignatureRow = digitalSignatureTable.CreateRow(null);
212
213 string table = digitalSignatureRecord.GetString(0);
214 string signObject = digitalSignatureRecord.GetString(1);
215
216 digitalSignatureRow[0] = table;
217 digitalSignatureRow[1] = signObject;
218 digitalSignatureRow[2] = digitalSignatureRecord.GetString(2);
219
220 if (false == digitalSignatureRecord.IsNull(3))
221 {
222 // Export to a file, because the MSI API's require us to provide a file path on disk
223 string hashPath = Path.Combine(this.TempFilesLocation, "MsiDigitalSignature");
224 string hashFileName = string.Concat(table, ".", signObject, ".bin");
225
226 Directory.CreateDirectory(hashPath);
227 hashPath = Path.Combine(hashPath, hashFileName);
228
229 using (FileStream fs = File.Create(hashPath))
230 {
231 int bytesRead;
232 byte[] buffer = new byte[1024 * 4];
233
234 while (0 != (bytesRead = digitalSignatureRecord.GetStream(3, buffer, buffer.Length)))
235 {
236 fs.Write(buffer, 0, bytesRead);
237 }
238 }
239
240 digitalSignatureRow[3] = hashFileName;
241 }
242 }
243 }
244 }
245 }
246
247 // If any digital certificates exist, extract and preserve them
248 if (database.TableExists("MsiDigitalCertificate"))
249 {
250 using (View digitalCertificateView = database.OpenExecuteView("SELECT * FROM `MsiDigitalCertificate`"))
251 {
252 while (true)
253 {
254 using (Record digitalCertificateRecord = digitalCertificateView.Fetch())
255 {
256 if (null == digitalCertificateRecord)
257 {
258 break;
259 }
260
261 string certificateId = digitalCertificateRecord.GetString(1); // get the identifier of the certificate
262
263 // Export to a file, because the MSI API's require us to provide a file path on disk
264 string certPath = Path.Combine(this.TempFilesLocation, "MsiDigitalCertificate");
265 Directory.CreateDirectory(certPath);
266 certPath = Path.Combine(certPath, string.Concat(certificateId, ".cer"));
267
268 using (FileStream fs = File.Create(certPath))
269 {
270 int bytesRead;
271 byte[] buffer = new byte[1024 * 4];
272
273 while (0 != (bytesRead = digitalCertificateRecord.GetStream(2, buffer, buffer.Length)))
274 {
275 fs.Write(buffer, 0, bytesRead);
276 }
277 }
278
279 // Add it to our "add to MsiDigitalCertificate" table dictionary
280 Row digitalCertificateRow = digitalCertificateTable.CreateRow(null);
281 digitalCertificateRow[0] = certificateId;
282
283 // Now set the file path on disk where this binary stream will be picked up at import time
284 digitalCertificateRow[1] = string.Concat(certificateId, ".cer");
285
286 // Load the cert to get it's thumbprint
287 X509Certificate cert = X509Certificate.CreateFromCertFile(certPath);
288 X509Certificate2 cert2 = new X509Certificate2(cert);
289
290 certificates.Add(cert2.Thumbprint, certificateId);
291 }
292 }
293 }
294 }
295
296 using (View mediaView = database.OpenExecuteView("SELECT * FROM `Media`"))
297 {
298 while (true)
299 {
300 using (Record mediaRecord = mediaView.Fetch())
301 {
302 if (null == mediaRecord)
303 {
304 break;
305 }
306
307 X509Certificate2 cert2 = null;
308 Row digitalSignatureRow = null;
309
310 string cabName = mediaRecord.GetString(4); // get the name of the cab
311 // If there is no cabinet or it's an internal cab, skip it.
312 if (String.IsNullOrEmpty(cabName) || cabName.StartsWith("#", StringComparison.Ordinal))
313 {
314 continue;
315 }
316
317 string cabId = mediaRecord.GetString(1); // get the ID of the cab
318 string cabPath = Path.Combine(Path.GetDirectoryName(databaseFile), cabName);
319
320 // If the cabs aren't there, throw an error but continue to catch the other errors
321 if (!File.Exists(cabPath))
322 {
323 this.OnMessage(WixErrors.WixFileNotFound(cabPath));
324 continue;
325 }
326
327 try
328 {
329 // Get the certificate from the cab
330 X509Certificate signedFileCert = X509Certificate.CreateFromSignedFile(cabPath);
331 cert2 = new X509Certificate2(signedFileCert);
332 }
333 catch (System.Security.Cryptography.CryptographicException e)
334 {
335 uint HResult = unchecked((uint)Marshal.GetHRForException(e));
336
337 // If the file has no cert, continue, but flag that we found at least one so we can later give a warning
338 if (0x80092009 == HResult) // CRYPT_E_NO_MATCH
339 {
340 foundUnsignedExternals = true;
341 continue;
342 }
343
344 // todo: exactly which HRESULT corresponds to this issue?
345 // If it's one of these exact platforms, warn the user that it may be due to their OS.
346 if ((5 == Environment.OSVersion.Version.Major && 2 == Environment.OSVersion.Version.Minor) || // W2K3
347 (5 == Environment.OSVersion.Version.Major && 1 == Environment.OSVersion.Version.Minor)) // XP
348 {
349 this.OnMessage(WixErrors.UnableToGetAuthenticodeCertOfFileDownlevelOS(cabPath, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", HResult)));
350 }
351 else // otherwise, generic error
352 {
353 this.OnMessage(WixErrors.UnableToGetAuthenticodeCertOfFile(cabPath, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", HResult)));
354 }
355 }
356
357 // If we haven't added this cert to the MsiDigitalCertificate table, set it up to be added
358 if (!certificates.ContainsKey(cert2.Thumbprint))
359 {
360 // generate a stable identifier
361 string certificateGeneratedId = Common.GenerateIdentifier("cer", cert2.Thumbprint);
362
363 // Add it to our "add to MsiDigitalCertificate" table dictionary
364 Row digitalCertificateRow = digitalCertificateTable.CreateRow(null);
365 digitalCertificateRow[0] = certificateGeneratedId;
366
367 // Export to a file, because the MSI API's require us to provide a file path on disk
368 string certPath = Path.Combine(this.TempFilesLocation, "MsiDigitalCertificate");
369 Directory.CreateDirectory(certPath);
370 certPath = Path.Combine(certPath, string.Concat(cert2.Thumbprint, ".cer"));
371 File.Delete(certPath);
372
373 using (BinaryWriter writer = new BinaryWriter(File.Open(certPath, FileMode.Create)))
374 {
375 writer.Write(cert2.RawData);
376 writer.Close();
377 }
378
379 // Now set the file path on disk where this binary stream will be picked up at import time
380 digitalCertificateRow[1] = string.Concat(cert2.Thumbprint, ".cer");
381
382 certificates.Add(cert2.Thumbprint, certificateGeneratedId);
383 }
384
385 digitalSignatureRow = digitalSignatureTable.CreateRow(null);
386
387 digitalSignatureRow[0] = "Media";
388 digitalSignatureRow[1] = cabId;
389 digitalSignatureRow[2] = certificates[cert2.Thumbprint];
390 }
391 }
392 }
393
394 if (digitalCertificateTable.Rows.Count > 0)
395 {
396 database.ImportTable(codepage, digitalCertificateTable, this.TempFilesLocation, true);
397 shouldCommit = true;
398 }
399
400 if (digitalSignatureTable.Rows.Count > 0)
401 {
402 database.ImportTable(codepage, digitalSignatureTable, this.TempFilesLocation, true);
403 shouldCommit = true;
404 }
405
406 // TODO: if we created the table(s), then we should add the _Validation records for them.
407
408 certificates = null;
409
410 // If we did find external cabs but none of them were signed, give a warning
411 if (foundUnsignedExternals)
412 {
413 this.OnMessage(WixWarnings.ExternalCabsAreNotSigned(databaseFile));
414 }
415
416 if (shouldCommit)
417 {
418 database.Commit();
419 }
420 }
421
422 return shouldCommit;
423 }
424
425 /// <summary>
426 /// Cleans up the temp files used by the Inscriber.
427 /// </summary>
428 /// <returns>True if all files were deleted, false otherwise.</returns>
429 public bool DeleteTempFiles()
430 {
431#if REDO_IN_NETCORE
432 if (null == this.tempFiles)
433 {
434 return true; // no work to do
435 }
436 else
437 {
438 bool deleted = Common.DeleteTempFiles(this.TempFilesLocation, this);
439
440 if (deleted)
441 {
442 ((IDisposable)this.tempFiles).Dispose();
443 this.tempFiles = null; // temp files have been deleted, no need to remember this now
444 }
445
446 return deleted;
447 }
448#endif
449 return true;
450 }
451
452 public void OnMessage(MessageEventArgs e)
453 {
454 Messaging.Instance.OnMessage(e);
455 }
456 }
457}