使用 CryptUIWizDigitalSign API 对 appxbundle 进行签名

Posted

技术标签:

【中文标题】使用 CryptUIWizDigitalSign API 对 appxbundle 进行签名【英文标题】:Signing an appxbundle using CryptUIWizDigitalSign API 【发布时间】:2018-02-15 09:44:14 【问题描述】:

关于 Authenticode 签署 UWP appxbundle 文件,我面临一个相当有趣的问题。

一些背景: 客户向我们提供了包含签名证书的 SafeNet USB 令牌。当然,私钥是不可导出的。我希望能够将此证书用于我们的自动发布版本以对包进行签名。不幸的是,令牌需要在每个会话中输入一次 PIN,例如,如果构建代理重新启动,构建将失败。我们在令牌上启用了单点登录,因此每次会话解锁一次就足够了。

当前状态: 鉴于令牌已解锁,我们可以在 appxbundle 上使用 signtool 没有任何问题。这工作得很好,但一旦机器重新启动或工作站被锁定就会中断。

经过一番搜索,我找到了this 一段代码。这采用签名参数(包括令牌 PIN)并调用 Windows API 来对目标文件进行签名。我设法编译了它,它完美地为安装包装(EXE 文件)签名 - 令牌没有要求输入 PIN 并且由 API 调用自动解锁。

但是,当我在 appxbundle 文件上调用相同的代码时,对 CryptUIWizDigitalSign 的调用失败,错误代码为 0x80080209 APPX_E_INVALID_SIP_CLIENT_DATA。这对我来说是个谜,因为在同一个包上调用 signtool,使用相同的参数/证书可以正常工作,因此证书应该与包完全兼容。

有没有人有类似的经验?有没有办法找出错误的根本原因(我的证书和捆绑包之间不兼容的地方)?

编辑 1

回应评论:

我用来调用 API 的代码(直接取自上述 SO 问题)

#include <windows.h>
#include <cryptuiapi.h>
#include <iostream>
#include <string>
#pragma comment (lib, "cryptui.lib")

const std::wstring ETOKEN_BASE_CRYPT_PROV_NAME = L"eToken Base Cryptographic Provider";

std::string utf16_to_utf8(const std::wstring& str)

    if (str.empty())
    
        return "";
    

    auto utf8len = ::WideCharToMultiByte(CP_UTF8, 0, str.data(), str.size(), NULL, 0, NULL, NULL);
    if (utf8len == 0)
    
        return "";
    

    std::string utf8Str;
    utf8Str.resize(utf8len);
    ::WideCharToMultiByte(CP_UTF8, 0, str.data(), str.size(), &utf8Str[0], utf8Str.size(), NULL, NULL);

    return utf8Str;


struct CryptProvHandle

    HCRYPTPROV Handle = NULL;
    CryptProvHandle(HCRYPTPROV handle = NULL) : Handle(handle) 
    ~CryptProvHandle()  if (Handle) ::CryptReleaseContext(Handle, 0); 
;

HCRYPTPROV token_logon(const std::wstring& containerName, const std::string& tokenPin)

    CryptProvHandle cryptProv;
    if (!::CryptAcquireContext(&cryptProv.Handle, containerName.c_str(), ETOKEN_BASE_CRYPT_PROV_NAME.c_str(), PROV_RSA_FULL, CRYPT_SILENT))
    
        std::wcerr << L"CryptAcquireContext failed, error " << std::hex << std::showbase << ::GetLastError() << L"\n";
        return NULL;
    

    if (!::CryptSetProvParam(cryptProv.Handle, PP_SIGNATURE_PIN, reinterpret_cast<const BYTE*>(tokenPin.c_str()), 0))
    
        std::wcerr << L"CryptSetProvParam failed, error " << std::hex << std::showbase << ::GetLastError() << L"\n";
        return NULL;
    

    auto result = cryptProv.Handle;
    cryptProv.Handle = NULL;
    return result;


int wmain(int argc, wchar_t** argv)

    if (argc < 6)
    
        std::wcerr << L"usage: etokensign.exe <certificate file path> <private key container name> <token PIN> <timestamp URL> <path to file to sign>\n";
        return 1;
    

    const std::wstring certFile = argv[1];
    const std::wstring containerName = argv[2];
    const std::wstring tokenPin = argv[3];
    const std::wstring timestampUrl = argv[4];
    const std::wstring fileToSign = argv[5];

    CryptProvHandle cryptProv = token_logon(containerName, utf16_to_utf8(tokenPin));
    if (!cryptProv.Handle)
    
        return 1;
    

    CRYPTUI_WIZ_DIGITAL_SIGN_EXTENDED_INFO extInfo = ;
    extInfo.dwSize = sizeof(extInfo);
    extInfo.pszHashAlg = szOID_NIST_sha256; // Use SHA256 instead of default SHA1

    CRYPT_KEY_PROV_INFO keyProvInfo = ;
    keyProvInfo.pwszContainerName = const_cast<wchar_t*>(containerName.c_str());
    keyProvInfo.pwszProvName = const_cast<wchar_t*>(ETOKEN_BASE_CRYPT_PROV_NAME.c_str());
    keyProvInfo.dwProvType = PROV_RSA_FULL;

    CRYPTUI_WIZ_DIGITAL_SIGN_CERT_PVK_INFO pvkInfo = ;
    pvkInfo.dwSize = sizeof(pvkInfo);
    pvkInfo.pwszSigningCertFileName = const_cast<wchar_t*>(certFile.c_str());
    pvkInfo.dwPvkChoice = CRYPTUI_WIZ_DIGITAL_SIGN_PVK_PROV;
    pvkInfo.pPvkProvInfo = &keyProvInfo;

    CRYPTUI_WIZ_DIGITAL_SIGN_INFO signInfo = ;
    signInfo.dwSize = sizeof(signInfo);
    signInfo.dwSubjectChoice = CRYPTUI_WIZ_DIGITAL_SIGN_SUBJECT_FILE;
    signInfo.pwszFileName = fileToSign.c_str();
    signInfo.dwSigningCertChoice = CRYPTUI_WIZ_DIGITAL_SIGN_PVK;
    signInfo.pSigningCertPvkInfo = &pvkInfo;
    signInfo.pwszTimestampURL = timestampUrl.c_str();
    signInfo.pSignExtInfo = &extInfo;

    if (!::CryptUIWizDigitalSign(CRYPTUI_WIZ_NO_UI, NULL, NULL, &signInfo, NULL))
    
        std::wcerr << L"CryptUIWizDigitalSign failed, error " << std::hex << std::showbase << ::GetLastError() << L"\n";
        return 1;
    

    std::wcout << L"Successfully signed " << fileToSign << L"\n";
    return 0;

证书是从令牌导出的 CER 文件(仅限公共部分),容器名称取自令牌的信息。正如我所提到的,这适用于 EXE 文件。

signtool 命令

signtool sign /sha1 "cert thumbprint" /fd SHA256 /n "subject name" /t "http://timestamp.verisign.com/scripts/timestamp.dll" /debug "$path"

当我在令牌解锁时手动或从 CI 构建调用它时,这也有效。但上面的代码因上述错误而失败。

编辑 2

感谢大家,我现在有了一个有效的实现!正如 RbMm 所建议的那样,我最终使用了SignerSignEx2 API。这似乎适用于 appx 包和 PE 文件(每个文件的参数不同)。在 Windows 10 上使用 TFS 2017 构建代理进行验证 - 解锁令牌,在证书存储中找到指定的证书,并对指定的文件进行签名和时间戳

我将结果发布在 GitHub 上,如果有人感兴趣:https://github.com/mareklinka/SafeNetTokenSigner

【问题讨论】:

能否同时添加失败的代码以及您正在使用的signtool.exe命令行? 您能否检查您的事件日志,看看是否可以找到有关您的错误的任何建议:“用于签署包的 SIP_SUBJECTINFO 结构不包含所需的数据。”。对于故障排除,我们可以看到here 我在Event Viewer (Local) &gt; Applications and Services Logs &gt; Microsoft &gt; Windows &gt; AppxPackagingOM &gt; Microsoft-Windows-AppxPackaging/Operational查看了日志,但是当我点击API时只添加了这个信息级记录:The bundle reader was created successfully without manifest validation. 我可能应该补充一点,我尝试签名的 appxbundle 在由 CI 服务器构建时使用自签名证书进行签名 - 此自签名证书安装在构建机器上,但不是作为受信任的根。因此,当我调用 API 调用时已经存在一个签名 - 我想用全球信任的证书替换它。 你试试这个代码 - How to programmatically sign an app package 吗? 【参考方案1】:

首先我看看CryptUIWizDigitalSign 失败的地方:

CryptUIWizDigitalSign 调用了SignerSignEx 函数,带有pSipData == 0。用于签署 PE 文件(exedllsys) - 这没关系,并且会起作用。但是对于 appxbundle(zip 存档文件类型),此参数是必需的,并且必须指向 APPX_SIP_CLIENT_DATA:对于 appxbundle,调用堆栈是

CryptUIWizDigitalSign 
SignerSignEx
HRESULT Appx::Packaging::AppxSipClientData::Initialize(SIP_SUBJECTINFO* subjectInfo)

Appx::Packaging::AppxSipClientData::Initialize的开头我们可以查看下一个代码:

if (!subjectInfo->pClientData) return APPX_E_INVALID_SIP_CLIENT_DATA;

这正是您的代码失败的地方。

在这种情况下需要直接调用SignerSignEx2 而不是CryptUIWizDigitalSignpSipData 是强制参数。

在 msdn 中存在完整的工作示例 - How to programmatically sign an app package (C++)

这里的重点:

APPX_SIP_CLIENT_DATA sipClientData = ;
sipClientData.pSignerParams = &signerParams;
signerParams.pSipData = &sipClientData;

现代SignTool直接拨打SignerSignEx2

这里再次清晰可见:

if (!subjectInfo->pClientData) return APPX_E_INVALID_SIP_CLIENT_DATA;

在这之后调用

    HRESULT Appx::Packaging::Packaging::SignFile(
                 PCWSTR FileName, APPX_SIP_CLIENT_DATA* sipClientData)

这里开始下一个代码:

if (!sipClientData->pSignerParams) return APPX_E_INVALID_SIP_CLIENT_DATA;

msdn:

您必须提供一个指向 APPX_SIP_CLIENT_DATA 结构的指针 签署应用程序包时的 pSipData 参数。你必须 填充 APPX_SIP_CLIENT_DATApSignerParams 成员 用于签署应用程序包的相同参数。去做这个, 在 SIGNER_SIGN_EX2_PARAMS 上定义您想要的参数 结构体,将此结构体的地址分配给pSignerParams, 然后直接引用结构的成员,当你 调用 SignerSignEx2

问题 - 为什么需要再次提供在调用 SignerSignEx2 中使用的相同参数?因为appxbundle 是真正的存档,其中包含多个文件。每个文件都需要签名。对于这个Appx::Packaging::Packaging::SignFile递归再次调用SignerSignEx2

用于此递归调用pSignerParams 并用于调用SignerSignEx2,其参数与***调用完全相同

【讨论】:

感谢您的详细分析。我将在接下来的周末尝试实施这一点并回复您。 更新:我设法实现了一个签名应用程序,它可以对标准 PE 可执行文件和 appx 包进行签名,并且会自动登录到安全令牌。我们终于可以正确地自动化我们的发布构建了。再次感谢您的详细分析。 @mlinka - 如果PE 我们可以使用SignerSignExpSipData == 0。但对于 appx,只需要使用 SignerSignEx2pSipData 指向 APPX_SIP_CLIENT_DATA。同样对于 PE(驱动程序),我们有时需要使用 SPC_INC_PE_PAGE_HASHES_FLAG 标志(/ph option with SignTool。对于 appx 始终使用 SPC_EXC_PE_PAGE_HASHES_FLAG 看来SignerSignEx2 也适用于PE 文件,我只是跳过了SIP 数据(如SignerSignEx)。希望我们很快就不需要签署驱动程序代码,但它可能对其他人有所帮助。干杯! @mlinka 是下一个调用链 SignerSign -> SignerSignEx -> SignerSignEx2 -> SignerSignEx3 - 所以每个 api 调用扩展版本,附加参数设置为 0 。当然SignerSignEx2 也适用于 PE。我最初编写的代码使用“SignerSignEx”来自动签署自己的驱动程序:)

以上是关于使用 CryptUIWizDigitalSign API 对 appxbundle 进行签名的主要内容,如果未能解决你的问题,请参考以下文章

第一篇 用于测试使用

在使用加载数据流步骤的猪中,使用(使用 PigStorage)和不使用它有啥区别?

今目标使用教程 今目标任务使用篇

Qt静态编译时使用OpenSSL有三种方式(不使用,动态使用,静态使用,默认是动态使用)

MySQL db 在按日期排序时使用“使用位置;使用临时;使用文件排序”

使用“使用严格”作为“使用强”的备份