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