Подписание appxbundle для CryptUIWizDigitalSign, используя API-интерфейс
Я столкнулся с довольно интересной проблемой в отношении подписи Authenticode файла UWP appxbundle.
Некоторые предпосылки: Клиент предоставил нам USB-маркер SafeNet, содержащий сертификат подписи. Разумеется, закрытый ключ не подлежит экспорту. Я хочу иметь возможность использовать этот сертификат для наших автоматизированных сборок выпуска, чтобы подписать пакет. К сожалению, маркер требует ввода PIN-кода один раз за сеанс, поэтому, например, если агент сборки перезагрузится, сборка завершится неудачей. Мы включен одиночный вход в систему на токене, так что достаточно разблокировать его один раз за сеанс.
Текущее состояние: Мы можем использовать signtool на appxbundle без каких-либо проблем, учитывая, что маркер был разблокирован. Это работает достаточно хорошо, но ломается, как только машина перезагружается или рабочая станция блокируется.
После некоторых поисков мне удалось найти Этот кусок кода. При этом берутся параметры подписи (включая пин-код токена) и вызывается Windows API для подписи целевого файла. Мне это удалось чтобы скомпилировать это, и он работал безупречно для подписи установочной оболочки (EXE-файла) - токен не просил PIN-код и был разблокирован автоматически вызовом API. Однако, когда я вызвал тот же код в файле appxbundle, вызовCryptUIWizDigitalSign
не удался с кодом ошибки 0x80080209 APPX_E_INVALID_SIP_CLIENT_DATA
. Это для меня загадка, потому что вызов signtool на том же пакете, с теми же параметрами / сертификат работает без проблем поэтому сертификат должен быть полностью совместим с пакет.
Есть ли у кого-нибудь опыт работы с чем-то подобным? Есть ли способ выяснить, что является основной причиной ошибки (что несовместимо между моим сертификатом и пакетом)?
EDIT 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
1 ответ:
Прежде всего я смотрю, где
CryptUIWizDigitalSign
потерпел неудачу:Вызванный
CryptUIWizDigitalSign
SignerSignEx
функция, сpSipData == 0
. для знака PE file (exe, dll, sys ) - это нормально и будет работать. но для appxbundle (zip archive file type) этот параметр обязателен и должен указывать на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 существует полный рабочий пример - как программно подписать пакет приложения (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 :
Необходимо указать указатель на структуру 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
с точно такими же параметрами, как и верхний вызов