Java (23) - omezování práv II.

Po minulém teoretickém úvodu do problematiky zabezpečení přejdeme k praxi - tedy k nastavování bezpečnostní politiky a práci se zavaděči tříd. Stranou nezůstane ani podepisování kódu.

14.3.2006 06:00 | Lukáš Jelínek | přečteno 17399×

Zavaděče tříd

Při vytváření instancí objektů (a samozřejmě při používání statických metod třídy) musí být příslušná třída přítomna v JVM - musí být zavedena. Jak jsme si již dříve řekli, nezavede se sama od sebe, udělá to tzv. zavaděč tříd (classloader), ať už implicitně nebo explicitně.

Zavaděčů tříd můžeme mít v systému libovolný počet (a používat je podle potřeby), jeden z nich má však výsadní postavení. Je to tzv. prvotní zavaděč, sloužící k zavedení základních systémových tříd. Nemá svoji javovskou reprezentaci, jedná se o nativní kód a jeho chování je platformově závislé.

Zavádění tříd pracuje na bázi delegování - jeden zavaděč tedy může nechat zavést nějakou třídu jiný zavaděč (nacházející se výše v hierarchii). Takto se může zavedení třídy postupně přenést až k prvotnímu zavaděči.

Každá situace má svůj výchozí zavaděč. Pro aplikace je to URLClassLoader, pro applety AppletClassLoader (je potomkem URLClassLoader, ale jedná se o soukromou třídu v balíku sun.applet, takže nelze běžně používat) a při zavedení kvůli odkazu z již zavedené třídy se použije ten zavaděč, kterým se zavedla příslušná třída.

Jak zavádět

Přestože si můžeme vytvořit svůj vlastní zavaděč a používat ho, v drtivé většině si vystačíme s tím, co už je ve standardní knihovně - tedy se třídou URLClassLoader. Tento zavaděč má totiž vše potřebné k tomu, abychom mohli zavést neznámou třídu tak, aby nemohla udělat v systému paseku.

Potřebujeme-li při zavádění použít jiný zavaděč, jednoduše ho specifikujeme v metodě Class.forName(). Při tomto volání lze rovněž určit, zda se má třída hned inicializovat (což mj. znamená, že se provede kód v jejím statickém inicializátoru) - pokud by inicializována nebyla, došlo by k tomu až při požadavku na vytvoření první instance. Příklad uvedu později.

Existuje ještě jiná, nízkoúrovňovější cesta - zavolání metody loadClass() příslušného zavaděče. Zde už se ale nepracuje s běžnými názvy tříd, nýbrž s tzv. binárními názvy, které už známe odjinud. Stačí se podívat, jaké soubory kompilátor vytvoří pro vnořené třídy (ať už pojmenované nebo anonymní). Právě tyto názvy souborů odpovídají binárním názvům tříd, samozřejmě bez specifikace balíku (která je tvořena adresářovou hierarchií).

Bezpečné zavádění

Použijeme-li k zavedení třídy zavaděč URLClassLoader, můžeme (pomocí nastavení bezpečnostní politiky) určit, jaká opravnění budou mít třídy zavedené z určitého zdroje. Lze tak definovat důvěryhodné zdroje a ty, které naši důvěru nemají - nezáleží na tom, zda se jedná o lokální zdroje (adresář, JAR soubor) nebo vzdálené. Jak již všichni vědí z minulé kapitoly, zdroj je představován třídou CodeSource a kromě URL může obsahovat i certifikáty.

Nastavení bezpečnostní politiky je klíčová záležitost. Jsou-li aktivní bezpečnostní omezení, kód smí provádět jen ty operace, které mu bezpečnostní politika povolí - výjimkou je pouze čtení souborů ze zdroje, odkud byl kód získán. Než se k nastavování politiky dostaneme, podívejme se nejprve na příklad, jak se takové bezpečné zavádění tříd provádí:

try {
    URL urls[] = { new URL("http://www.linuxsoft.cz/") };
    URLClassLoader ucl = URLClassLoader.newInstance(urls);
    Class c = Class.forName("MyClass", true, ucl);
    ...
} catch (Exception e) {
    ...
}

Pokud byste chtěli uvedený příklad vyzkoušet, nahraďte prosím URL nějakou vhodnější hodnotou (nejlépe místem, kam uložíte své třídy), aby nebyl server Linuxsoftu zbytečně "olizován" vaším classloaderem.

Nastavování bezpečnostní politiky

Bezpečnostní politika je v Javě představována abstraktní třídou java.security.Policy. Instancí této třídy může být libovolný počet, aktivní je však vždy pouze jediná. Nastavení lze za běhu aplikace změnit, ale samozřejmě jen tehdy, má-li k tomu kód potřebné oprávnění. Objekt má svoje úložiště nastavení (konfigurace), např. v souboru na disku - změnami v tomto nastavení měníme bezpečnostní politiku. Pokud ke změně dojde za běhu aplikace, změny se projeví až v okamžiku, kdy je nastavení aktualizováno metodou refresh().

Výchozí nastavení politiky je uloženo v souborech, jejichž umístění se určuje v konfiguraci bezpečnosti Javy (soubor java.security v adresáři /lib/security pod instalačním adresářem Javy). Typicky se používají dva soubory - jeden pro úroveň systému (java.policy v témže adresáři), a druhý pro uživatelská nastavení (.java.policy v domovském adresáři). V konfiguračním souboru java.security lze nastavit ještě mnoho dalších věcí - doporučuji do něj nahlédnout, jsou tam velice srozumitelné komentáře k jednotlivým položkám.

Object Policy funguje tak, že pomocí dvou metod getPermissions() vrací kolekce povolení, které tato politika obsahuje. Jedna z těchto metod poskytuje globální povolení pro určitou doménu (ProtectionDomain), druhá pro zdroj kódu (CodeSource). Dále je tu ještě metoda implies(), která zjišťuje, zda tato politika poskytuje určité povolení pro danou bezpečnostní doménu.

Soubor bezpečnostní politiky

I když politiku lze uložit obecně jakkoliv, nejčastěji se používá soubor, proto se na něj nyní podíváme. Platí zde zásadní pravidlo, že implicitně není povoleno nic a my tedy musíme zvolit, co všechno povolíme. Syntaxe souboru je poněkud složitější, podíváme se tedy jen na základní věci - zájemci o hlubší proniknutí do této problematiky nechť nahlédnou do bezpečnostní příručky Javy.

První příklad ukáže, jak povolit úplně všechno všem. Důrazně varuji před aplikací v praxi, jen chci ukázat, jak by se to udělalo:

grant {
  permission java.security.AllPermission;
};

Třída AllPermission, jak známo, implikuje všechna povolení, proto vložení tohoto pravidla povolí všechny operace. Protože není uvedeno, koho se to týká, uplatní se pravidlo globálně. Tohle je ale pravým opakem stavu, který chceme dosáhnout - tedy aby měl kód méně práv. Podívejme se tedy na další příklad:

grant codeBase "file:/home/user/trusted/*" {
  permission java.security.AllPermission;
};

grant {
  permission java.io.FilePermission "read","/-";
  permission java.io.FilePermission "read,write","/tmp/*";
};

Těmito dvěma pravidly říkáme, že kód pocházející z adresáře /home/user/trusted má povoleno všechno, kdežto všechen ostatní kód smí číst v celém filesystému a zapisovat pouze do adresáře /tmp. Nemusí se jednat o lokální kód - můžeme specifikovat i pravidla pro kód odjinud. Podobně i parametry nemusíme psát "natvrdo", ale lze používat vlastnosti systému:

grant codeBase "http://www.linuxsoft.cz/*" {
  permission java.io.FilePermission "read,write,execute,delete",\
          "${user.home}/-";
  permission java.lang.RuntimePermission "queuePrintJob";
};

Uvedené pravidlo se vztahuje na kód pocházející ze serveru Linuxsoft. Zajišťuje povolení provádět veškeré souborové operace v domovském adresáři a jeho podadresářích, a povolení poslat úlohu do tiskové fronty (může to vypadat jako malichernost, ale komu nějaký vtipálek nechá vyplýtvat drahou inkoustovou náplň, nebude považovat takové omezení za zbytečnost).

Uvedená pravidla mají jedno společné - rozlišují kód jen podle zdroje, a již nehodnotí jeho vlastní důvěryhodnost. To není příliš bezpečné, ale vrátíme se k tomu až poté, co zběžně projdeme problematikou podepisování kódu.

Aktivace bezpečnostních omezení

Všechna bezpečnostní pravidla jsou sice hezká, ale zatím se nemohla nijak projevit. Nemáme je totiž aktivována. Pokud je chceme uplatnit, je potřeba spouštět Javu trochu jinak, než jak jsme byli zvyklí. Vypadá to zhruba takto:

java -Djava.security.manager Aplikace

Toto spuštění zavede výchozího security managera, a aktivuje tím i definovanou bezpečnostní politiku. Můžete si to vyzkoušet na některé běžné aplikaci - uvidíte, co všechno nebude fungovat (kvůli tomu, že množina hlídaných operací je opravdu velká). Kromě výchozích souborů pro politiku můžete nějaký specifikovat i při spuštění. Pravidla v něm se připojí k těm definovaným ve výchozích souborech (můžeme tak udělit nějaké aplikaci větší práva), anebo je úplně nahradí. Třeba takto:

java -Djava.security.manager -Djava.security.policy=./java.security Aplikace
java -Djava.security.manager -Djava.security.policy==./java.security Aplikace

Oba příkazy se liší pouze tím, že v prvním případě se politika přidává, kdežto ve druhém nahrazuje.

Podepisování kódu

Zejména u kódu stahovaného ze vzdáleného serveru je vždy problém s tím, že nám může někdo podvrhnout nebezpečný kód, a to i v případech, kdy server sám patří důvěryhodné osobě. Může být však napaden útočníkem, může být též obětí pharmingu (podvržení DNS odpovědi), čímž se snadno do systému zavleče lecjaká havěť. Proto je žádoucí, aby byla bezpečnost zajištěna lepšími mechanismy - kam patří i podepisování kódu.

Když je kód podepsaný a máme veřejný certifikát pro kontrolu podpisu, lze snadno zjistit, zda je podpis platný a zda do kódu někdo nezasahoval. Základní vlastnosti jsou tu stejné jako u jiného použití elektronického podpisu, zaměřme se tedy na to, jak to použít při zajištění bezpečného prostředí. V Javě je tato oblast dost široce pojata, ale nám teď stačí jen malá podmnožina.

Již dříve jsem se zmínil o tom, že object CodeSource obsahuje nejen samotný zdroj, ale i certifikáty. Má to ten význam, že si v souboru konfigurace politiky nastavíme, že určitá povolení se budou poskytovat pouze kódu podepsanému určitou osobou. Viz příklad:

grant signedBy "TrustedPerson" {
  permission java.security.AllPermission;
};

grant codeBase "file:/-" signedby "osoba1,osoba2,osoba3" {
  permission java.io.FilePermission "read,write","/-";
};

První pravidlo říká, že kód podepsaný osobou (přesněji řečeno aliasem) nazvanou TrustedPerson bude mít povoleno úplně všechno. Druhé potom, že kód podepsaný uvedenými osobami, který pochází z místního filesystému, bude mít plný přístup k souborům.

Manipulace s certifikáty

Nyní samozřejmě vyvstává otázka, kde vezmeme certifikáty pro ověření podpisu. Java používá tzv. úložiště klíčů a certifikátů (keystore), kam se tato data ukládají - umístění je určeno v bezpečnostním konfiguračním souboru. Certifikáty samozřejmě musíme získat bezpečnou cestou - to se netýká těch, které jsou podepsány důvěryhodnou certifikační autoritou, jejíž certifikát máme k dispozici.

S certifikáty a klíči lze manipulovat dvěma způsoby. Prvním je konzolový program keytool. Tím lze provádět nejrůznější operace nad úložištěm - importovat a exportovat certifikáty a klíče, vytvářet páry klíč-certifikát, odstraňovat položky z úložiště apod. Zde je několik příkladů:

keytool -import -alias TrustedPerson -file TrustedPerson.cer
keytool -export -alias osoba -file osoba.cer
keytool -list
keytool -selfcert -alias lukas -keypass nejakeheslo\
      -dname "cn=Lukas Jelinek, ou=, o=Linuxsoft, c=CZ"
      

První příkaz importuje uvedený certifikát do úložiště (pokud úložiště neexistuje, vytvoří ho) a přiřadí mu alias. Druhý řádek naopak certifikát exportuje. Třetí potom vypíše informace o všech uložených certifikátech. A konečně poslední vytváří certifikát podepsaný sám sebou - použije k tomu klíč uložený pod uvedeným aliasem.

Druhou možností je použít grafický program policytool. Je už poměrně obstarožní, ale pro jednoduchou správu úložiště, a také pro práci s pravidly bezpečnostní politiky ho lze s výhodou použít.

Program jarsigner

Poslední věcí, na kterou se ještě podíváme, je podepisování kódu. Pro distribuci takového kódu je samozřejmě výhodné, máme-li klíč/certifikát podepsané důvěryhodnou certifikační autoritou, ale k podepisování jako takovému to není třeba. Použijeme k tomu program jarsigner, který je opět standardní součástí balíku JDK. Lze podepisovat dokonce i přímo v aplikaci, ale to je poněkud složitější činnost.

jarsigner lze použít jak k podepsání JAR archivu, tak ke kontrole podpisu, máme-li příslušný certifikát. Zde jsou dva příklady:

jarsigner -storepass heslo_uloziste -keypass heslo_klice balik.jar lukas
jarsigner -verify balik.jar

První příkaz podepíše archiv balik.jar pomocí klíče pro alias lukas. Je k tomu potřeba jak heslo úložiště, tak heslo soukromého klíče. Lze samozřejmě balík podepsat i vícekrát. Druhý příkaz pak podpis balíku ověřuje. Uvedené příklady pochopitelně pokrývají jen nepatrnou část možností jak uvedených programů, tak oblasti podepisování jako takové. Dokumentace Javy se tím zabývá velmi široce, ale k dobrému pochopení jsou nezbytné obecné znalosti problematiky elektronického podepisování.

Změňme téma...

Sice jsem měl původně poněkud jiné plány, ale na základě četných žádostí čtenářů bude všechno jinak. Příští díl a několik dalších věnuji grafice a grafickým uživatelským rozhraním. Doufám, že se kromě jiného podaří i vyvrátit různé mýty, které se okolo této javovské oblasti vynořují. Bylo by totiž škoda, aby by byla Java zbytečně odmítána, přestože toho má tolik co nabídnout.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1123