Protocollo SSH - Università degli Studi di Parma

Transcript

Protocollo SSH - Università degli Studi di Parma
III.2 Il protocollo di trasporto (Transport Layer Protocol)
III.2.1 Introduzione
Il protocollo di trasporto (Transport Layer Protocol) è l’elemento più “basso” dei tre livelli
in cui è diviso il protocollo SSH. In questo paragrafo cercheremo di illustrare in dettaglio la
sua struttura del protocollo di trasporto e le sue potenzialità.
Il Transport Layer Protocol integra diversi servizi: l’autenticazione del server, la crittografia
forte e la garanzia di integrità dati; inoltre, può essere usato indipendentemente da SSH come
base di sviluppo per applicazioni che richiedono servizi sicuri attraverso la rete. In questa sezione descriveremo il minimo numero di algoritmi necessari per la realizzazione del Transport Layer Protocol, l’implementazione specifica del metodo di scambio chiavi DiffieHellman, la struttura binaria dei pacchetti e come avviene la fase di scambio chiavi.
E’ importante sottolineare che il Transport Layer Protocol è un protocollo di basso livello
per il trasferimento sicuro di dati e a questo livello viene effettuata l’autenticazione dell’host
remoto ma non l’autenticazione dell’utente. Infatti, questo tipo di autenticazione verrà eseguita successivamente quando il Transport Layer Protocol sarà stato creato (vedi par. III.3).
Il protocollo è stato progettato per essere semplice, flessibile e con la possibilità di effettuare
la negoziazione di tutti i parametri di sessione (metodi di scambio chiavi, algoritmi di autenticazione, algoritmi di integrità ecc.)
III.2.2 Inizio della connessione
Il protocollo che opera sotto il Transport Layer Protocol (es. TCP/IP) dovrebbe garantire
l’assoluta mancanza di errori di trasmissione. In caso contrario il Transport Layer Protocol
prevede che la connessione venga terminata. Quando il Transport Layer Protocol viene attivato sopra TCP/IP, il server normalmente riceve le nuove connessioni sulla porta di rete 22.
Questa porta è stata registrata presso lo IANA (Internet Assigned Numbers Authority) ed è
ufficialmente riservata al protocollo SSH.
Una volta che la connessione tra client e server viene stabilita, entrambe le parti inviano una
stringa di identificazione del tipo:
“SSH-versione_del_protocollo-commenti_alla_versione_del_protocollo” (es. SSH-1.992.0.13 (non-commercial)) seguita da un carattere di “carriage return (CR)” e uno di “line feed (LF)”.
La parte di stringa che contiene la versione del protocollo è usata principalmente per impostare le estensioni di compatibilità tra le versioni uno e due del programma, oltre a indicare
le capacità dell’implementazione (aggiornamenti, bug noti e fix ecc.). La parte di stringa dedicata ai commenti sul protocollo dovrebbe contenere informazioni supplementari che possono essere utili alla risoluzione di problemi dell’utente.
Inoltre, come vedremo al par. III.2.6, la parte di stringa di identificazione che precede il carattere di CR e quello di LF viene usata come componente per la definizione del “challenge”
47
utilizzato nello scambio chiavi di tipo Diffie-Hellman. La fase di scambio chiavi inizia subito dopo l’invio della stringa di identificazione di entrambe le parti e tutti i pacchetti che seguono la stringa di identificazione sono di tipo binario, secondo il modello descritto di seguito.
III.2.3 Struttura binaria dei pacchetti
I pacchetti utilizzati dal protocollo SSH sono costituiti da diverse aree di memoria riempite
con codifiche di numeri o dati di tipo binario. Come si può vedere dalla figura III.4, le aree
di memoria del pacchetto sono cinque:
packet_length
Questo campo contiene la lunghezza del pacchetto definita come numero di byte totali, non
includendo il campo “MAC” e il campo “packet_length” stesso.
padding_length
In quest’area di memoria viene scritta la lunghezza in byte del campo “padding”.
payload
Qui vengono scritti i dati da trasmettere all’altro lato della comunicazione. Si può dire che
quest’area di memoria è lo spazio utile del pacchetto per la trasmissione dati. Se la compressione è stata negoziata positivamente da ambo le parti, questo campo contiene dati compressi.
padding
La lunghezza del campo di padding è arbitraria, essa viene definita in modo che la lunghezza
totale (packet_length + padding_length + payload + padding) sia un multiplo della lunghezza
del blocco di cifratura (spesso 64-bit) usato dall’algoritmo simmetrico. Generalmente il protocollo riempie questo campo con dei byte casuali, comunque il contenuto non ha importanza ai fini dell’utilizzo del protocollo, esso serve solo ad “allungare” il pacchetto per rendere
possibile la cifratura a blocchi.
MAC
La sigla MAC sta per Message Authentication Code. Come vedremo più avanti, questo campo serve a garantire l’integrità dei pacchetti trasmessi. Il contenuto del MAC sarà riempito
solo se l’autenticazione dei messaggi è stata negoziata positivamente da entrambi i lati della
comunicazione e la sua lunghezza dipenderà dal tipo di algoritmo usato.
48
packet_length
padding_length
total
length
payload
packet
length
padding
padding
length
MAC
Figura III.4 Struttura del pacchetto binario.
E’ da notare che il campo che specifica la lunghezza del pacchetto è anch’esso criptato e che
la capacità del programma di interpretare i dati contenuti in tutti i successivi campi dipende
proprio dalla possibilità o meno di leggere questo campo. In genere questo campo è contenuto nel primo blocco di cifratura e viene letto subito dopo la sua decodifica.
La lunghezza minima di un pacchetto è pari a 16-bit o alla lunghezza del blocco di cifratura
(se il blocco di cifratura è lungo 64-bit, la lunghezza minima del pacchetto diventa necessariamente 64-bit). La lunghezza minima del pacchetto non include il campo MAC.
Tutte le implementazioni del protocollo devono garantire una lunghezza massima del pacchetto di almeno 35.000 byte con tutti i campi compresi (packet_length, padding_length, payload, padding e MAC); inoltre la lunghezza massima del campo di payload senza compressione deve essere di 32.768 byte.
Nel caso in cui venga abilitata la compressione, i dati contenuti nel campo di payload saranno compressi con l’algoritmo stabilito. La compressione dati può agire solo sul campo di payload, tutti gli altri campi conterranno dati non compressi. Comunque, il campo packet_length e MAC saranno calcolati a partire dalla lunghezza e dal contenuto del campo di
payload compresso.
Al momento l’unico metodo di compressione integrato nel protocollo è lo “zlib”.
III.2.4 Algoritmi di crittografia e integrità
Nel Transport Layer Protocol vengono utilizzati tre tipi di crittografia: crittografia simmetrica, crittografia asimmetrica e funzioni di Hash. Inoltre vengono anche utilizzati degli algoritmi senza possibilità di cifrare dati, ma col solo scopo di scambiare chiavi segrete in modo
sicuro.
In questo paragrafo faremo una breve descrizione di tutti i metodi crittografici utilizzati a
partire dagli algoritmi di scambio chiavi fino alle funzioni di Hash, passando per quelli simmetrici e quelli asimmetrici.
49
Per quanto riguarda gli algoritmi di scambio chiavi, al momento il protocollo ne richiede uno
solo: il diffie-hellman-group1-sha1. Quest’algoritmo è la combinazione di una serie di metodi matematici per lo scambio di un segreto comune e di una funzione di Hash. I dettagli implementativi sono riportati più avanti al par. III.2.6.
La scelta dell’algoritmo di crittografia simmetrica da utilizzare viene fatta durante la fase di
scambio chiavi. Dal momento in cui l’algoritmo simmetrico è stato definito ed entrambe le
parti sono a conoscenza della chiave di sessione, tutti i campi del pacchetto, ad eccezione di
MAC, saranno cifrati con l’algoritmo definito.
Gli algoritmi di crittografia simmetrica previsti dal protocollo sono i seguenti:
♦
♦
♦
♦
♦
♦
3des-cbc (algoritmo DES con l’utilizzo di tre chiavi);
Blowfish-cbc
Twofish-cbc
Arcfour (algoritmo di tipo stream cipher);
Idea-cbc
Cast128-cbc (algoritmo Cast con chiavi a 128-bit).
Gli algoritmi 3des-cbc, Blowfish-cbc, Twofish-cbc, Idea-cbc e Cast128-cbc sono tutti di tipo
block cipher implementati col metodo Cipher Block Chaining (CBC).
Maggiori dettagli sui concetti di stream cipher, block cipher e Cipher Block Chaining (CBC)
si trovano al par. II.1.3. Per dei ragguagli sul significato e il funzionamento di S-box e P-box
e Feistel network si veda il par. II.1.4.
3des-cbc
Questo è un algoritmo di tipo block cipher con capacità di cifrare blocchi di 64-bit, 3DES è
una variante del noto algoritmo DES. L’algoritmo utilizza una chiave a 168-bit e opera in tre
fasi: cifratura, decifratura e cifratura. I primi 8 byte della chiave (64-bit) vengono usati per la
prima cifratura, i successivi 8 per la decifratura e gli ultimi 8 per la cifratura finale. In questo
modo tutti i 24 byte della chiave vengono utilizzati dall’algoritmo. L’implementazione del
CBC mode richiede una concatenazione dei blocchi esterna all’algoritmo.
blowfish-cbc
Questo è un algoritmo di tipo block cipher e lavora su blocchi di 64-bit con chiavi che possono arrivare fino ad una lunghezza massima di 448-bit. L’algoritmo è diviso in due parti:
l’espansione della chiave e la cifratura dati. L’espansione della chiave consiste nel convertire
una chiave singola in un insieme di sottochiavi fino ad un massimo di 4168 byte, mentre il
lavoro di cifratura è basato su 18 iterazioni di tipo Feistel network, su una funzione di trasformazione con 4 S-box, 18 P-box e semplici operazioni algebriche tipo somme XOR o addizione mod 32. Per ulteriori dettagli sul funzionamento di questo algoritmo si veda il par.
II.3.
twofish-cbc
50
Questo è uno degli algoritmi simmetrici di ultima generazione iscritto alla selezione AES
(Advanced Encryption Standard). La sua struttura è di tipo block cipher con blocchi in input
di 128-bit e con chiavi a lunghezza variabile fino a 256-bits. L’algoritmo utilizza 16 iterazioni Feistel network con una funzione di trasformazione dipendente da 4 S-box da 8-bit in
input e 8-bit in output.
arcfour
Questo è un algoritmo di tipo stream cipher con chiavi a 128-bits. E’ un algoritmo compatibile con RC4 della RSA Data Security Inc.
idea-cbc
Questo e’ un algoritmo di tipo block cipher con capacità di operare su blocchi di 64-bit.
L’algoritmo sfrutta tre tipi di gruppi algebrici: l’XOR, le addizioni di mod 216 e le moltiplicazioni mod 216 + 1. Tutte le operazioni algebriche vengono svolte su sottoblocchi di testo in
chiaro di 16-bit ciascuno e questo rende l’algoritmo particolarmente efficace e veloce su
processori a 16-bit. L’algoritmo è stato brevettato dalla Ascom AG.
cast128-cbc
Questo algoritmo è di tipo block cipher e utilizza blocchi in input di dimensioni pari a 64-bit.
Le chiavi di questo algoritmo partono da una lunghezza minima di 64-bit e tutta la robustezza dell’algoritmo dipende dal tipo d’implementazione utilizzata per le sei S-box con 8-bit di
input e 32-bit di output. Per ragioni di sicurezza alcune organizzazioni che utilizzano questo
algoritmo mantengono la composizione delle loro S-box non pubblica.
Un ultimo dettaglio sull’implementazione di questi algoritmi a chiave simmetrica è che la
lunghezza delle chiavi utilizzate non è mai inferiore ai 128-bit.
Prima di passare alla trattazione degli algoritmi a chiave pubblica integrati nel protocollo, è
bene fare un breve accenno ai diversi formati delle chiavi pubbliche. Non essendo mai esistito uno standard unificato sui formati delle chiavi pubbliche, molte organizzazioni hanno
provveduto a crearne uno loro. Questo ha contribuito alla crescita di un ambiente piuttosto
eterogeneo e variegato.
Il formato di una chiave pubblica ne specifica la codifica e i certificati eventualmente aggiunti ne garantiscono l’identità. Non è detto che tutte le chiavi pubbliche siano utilizzabili
sia per la firma digitale che per la cifratura dei dati, inoltre l’utilizzo di alcune chiavi può essere ristretto dalle politiche definite dal certificato incluso.
Il Transport Layer Protocol è stato progettato per poter funzionare con quasi tutti i tipi di
chiave pubblica attualmente esistenti e per ora garantisce compatibilità con i seguenti formati:
♦ ssh-dss (formato nativo del protocollo, usato solo per firma digitale);
♦ x509v3 (formato che integra la possibilità di certificazione, usato solo per firma digitale);
♦ spki (formato che integra la possibilità di certificazione, usato solo per firma digitale);
51
♦ pgp (formato che integra la possibilità di certificazione, usato solo per firma digitale).
ssh-dss
Questo metodo di codifica chiavi è quello nativo del programma; le operazioni di firma e verifica con l’utilizzo di questo formato vengono fatte in accordo con il DSS (Digital Signature
Standard).
x509v3
Questo metodo indica che il certificato, la chiave pubblica e la firma risultante sono compatibili con il formato X.509v3.
spki
Questo metodo di codifica prevede che la chiave pubblica integri una sequenza di certificati
del tipo spki.
pgp
Questo metodo indica che il certificato, la chiave pubblica e la firma digitale sono compatibili con chiavi in formato binario del tipo OpenPGP.
Il protocollo SSH prevede l’utilizzo di due tipi di algoritmi a chiave pubblica per la cifratura
e/o firma dei dati:
♦ RSA
♦ DSA
Tuttavia l’unica implementazione conosciuta che supporta il protocollo RSA è quella commercializzata da F-Secure.
RSA
Questo è probabilmente l’algoritmo a chiave pubblica più conosciuto e diffuso al mondo, il
suo nome deriva dalle iniziali dei suoi inventori Ron Rivest, Adi Shamir e Leonard Adleman. RSA deve la sua robustezza alla difficoltà implicita di fattorizzare numeri molto grandi. Molti e diversi sono stati i metodi di criptanalisi utilizzati per cercare di violare
quest’algoritmo, ma nessuno di essi è mai riuscito in maniera completa. RSA può essere utilizzato per operazioni di firma e cifratura. Per ulteriori dettagli su questo algoritmo asimmetrico e sulla sua struttura matematica si veda par. II.2.
DSA
Questo algoritmo rappresenta la novità nel mondo della crittografia. Nel 1991 fu proposto
come standard dal NIST (National Institute of Standard and Technology) e lentamente molte applicazioni che utilizzano crittografia a chiave pubblica lo stanno adottando. E’ un algoritmo che non permette crittografia, ma solo firma digitale.
52
Come abbiamo detto precedentemente, l’algoritmo RSA è incluso solo nella distribuzione
commerciale di F-Secure. La sensazione è che le politiche mondiali nel settore della crittografia stiano spingendo verso l’abbandono di RSA a favore di DSA.
L’integrità dei dati trasmessi viene garantita tramite l’inclusione di un campo di MAC (Message Authentication Code) alla fine di ogni pacchetto. Il MAC viene calcolato in base a tre
elementi: un elemento segreto conosciuto da entrambe le parti che chiameremo chiave (es.
un numero random), il numero progressivo del pacchetto e il contenuto del pacchetto.
All’inizio della connessione il campo MAC avrà lunghezza zero e non ci sarà alcun tipo di
integrità dati, una volta che la fase di scambio chiavi è avvenuta con successo il MAC sarà
calcolato prima della cifratura del pacchetto tramite una funzione di questo tipo:
mac = MAC(chiave, numero_di_sequenza || pacchetto_non_criptato)
dove “pacchetto_non_criptato” è l’intero contenuto del pacchetto senza MAC (campi relativi
alla lunghezza, payload e padding) e “numero_di_sequenza” è un numero intero che indica
la sequenza implicita del pacchetto.
Il numero di sequenza viene impostato a zero per il primo pacchetto ed è incrementato ogni
volta che un nuovo pacchetto viene creato, senza tenere conto se il pacchetto sarà poi cifrato
o se si utilizzerà il MAC. Esso non viene mai ripristinato, anche se a un certo punto della
sessione vi è una nuova negoziazione delle chiavi e degli algoritmi (vedi par. III.2.7). Comunque, il numero di sequenza è pur sempre un contatore e quindi ritorna a zero ogni 232
pacchetti creati. Il numero di sequenza non viene mai incluso nel pacchetto che viene spedito, viene solo utilizzato per calcolare il contenuto del MAC.
E’ importante ricordare che i byte inclusi nel campo MAC vengono trasmessi senza essere
cifrati e che la lunghezza di tale campo dipende dall’algoritmo usato.
Gli algoritmi di MAC utilizzabili dal protocollo sono i seguenti:
♦
♦
♦
♦
♦
♦
hmac-sha (algoritmo con lunghezza dell’hash pari a 20 byte);
hmac-sha-96 (i primi 96-bit di HMAC-SHA, lunghezza dell’hash 12 byte);
hmac-md5 (algoritmo con lunghezza dell’hash pari a 16 byte);
hmac-md5-96 (i primi 96-bit di HMAC-MD5, lunghezza dell’hash 12 byte);
hmac-ripemd (algoritmo con lunghezza dell’hash pari a 20 byte);
hmac-ripemd-96 (i primi 96-bit di HMAC-RIPEMD, lunghezza dell’hash 12 byte).
hmac-sha
In questo caso la funzione di Hash utilizzata per il MAC è SHA. Il NIST (National Institute
of Standard and Technology) ha definito SHA la funzione di Hash standard per la firma di
documenti elettronici (DSS). SHA è una variante di MD5, con la differenza che produce un
valore di hash finale della lunghezza di 160-bit.
hmac-md5
53
In questo caso il MAC viene creato con la funzione di Hash MD5. Questa funzione è una
versione migliorata della precedente MD4 disegnata da Ron Rivest. MD5 riceve in input
blocchi di testo di 512-bit che divide ulteriormente in 16 sottoblocchi di 32-bit ciascuno;
l’output dell’algoritmo è un insieme di quattro blocchi di 32-bit per un totale di 128-bit di
valore hash ritornato.
hmac-ripemd
Questo algoritmo è anch’esso una variazione di MD4 ed è stato progettato per resistere agli
attacchi criptoanalitici più conosciuti. L’output prodotto dalla funzione di Hash ha lunghezza
128-bit.
Tutte le varianti degli algoritmi descritti che finiscono con “-96” non cambiano nella sostanza ma solo nella lunghezza del valore di hash ritornato dalla funzione.
III.2.5 La fase di scambio chiavi
La fase di scambio chiavi può essere divisa in tre diversi stadi: negoziazione degli algoritmi,
scambio delle chiavi e calcolo delle chiavi simmetriche.
1. Il primo stadio di questa fase inizia con l’invio della lista di algoritmi implementati o
permessi da entrambe le parti. La lista è costituita da una stringa per ogni categoria di algoritmi (simmetrici, asimmetrici, di integrità ecc.) e la stringa contiene i nomi degli algoritmi separati da una virgola, partendo da quello preferito.
Alcune implementazioni utilizzano l’invio di un pacchetto di “guess” subito dopo la trasmissione della lista degli algoritmi. Il pacchetto di “guess” è già un pacchetto dello stadio successivo (quello di scambio chiavi) e viene inviato da entrambe le parti prima di aver negoziato gli algoritmi. In sostanza entrambe le parti iniziano lo stadio di scambio chiavi presupponendo che anche l’altra parte stia usando lo stesso algoritmo asimmetrico preferito. Se entrambe le parti sono fortunate tutta la fase di negoziazione degli algoritmi viene saltata, con
risparmio di risorse in termini di tempi di trasmissione.
Il primo pacchetto inviato da entrambe la parti contiene i seguenti campi:
SSH_MSG_KEXINIT
cookie (random byte)
kex_algorithms
server_host_key_algorithms
encryption_algorithms_client_to_server
encryption_algorithms_server_to_client
mac_algorithms_client_to_server
mac_algorithms_server_to_client
compression_algorithms_client_to_server
compression_algorithms_server_to_client
languages_client_to_server
Languages_server_to_client
First_kex_packet_follows
54
SSH_MSG_KEXINIT
Questo è un messaggio che informa sull’inizio della fase di scambio chiavi.
cookie
Il cookie è un valore random di lunghezza 16 byte che viene generato per assicurarsi che le
chiavi di sessione e l’identificatore di sessione non siano determinati unicamente da una delle due parti (per ulteriori approfondimenti sul suo utilizzo vedi par. III.2.6).
kex_algorithms
In questo campo sono elencati gli algoritmi di scambio chiavi con la convenzione che il primo algoritmo è quello preferito. Se entrambe le parti hanno lo stesso algoritmo preferito si
potrà passare allo stadio successivo, altrimenti la scelta dell’algoritmo di scambio chiavi sarà
effettuata scandendo dalla lista del client un algoritmo alla volta e controllando che anche il
server supporti l’algoritmo.
Se non si trova nessun algoritmo della lista che soddisfa queste condizioni, la connessione
fallisce ed entrambe le parti si disconnettono. Tuttavia fino a questo momento l’unico metodo di scambio chiavi previsto dal protocollo è il diffie-hellman-group1-sha1 (vedi par.
III.2.4).
server_host_key_algorithms
Il server utilizza questo campo per specificare gli algoritmi per i quali ha almeno una chiave
di host (le chiavi di host possono essere più d’una); il client utilizza questo campo per dire al
server quali sono gli algoritmi che desidera accettare per lo scambio chiavi.
encryption_algorithms
In questa stringa sono elencate, in ordine di preferenza, le procedure di crittografia simmetrica accettate da entrambe le parti. L’algoritmo di crittografia scelto per entrambe le direzioni
di comunicazione è il primo della lista del client che si trova anche sulla lista del server. Se
non si riesce a trovare nessun algoritmo comune la comunicazione fallisce, ed entrambe le
parti si disconnettono.
mac_algorithms
In questa stringa sono elencate, in ordine di preferenza, le procedure di integrità dati accettate da entrambe le parti. L’algoritmo di MAC scelto per entrambe le direzioni di comunicazione è il primo della lista del client che si trova anche sulla lista del server. Se non si riesce
a trovare nessun algoritmo di integrità comune, la comunicazione fallisce ed entrambe le parti si disconnettono.
compression_algorithms
In questa stringa sono elencate, in ordine di preferenza, le procedure di compressione accettate da entrambe le parti. L’algoritmo di compressione scelto per entrambe le direzioni di
comunicazione è il primo della lista del client che si trova anche sulla lista del server. Se non
55
si riesce a trovare nessun algoritmo comune la comunicazione fallisce ed entrambe le parti si
disconnettono.
languages
In questo campo sono elencati, in ordine di preferenza, i linguaggi accettati. Se non ci sono
preferenze di linguaggio questo campo rimarrà vuoto.
first_kex_packet_follows
Questo campo è di tipo booleano (vero o falso) e segnala se seguirà o meno il pacchetto di
“guess”.
In realtà, come si vede dalla struttura del pacchetto, il protocollo prevede due aree di memoria distinte per i seguenti campi:
encryption_algorithms, mac_algorithms, compression_algorithms, languages. Queste aree di
memoria distinte servono a poter differenziare gli algoritmi che si vogliono utilizzare nelle
due direzioni di comunicazione (client ⇒ server e server ⇒ client). Tuttavia al momento tutte le implementazioni conosciute (compresa la nostra) considerano una sola serie di algoritmi
per entrambi i sensi di comunicazione. In pratica il pacchetto è costituito da due campi che
contengono la stessa stringa.
2. Dopo lo scambio del pacchetto iniziale, e dopo che tutti gli algoritmi sono stati negoziati
con successo, avrà inizio la procedura di scambio chiavi mediante l’algoritmo definito.
Questa procedura può anche implicare lo scambio di diversi pacchetti prima di terminare,
tutto dipende dall’algoritmo definito (per maggiori dettagli sullo stadio di scambio chiavi
vedi par.III.2.6).
3. Una volta che lo stadio di scambio chiavi è terminato con successo, sia il client che il
server sono a conoscenza di due valori comuni: un segreto k (nessun altro all’infuori del
client e del server potrà conoscere k) e un valore di hash H (questo valore di hash non è
segreto).
Questi due valori saranno utilizzati per il terzo ed ultimo stadio della fase di scambio chiavi:
il calcolo delle chiavi simmetriche. Questo stadio prevede che H sia usato come parte del
“challenge” d’autenticazione dell’host e che H e k insieme siano usati per il calcolo del vettore di inizializzazione (IV) della chiave di cifratura e della chiave del MAC.
In ogni algoritmo di scambio chiavi viene sempre definito un algoritmo di hash; tale algoritmo di hash deve anche essere usato per la derivazione delle chiavi. In seguito identificheremo questo algoritmo come HASH. I valori delle chiavi calcolati con HASH sono o seguenti:
♦ Vettore di inizializzazione (IV) da server a client: HASH(k || H || “A” || session_id) (dove
“A” è il carattere ASCII 65 e session_id è una stringa di dati casuali);
♦ Vettore di inizializzazione (IV) da client a server: HASH(k || H || “B” || session_id);
♦ Chiave simmetrica da client a server: HASH(k || H || “C” || session_id);
♦ Chiave simmetrica da server a client: HASH(k || H || “D” || session_id);
♦ Chiave di MAC da client a server: HASH(k || H || “E” || session_id);
♦ Chiave di MAC da server a client: HASH(k || H || “F” || session_id).
56
A seconda del tipo di algoritmo di hash utilizzato si avranno output di lunghezza diversa, tuttavia l’implementazione assicura che tutte le chiavi siano lunghe almeno 128-bit. Se la lunghezza della chiave richiesta è più grande dell’output della funzione di hash, quest’ultima
viene allungata concatenandola con un ulteriore hash ottenuto a partire da k, H e dalla chiave
già calcolata. Questo processo può essere ripetuto finché non viene raggiunta tutta la lunghezza della chiave. In altre parole una chiave di sessione “key” più lunga dell’output della
funzione HASH viene ottenuta così:
K1 = HASH(K || H || X || session_id)
K2 = HASH(K || H || X || K1)
K3 = HASH(K || H || X || K2)
…
(es. X può essere la lettera “A”)
key = K1 || K2 || K3 || …
Sebbene il protocollo preveda la creazione di sei chiavi distinte, in realtà tutte le implementazioni ne generano solo una per tipo (IV, simmetrica e MAC). In questo modo il traffico tra
client e server sarà sempre cifrato con la stessa chiave in entrambe le direzioni.
III.2.6 Scambio chiavi con l’algoritmo Diffie-Hellman
Diffie-Hellman è stato il primo algoritmo a chiave pubblica ad essere creato (per dettagli si
veda [DH76]). La sua sicurezza risiede nella difficoltà di calcolare logaritmi discreti in un
campo finito, rispetto alla facilità dell’operazione inversa (come argomento relativo vedi par.
II.2.5). L’algoritmo può essere utilizzato per la distribuzione di chiavi, ma non per cifrare,
decifrare o firmare messaggi.
Lo scambio di chiavi mediante Diffie-Hellman consente di ricavare un segreto comune definito da entrambe le parti; in altre parole non avviene che una delle due parti sceglie indipendentemente il segreto, ma entrambe concorrono in ugual misura alla sua definizione. In
questo protocollo lo scambio chiavi mediante Diffie-Hellman è unito all’autenticazione
dell’host tramite firma con chiave pubblica.
Prima di iniziare con la descrizione dell’algoritmo definiremo alcune convenzioni sulle abbreviazioni usate:
V_C = stringa che specifica la versione del protocollo usata dal client (vedi par. III.2.2);
V_S = stringa che specifica la versione del protocollo usata dal server (vedi par. III.2.2);
I_C = numero random (cookie) di 16 byte inviato dal client (vedi par. III.2.5);
I_S = numero random (cookie) di 16 byte inviato dal server (vedi par. III.2.5);
K_S = chiave pubblica del server;
p = numero primo molto grande;
q = generatore mod p.
57
Un “generatore mod p” è un numero q < p tale che per ogni 1 < b < p − 1 esiste sempre almeno un numero a che soddisfi la relazione:
q a ≡ b mod p .
Durante la fase di scambio chiavi con autenticazione si presume che entrambi client e server
conoscano tutte le variabili fin qui descritte (V_C, V_S, I_C, I_S, K_S, p, q).
Passiamo ora alla descrizione delle sequenze dello scambio chiavi con autenticazione
dell’host,
1) Il client genera un numero random x tale che 1 < x < q e calcola:
e = q x mod p .
Dopodiché il client invia al server e con I_C.
Client
Server
☺
I_C
e
2) Il server, dopo aver ricevuto e, genera un numero random y tale che 1 < y < q e calcola:
f = q y mod p ;
k = e y mod p ;
H = HASH (V_C || V_S || I_C || I_S || K_S || e || f || k) .
(“HASH” è la funzione di Hash SHA utilizzata per l’algoritmo Diffie-Hellman). A questo
punto il server ricava la firma s a partire da H tramite l’algoritmo asimmetrico definito (RSA
o DSA). Finite tutte queste operazioni di calcolo, il server invia al client I_S, K_S, f, s.
58
Client
Server
I_S
f
K_S
☺
s
3) Il client verifica che K_S appartiene veramente all’host remoto mediante un confronto
col database locale contenente le chiavi pubbliche degli host. Nel caso il client si stesse
connettendo al server per la prima volta, il protocollo permette di accettare la chiave anche senza verifica (i dettagli sulla sicurezza di questa opzione si trovano al par. III.1.2).
Dopo la verifica della chiave il client calcola:
k ′ = f x mod p ;
H = HASH(V_C || V_S || I_C || I_S || K_S || e || f || k) .
A questo punto il client può decifrare la firma s e verificarne la validità facendo un confronto
con H. D’altro canto la condivisione di un segreto comune è assicurata dall’uguaglianza tra
k’ e k, infatti secondo le formule matematiche abbiamo:
k ′ = f x mod p = ( q y ) x mod p = ( q x ) y mod p = e y mod p = k .
Alla fine della fase scambio chiavi il client avrà ottenuto l’autenticazione dell’host ed entrambe le parti saranno a conoscenza dei parametri H e k che serviranno per calcolare le
chiavi simmetriche (vedi par. III.2.5). Dopo di ciò potrà avere inizio la comunicazione cifrata mediante l’algoritmo definito.
Client
☺
Server
k
☺
H
k
H
Bisogna fare alcune considerazioni sui motivi che rendono necessario l’utilizzo di un “challenge” d’autenticazione così complicato come H. In realtà, per verificare l’identità del server
sarebbe sufficiente firmare un numero random, ma alcuni dei protocolli a chiave pubblica
hanno problemi di sicurezza con questo tipo di implementazione e quindi bisogna garantire
che il “challenge” non venga scelto da una sola delle parti (vedi par. II.2.5). Infatti, pur non
59
scendendo nei dettagli matematici, è importante ricordare che determinati “challenge” rendono insicuri i protocolli di alcuni algoritmi a chiave pubblica contro attacchi di tipo “Chosen Ciphertext Attack”. In questo senso è sempre buona norma usare la propria chiave privata solo per firmare “challenge” non del tutto sconosciuti e, come abbiamo visto, il “challenge” definito dal protocollo SSH è costituito da H, che è il valore di ritorno di una funzione di
Hash che prende in input parametri appartenenti sia al client che al server.
Riguardo agli algoritmi a chiave pubblica usati per l’autenticazione dell’host, bisogna ricordare che la maggior parte di quelli visti include un ulteriore “hashing” e “padding” del “challenge”. Per esempio l’algoritmo “ssh-dss” (DSA adattato ad SSH) specifica come funzione
di Hash da usare SHA, e questo significa che durante le operazioni di firma il “challenge”
che contiene H sarà ulteriormente utilizzato come parametro in input della funzione SHA.
Come abbiamo accennato all’inizio di questo paragrafo i due numeri p e q non sono segreti e
possono essere usati per più applicazioni. A questo proposito il protocollo SSH suggerisce
che le implementazioni facciano utilizzo dello standard “diffie-hellman-group1-sha1” per lo
scambio delle chiavi alla Diffie-Hellman. Questo standard prevede che il metodo appena descritto faccia uso della funzione di Hash SHA e dei seguenti valori di p e q:
p = 21024 − 2 960 − 1 + 2 64 ∗ floor( 2894 π + 129093) .
Che ha un valore decimale è pari a:
179769313486231590770839156793787453197860296048756011706444
423684197180216158519368947833795864925541502180565485980503
646440548199239100050792877003355816639229553136239076508735
759914822574862575007425302077447712589550957937778424442426
617334727629299387668709205606050270810842907692932019128194
467627007.
Mentre il generatore q è semplicemente uguale a:
q=
( p − 1)
.
2
III.2.7 Nuova negoziazione delle chiavi
Un attacco contro il quale il programma non può fare nulla è l’intercettazione passiva dei
pacchetti (vedi par. I.1.3). In sostanza SSH (come qualsiasi altro protocollo) non può impedire che qualcuno registri il traffico, seppur cifrato, che avviene tra client e server.
Ora supponiamo che due host distinti A e B aprano una connessione SSH tra loro e che lascino questa connessione aperta per un lungo lasso di tempo, diciamo alcuni giorni. Il fatto
che qualcuno sia in ascolto passivo del traffico tra A e B potrebbe essere irrilevante, visto
che le informazioni sono cifrate, ma non si può escludere che l’ascoltatore passivo registri il
traffico cifrato per riuscire in un secondo tempo, tramite qualche metodo di criptanalisi, a risalire al contenuto della comunicazione.
60
Per ovviare a questo inconveniente il protocollo prevede la rinegoziazione delle chiavi di
sessione dopo ogni ora di connessione o dopo la trasmissione di un Gigabyte di dati. Tale rinegoziazione viene effettuata senza che i protocolli che operano sopra al Transport Layer
Protocol siano coinvolti. In questo modo se la trasmissione cifrata dura molte ore, un possibile ascoltatore passivo dovrebbe essere in grado, per decifrare il contenuto dei pacchetti, di
calcolare tutte le chiavi utilizzate durante il tempo di trasmissione.
III.2.8 Conclusioni
Quando la fase di scambio chiavi viene terminata con successo e il Transport Layer Protocol
è stato creato, il client inoltra al server una richiesta dei servizi che vuole ricevere. Per ulteriori dettagli riguardo a questo protocollo si veda [SSH-TRAN].
61
III.3 Il protocollo di autenticazione (Authentication Protocol)
III.3.1 Introduzione
L’Authentication Protocol è un protocollo d’autenticazione per l’utente che opera al di sopra
del Transport Layer Protocol. Il protocollo d’autenticazione suppone che l’integrità dati e la
loro riservatezza venga assicurata dal protocollo sottostante, ammettendo così lo scambio di
dati “sensibili” tra client e server (informazioni sull’utente, password in chiaro ecc.).
Quando l’Authentication Protocol entra in funzione, il protocollo di trasporto sottostante gli
passa l’identificatore di sessione H. Come abbiamo visto al par. III.2.6, H identifica univocamente la sessione ed è usato come parte del “challenge” per provare la paternità della
chiave privata del server.
I metodi d’autenticazione per ora previsti dal protocollo sono tre: a chiave pubblica, con
password e “hostbased”. Tuttavia, il protocollo prevede la possibilità di aggiungere nuovi
metodi di autenticazione oltre a quelli già esistenti.
III.3.2 Struttura del protocollo d’autenticazione
La struttura del protocollo stabilisce che sia il server a guidare l’autenticazione comunicando
ogni volta al client i metodi d’autenticazione permessi o rifiutati. Mentre il client ha la capacità di provare i metodi d’autenticazione elencati dal server in qualsiasi ordine esso desideri.
Questa scelta progettuale lascia al server completa autonomia decisionale sui metodi
d’autenticazione concessi, ma, nello stesso tempo, dà al client la possibilità di provare i metodi supportati in ordine di preferenza.
Il server dovrebbe avere un tempo di timeout oltre il quale la connessione viene interrotta e il
protocollo consiglia di non concedere timeout di durata superiore ai dieci minuti. Inoltre, anche il numero di tentativi d’autenticazione dovrebbe essere limitato, e viene consigliato che
questo numero non superi mai i venti tentativi per sessione. Se il numero di tentativi
d’autenticazione supera la soglia definita, il server dovrebbe chiudere la connessione.
Le richieste d’autenticazione che il client invia al server possono essere più d’una e ogni volta che una richiesta d’autenticazione viene inviata il server riceve un pacchetto formato dai
seguenti campi:
SSH_MSG_USERAUTH_REQUEST
user_name (in ISO-10646 UTF-8 encoding)
service_name (in US-ASCII)
method_name (in US-ASCII)
(il resto del pacchetto dipende dal
metodo di autenticazione)
…
Il messaggio "SSH_MSG_USERAUTH_REQUEST” informa il server che il client ha inviato un pacchetto contenente una richiesta d’autenticazione. Nel campo “user_name” viene
62
scritto il nome dell’utente (più precisamente dell’account dell’utente) che vuole autenticarsi
sul server remoto. Se il nome dell’utente non è tra quelli registrati sul server, questo non deve accettare la richiesta d’autenticazione e chiudere la connessione. Il campo “service_name” contiene il nome del servizio richiesto dall’utente subito dopo l’autenticazione (es.
“ssh-connection”). Se il client richiede un servizio non disponibile il server può scegliere di
disconnettersi oppure di passare al successivo metodo d’autenticazione qualora ve ne fosse
uno. Infine nel campo “method_name” viene specificato il nome del metodo
d’autenticazione utilizzato per quell’utente (password, chiave pubblica ecc.).
Se il server rifiuta la richiesta d’autenticazione, il messaggio inviato al client sarà del tipo:
SSH_MSG_USERAUTH_FAILURE
authentications_that_can_continue
partial_success
Il messaggio “SSH_MSG_USERAUTH_FAILURE” informa il client che il server ha rifiutato la precedente richiesta d’autenticazione, oppure che devono essere effettuati ulteriori
metodi d’autenticazione prima che l’utente possa essere riconosciuto valido. La stringa “authentications_that_can_continue” elenca i nomi degli ulteriori metodi da usare per la richiesta d’autenticazione dell’utente. Infine il campo “partial_success” contiene un valore booleano. Il suo valore deve essere impostato a TRUE se il pacchetto viene inviato dal server come risposta positiva ad un tentativo d’autenticazione con uno dei metodi richiesti.
Quando tutti i metodi di autenticazione previsti sono stati superati con successo, il server riconosce l’utente come valido e risponde al client con il seguente pacchetto ad un solo campo:
SSH_MSG_USERAUTH_SUCCESS
E’ da notare che nel caso il server sia configurato per più metodi d’autenticazione successivi,
questo pacchetto viene inviato una sola volta dopo che tutti i metodi d’autenticazione hanno
avuto successo. Le eventuali richieste d’autenticazione inviate dopo aver ricevuto questo
messaggio, saranno ignorate dal server in modo silenzioso.
Tutti i pacchetti successivi a questo messaggio sono passati al servizio specificato nel campo
“service_name” che opera sopra l’Authentication Protocol.
III.3.3 Metodo d’autenticazione con chiave pubblica
L’autenticazione con chiave pubblica segue la stessa filosofia usata per l’autenticazione
dell’host descritta in dettaglio al par. III.2.6. In questo caso, però, i ruoli sono invertiti:
l’utente del client utilizza la sua chiave privata per firmare un “challenge” noto ad entrambi,
quindi invia al server il “challenge” in chiaro insieme con la firma generata. Il server ricava
il “challenge” dalla firma tramite l’algoritmo asimmetrico convenuto e poi lo confronta con
63
il “challenge” ricevuto in chiaro se i due corrispondono l’utente viene considerato autenticato. Di solito la chiave privata dell’utente viene mantenuta cifrata sull’host del client e
l’utente deve digitare una password affinché questa possa essere utilizzata per la firma del
“challenge”.
E’ da notare che i processi matematici per la firma con chiave pubblica/privata sono piuttosto dispendiosi in termini di tempi di calcolo, quindi può essere utile che il client verifichi la
capacità del server di gestire correttamente il metodo d’autenticazione a chiave pubblica
prima di firmare il “challenge” da inviare. Tuttavia, nel caso in cui il server e il client si trovino fisicamente molto lontani (es. uno in Italia e l’altro in Australia), può essere meno dispendioso l’invio di un pacchetto che contenga già il “challange” firmato.
Il pacchetto che viene inviato dal client per effettuare l’autenticazione con chiave pubblica/privata contiene i seguenti campi:
SSH_MSG_USERAUTH_REQUEST
user_name = “user”
service_name = “service”
method_name = “publickey”
include_sign = TRUE
public_key_algorithm_name
public_key_blob
signature
I primi quattro campi li abbiamo già commentati nel par. III.3.2. Oltre a questi il pacchetto
contiene il campo “include_sign” che ha un valore di tipo booleano. Questo valore viene impostato a TRUE se nel pacchetto viene inclusa la firma del “challenge”. In questo caso al
pacchetto viene aggiunto il campo “signature” contenente la firma del “challenge” e il “challenge” è essere formato dalle seguenti componenti: l’identificatore di sessione H e il payload
del pacchetto stesso senza il campo “signature”.
Nel caso in cui il campo “include_sign” fosse impostato a FALSE il campo “signature” non
viene aggiunto al pacchetto e il protocollo si aspetta un pacchetto successivo dello stesso tipo
contenente la firma del “challenge”.
Il campo “public_key_algorithm_name” contiene il nome dell’algoritmo asimmetrico da utilizzare per la firma del “challenge”. Mentre la stringa “public_key_blob” contiene la chiave
pubblica dell’utente più gli eventuali certificati associati. I certificati possono dare informazioni sul tipo e lo stato delle chiavi (es. la chiave è in formato PGP e la sua validità è scaduta
ecc.)
Se l’algoritmo asimmetrico specificato dal client nel campo “public_key_algorithm_name”
non è integrato sul server la richiesta d’autenticazione viene rifiutata e il server risponde con
un pacchetto del tipo “SSH_MSG_USERAUTH_FAILURE” descritto al par. III.3.2.
Se il metodo d’autenticazione e l’algoritmo di firma sono accettati, il server risponde al
client con un pacchetto avente il seguente formato:
SSH_MSG_USERAUTH_PK_OK
64
public_key_algorithm_name
public_key_blob
dove gli ultimi due campi contengono esattamente le informazioni inviate dal client nel pacchetto di richiesta d’autenticazione.
Quando il server riceve questo pacchetto deve per prima cosa controllare che la chiave fornita sia adatta per l’autenticazione e poi che la firma sia corretta. Se entrambe le verifiche hanno successo, l’utente viene considerato autenticato. In questo caso, se non sono richiesti altri
metodi d’autenticazione, il server invierà al client un pacchetto del tipo
SSH_MSG_USERAUTH_ SUCCESS; se invece sono richiesti ulteriori metodi oltre a quello
a chiave pubblica il server risponderà con un pacchetto del tipo
SSH_MSG_USERAUTH_FAILURE con il campo “partial_success” = TRUE (vedi par.
III.3.2).
III.3.4 Metodo d’autenticazione con password
Tutte le implementazioni del protocollo dovrebbero integrare l’autenticazione con password,
perché questo tipo d’autenticazione è di gran lunga il più diffuso. Il protocollo non specifica
come debba avvenire l’autenticazione sul server remoto, ma fornisce solo alcune indicazioni
sulle modalità con cui il client deve inviare le informazioni dell’utente al server.
Quando il client vuole fare uso dell’autenticazione mediante password invia al server il seguente pacchetto:
SSH_MSG_USERAUTH_REQUEST
user_name = “user”
service_name = “service”
method_name = “password”
password_change = FALSE
plaintext_password (ISO-10646 UTF-8)
La prima parte del pacchetto è uguale a quella descritta al par. III.3.2, mentre gli ultimi due
campi sono definiti nel seguente modo: “password_change” contiene valori di tipo booleano
e viene impostato a TRUE solo nel caso che si voglia cambiare la password dell’utente, mentre “plaintext_password” contiene la password dell’utente non cifrata; secondo le specifiche
del formato di testo ISO-10646 UTF-8.
Anche se potrebbe sembrare il contrario, scrivere la password dell’utente in chiaro non costituisce un problema di sicurezza. Infatti tutto il contenuto del pacchetto inviato viene cifrato
tramite il Transport Layer Protocol impedendo a chiunque, all’infuori del server, di leggere
la password dell’utente. Naturalmente bisogna che sia client che server verifichino la capacità del protocollo sottostante di garantire la cifratura dei pacchetti.
Normalmente il server risponde a questo pacchetto con un messaggio del tipo
SSH_MSG_USERAUTH_ SUCCESS in caso d’autenticazione avvenuta, oppure con
65
SSH_MSG_USERAUTH_FAILURE in caso di fallimento o di necessità che altri metodi
d’autenticazione vengano superati (vedi par. III.3.2).
Tuttavia il server potrebbe anche rispondere con un pacchetto avente il seguente formato:
SSH_MSG_USERAUTH_PASSWD_CHANGEREQ
prompt (ISO-10646 UTF-8)
language_tag
In questo caso il server comunica al client che l’utente del quale si vuole ottenere
l’autenticazione ha la password scaduta. I campi di questo pacchetto comunicano il “prompt”
da utilizzare per l’immissione di una nuova password e il tipo di linguaggio utilizzato.
Una volta ricevuto questo messaggio, il client chiede all’utente di inserire una nuova password, e comunica il cambiamento di password al server tramite un pacchetto del tipo:
SSH_MSG_USERAUTH_REQUEST
user_name = “user”
service_name = “service”
method_name = “password”
password_change = TRUE
plaintext_old_password (ISO-10646 UTF-8)
plaintext_new_password (ISO-10646 UTF-8)
Il contenuto degli ultimi due campi è abbastanza chiaro e non richiede ulteriori spiegazioni.
Dopo aver ricevuto questo messaggio il server può rispondere al client con tre tipi di messaggi: SSH_MSG_USERAUTH_SUCCESS, SSH_MSG_USERAUTH_FAILURE oppure
con il messaggio SSH_MSG_USERAUTH_PASSWD_CHANGEREQ. Il significato di queste diverse risposte è il seguente:
SSH_MSG_USERAUTH_ SUCCESS
La password è stata cambiata e l’autenticazione dell’utente è terminata con successo.
SSH_MSG_USERAUTH_FAILURE (col campo “partial_success” = TRUE)
La password è stata cambiata, ma il server richiede altri metodi d’autenticazione.
SSH_MSG_USERAUTH_FAILURE (col campo “partial_success” = FALSE)
La password non è stata cambiata. E’ possibile che il server non consenta il cambio della
password o che la vecchia password non sia valida.
SSH_MSG_USERAUTH_PASSWD_CHANGEREQ
La password non è stata cambiata perché la nuova password non è adatta (es. troppo corta,
troppo facile da indovinare ecc.)
Un’ultima considerazione sulla possibilità del cambio della password, è che questa dovrebbe
essere disabilitata nel caso non vi sia garanzia d’integrità (MAC) da parte del protocollo sottostante. Questo per impedire che un possibile intruso nella comunicazione modifichi il testo
66
cifrato contenente la password, rendendo i nuovi dati per l’autenticazione non utilizzabili (attacco di tipo “denial of service”).
III.3.5 Metodo d’autenticazione hostbased
Alcuni server di rete permettono un tipo di autenticazione basato sul nome dell’utente e sul
nome dell’host dal quale l’utente tenta di connettersi. Questo tipo di autenticazione non garantisce un livello di sicurezza molto elevato, eppure può essere molto utile per certi tipi di
ambienti.
La struttura di questo tipo di autenticazione è piuttosto simile a “rhosts” e “hosts.equiv” di
UNIX; tuttavia l’identità dell’host da cui si collega il client viene controllata con maggiore
scrupolo. Questo tipo di autenticazione stabilisce che il client utilizzi la chiave privata del
suo host per firmare un “challenge” da inviare al server il quale controllerà la firma con la
chiave pubblica dell’host del client. Una volta che l’identità dell’host del client è stata verificata, l’autorizzazione viene concessa in base al nome dell’utente e all’indirizzo dell’host del
client. Il client richiede questo tipo d’autenticazione inviando al sever un pacchetto del tipo:
SSH_MSG_USERAUTH_REQUEST
user_name = “user”
service_name = “service”
method_name = “hostbased”
public_key_algorithm_name_for_host_key
public_host_key_blob
client_host_name
client_user_name_on_the_remote_host
signature
Come al solito i primi quattro campi sono quelli già descritti al par. III.3.2, mentre nel campo
“public_key_algorithm_name_for_host_key” vengono specificati i nomi degli algoritmi asimmetrici utilizzati per la firma del “challenge”. La lista di questi algoritmi è definita al par.
III.2.5. Il campo “public_host_key_blob” include la chiave pubblica dell’host del client e i
suoi eventuali certificati. La stringa “client_host_name” specifica il nome dell’host del client
(es. prix7.pr.infn.it), mentre nella stringa “client_user_name_on_the_remote_host” viene
scritto il nome dell’account remoto dell’utente che vuole autenticarsi.
Infine il campo “signature” contiene la firma con chiave privata dell’host del client dei seguenti elementi: l’identificatore di sessione H e il payload del pacchetto stesso senza il campo “signature”.
Come per l’autenticazione dell’host descritta nel par. III.1.2, e’ importante che il server controlli che la chiave pubblica dell’host contenuta nel campo “public_host_key_blob” sia identica a quella contenuta nel database locale relativamente al nome specificato in
“client_host_name”. Inoltre, il protocollo raccomanda che si faccia sempre un controllo tra
l’indirizzo IP del client ottenuto dalla rete e quello specificato nel pacchetto. Questo per evitare l’accesso di host indesiderati nel caso di compromissione della chiave pubblica.
67
III.3.6 Conclusioni
Molti altri metodi d’autenticazione sono possibili oltre a quelli descritti, tutti con caratteristiche di sicurezza diverse. Spetta poi alla policy locale del server decidere quali metodi (o
combinazioni di metodi) è bene accettare per ogni utente.
Inoltre, sarebbe opportuno includere messaggi di “debug” molto generici durante la fase
d’autenticazione dell’utente. Infatti questi messaggi possono costituire una sorprendente fonte di informazioni sugli utenti e sul server nel caso di un tentativo di attacco. Per ulteriori
dettagli riguardo a questo protocollo si veda [SSH-AUTH].
68
III.4 Il protocollo di connessione (Connection Protocol)
III.4.1 Introduzione
Il Connection Protocol è stato progettato per operare sopra l’Authentication Protocol. Questo protocollo permette di aprire una shell remota, di eseguire comandi sul server, di effettuare il forwarding di altre connessioni di tipo TCP/IP e il forwarding dell’ambiente grafico di
UNIX chiamato X11. Il Connection Protocol è uno dei possibili servizi che vengono richiesti subito dopo l’autenticazione dell’utente e viene identificato col nome “ssh-connection”.
Questo nome viene scritto nel campo “service_name” del pacchetto di richiesta
d’autenticazione inviato dal client al server (vedi par. III.3.2).
Come vedremo, la struttura di questo protocollo è basata su un’unica connessione divisa in
più canali logici. I diversi canali logici permettono di distribuire i dati della comunicazione
in più flussi distinti ed indipendenti.
III.4.2 Il meccanismo dei canali logici
Tutti i tipi di dati trasmessi dal Connection Protocol sono divisi in canali logici. Se per esempio il client volesse aprire una shell e contemporaneamente effettuare il forwarding
dell’X11, il protocollo creerebbe due canali logici distinti che condividono la stessa connessione sicura stabilita dal Transport Layer Protocol.
I canali logici possono essere aperti da entrambi i lati della comunicazione e su entrambi i
lati sono identificati con un numero diverso. Per ogni canale aperto, entrambe le parti effettuano un controllo del flusso; le due parti non possono inviare dati sul canale fintanto che
non ricevono un messaggio che specifica la disponibilità di spazio sul “buffer di ricezione”
dall’altra parte (il buffer di ricezione è un’area di memoria condivisa da tutti i canali).
Quando una delle due parti intende aprire un nuovo canale logico, registra il numero di identificazione per il canale e invia il seguente messaggio all’altra parte:
SSH_MSG_CHANNEL_OPEN
channel_type (restricted to US-ASCII)
sender_channel
initial_window_size
maximum_packet_size
(il resto del pacchetto dipende dal
tipo di canale logico)
…
Il campo “channel_type” indica il tipo di canale che si vuole aprire, “sender_channel”
specifica il numero di identificazione registrato da inviare all’altro capo della comunicazione, “initial_window_size” determina la dimensione massima del buffer di ricezione di chi sta
inviando il pacchetto e “maximum_packet_size” definisce la dimensione massima dei pac-
69
chetti (per esempio una delle due parti potrebbe richiedere l’uso di pacchetti più piccoli per
ottenere migliori prestazioni su comunicazioni lente).
Se l’altra parte può accettare la richiesta d’apertura del canale logico, risponderà con il seguente pacchetto:
SSH_MSG_CHANNEL_OPEN_CONFIRMATION
recipient_channel
sender_channel
initial_window_size
maximum_packet_size
(il resto del pacchetto dipende
dal tipo di canale logico)
…
Il campo “recipient_channel” contiene il numero d’identificazione del canale remoto ricevuto col pacchetto d’apertura; in sostanza il valore di “recipient_channel” è uguale a quello di
“sender_channel” scritto nel precedente pacchetto.
Se invece il destinatario del messaggio d’apertura non è in grado di gestire il tipo di canale
richiesto, risponderà semplicemente con un pacchetto avente il seguente formato:
SSH_MSG_CHANNEL_OPEN_FAILURE
recipient_channel
reason_code
additional_information (ISO-10646 UTF-8)
language_tag
Il campo “reason_code” contiene la motivazione per cui il canale non può essere aperto. La
motivazione può essere una delle seguenti:
♦
♦
♦
♦
SSH_OPEN_ADMINISTRATIVELY_PROHIBITED
SSH_OPEN_CONNECT_FAILED
SSH_OPEN_UNKNOWN_CHANNEL_TYPE
SSH_OPEN_RESOURCE_SHORTAGE
Il campo “additional_information” viene usato per rivelare all’utente ulteriori informazioni
sul fallimento dell’apertura del canale.
Quando un nuovo canale è stato aperto ed entrambe le parti hanno ben definito il numero
d’identificazione per tale canale, è possibile utilizzare quel canale per inviare dati o pacchetti
con richieste specifiche. Vedremo più avanti che alcune richieste d’apertura di un nuovo canale possono essere inviate solo tramite un canale già aperto. Tutti i tipi di richieste relative
ad un canale sono inviate tramite pacchetti aventi il seguente formato:
70
SSH_MSG_CHANNEL_REQUEST
recipient_channel
requested_type
want_reply
(il resto del pacchetto dipende
dal tipo di canale logico)
…
La stringa “requested_type” specifica il tipo di richiesta; un esempio di richiesta potrebbe essere “pty-req” (pseudo terminal) dove il client comunica al server che intende aprire una sessione interattiva e che ha bisogno di un’emulazione di terminale. Chiaramente questo tipo di
richiesta ha senso solo se l’implementazione del server è scritta per sistemi operativi di tipo
UNIX. Il campo “want_replay” è di tipo booleano; se il suo valore viene impostato a FALSE
nessuna risposta seguirà questo pacchetto, altrimenti il destinatario del pacchetto può rispondere coi messaggi:
SSH_MSG_CHANNEL_SUCCESS, SSH_MSG_CHANNEL_FAILURE oppure un messaggio specifico relativo alla richiesta fatta. I primi due pacchetti sono del tipo:
SSH_MSG_CHANNEL_SUCCESS
recipient_channel
SSH_MSG_CHANNEL_FAILURE
recipient_channel
Quando una delle due parti non ha intenzione di trasmettere ulteriori dati su un canale, dovrebbe inviare il seguente messaggio:
SSH_MSG_CHANNEL_EOF
recipient_channel
Questo tipo di messaggio non implica la chiusura automatica del canale logico, ma il canale
rimane aperto e i dati possono solo essere inviati solo nell’altra direzione. Quando una delle
due parti intende chiudere il canale definitivamente invia il seguente messaggio:
SSH_MSG_CHANNEL_CLOSE
recipient_channel
e l’altra parte che riceve questo messaggio dovrà rispondere con uno uguale. Il canale viene
considerato chiuso quando entrambe le parti hanno ricevuto il messaggio di chiusura.
III.4.3 Apertura di sessioni interattive
Quando si parla di sessione interattiva si intende l’esecuzione remota di un programma. Questo programma potrebbe essere una shell, un’applicazione, un comando del sistema oppure
un subsystem. Ogni sessione interattiva è associata ad un canale logico per la trasmissione
dei dati, come vedremo, tale canale logico è usato per inviare anche altre richieste. Infatti la
71
maggior parte dei servizi offerti dal protocollo usa il canale di sessione come canale di appoggio per le richieste d’apertura dei propri canali logici.
Generalmente l’apertura di una sessione interattiva viene iniziata dal client inviando al server
un pacchetto avente il seguente formato:
SSH_MSG_CHANNEL_OPEN
channel_type = “session”
sender_channel
initial_window_size
maximum_packet_size
Tutte le implementazioni del client dovrebbero essere in grado di scartare questo tipo di richiesta d’apertura, questo al fine di rendere più difficile ogni tipo di attacco da parte di un
server corrotto che voglia ottenere informazioni e privilegi sull’host del client.
Se il server risponde positivamente a questa richiesta d’apertura di sessione da parte del
client il canale viene stabilito, e dopo che questo canale è stato stabilito possono essere inviati una serie di pacchetti contenenti richieste specifiche. Per brevità non descriveremo tutte le
possibili richieste relative al canale di sessione, ma ci concentreremo solo su quelle più significative. Ci sono tre tipi di richieste che sono mutuamente esclusive e che vengono inviate
alla fine di ogni altro tipo di richiesta: l’esecuzione di una shell remota, l’esecuzione di un
comando e l’esecuzione di un subsystem. Nel primo caso il client invia un pacchetto avente
il seguente formato:
SSH_MSG_CHANNEL_REQUEST
recipient_channel
requested_type = “shell”
want_reply
Questo messaggio richiederà al server di aprire una shell secondo le specifiche dell’utente
(generalmente i sistemi UNIX definiscono il tipo di shell per ogni utente nel file /etc/passwd)
o secondo le specifiche del sistema se non esistono più tipi di shell.
Nel caso il client non abbia intenzione di aprire una shell, ma voglia solo far eseguire un comando al server, il pacchetto di richiesta sarà il seguente:
SSH_MSG_CHANNEL_REQUEST
recipient_channel
requested_type = “exec”
want_reply
command
Questo messaggio chiede al server di eseguire il comando specificato nella stringa “command”; l’implementazione del server deve essere studiata in modo da prevenire l’esecuzione
di comandi non autorizzati.
L’accezione di subsystem non ha un significato ben preciso; generalmente i subsystem sono
dei programmi che si occupano di gestire il trasferimento di file in modo sicuro (sftp ecc.). Il
client può richiedere al server l’attivazione di un subsystem tramite un pacchetto del tipo:
72
SSH_MSG_CHANNEL_REQUEST
recipient_channel
requested_type = “subsystem”
want_reply
sybsystem_name
L’esecuzione di una di queste richieste non dovrebbe bloccare lo svolgimento del protocollo,
i nuovi processi dovrebbero essere fatti in modo da ridirigere il loro input ed output sul canale di sessione e quindi sul canale cifrato.
III.4.4 Apertura di canali logici per l’X11
L’X11 è lo standard grafico di tutti i sistemi operativi UNIX ed è tuttora uno dei più sofisticati tra quelli esistenti. Questo standard gestisce la visualizzazione della grafica su un modello di tipo client/server. Molto schematicamente si può dire che un’applicazione che utilizza
la grafica in UNIX si comporta come un client che interroga un server: il client è
l’applicazione stessa, mentre il server è un demone che riceve la connessione
dall’applicazione e si occupa di gestire la visualizzazione su terminale. In altre parole quando un’applicazione grafica (es. xemacs, xedit ecc.) viene fatta partire da una shell di UNIX,
questa invia al server X tutte le informazioni necessarie per la visualizzazione delle finestre.
Il server rimane in ascolto su una determinata porta di rete e quando riceve le informazioni
dall’applicazione le invia al video in maniera corretta.
Questo tipo di impostazione può sembrare un po’ ridondante per un utente che debba aprire
una sessione grafica locale, però una gestione così strutturata rende molto più semplice il
controllo della grafica via rete. Infatti un terminale non intelligente (che integri almeno un
server X) collegato tramite la rete ad un host remoto, può visualizzare in locale delle applicazioni grafiche che in realtà vengono eseguite e gestite sul server remoto.
Un client SSH può chiedere il forwarding dell’X11 inviando sul canale di sessione un pacchetto di questa forma:
SSH_MSG_CHANNEL_REQUEST
recipient_channel
requested_type = “X11-req”
want_reply
single_connection
x11_authentication_protocol
x11_authentication_cookie
x11_screen_number
I primi quattro campi del pacchetto sono già stati commentati nel par. III.4.2. Il campo
“single_connection” è di tipo booleano; il suo valore viene impostato a TRUE se si desidera
l’apertura di un solo canale di forwarding per l’X11. I rimanenti campi sono specifici della
struttura del protocollo X11.
73
Ogni volta che un’applicazione remota vuole aprire una finestra, il server invia al client la
richiesta d’apertura di un canale logico per l’X11 tramite un pacchetto avente il seguente
formato:
SSH_MSG_CHANNEL_OPEN
channel_type = “x11”
sender_channel
initial_window_size
maximum_packet_size
originator_address (es. 192.34.7.38)
originator_port (es. 6057)
I primi cinque campi del pacchetto sono già stati commentati nel par. III.4.2. Il campo “originator_address” specifica l’indirizzo IP dell’host da dove viene inviato il pacchetto e la
stringa “originator_port” definisce la porta di ascolto del proxy SSH al server X locale.
Il client dovrebbe rispondere a questo messaggio con un pacchetto del tipo:
SSH_MSG_CHANNEL_OPEN_CONFIRMATION nel caso possa accettare l’apertura del
canale logico per l’X11, oppure col messaggio SSH_MSG_CHANNEL_OPEN_FAILURE
in caso contrario. Le implementazioni del protocollo dovrebbero rifiutare ogni richiesta
d’apertura d’un canale per l’X11 se questa non è preceduta da un pacchetto di richiesta di
forwarding dell’X11. Tutti i canali logici aperti sono indipendenti dalla sessione, e la chiusura del canale di sessione non implica la chiusura automatica dei canali dell’X11. Tuttavia
dopo la chiusura del canale di sessione non è più possibile aprire nessun altro canale di forwarding per l’X11.
Infine è bene ricordare che il forwarding dell’X11 ha senso solo per quei client installati su
host che integrano un server X funzionante al momento della connessione.
III.4.5 Apertura di canali logici per l’autentication agent
Come abbiamo visto nel par. III.3, tutti i metodi d’autenticazione dell’utente hanno bisogno
d’una password per poter funzionare. Infatti, anche il metodo d’autenticazione con chiave
pubblica richiede sempre che l’utente immetta una password per utilizzare la propria chiave
privata per firmare il “challenge”. Il fatto di dover immettere una password ogni volta che si
vuole effettuare una connessione può essere un po’ frustrante, soprattutto per quegli amministratori di sistema che devono effettuare molte connessioni al giorno su tante macchine diverse.
A questo scopo, l’implementazione UNIX del protocollo include un programma chiamato
“Autentication Agent” che è in grado d’automatizzare l’operazione d’immissione password.
Questo programma è stato progettato per effettuare l’autenticazione di più connessioni in cascata e per questo ha bisogno di un suo canale di “forwarding” delle informazioni relative
all’autenticazione. Il protocollo stabilisce che la richiesta d’apertura del canale di forwarding
per l’agent debba essere inviata tramite il canale di sessione già aperto tramite un pacchetto
di questo tipo:
74
SSH_MSG_CHANNEL_REQUEST
recipient_channel
requested_type = “auth-agent-req”
want_reply
I campi di questo pacchetto sono già stati commentati al par. III.4.2.
Il fatto che venga inviata una richiesta di forwarding dell’agent non significa che subito dopo
venga aperto un canale logico per lo scambio dati sull’autenticazione. Con questo tipo di
pacchetto il client sta solo comunicando al server la possibilità futura di aprire dei canali per
il forwarding dell’agent. Se il campo “want_replay” è impostato a TRUE, il server può rispondere a questa richiesta con due tipi di messaggi:
SSH_MSG_CHANNEL_SUCCESS o SSH_MSG_CHANNEL_FAILURE (abbiamo già visto questi messaggi al par. III.4.2). Tuttavia l’implementazione del client può anche trasmettere ulteriori messaggi senza aspettare la risposta, positiva o negativa, al suo messaggio di
forwarding.
Quando una delle due parti intende richiedere una connessione all’autentication agent viene
inviato il seguente messaggio:
SSH_MSG_CHANNEL_OPEN
channel_type = “auth-agent”
sender_channel
initial_window_size
maximum_packet_size
Il ricevente di questo messaggio dovrebbe rispondere con un pacchetto del tipo
SSH_MSG_CHANNEL_OPEN_CONFIRMATION nel caso possa accettare l’apertura di
tale canale logico, oppure col messaggio SSH_MSG_CHANNEL_OPEN_FAILURE in caso
contrario. Per problemi di sicurezza le implementazioni del protocollo devono rifiutare ogni
richiesta d’apertura del canale che non sia stata preceduta da una richiesta di agent forwarding.
III.4.6 TCP/IP port forwarding
Il TCP/IP port forwarding è una caratteristica del protocollo che permette di convogliare attraverso la connessione cifrata qualsiasi flusso dati indirizzato ad una porta di rete remota
(sull’host del server) o locale (sull’host del client).
Per il forwarding di una porta di rete remota, il client deve prima comunicare al server
l’indirizzo e la porta su cui ascoltare.
Questo tipo di richiesta d’ascolto non viene fatta tramite un canale di appoggio (come il canale di sessione), ma tramite una “richiesta globale”. Le “richieste globali” vengono inviate
al server indipendentemente da qualsiasi canale aperto e hanno questo formato:
75
SSH_MSG_GLOBAL_REQUEST
request_name = “tcpip-forward”
want_replay
address_to_bind (e.g. 0.0.0.0)
port_number_to_bind (e.g. 314)
I campi “address_to_bind” e “port_number_to_bind” specificano l’indirizzo IP e la porta sui
quali il server deve aprire un socket e rimanere in ascolto. Generalmente “address_to_bind”
viene impostato a “0.0.0.0” se si vuole che il socket possa ricevere connessioni su tutti gli
indirizzi del server (ammesso che il server abbia più di un indirizzo IP).
Le diverse implementazioni dovrebbero permettere forwarding di porte privilegiate solo agli
utenti autenticati con privilegi d’amministratore (il concetto di porta di rete privilegiata al
momento viene utilizzato solo in UNIX; le porte privilegiate sono quelle inferiori alla porta
1024). Questo tipo di “richiesta globale” può essere inviata solo dal client verso il server e le
implementazioni del client dovrebbero rifiutare messaggi di questo tipo per motivi di sicurezza.
Una richiesta di port forwarding come quella appena vista può essere cancellata dal client
con il seguente messaggio:
SSH_MSG_GLOBAL_REQUEST
request_name = “cancel-tcpip-forward”
want_replay
address_to_bind (e.g. 127.0.0.1)
port_number_to_bind (e.g. 314)
E’ però da notare che nel tempo intercorso tra il messaggio di richiesta e quello di cancellazione, il server può ricevere dei messaggi d’apertura d’un canale di trasmissione dati per il
port forwarding (vedremo questo tipo di messaggi tra poco).
Come si può vedere dalla struttura dei pacchetti, tutte le richieste globali contengono un
campo “want_replay” di tipo booleano. Se il valore di questo campo è impostato a TRUE il
client si aspetta una risposta alla richiesta globale, e se la risposta è positiva il server invia al
client un messaggio di tipo:
SSH_MSG_REQUEST_SUCCESS
Se invece il server non riconosce tale richiesta globale, invia al client un messaggio del tipo:
SSH_MSG_REQUEST_FAILURE
Quando arriva una connessione sulla porta per la quale il client ha richiesto il forwarding, il
server invia al client il seguente pacchetto di richiesta d’apertura del canale di forwarding:
76
SSH_MSG_CHANNEL_OPEN
channel_type = “forwarded-tcpip”
sender_channel
initial_window_size
maximum_packet_size
address_that_was_connected
port_that_was_connected
originator_IP_address
originator_port
I primi cinque campi del pacchetto sono già stati commentati nel par. III.4.2.
Le connessioni che arrivano al server sulla porta “forwardata” dal client (specificata nel
campo “port_number_to_bind” del pacchetto di richiesta globale), possono giungere da
qualsiasi punto della rete.
I campi “address_that_was_connected” e “port_that_was_connected” informano il client
dell’indirizzo IP e della porta di origine di chi si è connesso al server sulla porta “forwardata”. Il campo “originator_IP_address” definisce da quale indirizzo del server arrivano i dati
(nel caso il server supporti più indirizzi IP) e “originator_port” specifica su quale porta “forwardata” è arrivata la connessione (questo campo è utile nel caso il client abbia richiesto il
forwarding remoto di più porte). L’implementazione del client deve essere fatta in modo da
rigettare tutte le richieste d’apertura d’un canale di forwarding che non siano state precedute
da una richiesta globale da parte del client stesso.
Fino a questo momento abbiamo visto quello che il client deve fare per chiedere al server di
mettersi in ascolto su una porta di rete e “forwardare” tramite il tunnel cifrato le connessioni
che arrivano su quella porta a una porta dell’host del client. Tuttavia la procedura è completamente simmetrica: il client può mettersi in ascolto su una propria porta locale e “forwardare” tutto quello che arriva a quella porta su una porta del server remoto. In questo caso non
c’è bisogno di mandare una richiesta globale, perché il client stesso provvede alle operazioni
di inizializzazione del socket su cui vuole ascoltare. In questo modo quando giunge una richiesta di connessione alla porta in ascolto, il client invia al server un pacchetto di questo tipo:
SSH_MSG_CHANNEL_OPEN
channel_type = “direct-tcpip”
sender_channel
initial_window_size
maximum_packet_size
host_to_connect
port_to_connect
originator_IP_address
originator_port
Come al solito i primi cinque campi del pacchetto sono già stati illustrati nel par. III.4.2.
Il campo “host_to_connect” comunica al server su quale dei suoi indirizzi deve collegare il
canale di forwarding (sempre supposto che il server abbia più di un indirizzo IP), mentre il
campo “port_to_connect ” specifica quale sia la porta del server su cui dirigere il forwarding
dei dati. Gli ultimi due campi sono diversi da quelli del pacchetto precedente nonostante ab-
77
biano lo stesso nome. In questo caso “originator_IP_address” è l’indirizzo IP numerico della
macchina da cui è arrivata la richiesta di connessione alla porta del client e “originator_port”
è la sua porta, in sostanza questi ultimi due campi sono uguali rispettivamente a “address_that_was_connected” e “port_that_was_connected” del pacchetto precedente.
Tutti i canali di forwarding aperti sono indipendenti dalla sessione, e la chiusura del canale
di sessione non implica automaticamente la chiusura dei canali di forwarding. L’ultimo tipo
di pacchetto descritto deve essere inviato solamente dal client al server. Per ragioni di sicurezza, tutte le implementazioni del client devono essere in grado di scartare tali pacchetti.
III.4.7 Conclusioni
Il protocollo di connessione fornisce all’utente diversi strumenti per poter sfruttare al meglio
una connessione cifrata tra due host, tuttavia alcuni problemi di sicurezza possono emergere
dall’utilizzo improprio di questo protocollo. Per esempio il TCP/IP port forwarding può essere adoperato in un tentativo di intrusione per aggirare programmi che controllano gli accessi (es. firewalls ecc.), oppure il forwarding dell’X11 può potenzialmente permettere
l’accesso al server X locale senza tener conto dei parametri di sicurezza imposti
dall’amministratore della macchina. Inoltre il traffico cifrato non permette di poter controllare quello che fa un utente malintenzionato. In conclusione si può dire che tutti i servizi offerti dal Connection Protocol possono essere utilizzati per aumentare la sicurezza della comunicazione tra host, ma, se usati in maniera inopportuna, possono essere un veicolo per eventuali attacchi ai medesimi host. Per ulteriori dettagli su questo protocollo si veda [SSHCONN].
78