在 Windows 的安全体系中,数字签名扮演着“软件身份证”的角色。它可以证明一个程序确实来自某个发布者,并且在分发的过程中没有被篡改。
当下载一个系统更新、驱动程序,或者安装第三方应用时,操作系统往往会验证数字签名,确保软件来源合法、安全。那么,作为开发者或安全研究人员,该如何编程获取并验证这些数字签名呢?
本文将通过代码示例,逐步实现一个数字签名验证工具,并解析其原理和应用场景。
为什么要验证数字签名?
在信息安全领域,数字签名主要解决两个问题:
- 来源认证
通过证书绑定发布者的身份,确认文件确实来自于声称的公司或组织。比如系统文件通常签名于 Microsoft Corporation。 - 完整性保护
签名过程包含哈希计算,如果文件在传输中被篡改,签名将立刻失效。
对普通用户来说,这能防止中途被植入恶意代码;对企业来说,则能确保生产环境中运行的软件是可信的。
验证思路与 Windows API
Windows 提供了专门的 API 来处理数字签名验证,其中最关键的包括:
- WinVerifyTrust:验证文件的数字签名是否有效;
- WTHelperProvDataFromStateData:获取验证过程中的证书链数据;
- WTHelperGetProvSignerFromChain:提取签名者、时间戳签名者等信息;
- CertGetNameString:获取证书的持有人或颁发者名称;
- CryptHashCertificate2:计算证书的 SHA1 指纹。
我们的目标是用这些 API 编写一个签名验证器,它不仅要能告诉我们签名是否有效,还能展示签名的详细信息。
定义数据结构:存储验证结果
首先,需要定义一个结构体,用来存储每次验证得到的签名信息。
struct SignatureInfo {std::string status; // 验证状态简写(如 Valid、Expired)std::string statusMessage; // 状态详细描述std::string thumbprint; // 证书指纹(SHA1)std::string signer; // 签名者std::string issuer; // 颁发者std::string timeStamper; // 时间戳服务器(如果存在)bool isValid = false; // 签名是否有效
};
通过这样的结构体,就能清晰地记录和返回验证结果,方便展示和后续处理。
调用 WinVerifyTrust 验证签名
验证的第一步就是调用 WinVerifyTrust
,系统会说明这个文件的签名是否可信。
WINTRUST_FILE_INFO fileData = { sizeof(WINTRUST_FILE_INFO) };
fileData.pcwszFilePath = filePath;WINTRUST_DATA wintrustData = { sizeof(WINTRUST_DATA) };
wintrustData.dwUIChoice = WTD_UI_NONE; // 不弹出 UI
wintrustData.fdwRevocationChecks = WTD_REVOKE_WHOLECHAIN; // 检查整个证书链
wintrustData.dwUnionChoice = WTD_CHOICE_FILE;
wintrustData.pFile = &fileData;
wintrustData.dwStateAction = WTD_STATEACTION_VERIFY;GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2;
LONG result = WinVerifyTrust(NULL, &action, &wintrustData);
通过返回值 result
可以验证是否成功:
ERROR_SUCCESS
:签名有效;- 其他值:签名无效,需要进一步分析原因,比如证书过期、吊销、不受信任等。
这相当于签名验证的入口,后续的步骤都建立在这之上。
提取签名证书信息
如果验证成功,便可以提取证书链的详细信息,包括:
- 签名者(Signer):谁签了这个文件;
- 颁发者(Issuer):证书由谁颁发;
- 证书指纹(Thumbprint):用于唯一标识证书;
- 时间戳(Timestamp):签名时刻的时间戳,确保即使证书后来过期,签名依然有效。
核心代码如下:
CRYPT_PROVIDER_DATA* providerData = WTHelperProvDataFromStateData(wintrustData.hWVTStateData);
if (providerData) {CRYPT_PROVIDER_SGNR* signer = WTHelperGetProvSignerFromChain(providerData, 0, FALSE, 0);if (signer && signer->pasCertChain[0].pCert) {PCCERT_CONTEXT certContext = signer->pasCertChain[0].pCert;info.thumbprint = GetCertificateThumbprint(certContext);info.signer = GetCertificateName(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE);info.issuer = GetCertificateName(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE | CERT_NAME_ISSUER_FLAG);// 获取时间戳(如果存在)if (signer->csCounterSigners > 0) {CRYPT_PROVIDER_SGNR* timeStamper = WTHelperGetProvSignerFromChain(providerData, 0, TRUE, 0);if (timeStamper && timeStamper->pasCertChain[0].pCert) {info.timeStamper = GetCertificateName(timeStamper->pasCertChain[0].pCert, CERT_NAME_SIMPLE_DISPLAY_TYPE);}}}
}
通过证书链,可以把签名的每一个关键细节抽取出来。
辅助函数:让结果更直观
仅仅有 API 返回的原始数据并不友好,还需要写几个辅助函数,便于转化结果。
获取证书指纹
证书指纹是证书的 SHA1 哈希,常用于唯一标识:
static std::string GetCertificateThumbprint(PCCERT_CONTEXT certContext) {BYTE thumbprint[20] = {0};DWORD thumbprintSize = sizeof(thumbprint);if (CryptHashCertificate2(BCRYPT_SHA1_ALGORITHM, 0, NULL, certContext->pbCertEncoded, certContext->cbCertEncoded, thumbprint, &thumbprintSize)) {std::stringstream ss;for (DWORD i = 0; i < thumbprintSize; i++) {ss << std::hex << std::setw(2) << std::setfill('0') << (int)thumbprint[i];}return ss.str();}return "";
}
获取证书名称
证书包含了签名者和颁发者的名称,我们用 CertGetNameString
提取:
static std::string GetCertificateName(PCCERT_CONTEXT certContext, DWORD type) {DWORD size = CertGetNameStringA(certContext, type, 0, NULL, NULL, 0);if (size > 1) {std::vector<char> buffer(size);CertGetNameStringA(certContext, type, 0, NULL, buffer.data(), size);return buffer.data();}return "";
}
状态码映射
不同的错误码代表不同的签名状态,可以映射为更易懂的文字:
static std::string GetStatusText(LONG status) {switch (status) {case ERROR_SUCCESS: return "Valid";case TRUST_E_NOSIGNATURE: return "NotSigned";case CERT_E_EXPIRED: return "Expired";case CERT_E_REVOKED: return "Revoked";case TRUST_E_EXPLICIT_DISTRUST: return "Distrusted";case TRUST_E_BAD_DIGEST: return "Invalid";case CERT_E_UNTRUSTEDROOT: return "UntrustedRoot";default: return "Error";}
}
测试与运行结果
最后,可以这样使用:
int main() {auto info = SignatureVerifier::GetSignatureInfo(L"C:\\Windows\\explorer.exe");printf("Status: %s\n", info.status.c_str());printf("Message: %s\n", info.statusMessage.c_str());printf("Thumbprint: %s\n", info.thumbprint.c_str());printf("Signer: %s\n", info.signer.c_str());printf("Issuer: %s\n", info.issuer.c_str());printf("TimeStamper: %s\n", info.timeStamper.c_str());printf("IsValid: %s\n", info.isValid ? "Yes" : "No");
}
运行结果:
Status: Valid
Message: Signature verified
Thumbprint: 3b77d.........
Signer: Microsoft Windows
Issuer: Microsoft Windows
TimeStamper: Microsoft Time-Stamp Service
IsValid: Yes
这表明 explorer.exe
的签名有效,签名者是微软,签名带有可信时间戳。
应用场景
签名验证器不仅仅是一个演示工具,它在实际工作中有很多应用:
- 企业安全管理:批量检查公司内部运行的软件,确保没有未签名或不明来源的程序。
- 安全软件开发:防病毒软件、白名单系统常常依赖数字签名来判断文件的可信度。
- CI/CD 发布流程:在软件发布前自动验证签名,避免被篡改的文件进入生产环境。
在实际应用中,除了数字签名验证以外,借助专业的加固工具也能进一步增强软件的防护效果。例如 Virbox Protector 就是一款专注于 Native 层安全的商业级解决方案,能够在抵御调试和逆向方面提供额外保障,为软件供应链安全增加一道坚实的屏障。
总结
数字签名是验证软件来源和完整性的重要手段。通过调用 Windows 提供的 API,可以有效地检查文件签名的有效性,并获取证书的详细信息。无论是系统安全防护、软件发布流程,还是日常文件校验,合理使用签名验证机制都能提升整体的安全性和可信度。