|
Vero progresso quando i vantaggi di una nuova tecnologia diventano per tutti
|
|
| Home Page |
|
| Articoli |
|
| News |
|
| Forum |
|
| Classi |
|
|
|
|
|
Implementare un server SSL multithread
By
Enrico Cesaretti
3 novembre 2008
|
 |
|
Preparazione dell'ambiente
Bene per prima cosa dobbiamo creare una coppia di chiavi ssl con l’utility keytool della JVM. Io stò usando JVM1.5.0, keytool della versione 1.6.0 è un po’ diverso ma le funzioni sono comunque tutte implementate.
Prima di scrivere il codice è necessario che illustri il funzionamento SSL nei programmi java. Sappiamo che esistono due tipologie di comunicazione SSL:
- one way – in cui il server fornisce il certificato digitale al client e questo lo usa per la codifica.
- two way – client e server si scambiano i certificati.
Nel nostro caso andremo ad implementare la versione two way. Ecco cosa server per far funzionare il tutto:
- Il client deve generare una coppia di chiave che chiameremo Client-Identity memorizzata in un keystore.
- Il server deve generare una coppia di chiave che chiameremo Server- Identity memorizzata in un keystore.
- Il client deve specificare la lista di certificati digitali di cui si fida Trust-list. Tale lista per default è già esistente all’interno della JVM: %JAVA_HOME%/jre/lib/security/cacerts. Ovviamente il client deve importare in questo keystore il certificato o chiave pubblica del server.
- Il server deve specificare la lista di certificati digitali di cui si fida Trust-list. Tale lista per default è già esistente all’interno della JVM: %JAVA_HOME%/jre/lib/security/cacerts. Ovviamente il server deve importare in questo keystore il certificato o chiave pubblica del client.
|
|
|
Generazione chiavi
|
top
|
Per generare una coppia di chiavi basta aprire un prompt dei comandi e lanciare il comando che segue:
 A questo punto abbiamo generato il nostro keystore, abbiamo inserito una coppia di chiavi con alias “alias”. Se vogliamo verificare il corretto inserimento delle chiavi possiamo digitare quanto segue:
 Ovviamente è necessario generare una coppia di chiavi per il client ed una coppia di chiavi per il server. Una volta generate le chiavi, dobbiamo:
- Lato client importare il certificato server dentro il cacerts.
- Lato server importare il certificato client dentro il cacerts.
|
|
Esportazione certificati
|
top
|
|
Esportiamo il certificato client in un file c:\certif.cert
 Adesso importiamo il certificato client nella macchina server:
 Possiamo verificare la corretta installazione del nostro certificato client all’interno del cacerts server con portecle:

La stessa operazione la dobbiamo eseguire anche lato client, esportiamo il certificato server in un file e lo importiamo nel cacerts del client.
Se fate funzionare client e server sulla stessa JVM l’import verrà fatto sull’unico cacerts.
…Siamo pronti all’implementazione di client e server.
|
|
Implementazione client e server
|
top
|
Ovviamente la parte interessante è quella in cui andiamo a creare un oggetto SSLSocket figlio di Socket che si occupa di tutte le operazioni si cifratura e decifratura sul socket. Questa operazione avverrà sia lato client che lato server. I passi sono comunque abbastanza semplici:
Lato client:
SSLContext ctx = SSLContext.getInstance("TLS");
Genera un oggetto SSLContext che implementa il protocollo secure socket specificato. In questo caso il nostro codice lato client lavorerà su protocollo TLS. Tale oggetto verrà utilizzato per creare un socket sicuro che lavora in protocollo TLS.
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
Questa classe agisce come una factory per la key manager basato su uno standard di memorizzazione chiave che nel nostro caso è SunX509. Ogni KeyManager gestisce un tipo specifico di chiavi per l'utilizzo da parte SecureSockets. Le chiavi saranno reperite dal keystore.
KeyStore ks = KeyStore.getInstance("JKS");
Creiamo un oggetto Keystore, wrapper del nostro database di chiavi inizializzandolo con un metodo factory e specificando il tipo di keystore, in questo caso JKS (Java Key Store).
ks.load(new FileInputStream(“C:\keystore.jks”), “storepass”.toCharArray());
Con questa istruzione andiamo a caricare nell’oggetto Keystore il contenuto del keystore precedentemente generato. Ovviamente passiamo in input a questo metodo un FileInputStream che punta al nostro file keystore e la storepass sottoforma di array di byte.
kmf.init(ks, “storepass”.toCharArray());
Inizializziamo la KeyManagerFactory con il metodo init passandogli il riferimento all’oggetto keystore e la storepass.
ctx.init(kmf.getKeyManagers(), null, null);
Inizializziamo il contesto ssl passandogli in input un array di oggetti KeyManager che rappresentano l’insieme di chiavi e certificati presenti nel keystore.
SSLSocketFactory factory = ctx.getSocketFactory();
Inizializziamo un oggetto SSLSocketFactory che ci permette poi digenerare il socket ssl.
SSLSocket socket = (SSLSocket)factory.createSocket(host, port);
Inizializiamo il socket sicuro passando host e porta al metodo createSocket di SSLSocketFactory.
Lato server:
Illustriamo soltanto le differenze con la parte client:
SSLContext ctx = SSLContext.getInstance("SSL");
Genera un oggetto SSLContext che implementa il protocollo secure socket specificato. In questo caso il nostro codice lato server lavorerà su protocollo SSL. Tale oggetto verrà utilizzato per creare un socket sicuro che lavora in protocollo SSL.
ServerSocketFactory ssocketFactory =ctx.getServerSocketFactory();
ServerSocket serverSocket = ssocketFactory.createServerSocket(9443);
Dal contesto ssl otteniamo un oggetto ServerSocketFactory che useremo per creare un server socket ssl. A questo punto il nostro server userà SSL.
Siamo pronti adesso a vedere il codice finito.
|
|
Server
|
top
|
|
Il codice lato server si compone di due classi:
- TCPParallelServer – classe eseguibile che genera un ServerSocket e vi si mette in ascolto.
- ServerThread – classe thread che viene istanziata ogni volta che si connette un client, ne legge le richieste e genera risposte.
La prima classe che vediamo è TCPParallelServer:
import java.io.FileInputStream; import java.net.ServerSocket; import java.net.Socket; import java.security.KeyStore; import javax.net.ServerSocketFactory; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSocketFactory; public class TCPParallelServer { //IL PERCORSO DEL KEYSTORE SERVER E LA RELATIVA PASSWORD protected static final String STORENAME = "c:\\keystore.jks"; protected static final String STOREPASS = "storepass"; //METODO CHE APRE IL SERVER-SOCKET-SSL public void avvia() throws Exception { //DICHIARIAMO TUTTI GLI OGGETTI PER LAVORARE CON SSL ServerSocketFactory ssocketFactory = null; SSLContext ctx = null; KeyManagerFactory kmf = null; KeyStore ks = null;
try { //IN QUESTA PARTE ANDIAMO A CARICARE LE CHIAVI DAL KEYSTORE. char[] storepass = STOREPASS.toCharArray(); ctx = SSLContext.getInstance("SSL"); kmf = KeyManagerFactory.getInstance("SunX509"); ks = KeyStore.getInstance("JKS"); ks.load(new FileInputStream(STORENAME), storepass); kmf.init(ks, storepass); ctx.init(kmf.getKeyManagers(), null, null); ssocketFactory=ctx.getServerSocketFactory(); }catch (Exception e) { e.printStackTrace(); } //QUI INIZIALIZZIAMO IL NOSTRO SERVER-SOCKET-SSL. ServerSocket serverSocket = ssocketFactory.createServerSocket(9443);
//CICLO INFINITO PER L’ASCOLTO DELLE CONNESSIONI CLIENT.. //QUANDO UN CLIENT SI CONNETTE LA COMUNICAZIONE VIENE GESTITA DA UN THREAD //SEPARATO DAL CORRENTE E L’APPLICAZIONE //SUCCESSIVAMENTE RITORNA IN ASCOLTO DI ALTRI CLIENT. while(true) { System.out.println("In attesa di chiamate dai Client... "); Socket socket = serverSocket.accept(); System.out.println ("Ho ricevuto una chiamata di apertura da:\n" + socket);
//OGNI VOLTA CHE UN CLIENT SI CONNETTE CREIAMO UN OGGETTO //ServerThread(illustrato più avanti) CHE ESTENDE Thread E LO //LANCIAMO CHIAMANDONE //IL METODO start(). TALE OGGETTO LAVORA CON UN SOCKET E GESTISC //LA COMUNICAZIONE CON IL CLIENT CORRENTE. ServerThread serverThread = new ServerThread(socket); serverThread.start(); } } //QUESTO METODO RENDE ESEGUIBILE IL NOSTRO SERVER, NE CREA UN’ISTANZA //E NE CHIAMA IL METODO avvia();.
public static void main (String[] args) throws Exception { TCPParallelServer tcpServer = new TCPParallelServer(); tcpServer.avvia(); } }
Adesso andiamo a vedere come viene gestita la comunicazione con il client per ogni connessione nella classe ServerThread: import java.net.*; import java.io.*;
//LA CLASSE DEVE LAVORARE SEPARATA DA TCPParallelServer PER QUESTO ESTENDE Thread. class ServerThread extends Thread { //DICHIARIAMO TUTTI IL SOCKET CHE VERRA’ PASSATO DA TCPParallelServer. private Socket socket; public ServerThread(Socket socket) { this.socket = socket; } //esecuzione del Thread sul Socket. Il socket in input //viene usato per comunicare con il client. public void run() { //DICHIARIAMO TUTTI GLI OGGETTI PER LAVORARE COL SOCKET BufferedReader is = null; DataOutputStream os = null; try { is = new BufferedReader( new InputStreamReader(socket.getInputStream()));
os = new DataOutputStream(socket.getOutputStream());
//Leggiamo in un ciclo infinito l’input del client e lo //scriviamo nella risposta(come un eco), Successivament //lo stampiamo a video lato server. Se la stringa catturata ha //valore "exit" il ciclo si interrompe e vengono chiuse le //risorse allocate per la connessione. while(true) { String userInput = is.readLine(); if (userInput == null || userInput.equals("exit"))break; os.writeBytes(userInput + '\n'); System.out.println("Il Client ha scritto: " + userInput); } System.out.println ("Ho ricevuto una chiamata di chiusura da:\n" + socket + "\n");
}catch (IOException e) { e.printStackTrace(); }finally { try{ os.close(); is.close(); socket.close(); }catch(Exception e) { e.printStackTrace(); } } } }
|
|
Client
|
top
|
|
Lato client dobbiamo creare un eseguibile che carica le chiavi dal keystore e le usa per creare un socket ssl, si connette al server ed invia messaggi criptati:
import java.net.*; import java.security.KeyStore; import java.io.*; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory;
public class TCPClient { //IL PERCORSO DEL KEYSTORE CLIENT E LA RELATIVA PASSWORD protected static final String STORENAME = "c:\\keystore.jks"; protected static final String STOREPASS = "storepass"; public void connetti(String host, int port) { //DICHIARIAMO TUTTI GLI OGGETTI PER LAVORARE COL SOCKET DataOutputStream os = null; BufferedReader is = null; BufferedReader stdIn = null; SSLSocket socket = null; //DICHIARIAMO TUTTI GLI OGGETTI PER LAVORARE CON SSL SSLContext ctx = null; KeyManagerFactory kmf = null; KeyStore ks = null; SSLSocketFactory factory = null; try { //IN QUESTA PARTE ANDIAMO A CARICARE LE CHIAVI DAL KEYSTORE. try { char[] storepass = STOREPASS.toCharArray(); ctx = SSLContext.getInstance("TLS"); kmf = KeyManagerFactory.getInstance("SunX509"); ks = KeyStore.getInstance("JKS"); ks.load(new FileInputStream(STORENAME), storepass); kmf.init(ks, storepass); ctx.init(kmf.getKeyManagers(), null, null); factory = ctx.getSocketFactory(); }catch (Exception e) { e.printStackTrace(); }
//OTTENGO IL SOCKET SSL socket = (SSLSocket)factory.createSocket(host, port); //Stream di byte da passare al Socket os = new DataOutputStream(socket.getOutputStream()); is = new BufferedReader(new InputStreamReader(socket.getInputStream())); stdIn = new BufferedReader(new InputStreamReader(System.in)); System.out.print("Per disconnettersi dal Server scrivere: exit\n"); //Ciclo infinito per inserimento testo del Client. //LEGGE IL TESTO DALL’INPUT STANDARD E LO INVIA AL SERVER. while (true) { System.out.print("Inserisci: "); String userInput = stdIn.readLine(); if(userInput.equals("exit")) { os.writeBytes(userInput + '\n'); break; } os.writeBytes(userInput + '\n'); System.out.println("Hai digitato: " + is.readLine()); } }catch(Exception e){ e.printStackTrace(); }finally{ //Chiusura delle risorse try { is.close(); os.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } } } //QUESTO METODO RENDE ESEGUIBILE IL NOSTRO CLIENT, NE CREA UN’ISTANZA //E NE CHIAMA IL METODO connetti();. public static void main (String[] args) { TCPClient tcpClient = new TCPClient(); tcpClient.connetti("localhost",9443); } }
A questo punto non ci resta che compilare il tutto e lanciare prima il server e poi il client: Lancio del server:

Lancio del client:
 Il server cattura la richiesta, crea un oggetto ServerThread, ne chiama il metodo start()e si rimette in attesa di altre chiamate client.
 Il client può digitare il testo a video e premere invio, riceverà l’eco del server:
 Il sever cattura il messaggio e lo stampa a video:
 Per testare il funzionamento multithread del nostro server possiamo anche lancia più client contemporaneamente. Siamo alla fine dell'articolo! Buon lavoro! Enrico.
|
|
|

Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto licenza Creative Commons
|
|
|