I'm facing a rather interesting issue in regards to Authenticode signing an UWP appxbundle file.
Some background: The client provided us with a SafeNet USB token containing the signing certificate. The private key is not exportable, of course. I want to be able to use this certificate for our automated release builds to sign the package. Unfortunately, the token requires a PIN to be entered once per session, so for example if the build agent reboots, the build will fail. We enabled single login on the token so it's enough to unlock it once a session.
Current state: We can use signtool on the appxbundle without any problems, given the token has been unlocked. This works well enough but breaks as soon as the machine is rebooted or the workstation is locked.
After some searching I managed to find this piece of code. This takes the signing parameters (including the token PIN) and invokes Windows API to sign the target file. I managed to compile this and it worked flawlessly for signing the installation wrapper (EXE file) - the token did not ask for PIN and was unlocked automatically by the API call.
However, when I invoked the same code on the appxbundle file, the call to CryptUIWizDigitalSign failed with error code 0x80080209 APPX_E_INVALID_SIP_CLIENT_DATA. This is a mystery to me because invoking signtool on the same bundle, with the same parameters/certificate works without problem so the certificate should be fully compatible with the package.
Does anyone have experience with something like this? Is there a way to figure out what is the root cause of the error (what is incompatible between my cert and the bundle)?
EDIT 1
In response to a comment:
The code I'm using to call the APIs (taken directly from the aforementioned SO question)
#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;
}
The certificate is a CER file (public portion only) exported from the token and the container name is taken from the token's info. As I mentioned, this works correctly for EXE files.
The signtool command
signtool sign /sha1 "cert thumbprint" /fd SHA256 /n "subject name" /t "http://timestamp.verisign.com/scripts/timestamp.dll" /debug "$path"
This also works, when I call it either manually or from the CI build when the token is unlocked. But the code above fails with the mentioned error.
EDIT 2
Thanks to all of you, I now have a working implementation! I ended up using the SignerSignEx2 API, as suggested by RbMm. This seems to work fine for both appx bundles and PE files (different parameters for each). Verified on Windows 10 with a TFS 2017 build agent - unlocks the token, finds a specified certificate in the cert store, and signs+timestamps the specified file.
I published the result on GitHub, if anyone is interested: https://github.com/mareklinka/SafeNetTokenSigner



