RFC 6347 - 4.2. Protocollo di handshake DTLS
4.2. The DTLS Handshake Protocol
DTLS usa gli stessi messaggi e flussi di handshake di TLS, con tre cambiamenti principali:
-
Scambio di cookie senza stato per prevenire attacchi di denial of service.
-
Intestazione di handshake modificata per perdita, riordino e frammentazione DTLS (evitare frammentazione IP).
-
Timer di ritrasmissione per la perdita di messaggi.
Salvo ciò, formati, flussi e logica sono quelli di TLS 1.2.
4.2.1. Denial-of-Service Countermeasures
I protocolli di sicurezza a datagramma sono molto vulnerabili al DoS. Due casi rilevanti:
-
L'attaccante invia richieste di handshake in serie esaurendo risorse del server (stato, operazioni crittografiche costose).
-
L'attaccante usa il server come amplificatore falsificando la sorgente della vittima; il server invia messaggi grandi (es. Certificate) alla vittima.
Per contrastarli, DTLS riprende il cookie senza stato di Photuris [PHOTURIS] e IKE [IKEv2]. Alla ricezione di ClientHello, il server PUÒ rispondere con HelloVerifyRequest con cookie come in [PHOTURIS]. Il client DEVE rinviare ClientHello con il cookie. Il server verifica e continua solo se valido. L'attaccante deve poter ricevere il cookie, rendendo difficile il DoS con IP spoofate. Nessuna difesa contro DoS da IP legittime.
Lo scambio è illustrato sotto:
Client Server
------ ------
ClientHello ------>
<----- HelloVerifyRequest
(contains cookie)
ClientHello ------>
(with cookie)
[Rest of handshake]
DTLS modifica quindi ClientHello per aggiungere il cookie.
struct {
ProtocolVersion client_version;
Random random;
SessionID session_id;
opaque cookie<0..2^8-1>; // New field
CipherSuite cipher_suites<2..2^16-1>;
CompressionMethod compression_methods<1..2^8-1>;
} ClientHello;
Al primo ClientHello il client non ha ancora cookie; il campo Cookie è vuoto (lunghezza zero).
HelloVerifyRequest è definito come segue:
struct {
ProtocolVersion server_version;
opaque cookie<0..2^8-1>;
} HelloVerifyRequest;
Il tipo di messaggio HelloVerifyRequest è hello_verify_request(3).
server_version ha la stessa sintassi di TLS. Per evitare negoziazione di versione nell'handshake iniziale, i server DTLS 1.2 DOVREBBERO usare DTLS 1.0 indipendentemente dalla versione TLS attesa. I client DTLS 1.2 e 1.0 DEVONO usare la versione solo per il formato pacchetto (uguale in 1.2 e 1.0), non per la negoziazione. In particolare, un client DTLS 1.2 NON DEVE dedurre dal 1.0 in HelloVerifyRequest che il server non sia 1.2 o che negozierà 1.0.
Rispondendo a HelloVerifyRequest, il client DEVE riusare gli stessi parametri (version, random, session_id, cipher_suites, compression_method) del ClientHello iniziale. Il server DOVREBBE usarli per generare e verificare il cookie. Il server DEVE usare in HelloVerifyRequest lo stesso numero di versione che per ServerHello. Alla ricezione di ServerHello, il client DEVE verificare la coerenza delle versioni. Per evitare duplicati di sequenza con più HelloVerifyRequest, il server DEVE riprendere il numero di sequenza del record da ClientHello in HelloVerifyRequest.
Nota: il limite del cookie è 255 byte; resta 32 per le versioni DTLS precedenti.
Il server DTLS DOVREBBE generare cookie verificabili senza stato per client. Una tecnica usa un segreto casuale:
Cookie = HMAC(Secret, Client-IP, Client-Parameters)
Alla ricezione del secondo ClientHello, il server verifica il cookie e che il client riceva a quell'indirizzo IP. Per evitare duplicati in scambi multipli di cookie, il server DEVE riusare il numero di sequenza del record da ClientHello nel ServerHello iniziale. I ServerHello successivi avvengono solo dopo creazione dello stato e DEVONO incrementarsi normalmente.
Un attaccante può raccogliere cookie da più indirizzi e riutilizzarli; il server può cambiare spesso Secret per invalidarli. Per la transizione tra segreti, una finestra che accetta entrambi è possibile; [IKEv2] propone un numero di versione nel cookie; si può verificare con entrambi i segreti.
I server DTLS DOVREBBERO effettuare scambio cookie ad ogni nuovo handshake. Senza rischio di amplificazione, la disattivazione è possibile (MAY); per default lo scambio DOVREBBE essere attivo. In ripresa di sessione, il server PUÒ omettere lo scambio. I client DEVONO essere pronti allo scambio ad ogni handshake.
Se HelloVerifyRequest è usato, il ClientHello iniziale e HelloVerifyRequest non entrano in handshake_messages (CertificateVerify) né verify_data (Finished).
Un ClientHello con cookie invalido DOVREBBE essere trattato come senza cookie, per evitare race o stallo se il client ha un cookie errato (cambio chiave di firma sul server).
Nota agli implementatori: possono arrivare più HelloVerifyRequest con cookie diversi; il client DOVREBBE rispondere con un nuovo ClientHello con cookie.
4.2.2. Handshake Message Format
Per perdita, riordino e frammentazione, DTLS modifica l'intestazione di handshake TLS 1.2:
struct {
HandshakeType msg_type;
uint24 length;
uint16 message_seq; // New field
uint24 fragment_offset; // New field
uint24 fragment_length; // New field
select (HandshakeType) {
case hello_request: HelloRequest;
case client_hello: ClientHello;
case hello_verify_request: HelloVerifyRequest; // New type
case server_hello: ServerHello;
case certificate:Certificate;
case server_key_exchange: ServerKeyExchange;
case certificate_request: CertificateRequest;
case server_hello_done:ServerHelloDone;
case certificate_verify: CertificateVerify;
case client_key_exchange: ClientKeyExchange;
case finished: Finished;
} body;
} Handshake;
Il primo messaggio di ogni lato in ogni handshake ha sempre message_seq = 0. Ogni nuovo messaggio incrementa message_seq. In un re-handshake, HelloRequest ha message_seq = 0 e ServerHello = 1. La ritrasmissione riusa lo stesso message_seq. Esempio:
Client Server
------ ------
ClientHello (seq=0) ------>
X<-- HelloVerifyRequest (seq=0)
(lost)
[Timer Expires]
ClientHello (seq=0) ------>
(retransmit)
<------ HelloVerifyRequest (seq=0)
ClientHello (seq=1) ------>
(with cookie)
<------ ServerHello (seq=1)
<------ Certificate (seq=2)
<------ ServerHelloDone (seq=3)
[Rest of handshake]
Dal punto di vista del livello record DTLS, la ritrasmissione è un nuovo record con nuovo DTLSPlaintext.sequence_number.
Le implementazioni mantengono un contatore next_receive_seq (almeno concettualmente), inizialmente zero. Se il numero ricevuto coincide con next_receive_seq, si incrementa e si elabora; se è inferiore, il messaggio DEVE essere scartato; se è superiore, l'implementazione DOVREBBE metterlo in coda ma PUÒ scartarlo (compromesso spazio/banda).
4.2.3. Handshake Message Fragmentation and Reassembly
Come in 4.1.1, ogni messaggio DTLS DEVE stare in un datagramma del livello trasporto. I messaggi di handshake possono superare la dimensione massima di record; DTLS può frammentare su più record trasmessi separatamente per evitare frammentazione IP.
L'emittente divide in N intervalli contigui senza superare la dimensione massima di frammento, coprendo l'intero messaggio; NON DOVREBBERO sovrapporsi. N messaggi handshake condividono il message_seq originale; ogni frammento ha fragment_offset e fragment_length; length resta quella del messaggio completo. Senza frammentazione: offset 0 e fragment_length = length.
Il ricevente DEVE bufferizzare fino a ricezione completa e DEVE accettare intervalli sovrapposti (ritrasmissioni con frammenti più piccoli se cambia la PMTU).
Come TLS, più messaggi dello stesso volo possono condividere un record DTLS se c'è spazio; due messaggi in un datagramma possono essere nello stesso record o in record distinti.
4.2.4. Timeout and Retransmission
I messaggi DTLS sono raggruppati in «voli» come nelle figure sotto. Ogni volo può contenere più messaggi ma è trattato come monolitico per timeout e ritrasmissione.
Client Server
------ ------
ClientHello --------> Flight 1
<------- HelloVerifyRequest Flight 2
ClientHello --------> Flight 3
ServerHello \
Certificate* \
ServerKeyExchange* Flight 4
CertificateRequest* /
<-------- ServerHelloDone /
Certificate* \
ClientKeyExchange \
CertificateVerify* Flight 5
[ChangeCipherSpec] /
Finished --------> /
[ChangeCipherSpec] \ Flight 6
<-------- Finished /
Figura 1. Voli di messaggi per handshake completo
Client Server
------ ------
ClientHello --------> Flight 1
ServerHello \
[ChangeCipherSpec] Flight 2
<-------- Finished /
[ChangeCipherSpec] \Flight 3
Finished --------> /
Figura 2. Voli per handshake con ripresa sessione
(senza scambio cookie)
DTLS usa un semplice schema di timeout e ritrasmissione con la macchina a stati sotto. I client inviano ClientHello per primi e partono in PREPARING. I server partono in WAITING con buffer vuoti e senza timer di ritrasmissione.
+-----------+
| PREPARING |
+---> | | <--------------------+
| | | |
| +-----------+ |
| | |
| | Buffer next flight |
| | |
| \|/ |
| +-----------+ |
| | | |
| | SENDING |<------------------+ |
| | | | | Send
Receive | | | | HelloRequest
next | | Send flight | |
flight | +--------+ | | or
| | | Set retransmit timer | | Receive
| | \|/ | | HelloRequest
| | +-----------+ | | Send
| | | | | | ClientHello
+--)--| WAITING |-------------------+ |
| | | | Timer expires | |
| | +-----------+ | |
| | | | |
| | | | |
| | +------------------------+ |
| | Read retransmit |
Receive | | |
last | | |
flight | | |
| | |
\|/\|/ |
|
+-----------+ |
| | |
| FINISHED | -------------------------------+
| |
+-----------+
| /|\
| |
| |
+---+
Read retransmit
Retransmit last flight
Figura 3. Macchina a stati per timeout e ritrasmissione DTLS
La macchina ha tre stati di base.
In PREPARING, l'implementazione prepara il prossimo volo, svuota il buffer, memorizza i messaggi e passa a SENDING.
In SENDING invia il volo; se è l'ultimo volo dell'handshake, passa a FINISHED; altrimenti imposta il timer di ritrasmissione e passa a WAITING.
Tre uscite da WAITING:
-
Scadenza del timer: torna a SENDING, ritrasmette il volo, resetta il timer, torna a WAITING.
-
Lettura di un volo ritrasmetto dal peer: stesso trattamento; la duplicazione indica spesso scadenza del timer sul peer e perdita parziale del nostro volo precedente.
-
Ricezione del volo successivo: se finale → FINISHED; altrimenti → PREPARING. Letture parziali non cambiano stato né timer.
I client partono in PREPARING, i server in WAITING (buffer vuoti, nessun timer).
Re-handshake server: FINISHED → PREPARING (HelloRequest). Client che riceve HelloRequest: FINISHED → PREPARING (ClientHello).
Per almeno due volte il MSL predefinito di [TCP], in FINISHED, il nodo che ha inviato l'ultimo volo (server in handshake normale, client in ripresa) DEVE rispondere a una ritrasmissione dell'ultimo volo del peer con una ritrasmissione del proprio ultimo volo, per evitare stallo se quel volo si perde. Vale anche per DTLS 1.0 ([DTLS1] non lo diceva esplicitamente). Esempio: Finished server perso → il client ritrasmette il suo Finished → il server rinvia Finished.
A causa delle perdite, un lato può inviare dati applicativi prima che l'altro abbia ricevuto Finished. Le implementazioni DEVONO scartare o bufferizzare tutti i dati applicativi della nuova epoch fino al Finished corrispondente. POSSONO interpretare dati applicativi di nuova epoch prima del Finished come riordino/perdita e ritrasmettere subito l'ultimo volo.
4.2.4.1. Timer Values
I valori sono a scelta dell'implementazione; una gestione errata causa congestione. Il valore iniziale DOVREBBE essere 1 s (minimo RFC 6298 [RFC6298]), raddoppiato a ogni ritrasmissione fino ad almeno 60 s (massimo RFC 6298). 1 s anziché 3 s predefiniti per la latenza. DTLS ritrasmette solo l'handshake.
Mantenere il valore corrente fino a una trasmissione senza perdita, poi reset possibile. Dopo lunga inattività (≥ 10 × valore attuale), reset possibile (re-handshake dopo grandi trasferimenti).
4.2.5. ChangeCipherSpec
Come in TLS, ChangeCipherSpec non è tecnicamente un messaggio di handshake ma DEVE essere trattato nello stesso volo del Finished associato per timeout e ritrasmissione, creando possibile ambiguità d'ordine in caso di perdita.
Le modalità TLS attuali restano prevedibili; le future DEVONO evitare ambiguità.
4.2.6. CertificateVerify and Finished Messages
Stesso formato di TLS. Gli hash includono i messaggi interi con message_seq, fragment_offset, fragment_length. Il MAC Finished DEVE essere calcolato come se ogni messaggio handshake fosse stato inviato in un singolo frammento. Con scambio cookie, il ClientHello iniziale e HelloVerifyRequest NON DEVONO entrare in CertificateVerify né Finished.
4.2.7. Alert Messages
I messaggi Alert non vengono mai ritrasmessi, nemmeno durante l'handshake. Un'implementazione che emetterebbe un alert DOVREBBE generarne uno nuovo se il record offensivo torna. Rilevare un peer che invia continuamente messaggi errati e terminare lo stato locale.
4.2.8. Establishing New Associations with Existing Parameters
Se lo stesso quartetto host/porta vede connessioni ripetute, un client può abbandonare silenziosamente una connessione e poi aprirne un'altra (reboot): il server vede handshake con epoch=0. Se crede di avere un'associazione, DOVREBBE proseguire il nuovo handshake ma NON DEVE distruggere quella vecchia finché il client non dimostra raggiungibilità (cookie o handshake completo con Finished verificabile). Dopo un Finished corretto, DEVE abbandonare l'associazione precedente per evitare due associazioni valide con epoch sovrapposte. Il requisito di raggiungibilità impedisce a un attaccante fuori percorso di distruggere l'associazione con soli ClientHello falsi.