MokaByte Numero  37  - Gennaio 2000
La crittografia 
in Java
di 
Giovanni Puliti
Implementazione di tecniche crittografiche in Java


 

Concetti come crittografia, firma digitale e certificati elettronici sono quanto mai attuali, in un mondo pervaso da un  internet insicuro,  e dal sempre crescente successo del commercio elettronico, che richiede invece sicurezza.  Java offre da tempo numerosi strumenti per il supporto di tali strumenti, vediamo quali

Introduzione
Con l’avvento di internet, e delle reti di comunicazione in genere, sono sorte una serie di problematiche (o più precisamente si sono riadattate le vecchie al nuovo supporto digitale), legate alla sicurezza, riservatezza e certificazione. 
Due sono le tipologie principali di problematiche legate alla sicurezza: riservatezza e certificazione. Nel primo caso nel momento in cui un flusso di dati passa da un punto ad un altro, l’obiettivo è rendere  possibile la comprensione dei dati solo al mittente ed al destinatario, vanificando eventuali intercettazioni durante la trasmissione. La crittazione serve proprio a questo: non rendere il canale di comunicazione impenetrabile, ma rendere i dati che vi fluiscono, del tutto incomprensibili ad estranei. 
Nel caso di commercio elettronico ad esempio,  le informazioni trasmesse durante la transazione relative all’acquisto di un certo bene (dati client, numero carta di credito e conto corrente), non devono cadere in mano ad estranei.
La certificazione di un messaggio invece   permette di risolvere l’altro problema, quello della identificazione certa circa l mittente del messaggio: ad esempio se io sono un commerciante di beni preziosi, vorrei avere la certezza sulla identità di chi effettua un acquisto, al fine di accreditare alla persona giusta il pagamento del bene acquistato.
In questo articolo vedremo quindi  come implementare in Java particolari tecniche al fine di garantire la massima sicurezza e riservatezza dei dati che circolano in rete.
Ovviamente non prenderemo in considerazione tutti quegli aspetti legati alla sicurezza di sistema, argomenti relativi all’amministrazione di rete, concentrandoci invece sulla programmazione.
Breve introduzione alla Crittografia 
La crittografia è un particolare processo grazie al quale, per mezzo di sofisticati algoritmi, è possibile trasformare una sequenza di byte con senso logico (messaggio) in un altra del tutto incomprensibile. Questa trasformazione avviene grazie ad una chiave: solo chi possiede la chiave per aprire  e chiudere il messaggio potrà crittare  e decrittare il messaggio.
Gli algoritmi più semplici sono quelli cosiddetti a chiave simmetrica: con una sola chiave infatti permettono sia di crittare che di decrittare un messaggio (vedi Figura 1). 
 
 
 Figura 1 Un algoritmo di crittatura simmetrico permette di crittare e di decrittare un documento con la stessa chiave che viene detta in questo caso simmetrica.

Proprio per la loro semplicità (ma anche debolezza), sono stati inventati i cosiddetti algoritmi a chiave asimmetrica: una chiave privata (in possesso solo del mittente), serve per il processo di decrittazione, mentre una pubblica (accessibile a chiunque)  permette di offuscare il messaggio. In questo modo, chiunque  desideri inviare un messaggio a me, con la certezza che solo io possa decifrare tale messaggio,  dovrà  effettuare una crittazione del messaggio con la mia chiave pubblica (Figura 2).
 

 Figura 2 Un sistema di crittografia basato su chiave asimmetrica, utilizzando la chiave pubblica di un  certo destinatario per crittare un messaggio, si ha la certezza che solo tale destinatario potrà decrittare il messaggio.

Invertendo le chiavi si può risolvere il problema dell’identificazione certa, cosa che vedremo più avanti.
Gli algoritmi a chiave asimmetrica  sono più sicuri, ma computazionalmente più pesanti, e generano codici più ingombranti. Per questo motivo spesso  nella pratica, quando si necessita leggerezza e velocità, si preferisce utilizzare una tecnica mista: il mittente critta il messaggio con una chiave simmetrica,  procede a crittare la stessa chiave simmetrica con la chiave asimmetrica pubblica del destinatario, ed invia poi messaggio e chiave entrambi crittati. Il ricevente, con la sua chiave privata, decritterà prima la chiave simmetrica e poi con tale chiave il messaggio stesso (Figura 3) 
Questo meccanismo risulta essere attualmente piuttosto sicuro, anche se come al solito, benché nessuno ne abbia  dimostrato l’impossibilità,  ad oggi non risultano esserci state violazioni degne di nota, 
 
 

 Figura 3 Gli algoritmi asimmetrici sono computazionalmente pesanti,  ed i codici hash prodotti ingombranti: perciò si critta il messaggio con una chiave pubblica, e si invia tale pubblica precedentemente crittata con chiave asimmetrica

 
 
 
 

Firme elettroniche e certificati
Anche se molto brevemente e sinteticamente abbiamo visto come sia possibile oscurare i dati ad occhi non desiderati; resta quindi da vedere come rendere possibile l’identificazione certa delle parti in gioco.  Affronteremo anche questo aspetto in maniera piuttosto stringata, rimandando a [JavaSecurity] e [JavaIO] per maggiori approfondimenti.
L’elemento fondamentale alla base di tutto è la cosiddetta firma elettronica, strettamente legata al concetto di certificato elettronico. Vediamo come si compone un caso tipico: un soggetto (che chiameremo MITT) deve inviare un messaggio ad un destinatario (DEST). Vogliamo che  DEST abbia la certezza che il messaggio arrivi proprio da MITT e non da un altro.
Per permettere ciò MITT deve per prima cosa effettuare una trasformazione del messaggio: questo passaggio viene effettuato per mezzo di una funzione hash che da una sequenza di byte restituisce un numero o codice.  Tale codice è indispensabile che sia univoco ed il processo irreversibile (anche se tale vincolo lo si presuppone solo empirico, ma indimostrabile, come vedremo avanti).
A questo punto MITT critta il codice hash con la sua chiave privata (esattamente al contrario di quanto abbiamo visto prima), ottenendo in questo modo la firma elettronica del messaggio: tale messaggio viene inviato a DEST, insieme alla firma elettronica (si noti che in questo caso non è importante offuscare il messaggio che viene quindi inviato in chiaro).
Dall’altra parte  DEST  provvede prima a  decrittare  la firma ottenendo il codice hash del messaggio (detto anche impronta del messaggio), e la confronta con il codice hash da lui calcolato sul messaggio in chiaro. Se le due impronte coincidono, allora si ha la garanzia che  il messaggio provenga effettivamente da MITT.
In questo passaggio un punto fondamentale è  poter stabilire da parte di DEST con certezza che la chiave pubblica utilizzata ed attribuita a MITT  sia effettivamente di MITT, e non di un’altra persona (certezza senza la quale il meccanismo, pur formalmente corretto, perde di ogni significato).
Per garantire ciò, si ricorre spesso agli enti di certificazione internazionali: in pratica con un processo di registrazione, l’ente, dopo accurati controlli “dichiara” con tanto di certificato elettronico (del tutto simile a quanto visto poco sopra) che una certa chiave sia effettivamente di MITT e non di un altro soggetto .
I concetti legati a sicurezza, chiavi, firme elettroniche e certificati richiederebbero molto spazio per una accurata trattazione, spazio che in questo caso non abbiamo a disposizione: per chi fosse ulteriormente curioso si rimanda alla bibliografia.
 
 
 

Il package JCE e le problematiche degli USA 
Ora che abbiamo fatto una rapida panoramica sulla crittografia, vediamo come sia possibile implementare tali tecniche in Java.  Come molti sanno, il governo degli Stati Uniti d’America proibisce l’esportazione non autorizzata di algoritmi crittografici (sui quali è imposto un vincolo militare  equivalente a quello delle armi belliche); è per questo motivo che Sun ha organizzato le varie classi necessarie per la security in un package a se stante (il Java Criptography Extension, JCE in breve) scaricabile liberamente in USA e Canada. 
E’ per questo, nel tipico spirito americano quindi che ogni cittadino della federazione può scaricare il JCE a patto di affermare di non essere un terrorista internazionale (seguendo questa politica contorta, i terroristi del paese possono scaricare liberamente il package). 
La cosa è piuttosto umoristica agli occhi di un europeo, e ricorda molto il testo del modulo I94W (la cosiddetta Green Card), obbligatorio per chiunque desideri accedere agli USA. Gli americani invece sono molto attenti a questi aspetti, quasi da raggiungere  livelli di paranoia.
Chi volesse provare ad implementare tecniche di crittografia in Java, potrà utilizzare le classi equivalenti che si trovano in molti pacchetti free prodotti da terze parti. Un ottimo prodotto in  tal senso è quello messo a disposizione dall’Insitute  for Applied Information Processing and Communications (Austria), che ha rilasciato il IAIK_JCE [IAIK]
 
 
 

Hash coding
Come si è avuto modo di vedere, i vari processi basati su crittografia fanno uso di una trasformazione tanto semplice quanto fondamentale, ovvero quella della codifica hash. 
Tale trasformazione opera su uno stream di dati (detti in chiaro) e restituisce un codice identificativo di tale stringa. Questo codice non ha significato agli occhi di un umano, dato che è frutto di una trasformazione non deducibile manualmente. Tale trasformazione infatti deve sottostare alle seguenti regole:

  • Una funzione hash è deterministica: lo stesso documento deve generare sempre lo stesso codice. Il codice quindi non deve dipendere dal momento in cui viene calcolato, o da nessun altro parametro esterno alla sequenza di byte che identifica il documento stesso.
  • Un codice hash  deve essere uniformemente distribuito su tutto il dominio disponibile: la sequenza di caratteri del codice deve essere il più casuale possibile, onde evitare di ricondurre al documento.
  • Deve essere estremamente difficile il processo inverso decodifica: anche se non è possibile dimostrare l’impossibilità del processo inverso di decodifica, tale trasformazione deve essere il più difficile possibile. Un buon algoritmo di decodifica deve produrre codici che siano decodificabili solo per mezzo della forza bruta (ovvero provando tutte le possibili soluzioni), e non per mezzo di un qualche accorgimento particolare. E’ quindi chiaro che maggiore sarà il numero di bit utilizzati per codificare il messaggio, e più garanzie di sicurezza può dare l’algoritmo.
  • La probabilità che due documenti producano lo stesso codice hash deve essere prossima a zero: anche in questo caso non si può avere la certezza matematica, e ci si deve affidare alla statistica. Questo è comunque è un requisito fondamentale (ad esempio nel caso dell’identificazione dell’autenticità di un messaggio).
  • Alta dipendenza dalle condizioni iniziali: piccole modifiche del messaggio devono portare a grosse differenze nel codice risultante.
  • Il codice non deve dare nessuna informazione sul messaggio originale: per garantire una maggiore sicurezza, il codice non deve contenere  nessun riferimento diretto relativo al messaggio di origine.


A questo punto molti di voi avranno in mente la classe java.util.Hashtable, sempre molto utilizzata nella maggior parte dei programmi. In questo caso però il metodo hashcode() che essa mette a disposizione, ha il solo scopo di fornire un ID per ogni elemento memorizzato nella tabella, e non fornisce nessuna protezione sicura dal punto di vista crittografico. 
Questo tipo di codifica hash, utilizzata essenzialmente per scopi di ordinamento e di ricerca, nella maggior parte dei casi necessita di rispondere solamente ai primi due punti della lista vista sopra, e molto spesso nella pratica nemmeno questa ipotesi è rispettata. 
Produrre  buoni algoritmi di hashing è sicuramente un compito che è bene lasciare agli esperti, affidandosi a quello che si trova già disponibile. 
Il JCE offre  a tal proposito la classe java.secuirity.MessageDigest, una classe astratta che serve come riferimento ad un generico algoritmo di codifica hash. Le sotto-classi concrete (come ad esempio java.security.MessageDigestSPI) implementano un particolare algoritmo. 
In questo caso, invece di implementare la classe astratta (cosa formalmente non possibile), si può ottenere un factory utilizzando il metodo MessageDiget.getInstance() per ottenere una implementazione di un particolare codificatore hash. Nella tabella “Java 1.1 ed algoritmi di Hash” sono elencati gli algoritmi disponibili con il JCE, e le loro caratteristiche principali
Vediamo quindi come si può ottenere il codice hash data una qualsiasi sorgente di byte (che per brevità e detta messaggio). L’algoritmo tipico si compone dei seguenti passi

  • ricavare una istanza di MessageDigest per un particolare algoritmo
  • leggere i dati
  • passare i dati al metodo update
  • se ci sono altri dati ripeti il passo 2
  • invocare il metodo digest() per ottenere il codice
Dopo l’invocazione del metodo digest il buffer dei dati viene resettato, e si può riprendere da capo.
L’esempio contenuto in URLDigest.java mostra come ottenere un codice hash di una particolare pagina web (passando l’URL alla come parametro a linea di comando). In  questo caso l’algoritmo utilizzato è l’SHA-1
Il metodo getInstance() può essere invocato, oltre che con il nome dell’algoritmo, anche con un ulteriore parametro aggiuntivo, ovvero il nome del provider da dove prelevare l’algoritmo particolare. 
Con l’installazione del JDK (JCE) abbiamo a disposizione i principali algoritmi di codifica, ma niente vieta di utilizzarne altri di terze parti (come Criptyx). Installare un provider significa mettere a disposizione della applicazione delle classi, per cui al solito il procedimento è molto semplice, e si basa a grandi linee sul copiare dei file e modificare il classpath.
Infine, dato che è utile nel processo di firma elettronica di un documento, da notare il metodo isEqual che confronta fra loro due Digest  differenti (attenzione che il metodo equal() invece, secondo la tipica logica di  Java, compara due reference della  stessa istanza).
 
 
 

Crittografia
Adesso che sappiamo come creare un codice hash, vediamo come sfruttare questa conoscenza per dotare una applicazione di un meccanismo di crittazione a chiave pubblica.
La prima cosa che dobbiamo fare è generare la coppia di chiavi necessarie: per fare questo ci possiamo avvalere delle classi  Key, KeyPair, e KeyPairGenerator. Le prime due sono semplici contenitori per chiavi singole e doppie, mentre la terza è un generatore di chiavi: tale classe  deve essere  istanziata con il nome di un particolare algoritmo (useremo in questo caso il DSA), e con un numero casuale. 
E’ questo il punto più importante di tutto il processo, dato che tale numero deve essere effettivamente il più casuale possibile: spesso si utilizzano valori derivanti dalle pause che l’utente compie fra la pressione di un tasto e l’altro, oppure monitorando i movimenti del mouse. Tecniche più sofisticate invece tengono conto del rumore bianco (segnale caotico per definizione) generato ad esempio da una scheda sonora in assenza di segnale di entrata.
Nel caso di una transazione potremmo utilizzare il nome del client, l’url del server, la data più qualche altra informazione. Tale combinazione può essere codificata con un algoritmo di hash come visto in precedenza, a formare la firma digitale. Ad esempio le poche righe di codice che seguono possono servire allo scopo
 
 

MessageDigest md = MessageDigest.getInstance("SHA");
String string = url + user + (new Date()).toString();
md.update(string.getBytes());
KeyPairGenerator keygen = KeyPairGenerator.getInstance("DSA");
keygen.initialize(512, new SecureRandom(md.digest()));
KeyPair pair = keygen.generateKeyPair();
ps.println(" Public-key:  " + pair.getPublic().getEncoded()+ " Private-key: " + pair.getPrivate().getEncoded()));


L’utilizzo della classe SecureRandom  offre maggiori sicurezze per quanto riguarda la reale casualità del numero generato. Ora che abbiamo generato la chiave, possiamo creare il motore di crittazione.
 

Cipher myCipher = Cipher.newInstance("PKA");
myCipher.initEncrypt(key);
Byte[] data;
myCipher.cript(data);


A questo punto possiamo crittare i dati (procedimento effettuato a blocchi di byte per maggior sicurezza) semplicemente con la seguente istruzione
 

CipherOutputStream cos = new CipherOutputStream(new  FileOutputStream("name", myCipher));


La classe CipherOutputStream (e l’equivalente CipherInputStream) permettono di inviare dati direttamente in uno stream in forma crittata.  Può essere associata ad uno stream di compressione, da un lato, e ad uno di scrittura su file, dall’altro. Questo è proprio quello che è mostrato nell’esempio FileDigest.java.
 
 
 

Firme elettroniche e Certificati
Nel riquadro “La ricorsività della certificazione” è mostrato cosa sia un certificato, a cosa serve, e come lo si può ottenere. Come abbiamo avuto modo vedere in precedenza, alla base del concetto di certificato c’è quello di firma. 
E’ per questo motivo che nel JDK 1.2 è stato introdotto qualcosa che rende i vari passaggi più semplici, ovvero la classe Signature. Il suo compito è duplice, ovvero da un lato operare la firma elettronica di una sequenza di byte, dall’altro di verificare la correttezza di una firma di un’altra sequenza.
Il metodo pubblico col quale ottenere una Segnature (il costruttore infatti è protetto), è il getInstance, che prende il nome dell’algoritmo utilizzato 

public static Signature  getinstance(Strgin algorithm, String provider)
Il concetto di provider lo si è visto in precedenza, e nel JDK 1.2 ad esempio si può utilizzare quello di Sun e il DSA come algoritmo.  Per una creazione della firma, si può utilizzare il metodo 
public final void initSign(PrivateKey key)
che crea di fatto una nuova firma digitale. Se invece si desidera verificare la validità di una firma, si può utilizzare il metodo
public initVerify(PublicKey key)
Entrambe le operazioni di creazione e verifica si basano su una sequenza di byte di base: ad esempio, se stiamo firmando in file, dovremmo leggerne tutti i byte ed utilizzare poi tale  sequenza come sorgente.
I metodi
public void update (byte b)
public void update (byte[] b)
hanno proprio lo scopo di alimentare il buffer interno della classe Signature.
Nel file Signer.java è mostrato come utilizzare la classe Signature ed i suoi metodi principali.
 
 
 
 
 

Conclusione
L'argomento è "delicato" e la progettazione di codice "sicuro" non è cosa banale… per esempio bisogna assicurarsi che le chiavi non viaggino su rete e non vengano memorizzate in strutture dati facilmente accessibili. 
Tuttavia le proprietà di safety tipiche di Java ci danno un grande aiuto, perché impediscono ai  "cattivi" di penetrare dentro le strutture dati di un programma usando trucchi come puntatori vaganti o sfondamenti di array o dello stack. Queste caratteristiche, insieme alla presenza di interfacce appositamente studiate e standardizzate, come quelle contenute nella Security API, fanno di Java il linguaggio più adatto per l'implementazione di transazioni di rete sicure. 
 
 
 

Bibliografia
[IAIK] http://wwwjce.iaik.tu-graz.ac.at/IAIK_JCE/jce.htm
[JavaCript] “Java Security”  di Scott Oaks, Ed. O’Reilly
[JavaIO] “Java I/O” di E. R. Harold, Ed. O’Reilly
[JavaCripto] “Java Criptografy” di Knudsen, Ed. O’Reilly
[MB0699] “La crittografia per la protezione delle informazioni” di Ugo Chirico, MokaByte 31, Giugno 99, www.mokabyte.it/0699
[MB1197] “Security: implementazione di tecniche crittografiche con la Java Security API” – di Fabrizio Giudici, Mokabyte 13, novembre 97 – www.mokabyte.it/1197
[MB0197] “Java & Security” MB04_Gen97
[MB0697] Intervista sulla sicurezza in Java fatta a Gary McGraw, MokaByte 09, www.mokabyte.it/0697
 

Esempi
Gli esempi descritti in  questo articolo si possono scaricare qui
 
 
 

Riquadro 1 - La Java Security API
Java possiede un'API appositamente dedicata alle funzionalità crittografiche e facilmente incorporabile nei propri programmi: la Security API. Essa è disponibile come parte integrante del JDK 1.1, anche se alcune funzionalità sono state rese disponibili solo recentemente in un'estensione (JCE - Java Cryptographic Extension) che non è attualmente disponibile al di fuori degli USA per i già citati problemi legali. 
Le funzionalità crittografiche presenti nella Security API sono: 
· Crittazione: trasformazione di dati in modo che essi possano essere accessibili solo ad utenti     autorizzati. 
· Firme Digitali: particolari sequenze di numeri che possono essere associate a dati digitali per assicurare l'utente della loro provenienza. Possono anche essere utilizzate per firmare"  del codice Java: in questo modo, applet o servlet particolari possono essere in grado di  rilassare i vincoli del proprio Security Manager, in quanto sono in grado di provare la loro  provenienza "fidata".
· Gestione delle chiavi. A come visto nell'introduzione, le chiavi sono gli oggetti con cui un utente può provare di avere l'autorizzazione all'accesso di dati riservati. Le chiavi, che poi  altro non sono che numeri "speciali", vanno generate con algoritmi appositi e gestite in modo opportuno. 
La Java Security API è stata progettata seguendo due principali fini: 
 

  • implementation independence and interoperability 
  • algorithm independence and extensibility 


L'indipendenza dell'implementazione e dell'algoritmo vuol dire rendere gli utenti in grado di utilizzare funzionalità crittografiche senza preoccuparsi di come esse sono effettivamente implementate. Questo vuol dire, per esempio, che un nuovo algoritmo crittografico può essere acquistato da un produttore indipendente e facilmente integrato in un'applicazione già esistente. 
L'interoperabilità delle implementazioni vuol dire che diverse implementazioni di funzionalità crittografiche possono funzionare tra di loro, scambiarsi le rispettive chiavi, o verificare le rispettive firme digitali.

Riquadro 2 - La ricorsività della certificazione
Il concetto fondamentale di certificato elettronico è molto simile ad uno cartaceo, in quanto ha lo scopo di garantire che una determinata persona sia effettivamente quello che dice di essere.
Più precisamente, dato che il processo di firma si basa sulla chiave pubblica di un ente, gioca un ruolo fondamentale il poter sapere con certezza che una certa  chiave sia effettivamente di una certa persona. Il certificato digitale svolge proprio questo compito.
In pratica una certa persona chiede di essere certificato da un ente certificatore (EC), mostrando i suoi dati. Dopo una accurato controllo, l’ente certifica che Tizio è veramente Tizio e che la sua chiave è xcfu9530.
Per garantire la validità del certificato, l’ente lo firma digitalmente con la sua chiave.
In questo modo se Caio vorrà avere  la garanzia che la chiave di Tizio sia effettivamente xcfu9530, potrà reperire tale chiave sul certificato, e controllare la validità del certificato stesso per mezzo della firma che EC ha apposto al documento.
Il certificato, di validità un anno, viene quindi apposto come garanzia durante la transazione.
Come si intuisce a questo punto sorge il problema della ricorsività delle firme elettroniche: in teoria infatti per avere la certezza che la chiave pubblica di EC sia proprio una certa specificata da qualche parte, dovrei ricorrere alla certificazione di  un altro EC, e così via, fino ad arrivare al certificatore assoluto. Ovviamente questo non esiste, e quindi in genere ci si basa sul fatto che i certificatori più conosciuti hanno una chiave pubblica nota a tutti, o che almeno sono memorizzati all’interno dell’applicazione. 
A tal proposito si può incorrere nel problema di dover leggere la firma di un EC non conosciuto dall’applicazione. E’ bene quindi da un lato utilizzare enti certificatori i più famosi possibili, e dall’altro di memorizzare all’interno della applicazione più enti possibili. 
 


MokaByte rivista web su Java
MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it