使用 CryptUIWizDigitalSign 签署 appxbundle API
Signing an appxbundle using CryptUIWizDigitalSign API
我遇到了一个关于 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,使用相同的 parameters/certificate 没有问题 所以证书应该与包完全兼容。
有没有人有过这样的经历?有没有办法找出错误的根本原因(我的证书和捆绑包之间不兼容的原因)?
编辑 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 文件(仅 public 部分),容器名称取自令牌的信息。正如我提到的,这适用于 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 文件(每个文件的参数不同)。使用 TFS 2017 构建代理在 Windows 10 上验证 - 解锁令牌,在证书库中找到指定的证书,并为指定的文件签名+时间戳。
我在GitHub上发布了结果,如果有人感兴趣:https://github.com/mareklinka/SafeNetTokenSigner
首先我看看 CryptUIWizDigitalSign
失败的地方:
CryptUIWizDigitalSign
调用了 SignerSignEx
function, with pSipData == 0
. for sign PE file (exe, dll, sys) - this is ok and will be work. but for appxbundle (zip archive file type) this parameter mandatory and must point to 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
:
这里再次清晰可见:
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 中明确说明:
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
:
对于此递归调用 pSignerParams
并用于调用 SignerSignEx2
与顶部调用
具有完全相同的参数
我遇到了一个关于 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,使用相同的 parameters/certificate 没有问题 所以证书应该与包完全兼容。
有没有人有过这样的经历?有没有办法找出错误的根本原因(我的证书和捆绑包之间不兼容的原因)?
编辑 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 文件(仅 public 部分),容器名称取自令牌的信息。正如我提到的,这适用于 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 文件(每个文件的参数不同)。使用 TFS 2017 构建代理在 Windows 10 上验证 - 解锁令牌,在证书库中找到指定的证书,并为指定的文件签名+时间戳。
我在GitHub上发布了结果,如果有人感兴趣:https://github.com/mareklinka/SafeNetTokenSigner
首先我看看 CryptUIWizDigitalSign
失败的地方:
CryptUIWizDigitalSign
调用了 SignerSignEx
function, with pSipData == 0
. for sign PE file (exe, dll, sys) - this is ok and will be work. but for appxbundle (zip archive file type) this parameter mandatory and must point to 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
:
这里再次清晰可见:
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 中明确说明:
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
:
对于此递归调用 pSignerParams
并用于调用 SignerSignEx2
与顶部调用