1. 项目概述:当浏览器遇到“不安全”的网站
作为一名在客户端安全领域摸爬滚打了十多年的开发者,我几乎每天都要和SSL/TLS证书打交道。无论是开发浏览器内核,还是调试一个需要安全连接的移动应用,证书验证这道“门禁”都是绕不开的核心环节。最近,我在为一个名为“Lightning-Browser”的开源浏览器项目贡献代码时,深入处理了其SSL安全模块,特别是证书验证失败时,如何向用户清晰、安全地展示警告对话框。这听起来像是一个简单的UI提示,但背后涉及了从密码学验证到用户体验设计的完整链条,任何一个环节的疏漏都可能导致严重的安全问题或糟糕的用户体验。
简单来说,这个项目的核心任务就是:当Lightning-Browser访问一个HTTPS网站,但服务器的SSL证书存在问题(比如过期、域名不匹配、签发机构不受信任)时,浏览器不能简单地、静默地阻断连接,也不能鲁莽地放行。它必须中断当前的连接流程,弹出一个清晰、易懂且操作指引明确的警告对话框,将风险告知用户,并由用户决定是“冒险继续”还是“安全退回”。这个过程,我们称之为“证书验证与警告处理”。对于普通用户,这可能只是一个偶尔弹出的红色警告页;但对于我们开发者而言,这里面包含了证书链验证、错误分类、风险评级、界面信息组织以及后续行为处理等一系列精密的设计与实现。接下来,我就结合在Lightning-Browser中的实践,把这套机制的“里里外外”拆解清楚。
2. 证书验证的核心原理与Lightning-Browser的实现考量
在深入代码之前,我们必须先搞清楚浏览器到底在验证什么。SSL/TLS证书不是一个孤立的文件,它本质上是一个由可信的证书颁发机构(CA)签发的数字“身份证”,遵循X.509标准。浏览器的验证是一个多层次、链式的过程。
2.1 证书验证的“四道安检门”
当Lightning-Browser接收到服务器发来的证书后,验证流程会依次通过以下几道关卡:
-
有效性检查
:这是最基础的检查。包括证书是否在声明的“生效日期”和“过期日期”之内。一个过期的证书就像一张过期的身份证,失去了效力。在实现时,我们需要严格比对当前系统时间与证书中的
notBefore和notAfter字段。 -
域名匹配检查
:证书是为特定域名签发的。浏览器需要检查当前访问的网站主机名(Hostname)是否与证书中
Subject Alternative Name (SAN)扩展字段或Common Name (CN)字段匹配。这里有个关键点:现代实践 强烈推荐使用SAN扩展 ,CN字段已被弃用用于主机名验证。我们的验证逻辑必须优先检查SAN列表。不匹配的典型错误就是“NET::ERR_CERT_COMMON_NAME_INVALID”。 -
签名与链式信任验证
:这是密码学的核心。服务器证书并非由根CA直接签发,通常存在一个或多个中间CA证书。浏览器需要:
- 使用颁发者CA的公钥,去验证服务器证书上数字签名的有效性。
- 递归地验证整个证书链,直到一个受浏览器信任的根CA证书。这些根CA证书通常预置在操作系统的证书存储(如Windows的Cert Store, macOS的Keychain)或浏览器自带的根证书列表中(如Firefox)。Lightning-Browser作为一个轻量级浏览器,需要决定是依赖系统存储还是维护自己的信任库。为了兼容性和轻量化,初期我们选择了依赖系统信任库。
- 吊销状态检查 :即使证书有效且被信任,也可能因为私钥泄露等原因而被CA吊销。浏览器需要通过在线证书状态协议(OCSP)或证书吊销列表(CRL)来查询证书是否已被吊销。 这是一个常常被简化或忽略的环节 ,因为OCSP查询会产生额外的网络延迟和隐私顾虑(向CA服务器泄露访问行为)。在Lightning-Browser中,出于性能和隐私的权衡,我们默认没有启用严格的OCSP装订(Stapling)验证,但这在安全要求极高的场景下是一个可配置的选项。
2.2 Lightning-Browser的验证器架构选择
在实现验证器时,我们面临几个关键选择:
-
使用系统库还是第三方库?
像Windows的
Schannel、macOS的SecureTransport和Linux上常用的OpenSSL,都是成熟的系统级解决方案。它们的优点是稳定、与系统安全策略深度集成、性能优化好。Lightning-Browser为了保持跨平台一致性和避免平台特定代码的复杂性,选择了使用OpenSSL(在Android上使用BoringSSL变体)作为底层的密码学库。这让我们能用一套相对统一的C/C++代码处理所有平台的证书解析与验证逻辑。 - 同步验证还是异步验证? 证书验证,特别是涉及网络请求的OCSP检查,是一个可能耗时的I/O操作。如果在网络主线程上进行同步验证,会直接导致页面加载卡顿。因此,我们的设计是将证书验证任务抛到一个独立的、高优先级的后台安全线程中执行。只有当验证完成后,才将结果(成功或具体的错误码)回调到UI线程,决定是否弹出警告对话框。这种异步架构是保证浏览器响应流畅的关键。
-
错误分类与粒度
:不是所有证书错误都是同等严重的。我们将错误分为几个等级:
- 致命错误 :如证书签名无效、无法找到信任链、证书被明确吊销。这类错误通常直接阻止连接,警告对话框只提供“返回安全页”的选项。
- 严重警告 :如域名不匹配、证书过期。风险很高,但某些内部或测试环境可能需要临时访问。对话框会提供强烈的警告,但允许用户“高级”->“继续前往(不安全)”。
- 一般警告 :如使用了弱签名算法(SHA-1)、证书即将过期。这些信息可能记录在日志里,但未必会打断普通用户的访问,除非启用严格模式。
踩坑心得 :在早期版本中,我们曾将“自签名证书”简单归类为致命错误。但很多开发者在本地搭建测试环境时都会使用自签名证书。这导致开发者群体抱怨不断。后来我们调整了策略:对于自签名证书,将其归类为严重警告,并在错误信息中明确提示“此证书为自签名,无法验证其真实性”,同时允许高级用户选择例外并临时信任。这个改动极大地改善了开发体验。
3. 警告对话框的设计哲学与信息呈现
验证失败后,如何与用户沟通是下一个挑战。目标是在不引起恐慌的前提下,让用户理解风险并做出知情决策。一个糟糕的警告框要么过于技术化吓跑用户,要么过于模糊让用户轻易忽略风险。
3.1 对话框的UI/UX设计原则
我们为Lightning-Browser的证书警告对话框制定了几个核心原则:
- 视觉层级清晰 :使用强烈的颜色(如红色或橙色)和图标(如感叹号或锁的断裂图标)第一时间吸引用户注意。将最重要的信息——“您的连接不是私密连接”或“此网站的安全证书有问题”——放在最显眼的位置。
-
语言通俗化
:避免直接输出像“CERTIFICATE_VERIFY_FAILED”或“X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY”这样的原始错误码。必须将其翻译成用户能懂的语言。例如:
-
ERR_CERT_DATE_INVALID-> “此网站的安全证书已过期。” -
ERR_CERT_COMMON_NAME_INVALID-> “此网站的安全证书并非针对您尝试访问的网站地址颁发。” -
ERR_CERT_AUTHORITY_INVALID-> “此网站的安全证书并非由受信任的机构颁发。”
-
-
提供可操作的选项
:按钮文本必须明确。通常我们提供:
- 主按钮(安全推荐) :“返回安全页”或“转到其他网站”。
- 次要按钮(高风险操作) :“高级” -> “继续前往(不安全)”。这个选项必须被“藏”在高级选项下,并且按钮本身要使用次级样式,避免用户误点。
- 展示技术细节的通道 :对于高级用户或开发者,他们需要看到具体错误码和证书信息来调试问题。我们设计了一个“高级”或“详细信息”可展开区域,里面会显示原始错误码、证书颁发者、有效期等。这既满足了专业需求,又避免了对普通用户造成信息过载。
3.2 实现中的关键数据结构与流程
在代码层面,我们定义了一个
CertError
结构体来封装一次验证失败的所有信息:
struct CertError {
enum Severity {
FATAL,
SEVERE_WARNING,
WARNING
};
int net_error_code; // 例如 net::ERR_CERT_DATE_INVALID
Severity severity;
std::string message_localized; // 本地化的用户友好信息
std::string details_technical; // 技术详情,用于高级视图
scoped_refptr<SSLCertificate> failed_cert; // 指向出问题证书的引用
// ... 其他上下文信息,如主机名、请求URL等
};
当后台验证线程检测到错误时,它会创建一个
CertError
实例,并通过线程间通信机制(如通过
base::PostTask
)将其发送到UI线程。UI线程的事件循环接收到这个错误对象后,会触发警告对话框的显示逻辑。
对话框本身是一个独立的视图组件,它接收
CertError
对象,并根据其
severity
级别决定最终的UI表现。例如,对于
FATAL
级别的错误,对话框可能直接禁用“继续”按钮。
// 伪代码示例:UI线程处理错误
void OnCertVerificationFailed(const CertError& error) {
// 1. 检查是否已有针对此主机名的用户例外记录
if (HasUserMadeExceptionForHost(error.hostname)) {
// 静默跳过警告,继续连接(记录日志)
ProceedWithConnection();
return;
}
// 2. 根据错误严重性创建并显示对话框
CertWarningDialog* dialog = new CertWarningDialog(error);
dialog->SetCallback(base::BindOnce(&OnUserDecision, error.hostname, error.net_error_code));
dialog->Show();
}
void OnUserDecision(const std::string& hostname, int error_code, bool user_proceeded) {
if (user_proceeded) {
// 用户选择继续
RecordUserException(hostname, error_code); // 可能临时或永久记住此例外
RetryConnection(hostname); // 重新发起连接,本次跳过验证
} else {
// 用户选择返回
NavigateBackToSafety();
}
}
实操要点 :实现“记住此例外”功能要非常谨慎。我们最初设计的是“永久记住”,但这带来了安全风险:如果某个合法网站后来证书被劫持,浏览器将因为旧例外而不再警告。因此,我们改为了“仅对本次浏览会话有效”,即浏览器进程退出后例外自动清除。更复杂的实现可以参考Chrome,它会将例外与证书的公钥哈希绑定,如果证书换了,例外自动失效。
4. 深入证书验证的代码实现与调试
让我们深入到Lightning-Browser使用OpenSSL进行验证的具体代码片段。虽然不同浏览器内核细节不同,但核心流程大同小异。
4.1 使用OpenSSL进行链式验证
以下是一个高度简化的验证函数核心部分,展示了如何调用OpenSSL API:
#include <openssl/x509_vfy.h>
#include <openssl/ssl.h>
bool VerifyCertificateChain(X509* server_cert, const std::string& hostname) {
bool is_verified = false;
// 1. 创建一个证书存储上下文(X509_STORE_CTX)
X509_STORE_CTX* ctx = X509_STORE_CTX_new();
if (!ctx) return false;
// 2. 获取系统默认的信任证书存储(包含受信任的根CA)
X509_STORE* store = X509_STORE_get_default_store(); // 这是一个简化,实际需要跨平台适配
// 3. 初始化上下文,将待验证证书和信任库关联
X509_STORE_CTX_init(ctx, store, server_cert, nullptr /* 无额外中间证书链 */);
// 4. 设置验证参数:主机名检查和用途
X509_VERIFY_PARAM* param = X509_VERIFY_PARAM_new();
X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
X509_VERIFY_PARAM_set1_host(param, hostname.c_str(), hostname.length());
X509_STORE_CTX_set0_param(ctx, param);
// 5. 执行验证!
int verify_result = X509_verify_cert(ctx);
if (verify_result == 1) {
// 验证成功
is_verified = true;
} else {
// 验证失败,获取具体错误
int error_depth = X509_STORE_CTX_get_error_depth(ctx);
int error_code = X509_STORE_CTX_get_error(ctx);
X509* error_cert = X509_STORE_CTX_get_current_cert(ctx);
// 将OpenSSL错误码映射为我们内部的 CertError
CertError error = MapOpenSSLErrorToCertError(error_code, error_cert, hostname);
// ... 处理错误,准备触发UI警告
}
// 6. 清理资源
X509_STORE_CTX_free(ctx);
return is_verified;
}
4.2 错误码映射与处理
MapOpenSSLErrorToCertError
函数是核心,它负责将晦涩的OpenSSL错误(如
X509_V_ERR_CERT_HAS_EXPIRED
)转换为我们之前定义的、带有严重等级和友好信息的
CertError
对象。
CertError MapOpenSSLErrorToCertError(int openssl_err, X509* cert, const std::string& hostname) {
CertError error;
error.failed_cert = cert; // 假设已封装
switch (openssl_err) {
case X509_V_ERR_CERT_HAS_EXPIRED:
case X509_V_ERR_CERT_NOT_YET_VALID:
error.net_error_code = net::ERR_CERT_DATE_INVALID;
error.severity = CertError::SEVERE_WARNING;
error.message_localized = l10n_util::GetStringUTF16(IDS_CERT_ERROR_EXPIRED);
break;
case X509_V_ERR_HOSTNAME_MISMATCH:
error.net_error_code = net::ERR_CERT_COMMON_NAME_INVALID;
error.severity = CertError::SEVERE_WARNING;
error.message_localized = l10n_util::GetStringUTF16(IDS_CERT_ERROR_COMMON_NAME_INVALID);
break;
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN:
error.net_error_code = net::ERR_CERT_AUTHORITY_INVALID;
error.severity = CertError::SEVERE_WARNING; // 自签名视为严重警告,非致命
error.message_localized = l10n_util::GetStringUTF16(IDS_CERT_ERROR_AUTHORITY_INVALID);
break;
case X509_V_ERR_CERT_REVOKED:
error.net_error_code = net::ERR_CERT_REVOKED;
error.severity = CertError::FATAL; // 吊销是致命错误
error.message_localized = l10n_util::GetStringUTF16(IDS_CERT_ERROR_REVOKED);
break;
// ... 处理更多错误码
default:
// 未知错误,按致命处理
error.net_error_code = net::ERR_CERT_INVALID;
error.severity = CertError::FATAL;
error.message_localized = l10n_util::GetStringUTF16(IDS_CERT_ERROR_GENERIC);
}
// 填充技术细节,便于调试
std::stringstream details;
details << "OpenSSL Error Code: " << openssl_err << "\n";
details << "Error String: " << X509_verify_cert_error_string(openssl_err) << "\n";
if (cert) {
char subject[256];
X509_NAME_oneline(X509_get_subject_name(cert), subject, 256);
details << "Certificate Subject: " << subject << "\n";
}
error.details_technical = details.str();
return error;
}
4.3 调试与测试策略
开发和调试证书验证逻辑非常具有挑战性,因为你不能总去访问一个有真实问题的生产网站。我们建立了一套本地测试环境:
-
使用
mkcert工具 :这是一个极佳的工具,可以一键在本地创建被本地信任的根CA,并签发任意域名的证书。这让我们能完美模拟“受信任的证书”场景。 -
搭建有问题的测试服务器
:我们使用一个简单的Python HTTPS服务器,并动态加载不同的测试证书。
-
过期证书
:修改系统时间,或使用
openssl命令修改证书有效期后重启服务器。 -
域名不匹配证书
:用
mkcert为example.test签发证书,但让服务器在localhost上使用它。 -
自签名证书
:直接用
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes生成。
-
过期证书
:修改系统时间,或使用
-
单元测试与网络测试
:我们将核心的验证函数
VerifyCertificateChain进行单元测试,模拟输入各种错误的证书数据。同时,编写网络层测试,让浏览器实际去连接我们控制的、证书有问题的测试服务器,并断言正确的警告对话框被触发。
排查技巧 :在调试证书错误时,一个非常有效的方法是启用OpenSSL的详细日志。在编译Lightning-Browser的Debug版本时,可以设置环境变量
OPENSSL_DEBUG=1,并在代码中调用SSL_CTX_set_info_callback来注册一个回调函数,打印出SSL握手和验证过程中的所有细节,这对于定位复杂的链式信任问题至关重要。
5. 高级话题:证书固定、HPKP的教训与现代替代方案
在实现了基本的证书验证和警告后,我们还需要考虑更高级的攻击场景:如果一个受信任的CA本身被攻破(并非天方夜谭),它就可以签发任意域名的欺诈证书。传统的CA信任模型在此刻失效。为此,业界曾引入“证书固定”(Certificate Pinning)和“HTTP公钥固定”(HPKP)技术。
5.1 证书固定的原理与实现
证书固定的思想很简单:浏览器或应用预先知道某个网站 应该 使用哪个或哪几个特定的公钥(或证书)。在连接时,即使服务器出示的证书能被CA链验证通过,但如果其公钥不在预置的固定列表中,连接也会被拒绝。
在Lightning-Browser中,我们曾为少数高安全需求的内部站点实现了编译时固定。具体做法是将目标站点证书的公钥哈希(SHA-256)硬编码在浏览器的源代码或一个配置文件中:
// 伪代码:在验证通过后,额外进行公钥固定检查
bool CheckPublicKeyPinning(const std::string& hostname, X509* cert) {
// 从固定列表中查找该主机名的预期公钥哈希
std::vector<std::string> pinned_hashes = GetPinnedHashesForHost(hostname);
if (pinned_hashes.empty()) {
return true; // 该站点未配置固定,跳过检查
}
// 提取当前证书的公钥并计算哈希
EVP_PKEY* pubkey = X509_get_pubkey(cert);
std::string actual_hash = CalculateSPKIHash(pubkey); // 计算主题公钥信息哈希
EVP_PKEY_free(pubkey);
// 检查是否匹配
for (const auto& pinned_hash : pinned_hashes) {
if (actual_hash == pinned_hash) {
return true; // 匹配成功
}
}
// 没有匹配项,固定失败
LogPinFailure(hostname, actual_hash);
return false; // 这将触发一个特殊的固定失败错误
}
5.2 HPKP的兴衰与教训
HTTP公钥固定(HPKP)是一种通过HTTP响应头(
Public-Key-Pins
)动态告知浏览器固定信息的机制。它比编译时固定更灵活,但因其高风险性已被弃用。
HPKP的主要风险 :
- 锁定风险(Lockout) :如果网站管理员配置错误,比如固定了一个即将过期的备份密钥,而忘记更新HPKP头,会导致所有客户端在固定头过期前(可能长达数月)无法访问该网站。这是一种“自杀式”配置。
- 恶意固定 :如果攻击者能够中间人劫持一次HTTP连接(在HTTPS未完全覆盖的时代有可能),他就可以注入一个恶意的HPKP头,将网站固定到攻击者控制的密钥上,从而实现长期劫持。
正因为这些难以承受的风险,Chrome、Firefox等主流浏览器都已移除了对HPKP的支持。 这是一个重要的教训:安全机制如果过于复杂且容易误用,其带来的风险可能超过收益。
5.3 现代的替代方案:Expect-CT与Certificate Transparency
鉴于HPKP的失败,业界转向了更温和、可审计的方案。
-
Expect-CT :这是一个HTTP头,网站可以通过它声明“我希望浏览器使用Certificate Transparency(CT)策略来检查我的证书”。CT是一个公开的、可审计的日志系统,所有公开信任的CA都必须将其签发的证书提交到这些日志中。浏览器可以检查服务器证书是否出现在这些公开日志中。如果网站声明了
Expect-CT,但证书不在CT日志中,浏览器就会报告错误。这有效防止了CA私下签发未公开的欺诈证书。Lightning-Browser在实现时,会维护一个已知的CT日志服务器列表,并在验证证书时进行查询(或检查证书中的SCT扩展)。 -
Certificate Transparency (CT) 集成 :现代浏览器(包括我们的Lightning-Browser在较新版本中)已经将CT检查作为证书验证的 强制 部分。对于EV证书或新的普通证书,如果缺少有效的SCT(Signed Certificate Timestamp,由CT日志服务器签发),验证将直接失败。这大大增强了整个CA生态系统的可审计性和透明度。
个人体会 :从HPKP到Expect-CT和强制CT的演进,让我深刻体会到安全设计中的“灰度”思维。绝对化的、非黑即白的强制机制(如HPKP)在复杂的现实网络中往往很脆弱。而像CT这样的机制,它不直接阻止连接,而是通过增强透明度和事后审计能力来威慑作恶者,同时给了运维人员更大的容错空间,是一种更可持续的安全进化路径。在Lightning-Browser中,我们优先集成了CT验证,而放弃了动态HPKP的支持。
6. 跨平台差异与系统证书存储的坑
Lightning-Browser的目标是支持多个平台(Windows, macOS, Linux, Android),而不同操作系统的证书存储管理方式千差万别,这是实现中最繁琐的部分之一。
6.1 各平台证书存储访问方式
| 平台 | 主要证书存储 | 访问方式 | 注意事项 |
|---|---|---|---|
| Windows | Windows证书存储 (Cert Store) |
通过CryptoAPI或WinHTTP API。常用
CertOpenSystemStore
,
CertFindCertificateInStore
等函数。
| 存储分为“当前用户”和“本地计算机”,需要决定读取哪个。用户安装的根证书通常在“当前用户”的“Root”容器。 |
| macOS / iOS | Keychain(钥匙串) |
通过Security Framework的
SecTrustSettingsCopyTrustSettings
和
SecItemCopyMatching
等API。
|
钥匙串访问权限复杂,需要正确处理。系统根证书在
/System/Library/Keychains
,用户添加的在
~/Library/Keychains
。
|
| Linux | 通常为文件系统路径 |
常见路径:
/etc/ssl/certs/ca-certificates.crt
(Debian/Ubuntu),
/etc/pki/tls/certs/ca-bundle.crt
(RHEL/Fedora)。
|
依赖发行版。最可靠的方式是使用OpenSSL的
SSL_CTX_set_default_verify_paths
,它会读取
SSL_CERT_FILE
和
SSL_CERT_DIR
环境变量。
|
| Android | 系统CA存储 + 用户CA |
通过
java.security.cert
包或NDK中的
android_security_cert
相关API(较新版本)。App也可以自带CA包。
| 从Android 7.0开始,用户安装的CA证书默认不被App信任(除非App显式配置)。需要处理系统CA和用户CA的合并。 |
在Lightning-Browser中,我们抽象了一个
CertStoreManager
接口,并为每个平台提供了具体实现。其核心方法
GetSystemTrustAnchors()
负责返回一个包含所有受信任根CA证书的集合,供OpenSSL的
X509_STORE
使用。
6.2 遇到的典型问题与解决方案
-
Windows平台:证书存储枚举不全
-
问题
:初期使用
CertEnumCertificatesInStore时,发现某些系统根证书没有枚举出来。 -
排查
:发现有些根证书被标记了特殊属性,或者位于不同的物理存储区。需要同时打开“
CURRENT_USER\Root”和“LOCAL_MACHINE\Root”存储区进行合并。 - 解决 :实现一个更全面的枚举函数,遍历所有相关的存储区,并过滤掉重复项。
-
问题
:初期使用
-
macOS平台:权限与沙盒
- 问题 :在沙盒化的App中,直接读取系统钥匙串可能受限。
-
解决
:使用
SecTrustCopyAnchorCertificatesAPI,它专门用于获取系统信任的锚点证书,在沙盒内也有权限。对于用户证书,则依赖用户通过系统对话框手动导入到钥匙串,浏览器只需读取即可。
-
Linux平台:证书文件格式与更新
- 问题 :不同发行版的证书文件格式可能是PEM(文本)或DER(二进制),且文件路径不同。系统更新CA证书后,浏览器可能还在使用旧缓存。
-
解决
:实现一个文件监视器(如
inotify),监听标准CA证书文件或目录的变化。当检测到文件被修改(如运行了update-ca-certificates命令),就重新加载信任库。同时,在加载时同时尝试PEM和DER解析。
-
Android平台:用户证书的兼容性
- 问题 :在Android高版本上,即使用户在系统设置中安装了CA证书(例如用于调试抓包),我们的浏览器默认也无法信任它,导致无法访问配置了该证书的内部服务器。
-
解决
:我们提供了两个方案。一是在App设置中增加一个“信任用户安装的CA证书”的开关(需要向用户明确风险)。二是在App资源中内置一个自定义的CA证书包(
.pem文件),并在初始化时将其加载到X509_STORE中。对于企业级或调试版本,第二种方式更可控。
经验之谈 :处理系统证书存储是“脏活累活”,但极其重要。一个健壮的实现必须包含 回退机制 和 日志记录 。例如,如果从系统API获取信任锚失败,应该有一个备用的、内置的、最小化的根证书列表来保证基本功能。同时,在Debug版本中,详细记录加载到了哪些根证书、数量多少,这对于诊断“找不到信任链”这类问题有巨大帮助。我们曾在日志中发现,某台企业电脑因为组策略删除了某个特定根证书,导致公司内网的一个服务在所有基于我们浏览器的应用中都无法访问,正是通过详细的加载日志快速定位了问题。
1240

被折叠的 条评论
为什么被折叠?



