1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 // We need Windows 8 functions and structures to be able to verify SHA-256.
8 #if defined(_WIN32_WINNT)
9 #  undef _WIN32_WINNT
10 #  define _WIN32_WINNT _WIN32_WINNT_WIN8
11 #endif  // defined(_WIN32_WINNT)
12 #if defined(NTDDI_VERSION)
13 #  undef NTDDI_VERSION
14 #  define NTDDI_VERSION NTDDI_WIN8
15 #endif  // defined(NTDDI_VERSION)
16 
17 #include "Authenticode.h"
18 
19 #include "mozilla/ArrayUtils.h"
20 #include "mozilla/Assertions.h"
21 #include "mozilla/DynamicallyLinkedFunctionPtr.h"
22 #include "mozilla/ScopeExit.h"
23 #include "mozilla/UniquePtr.h"
24 #include "mozilla/WindowsVersion.h"
25 #include "nsWindowsHelpers.h"
26 
27 #include <windows.h>
28 #include <softpub.h>
29 #include <wincrypt.h>
30 #include <wintrust.h>
31 #include <mscat.h>
32 
33 #include <string.h>
34 
35 namespace {
36 
37 struct CertStoreDeleter {
38   typedef HCERTSTORE pointer;
operator ()__anon2b30b1da0111::CertStoreDeleter39   void operator()(pointer aStore) { ::CertCloseStore(aStore, 0); }
40 };
41 
42 struct CryptMsgDeleter {
43   typedef HCRYPTMSG pointer;
operator ()__anon2b30b1da0111::CryptMsgDeleter44   void operator()(pointer aMsg) { ::CryptMsgClose(aMsg); }
45 };
46 
47 struct CertContextDeleter {
operator ()__anon2b30b1da0111::CertContextDeleter48   void operator()(PCCERT_CONTEXT aCertContext) {
49     ::CertFreeCertificateContext(aCertContext);
50   }
51 };
52 
53 struct CATAdminContextDeleter {
54   typedef HCATADMIN pointer;
operator ()__anon2b30b1da0111::CATAdminContextDeleter55   void operator()(pointer aCtx) {
56     static const mozilla::StaticDynamicallyLinkedFunctionPtr<
57         decltype(&::CryptCATAdminReleaseContext)>
58         pCryptCATAdminReleaseContext(L"wintrust.dll",
59                                      "CryptCATAdminReleaseContext");
60 
61     MOZ_ASSERT(!!pCryptCATAdminReleaseContext);
62     if (!pCryptCATAdminReleaseContext) {
63       return;
64     }
65 
66     pCryptCATAdminReleaseContext(aCtx, 0);
67   }
68 };
69 
70 typedef mozilla::UniquePtr<HCERTSTORE, CertStoreDeleter> CertStoreUniquePtr;
71 typedef mozilla::UniquePtr<HCRYPTMSG, CryptMsgDeleter> CryptMsgUniquePtr;
72 typedef mozilla::UniquePtr<const CERT_CONTEXT, CertContextDeleter>
73     CertContextUniquePtr;
74 typedef mozilla::UniquePtr<HCATADMIN, CATAdminContextDeleter>
75     CATAdminContextUniquePtr;
76 
77 static const DWORD kEncodingTypes = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING;
78 
79 class SignedBinary final {
80  public:
81   SignedBinary(const wchar_t* aFilePath, mozilla::AuthenticodeFlags aFlags);
82 
operator bool() const83   explicit operator bool() const { return mCertStore && mCryptMsg && mCertCtx; }
84 
85   mozilla::UniquePtr<wchar_t[]> GetOrgName();
86 
87   SignedBinary(const SignedBinary&) = delete;
88   SignedBinary(SignedBinary&&) = delete;
89   SignedBinary& operator=(const SignedBinary&) = delete;
90   SignedBinary& operator=(SignedBinary&&) = delete;
91 
92  private:
93   bool VerifySignature(const wchar_t* aFilePath);
94   bool QueryObject(const wchar_t* aFilePath);
95   static bool VerifySignatureInternal(WINTRUST_DATA& aTrustData);
96 
97  private:
98   enum class TrustSource { eNone, eEmbedded, eCatalog };
99 
100  private:
101   const mozilla::AuthenticodeFlags mFlags;
102   TrustSource mTrustSource;
103   CertStoreUniquePtr mCertStore;
104   CryptMsgUniquePtr mCryptMsg;
105   CertContextUniquePtr mCertCtx;
106 };
107 
SignedBinary(const wchar_t * aFilePath,mozilla::AuthenticodeFlags aFlags)108 SignedBinary::SignedBinary(const wchar_t* aFilePath,
109                            mozilla::AuthenticodeFlags aFlags)
110     : mFlags(aFlags), mTrustSource(TrustSource::eNone) {
111   if (!VerifySignature(aFilePath)) {
112     return;
113   }
114 
115   DWORD certInfoLen = 0;
116   BOOL ok = CryptMsgGetParam(mCryptMsg.get(), CMSG_SIGNER_CERT_INFO_PARAM, 0,
117                              nullptr, &certInfoLen);
118   if (!ok) {
119     return;
120   }
121 
122   auto certInfoBuf = mozilla::MakeUnique<char[]>(certInfoLen);
123 
124   ok = CryptMsgGetParam(mCryptMsg.get(), CMSG_SIGNER_CERT_INFO_PARAM, 0,
125                         certInfoBuf.get(), &certInfoLen);
126   if (!ok) {
127     return;
128   }
129 
130   auto certInfo = reinterpret_cast<CERT_INFO*>(certInfoBuf.get());
131 
132   PCCERT_CONTEXT certCtx =
133       CertFindCertificateInStore(mCertStore.get(), kEncodingTypes, 0,
134                                  CERT_FIND_SUBJECT_CERT, certInfo, nullptr);
135   if (!certCtx) {
136     return;
137   }
138 
139   mCertCtx.reset(certCtx);
140 }
141 
QueryObject(const wchar_t * aFilePath)142 bool SignedBinary::QueryObject(const wchar_t* aFilePath) {
143   DWORD encodingType, contentType, formatType;
144   HCERTSTORE rawCertStore;
145   HCRYPTMSG rawCryptMsg;
146   BOOL result = ::CryptQueryObject(CERT_QUERY_OBJECT_FILE, aFilePath,
147                                    CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED,
148                                    CERT_QUERY_FORMAT_FLAG_BINARY, 0,
149                                    &encodingType, &contentType, &formatType,
150                                    &rawCertStore, &rawCryptMsg, nullptr);
151   if (!result) {
152     return false;
153   }
154 
155   mCertStore.reset(rawCertStore);
156   mCryptMsg.reset(rawCryptMsg);
157 
158   return true;
159 }
160 
161 /**
162  * @param aTrustData must be a WINTRUST_DATA structure that has been zeroed out
163  *                   and then populated at least with its |cbStruct|,
164  *                   |dwUnionChoice|, and appropriate union field. This function
165  *                   will then populate the remaining fields as appropriate.
166  */
167 /* static */
VerifySignatureInternal(WINTRUST_DATA & aTrustData)168 bool SignedBinary::VerifySignatureInternal(WINTRUST_DATA& aTrustData) {
169   aTrustData.dwUIChoice = WTD_UI_NONE;
170   aTrustData.fdwRevocationChecks = WTD_REVOKE_NONE;
171   aTrustData.dwStateAction = WTD_STATEACTION_VERIFY;
172   aTrustData.dwProvFlags = WTD_CACHE_ONLY_URL_RETRIEVAL;
173 
174   const HWND hwnd = (HWND)INVALID_HANDLE_VALUE;
175   GUID policyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
176   LONG result = ::WinVerifyTrust(hwnd, &policyGUID, &aTrustData);
177 
178   aTrustData.dwStateAction = WTD_STATEACTION_CLOSE;
179   ::WinVerifyTrust(hwnd, &policyGUID, &aTrustData);
180 
181   return result == ERROR_SUCCESS;
182 }
183 
VerifySignature(const wchar_t * aFilePath)184 bool SignedBinary::VerifySignature(const wchar_t* aFilePath) {
185   // First, try the binary itself
186   if (QueryObject(aFilePath)) {
187     mTrustSource = TrustSource::eEmbedded;
188     if (mFlags & mozilla::AuthenticodeFlags::SkipTrustVerification) {
189       return true;
190     }
191 
192     WINTRUST_FILE_INFO fileInfo = {sizeof(fileInfo)};
193     fileInfo.pcwszFilePath = aFilePath;
194 
195     WINTRUST_DATA trustData = {sizeof(trustData)};
196     trustData.dwUnionChoice = WTD_CHOICE_FILE;
197     trustData.pFile = &fileInfo;
198 
199     return VerifySignatureInternal(trustData);
200   }
201 
202   // We didn't find anything in the binary, so now try a catalog file.
203 
204   // First, we open a catalog admin context.
205   HCATADMIN rawCatAdmin;
206 
207   // Windows 7 also exports the CryptCATAdminAcquireContext2 API, but it does
208   // *not* sign its binaries with SHA-256, so we use the old API in that case.
209   if (mozilla::IsWin8OrLater()) {
210     static const mozilla::StaticDynamicallyLinkedFunctionPtr<
211         decltype(&::CryptCATAdminAcquireContext2)>
212         pCryptCATAdminAcquireContext2(L"wintrust.dll",
213                                       "CryptCATAdminAcquireContext2");
214     if (!pCryptCATAdminAcquireContext2) {
215       return false;
216     }
217 
218     CERT_STRONG_SIGN_PARA policy = {sizeof(policy)};
219     policy.dwInfoChoice = CERT_STRONG_SIGN_OID_INFO_CHOICE;
220     policy.pszOID = const_cast<char*>(
221         szOID_CERT_STRONG_SIGN_OS_CURRENT);  // -Wwritable-strings
222 
223     if (!pCryptCATAdminAcquireContext2(&rawCatAdmin, nullptr,
224                                        BCRYPT_SHA256_ALGORITHM, &policy, 0)) {
225       return false;
226     }
227   } else {
228     static const mozilla::StaticDynamicallyLinkedFunctionPtr<
229         decltype(&::CryptCATAdminAcquireContext)>
230         pCryptCATAdminAcquireContext(L"wintrust.dll",
231                                      "CryptCATAdminAcquireContext");
232 
233     if (!pCryptCATAdminAcquireContext ||
234         !pCryptCATAdminAcquireContext(&rawCatAdmin, nullptr, 0)) {
235       return false;
236     }
237   }
238 
239   CATAdminContextUniquePtr catAdmin(rawCatAdmin);
240 
241   // Now we need to hash the file at aFilePath.
242   // Since we're hashing this file, let's open it with a sequential scan hint.
243   HANDLE rawFile =
244       ::CreateFileW(aFilePath, GENERIC_READ,
245                     FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
246                     nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr);
247   if (rawFile == INVALID_HANDLE_VALUE) {
248     return false;
249   }
250 
251   nsAutoHandle file(rawFile);
252   DWORD hashLen = 0;
253   mozilla::UniquePtr<BYTE[]> hashBuf;
254 
255   static const mozilla::StaticDynamicallyLinkedFunctionPtr<
256       decltype(&::CryptCATAdminCalcHashFromFileHandle2)>
257       pCryptCATAdminCalcHashFromFileHandle2(
258           L"wintrust.dll", "CryptCATAdminCalcHashFromFileHandle2");
259   if (pCryptCATAdminCalcHashFromFileHandle2) {
260     if (!pCryptCATAdminCalcHashFromFileHandle2(rawCatAdmin, rawFile, &hashLen,
261                                                nullptr, 0) &&
262         ::GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
263       return false;
264     }
265 
266     hashBuf = mozilla::MakeUnique<BYTE[]>(hashLen);
267 
268     if (!pCryptCATAdminCalcHashFromFileHandle2(rawCatAdmin, rawFile, &hashLen,
269                                                hashBuf.get(), 0)) {
270       return false;
271     }
272   } else {
273     static const mozilla::StaticDynamicallyLinkedFunctionPtr<
274         decltype(&::CryptCATAdminCalcHashFromFileHandle)>
275         pCryptCATAdminCalcHashFromFileHandle(
276             L"wintrust.dll", "CryptCATAdminCalcHashFromFileHandle");
277 
278     if (!pCryptCATAdminCalcHashFromFileHandle) {
279       return false;
280     }
281 
282     if (!pCryptCATAdminCalcHashFromFileHandle(rawFile, &hashLen, nullptr, 0) &&
283         ::GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
284       return false;
285     }
286 
287     hashBuf = mozilla::MakeUnique<BYTE[]>(hashLen);
288 
289     if (!pCryptCATAdminCalcHashFromFileHandle(rawFile, &hashLen, hashBuf.get(),
290                                               0)) {
291       return false;
292     }
293   }
294 
295   // Now that we've hashed the file, query the catalog system to see if any
296   // catalogs reference a binary with our hash.
297 
298   static const mozilla::StaticDynamicallyLinkedFunctionPtr<
299       decltype(&::CryptCATAdminEnumCatalogFromHash)>
300       pCryptCATAdminEnumCatalogFromHash(L"wintrust.dll",
301                                         "CryptCATAdminEnumCatalogFromHash");
302   if (!pCryptCATAdminEnumCatalogFromHash) {
303     return false;
304   }
305 
306   static const mozilla::StaticDynamicallyLinkedFunctionPtr<
307       decltype(&::CryptCATAdminReleaseCatalogContext)>
308       pCryptCATAdminReleaseCatalogContext(L"wintrust.dll",
309                                           "CryptCATAdminReleaseCatalogContext");
310   if (!pCryptCATAdminReleaseCatalogContext) {
311     return false;
312   }
313 
314   HCATINFO catInfoHdl = pCryptCATAdminEnumCatalogFromHash(
315       rawCatAdmin, hashBuf.get(), hashLen, 0, nullptr);
316   if (!catInfoHdl) {
317     return false;
318   }
319 
320   // We can't use UniquePtr for this because the deleter function requires two
321   // parameters.
322   auto cleanCatInfoHdl =
323       mozilla::MakeScopeExit([rawCatAdmin, catInfoHdl]() -> void {
324         pCryptCATAdminReleaseCatalogContext(rawCatAdmin, catInfoHdl, 0);
325       });
326 
327   // We found a catalog! Now query for the path to the catalog file.
328 
329   static const mozilla::StaticDynamicallyLinkedFunctionPtr<
330       decltype(&::CryptCATCatalogInfoFromContext)>
331       pCryptCATCatalogInfoFromContext(L"wintrust.dll",
332                                       "CryptCATCatalogInfoFromContext");
333   if (!pCryptCATCatalogInfoFromContext) {
334     return false;
335   }
336 
337   CATALOG_INFO_ catInfo = {sizeof(catInfo)};
338   if (!pCryptCATCatalogInfoFromContext(catInfoHdl, &catInfo, 0)) {
339     return false;
340   }
341 
342   if (!QueryObject(catInfo.wszCatalogFile)) {
343     return false;
344   }
345 
346   mTrustSource = TrustSource::eCatalog;
347 
348   if (mFlags & mozilla::AuthenticodeFlags::SkipTrustVerification) {
349     return true;
350   }
351 
352   // WINTRUST_CATALOG_INFO::pcwszMemberTag is commonly set to the string
353   // representation of the file hash, so we build that here.
354 
355   DWORD strHashBufLen = (hashLen * 2) + 1;
356   auto strHashBuf = mozilla::MakeUnique<wchar_t[]>(strHashBufLen);
357   if (!::CryptBinaryToStringW(hashBuf.get(), hashLen,
358                               CRYPT_STRING_HEXRAW | CRYPT_STRING_NOCRLF,
359                               strHashBuf.get(), &strHashBufLen)) {
360     return false;
361   }
362 
363   // Ensure that the tag is uppercase for WinVerifyTrust
364   // NB: CryptBinaryToStringW overwrites strHashBufLen with the length excluding
365   //     the null terminator, so we need to add it back for this call.
366   if (_wcsupr_s(strHashBuf.get(), strHashBufLen + 1)) {
367     return false;
368   }
369 
370   // Now, given the path to the catalog, and the path to the member (ie, the
371   // binary whose hash we are validating), we may now validate. If the
372   // validation is successful, we then QueryObject on the *catalog file*
373   // instead of the binary.
374 
375   WINTRUST_CATALOG_INFO wtCatInfo = {sizeof(wtCatInfo)};
376   wtCatInfo.pcwszCatalogFilePath = catInfo.wszCatalogFile;
377   wtCatInfo.pcwszMemberTag = strHashBuf.get();
378   wtCatInfo.pcwszMemberFilePath = aFilePath;
379   wtCatInfo.hMemberFile = rawFile;
380   if (mozilla::IsWin8OrLater()) {
381     wtCatInfo.hCatAdmin = rawCatAdmin;
382   }
383 
384   WINTRUST_DATA trustData = {sizeof(trustData)};
385   trustData.dwUnionChoice = WTD_CHOICE_CATALOG;
386   trustData.pCatalog = &wtCatInfo;
387 
388   return VerifySignatureInternal(trustData);
389 }
390 
GetOrgName()391 mozilla::UniquePtr<wchar_t[]> SignedBinary::GetOrgName() {
392   DWORD charCount = CertGetNameStringW(
393       mCertCtx.get(), CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nullptr, nullptr, 0);
394   if (charCount <= 1) {
395     // Not found
396     return nullptr;
397   }
398 
399   auto result = mozilla::MakeUnique<wchar_t[]>(charCount);
400   charCount = CertGetNameStringW(mCertCtx.get(), CERT_NAME_SIMPLE_DISPLAY_TYPE,
401                                  0, nullptr, result.get(), charCount);
402   MOZ_ASSERT(charCount > 1);
403 
404   return result;
405 }
406 
407 }  // anonymous namespace
408 
409 namespace mozilla {
410 
411 class AuthenticodeImpl : public Authenticode {
412  public:
413   virtual UniquePtr<wchar_t[]> GetBinaryOrgName(
414       const wchar_t* aFilePath,
415       AuthenticodeFlags aFlags = AuthenticodeFlags::Default) override;
416 };
417 
GetBinaryOrgName(const wchar_t * aFilePath,AuthenticodeFlags aFlags)418 UniquePtr<wchar_t[]> AuthenticodeImpl::GetBinaryOrgName(
419     const wchar_t* aFilePath, AuthenticodeFlags aFlags) {
420   SignedBinary bin(aFilePath, aFlags);
421   if (!bin) {
422     return nullptr;
423   }
424 
425   return bin.GetOrgName();
426 }
427 
428 static AuthenticodeImpl sAuthenticodeImpl;
429 
GetAuthenticode()430 Authenticode* GetAuthenticode() { return &sAuthenticodeImpl; }
431 
432 }  // namespace mozilla
433