Bevezető

A project célja egy kisebb PKI infrastruktúra felépítése volt. Olyan felhasználóazonosítás volt a lényege, ahol az emberek titkos kulccsal tudták bizonyítani a hozzáférési jogukat. Az ilyesféle kulcsokról köztudott, hogy általában sokkal biztonságosabbak a pusztán jelszó alapú azonosításnál. És ha már vannak kulcsok, használjuk ki a többi lehetőséget is: nehezen hamisítható naplózás is van a keretrendszerben. Ha valaki telepíteni szeretné, akkor szükség van Apache-SSL-re, PHP-ra, PHP OpenSSL modulra, OpenSSL parancssori programra és PostgreSQL-re. Ha minden sikerült, akkor bejelentkezéskor egy ilyen kép fog fogadni minket:

Módszerek, problémák

Sajnos nem sikerült szépen megtervezett CA-rendszert összehozni. Az volt a tervem, hogy létrehozok egy képzeletbeli root CA-t, és egy másodlagos CA-t, ami a project CA-ja lett volna. A root-tal aláírom az Apache és a project tanúsítványát is, a kiadott tanúsítványokat pedig a project CA írta volna alá, és minden exportált kulccsal együtt a root és project CA tanúsítványokat is odaadtam volna a kliensnek. Kétféle eredményt értem el: MSIE szépen látta a tanúsítványláncot, viszont nem tudott autentikálni a szerverrel, vagy MSIE már a tanúsítványláncot is érvénytelennek találta. A probléma ott lehet, hogy rosszul állítottam be a root és project tanúsítvány felhasználási céljait, ez már egy hosszabb kutatást igényelt volna. A tesztüzembnen tehát az Apachenak önmaga által aláírt tanúsítványa volt, a rendszer pedig egyetlen CA-n alapul, ami az előző terv szerinti project CA, ez szintén önmaga által van aláírva.

Voltak gondok az alkalmazott szoftverekkel. A PHP OpenSSL interfésze nem támogatja a PKCS#12 formátumot, amivel MSIE-be és más böngészőkbe lehetne exportálni a kulcsot, egyéb megfelelő formátumot nem találtam. Írtam a PHP fejlesztőinek, ha lesz idejük, megcsinálják. Így jelenleg egy csúnya hacket alkalmazok, fájlba exportálom a kulcsokat és a parancssori openssl-el készítem el a kulcsot. Jó esetben már csak az exportált, jelszóval védett kulcsnak kellene kikerülnie a fájlrendszerre.

A legnagyobb kihívas az volt, hogy ne lehessen hamisítani a naplót. Ezt teljesen biztosra lehetetlen megcsinálni, de szerintem egy elfogadható, mégis csúnya dologgal sikerült biztonságot elérni. A napló hamisításához az kell, hogy valaki rendelkezzen a felhasználó titkos kulcsával. Az egész projectnek a gyengesége, hogy a titkos kulcsokat muszáj a szerveren tárolni, különben nem tudnánk használni őket. Ha csak a kliens rendelkezne a kulccsal, akkor szükség volna kliens oldali programokra, és így már az egész böngészős-webszerveres dolog elveszítené az értelmét, mert akkor már az autentikációhoz se volna szükség ezekre, azt is megcsinálná a program. Szóval a kényelem ára, hogy a szerveren vannak a titkos kulcsok.

A kulcsot egyértelműen jelszóval kellett védeni. Ha ez megegyezik a felhasználó jelszavával, akkor a jelszó elfelejtése esetén búcsút mondhatunk az adott embernek. Külön jelszó használata már túl kényelmetlen lenne, ha minden kulcs- használat alkalmával bekérnénk a jelszót. Erre lett volna egy nagyon szép megoldás, hogy a felhasználói tanúsítványnak generálunk egy jó sok bites sorozatszámot, evvel titkosítjuk a titkos kulcsot. Ez a módszer egészen biztonságos es kényelmes, $_SERVER változókból simán ki lehet szedni a használt tanúsítvány sorozatszámát (ahogy a többi adatot is, így el tudtam kerülni a felhasználónév bekérését). Itt is egy nagy falba utkoztem, PHP-ban maximum 32 bites számot lehet használni, holott már láttam 128 bites sorozatszámot, és PHP támogatja a nagy számokat. Ezt is bejelentettem a fejlesztőknek.

Normális sorozatszám híján a tanúsítvány organizationalUnitName változójába egy hosszú véletlenszerű stringet teszek új felhasználó létrehozásakor. A dolog működik, ez a string csak a júzernél létezik, és bejelentezéskor PHP ezt le tudja kérdezni. Megemlítem, hogy itt is találtam bugot. Az első verzió szerint BASE64 karakterkészletű volt a string, amiben van / jel, és a tanúsítványok tárgyaban az egyes változóknál is ez a szeparátor, Apache pedig rosszul értelmezte a tanúsítványt és haszálni sem lehetett PHP-ból, ezt a hibát is reportáltam, és azóta nem használok /-t. Tehát ha valaki hamisítani akar, ki kell találnia ezt a stringet, ami nem egyszerű dolog. Az admin esetleg megteheti, hogy kicseréli a kulcsot, akkor működni fog a csalás, de csak addig, amíg be nem jelentkezik a felhasználó, mert akkor rögtön észreveszi, hogy az ő kulcsa nem nyitja a titkos kulcsot.

Egy fontos hianyosság, hogy nem lehet felhasználókat törölni. Ennek az az oka, hogy ha már egyszer adtunk a felhasználónak kulcsot, akkor azt tőle nem tudjuk letörölni. Lehetne manipulálni a kulcsok lejárati időpontjával, és a lejárat után engedélyezni a törlést, amikor már úgysem tud belépni. Itt az az újabb probléma, hogy a többi felhasználónak is egyszer lejárnak a kulcsai, és az átmeneti időszakban, amikor még régi és új kulcsok is forgalomban vannak, sok újabb problémákkal kerülnénk szembe. Erre egy jó megoldás, hogy ha minden évben január elsején lép életbe új tanúsítvány a szerveren, az exportált kulcsokban pedig a felhasználó neve után irjuk, hogy az melyik évre érvényes, pl. admin-2004, így a felhasználó is könnyen eldönthetnő, hogy melyik kulccsal tud belépni.

Telepítés

A telepítés úgy működik, hogy először HTTP kapcsolaton elindítjuk az init.php-t. A biztonság kedvéért ez nem nyúl az adatbázishoz, meg semmi máshoz, csak elkészíti nekünk azokat a fájlokat, amikre szükség van az induláshoz. Létrehozza a szükséges könyvtárakat is, és ha ezek léteznek, akkor már nem fut le újból, de akár le is lehet törölni. Létrehozza az admin nevű felhasználót, generál neki kulcsot, generál egy SQL scriptet, ami felépíti az egész adatbázist és az admin júzer adatait is beleteszi, valamint létrehozza a project tanúsitványát. Az admin.p12 fájlt kell a böngészőbe importálni, avval a jelszóval van védve, amivel regisztráltunk is. Az initdb.sql-t be kell adagolni psql-nek, a tanúsítványt pedig Apache-ban beállítani. A PostgreSQL felhasználói adatokat base.php-ban tudjuk beállítani a pg_connect sorában. Alapkövetelmény egy már működő Apache-ssl, teljesen jól beállítva. Az autentikációhoz ennyire van szükség az adott -ban:

SSLRequireSSL
SSLCACertificateFile /var/www/pki/keys/ca.crt
SSLVerifyClient 2
SSLVerifyDepth 10

A dokumentáció szerint ezt lehet -nel is korlátozni, gyakorlatilag meg nem sikerült. Apache restart, és máris kész a rendszer. PHP-ban kapcsoljuk ki register_globals-t! És persze Apache-ban engedélyezni kell a .htaccess használatát, hogy ne tudják letölteni a kulcsokat. A rendszer addig nem működik, amíg létezik a setup könyvtár.

Működés kicsit részletesebben

A ki-belépés egy teljesen alap, általam írt session módszerrel van megoldva. A timeout fél óra, de ha a felhasználó éppen POST-ol, mikor lejárt a session, a jelszó beírása után a kért adatok feldolgozásra kerülnek, nem vesznek el. Minden egyes oldalletöltéskor ellenőrizzük, hogy a tanúsítvány megfelelő-e. Naplóba íráskor hibát adunk, ha üres a napló, vagy nem stimmel az aláírás. Az adminisztrátor le tudja ellenőrizni mindenkinek a naplóját, és a feladatnak megfelelően letilthat embereket. Fentebb írtak alapján hibát adunk, ha a titkos kulcson megváltozott a jelszó, és külön akkor is, ha nincsen jelszó, mert akkor egyébkent bármilyen jelszóval hozzá lehet férni. Ha a felhasználó használja a kilépés funkciót, akkor így garantált, hogy amíg be volt lépve, senki sem nyúlt hozzá a dolgaihoz, mert a kilépés naplózása közben észrevennénk a trükközést.

Az adatbázisban a publikus kulcsot tanúsítvány formájában tároljuk, sajnos PHP-ban nincs más mód erre, a titkos kulcsot viszont egyszerűen tudjuk stringbe exportálni. Mindkettő BASE64 kódolású. A naplóban minden sor egy bejegyzés, egy bejegyzés egy Unix időből és egy stringből áll. Az OpenSSL aláíró eljárás sajnos csak bináris adatot ad kimenetként, ezt nekünk kell BASE64-be kódólni, és vissza, ha ellenőrzünk. Kétszer írtam, hogy sajnos, ezeket a dolgokat szintén tudattam a fejlesztőkkel. A naplót azért nem soronként tároljuk, mert akkor túl bonyolult lenne ellenőrizni a konzisztenciát, így viszont teljesen egyszerű és kézenfekvő a dolog.

A felhasználóknak generált kulcsokat a fájlrendszeren tárolom. A feladat úgy volt értelmes, hogy az adatbázis admin és a fájlok tulajdonosa a rendszeren nem egy személy. Ugyanis aki írni tudja a php fájlokat, az onnan kezdve nyugodtan sniffelheti magának a jelszavakat, meg bármit, szóval nem veszítünk semmit, ha ez az illető meg tudja nézni a p12 fájlokat. Bár az sem könnyű neki, mivel jelszóval védettek, de ez nem túl komoly védelem. Az adatbázis admin pedig így nem tudja megszerezni a tanúsítványokat. Szerintem így egész jól szét sikerült választani kétfelé a dolgokat, az adatbázisba már csak jól védett adatok kerülnek. Ha bízunk benne, hogy a felhasználó nem veszíti el a kulcsát, akkor esetleges feltörések esetén nem árt, ha a már kiadott kulcsokat letöröljük, hiszen nekünk sincs már szükségünk rá. Persze hangsúlyozni kell a júzereknek, hogy a p12-bol csak egy példány van, tartsa floppyn, nyomtassa ki vagy bármi.

A keretrendszer

A rendszer nagyon könnyen bővíthető. Egy új funkciót a következőképpen hozunk létre. A fájl elején minden POST actionnek kell egy feldolgozó függvény, function post_action() formában, ahol action-t értelem szerint helyettesítjük. Ennek visszatérési értéke az eredmény, szöveg formában. Ezek után kell egy require("base.php"), majd require("menu.php"). Ha akarunk valamit csinálni, még mielőtt bármi output törtenik (pl. keys.php), azt a két require között tehetjük meg. Ezek után pedig azt írunk ki, amit akarunk. Tegyük bele a lapot menu.php-be, és készen is vagyunk.

A formok feldolgozását úgy szeretem, hogy lehessen refreshelni a POST újraküldése nélkül, ezért a feldolgozás után kapásból továbbküldjük a júzert oda, ahonnan jött, egy result parametérrel együtt, amit base.php fog kiírni a menü alá. Így nem kell hibakezelő függvényeket írni, nem nekünk kell eldönteni, hogy melyik functiont kell végrehajtani, nem kell foglalkozni az eredmény kiírásával, stb. Nem szabad loginpass és loginaction nevű mezőt használni, mert ezt az esetlegesen lejáró session esetén a belépéskor használjuk.

A következő változókat használhatjuk:
- felhasználó adatai: $usr[mezo], mezők: usrid, login, name, pass, email, privkey, pubkey, keypass
- $admin: a felhasználó adminisztrátor-e
- query(sql) függvény
- naplo(szoveg) függvény
- más publikus kulcsának lekérdezése: getpubkey(usrid) (hasznos, ha például üzenetet akarunk neki küldeni)

Itt a vége

Ezt az egészet összehozta:
Fejes József (Joco)
SCH 1804
joco@jocohp.hu