MokaByte Numero 25  -  Dicembre 98
Gestione della 
sicurezza 
con il JDK 1.2
di 
Giovanni Puliti
Analisi del  modello della sicurezza inserito nel nuovo JDK 1.2.



Con il rilascio del nuovo JDK 1.2, i  progettisti della Sun hanno completamente cambiato il modello relativo alla gestione della sicurezza.  Adesso è possibile implementare soluzioni più flessibili e potenti che in passato, vediamo come...

Se provate a chiedere ad una persona con una cultura informatica media, se ha sentito parlare di Java e perché, sicuramente vi risponderà di sì, e menzionerà una parola magica: "portabilità". In effetti questo aspetto è sicuramente quello che ha fatto di Java un linguaggio innovativo, ed al tempo stesso lo ha proposto come la risposta ad uno dei maggiori problemi del 90% degli sviluppatori. 
Tuttavia studiando a fondo la piattaforma Java, la sua tecnologia, e soprattutto la filosofia annessa, notiamo che la portabilità è sì un aspetto fondamentale, ma non avrebbe ragione di esistere se non venissero offerte nel contempo anche altre feature. Una di queste è quella legata alla sicurezza: affinché il modello di codice portabile multipiattaforma di Java possa avere successo è infatti indispensabile che venga offerto un sufficiente livello di protezione da operazioni pericolose contro il sistema che ospita tale codice mobile. 
In Java la sicurezza è da sempre stata considerata sotto due punti di vista: proteggere il programma da possibili crash o stati del tutto incongruenti, oppure impedire alla applicazione di arrecare danni o risultare intrusiva nei confronti del computer sul quale JVM ed applicazione Java vengono eseguite. Nella terminologia abituale però quando si parla di sicurezza ci si riferisce a questo secondo aspetto. Parleremo proprio di questo aspetto della sicurezza, vedendo come il nuovo modello introdotto nel JDK 1.2 permetta una gestione completamente differente rispetto al passato. 
Molte delle considerazioni che faremo saranno basate sulle applet, dato che si tratta del caso più alla portata di tutti, ma i concetti che esporremo sono adattabili a tutta la piattaforma Java. 

Modello on-off

Prima dell'introduzione del nuovo modello, era il Security Manager (SM) ad eseguire un controllo su tutte le operazioni eseguite dalla applet proibendo quelle vietate. In base a tali restrizioni una applet non poteva collegarsi con server differenti da quello di provenienza, non poteva scrivere sul server, non poteva leggere né scrivere sul client. 
Una alternativa a tale schema è rappresentata dalla possibilità di utilizzare firme elettroniche: in questo caso infatti il client è in grado di sapere chi sia l'autore della applet (cioè chi l'ha firmata) e decidere se fornire maggiori libertà: come conseguenza l'applet possiede pieni poteri e può eseguire una qualsiasi operazione sulla macchina client. Tipicamente una soluzione del genere viene messa in atto solo in situazioni particolari, ad esempio in una intranet. Il meccanismo delle firme elettroniche non ha incontrato un grosso successo, sia per una parziale compatibilità dei browser, sia per la sua scarsa flessibilità, che per motivi legati alla logica delle applicazioni distribuite (Riquadro 1). 
 
Riquadro 1: cratteristiche operative di un client 
Da sempre gli evangelist di Java hanno portato avanti un concetto: il client è  e deve essere un client  e per questo motivo non diamogli troppa importanza. Questo significa che in una struttura distribuita il lato client dovrà sempre più fungere da controllore della parte back-end che suddivisa in un numero n di strati, dovrà svolgere tutte le operazioni corpose della applicazione complessiva. Per questo motivo una applet nella maggior parte dei casi non dovrà accedere alle risorse della macchina locale, ma fare affidamento solo sul server. Se ad esempio l’utente dopo aver effettuato delle operazioni di un certo tipo, vorrà salvare lo stato del lavoro, non dovrà salvare in locale, ma sul file system del server, demandando a lui tale operazione. Sarà necessario quindi un meccanismo di identificazione, al momento dell’esecuzione del client, per riottenere tali informazioni. 
Questo modello trova la sua naturale messa in pratica nel networkcomputer, che come noto affida tutte le operazioni di read e write di dati al server: da questo punto di vista un NC è solo una unità di calcolo. 
Il grosso vantaggio di non salvare dati in locale (oltre alla sicurezza), è che non siamo vincolati alla stazione di lavoro, ma possiamo collegarci ogni volta ad un NC differente. 
Infine, non facendo riferimento alle risorse locali, una applet potrà trovarsi a suo agio sia su un potente PC, su un NC, ma anche su un qualsiasi device embedded (cellulari,  pc palmari, orologi, smart cards) che contengano una JVM.

Il modello a permessi variabili 

La limitazione maggiore del modello on-off è la scarsa granularità ottenibile: o l'utente non può fare niente o può fare tutto. In scenari sempre più orientati all'interconnettività e alla gestione di architetture distribuite, poter disporre di un sistema maggiormente flessibile, personalizzabile e configurabile dinamicamente, è una esigenza sempre più sentita. Può essere utile diversificare il tipo di operazioni che una applet può eseguire in base al proprietario della stessa (cioè da dove la si è scaricata), oppure in base al tipo delle operazioni (su quale risorsa si intende lavorare). Praticamente un sistema molto simile alla gestione degli utenti e dei permessi che si trova in Unix. Il modello della sicurezza del JDK 1.2 risponde proprio a questa esigenza: è possibile adesso creare tante tipologie di permessi a seconda delle esigenze potendo controllare nei minimi dettagli tutti gli aspetti coinvolti. Vediamo brevemente come è strutturato il nuovo scenario. Il tutto parte dal lato client dove si può specificare, in base alla provenienza della applet, il tipo di operazioni che essa potrà eseguire in locale, e su quali risorse. Non si tratta quindi di un meccanismo on-off, ma della possibilità di creare una politica molto più dettagliata di permessi. Al posto del SM adesso troviamo l'Access Controller (AC), il cui compito è lo stesso del suo predecessore, ma che offre la possibilità di personalizzare gli accessi. In realtà il SM continua ad essere presente per mantenere la compatibilità all'indietro con le applicazioni basate sul JDK 1.1: in questo caso però esso esegue solo una interfaccia per l'AC che esegue in background tutte i controlli del caso. In Figura 1 è riportata l'attuale configurazione del sistema. La possibilità di modificare la gestione dei permessi consente di cambiare completamente la politica della sicurezza semplicemente editando un file di configurazione (anche in runtime), cosa possibile in precedenza solo sostituendo completamente il SM (operazione non facile e non sempre possibile). 
 
Figura 1: Architettura della gestione della sicurezza: ogni operazioni richiesta dal programma deve essere filtrata da vari strati di software. Il security manager è stato rimpiazzato dall’Access controller, ma funziona ancora da interfaccia per mantenere la compatibilità all’indietro 

Il funzionamento dell'AC si basa sui seguenti concetti: 

bulletverde.gif (904 byte) Provenienza del codice: specifica da dove il codice è stato scaricato se si tratta di un host remoto, o caricato se proviene dal file system locale; 

 Permesso: una serie di permessi disponibili che verranno associati alle singole operazioni eseguibili sulla macchina; 

 Politica di sicurezza: un insieme di associazioni che specificano le operazioni che possono essere eseguite e da chi; 

 Dominio di protezione: un insieme di privilegi (o permessi) associati ad un insieme di programmi Java (applet o applicazioni) con la stessa provenienza, e firmati elettronicamente da utenti appartenenti allo stesso gruppo. 

Vediamo di entrare nel dettaglio dei singoli elementi per capirne il significato ed il funzionamento. 

Permessi

L'elemento base su cui l'AC basa il suo funzionamento è l'oggetto (inteso come entità) Permesso, che corrisponde alla classe java.security.Permission. 

A tale classe sono attribuiti due significati distinti: quando un oggetto Permission è associato ad una classe, esso rappresenta l'insieme di operazioni che sono state garantite alla classe stessa; altrimenti esso consente di sapere se si possiedono i permessi necessari per poter eseguire una certa operazione. Il possedere una classe di permesso di accesso a file non significa che si possa realmente accedere al file, ma rende noto se è possibile farlo. 

Una classe Permission ha tre proprietà fondamentali: 

bulletverde.gif (904 byte)Un tipo: il tipo di permesso che si intende specificare (accesso a file o a socket). Le API di Java mettono a disposizione 11 tipi di permessi, che vanno dall'accesso a file (java.io.FilePermission), a permessi legati alla possibilità di accedere a risorse di rete via socket (java.net.SocketPermission), a classi legate a permessi di esecuzione di operazioni su thread e simili (java.lang.Runtimecission); 

Un nome: anche se non è obbligatorio e non è stata definita una regola precisa di naming, in genere si cerca di mantenere una relazione piuttosto stretta fra il nome e la sua area di interesse. MyFilePermission piuttosto che MyPermissioXYZ nel caso di permessi su file; 

Una serie di azioni: in funzione della tipologia di permesso, è specificata una serie di operazioni che è possibile eseguire. Ad esempio, creando ex-novo un oggetto FilePermission possiamo pensare di fornire azioni come la scrittura o lettura del file, mentre in altri casi tali azioni non avrebbero senso. 

Da notare che la classe Permission è astratta, e che quindi non è utilizzabile direttamente: dal punto di vista del programmatore generalmente essa serve principalmente per la definizione di permessi particolari. Dispone di alcuni metodi di servizio che però non sono utilizzati nella programmazione generica. 
La classe BasicPermission può essere utile in fase di sviluppo, dato che può essere utilizzata come mattone elementare da cui partire: non dispone di azioni e rappresenta in pratica un permesso binario (si/no) molto simile a quello del modello precedente. In [1] è possibile trovare maggiori informazioni riguardo a tutti i permessi inseriti nei package di sistema. 

La class Policy 

Dopo aver definito un set di permessi, si deve consentire all'AC di dedurre quali di essi devono essere applicati e a quali codici: la coppia AC+Policy svolge il compito che precedentemente era assolto dal SM. 
È forse utile fare una piccola parentesi per chiarire un punto inerente la nomenclatura: in questo caso il termine codice si riferisce a codice di programma, ovvero una classe (un file). Il concetto di file .class è importante per il fatto che permette l'individuazione del codice nella rete, e sulla base di tale provenienza può essere associato ad una serie di permessi. Anche in questo caso un solo Policy può essere installato in una VM, ma contrariamente al SM può essere rimpiazzato utilizzando i metodi 

 

public static Policy getPolicy() 

public static Policy setPolicy() 

Il passo successivo è quello di creare una serie di accoppiamenti fra programmi Java e permessi, in modo da definire una politica più o meno restrittiva. La java.security.Policy provvede al caricamento delle informazioni che portano alla politica di permessi. La classe è package-protected per cui non può essere acceduta direttamente in fase di programmazione: non è possibile cioè modificare il meccanismo di lettura dei permessi ma solo sostituirlo in toto. Si potrebbe in teoria creare un meccanismo ex novo, ma è un compito non banale, e nemmeno estremamente necessario. La configurazione scelta viene poi memorizzata in un file editabile e modificabile, in modo da garantire la massima flessibilità in ogni momento. Se ad esempio vogliamo che un programma possa leggere una singola directory o un solo campo di una tabella basta inserire tali informazioni in tale file. 
Si tratta del JAVAHOME/lib/security/java.security. 
Nel file java.security vi è un esempio di come può essere strutturato tale file. 

Domini di Protezione 

Un dominio di protezione rappresenta l'insieme di tutti i permessi che una certa classe (programma client sulla JVM) può eseguire. Un dominio è rappresentato materialmente dalla classe java.security.ProtectionDomain, il cui costruttore è 

public ProtectionDomain(CodeSource cs, Permission p) 
Associare un ProtectionDomain ad una certa classe, permette di specificarne il sito di provenienza (specificato dal nome del sorgente cs), la firma elettronica (sempre specificata in cs), ed il set di regole (date da p) alle quali deve sottostare. 
Non tutte le classi che vengono utilizzate da una applicazione, hanno associato un dominio di protezione: è il caso delle classi di sistema, che hanno un dominio per default detto system protection domain.

La Classe Access Control in azione 

Adesso abbiamo tutti i pezzi per poter passare al soggetto principale di tutto il sistema, ovvero l'Access Controller. Esso corrisponde alla classe java.security.AccessController, classe non accessibile direttamente dato che il costruttore è protetto e possiede una serie di metodi statici molto utili all'applicazione client per determinare a quali restrizioni è soggetta. In tal senso il metodo fondamentale, il checkPermission(), prende come parametro un permesso e determina se la classe è autorizzata a procedere. Nel file AccessControllerTest.java, presente sul dischetto allegato, è riportato un esempio molto semplice di come possa essere utilizzata tale tecnica. 
Vediamo adesso come viene eseguito il controllo dei permessi: il tutto si basa sullo stack delle operazioni che l'applicazione ha richiesto di eseguire, e sul confronto di tale stack con quello dei domini di protezione (Figura 2). 
 
Figura 2 - La sequenza, memorizzata nello stack, delle operazioni viene controllata ed associata al dominio di provenienza per controllarne i permessi.

Supponiamo il caso di una applet in esecuzione all'interno dell'appletviewer: dato che l'appletviewer stesso è basato su thread, è suo il primo metodo nello stack. Successivamente, al momento dell'istanziazione della applet, viene chiamato il thread della classe Applet: abbiamo quindi due thread in esecuzione, il cui dominio di protezione è quello di sistema. Il controllo passa poi al metodo init() della applet, ed a questo punto viene effettuato il controllo del dominio di provenienza. 
Se l'applet deve utilizzare diverse classi provenienti da domini differenti (Figura 3), allora verranno fatti più controlli, e complessivamente essa avrà un insieme di permessi corrispondente all'intersezione di tutti gli insiemi referenziati.
 Il motivo di tutto questo è piuttosto ovvio, garantire la massima sicurezza possibile (Figura 4). Si tenga presente che il dominio di sistema ha pieno potere su tutte le operazioni possibili, ed il controllo di cui sopra avviene regolarmente, ma il metodo checkPermission ritorna sempre; il dominio sistema è un superset, per cui non introduce riduzioni sugli insiemi tra i quali viene fatta l'intersezione. 
 
Figura 4 - L’insieme dei permessi che una applet acquista, è dato dall’intersezione dei vari set: il dominio di sistema non compare essendo un super set di tutti i possibili.

Tipicamente una applet sarà costituita da classi provenienti da un solo host ed appartenenti allo stesso autore, per cui il problema della coesistenza di domini differenti è praticamente molto rara. Inoltre il poter scaricare classi da più server differenti è una prerogativa possibile in una applet, ma solo se si è predisposto un permesso apposito. 

Permessi Speciali

Abbiamo visto quindi come sia possibile controllare le azioni che una applicazione Java intende eseguire sia per mezzo del restrittivo e rigido Security Manager, sia del più flessibile e personalizzabile Access Controller. Nel primo caso l'applicazione, o non può fare praticamente niente in locale, oppure ha pieno possesso della macchina (virtuale ma anche e soprattutto reale); nel secondo caso invece è possibile specificare a priori una certa politica di permessi ed associarla ad un determinato codice. In entrambe le situazioni però il carattere per così dire della VM è predeterminato al momento dell'istanziazione e non può essere modificato in fase di run. Questa può essere una limitazione in quei casi in cui, pur volendo mantenere un alto livello di sicurezza, si desideri abbassare la guardia per un breve lasso di tempo. Il modello AC permette di risolvere il problema per mezzo della coppia

 

public static void beginPrivileged() 

public static void endPrivileged() 

Tutto il codice inserito fra questi due metodi ha il potere di agire con i permessi della classe che ha chiamato beginPrivileged (classe contenitore). Tutte le chiamate interne al blocco a metodi di classi possono agire con maggiore libertà. Ovviamente questo meccanismo non porta nessun particolare vantaggio per quel codice costituito da semplici sequenze di operazioni non facenti riferimento a classi esterne: esso infatti viene in ogni caso eseguito con il permesso della classe contenitore. Analogamente la tecnica è inutile per le chiamate a metodi di classi aventi diritti più ampi della classe contenitore, proprio per la regola dell'intersezione. Vediamo un esempio: 
 

Public class MyClass{ 

 Public myMethod(){ 

  Try{ 

   AccessController.BeginPriviliged(); 

   // fai qualcosa con molto potere…… 

  } 

  Catch(Exception e){} 

  Finally{ AccessController.endPrivileged(); } 

 } 

Come si può notare la coppia begin/end deve essere inserita all'interno di una coppia try-catch e deve essere prevista anche la clausola finally: si deve impedire infatti che una generica eccezione interrompa l'esecuzione del programma lasciando in sospeso la begin. Per questo motivo è necessario l'uso della try ed, analogamente, è molto importante la finally che permette di chiudere il blocco privilegiato. Per chi dimentica sempre la porta di casa aperta, nonostante abbia appena comprato una porta blindata di ultimo tipo, c'è una notizia confortante: tutta la gestione di una begin/end avviene in modo molto simile ad una transazione. Sono infatti previsti meccanismi automatici di timeout e di controllo delle referenze ad oggetti esterni non più utilizzati (finiti nel cestino del garbage collector) che garantiscono in ogni caso la chiusura della transazione. 

Passiamo alla pratica…

Dopo una dose massiccia di teoria, qualcuno di voi si starà chiedendo come mettere in pratica quanto affrontato fino ad adesso. 
Supponiamo che un programmatore voglia dotare una certa applet di particolari diritti e che tale applet venga mandata in esecuzione all'interno della JVM di un browser client noto. Ovviamente tale client deve poter dare il suo assenso a concedere maggiore libertà all'applet stessa (si parla infatti in questi casi di applet fidate). Per prima cosa il programmatore deve provvedere a creare due chiavi crittografiche (una privata ed una pubblica): la prima serve per firmare e sigillare i file da inviare sul client, mentre la pubblica serve al client per identificare l'autore della applet. Per generare la coppia di chiavi si può utilizzare il comando genkey. Ad esempio:

 

keytool -genkey -alias Criptor -keypass myPassGiovanniXYZ 

genera una chiave pubblica associandovi un alias dell'autore delle chiavi (Criptor) e una password (myPassGiovanniXYZ). Le chiavi vegono memorizzate in un oggetto java.security.KeyStore il quale a sua volta viene inserito in un file criptato .keystore, situato nella home directory dell'utente, e protetto da una password che il keytool richiede all'utente insieme ad altre informazioni sull'identità dello stesso. A questo punto deve essere generato un file .jar contenente i .class della applet (supponiamo per brevità solo il file MyApplet.class): 
 

Jar cf MySignedApplet.jar MyApplet.class 

Si deve quindi firmare l'applet contenuta nel jar per mezzo di un comando tipo 
 

jarsigner -keystore /.keystore - storepass pippo 

-keypass myPassGiovanniXYZ MySignedApplet.jar Criptor 

le due password "pippo" e "myPassGiovanniXYZ" servono rispettivamente per accedere al file .keytool (che, come appena detto, è protetto), ed alla chiave privata in esso contenuta. 
Conseguentemente di una azione di questo tipo, adesso il file jar MySignedApplet, contiene, oltre al MyApplet.class relativo alla applet ed il manifest.mf, anche due file Criptor.sf e Criptor.sda, che serviranno al client per autenticare l'autore della applet e per controllarne l'integrità.A questo punto deve essere preparata la pagina Html in modo che preveda questa nuova situazione: 
 

<applet code=MyApplet.class archive= "MySignedApplet.jar" width=100 height=100>

</applet> 

Il client però deve ottenere la chiave pubblica di Critpor in modo da poterne verificare la firma. Per consegnare tale chiave al client, si può esportare dall'archivio personale di Criptor in un file (Criptor.cert), per mezzo del comando: 
 

keytool -export -alias Fido -file Criptor.cert 

Questa operazione di fatto esporta la chiave di Criptor dal suo archivio personale nel file .cert. Operazione del tutto analoga ma opposta, deve essere fatta dal client che dovrà inserire tale chiave in un archivio locale (un file .keystore) per mezzo di: 
 

keytool -import -alias AliasCriptor -file Criptor.cert 

associandola ad un alias (AliasCriptor). Vediamo infine come sia possibile per il client dare maggiore libertà d'azione all'applet ospite: esso può inserire in un file denominato ad esempio "MyPolicies.txt": 
 

grant signedBy "AliasCriptor" { 

 permission java.io.FilePermission "*", "read"; 

 permission java.io.FilePermission "/data/criptor", "read, write"; 

}; 

Questo grant rilascia i privilegi d'accesso in lettura su tutto il file system del client e directory /data/criptor a quelle applicazioni firmate da Criptor (identificato nel db locale per mezzo dell'alias "AliasCriptor"). Per attivare il proprio file di policy quando si utilizza l'appletviewer si può ad esempio utilizzare un comando del tipo 
 

appletviewer -J-Djava.policy=/homedir/MyPolicies.txt 

  http://www.infomedia.it/ MyApplet.html 

Come si è potuto osservare si tratta di eseguire una serie di operazioni piuttosto lunga, ma tutto sommato semplice. È bene comunque tenere presente che il modello a sandbox continuerà ad essere valido (anche se filtrato da questo nuovo), per cui utilizzando la JVM 1.2 non è necessario per forza di cose utilizzare firme elettroniche o file di permessi. Il modello AC sarà in ogni caso sempre presente ed attivo, ma entrerà in gioco in maniera attiva solo quando richiesto esplicitamente. 

Conclusioni

Abbiamo visto come il nuovo modello di gestione della sicurezza rappresenta sicuramente un notevole passo in avanti rispetto al pur lodevole meccanismo del Security Manager. 
Innegabilmente è piuttosto complesso, ma sicuramente offre notevoli potenzialità sia adesso che in futuro, grazie alla sua flessibilità ed aggiornabilità. 
È molto probabile che, per il prossimo futuro, la maggior parte dei client che saranno progettati non avranno necessità tali da richiedere l'utilizzo del modello, ma in ogni caso la sua presenza è sicuramente una garanzia. Quando Miko Matsumura mi disse in una intervista ([3]) che con il JDK 1.2 "adesso la piattaforma Java è veramente completa. possiamo supporre che il prossimo futuro sarà per la piattaforma Java un periodo di assestamento", ebbene nel caso della sicurezza molto probabilmente questo è vero. Non resta che attendere i prossimi browser versione 5.xx per la compatibilità con il nuovo sistema. 

Bibliografia

bulletverde.gif (904 byte)[1] "The Java Security", di Scott Oaks Ed. O'Reilly. 
[2] "Il modello della sicurezza nel JDK 1.2", MokaByte Marzo 1998 http://www.mokabyte.it/0398. 
[3] "JavaOne 3ª Ed.: inizia l'era Java", di Giovanni Puliti in Computer Programming 70 - Giugno 1998. 
[4] Tutta la documentazione ufficiale sul sito Sun http://java.sun.com/security. 


 
 


MokaByte Web  1998 - www.mokabyte.it 
MokaByte ricerca nuovi collaboratori. Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it