main
cs 285 lines 14.3 KB
Raw
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.WindowsInstaller.Inscribe
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO;
9 using System.Runtime.InteropServices;
10 using System.Security.Cryptography.X509Certificates;
11 using WixToolset.Core.Native.Msi;
12 using WixToolset.Core.WindowsInstaller.Bind;
13 using WixToolset.Data;
14 using WixToolset.Data.WindowsInstaller;
15 using WixToolset.Extensibility.Services;
16
17 internal class InscribeMsiPackageCommand
18 {
19 public InscribeMsiPackageCommand(IServiceProvider serviceProvider, string inputPath, string intermediateFolder, string outputPath)
20 {
21 this.Messaging = serviceProvider.GetService<IMessaging>();
22 this.FileSystem = serviceProvider.GetService<IFileSystem>();
23 this.WindowsInstallerBackendHelper = serviceProvider.GetService<IWindowsInstallerBackendHelper>();
24 this.TableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All);
25 this.InputPath = inputPath;
26 this.IntermediateFolder = intermediateFolder;
27 this.OutputPath = outputPath;
28 }
29
30 private string InputPath { get; }
31
32 private string IntermediateFolder { get; }
33
34 private string OutputPath { get; }
35
36 private IMessaging Messaging { get; }
37
38 private IFileSystem FileSystem { get; }
39
40 private IWindowsInstallerBackendHelper WindowsInstallerBackendHelper { get; }
41
42 private TableDefinitionCollection TableDefinitions { get; }
43
44 public bool Execute()
45 {
46 // 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
47 var foundUnsignedExternals = false;
48 var shouldCommit = false;
49
50 var databasePath = this.OutputPath;
51
52 if (!String.Equals(this.InputPath, this.OutputPath, StringComparison.OrdinalIgnoreCase))
53 {
54 this.FileSystem.CopyFile(null, this.InputPath, this.OutputPath, allowHardlink: false);
55 }
56
57 var attributes = File.GetAttributes(databasePath);
58 if (FileAttributes.ReadOnly == (attributes & FileAttributes.ReadOnly))
59 {
60 this.Messaging.Write(ErrorMessages.ReadOnlyOutputFile(databasePath));
61 return shouldCommit;
62 }
63
64 using (var database = new Database(databasePath, OpenDatabase.Transact))
65 {
66 // Just use the English codepage, because the tables we're importing only have binary streams / MSI identifiers / other non-localizable content
67 var codepage = 1252;
68
69 // list of certificates for this database (hash/identifier)
70 var certificates = new Dictionary<string, string>();
71
72 // Reset the in-memory tables for this new database
73 var digitalSignatureTable = new Table(this.TableDefinitions["MsiDigitalSignature"]);
74 var digitalCertificateTable = new Table(this.TableDefinitions["MsiDigitalCertificate"]);
75
76 // If any digital signature records exist that are not of the media type, preserve them
77 if (database.TableExists("MsiDigitalSignature"))
78 {
79 using (var digitalSignatureView = database.OpenExecuteView("SELECT `Table`, `SignObject`, `DigitalCertificate_`, `Hash` FROM `MsiDigitalSignature` WHERE `Table` <> 'Media'"))
80 {
81 foreach (var digitalSignatureRecord in digitalSignatureView.Records)
82 {
83 var digitalSignatureRow = digitalSignatureTable.CreateRow(null);
84
85 var table = digitalSignatureRecord.GetString(0);
86 var signObject = digitalSignatureRecord.GetString(1);
87
88 digitalSignatureRow[0] = table;
89 digitalSignatureRow[1] = signObject;
90 digitalSignatureRow[2] = digitalSignatureRecord.GetString(2);
91
92 if (false == digitalSignatureRecord.IsNull(3))
93 {
94 // Export to a file, because the MSI API's require us to provide a file path on disk
95 var hashPath = Path.Combine(this.IntermediateFolder, "MsiDigitalSignature");
96 var hashFileName = String.Concat(table, ".", signObject, ".bin");
97
98 Directory.CreateDirectory(hashPath);
99 hashPath = Path.Combine(hashPath, hashFileName);
100
101 using (var fs = this.FileSystem.OpenFile(null, hashPath, FileMode.Create, FileAccess.Write, FileShare.None))
102 {
103 int bytesRead;
104 var buffer = new byte[1024 * 4];
105
106 while (0 != (bytesRead = digitalSignatureRecord.GetStream(3, buffer, buffer.Length)))
107 {
108 fs.Write(buffer, 0, bytesRead);
109 }
110 }
111
112 digitalSignatureRow[3] = hashFileName;
113 }
114 }
115 }
116 }
117
118 // If any digital certificates exist, extract and preserve them
119 if (database.TableExists("MsiDigitalCertificate"))
120 {
121 using (var digitalCertificateView = database.OpenExecuteView("SELECT * FROM `MsiDigitalCertificate`"))
122 {
123 foreach (var digitalCertificateRecord in digitalCertificateView.Records)
124 {
125 var certificateId = digitalCertificateRecord.GetString(1); // get the identifier of the certificate
126
127 // Export to a file, because the MSI API's require us to provide a file path on disk
128 var certPath = Path.Combine(this.IntermediateFolder, "MsiDigitalCertificate");
129 Directory.CreateDirectory(certPath);
130 certPath = Path.Combine(certPath, String.Concat(certificateId, ".cer"));
131
132 using (var fs = this.FileSystem.OpenFile(null, certPath, FileMode.Create, FileAccess.Write, FileShare.None))
133 {
134 int bytesRead;
135 var buffer = new byte[1024 * 4];
136
137 while (0 != (bytesRead = digitalCertificateRecord.GetStream(2, buffer, buffer.Length)))
138 {
139 fs.Write(buffer, 0, bytesRead);
140 }
141 }
142
143 // Add it to our "add to MsiDigitalCertificate" table dictionary
144 var digitalCertificateRow = digitalCertificateTable.CreateRow(null);
145 digitalCertificateRow[0] = certificateId;
146
147 // Now set the file path on disk where this binary stream will be picked up at import time
148 digitalCertificateRow[1] = String.Concat(certificateId, ".cer");
149
150 // Load the cert to get it's thumbprint
151 var cert = X509Certificate.CreateFromCertFile(certPath);
152 var cert2 = new X509Certificate2(cert);
153
154 certificates.Add(cert2.Thumbprint, certificateId);
155 }
156 }
157 }
158
159 using (var mediaView = database.OpenExecuteView("SELECT * FROM `Media`"))
160 {
161 foreach (var mediaRecord in mediaView.Records)
162 {
163 X509Certificate2 cert2 = null;
164
165 var cabName = mediaRecord.GetString(4); // get the name of the cab
166 // If there is no cabinet or it's an internal cab, skip it.
167 if (String.IsNullOrEmpty(cabName) || cabName.StartsWith("#", StringComparison.Ordinal))
168 {
169 continue;
170 }
171
172 var cabId = mediaRecord.GetString(1); // get the ID of the cab
173 var cabPath = Path.Combine(Path.GetDirectoryName(this.InputPath), cabName);
174
175 // If the cabs aren't there, throw an error but continue to catch the other errors
176 if (!File.Exists(cabPath))
177 {
178 this.Messaging.Write(ErrorMessages.WixFileNotFound(cabPath));
179 continue;
180 }
181
182 try
183 {
184 // Get the certificate from the cab
185 var signedFileCert = X509Certificate.CreateFromSignedFile(cabPath);
186 cert2 = new X509Certificate2(signedFileCert);
187 }
188 catch (System.Security.Cryptography.CryptographicException e)
189 {
190 var HResult = unchecked((uint)Marshal.GetHRForException(e));
191
192 // If the file has no cert, continue, but flag that we found at least one so we can later give a warning
193 if (0x80092009 == HResult) // CRYPT_E_NO_MATCH
194 {
195 foundUnsignedExternals = true;
196 continue;
197 }
198
199 // todo: exactly which HRESULT corresponds to this issue?
200 // If it's one of these exact platforms, warn the user that it may be due to their OS.
201 if ((5 == Environment.OSVersion.Version.Major && 2 == Environment.OSVersion.Version.Minor) || // W2K3
202 (5 == Environment.OSVersion.Version.Major && 1 == Environment.OSVersion.Version.Minor)) // XP
203 {
204 this.Messaging.Write(ErrorMessages.UnableToGetAuthenticodeCertOfFileDownlevelOS(cabPath, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", HResult)));
205 }
206 else // otherwise, generic error
207 {
208 this.Messaging.Write(ErrorMessages.UnableToGetAuthenticodeCertOfFile(cabPath, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", HResult)));
209 }
210 }
211
212 // If we haven't added this cert to the MsiDigitalCertificate table, set it up to be added
213 if (!certificates.ContainsKey(cert2.Thumbprint))
214 {
215 // generate a stable identifier
216 var certificateGeneratedId = this.WindowsInstallerBackendHelper.GenerateIdentifier("cer", cert2.Thumbprint);
217
218 // Add it to our "add to MsiDigitalCertificate" table dictionary
219 var digitalCertificateRow = digitalCertificateTable.CreateRow(null);
220 digitalCertificateRow[0] = certificateGeneratedId;
221
222 // Export to a file, because the MSI API's require us to provide a file path on disk
223 var certPath = Path.Combine(this.IntermediateFolder, "MsiDigitalCertificate");
224 Directory.CreateDirectory(certPath);
225 certPath = Path.Combine(certPath, String.Concat(cert2.Thumbprint, ".cer"));
226 this.FileSystem.DeleteFile(null, certPath, true);
227
228 using (var writer = new BinaryWriter(this.FileSystem.OpenFile(null, certPath, FileMode.Create, FileAccess.Write, FileShare.Read)))
229 {
230 writer.Write(cert2.RawData);
231 writer.Close();
232 }
233
234 // Now set the file path on disk where this binary stream will be picked up at import time
235 digitalCertificateRow[1] = String.Concat(cert2.Thumbprint, ".cer");
236
237 certificates.Add(cert2.Thumbprint, certificateGeneratedId);
238 }
239
240 var digitalSignatureRow = digitalSignatureTable.CreateRow(null);
241
242 digitalSignatureRow[0] = "Media";
243 digitalSignatureRow[1] = cabId;
244 digitalSignatureRow[2] = certificates[cert2.Thumbprint];
245 }
246 }
247
248 if (digitalCertificateTable.Rows.Count > 0)
249 {
250 var command = new CreateIdtFileCommand(this.Messaging, digitalCertificateTable, codepage, this.IntermediateFolder, true);
251 command.Execute();
252
253 database.Import(command.IdtPath);
254 shouldCommit = true;
255 }
256
257 if (digitalSignatureTable.Rows.Count > 0)
258 {
259 var command = new CreateIdtFileCommand(this.Messaging, digitalSignatureTable, codepage, this.IntermediateFolder, true);
260 command.Execute();
261
262 database.Import(command.IdtPath);
263 shouldCommit = true;
264 }
265
266 // TODO: if we created the table(s), then we should add the _Validation records for them.
267
268 certificates = null;
269
270 // If we did find external cabs but not all of them were signed, give a warning
271 if (foundUnsignedExternals)
272 {
273 this.Messaging.Write(WarningMessages.ExternalCabsAreNotSigned(this.InputPath));
274 }
275
276 if (shouldCommit)
277 {
278 database.Commit();
279 }
280 }
281
282 return shouldCommit;
283 }
284 }
285 }