Le API JSSE, presenti nei package javax.net
e javax.net.ssl
forniscono le seguenti funzionalità:
- Socket sicure: comunicazione Client/Server.
- Un non-blocking engine: produzione e consumo di stream basati sui protocolli SSL/TLS/DTLS (SSLEngine).
- Factories: creazione di sockets, server sockets, SSL sockets e SSL server sockets.
- Una classe rappresentante un contesto di sicurezza per un socket.
- Interfacce Key e trust manager.
- Una classe per le connessioni HTTPS.
La classe javax.net.ssl.SSLEngine
rappresenta il core delle API incapsulando una macchina a stati finiti per SSL/TLS/DTLS. La classe SSLEngine rappresenta l'engine SSL/TLS/DTLS che si colloca tra lo strato applicativo e quello di trasporto della pila protocollare di Internet, fornendo il servizio di cifratura ed integrità dei dati:
Un'istanza della classe SSLEngine può trovarsi in uno dei seguenti stati:
Stato | Descrizione |
---|---|
Creation | L'SSLEngine è stato creato e inizializzato ma non ancora utilizzato. |
Initial handshaking | L'handshake iniziale è una fase dove le entità coinvolte si scambiano messaggi per realizzare operazioni quali lo scambio dei certificati e l'autenticazione di una o entrambe le parti coinvolte. I dati applicativi possono essere scambiati solo se l'handshake si conclude positivaente. |
Application Data | In questa fase avviene lo scambio dei dati applicativi. |
Rehandshaking | Entrambi le parti coinvolte nella comunicazione possono richiedere una rinegoziazione durante lo stato Application Data. |
Closure | Quando la connessione non è più necessaria è possibile chiudere l'engine SSL. |
Con i concetti esposti siamo in grado di utilizzare le API per creare un programma che utilizzi due Thread per simulare il Client ed il Server di una comunicazione sicura basata sul protocollo DTLS (SSL over Datagram). L'unico aspetto che dobbiamo tener presente, rispetto al classico uso di SSL over TCP, è che con DTLS non abbiamo, a livello di trasporto, un affidabile trasferimento dei dati. Questa caratteristica impone di inserire, all'interno del codice un meccanismo di ritrasmissione dei dati quando necessario.
Iniziamo con il creare la classe principale DtlsDatagramDemo.java
inserendo in essa i primi metodi per la costruzione di un SSLContext:
import java.io.FileInputStream;
import java.net.*;
import java.nio.ByteBuffer;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import javax.net.ssl.*;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.*;
import static javax.net.ssl.SSLEngineResult.Status.*;
public class DtlsDatagramDemo {
private final String dtls1 = "DTLSv1.0";
private final String dtls12 = "DTLSv1.2";
..
private SSLContext getClientDTLSContext() throws Exception {
KeyStore ks = KeyStore.getInstance("JKS");
KeyStore ts = KeyStore.getInstance("JKS");
char[] password = "password".toCharArray();
ks.load(new FileInputStream("c:\\cert\\client\\local\\client.jks"), password);
ts.load(new FileInputStream("c:\\cert\\client\\others\\server.jks"), password);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
kmf.init(ks, password);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ts);
SSLContext sslCtx = SSLContext.getInstance(dtls1);
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return sslCtx;
}
private SSLContext getServerDTLSContext() throws Exception {
KeyStore ks = KeyStore.getInstance("JKS");
KeyStore ts = KeyStore.getInstance("JKS");
char[] password = "password".toCharArray();
ks.load(new FileInputStream("c:\\cert\\server\\local\\server.jks"), password);
ts.load(new FileInputStream("c:\\cert\\server\\others\\client.jks"), password);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
kmf.init(ks, password);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(ts);
SSLContext sslCtx = SSLContext.getInstance(dtls1);
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return sslCtx;
}
SSLContext
rappresenta il contesto di configurazione dal quale è possibile ottenere un SSLEngine. Attraverso di essa possiamo impostare diversi parametri. Nel codice presentato, definiamo un keystore ed un truststore di tipo JKS sia per il client che per il server.
Un keystore è un file con estensione JKS contenente informazioni sulla chiave privata e chiave pubblica con certificato. Per il nostro programma dimostrativo non possiamo avvalerci di una CA, ragion per cui genereremo un keystore, sia per il client che per il server, di tipo self-signed (autofirmato). Questa operazione ci consente di testare il funzionamento dell'SSLEngine considerando sicuri, nel nostro programma, i certificati da noi firmati.
Generare un keystore è relativamente semplice: è sufficiente utilizzare l'utility keytool da console Windows per la creazione del certificato appartenente al client:
keytool -genkey -keyalg RSA -alias client -keystore c:\cert\client\local\client.jks -storepass password -validity 360 -keysize 2048
e successivamente quello appartenente al server dopo aver creato preliminarmente i path che conducono ai file JKS:
keytool -genkey -keyalg RSA -alias server -keystore c:\cert\client\others\server.jks -storepass password -validity 360 -keysize 2048
Mentre un keystore è utilizzato da un entità comunicante come database dal quale reperire certificati da inviare quando richiesto (generalmente per autenticarsi), un truststore contiene invece certificati di terze parti che l'entità utilizzerà per autenticare il lato opposto coinvolto nella comunicazione.
Per il nostro programma dimostrativo, client.jks
rappresenta il keystore del client mentre server.jks
il suo truststore. Possiamo utilizzare in modo inverso gli stessi certificati lato server: dal punto di vista del server server.jks
rappresenta il keystore mentre client.jks
il suo trustsore.
Come mostra il codice del metodo getServerDTLSContext()
per il server abbiamo utilizzato percorsi diversi per i file JKS per evidenziare meglio quanto appena detto. I metodi getClientDTLSContext()
e getServerDTLSContext()
svolgono il medesimo lavoro: recuperare i file di keystore e truststore, costruire un KeyManagerFactory
ed un TrustManagerFactory
, costruire un SSLContext scegliendo la versione DTLS desiderata e, infine, inizializzare il contesto prima di restituirlo.
Queste operazioni creano un contesto SSL in cui sono specificati i certificati che client e server utilizzeranno durante la comunicazione.
Il lavoro appena svolto ci consente di creare un SSL Engine per il client ed uno per il server:
private SSLEngine getClientSSLEngine() throws Exception {
SSLContext context = getClientDTLSContext();
SSLEngine engine = context.createSSLEngine();
SSLParameters paras = engine.getSSLParameters();
paras.setMaximumPacketSize(1024);
engine.setUseClientMode(true);
engine.setSSLParameters(paras);
return engine;
}
private SSLEngine getServerSSLEngine() throws Exception {
SSLContext context = getServerDTLSContext();
SSLEngine engine = context.createSSLEngine();
engine.setNeedClientAuth(true);
SSLParameters paras = engine.getSSLParameters();
paras.setMaximumPacketSize(1024);
engine.setUseClientMode(false);
engine.setSSLParameters(paras);
return engine;
}
Nel codice notiamo la chiamata preliminare ai metodi di creazione del contesto e la creazione dell'SSLEngine attraverso createSSLEngine()
. Di particolare interesse è l'uso del metodo setUseClientMode(boolean)
che consente la creazione di un SSLEngine di tipo client(valore true)
o server (valore false)
.
Per un engine di tipo server possiamo specificare la richiesta di autenticazione per il client attraverso il metodo setNeedClientAuth(boolean)
. Con l'impostazione al valore true
per il parametro, il server farà fallire la procedura di handshake se il client non fornirà un certificato valido.
Il codice proposto può essere integrato analizzando la macchina a stati che implementa la procedura di handshake, ma questo sarà il tema di un prossimo approfondimento.