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