/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "Authenticode.h" #include "mozilla/ArrayUtils.h" #include "mozilla/Assertions.h" #include "mozilla/DynamicallyLinkedFunctionPtr.h" #include "mozilla/ScopeExit.h" #include "mozilla/UniquePtr.h" #include "nsWindowsHelpers.h" #include #include #include #include #include #include // mingw build doesn't have this defined (in other builds // it gets pulled in from wintrust.h) #if !defined(szOID_NESTED_SIGNATURE) # define szOID_NESTED_SIGNATURE "1.3.6.1.4.1.311.2.4.1" #endif // !defined(szOID_NESTED_SIGNATURE) namespace { struct CertStoreDeleter { typedef HCERTSTORE pointer; void operator()(pointer aStore) { ::CertCloseStore(aStore, 0); } }; struct CryptMsgDeleter { typedef HCRYPTMSG pointer; void operator()(pointer aMsg) { ::CryptMsgClose(aMsg); } }; struct CertContextDeleter { void operator()(PCCERT_CONTEXT aCertContext) { ::CertFreeCertificateContext(aCertContext); } }; struct CATAdminContextDeleter { typedef HCATADMIN pointer; void operator()(pointer aCtx) { static const mozilla::StaticDynamicallyLinkedFunctionPtr< decltype(&::CryptCATAdminReleaseContext)> pCryptCATAdminReleaseContext(L"wintrust.dll", "CryptCATAdminReleaseContext"); MOZ_ASSERT(!!pCryptCATAdminReleaseContext); if (!pCryptCATAdminReleaseContext) { return; } pCryptCATAdminReleaseContext(aCtx, 0); } }; typedef mozilla::UniquePtr CertStoreUniquePtr; typedef mozilla::UniquePtr CryptMsgUniquePtr; typedef mozilla::UniquePtr CertContextUniquePtr; typedef mozilla::UniquePtr CATAdminContextUniquePtr; static const DWORD kEncodingTypes = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING; class SignedBinary final { public: SignedBinary(const wchar_t* aFilePath, mozilla::AuthenticodeFlags aFlags); explicit operator bool() const { return mCertStore && mCryptMsg && mCertCtx; } bool HasNestedMicrosoftSignature() const; mozilla::UniquePtr GetOrgName(); SignedBinary(const SignedBinary&) = delete; SignedBinary(SignedBinary&&) = delete; SignedBinary& operator=(const SignedBinary&) = delete; SignedBinary& operator=(SignedBinary&&) = delete; private: bool VerifySignature(const wchar_t* aFilePath); bool QueryObject(const wchar_t* aFilePath); static bool VerifySignatureInternal(WINTRUST_DATA& aTrustData); private: enum class TrustSource { eNone, eEmbedded, eCatalog }; private: const mozilla::AuthenticodeFlags mFlags; TrustSource mTrustSource; CertStoreUniquePtr mCertStore; CryptMsgUniquePtr mCryptMsg; CertContextUniquePtr mCertCtx; }; static CertContextUniquePtr GetCertificateFromCryptMsg( const CryptMsgUniquePtr& aCryptMsg, const CertStoreUniquePtr& aCertStore) { DWORD certInfoLen = 0; BOOL ok = CryptMsgGetParam(aCryptMsg.get(), CMSG_SIGNER_CERT_INFO_PARAM, 0, nullptr, &certInfoLen); if (!ok) { return nullptr; } auto certInfoBuf = mozilla::MakeUnique(certInfoLen); ok = CryptMsgGetParam(aCryptMsg.get(), CMSG_SIGNER_CERT_INFO_PARAM, 0, certInfoBuf.get(), &certInfoLen); if (!ok) { return nullptr; } auto certInfo = reinterpret_cast(certInfoBuf.get()); PCCERT_CONTEXT certCtx = CertFindCertificateInStore(aCertStore.get(), kEncodingTypes, 0, CERT_FIND_SUBJECT_CERT, certInfo, nullptr); return CertContextUniquePtr(certCtx); } static mozilla::UniquePtr GetSignerOrganizationFromCertificate( const CertContextUniquePtr& certCtx) { DWORD charCount = CertGetNameStringW( certCtx.get(), CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nullptr, nullptr, 0); if (charCount <= 1) { // Not found return nullptr; } auto result = mozilla::MakeUnique(charCount); charCount = CertGetNameStringW(certCtx.get(), CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nullptr, result.get(), charCount); MOZ_ASSERT(charCount > 1); return result; } SignedBinary::SignedBinary(const wchar_t* aFilePath, mozilla::AuthenticodeFlags aFlags) : mFlags(aFlags), mTrustSource(TrustSource::eNone) { if (!VerifySignature(aFilePath)) { return; } CertContextUniquePtr certCtx = GetCertificateFromCryptMsg(mCryptMsg, mCertStore); if (!certCtx) { return; } mCertCtx.swap(certCtx); } bool SignedBinary::HasNestedMicrosoftSignature() const { // Look for nested certificates - see bug 1817026 DWORD attributesLen = 0; BOOL ok = CryptMsgGetParam(mCryptMsg.get(), CMSG_SIGNER_UNAUTH_ATTR_PARAM, 0, nullptr, &attributesLen); if (!ok) { return false; } auto attributesBuf = mozilla::MakeUnique(attributesLen); ok = CryptMsgGetParam(mCryptMsg.get(), CMSG_SIGNER_UNAUTH_ATTR_PARAM, 0, attributesBuf.get(), &attributesLen); if (!ok) { return false; } auto attributesInfo = reinterpret_cast(attributesBuf.get()); for (DWORD i = 0; i < attributesInfo->cAttr; ++i) { PCRYPT_ATTRIBUTE cur = &attributesInfo->rgAttr[i]; if (!strcmp(cur->pszObjId, szOID_NESTED_SIGNATURE)) { CryptMsgUniquePtr nestedMsg( CryptMsgOpenToDecode(kEncodingTypes, 0, 0, NULL, NULL, NULL)); if (!nestedMsg) { continue; } ok = CryptMsgUpdate(nestedMsg.get(), cur->rgValue->pbData, cur->rgValue->cbData, TRUE); if (!ok) { continue; } CertStoreUniquePtr nestedCertStore( CertOpenStore(CERT_STORE_PROV_MSG, kEncodingTypes, NULL, CERT_STORE_OPEN_EXISTING_FLAG, nestedMsg.get())); if (!nestedCertStore) { continue; } CertContextUniquePtr nestedCertCtx = GetCertificateFromCryptMsg(nestedMsg, nestedCertStore); if (!nestedCertCtx) { continue; } mozilla::UniquePtr nestedSigner = GetSignerOrganizationFromCertificate(nestedCertCtx); if (!nestedSigner) { continue; } if (!wcscmp(nestedSigner.get(), L"Microsoft Windows") || !wcscmp(nestedSigner.get(), L"Microsoft Corporation")) { return true; } } } return false; } bool SignedBinary::QueryObject(const wchar_t* aFilePath) { DWORD encodingType, contentType, formatType; HCERTSTORE rawCertStore; HCRYPTMSG rawCryptMsg; BOOL result = ::CryptQueryObject(CERT_QUERY_OBJECT_FILE, aFilePath, CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, CERT_QUERY_FORMAT_FLAG_BINARY, 0, &encodingType, &contentType, &formatType, &rawCertStore, &rawCryptMsg, nullptr); if (!result) { return false; } mCertStore.reset(rawCertStore); mCryptMsg.reset(rawCryptMsg); return true; } /** * @param aTrustData must be a WINTRUST_DATA structure that has been zeroed out * and then populated at least with its |cbStruct|, * |dwUnionChoice|, and appropriate union field. This function * will then populate the remaining fields as appropriate. */ /* static */ bool SignedBinary::VerifySignatureInternal(WINTRUST_DATA& aTrustData) { static const mozilla::StaticDynamicallyLinkedFunctionPtr< decltype(&::WinVerifyTrust)> pWinVerifyTrust(L"wintrust.dll", "WinVerifyTrust"); if (!pWinVerifyTrust) { return false; } aTrustData.dwUIChoice = WTD_UI_NONE; aTrustData.fdwRevocationChecks = WTD_REVOKE_NONE; aTrustData.dwStateAction = WTD_STATEACTION_VERIFY; aTrustData.dwProvFlags = WTD_CACHE_ONLY_URL_RETRIEVAL; const HWND hwnd = (HWND)INVALID_HANDLE_VALUE; GUID policyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2; LONG result = pWinVerifyTrust(hwnd, &policyGUID, &aTrustData); aTrustData.dwStateAction = WTD_STATEACTION_CLOSE; pWinVerifyTrust(hwnd, &policyGUID, &aTrustData); return result == ERROR_SUCCESS; } bool SignedBinary::VerifySignature(const wchar_t* aFilePath) { // First, try the binary itself if (QueryObject(aFilePath)) { mTrustSource = TrustSource::eEmbedded; if (mFlags & mozilla::AuthenticodeFlags::SkipTrustVerification) { return true; } WINTRUST_FILE_INFO fileInfo = {sizeof(fileInfo)}; fileInfo.pcwszFilePath = aFilePath; WINTRUST_DATA trustData = {sizeof(trustData)}; trustData.dwUnionChoice = WTD_CHOICE_FILE; trustData.pFile = &fileInfo; return VerifySignatureInternal(trustData); } // We didn't find anything in the binary, so now try a catalog file. // First, we open a catalog admin context. HCATADMIN rawCatAdmin; static const mozilla::StaticDynamicallyLinkedFunctionPtr< decltype(&::CryptCATAdminAcquireContext2)> pCryptCATAdminAcquireContext2(L"wintrust.dll", "CryptCATAdminAcquireContext2"); if (!pCryptCATAdminAcquireContext2) { return false; } CERT_STRONG_SIGN_PARA policy = {sizeof(policy)}; policy.dwInfoChoice = CERT_STRONG_SIGN_OID_INFO_CHOICE; policy.pszOID = const_cast( szOID_CERT_STRONG_SIGN_OS_CURRENT); // -Wwritable-strings if (!pCryptCATAdminAcquireContext2(&rawCatAdmin, nullptr, BCRYPT_SHA256_ALGORITHM, &policy, 0)) { return false; } CATAdminContextUniquePtr catAdmin(rawCatAdmin); // Now we need to hash the file at aFilePath. // Since we're hashing this file, let's open it with a sequential scan hint. HANDLE rawFile = ::CreateFileW(aFilePath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr); if (rawFile == INVALID_HANDLE_VALUE) { return false; } nsAutoHandle file(rawFile); DWORD hashLen = 0; mozilla::UniquePtr hashBuf; static const mozilla::StaticDynamicallyLinkedFunctionPtr< decltype(&::CryptCATAdminCalcHashFromFileHandle2)> pCryptCATAdminCalcHashFromFileHandle2( L"wintrust.dll", "CryptCATAdminCalcHashFromFileHandle2"); if (pCryptCATAdminCalcHashFromFileHandle2) { if (!pCryptCATAdminCalcHashFromFileHandle2(rawCatAdmin, rawFile, &hashLen, nullptr, 0) && ::GetLastError() != ERROR_INSUFFICIENT_BUFFER) { return false; } hashBuf = mozilla::MakeUnique(hashLen); if (!pCryptCATAdminCalcHashFromFileHandle2(rawCatAdmin, rawFile, &hashLen, hashBuf.get(), 0)) { return false; } } else { static const mozilla::StaticDynamicallyLinkedFunctionPtr< decltype(&::CryptCATAdminCalcHashFromFileHandle)> pCryptCATAdminCalcHashFromFileHandle( L"wintrust.dll", "CryptCATAdminCalcHashFromFileHandle"); if (!pCryptCATAdminCalcHashFromFileHandle) { return false; } if (!pCryptCATAdminCalcHashFromFileHandle(rawFile, &hashLen, nullptr, 0) && ::GetLastError() != ERROR_INSUFFICIENT_BUFFER) { return false; } hashBuf = mozilla::MakeUnique(hashLen); if (!pCryptCATAdminCalcHashFromFileHandle(rawFile, &hashLen, hashBuf.get(), 0)) { return false; } } // Now that we've hashed the file, query the catalog system to see if any // catalogs reference a binary with our hash. static const mozilla::StaticDynamicallyLinkedFunctionPtr< decltype(&::CryptCATAdminEnumCatalogFromHash)> pCryptCATAdminEnumCatalogFromHash(L"wintrust.dll", "CryptCATAdminEnumCatalogFromHash"); if (!pCryptCATAdminEnumCatalogFromHash) { return false; } static const mozilla::StaticDynamicallyLinkedFunctionPtr< decltype(&::CryptCATAdminReleaseCatalogContext)> pCryptCATAdminReleaseCatalogContext(L"wintrust.dll", "CryptCATAdminReleaseCatalogContext"); if (!pCryptCATAdminReleaseCatalogContext) { return false; } HCATINFO catInfoHdl = pCryptCATAdminEnumCatalogFromHash( rawCatAdmin, hashBuf.get(), hashLen, 0, nullptr); if (!catInfoHdl) { return false; } // We can't use UniquePtr for this because the deleter function requires two // parameters. auto cleanCatInfoHdl = mozilla::MakeScopeExit([rawCatAdmin, catInfoHdl]() -> void { pCryptCATAdminReleaseCatalogContext(rawCatAdmin, catInfoHdl, 0); }); // We found a catalog! Now query for the path to the catalog file. static const mozilla::StaticDynamicallyLinkedFunctionPtr< decltype(&::CryptCATCatalogInfoFromContext)> pCryptCATCatalogInfoFromContext(L"wintrust.dll", "CryptCATCatalogInfoFromContext"); if (!pCryptCATCatalogInfoFromContext) { return false; } CATALOG_INFO_ catInfo = {sizeof(catInfo)}; if (!pCryptCATCatalogInfoFromContext(catInfoHdl, &catInfo, 0)) { return false; } if (!QueryObject(catInfo.wszCatalogFile)) { return false; } mTrustSource = TrustSource::eCatalog; if (mFlags & mozilla::AuthenticodeFlags::SkipTrustVerification) { return true; } // WINTRUST_CATALOG_INFO::pcwszMemberTag is commonly set to the string // representation of the file hash, so we build that here. DWORD strHashBufLen = (hashLen * 2) + 1; auto strHashBuf = mozilla::MakeUnique(strHashBufLen); if (!::CryptBinaryToStringW(hashBuf.get(), hashLen, CRYPT_STRING_HEXRAW | CRYPT_STRING_NOCRLF, strHashBuf.get(), &strHashBufLen)) { return false; } // Ensure that the tag is uppercase for WinVerifyTrust // NB: CryptBinaryToStringW overwrites strHashBufLen with the length excluding // the null terminator, so we need to add it back for this call. if (_wcsupr_s(strHashBuf.get(), strHashBufLen + 1)) { return false; } // Now, given the path to the catalog, and the path to the member (ie, the // binary whose hash we are validating), we may now validate. If the // validation is successful, we then QueryObject on the *catalog file* // instead of the binary. WINTRUST_CATALOG_INFO wtCatInfo = {sizeof(wtCatInfo)}; wtCatInfo.pcwszCatalogFilePath = catInfo.wszCatalogFile; wtCatInfo.pcwszMemberTag = strHashBuf.get(); wtCatInfo.pcwszMemberFilePath = aFilePath; wtCatInfo.hMemberFile = rawFile; wtCatInfo.hCatAdmin = rawCatAdmin; WINTRUST_DATA trustData = {sizeof(trustData)}; trustData.dwUnionChoice = WTD_CHOICE_CATALOG; trustData.pCatalog = &wtCatInfo; return VerifySignatureInternal(trustData); } mozilla::UniquePtr SignedBinary::GetOrgName() { return GetSignerOrganizationFromCertificate(mCertCtx); } } // anonymous namespace namespace mozilla { class AuthenticodeImpl : public Authenticode { public: virtual UniquePtr GetBinaryOrgName( const wchar_t* aFilePath, bool* aHasNestedMicrosoftSignature = nullptr, AuthenticodeFlags aFlags = AuthenticodeFlags::Default) override; }; UniquePtr AuthenticodeImpl::GetBinaryOrgName( const wchar_t* aFilePath, bool* aHasNestedMicrosoftSignature, AuthenticodeFlags aFlags) { if (aHasNestedMicrosoftSignature) { *aHasNestedMicrosoftSignature = false; } SignedBinary bin(aFilePath, aFlags); if (!bin) { return nullptr; } if (aHasNestedMicrosoftSignature) { *aHasNestedMicrosoftSignature = bin.HasNestedMicrosoftSignature(); } return bin.GetOrgName(); } static AuthenticodeImpl sAuthenticodeImpl; Authenticode* GetAuthenticode() { return &sAuthenticodeImpl; } } // namespace mozilla