Jak se programuje password filter

Ve svém posledním článku jsem psal o použití password filteru pro synchronizaci hesel z domény Microsoft Windows. Dnes se podíváme na to, jak se dá taková věc naprogramovat.

Co budeme potřebovat

Napřed si shrňme, co vlastně password filter bude dělat a jaké komponenty budeme potřebovat k jeho vytvoření.

Password filter je DLL knihovna pro Windows, budeme ho tedy programovat v Microsoft Visual Studiu, v jazyce C++. Dále budeme implementovat komunikaci s webovou službou, pro kterou existují už hotové knihovny. Protože přes webovou službu budeme posílat citlivé údaje, budeme chtít celou komunikaci zabezpečit protokolem SSL, takže použitá knihovna pro webové služby musí SSL podporovat.

Od obecného ke konkrétnímu

Jakou knihovnu pro webové služby použít? Existuje jich celá řada: gSoap, Apache Axis, Smartwin, WSO2 atd.
Od vybrané knihovny požadujeme následující vlastnosti:

  • podpora SSL
  • rozumná licence
  • pokud možno open source projekt
  • samostatnost – nezávislost na dalším softwaru, minimálně v runtime
  • spolehlivost, jednoduché použití

Já jsem pro password filter vybral knihovnu gSoap, která podporuje OpenSLL – osvědčenou open source implementaci SSL.

Jdeme na to

Máme připravené vývojové prostředí, vývojový balíček gSoap a OpenSSL pro Windows a můžeme se pustit do samotného programování. Ve Visual Studiu vytvoříme nový projekt ze šablony Win32 Console Application a v Applicaton Settings zvolíme Applicaton type DLL.

newProject

newProject2

Jak víme z mého minulého článku, operační systém bude od password filteru očekávat tři exportované funkce: InitializeChangeNotify, PasswordFilter a PasswordChangeNotify. Export těchto funkcí zadefinujeme v souboru s koncovkou .def a jeho použití linkerem nastavíme v Project Properties.

LIBRARY PasswordFilterCzechIdM

EXPORTS
	InitializeChangeNotify
	PasswordFilter
	PasswordChangeNotify

defExp2

Nyní se podívejme na zdrojový kód našich funkcí, zatím s prázdnými těly, pouze pro představu jak vypadají hlavičky, vstupní parametry a návratové hodnoty:

#include <atlbase.h>
#include <ntsecapi.h>

BOOL APIENTRY DllMain(HANDLE hModule,
                                          DWORD ul_reason_for_call,
                                          LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
        case DLL_PROCESS_ATTACH:
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
    break;
    }
    return TRUE;
}

BOOLEAN __stdcall InitializeChangeNotify(void)
{
    return TRUE;
}

BOOLEAN __stdcall PasswordFilter(
   PUNICODE_STRING AccountName,
   PUNICODE_STRING FullName,
   PUNICODE_STRING Password,
   BOOLEAN SetOperation
)
{
   return TRUE;
}

NTSTATUS __stdcall PasswordChangeNotify(
   PUNICODE_STRING AccountName,
   ULONG RelativeId,
   PUNICODE_STRING Password
)
{
   return 0;
}

Kromě funkcí, které chceme exportovat, je v kódu ještě standardní funkce DLLMain, která je implicitně volána systémem pokaždé, když se spouští nebo ukončuje proces nebo vlákno. Přítomnost této funkce v DLL knihovně není povinná, ale hodí se například pro konfiguraci proměnných, která je stejná pro všechny exportované funkce.

Klíčové slovo __stdcall u exportovaných funkcí je deklarace volající konvence, která říká překladači, jakým způsobem jsou předávány vstupní a výstupní hodnoty (pořadí argumentů, místo na zásobníku nebo v registrech pro jejich uložení apod.). __stdcall je standardní volající konvence pro Windows API.

PUNICODE_STRING je datový typ pro ukládání znakových řetěců, který používá proces Local Security Authority. Definice tohoto typu je v balíčku Microsoft SDK, který je instalován spolu s Visual Studiem, případně je volně dostupný ke stažení ze stránek Microsoftu.

Vytvoření klienta webové služby

Jak už jsem zmínil na začátku, použijeme k vytvoření klienta webové služby knihovnu gSoap. Součástí vývojového balíčku gSoap jsou utility wsdl2h a soapcpp2, které slouží k vygenerování zdrojových kódů pro klientskou aplikaci na základě WSDL popisovače webové služby.

Předpokládejme, že WSDL popisovač máme uložený v souboru wsdl.wsdl, v pracovním adresáři, ve kterém jsou i spustitelné kompilace wsdl2h a soapcpp2. Zdrojové kódy pro klienta webové služby pak vygenerujeme spuštěním následujících příkazů v příkazové řádce:

wsdl2h.exe -o wsdl.h wsdl.wsdl
soapcpp.exe -IC:\gSoap\gsoap-2.8\gsoap\import wsdl.h

Vygenerované soubory poté zkopírujeme do domovského adresáře našeho projektu, nejlépe do vlastního podadresáře, aby se nepletly se soubory pro samotnou DLL knihovnu a nastavíme cestu k nim v Include Directories v Project Properties. Vygenerované soubory sice obsahují funkce pro volání funkcí webové služby, ale na zprovoznění klienta jsou pochopitelně potřeba i standardní funkce z balíčku gSoap, proto budeme muset do projektu zkopírovat ještě některé soubory z něj. Je sice možné přidat do Include Directories celý balíček gSoap, ale vzhledem k jeho velikosti (160 MB) se vyplatí dát si trochu práce a vybrat z něj pouze potřebné soubory (soapdefs.h, stdsoap2.h, stdsoap2.cpp). Adresář s nimi opět přidáme do Include Directories.

SSL

Nyní do projektu přidáme komponenty pro SSL. Z balíčku OpenSSL zkopírujeme do domovského adresáře projektu celý podadresář include a přidáme ho do Include Directories. Dobrá zpráva je, že tím jsme s includováním zdrojových kódů skončili.

configInc

configInc2

OpenSSL bude dále potřebovat statické knihovny libeay32.lib a ssleay32.lib, které také zkopírujeme do projektu a nastavíme jejich použití v konfiguraci linkeru.

libInc

libInc2

Na tomto místě je třeba dát si pozor na dvě věci. Zaprvé – statické knihovny jsou již přeložené pro určitou platformu (32 nebo 64-bit Windows). Je proto nutné použít ty správné, podle cílové platformy. Zadruhé – funkce ve statických knihovnách OpenSSL jsou přeložené podle volající konvence __cdecl a podle této konvence tedy musíme překládat náš projekt. V opačném případě na nás při buildu čeká hromada chybových hlášení o nenalezených názvech funkcí:

error LNK2019: unresolved external symbol _sk_num@4 referenced in function _tcp_connect@16
error LNK2019: unresolved external symbol _RAND_status@0 referenced in function _soap_ssl_init@0
error LNK2019: unresolved external symbol _RAND_seed@8 referenced in function _soap_ssl_init@0
error LNK2019: unresolved external symbol _RAND_load_file@8 referenced in function _soap_ssl_init@0
...

Defaultní volající konvenci nastavíme v Project Propeties -> C/C++ -> Advanced -> Calling Convention. Nemusíme se bát, že by se podle __cdecl přeložily i naše exportované funkce. Deklarace volající konvence __stdcall v kódu defaultní volající konvenci přepíše a vše tak bude fungovat správně.

callingConv

Dokončení konfigurace

Použití OpenSSL při komunikaci s webovou službou je třeba explicitně uvést v hlavičkovém souboru stdsoap2.h. Dále je třeba uvést používání cookies, které jsou nutné pro udržení session, ve které jsme se přihlásili do CzechIdM. Do souboru stdsoap2.h proto doplníme tyto dva řádky:

#define WITH_OPENSSL
#define WITH_COOKIES

Posledním nastavením v Project Properties bude určení cílové platformy. Toto nastavení se provádí v Linker -> Advanced -> Target Machine.

configPlatform

Nyní jsou všechny komponenty uloženy na patřičných místech, projekt je správně nakonfigurován a můžeme spustit build.

schema

Schéma konstrukce password filteru z jednotlivých komponent

Volání webové služby

Projekt už je sice připraven k buildu, nic ale zatím nedělá. Do těl funkcí je třeba přidat volání webové služby a návratové hodnoty nastavit podle výsledku tohoto volání. Podívejme se, jak vypadá přihlášení do CzechIdM a spuštění workflow pro validaci hesla přes webovou službu:

#include "soapH.h"
#include "WFLauncherSEIBinding.nsmap"
#include "soapC.cpp"
#include "soapClient.cpp"
#include "stdsoap2.cpp"

...
	//
	// INCIALIZACE WS
	//
	struct soap * soap;
	if(!err) {
		soap = soap_new();
		soap_ssl_init(); /* init OpenSSL (just once) */ 
		WriteToLog("Inicializace webove sluzby...");
		if (soap_ssl_client_context(soap,
		   strcmp(sslSkipHostCheck, "true")==0 ? (SOAP_SSL_REQUIRE_SERVER_AUTHENTICATION | SOAP_SSL_SKIP_HOST_CHECK) : SOAP_SSL_REQUIRE_SERVER_AUTHENTICATION,
		   NULL,
		   NULL,
		   cert,
		   NULL,
		   NULL
		))
		{
			WriteToLog("chyba pri inicializaci webove sluzby");
			err = TRUE;
		}
		else {
			WriteToLog("Webova sluzba inicializovana.");
		}
	}

	//
	// LOGIN
	//

	ns1__login * argumenty = new ns1__login();
	argumenty->arg0 = new string(loginName);
	argumenty->arg1 = new string(loginPassword);

	ns1__loginResponse * response = new ns1__loginResponse();
	response->return_ = false;

	if(!err) {
		WriteToLog("zkousim login k IdM...");
		if(soap_call___ns1__login(soap, url, NULL, argumenty, response) == SOAP_OK) {
			if(response->return_) {
				WriteToLog("login OK");		
			}
			else {
				WriteToLog("prihlaseni se nezdarilo");
				err = TRUE;
			}
		}
		else {
			WriteToLog("IdM nereaguje");
			err = TRUE;
		}
	}
	
        // 
	// TEST HESLA
	// 

	ns1__launchWorkflowSynchronouslyStringVars * argumenty15 = new ns1__launchWorkflowSynchronouslyStringVars();

...

	ns1__launchWorkflowSynchronouslyStringVarsResponse * response15 = new ns1__launchWorkflowSynchronouslyStringVarsResponse();

	if(!err) {
		if(soap_call___ns1__launchWorkflowSynchronouslyStringVars(soap, url, NULL, argumenty15, response15) == SOAP_OK) {
			WriteToLog(response15->return_->c_str());
			result = (strcmp(response15->return_->c_str(), "true")==0);
		}
		else {
			WriteToLog("nepovedlo se zavolat webovou sluzbu");	
			soap_print_fault(soap, stderr);
			err = TRUE;
		}	
	}

...

Jak jste si možná všimli, funkce pro volání webové služby mají stejný název jako samotné funkce WS, pouze s prefixem „soap_call___ns1__“. Obdobně je to s kontejnery vstupních a výstupních hodnot. K těm gSoap přidává prefix „ns1__“. Toto je pouze útržek kódu, který vykonává samotné volání webové služby. Tomu předchází inicializace proměnných a následuje zpracování výstupu, což jsou věci, které zde nebudu hlouběji rozebírat.

Několik tipů

Všechny zásadní kroky pro vytvoření password filtru jsem již popsal. Ale ještě než skončím, přidám několik zajímavostí k tomuto tématu, které mohou programátorům ušetřit čas a práci.

  • Local Security Authority přidává za uživatelské jméno, které posílá password filteru, jeden unicodový znak s kódem 01. Nejde o standardní ukončovací nulu. S touto úpravou je třeba počítat při práci s tímto parametrem. Heslo tímto způsobem upravené není.
  • gSoap definuje některé datové typy se stejnými jmény, jako Microsoft SDK, což při současném includování obou zdrojových kódů způsobuje redefinition error. Je to nepříjemná situace, kterou je možné řešit vypuštěním kolidujích zdrojových souborů z Microsoft SDK a dodefinováním potřebných datových typů samostatně v projektu.
  • Testování hotového password filtru skutečnou instalací a měněním hesel na Windows je velmi pomalé a neohrabané. Proto doporučuji vytvořit si pro testování speciální program, který bude umět password filter inicializovat a volat jeho exportované funkce se zadanými parametry.

Závěr

V dnešním článku jsem popsal, jak lze naprogramovat password filter pro synchronizaci hesel, který jsem představil minule. Zaměřil jsem se zejména na dva nejzajímavější problémy: integraci jednotlivých komponent v projektu a volání webové služby z C++. Naopak opomenuty zůstaly standardní programátorské záležitosti, jako je načítání konfigurace z konfiguračního souboru, logování, převody mezi formáty znakových řetězců, či detailní sestavování parametrů před voláním webové služby. Tyto záležitosti nesouvisí přímo s předmětem článku, který je už tak dost vyčerpávající, a nechtěl jsem ho jimi ještě více prodlužovat.

Pokud máte k programování password filtru jakékoliv otázky nebo zlepšující návrhy, napište mi na jakub.tomek@bcvsolutions.eu. Rád je s Vámi proberu.