winapi - 使用 CryptUIWizDigitalSign API 签署 appxbundle

标签 winapi uwp cryptography signtool authenticode

我在对 UWP appxbundle 文件进行 Authenticode 签名方面遇到了一个相当有趣的问题。

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

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

经过一番搜索,我设法找到 this一段代码。这将获取签名参数(包括 token PIN)并调用 Windows API 对目标文件进行签名。我成功地编译了它,并且它完美地用于签署安装包装程序(EXE 文件) - token 不要求 PIN,并且通过 API 调用自动解锁。

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

有人有过类似的经历吗?有没有办法找出错误的根本原因(我的证书和 bundle 之间不兼容)?

编辑 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;
}

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

signtool 命令

signtool sign/sha1“证书指纹”/fd SHA256/n“主题名称”/t“http://timestamp.verisign.com/scripts/timestamp.dll”/debug“$path”

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

编辑2

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

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

最佳答案

首先我看看CryptUIWizDigitalSign失败的地方: enter image description here

名为 SignerSignExCryptUIWizDigitalSign函数,其中 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;

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

而不是CryptUIWizDigitalSign需要直接调用 SignerSignEx2在这种情况下,pSipData 是必需参数。

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

这里的关键点:

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

现代SignTool调用SignerSignEx2直接:

enter image description here

这里再次清晰可见:

if (!subjectInfo->pClientData) return APPX_E_INVALID_SIP_CLIENT_DATA;

在此之后调用

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

enter image description here

从这里开始下一个代码:

if (!sipClientData->pSignerParams) return APPX_E_INVALID_SIP_CLIENT_DATA;

这在 msdn 中有明确说明:

You must provide a pointer to an APPX_SIP_CLIENT_DATA structure as the pSipData parameter when you sign an app package. You must populate the pSignerParams member of APPX_SIP_CLIENT_DATA with the same parameters that you use to sign the app package. To do this, define your desired parameters on the SIGNER_SIGN_EX2_PARAMS structure, assign the address of this structure to pSignerParams, and then directly reference the structure's members as well when you call SignerSignEx2.

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

enter image description here

对于此递归调用 pSignerParams 并使用 - 用于调用 SignerSignEx2 ,其参数与顶部调用完全相同

关于winapi - 使用 CryptUIWizDigitalSign API 签署 appxbundle,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48804073/

相关文章:

Windows 8 : Unable to allocate 2GB with 3GB User Address Space

c++ - WinVerifyTrust 返回 CERT_E_CHAINING

c# - Windows 10 IoT 中的 ConnectionRefused

c# - UWP 如何创建承载内容的用户控件?

networking - AES Rijndael 和小/大端?

c# - RSACng 无法验证哈希

c# - 是否有可能杀死 WaitForSingleObject(handle, INFINITE)?

c - 如何使用调试器 (VS 2013) 跟踪 win32 API 程序中的系统调用?

c# - 如何正确读取/解释原始 C# 堆栈跟踪?

javascript - 浏览器 `crypto.createDecipher` "Uncaught RangeError: Invalid array length"