passwords - tipos - Automatice la firma de código de validación extendida(EV)
ssl purchase form (8)
Recientemente compramos un certificado de firma de código de DigiCert EV. Podemos firmar archivos .exe usando signtool.exe. Sin embargo, cada vez que firmamos un archivo, solicita la contraseña de SafeNet eToken.
¿Cómo podemos automatizar este proceso, sin intervención del usuario, almacenando / almacenando en caché la contraseña en algún lugar?
Ampliando esta respuesta , esto se puede automatizar usando CryptAcquireContext y CryptSetProvParam para ingresar el PIN del token programáticamente y CryptUIWizDigitalSign para realizar la firma mediante programación. Creé una aplicación de consola (código debajo) que toma como entrada el archivo de certificado (exportado haciendo clic derecho en el certificado en SafeNet Authentication Client y seleccionando "Exportar ..."), el nombre del contenedor de clave privada (que se encuentra en SafeNet Authentication Client). el PIN del token, la URL de marca de tiempo y la ruta del archivo para firmar. Esta aplicación de consola funcionó cuando lo llamó el agente de compilación de TeamCity donde estaba conectado el token USB.
Ejemplo de uso:
etokensign.exe c:/CodeSigning.cert CONTAINER PIN http://timestamp.digicert.com C:/program.exe
Código:
#include <windows.h>
#include <cryptuiapi.h>
#include <iostream>
#include <string>
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;
}
En mi caso, Digicert emite un certificado estándar (OV) para el CI, gratis si ya tiene un certificado EV.
Sé que esta no es la solución, pero si no puedes poner el token en el servidor (un servidor en la nube) este es el camino a seguir.
En realidad, en Windows puede especificar la contraseña del token de forma totalmente programática. Esto se puede hacer creando un contexto ( CryptAcquireContext ) con el indicador CRYPT_SILENT usando el nombre de token en la forma "//. / AKS ifdh 0" o el nombre del contenedor de token, que es un guid visible en las propiedades de cerificate en la aplicación de Authentication Client. Luego debe usar CryptSetProvParam con el parámetro PP_SIGNATURE_PIN para especificar su contraseña de token. Después de eso, el proceso puede usar certificados en ese token para firmar archivos.
Nota: una vez que crea el contexto, parece que solo funciona para el proceso actual por completo, sin necesidad de pasarlo a otras funciones de API de Crypto ni a nada. Pero no dude en comentar si encuentra una situación en la que se requieran más esfuerzos.
Editar: ejemplo de código agregado
HCRYPTPROV OpenToken(const std::wstring& TokenName, const std::string& TokenPin)
{
const wchar_t DefProviderName[] = L"eToken Base Cryptographic Provider";
HCRYPTPROV hProv = NULL;
// Token naming can be found in "eToken Software Developer''s Guide"
// Basically you can either use "//./AKS ifdh 0" form
// Or use token''s default container name, which looks like "ab-c0473610-8e6f-4a6a-ae2c-af944d09e01c"
if(!CryptAcquireContextW(&hProv, TokenName.c_str(), DefProviderName, PROV_RSA_FULL, CRYPT_SILENT))
{
DWORD Error = GetLastError();
//TracePrint("CryptAcquireContext for token %ws failed, error 0x%08X/n", TokenName.c_str(), Error);
return NULL;
}
if(!CryptSetProvParam(hProv, PP_SIGNATURE_PIN, (BYTE*)TokenPin.c_str(), 0))
{
DWORD Error = GetLastError();
//TracePrint("Token %ws unlock failed, error 0x%08X/n", TokenName.c_str(), Error);
CryptReleaseContext(hProv, 0);
return NULL;
}
else
{
//TracePrint("Unlocked token %ws/n", TokenName.c_str());
return hProv;
}
}
Me hicieron una herramienta beta que ayudará a automatizar el proceso de construcción.
Es una aplicación de Windows Cliente-Servidor. Puede iniciar el servidor en la computadora donde se insertó el token EV. Ingrese la contraseña para el token en el inicio de la aplicación del lado del servidor. Después de esto, puede firmar archivos de forma remota. La aplicación del lado del cliente reemplaza completamente a signtool.exe para que pueda usar los scripts de compilación existentes.
El código fuente se encuentra aquí: https://github.com/SirAlex/RemoteSignTool
Editar: Utilizamos con éxito esta herramienta para la firma de código el último medio año 24x7 en nuestro servidor de compilación. Todo funciona bien
No hay forma de eludir el diálogo de inicio de sesión AFAIK, pero lo que puede hacer es configurar SafeNet Authentication Client para que solo lo solicite una vez por sesión de inicio de sesión.
Cito el documento SAC (encontrado una vez instalado en /ProgramFiles/SafeNet/Authentication/SAC/SACHelp.chm
, capítulo '' Client Settings
'', '' Enabling Client Logon
de Enabling Client Logon
'') aquí:
Cuando el inicio de sesión único está habilitado, los usuarios pueden acceder a múltiples aplicaciones con solo una solicitud de la Contraseña del token durante cada sesión de la computadora. Esto alivia la necesidad de que el usuario inicie sesión en cada aplicación por separado.
Para habilitar esta función que está deshabilitada de manera predeterminada, vaya a la configuración avanzada de SAC y marque la casilla "habilitar inicio de sesión único":
Reinicie su computadora, y ahora solo debería solicitar la contraseña del token una vez. En nuestro caso, tenemos más de 200 binarios para firmar por cada compilación, por lo que esta es una necesidad total.
De lo contrario, aquí hay un pequeño código de muestra de la consola C # (equivalente a m1st0 uno) que le permite responder automáticamente a los cuadros de diálogo de inicio de sesión (probablemente tenga que ejecutarse como administrador):
static void SatisfyEverySafeNetTokenPasswordRequest(string password)
{
int count = 0;
Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent, AutomationElement.RootElement, TreeScope.Children, (sender, e) =>
{
var element = sender as AutomationElement;
if (element.Current.Name == "Token Logon")
{
WindowPattern pattern = (WindowPattern)element.GetCurrentPattern(WindowPattern.Pattern);
pattern.WaitForInputIdle(10000);
var edit = element.FindFirst(TreeScope.Descendants, new AndCondition(
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit),
new PropertyCondition(AutomationElement.NameProperty, "Token Password:")));
var ok = element.FindFirst(TreeScope.Descendants, new AndCondition(
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Button),
new PropertyCondition(AutomationElement.NameProperty, "OK")));
if (edit != null && ok != null)
{
count++;
ValuePattern vp = (ValuePattern)edit.GetCurrentPattern(ValuePattern.Pattern);
vp.SetValue(password);
Console.WriteLine("SafeNet window (count: " + count + " window(s)) detected. Setting password...");
InvokePattern ip = (InvokePattern)ok.GetCurrentPattern(InvokePattern.Pattern);
ip.Invoke();
}
else
{
Console.WriteLine("SafeNet window detected but not with edit and button...");
}
}
});
do
{
// press Q to quit...
ConsoleKeyInfo k = Console.ReadKey(true);
if (k.Key == ConsoleKey.Q)
break;
}
while (true);
Automation.RemoveAllEventHandlers();
}
Obtuvo una respuesta de Digicert:
Lamentablemente, parte de la seguridad con el Certificado de firma de código EV es que debe ingresar la contraseña cada vez. No hay una forma de automatizarlo.
Utilicé AutoHotKey para automatizar la entrada de la contraseña usando la siguiente secuencia de comandos. Hemos intentado hacer una interfaz web para que nuestros desarrolladores envíen los archivos binarios al cuadro de Windows con este script en ejecución para que pueda ser firmado y devuelto.
Loop
{
Sleep 2000
if (WinExist("Token Logon"))
{
WinActivate ; use the window found above
SendInput [your_password]
SendInput {Enter}
}
if (WinExist("DigiCert Certificate Utility for Windows©"))
{
WinActivate ; use the window found above
SendInput [your_password]
SendInput {Enter}
}
}
Debo señalar que lo que compartí no es completamente seguro, pero también abordamos este problema que requiere la compra de claves de firma para cada desarrollador o la asignación de un trabajo de un gerente de firma que apruebe la firma del software lanzado. Creo que esos son los procesos mejores y más seguros: una vez que las cosas pasan la garantía de calidad y se aprueban para su lanzamiento, pueden firmarse oficialmente. Sin embargo, las necesidades de una empresa más pequeña pueden dictar que esto se haga de alguna otra manera automatizada.
Originalmente utilicé osslsigncode en Linux (antes de los certificados EV) para automatizar la firma de los ejecutables de Windows (ya que teníamos un servidor Linux que hacía mucho trabajo para facilitar el desarrollo y la colaboración). Me he puesto en contacto con el desarrollador de osslsigncode para ver si puede utilizar los tokens de DigiCert SafeNet para ayudar a automatizarlo de otra manera, ya que puedo verlos en Linux. Su respuesta brindó esperanza pero no estoy seguro de ningún progreso y no pude dedicar más tiempo para ayudar
Variante de Python de la herramienta:
import pywintypes
import win32con
import win32gui
import time
DIALOG_CAPTION = ''Token Logon''
DIALOG_CLASS = ''#32770''
PASSWORD_EDIT_ID = 0x3ea
TOKEN_PASSWORD_FILE = ''password.txt''
SLEEP_TIME = 10
def get_token_password():
password = getattr(get_token_password, ''_password'', None)
if password is None:
with open(TOKEN_PASSWORD_FILE, ''r'') as f:
password = get_token_password._password = f.read()
return password
def enumHandler(hwnd, lParam):
if win32gui.IsWindowVisible(hwnd):
if win32gui.GetWindowText(hwnd) == DIALOG_CAPTION and win32gui.GetClassName(hwnd) == DIALOG_CLASS:
print(''Token logon dialog has been detected, trying to enter password...'')
try:
ed_hwnd = win32gui.GetDlgItem(hwnd, PASSWORD_EDIT_ID)
win32gui.SendMessage(ed_hwnd, win32con.WM_SETTEXT, None, get_token_password())
win32gui.PostMessage(ed_hwnd, win32con.WM_KEYDOWN, win32con.VK_RETURN, 0)
print(''Success.'')
except Exception as e:
print(''Fail: {}''.format(str(e)))
return False
return True
def main():
while True:
try:
win32gui.EnumWindows(enumHandler, None)
time.sleep(SLEEP_TIME)
except pywintypes.error as e:
if e.winerror != 0:
raise e
if __name__ == ''__main__'':
print(''Token unlocker has been started...'')
print(''DO NOT CLOSE THE WINDOW!'')
main()
Además, he encontrado que la consola oVirt tiene un comportamiento predeterminado para enviar el bloqueo a Windows. Debe deshabilitarlo en las opciones del servidor y configurar autologin.