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 dela gestione della sicurezza: ogni operazioni
richiesta dalprogramma deve essere filtrata da vari dtrati di software.
Il security manager è stato rimpiazzato dall’Access controller,
ma funziona ancora da interfaccia per mantenere la compatiblità
all’indietro |
 |
Il funzionamento dell'AC si basa sui seguenti concetti:
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:
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()
e
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.
Figura 3 - Può capitare che vi siano classi differenti provenienti
da domini differenti. Il permesso complessivo è dato dall’interezione
dei vari domini |
 |
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
[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.
|