使用CryptUIWizDigitalSign API签署appxbundle

mli*_*nka 18 winapi cryptography authenticode signtool uwp

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

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

当前状态:我们可以在appxbundle上使用signtool而不会出现任何问题,因为令牌已被解锁.这种方法运行良好,但一旦重新启动机器或工作站被锁定就会中断.

经过一番搜索,我找到了这段代码.这将获取签名参数(包括令牌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;
}
Run Code Online (Sandbox Code Playgroud)

证书是从令牌导出的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

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

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

RbM*_*bMm 7

首先,我看看CryptUIWizDigitalSign失败的地方: 在此输入图像描述

CryptUIWizDigitalSign调用的SignerSignEx函数,有pSipData == 0.用于签署PE文件(exe,dll,sys) - 这没关系,并且可以正常工作.但是对于appxbundle(zip存档文件类型)这个参数是必需的并且必须指向APPX_SIP_CLIENT_DATA:for appxbundle call stack is

CryptUIWizDigitalSign 
SignerSignEx
HRESULT Appx::Packaging::AppxSipClientData::Initialize(SIP_SUBJECTINFO* subjectInfo)
Run Code Online (Sandbox Code Playgroud)

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

if (!subjectInfo->pClientData) return APPX_E_INVALID_SIP_CLIENT_DATA;
Run Code Online (Sandbox Code Playgroud)

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

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

在msdn中存在完整的工作示例 - 如何以编程方式签署应用程序包(C++)

关键点在这里:

APPX_SIP_CLIENT_DATA sipClientData = {};
sipClientData.pSignerParams = &signerParams;
signerParams.pSipData = &sipClientData;
Run Code Online (Sandbox Code Playgroud)

现代SignTool电话SignerSignEx2直接:

在此输入图像描述

在这里再次明确可见:

if (!subjectInfo->pClientData) return APPX_E_INVALID_SIP_CLIENT_DATA;
Run Code Online (Sandbox Code Playgroud)

在此之后

    HRESULT Appx::Packaging::Packaging::SignFile(
                 PCWSTR FileName, APPX_SIP_CLIENT_DATA* sipClientData)
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

这里开始下一个代码:

if (!sipClientData->pSignerParams) return APPX_E_INVALID_SIP_CLIENT_DATA;
Run Code Online (Sandbox Code Playgroud)

msdn中明确说明:

在签署应用程序包时,必须提供指向APPX_SIP_CLIENT_DATA结构的指针作为pSipData参数.您必须填充pSignerParams成员APPX_SIP_CLIENT_DATA与您用来签署应用程序包相同的参数.为此,在SIGNER_SIGN_EX2_PARAMS 结构上定义所需的参数,将此结构的地址分配给pSignerParams,然后在调用SignerSignEx2时直接引用结构的成员.

问题 - 为什么需要再次提供相同的参数,用于通话SignerSignEx2?因为它appxbundle是真正的存档,包含多个文件.每个文件都需要签名.再次进行此Appx::Packaging::Packaging::SignFile 递归调用SignerSignEx2:

在此输入图像描述

对于这个递归调用pSignerParams和使用 - 用于调用SignerSignEx2与top调用完全相同的参数