winapi - que - Firmando un appxbundle usando la API CryptUIWizDigitalSign
signtool que es (1)
Me enfrento a un problema bastante interesante en lo que respecta a Authenticode que firma un archivo UWP appxbundle.
Algunos antecedentes: el cliente nos proporcionó un token USB de SafeNet que contiene el certificado de firma. La clave privada no es exportable, por supuesto. Quiero poder utilizar este certificado para nuestras versiones de lanzamiento automatizadas para firmar el paquete. Desafortunadamente, el token requiere que se ingrese un PIN una vez por sesión, por ejemplo, si el agente de compilación se reinicia, la compilación fallará. Hemos habilitado el inicio de sesión único en el token, por lo que es suficiente desbloquearlo una vez por sesión.
Estado actual: podemos usar signtool en appxbundle sin ningún problema, dado que el token se ha desbloqueado. Esto funciona bastante bien, pero se interrumpe tan pronto como se reinicia la máquina o se bloquea la estación de trabajo.
Después de algunas búsquedas logré encontrar this pedazo de código. Esto toma los parámetros de firma (incluido el PIN del token) e invoca la API de Windows para firmar el archivo de destino. Logré compilar esto y funcionó perfectamente para firmar el contenedor de instalación (archivo EXE). El token no solicitó el PIN y fue desbloqueado automáticamente por la llamada a la API.
Sin embargo, cuando invocé el mismo código en el archivo appxbundle, la llamada a CryptUIWizDigitalSign
falló con el código de error 0x80080209 APPX_E_INVALID_SIP_CLIENT_DATA
. Esto es un misterio para mí porque invocar signtool en el mismo paquete, con los mismos parámetros / certificado funciona sin problemas, por lo que el certificado debería ser totalmente compatible con el paquete.
¿Alguien tiene experiencia con algo como esto? ¿Hay alguna manera de averiguar cuál es la causa raíz del error (qué es incompatible entre mi certificado y el paquete)?
EDITAR 1
En respuesta a un comentario:
El código que estoy usando para llamar a las API (tomado directamente de la pregunta SO mencionada anteriormente)
#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;
}
El certificado es un archivo CER (solo parte pública) exportado desde el token y el nombre del contenedor se toma de la información del token. Como mencioné, esto funciona correctamente para archivos EXE.
El comando signtool
signtool sign /sha1 "cert thumbprint" /fd SHA256 /n "subject name" /t "http://timestamp.verisign.com/scripts/timestamp.dll" /debug "$path"
Esto también funciona, cuando lo llamo manualmente o desde la compilación de CI cuando el token está desbloqueado. Pero el código anterior falla con el error mencionado.
Editar 2
Gracias a todos ustedes, ahora tengo una implementación de trabajo! Terminé usando la API SignerSignEx2
, como lo sugiere RbMm. Esto parece funcionar bien tanto para los paquetes appx como para los archivos PE (diferentes parámetros para cada uno). Verificado en Windows 10 con un agente de compilación TFS 2017: desbloquea el token, encuentra un certificado específico en el almacén de certificados y firma y marca la hora del archivo especificado.
Publiqué el resultado en GitHub, si alguien está interesado: https://github.com/mareklinka/SafeNetTokenSigner
En primer lugar miro donde falló CryptUIWizDigitalSign
:
CryptUIWizDigitalSign
llama la función SignerSignEx
, con pSipData == 0
. para firmar el archivo PE ( exe , dll , sys ) - esto está bien y funcionará. pero para appxbundle (tipo de archivo de archivo zip) este parámetro es obligatorio y debe apuntar a APPX_SIP_CLIENT_DATA
: para appxbundle call stack es
CryptUIWizDigitalSign
SignerSignEx
HRESULT Appx::Packaging::AppxSipClientData::Initialize(SIP_SUBJECTINFO* subjectInfo)
al comienzo de Appx::Packaging::AppxSipClientData::Initialize
podemos ver el siguiente código:
if (!subjectInfo->pClientData) return APPX_E_INVALID_SIP_CLIENT_DATA;
Aquí es exactamente donde su código falla.
En lugar de CryptUIWizDigitalSign
necesita la llamada directa SignerSignEx2
y pSipData
es un parámetro obligatorio en este caso.
en msdn existe un ejemplo completo: cómo firmar mediante programación un paquete de aplicaciones (C ++)
El punto clave aquí:
APPX_SIP_CLIENT_DATA sipClientData = {};
sipClientData.pSignerParams = &signerParams;
signerParams.pSipData = &sipClientData;
El moderno here llama SignerSignEx2
directo:
Aquí nuevamente clara visible:
if (!subjectInfo->pClientData) return APPX_E_INVALID_SIP_CLIENT_DATA;
después de esto llamado
HRESULT Appx::Packaging::Packaging::SignFile(
PCWSTR FileName, APPX_SIP_CLIENT_DATA* sipClientData)
Aquí en el siguiente código comienza:
if (!sipClientData->pSignerParams) return APPX_E_INVALID_SIP_CLIENT_DATA;
este claro lo indica en msdn :
Debe proporcionar un puntero a una estructura APPX_SIP_CLIENT_DATA como el parámetro pSipData cuando firme un paquete de aplicación. Debe llenar el miembro pSignerParams de APPX_SIP_CLIENT_DATA con los mismos parámetros que utiliza para firmar el paquete de la aplicación. Para hacer esto, defina los parámetros deseados en la estructura SIGNER_SIGN_EX2_PARAMS , asigne la dirección de esta estructura a pSignerParams y luego haga referencia directamente a los miembros de la estructura cuando llame a SignerSignEx2 .
pregunta: ¿por qué es necesario volver a proporcionar los mismos parámetros que se utilizaron en la llamada SignerSignEx2
? porque appxbundle
es realmente archivo, que contiene múltiples archivos. y cada archivo necesita ser firmado. para este Appx::Packaging::Packaging::SignFile
recursive call nuevamente SignerSignEx2
:
para estas llamadas recursivas pSignerParams
y se usa - para la llamada SignerSignEx2
con exactamente los mismos parámetros que la llamada principal