La procedura di handshake tra client e server è caratterizzata da un intenso scambio di messaggi.
Nel nostro esempio il tutto ha inizio con il client che invia il messaggio ClientHello
verso il server, includendo la più alta versione del protocollo SSL e lista di cipher suits che è in grado di supportare. Le informazioni di cipher suits includono algoritmi di crittografia e dimensione delle chiavi.
Il server risponde con il messaggio ServerHello
tramite la scelta della più alta versione del protocollo SSL e della migliore cipher suite che è supportata da entrambi. Opzionalmente il server invia un certificato o una catena di certificati al client.
Se l'autenticazione del server è richiesta l'opzionalità decade. Il server può richiedere un certificato al client se l'autenticazione di quest'ultimo è richiesta. Il server può inoltre inviare un messaggio di key exchange se le informazioni ottenute dal certificato del client non sono sufficienti per uno scambio di chiavi. Ad esempio, se se una cipher suite è basata sull'algoritmo Diffie-Hellman il messaggio key exchange conterrà la chiave pubblica Diffie-Hellman del server.
Con il messaggio ServerHelloDone
il server informa il client di aver completato la sua fase di negoziazione. Ricevuta la richiesta di un certificato, il client può inviare il proprio certificato. Ricevuto il messaggio key exchange il client si adopera per generare una chiave simmetrica di cifratura da utilizzare per lo scambio dei dati con il server.
Nel caso di cipher suite basata su Diffie-Hellman, il client invierà la sua chiave pubblica Diffie-Hellman. Se il client deve inviare il proprio certificato può, non obbligatoriamente, inviare il messaggio Certificate verify per facilitare l'autenticazione verso il server. Un messaggio Change cipher spec può essere inviato da entrambe le parti per cambiare la modalità di cifratura dei dati.
Con il messaggio Finished client e server comunicano, l'uno a l'altro, di essere pronti allo scambio dei dati.
La comunicazione tra client e server appena descritta viene inserita all'interno di una macchina a stati finiti in cui sono rese evidenti le transizioni di stato che è fondamentale gestire al meglio, all'interno del codice Java, per realizzarne una corretta implementazione:
Sequenza di chiamate di metodi durante un tipico handshake, con i corrispondenti messaggi e stati:
Client | SSL/TLS Message | HandshakeStatus |
---|---|---|
wrap() | ClientHello | NEED_UNWRAP |
unwrap() | ServerHello/Cert/ServerHelloDone | NEED_WRAP |
wrap() | ClientKeyExchange | NEED_WRAP |
wrap() | ChangeCipherSpec | NEED_WRAP |
wrap() | Finished | NEED_UNWRAP |
unwrap() | ChangeCipherSpec | NEED_UNWRAP |
unwrap() | Finished | FINISHED |
Il codice che implementa la macchina a stati per l'handshaking è il seguente:
private void handshake(SSLEngine engine, DatagramSocket socket,
SocketAddress address, String s) throws Exception {
engine.beginHandshake();
SSLEngineResult.HandshakeStatus hs;
boolean endLoops = false;
int loops = 60;
while (!endLoops) {
if (--loops < 0) {
throw new RuntimeException(
"Too much loops to produce handshake packets");
}
hs = engine.getHandshakeStatus();
if (null != hs) {
switch (hs) {
case NEED_UNWRAP:
case NEED_UNWRAP_AGAIN:
// receive ClientHello request and other SSL/TLS records
ByteBuffer networkData;
ByteBuffer plainTextAppData;
if (hs == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) {
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
try {
socket.receive(packet);
} catch (SocketTimeoutException ste) {
List packets;
if (hs == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
packets = new ArrayList();
} else {
// retransmission of handshake messages
packets = produceHandshakePackets(engine, address);
}
for (DatagramPacket p : packets) {
socket.send(p);
}
}
networkData = ByteBuffer.wrap(buf, 0, packet.getLength());
plainTextAppData = ByteBuffer.allocate(1024);
} else {
networkData = ByteBuffer.allocate(0);
plainTextAppData = ByteBuffer.allocate(1024);
}
SSLEngineResult r = engine.unwrap(networkData, plainTextAppData);
if (r.getStatus() == SSLEngineResult.Status.CLOSED) {
endLoops = true;
}
break;
case NEED_WRAP:
List packets
= produceHandshakePackets(engine, address);
for (DatagramPacket p : packets) {
socket.send(p);
}
break;
case NEED_TASK:
runDelegatedTasks(engine);
break;
case NOT_HANDSHAKING:
endLoops = true;
break;
case FINISHED:
endLoops = true;
break;
default:
break;
}
}
}
}
Si impone un limite (loops=60
) al numero di interazioni tra client e server, superato questo limite la procedura viene interrotta.
Il metodo è caratterizzato da un ciclo while
nel quale, ad ogni iterazione, si controlla lo stato di handshake (engine.getHandshakeStatus()
). Lo stato di handshake identifica un valore di transizione da gestire come illustrato nella macchina a stati rappresentata dall'immagine precedente.
Nel caso di invio/ricezione dati, vengono preparati i buffer da fornire in input ai metodi wrap()
ed unwrap()
dell'SSLEngine. Nello specifico, il metodo wrap()
genera dati sulla rete mentre unwrap()
li consuma. In dipendenza dallo stato dell'SSLEngine i dati possono essere applicativi o di handshake.
Possiamo notare la gestione della ritrasmissione dei dati in caso di SocketTimeoutException
. La generazione dei pacchetti di handshake è delegata ad un metodo, produceHandshakePackets()
, che presenteremo nella prossima lezione cosi come il metodo runDelegatedTasks()
che gestisce lo stato Run Task.