Signature d'une appxbundle à L'aide de L'API CryptUIWizDigitalSign

je suis confronté à un problème assez intéressant en ce qui concerne la signature D'un fichier appxbundle UWP par Authenticode.

un peu de contexte: Le client nous a fourni un token USB SafeNet contenant le certificat de signature. La clé privée n'est pas exportable, bien sûr. Je veux être en mesure d'utiliser ce certificat pour notre automatisé release pour signer le package. Malheureusement, le token nécessite qu'une PIN soit entrée une fois par session, donc par exemple si l'agent de compilation redémarre, la compilation échouer. Nous avons activé une seule connexion sur le token, donc c'est suffisant pour le déverrouiller une fois par session.

état Actuel: Nous pouvons utiliser signtool sur l'appxbundle sans aucun problème, étant donné que le token a été déverrouillé. Cela fonctionne assez bien, mais les pauses dès que l'ordinateur est redémarré ou le poste de travail est verrouillé.

Après quelques recherches, j'ai réussi à trouver morceau de code. Cela prend les paramètres de signature (y compris la broche du token) et invoque L'API Windows pour signer le fichier cible. J'ai réussi à compiler ceci et cela a fonctionné sans problème pour signer le wrapper d'installation (fichier EXE) - le token n'a pas demandé de NIP et a été déverrouillé automatiquement par l'appel de L'API.

cependant, quand j'ai invoqué le même code sur le fichier appxbundle, l'appel à CryptUIWizDigitalSign a échoué avec le code d'erreur 0x80080209 APPX_E_INVALID_SIP_CLIENT_DATA. C'est un mystère pour moi car invoquer signtool sur le même paquet, avec les mêmes paramètres / certificats fonctionne sans problème donc le certificat doit être entièrement compatible avec le paquet.

quelqu'un a une expérience avec quelque chose comme ça? Est-il un moyen de comprendre quelle est la cause de l'erreur (ce qui est incompatible entre mon cert et le bundle)?

EDIT 1

En réponse à un commentaire:

Le code que j'utilise pour appeler l'Api (prises directement à partir de ladite DONC, la 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;
}

le certificat est un fichier CER (public portion seulement) exporté du jeton et le nom du conteneur est tiré des informations du jeton. Comme je l'ai mentionné, cela fonctionne correctement pour les fichiers EXE.

la commande signtool

signtool sign /sha1 "cert thumbprint" /fd SHA256 /n "subject name" /t "http://timestamp.verisign.com/scripts/timestamp.dll" /debug "$path"

cela fonctionne aussi, quand je l'appelle manuellement ou à partir de la construction CI quand le token est déverrouillé. Mais le code ci-dessus échoue avec l'erreur mentionné.

EDIT 2

merci à vous tous, j'ai maintenant une implémentation qui fonctionne! Je fini à l'aide de la SignerSignEx2 API, comme suggéré par RbMm. Cela semble fonctionner aussi bien pour les paquets d'appx que pour les fichiers PE (différents paramètres pour chacun). Vérifié sur Windows 10 avec un agent de construction TFS 2017-déverrouille le token, trouve un certificat spécifié dans le CERT store, et signe+timestamps le fichier spécifié.

j'ai publié le résultat sur GitHub, si quelqu'un est intéressé: https://github.com/mareklinka/SafeNetTokenSigner

17
demandé sur mlinka 2018-02-15 12:44:14

1 réponses

tout d'abord je regarde d'où CryptUIWizDigitalSign échec: enter image description here

CryptUIWizDigitalSignSignerSignEx fonction pSipData == 0. pour signer PE fichier ( exe, dll,sys) - c'est ok et de travail. mais pour appxbundle (archive zip type de fichier) ce paramètre obligatoire et doit pointer vers APPX_SIP_CLIENT_DATA: appxbundle la pile d'appels est

CryptUIWizDigitalSign 
SignerSignEx
HRESULT Appx::Packaging::AppxSipClientData::Initialize(SIP_SUBJECTINFO* subjectInfo)

au tout début de Appx::Packaging::AppxSipClientData::Initialize nous pouvons voir le code suivant:

if (!subjectInfo->pClientData) return APPX_E_INVALID_SIP_CLIENT_DATA;

c'est exactement là que votre code échoue.

au lieu de CryptUIWizDigitalSign besoin d'un appel direct SignerSignEx2 et pSipData est-paramètre obligatoire dans ce cas.

dans msdn existe plein travaillé exemple - procédure de programmation pour signer un package d'application (C++)

le point clé ici:

APPX_SIP_CLIENT_DATA sipClientData = {};
sipClientData.pSignerParams = &signerParams;
signerParams.pSipData = &sipClientData;

le moderne SignTool appel SignerSignEx2 direct:

enter image description here

ici encore clair visible:

if (!subjectInfo->pClientData) return APPX_E_INVALID_SIP_CLIENT_DATA;

après de ce qu'on appelle

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

enter image description here

ici, à commencer code suivant:

if (!sipClientData->pSignerParams) return APPX_E_INVALID_SIP_CLIENT_DATA;

c'clairement indiqué dans msdn:

vous devez fournir un pointeur vers un APPX_SIP_CLIENT_DATA structure pSipData paramètre lorsque vous signez un paquet app. Vous devez remplir le pSignerParams membre APPX_SIP_CLIENT_DATA avec les mêmes paramètres que vous utilisez pour signer le paquet app. Pour ce faire, définissez vos paramètres désirés sur le SIGNER_SIGN_EX2_PARAMS la structure, d'attribuer l'adresse de cette structure pSignerParams, et puis directement référez les membres de la structure ainsi que lorsque vous appeler SignerSignEx2.

question-Pourquoi besoin de fournir à nouveau les mêmes paramètres, qui sont utilisés dans call SignerSignEx2? parce que appxbundle est vraiment archive, contenant plusieurs fichiers. et chaque dossier doit être signé. pour ce Appx::Packaging::Packaging::SignFilerécursive appel SignerSignEx2 nouveau :

enter image description here

pour ce appels récursifs pSignerParams et utilisé - pour appeler SignerSignEx2 avec exactement les mêmes paramètres que top appeler

6
répondu RbMm 2018-02-21 11:53:14