bootstrap: Check code signing certificates (see #3824)

If either the bootstrap executable or the client dll is code signed
then both must be valid (all signatures) and signed with the same
primary certificate. This is a protection against mixing binaries
with different trust levels.
This commit is contained in:
Marshall Greenblatt
2025-05-19 16:56:36 -04:00
parent 4ceedd7f43
commit ce365d4987
8 changed files with 326 additions and 27 deletions

View File

@@ -1250,6 +1250,8 @@ if (is_win) {
"libcef_dll/bootstrap/bootstrap_util_win.cc",
"libcef_dll/bootstrap/bootstrap_util_win.h",
"libcef_dll/bootstrap/bootstrap_win.cc",
"libcef_dll/bootstrap/certificate_util_win.cc",
"libcef_dll/bootstrap/certificate_util_win.h",
"libcef_dll/bootstrap/win/bootstrap.rc",
"libcef_dll/bootstrap/win/resource.h",
"libcef/browser/preferred_stack_size_win.inc",
@@ -1262,6 +1264,20 @@ if (is_win) {
"//build/win:default_exe_manifest",
]
bootstrap_libs = [
"crypt32.lib",
"wintrust.lib",
]
bootstrap_configs = [
":libcef_includes_config",
# Delay-load as many DLLs as possible for sandbox and startup perf
# improvements.
"//build/config/win:delayloads",
"//build/config/win:delayloads_not_for_child_dll",
]
# Windows application that initializes the sandbox and then passes
# execution to a client-provided DLL.
executable("bootstrap") {
@@ -1270,17 +1286,13 @@ if (is_win) {
sources = bootstrap_sources
deps = bootstrap_deps
configs += [ ":libcef_includes_config" ]
libs = bootstrap_libs
configs += bootstrap_configs
# Set /SUBSYSTEM:WINDOWS.
configs -= [ "//build/config/win:console" ]
configs += [ "//build/config/win:windowed" ]
# Delay-load as many DLLs as possible for sandbox and startup perf
# improvements.
configs += [ "//build/config/win:delayloads" ]
defines = [
"CEF_BUILD_BOOTSTRAP",
]
@@ -1293,12 +1305,8 @@ if (is_win) {
sources = bootstrap_sources
deps = bootstrap_deps
configs += [ ":libcef_includes_config" ]
# Delay-load as many DLLs as possible for sandbox and startup perf
# improvements.
configs += [ "//build/config/win:delayloads" ]
libs = bootstrap_libs
configs += bootstrap_configs
defines = [
"CEF_BUILD_BOOTSTRAP",

View File

@@ -67,16 +67,39 @@ std::wstring GetDefaultModuleValue(const base::FilePath& exe_path) {
return NamePart(exe_path);
}
bool IsModulePathAllowed(HMODULE module, const base::FilePath& exe_path) {
bool IsModulePathAllowed(const base::FilePath& module_path,
const base::FilePath& exe_path) {
// Allow any module path if the bootstrap executable has the default name.
if (IsDefaultExeName(NamePart(exe_path))) {
return true;
}
const auto& module_path = GetModulePath(module);
// Module must be at the same path as the executable.
return module_path.DirName() == exe_path.DirName();
}
std::wstring GetLastErrorAsString() {
std::wstring error_message;
DWORD error_message_id = ::GetLastError();
if (error_message_id == 0) {
return error_message;
}
LPWSTR message_buffer = NULL;
DWORD size = FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, error_message_id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPWSTR)&message_buffer, 0, NULL);
if (message_buffer) {
error_message = std::wstring(message_buffer, size);
LocalFree(message_buffer);
}
return error_message;
}
} // namespace bootstrap_util

View File

@@ -43,8 +43,11 @@ std::wstring GetValidatedModuleValue(const base::CommandLine& command_line,
// Returns the default module name (executable name without extension).
std::wstring GetDefaultModuleValue(const base::FilePath& exe_path);
// Returns true if loading |module| is allowed.
bool IsModulePathAllowed(HMODULE module, const base::FilePath& exe_path);
// Returns true if loading |module_path| is allowed.
bool IsModulePathAllowed(const base::FilePath& module_path,
const base::FilePath& exe_path);
std::wstring GetLastErrorAsString();
} // namespace bootstrap_util

View File

@@ -15,6 +15,7 @@
#include "cef/include/cef_sandbox_win.h"
#include "cef/libcef/browser/preferred_stack_size_win.inc"
#include "cef/libcef_dll/bootstrap/bootstrap_util_win.h"
#include "cef/libcef_dll/bootstrap/certificate_util_win.h"
#include "cef/libcef_dll/bootstrap/win/resource.h"
namespace {
@@ -96,6 +97,7 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
std::wstring dll_name;
base::FilePath exe_path;
certificate_util::ThumbprintsInfo exe_thumbprints;
if (is_sandboxed) {
// Running as a sandboxed sub-process. May already be locked down, so we
@@ -124,6 +126,19 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
ShowError(LoadString(IDS_ERROR_NO_MODULE_NAME));
return 1;
}
certificate_util::GetClientThumbprints(
exe_path.value(), /*verify_binary=*/true, exe_thumbprints);
// The executable must either be unsigned or have all valid signatures.
if (!exe_thumbprints.IsUnsignedOrValid()) {
// Some part of the certificate validation process failed.
const auto subst = std::to_array<std::u16string>(
{base::WideToUTF16(exe_path.BaseName().value()),
base::WideToUTF16(exe_thumbprints.errors)});
ShowError(FormatErrorString(IDS_ERROR_INVALID_CERT, subst));
return 1;
}
}
// Manage the life span of the sandbox information object. This is necessary
@@ -142,8 +157,33 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
std::wstring error;
if (HMODULE hModule = ::LoadLibrary(dll_name.c_str())) {
if (is_sandboxed ||
bootstrap_util::IsModulePathAllowed(hModule, exe_path)) {
if (!is_sandboxed) {
const auto& dll_path = bootstrap_util::GetModulePath(hModule);
if (!bootstrap_util::IsModulePathAllowed(dll_path, exe_path)) {
const auto subst =
std::to_array<std::u16string>({base::WideToUTF16(dll_name)});
error = FormatErrorString(IDS_ERROR_INVALID_LOCATION, subst);
}
if (error.empty()) {
certificate_util::ThumbprintsInfo dll_thumbprints;
certificate_util::GetClientThumbprints(
dll_path.value(), /*verify_binary=*/true, dll_thumbprints);
// The DLL and EXE must either both be unsigned or both have all valid
// signatures and the same primary thumbprint.
if (!dll_thumbprints.IsSame(exe_thumbprints, /*allow_unsigned=*/true)) {
// Some part of the certificate validation process failed.
const auto subst = std::to_array<std::u16string>(
{base::WideToUTF16(dll_name + TEXT(".dll")),
base::WideToUTF16(dll_thumbprints.errors)});
error = FormatErrorString(IDS_ERROR_INVALID_CERT, subst);
}
}
}
if (error.empty()) {
if (auto* pFunc = (kProcType)::GetProcAddress(hModule, kProcName)) {
#if defined(CEF_BUILD_BOOTSTRAP_CONSOLE)
return pFunc(argc, argv, sandbox_info);
@@ -153,20 +193,17 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
} else if (!is_sandboxed) {
const auto subst = std::to_array<std::u16string>(
{base::WideToUTF16(dll_name),
base::NumberToString16(::GetLastError()),
base::WideToUTF16(bootstrap_util::GetLastErrorAsString()),
base::ASCIIToUTF16(std::string(kProcName))});
error = FormatErrorString(IDS_ERROR_NO_PROC_EXPORT, subst);
}
} else if (!is_sandboxed) {
const auto subst =
std::to_array<std::u16string>({base::WideToUTF16(dll_name)});
error = FormatErrorString(IDS_ERROR_INVALID_LOCATION, subst);
}
FreeLibrary(hModule);
} else if (!is_sandboxed) {
const auto subst = std::to_array<std::u16string>(
{base::WideToUTF16(dll_name),
base::NumberToString16(::GetLastError())});
base::WideToUTF16(bootstrap_util::GetLastErrorAsString())});
error = FormatErrorString(IDS_ERROR_LOAD_FAILED, subst);
}

View File

@@ -0,0 +1,167 @@
// Copyright (c) 2025 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/certificate_util_win.h"
#include <windows.h>
#include <Softpub.h>
#include <wincrypt.h>
#include <wintrust.h>
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "cef/libcef_dll/bootstrap/bootstrap_util_win.h"
namespace certificate_util {
namespace {
bool InitCryptProviderStructs(WINTRUST_DATA& win_trust_data,
CRYPT_PROVIDER_DATA*& prov_data_ptr,
CRYPT_PROVIDER_SGNR*& prov_signer_ptr,
CRYPT_PROVIDER_CERT*& prov_cert_ptr) {
prov_data_ptr = WTHelperProvDataFromStateData(win_trust_data.hWVTStateData);
if (prov_data_ptr) {
prov_signer_ptr = WTHelperGetProvSignerFromChain(
(PCRYPT_PROVIDER_DATA)prov_data_ptr, 0, FALSE, 0);
if (prov_signer_ptr) {
prov_cert_ptr = WTHelperGetProvCertFromChain(prov_signer_ptr, 0);
if (prov_cert_ptr) {
return true;
}
}
}
return false;
}
std::string BytesToHexString(const void* bytes, size_t length) {
const unsigned char* bytes_c = reinterpret_cast<const unsigned char*>(bytes);
std::string hex_string;
hex_string.reserve(length * 2);
for (size_t index = 0; index < length; ++index) {
hex_string.append(base::StringPrintf("%02x", bytes_c[index]));
}
return hex_string;
}
} // namespace
void GetClientThumbprints(const std::wstring& binary_path,
bool verify_binary,
ThumbprintsInfo& info) {
const HWND wvt_handle = static_cast<HWND>(INVALID_HANDLE_VALUE);
GUID wvt_policy = WINTRUST_ACTION_GENERIC_VERIFY_V2;
WINTRUST_FILE_INFO file_info = {};
file_info.cbStruct = sizeof(file_info);
file_info.pcwszFilePath = binary_path.c_str();
WINTRUST_SIGNATURE_SETTINGS sig_settings = {};
sig_settings.cbStruct = sizeof(sig_settings);
// We will verify each signature separately, but also get the number of
// secondary signatures present in the file.
sig_settings.dwFlags = WSS_GET_SECONDARY_SIG_COUNT | WSS_VERIFY_SPECIFIC;
// cSecondarySigs starts off as 0. We assume we have one primary signature.
// After the first WinVerifyTrust call succeeds, we will continue inspecting
// the rest of the signatures.
for (DWORD i = 0; i < sig_settings.cSecondarySigs + 1; ++i) {
const auto& error_prefix =
base::ASCIIToWide(base::StringPrintf("\nCertificate %d: ", i));
WINTRUST_DATA win_trust_data = {0};
win_trust_data.cbStruct = sizeof(win_trust_data);
win_trust_data.dwUIChoice = WTD_UI_NONE;
// No revocation checking.
win_trust_data.fdwRevocationChecks = WTD_REVOKE_NONE;
// Prevent revocation checks over the network.
win_trust_data.dwProvFlags = WTD_CACHE_ONLY_URL_RETRIEVAL;
win_trust_data.dwUnionChoice = WTD_CHOICE_FILE;
win_trust_data.dwStateAction = WTD_STATEACTION_VERIFY;
win_trust_data.pFile = &file_info;
win_trust_data.pSignatureSettings = &sig_settings;
sig_settings.dwIndex = i;
const auto status =
WinVerifyTrust(wvt_handle, &wvt_policy, &win_trust_data);
const bool valid = status == ERROR_SUCCESS;
if (!valid) {
if (i == 0 && status == TRUST_E_NOSIGNATURE) {
const auto last_error = static_cast<HRESULT>(::GetLastError());
if (TRUST_E_NOSIGNATURE == last_error ||
TRUST_E_SUBJECT_FORM_UNKNOWN == last_error ||
TRUST_E_PROVIDER_UNKNOWN == last_error) {
// The file is not signed.
return;
}
}
info.errors += error_prefix + TEXT("WinVerifyTrust failed: ") +
bootstrap_util::GetLastErrorAsString();
// WinVerifyTrust will fail if the signing certificates can't be verified,
// but it will still provide information about them in the StateData
// structure. We only continue if the method asks for this.
if (verify_binary) {
// If the primary signature fails, we will return and not inspect the
// rest of the signatures.
if (i == 0) {
info.has_signature = true;
return;
}
continue;
}
}
if (!win_trust_data.hWVTStateData) {
info.errors += error_prefix + TEXT("No WinVerifyTrust data");
continue;
}
CRYPT_PROVIDER_DATA* prov_data = NULL;
CRYPT_PROVIDER_SGNR* prov_signer = NULL;
CRYPT_PROVIDER_CERT* prov_cert = NULL;
if (InitCryptProviderStructs(win_trust_data, prov_data, prov_signer,
prov_cert)) {
// Using SHA1 hash here because: (a) SHA1 is used internally by default in
// most tools that inspect certificates, (b) the SHA1 value is more likely
// to aleady be cached, (c) SHA1 is faster to compute than SHA256 if not
// already cached, and (d) SHA1 is still resistant to preimage attacks
// (e.g. trying to match specific hashes), particularly when used on DEC
// formatted certificates as in this case.
// SHA1 hash = 20 bytes.
BYTE sha1_bytes[20] = {};
DWORD sha1_bytes_count = sizeof(sha1_bytes);
// Read or compute the SHA1 hash of the certificate (thumbprint), and
// convert it to a hex string.
if (CertGetCertificateContextProperty(prov_cert->pCert,
CERT_SHA1_HASH_PROP_ID, sha1_bytes,
&sha1_bytes_count)) {
auto& thumbprints =
valid ? info.valid_thumbprints : info.invalid_thumbprints;
thumbprints.emplace_back(
BytesToHexString(sha1_bytes, sha1_bytes_count));
} else {
info.errors += error_prefix +
TEXT("CertGetCertificateContextProperty failed: ") +
bootstrap_util::GetLastErrorAsString();
}
} else {
info.errors += error_prefix + TEXT("Invalid WinVerifyTrust data");
}
win_trust_data.dwStateAction = WTD_STATEACTION_CLOSE;
WinVerifyTrust(wvt_handle, &wvt_policy, &win_trust_data);
}
info.has_signature = true;
}
} // namespace certificate_util

View File

@@ -0,0 +1,59 @@
// Copyright (c) 2025 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_CERTIFICATE_UTIL_WIN_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_CERTIFICATE_UTIL_WIN_H_
#pragma once
#include <windows.h>
#include <string>
#include <vector>
namespace certificate_util {
struct ThumbprintsInfo {
public:
bool IsSignedAndValid() const {
return !valid_thumbprints.empty() && errors.empty();
}
bool IsUnsignedOrValid() const {
return !has_signature || IsSignedAndValid();
}
bool IsSame(const ThumbprintsInfo& other, bool allow_unsigned) const {
if (allow_unsigned && !has_signature && !other.has_signature) {
return true;
}
// Returns true if both are valid and have the same primary thumbprint.
return IsSignedAndValid() && other.IsSignedAndValid() &&
valid_thumbprints[0] == other.valid_thumbprints[0];
}
// True if a primary signature exists, irrespective of validity.
bool has_signature = false;
// Thumbprints for signatures, if any, that passed verification.
std::vector<std::string> valid_thumbprints;
// Thumbprints for signatures, if any, that failed verification. Will not be
// populated if |verify_binary=true| was passed to GetClientThumbprints().
std::vector<std::string> invalid_thumbprints;
// Errors (newline delimited) if any signatures failed verification.
std::wstring errors;
};
// Process client signatures for the binary at |binary_path| and populate
// |info|. If |verify_binary| is true and the primary signature fails
// verification then no further signatures will be processed.
void GetClientThumbprints(const std::wstring& binary_path,
bool verify_binary,
ThumbprintsInfo& info);
} // namespace certificate_util
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_CERTIFICATE_UTIL_WIN_H_

View File

@@ -85,9 +85,10 @@ BEGIN
IDS_ERROR_TITLE "Runtime Error: $1"
IDS_ERROR_EXTRA_INFO "\nTry reinstalling the application to fix this error."
IDS_ERROR_NO_MODULE_NAME "Missing module name"
IDS_ERROR_NO_PROC_EXPORT "Failed to find $3 in $1.dll (error $2)"
IDS_ERROR_NO_PROC_EXPORT "Failed to find $3 in $1.dll\n$2"
IDS_ERROR_INVALID_LOCATION "Found $1.dll in an unexpected location"
IDS_ERROR_LOAD_FAILED "Failed to load $1.dll (error $2)"
IDS_ERROR_LOAD_FAILED "Failed to load $1.dll\n$2"
IDS_ERROR_INVALID_CERT "Failed certificate validation for $1\n$2"
END
#endif // English (U.S.) resources

View File

@@ -12,6 +12,7 @@
#define IDS_ERROR_NO_PROC_EXPORT 103
#define IDS_ERROR_INVALID_LOCATION 104
#define IDS_ERROR_LOAD_FAILED 105
#define IDS_ERROR_INVALID_CERT 106
// Next default values for new objects
//