5. Specifica del Software


 


Il linguaggio di programmazione utilizzato per sviluppare il progetto è Java (JDK versione 1.2).

Questa scelta ci permette di utilizzare un’organizzazione ad Oggetti nella progettazione del sistema, rendendo più modulare e semplice la realizzazione dell’intero sistema.

Inoltre viene garantita la portabilità su sistemi differenti (Windows, Unix…), caratteristica fondamentale per un servizio distribuito.

Per quanto concerne la parte di Crittografia (non disponibile nelle distribuzioni europee di Java), abbiamo scelto l’Entrust Java Toolkit, che, rispettando le interfacce definite nel package java.security, consente una perfetta integrazione con il linguaggio Java.

Utilizzeremo, almeno in parte, il package JNDI 1.2, che definisce la struttura di un generico servizio di nomi.

Il progetto è stato suddiviso, nel suo sviluppo, in due parti principali:

sviluppate parallelamente pur mantenendo un buon grado di coesione.

5.1 Il DNS

Il principale obiettivo nella realizzazione del DNS sarà quello di non realizzare un prodotto “ad-hoc” per il problema specifico che vogliamo risolvere, bensì un sistema software modulare e facilmente espandibile.

5.1.1 Le classi per la gestione dei nomi

Con “binding” del DNS si intende ogni tipo di associazione che può essere presente in un Sistema di Nomi, in generale sarà una coppia del tipo: < Name , Value >.

 

La classe DNSName modella l’astrazione di nome del DNS, si può considerare costituita da un identificativo (il nome utente,o identificativo di un servizio, nel nostro caso NS o AS) e dal dominio di appartenenza che può essere eventualmente nullo per spazi di nomi "flat".

 

Type è una classe astratta, le cui sottoclassi rappresentano un “tipo” di nome, nel nostro caso abbiamo un identificativo di utente (UserType), di Name Server (NSType), e di Authentication Server (ASType). Per aggiungere un’altro tipo di servizio da utilizzare tramite il DNS è sufficiente aggiungere una sottoclasse corrispondente.

Domain è la classe che generalizza il nome di dominio, implementa l’interfaccia javax.naming.Name che definisce le caratteristiche di un generico nome in uno spazio di nomi gerarchico.

Definiamo brevemente la sintassi dei nomi di dominio in notazione BNF:

Dominio::= root |Nome . Dominio

root ::=“”

Nome::= alfa | alfa Nome

alfa ::= a..z | 0..9

I nomi di dominio sono assoluti, rappresentano dunque una lista, da destra verso sinistra, di nomi semplici, separati da ‘.’ il cui primo elemento è sempre il nome di root rappresentato per convenzione dalla stringa vuota.

Per cui, in un dominio, dovrebbe esserci sempre il punto finale (ad es. "b. a."), dove non presente ("b.a") si intende sottointeso.

L’utilizzo dei nomi è libero tranne che per il nome di root che non può essere replicato (ad es. “b..a.” è errato).

In caso di costruzione di un nome errato viene sollevata l’eccezione javax.naming.InvalidNameException .

I nomi utente seguono la seguente semplice sintassi:

NomeUtente::= UserId @ Dominio

La classe Parser data una stringa permette di ottenere il DNSName  corrispondente, questa eredita dalla classe astratta NameParser in modo che sia possibile definire diversi Parser per sintassi dei nomi differenti.

 

5.1.2 Le classi per la gestione del servizio di nomi

Il DNS è costituito, come già sottolineato, da un insieme di server partizionato e replicato. La classe principale è NameServer, che svolge un servizio di Naming senza replicazione.

Le sottoclassi PrimaryNameServer e SecondaryNameServer aggiungono il meccanismo della replicazione.

Funzionalità realizzate dalla classe NameServer:

Il NameServer è un caratteristico Server concorrente e parallelo:

 

La classe IterQueryResolver, che rappresenta un Thread separato, implementa tutta la logica della risoluzione iterativa dei nomi, in questo modo sarebbe possibile realizzare altri tipi di risoluzione (ricorsiva o transitiva), senza modificare la struttura del DNS.

BindingsTable è una tabella che permette di memorizzare e accedere ai Binding. Un NameServer ne utilizza due sottoclassi:

Table mantiene tutte le informazioni necessarie ad un NameServer, per cui alla tabella delle associazioni aggiunge due liste, una per i sottodomini delegati e una per quelli non delegati.

Cache gestisce il caching delle informazioni, ad ogni Binding associa un tempo di vita in secondi (quello del NameServer da cui è stato reperito), allo scadere del tempo di vita, l’associazione perde di validità e può essere eliminata.

Funzionalità realizzate dalla classe PrimaryNameServer:

Oltre alle funzionalità ereditate da NameServer , la classe PrimaryNameServer gestisce l’inserimento delle associazioni e la creazione dei sottodomini, con i relativi controlli di correttezza, e l’aggiornamento dei secondari. A questo scopo utilizza la classe UpdateThread ,che crea una porta di ascolto TCP per i secondari, ed utilizza a sua volta la classe UpdateSecondaryThread per il trasferimento dei dati.

 

Funzionalità realizzate dalla classe SecondaryNameServer :

La classe SecondaryNameServer richiede periodicamente una tabella aggiornata al Server Primario.

 

Le interrogazioni da parte dei client vengono indirizzate ad un Resolver,  che invia le richieste ad un Name Server del dominio. Il Resolver è un Server multithreading che accetta solo richieste locali.  In questo modo il caching effettuato dal Resolver è disponibile per tutti i client del nodo.

 

 

5.1.3 Le classi per la gestione dei messaggi

Le comunicazioni tra Name Server avvengono tramite scambio di messaggi:

 

La classe astratta PacketMsg rappresenta il generico messaggio, implementa i metodi per trasformare il messaggio in DatagramPacket e viceversa.

SignedMsg aggiunge al messaggio un meccanismo di Signature:

Il sistema di nomi potrebbe rappresentare un punto debole per la sicurezza (soprattutto nel nostro caso di un servizio basato sulla sicurezza), perciò deve assicurare:
  • l’ autenticità del mittente.
  • l’integrità dei dati
  • la  freshness delle informazioni.

Poichè non è necessaria la segretezza delle informazioni (che sono di dominio pubblico) è sufficiente un meccanismo di Signature associato al timestamping dei messaggi, che permette di ridurre le dimensioni del pacchetto rispetto alla cifratura del messaggio.

 

A ciascun pacchetto creato da SignedMsg viene quindi aggiunta la signature delle informazioni contenute, e questa viene controllata alla ricezione. In caso di violazioni della sicurezza o corruzione del pacchetto viene sollevata una eccezione.

TimestampedMsg gestisce il timestamping dei messaggi.

QueryMsg è la classe astratta che rappresenta i messaggi di richiesta di informazioni, mentre AnswerMsg i messaggi di risposta.

ReferToMsg è il messaggio che specifica qual è il Name Server cui rivolgersi per ottenere l’informazione richiesta.

Classi che ereditano da QueryMsg:

Queste classi non necessitano di autenticazione del mittente, per cui non ereditano da SignedMsg, a patto che AnswerMsg riporti nella risposta anche il nome che era stato richiesto in modo che sia possibile verificare che il messaggio non sia stato sostituito con la risposta ad un’altra richiesta.

Classi che ereditano da AnswerMsg:

Lo scambio di messaggi avviene con un protocollo UDP senza connessione. La scelta è stata fatta poichè i messaggi sono generalmente di dimensioni contenute e quindi non sarebbe giustificato l’overhead introdotto dalla connessione.

La semantica per l’invio dei messaggi è at-least-once poichè si assume che nel breve termine tutte le richieste siano idempotenti:

Il richiedente invia il messaggio e si pone in attesa con timeout, allo scadere si effettua il reinvio del messaggio (ad una altro Server se disponibile), fino al numero massimo di ripetizioni consentite, al ricevimento di un messaggio di risposta (qualunque sia la sua provenienza), tutti i successivi vengono ignorati.

Come ipotesi di guasto fissiamo il numero massimo di tentativi di reinvio a 3.

Le classi principali sono:

PacketMsg:

La classe PacketMsg definisce i due seguenti metodi:

Questo metodo trasforma il messaggio in un DatagramPacket affinchè possa essere inviato tramite una DatagramSocket  all’indirizzo addr:port.

Il metodo dato un pacchetto restituisce il messaggio corrispondente.

Per la trasformazione dei messaggi in pacchetti e viceversa si utilizza il concetto di Serializzazione del linguaggio Java. In questo modo è molto più agevole estendere il sistema con nuovi messaggi poiché non ci si deve preoccupare della codifica degli stessi in pacchetti e viceversa.

Il problema della Serializzazione è che l’interprete Java inserisce molte informazioni non necessarie, per poter codificare gli oggetti negli stream. Per questo tutti gli oggetti utilizzati nei messaggi (come DnsName, Domain ecc.) dovranno ridefinire la serializzazione, in modo da ridurre la dimensione dei messaggi, tramite l’overriding dei metodi:

  • private void writeObject( ObjectOutputStream out)
  • private void readObject (ObjectInputStream in)

In questo modo è possibile non serializzare le strutture che contengono i dati (come ArrayList o HashTable) per ricostruirle soltanto in seguito, oppure inviare delle rappresentazioni compatte dei dati.

Per evitare di eccedere nella dimensione dei pacchetti, la classe PacketMsg stabilisce una dimensione massima di 1024 byte, che viene controllata alla creazione del DatagramPacket, in caso di dimensioni eccessive viene sollevata un’eccezione MsgTooLongException.

SignedMsg:

La classe Signed message esegue l’overriding dei rispettivi metodi della classe PacketMsg:

Oltre alla serializzazione dell’oggetto, aggiunge una signature dell’array di byte che lo rappresenta, mediante l’oggetto iaik.java.security.Segnature.

Trasforma il pacchetto in messaggio e ne verifica l’integrità tramite la signature allegata.

 

5.1.4 La gestione del Servizio

Si assume che la porta utilizzata dal servizio sia nota e comuni a tutti i Server (Resolver, e NS primari e secondari).

Un Client alla necessità di risolvere un nome invia un QueryMsg (DomainQueryMsg per conoscere il proprio dominio, NameQueryMsg per ottenere l’indirizzo associato ad un nome), al Resolver locale (reperibile all’indirizzo locale alla porta 5656), questi conosce gli indirizzi dei NS del dominio, si occupa di risolvere la query e restituisce il risultato al Client come AnswerMsg (a seconda del risultato della query).

 

Il Resolver innanzitutto verifica che il nome richiesto nome richiesto non sia presente in Cache, nel caso in cui risponde immediatamente al Client. Altrimenti invia il QueryMsg ricevuto ad un Name Server del dominio, che si occuperà della ricerca iterativa e attende da questo la risposta.

 

Il NameServer all’attivazione entra in un ciclo infinito in cui si mette in ascolto su di una DatagramSocket in attesa di QueryMsg. All’arrivo di un messaggio ne affida la gestione ad un IterQueryResolver.

La classe IterQueryResolver svolge quindi due compiti:

In realtà sono disponibili due tipi di ricerca iterativa, una normale con la prima richiesta verso la Root:

 

, ed una con la prima richiesta verso il dominio di livello superiore:

 

La scelta è fatta utilizzando una funzione euristica:

che determina la "distanza" dal dominio richiesto, intesa come numero di Name Server che devono essere coinvolti nel caso peggiore, cioè senza l'intervento della cache.

Per fare ciò si verifica qual è il dominio superiore comune a quello richiesto e a quello richiedente (nel caso peggiore il dominio di root), e si confronta la distanza tra questo e, rispettivamente, il dominio richiedente e quello di root.

 

Flusso di esecuzione di IterQueryResolver:

 

Per prima cosa IterQueryResolver controlla se il nome richiesto è locale o appartiene ad un sottodominio non delegato, in questo caso il Name Server è competente e si risponde immediatamente al richiedente.

Se ciò non avviene si controlla la cache. Se il nome è presente si risponde, altrimenti si ricerca una "scorciatoia", in altre parole si ricerca un Name Server per un dominio superiore gerarchicamente a quello richiesto, la ricerca ha termine con successo quando il NS è trovato, e comunque quando si raggiunge un dominio genitore del dominio che effettua la ricerca (compreso il dominio stesso e la root) poiché potrebbe non essere più vantaggioso.

A questo punto si decide se effettuare:

  1. Ricerca Iterativa:

Primo passo:

Se in cache è stata trovata un Refer si utilizza, oppure se il nome richiesto appartiene ad un sottodominio delegato si invia la richiesta al NS corrispondente. Altrimenti si decide se effettuare la ricerca iterativa verso l'alto o se inviare la richiesta alla Root.

Passi Successivi:

Alla ricezione di un ReferTo si invia la query all'indirizzo corrispondente finchè non si riceve un AnswerMsg, che viene inviato come risposta al richiedente.

  1. Controllo e risposta:

Si controlla che il dominio sia un sottodominio, in questo caso sia invia un ReferTo del sottodominio delegato competente. Altrimenti si tratta di una ricerca iterativa verso l'alto per cui si invia il ReferTo del dominio superiore.

In caso di nomi errati o inesistenti bisogna prevenire l'innescarsi di cicli infiniti di ricerca. E' sempre possibile determinare quando un dominio è competente per un dato nome, e quindi se un nome è corretto o meno. Inoltre poniamo un limite massimo al numero di cicli iterativi che possono essere eseguiti.

Questo è il meccanismo di funzionamento dei Name Server indipendentemente dal fatto che siano primari o secondari. L'unica differenza è il metodo di reperimento della tabella. Il Primario mantiene la propria aggiornando periodicamente un file che è letto allo startup. Inoltre avvia un Thread che apre un a porta TCP per le richieste di aggiornamento da parte dei secondari.

All'avvio il Secondario attiva la connessione e riceve dal Primario la tabella.

Periodicamente il Secondario richiede l’aggiornamento della tabella al Primario, il tempo che intercorre tra un aggiornamento e l’altro è un parametro del Server, e può variare la consistenza dei dati. L’aggiornamento non avviene indiscriminatamente, ma solo se ci sono state variazioni della tabella dall’ultimo aggiornamento.

Poiché la classe PacketMsg ha una dimensione massima, viene posto un limite di 10 nella registrazione di valori da associare ad un determinato nome.

Non è quindi possibile registrare più di 10 Name Server per un determinato dominio (1 Primario e 9 Secondari).

Si noti che è comunque possibile inserire altri Name Server "non registrati" con il solo compito di effettuare ricerche e caching.

 

5.1.5 Tolleranza ai Guasti

Bisogna sottolineare che, nella progettazione, si è tenuto conto che, come avviene in generale, è ammesso che il DNS in alcuni casi possa non rispondere alle query, o rispondere con dati non consistenti.
In caso di caduta di un Server Secondario, al riavvio, chiederà la tabella al Primario, la sua caduta quindi non causa alcun problema, in quanto ai Resolver viene assegnato più di un Name Server di riferimento.
In caso di caduta di un Server Primario, il sistema continua a funzionare basandosi sui Secondari, in questo caso però potrebbero esserci inconsistenze per binding non aggiornati sino al ripristino del Primario, questo è ammissibile poiché si suppone che i cambiamenti nel DNS siano poco frequenti.
Il sistema può continuare a funzionare temporaneamente anche in caso di partizionamento della rete, basandosi sul caching effettuato in precedenza, ovviamente le prestazioni andranno degradando. Lo stesso discorso è valido per il temporaneo isolamento di un nodo client.

 

 

 

5.2 Authentication Server

I punti chiave dell’ AS sono:

 

5.2.1 IL GESTORE

Il Gestore mantiene una porta di ascolto dove attende le richieste da parte di:

Inoltre gestisce la lista delle Copie attualmente in funzione, mantiene la tabella delle chiavi pubbliche (sempre consistente) relativa al proprio dominio, fornisce un timestamp unico per ogni operazione in modo che tutti i messaggi inviati siano riconoscibili come inviati dal mittente Gestore e attuali (cioè non riutilizzati).

Per gestire la comunicazione, a seguito di ogni richiesta, genera un thread il quale svolge tutto il lavoro. La replicazione è realizzata mediante una serie di Gestori Secondari il cui primo passo è richiedere lo stato attuale delle Copie “attive” al Gestore primario; e quindi iniziare a fornire il servizio al pari del Gestore Primario.

Le nuove Copie che si rendono disponibili a fornire il servizio si registrano su tutti i Gestori del proprio dominio, in questo modo si sono evitati i messaggi di coordinazione tra il Gestore master e gli slave poiché tutti possiedono le stesse informazioni (ricordiamo che abbiamo trattato solo il problema della distribuzione delle chiavi, e non quello della registrazione di nuovi utenti, compito svolto da un autorità di certificazione esterna, e di conseguenza per quel che ci riguarda lavoriamo su una tabella di chiavi pubbliche “statica”).

Un ultima considerazione riguarda il fatto che il Gestore contattato dal Client non è quello relativo al proprio dominio ma è quello del Client su cui viene effettuata la richiesta; è questo infatti che possiede le informazioni di cui abbiamo bisogno.

 

5.2.1.2 Aggiornamento di una Copia

Ottenuta dal Dns la lista dei Gestori attivi, la Copia inoltra la domanda al primo Gestore della lista (ovvero al Master, sempre in cima alla lista), e resta in attesa dell’invio della tabella mediante la Socket TCP aperta (dal Gestore) in conseguenza alla sua richiesta.

Il primo passo svolto dal Gestore è la lettura dell'Hashtable (la lettura viene effettuata da file per semplicità nella realtà la tabella dovrebbe essere sempre a disposizione del Gestore memorizzata in un area protetta della memoria). Nel caso non sia possibile accedere alla tabella viene chiusa la socket in modo che la Copia non rimanga indefinitivamente in attesa. In realtà viene terminato anche il Gestore in quanto non ha senso di esistere se non è in possesso di una tabella (consistente) da fornire alle Copie, inoltre l' assenza della tabella potrebbe significare problemi o manomissioni. Noi trattando la distribuzione delle chiavi partiamo sempre dall’ipotesi che il Gestore abbia, in ogni momento, a sua disposizione una tabella consistente.

Si è scelto di non inviare la tabella come singolo oggetto ma di inviare singolarmente le varie entry della tabella stessa  (viene inviata su socket la dimensione della tabella da leggere quindi ogni singola entry viene firmata, e spedita). In questo modo si è ridotto il numero di byte inviati limitando il problema derivante dall’invio di tabelle di una certa dimensione (si sono così evitati piccoli problemi interni al pacchetto della sicurezza java in cui ci siamo imbattuti legati alla signature di tali pacchetti, inoltre viene così ridotto, seppure di poco, il tempo per la codifica/decodifica del messaggio).

Notare che ogni entry viene numerata a partire da un timestamp unico cioè i timestamp delle singole entry seguono un ordine logico di numerazione. Se anche il timestamp di una sola entry non coincide dalla parte della Copia, il processo di aggiornamento fallisce. Terminato l’invio delle entry della tabella delle chiavi pubbliche è il Gestore che si mette in attesa che la Copia gli invii l’ultimo timestamp ricevuto, incrementato come riprova del fatto che solo la Copia poteva essere a conoscenza della chiave per decodificare quel messaggio, che la tabella è stata inviata correttamente in tutte le sue entry, e che i messaggi sono effettivamente attuali.

L’ultimo messaggio serve infatti da conferma per verificare che le entry ricevute dalla Copia non siano TUTTI messaggi vecchi intercettati e riutilizzati.

Se anche dalla parte del Gestore  tutto coincide: il messaggio è corretto,  non ha cioè subito modifiche durante il suo cammino e il  timestamp ricevuto coincide con l’ultimo timestamp inviato alla Copia incrementato di uno, la Copia viene considerata attiva (funzionante) altrimenti attende il riinoltro della richiesta da parte della Copia.

 

5.2.1.3 Comunicazione con Client e interfacciamento con la Copia

Il Client, ottenuti gli indirizzi dei Gestori attivi dal DNS,  effettua una richiesta per ottenere la chiave pubblica di un altro Client ad un Gestore, il quale la invia ad una Copia. Le richieste vengono distribuite sequenzialmente in modo da mantenere un carico di lavoro abbastanza uniforme tra le varie Copie (considerando un medesimo tempo di risposta ).

Naturalmente la richiesta effettuata dal Gestore alla Copia puo avere esito positivo o negativo. Nel primo caso siamo in possesso della chiave pubblica richiesta dal Client e dobbiamo effettuare la risposta in modo che il Client sia sicuro della consistenza della chiave ricevuta. In caso negativo viene inviato al Cliente un messaggo indicante l’impossibilità di reperire la chiave o risposte indicanti il rifiuto o l’assenza del Cliente specificato nel database.

I messaggi di rifiuto li abbiampo solo nel caso in cui i messaggi vengano modificati durante il loro cammino, in questo caso il Gestore, o la Copia ricevendoli si rende conto della non consistenza di suddetti messaggi e li invalida.

Fondamentale in tutti i messaggi è il controllo della coincidenza dei timestamp per evitare il replying dei msg (anche in quest’ ultimo caso viene invalidata la risposta ricevuta e subito inviata un ulteriore richiesta).

Ma ci sono casi in cui non riceviamo risposta dalla Copia:

Nel caso una Copia non risponda (msg di timeout per 2 volte consecutive) viene cancellato il suo indirizzo da quelli disponibili e viene inoltrata la domanda a un altra Copia;

Ricevuto correttamente il messaggio dalla Copia recante la chiave del Client richiesta, il Gestore  gli manda un messaggio con il SUO (del Client) timestamp e con una signature per l' autenticazione.

 

5.2.2 LA COPIA

I passi svolti da una Copia sono:

La scelta di non generare un thread per gestire le richieste delle Copie è dettata dal fatto che:

La tabella hash è passata con il protocollo TCP. Nel caso si verifichino problemi, come ad esempio intromissioni di terzi, viene reinoltrata la richiesta di aggiornamento. La comunicazione tra Copia e Gestore per il servizio di distribuzione delle chiavi pubbliche utilizza invece UDP; si sono considerati quindi tutti i possibili casi di riemissione dei messaggi in caso di perdita o modifica degli stessi.

5.2.3 PROGETTAZIONE E DEFINIZIONE DELLE CLASSI

5.2.3.1 I Messaggi

Per messaggi si intendono tutti i messaggi utilizzati nell’ambito dell’Authentication Server e quindi quelli scambiati tra Client, Gestore e Copie. E’ stata creata una classe astratta PacketMsg che fornisce tutte le funzioni per creare pacchetti (tale funzione la ritroviamo anche come base per i messaggi del DNS).

 

Descrizione messaggi:

 

Questi messaggi non vengono inviati direttamente perché non permetterebbero l’autenticazione dell’identità del mittente (discorso a parte per il messaggio di timeout che non viene mai spedito, e il messaggio di reqCopieMsg che non necessita di autenticazione). Si è quindi preferito incapsularli all’interno di altri messaggi che contengano anche un campo signature in modo che sia sempre possibile verificare chi sia il mittente, se il messaggio è attuale e non è stato modificato . Questi messaggi “contenitori” sono quelli costruiti a partire dalla classe msgFirma che fornisce tutte la funzioni per l’autenticazione e la verifica degli oggetti inviati.

 

Descrizione messaggi di autenticazione:

 

La classe tistamp contiene un timestamp (un intero) e le funzioni per recuperarlo. La classe tistampc aggiunge un ulteriore timestamp, mentre la classe tistamperr permette di inviare un messaggio di errore.

La classe nonce è un oggetto contenente l’identità di chi invia il messaggio e un tistamp codificato (array di byte). Fornisce anche le funzioni per preparare (preparanonce) e verificare (verificanonce) i nonce. La classe nonceritorno contiene due nonce. Naturalmente c’è anche un messaggio di errore per indicare eventuali modifiche sul messaggio.

La classe entry mantiene un nome, una chiave pubblica (in pratica una entry della tabella delle chiavi pubbliche) e un timestamp

La classe indirizzo mantiene un indirizzo IP e un intero (che dovrebbe rappresentare la porta di ascolto del processo in questione).

La classe msgByte contiene un array di byte che rappresenta il messaggio del Client criptato.

La classe adrC contiene un campo indirizzo e un timestamp. Il suo utilizzo è legato esclusivamente all’invio della lista delle Copie attive dal Gestore primario al Secondario. Il protocollo di aggiornamento è identico a quello usato tra Gestore e Copia per l’invio della tabella delle chiavi (si inviano gli indirizzi uno alla volta firmati e numerati).

La classe RSACipher è la classe contenente tutte le funzioni per la cifratura e decifratura di messaggi. La vedremo meglio in seguito.

5.2.3.2 LE CLASSI PRINCIPALI

 

Gestore gestisce la porta di ascolto;

GestoreS eretita da Gestore, l’unica funzione nuova che implementa riguarda la richiesta di aggiornamento per la lista degli indirizzi delle Copie attualmente attive (classe ShtS)

msgFirma: vedi sopra.

GestoreThread: gestisce tutta la comunicazione del Gestore (dopo che è arrivata una richiesta).

connettiTCP: mantiene tutte le funzionalità per gestire una connessione TCP.

DtgRichiesta: mantiene tutte le funzionalità per gestire una comunicazione UDP:

Sht: mantiene tutte le funzionalità per gestire il trasferimento della tabella delle chiavi pubbliche da Gestore a Copia:

 

 

Client gestisce la comunicazione del Client richiedente;

ClientListen apre una porta di ascolto;

ClientThread gestisce tutta la comunicazione del Cliente “richiesto”;

ClientGui gestisce l’interfaccia grafica del Client;

connettiTCP: vedi sopra;

BtoA gestisce tutta la comunicazione tra i due Client. mantiene le funzioni per:

RSACipher è la classe contenente tutte le funzioni per la codifica dei messaggi:

 

 

Copia gestisce tutto il lavoro della Copia;

cli mantiene tutte le funzionalità per gestire il ricevimento della tabella delle chiavi pubbliche da Gestore a Copia ed effettua tutti i controlli relativi al caso;

 

5.2.4 IMPLEMENTAZIONE

5.2.4.1 COMUNICAZIONE TRA CLIENT

Il Client deve in ogni momento esser pronto a inviare e ricevere messaggi. Proprio per questo il corpo principale del Client chiama un thread che si mette in ascolto su una determinata porta. Chiunque voglia comunicare con il Client utilizzerà questa porta per instaurare una connessione. Ad ogni richiesta di connessione viene generato un ulteriore thread che gestisce la comunicazione stessa (si è cercato di evitare il più possibile la serializzazione).

 

Ottenuta la chiave pubblica  dal Gestore (di B), il Client è pronto per iniziare la procedura di autenticazione per la comunicazione con l’altro Client (naturalmente dopo averne ottenuto l’indirizzo dal DNS). Il protocollo cui facciamo riferimento è quello di Needahm-Schroeder lievemente modificato:

Finito questo protocollo i due Client sono a conoscenza delle rispettive identità e mantengono una socket TCP per la comunicazione. Tale comunicazione è stata semplificata e realizzata come un unico messaggio da A a B (in quanto si è ritenuto più importante il procedimento utilizzato rispetto al numero di messaggi che si potevano inviare).

Tale messaggio non può viaggiare in chiaro nel rispetto della privacy dei due Client  e quindi non si è potuta utilizzare una signature per i messaggi. Inoltre la signature viene autenticata con la chiave privata e verificata con la chiave pubblica; qui avviene l’esatto contrario il messaggio viene autenticato con la chiave pubblica e verificato con la chiave privata. Si è scelto quindi di codificare il messaggio e di inviarlo(msgByte). Nella realtà il messaggio è stato suddiviso in tanti blocchi, che sono alla fine riuniti in modo che venisse spedito un unico stream di byte. Tale stream viene poi letto e decifrato all’arrivo. Tutto questo avviene in modo totalmente trasparente all’utente. La suddivisione in blocchi è stata realizzata in quanto la procedura di crittografia di java non supporta blocchi superiori ai 64 byte.

 

5.2.4.2 COMUNICAZIONE TRA CLIENT E GESTORE

 

Il ClientA richiede il nome del  ClientB da ricercare.  Successivamente manda un requestMsg al Gestore. Il messaggio del Client viene inviato al Gestore in chiaro tanto il Client non ha bisogno di qualificarsi perché richiede informazioni pubbliche, inoltre qualsiasi modifica al messaggio porterebbe all’invalidazione del messaggio da parte dello stesso Client o all’ invio di una chiave sbagliata, nel qual caso fallirebbe la procedura di riconoscimento da parte dell’ altro Client. Il Gestore  comunica con una Copia per ottenere la chiave pubblica del Client richiesto (answerKey) e la invia codificata con la SUA (del Gestore) chiave privata al Client. Nel caso non sia possibile ottenere la chiave pubblica del Client il Gestore invia al richiedente un messaggio (sempre codificato con la sua chiave privata) indicante il motivo dell’impossibilità nel reperire la chiave ( nessuna Copia disponibile (notCopiaMsg), nome inesistente (notFindMsg) …).

Bisogna effettuare una distinzione tra il timestamp inviato al Gestore dal Client e quelli utilizzati tra Gestore e Copie. Il timestamp di ritorno inviato dal Gestore al Client deve coincidere con quello inviato dal Client. Allo stesso modo il Gestore genera un suo timestamp interno che viene man mano incrementato a seconda dei messaggi che invia, (o in certi casi viene creato in modo random) e questo viene utilizzato per le comunicazioni con le Copie.

 

5.2.4.3 COMUNICAZIONE TRA GESTORE E COPIE (per ottenimento chiave pubblica)

 

Una volta arrivata una richiesta e verificato che si tratta di una richiesta da parte di un Client, il Gestore deve decidere a quale Copia inviare la richiesta. Il protocollo utilizzato è, come abbiamo già detto, UDP. Il Gestore consulta la lista in suo possesso contenente le Copie in funzione, le richieste vengono inoltrate ciclicamente a tutte le Copie in modo che il lavoro sia suddiviso equamente e non si creino colli di bottiglia. Determinato l’indirizzo e la porta di ascolto della Copia (tale operazione è stata resa esclusiva in modo che più richieste non vengano inoltrate contemporaneamente alla stessa Copia mentre altre Copie rimangono senza lavoro), il Gestore invia un messaggio di richiesta (requestKey) in forma di datagramma alla Copia. In caso non si riceva risposta entro un tempo prefissato (15 sec) scatta un messaggio di timeout (timeoutMsg) e il messaggio viene riinoltrato ALLA STESSA Copia. Nel caso la richiesta fallisca ancora la Copia viene considata inaffidabile e il suo indirizzo viene cancellato dalla lista mantenuta dal Gestore. Nel caso non ci siano Copie “attive” o le Copie presenti non rispondano viene inviato un messaggio indicante l’assenza di Copie in grado di fornire il servizio (notCopiaKey). In tutti gli altri casi la Copia risponde fornendo le informazioni richieste (answerKey), o indicando l’assenza del Client richiesto nella tabella (notFindKey) o avvisando che il messaggio ricevuto ha subito modifiche (refuseKey).

 

5.2.4.4 COMUNICAZIONE TRA COPIA E GESTORE (per la richiesta della tabella delle chiavi pubbliche)

 

Il primo passo svolto dalla Copia  dopo la registrazione su una porta libera è l’invio di una richiesta al Gestore. Nella realtà la Copia invia al Gestore il suo indirizzo (inteso come indirizzo IP e porta di ascolto è indirizzo); a questo punto il Gestore è pronto per inviare la tabella delle chiavi pubbliche. Vengono quindi inviati, in sequenza, una serie di oggetti entryKey contenenti un oggetto entry e la signature dell’oggetto stesso. Il protocollo utilizzato per l’ invio della tabella parte con:

Mediante questo scambio di messaggi siamo certi della consistenza della tabella e dell’autenticità del mittente.

5.2.5 Modifiche al documento di specifica dei requisiti

Il progetto è stato realizzato cercando di seguire fedelmente le specifiche, ma in fase di programmazione certi aspetti sono stati lievemente modificati.

5.2.6 Tolleranza ai Guasti

Nel caso cada il master il servizio viene comunque reso possibile dai gestori Slave. Naturalmente il Gestore quando torna in funzione non può partire da zero, ma deve conoscere la lista delle Copie attualmente attive (si trova in una situazione duale a quella di un nuovo Gestore che deve aggiornarsi presso il master). Poiché abbiamo detto che questa distinzione tra gestori Master e Slave è solo virtuale, e il DNS conosce già il suo indirizzo, può aggiornarsi come Gestore secondario facendo richiesta a un Gestore Slave (in questo modo lo Slave funziona virtualmente da Master e passa la sua lista al nuovo entrato, che così può iniziare a lavorare; la lista che si vedrà passare avrà se stesso in testa).

La caduta di uno slave non causa invece preoccupazioni, nel momento in cui verrà riavviato inizierà la sua normale procedura di aggiornamento presso il Master. Manteniamo sempre l’ipotesi di guasto singolo, inoltre ricordiamo che per convenzione il primo Gestore che deve essere attivato è sempre il Master. Quindi l’aggiornamento dei server slave rimane comunque successivo al boot del server definito come master e fa riferimento a lui (ciò non toglie che in linea di principio uno slave potrebbe richiedere l’aggiornamento ad un altro slave; si è comunque preferito inoltrare queste richieste, a meno di gravi evenienze, sempre al master).

Nel caso cadano 1 o più (anche tutte) Copie  i gestori se ne accorgono e invalidano gli indirizzi in loro possesso fino al momento in cui non è più possibile fornire il servizio.