Подписание приложения с использованием CryptUIWizDigitalSign API

Я столкнулся с довольно интересной проблемой в отношении аутентификации подписи файла приложения 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

Ответ 1

в первую очередь я смотрю, где CryptUIWizDigitalSign не удалось: enter image description here

CryptUIWizDigitalSign называется функцией SignerSignEx, с pSipData == 0. для знака PE файла (exe, dll, sys) - это нормально и будет работать. но для appxbundle (тип файла архива zip) этот параметр обязателен и должен указывать на 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 прямой:

enter image description here

здесь снова ясно видно:

if (!subjectInfo->pClientData) return APPX_E_INVALID_SIP_CLIENT_DATA;

после этого

    HRESULT Appx::Packaging::Packaging::SignFile(
                 PCWSTR FileName, APPX_SIP_CLIENT_DATA* sipClientData)

enter image description here

здесь при запуске следующего кода:

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 снова:

enter image description here

для этих рекурсивных вызовов pSignerParams и используется для вызова SignerSignEx2 с точно такими же параметрами, как и верхний вызов