Я столкнулся с довольно интересной проблемой в отношении аутентификации подписи файла приложения UWP appxbundle.
Некоторые предпосылки: клиент предоставил нам USB-токен SafeNet, содержащий сертификат подписи. Конечно, закрытый ключ не является экспортируемым. Я хочу, чтобы иметь возможность использовать этот сертификат для наших автоматических выпусков, чтобы подписать пакет. К сожалению, токен требует ввода PIN-кода один раз за сеанс, поэтому, например, если агент сборки перезагружается, сборка завершится неудачей. Мы включили единый вход в токен, чтобы он мог разблокировать его после сеанса.
Текущее состояние: мы можем использовать signtool на appxbundle без каких-либо проблем, учитывая, что токен разблокирован. Это работает достаточно хорошо, но прерывается, как только машина перезагружается или рабочая станция заблокирована.
После некоторых поисков мне удалось найти этот фрагмент кода. Это принимает параметры подписи (включая 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, когда токен разблокирован. Но вышеописанный код не соответствует указанной ошибке.
EDIT 2
Благодаря всем вам, теперь у меня есть рабочая реализация! В итоге я использовал API SignerSignEx2
, как это было предложено RbMm. Это, похоже, отлично работает как для пакетов appx, так и для файлов PE (для каждого из них есть разные параметры). Проверено в Windows 10 с агентом сборки TFS 2017 - разблокирует токен, находит указанный сертификат в хранилище сертификатов и выдает + отметку времени указанному файлу.
Я опубликовал результат на GitHub, если кому-то интересно: https://github.com/mareklinka/SafeNetTokenSigner