´Cwiczenie 9 - Zaawansowane metody programowania w sieci
Transkrypt
´Cwiczenie 9 - Zaawansowane metody programowania w sieci
Ćwiczenie 9 - Zaawansowane metody programowania w sieci komputerowej Bezpieczne gniazda Poufna komunikacja przez otwarte medium, takie jak Internet, która ma uniemożliwiać podsłuch, bezwzgl˛ednie wymaga szyfrowania danych. Wi˛ekszość algorytmów szyfrowania, które moga˛ być realizowane przez komputery, opiera si˛e na poj˛eciu klucza - rodzaju hasła, które nie jest ograniczone do tekstu. Komunikat pisany jawnym tekstem jest łaczony ˛ z bitami klucza według pewnego algorytmu matematycznego, w wyniku czego otrzymuje si˛e tekst zaszyfrowany. Używanie kluczy o wi˛ekszej liczbie bitów wykładniczo utrudnia odszyfrowanie komunikatu przez odgadywanie kluczy metoda˛ brutalnej siły. W tradycyjnym szyfrowaniu kluczem tajnym (czyli szyfrowaniu symetrycznym) ten sam klucz służy do szyfrowania i deszyfrowania danych. Zarówno nadawca, jak i odbiorca musza˛ mieć jeden klucz. Przypuśćmy, że Anna chce wysłać Markowi tajna˛ wiadomość. Najpierw wysyła Markowi klucz, który posłuży do przesłania sekretu. Klucz nie może jednak być zaszyfrowany, ponieważ Marek nie zna jeszcze klucza, wi˛ec Anna wysyła go w niezaszyfrowanej postaci. Załóżmy teraz, że Jan podsłuchuje połaczenie ˛ mi˛edzy Anna˛ i Markiem. Uzyska on klucz w tym samym momencie co Marek. Od tej chwili b˛edzie mógł odczytywać wszystko, co Anna i Marek sobie przekazuja.˛ W szyfrowaniu kluczem publicznym (czyli szyfrowaniu asymetrycznym) do szyfrowania i deszyfrowania danych używa si˛e innych kluczy. Jeden, nazywany kluczem publicznym, służy do szyfrowania danych. Ten klucz można dać każdemu. Drugi, nazywany kluczem prywatnym, służy do deszyfrowania danych. Musi być utrzymywany w tajemnicy, ale wystarczy, że b˛edzie go miał tylko jeden z korespondentów. Jeśli Anna chce wysłać wiadomość do Marka, prosi go o klucz publiczny. Marek wysyła Annie klucz przez niezaszyfrowane połaczenie. ˛ Anna szyfruje swoja˛ wiadomość kluczem publicznym Marka i wysyła ja.˛ Jeśli Jan podsłuchiwał, kiedy Marek przesyłał Annie klucz, to przechwycił klucz publiczny Marka. Nie pozwoli mu to jednak na rozszyfrowanie wiadomości przesłanej przez Ann˛e, bo do tego niezb˛edny jest klucz prywatny Marka. Wiadomość jest bezpieczna nawet wtedy, gdy klucz publiczny zostanie przechwycony. Szyfrowanie asymetryczne może być także używane do uwierzytelniania i sprawdzania integralności komunikatów. Anna może na przykład zaszyfrować wiadomość swoim kluczem prywatnym. Kiedy Marek otrzyma wiadomość, spróbuje 1 rozszyfrować ja˛ za pomoca˛ klucza publicznego Anny. Jeśli to si˛e uda, Marek b˛edzie wiedział, że wiadomość przyszła od Anny - nikt inny nie mógłby utworzyć wiadomości, która˛ dałoby si˛e poprawnie rozszyfrować za pomoca˛ klucza publicznego Anny. Marek b˛edzie również wiedział, że wiadomość nie została zmodyfikowana po drodze, albo celowo (przez Jana), albo przypadkowo (przez bł˛edne oprogramowanie albo szum sieciowy), ponieważ każda zmiana uniemożliwiłaby jej rozszyfrowanie. Anna mogłaby także zaszyfrować wiadomość dwukrotnie, raz swoim kluczem prywatnym, a raz kluczem publicznym Marka, zyskujac ˛ za jednym zamachem poufność, uwierzytelnienie i ochron˛e integralności. W praktyce szyfrowanie kluczem publicznym dużo bardziej obcia˛ża procesor i jest znacznie wolniejsze niż szyfrowanie kluczem tajnym. Zamiast wi˛ec szyfrować cała˛ komunikacj˛e za pomoca˛ klucza publicznego Marka, Anna szyfruje tylko tradycyjny tajny klucz i przesyła go Markowi. Marek rozszyfrowuje go za pomoca˛ swojego klucza prywatnego. Teraz Anna i Marek znaja˛ tajny klucz, a Jan nie. Dzi˛eki temu Anna i Marek moga˛ skorzystać z szybszego szyfrowania kluczem tajnym, a Jan nie b˛edzie mógł ich podsłuchać. Jan może jednak zaatakować ten protokół w inny sposób. (Bardzo ważne: atakowany jest protokół wysyłania i odbierania wiadomości, a nie same algorytmy szyfrujace. ˛ Atak ten nie wymaga złamania algorytmu szyfrujacego ˛ i jest zupełnie niezależny od długości klucza). Jan nie tylko może odczytać klucz publiczny Marka w drodze do Anny, ale może też zastapić ˛ go swoim własnym! Kiedy Anna b˛edzie myślała, że szyfruje wiadomość kluczem publicznym Marka, w rzeczywistości b˛edzie szyfrowała go kluczem Jana. Kiedy wyśle wiadomość, Jan przechwyci ja,˛ rozszyfruje swoim kluczem prywatnym, zaszyfruje go kluczem publicznym Marka i prześle dalej do Marka. Nazywamy to atakiem typu ”człowiek w środku” (ang. man-in-the-mid-dle). Jeśli Anna i Marek byliby zdani tylko na siebie, nie mogliby w prosty sposób si˛e zabezpieczyć przed takim atakiem. Najpraktyczniejsze rozwiazanie ˛ polega na tym, że Marek i Anna przechowuja˛ i weryfikuja˛ swoje klucze publiczne w zaufanym, niezależnym urz˛edzie certyfikacyjnym. Zamiast przesyłać sobie klucze publiczne, pobieraja˛ je z urz˛edu certyfikacyjnego. Nadal nie jest to rozwiazanie ˛ idealne = Jan może wkraść si˛e mi˛edzy Marka i urzad ˛ certfikacyjny, Ann˛e i urzad ˛ certyfikacyjny oraz Ann˛e i Marka - ale utrudnia zadanie Janowi. Jak widać na tym przykładzie, teoria i praktyka szyfrowania oraz uwierzytelniania -zarówno algorytmy, jak i protokoły - to trudna dziedzina, usłana polami 2 minowymi, na których poległ niejeden kryptograf-amator. Dużo łatwiej jest opracować zły niż dobry algorytm lub protokół szyfrujacy, ˛ w dodatku nie zawsze wiadomo, które algorytmy i protokoły sa˛ dobre, a które nie. Żeby używać mocnego szyfrowania w swoich sieciowych programach Javy należy skorzystać z Java Secure Socket Extension (JSSE). JSSE to standardowe rozszerzenie Javy 1.2 i nowszych wersji, które wyr˛ecza ci˛e w szczegółach negocjowania algorytmów, wymiany kluczy, uwierzytelniania korespondentów i szyfrowania danych. JSSE pozwala na tworzenie gniazd klientów i serwerów, które niewidocznie obsługuja˛ negocjacje i szyfrowanie wymagane do bezpiecznej komunikacji. Ty musisz tylko wysłać swoje dane przez te same strumienie i gniazda, które znasz z poprzednich rozdziałów. Java Secure Socket Extension dzieli si˛e na cztery pakiety: javax.net.ssl Abstrakcyjne klasy, które definiuja˛ programowy interfejs Javy do bezpiecznej komunikacji sieciowej. javax.net Abstrakcyjne klasy ”fabryk” gniazd, używane zamiast konstruktorów do tworzenia bezpiecznych gniazd. javax.security.cert Minimalny zbiór klas do obsługi certyfikatów z kluczem publicznym, wymagany przez SSL w Javie l .1 (w Javie l.2 i późniejszych wersjach należy zamiast tego użyć pakietu java.security.cert). com.suń.net.ssl Konkretne klasy, które realizuja˛ algorytmy i protokoły szyfrujace ˛ w implementacji odniesienia JSSE opracowanej przez Suna. Z technicznego punktu widzenia nie sa˛ one cz˛eścia˛ standardu JSSE. Inni implementatorzy moga˛ zastapić ˛ ten pakiet swoim własnym, na przykład takim, który korzysta z rodzimego kodu danej platformy w celu przyspieszenia generacji kluczy i szyfrowania. Żaden z tych pakietów nie stanowi standardowej cz˛eści JDK. Zanim b˛edziesz 3 mógł skorzystać z zawartych w nich klas, b˛edziesz musiał pobrać JSSE (wersj˛e krajowa˛ lub mi˛edzynarodowa) ˛ pod adresem http://fava.sun.com/products/jsse - jak zwykle, ten adres może si˛e zmienić. W przyszłości interfejs ten moga˛ zrealizować także firmy trzecie. Implementacja odniesienia Suna˛ jest rozpowszechniana jako plik zip, który możesz rozpakować i umieścić w dowolnym katalogu. W podkatalogu lib tego pliku znajdziesz trzy archiwa JAR: jcert.jar, jnet.jar i jsse.jar. Musisz umieścić je na swojej ścieżce klas. W Javie 1.2 i nowszych wersjach wystarczy przenieść je do katalogu jre/lib/ext. Natomiast w Javie 1.1 b˛edziesz musiał dopisać do swojej zmiennej środowiskowej CLASSPATH ścieżk˛e do wszystkich archiwów. Nast˛epnie musisz zarejestrować dostawc˛e usług kryptograficznych, edytujac ˛ plik jre/lib/extlsecurity/java.secuńty. Otwórz ten plik w edytorze tekstów i poszukaj linii podobnej do poniższej: security.provider.1=sun.security.provider.Sun security.provider.2=com.sun.rsajca.Provider W twoim pliku może si˛e znajdować wi˛ecej albo mniej takich dostawców. Dodaj jeszcze jednego, wpisujac ˛ nast˛epujac ˛ a˛ lini˛e: security.provider.3=com.sun.net.ssl.internal.ssl.Provider Być może b˛edziesz musiał zmienić liczb˛e ”3” na 2,4,5 lub inna˛ kolejna˛ liczb˛e w twojej sekwencji dostawców usług bezpieczeństwa. Jeśli zainstalujesz implementacj˛e JSSE napisana˛ przez firm˛e trzecia,˛ dołaczysz ˛ nast˛epna˛ podobna˛ lini˛e z nazwa˛ klasy wzi˛eta˛ z dokumentacji twojej implementacji JSSE. UWAGA Jeśli używasz wielu kopii JRE, b˛edziesz musiał powtórzyć t˛e procedur˛e dla każdej kopii. Z niezrozumiałych dla mnie powodów Sun zainstalował oddzielne kopie JRE 1.3 w moim systemie plików, jedna˛ na potrzeby kompilacji, a druga˛ na potrzeby uruchamiania programów. Musiałem dokonać tych zmian w obu kopiach, aby nakłonić programy JSSE do działania. 4 Jeśli nie zrobisz tego poprawnie, zobaczysz wyjatki ˛ w rodzaju ”java.net.SocketException: SSL implementation not available”, kiedy spróbujesz uruchomić programy używajace ˛ JSSE. Zamiast edytować plik java.security, możesz dodać poniższa˛ lini˛e do klas używajacych ˛ implementacji JSSE Suna: java.security.Security.addProvider(new com.sun.net.ssl.internal.ssl. Może to być przydatne, jeśli piszesz oprogramowanie przeznaczone dla kogoś innego i nie chcesz prosić go o modyfikowanie pliku java.security. 2. Tworzenie bezpiecznych gniazd klienta Jeśli nie interesuja˛ ci˛e szczegóły, używanie zaszyfrowanych gniazd SSL w celu porozumienia si˛e z istniejacym ˛ zabezpieczonym serwerem jest bardzo proste. Zamiast tworzyć obiekt java.net.Socket za pomoca˛ konstruktora, uzyskujesz gniazdo z klasy javax.net.ssl.SSLSocketFactory, używajac ˛ jej metody create-Socket().SSLSocketFactory to klasa abstrakcyjna, która nie odbiega od typowego wzoru abstrakcyjnej ”fabryki” obiektów: public abstract class SSLSocketFactory extends SocketFactory Poniższy przykład to nieskomplikowany program, który łaczy ˛ si˛e z zabezpieczonym serwerem HTTP, wysyła proste żadanie ˛ GET i wyświetla odpowiedź. Przykład 1. Klient HTTPS import java.net.*; import java.io.*; import java.security.*; import javax.net.ssl.*; public class HTTPSClient { public static void main(String[] args) { if (args.length == 0) { System.out.println(”Użycie: java HTPSClient host”); return; 5 } int port = 443; // domyślny port https String host = args[0]; try { Security.addProvider(new com.sun.net.ssl.internal.ssl. Provider()); SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault(); SSLSocket socket = (SSLSocket) factory.createSocket(host, port); Writer out = new OutputStreamWriter (socket.getOutputStream ()); // https wymaga pełnego adresu URL w linii GET out.write(”GET http://” + host + ”/ HTTP/1 . l\r\n”); out.write(”\r\n”); out.flush(); // czytamy odpowiedź BufferedReader in = new BufferedReader( new InputStreamReader(socket.getlnputStream())); int c; while ((c = in.read()) != -1) { System.out.write(c); } out.close(); in.close(); socket.close(); } catch (IOException e) { System.err.println(e); } } } Kiedy uruchomisz ten program, zapewne zauważysz, że reaguje ze znacznym opóźnieniem. Generowanie i wymiana kluczy publicznych stanowi znaczne obcia˛żenie dla procesora i sieci. Jeśli nawet masz szybkie połaczenie ˛ sieciowe, nawiazywanie ˛ połaczenia ˛ może trwać 10 sekund, a nawet dłużej. Nie powinieneś wi˛ec udost˛epniać przez HTTPS wszystkich swoich danych, a tylko te, których prywat6 ność naprawd˛e trzeba chronić. 3. Tworzenie bezpiecznych gniazd serwera Bezpieczne gniazda klientów to tylko połowa równania. Druga˛ sa˛ gniazda serwerów z obsługa˛ SSL. Sa˛ to instancje klasy javax.net.SSLServerSocket: public abstract class SSLServerSocket extends ServerSocket Podobnie jak w przypadku SSLSocket, wszystkie konstruktory tej klasy sa˛ chronione. Wszystkie instancje SSLServerSocket sa˛ tworzone przez abstrakcyjna˛ klas˛e ”fabryczna”, ˛ javax.net.SSLServerSocketFactory: public abstract class SSLServerSocketFactory extends ServerSocketFactory Tak jak w przypadku SSLSocketFactory, instancja klasy SSLServerSocketFactory jest zwracana przez statyczna˛ metod˛e SSLServerSocketFactory.getDefault(): public static SSLServerSocketFactory.getDefault() Tak jak SSLSocketFactory, SSLServerSocketFactory ma trzy przedefiniowane metody createServerSocket(), zwracajace ˛ instancje klasy SSLServerSocket, których działanie łatwo zrozumieć przez analogi˛e z konstruktorami klasy java.net.ServerSocket: public abstract ServerSocket createServerSocket(int port) throws IOException public abstract ServerSocket createServerSocket(int port, int queueLength) throws IOException public abstract ServerSocket createServerSocket(int port, int aueueLength, ˛ InetAddress interface) throws IOException Jeśli to wystarczyłoby do utworzenia zabezpieczonych gniazd serwerów, wówczas byłyby one nieskomplikowane i łatwe w użyciu. Niestety, potrzeba czegoś wi˛ecej. Fabryka zwracana przez metod˛e SSLServerSocketFactory.get7 Default() obsługuje tylko uwierzytelnianie serwera. Nie obsługuje szyfrowania. Aby połaczenie ˛ było szyfrowane, bezpieczne gniazda serwerów musza˛ być odpowiednio zainicjowane i skonfigurowane. Szczegóły tej operacji zależa˛ od implementacji. W implementacji odniesienia opracowanej przez Suna˛ za tworzenie w pełni skonfigurowanych i zainicjowanych gniazd serwerów odpowiedzialny jest obiekt com.sun.net.ssl.SSLContext. W różnych implementacjach JSSE proces przebiega odmiennie, ale w celu utworzenia zabezpieczonego gniazda w implementacji odniesienia, b˛edziesz musiał: • wygenerować klucze publiczne i certyfikaty za pomoca˛ programu keytool, • zapłacić za uwierzytelnianie twoich certyfikatów przez zaufana˛ stron˛e trzecia,˛ na przykład Yerisign, • utworzyć SSLContext dla używanego algorytmu, • utworzyć TrustManagerFactory jako źródło danych certyfikacyjnych, których b˛edziesz używać, • utworzyć obiekt KeyS tore jako baz˛e danych z kluczami i certyfikatami (w implementacji Suna domyślnie jest to JKS), • wypełnić KeyStore kluczami i certyfikatami, na przykład ładujac ˛ je z pliku przy użyciu hasła, którym sa˛ zaszyfrowane, • zainicjować KeyManagerFactory za pomoca˛ obiektu KeyStore i jego hasła, • zainicjować kontekst za pomoca˛ odpowiednich menedżerów kluczy z KeyManagerFactory, menedżerów zaufania z TrustManagerFactory i źródła danych losowych (ostatnie dwa parametry moga˛ być puste, jeśli chcesz przyjać ˛ wartości domyślne). Poniższy przykład przedstawia t˛e procedur˛e w programie SecureOrderTaker, który przyjmuje zamówienia i wyświetla je na System.out. Oczywiście, w prawdziwej aplikacji zrobiłbyś z tymi zamówieniami coś bardziej interesujace˛ go. Przykład 2. SecureOrderTaker 8 import java.net.*; import java.io.*; import java.util.*; import java. security .*; import javax.net.ssl.*; import javax.net.*; import com.sun.net.ssl.*; public class SecureOrderTaker ( public finał static int DEFAULT_PORT = 7000; public finał static String algorithm = ”SSLv2”; public static void main (String [] args) { int port = DEFAULT_PORT; if (args.length > 0) { try { port = Integer.parselnt(args [0]); if (port <= 0 || port >= 65536) { System.out.println (”Port musi należeć do zakresu 0 - 65535”); return; } } catch (NumberFormatException e) {} } try { SSLContext context = SSLContext.getlnstance(”SSL”); // Implementacja odniesienia obsługuje tylko klucze X.509 KeyManagerFactory kmf = KeyManagerFactory.getlnstance(”SunX509”); // Domyślny rodzaj magazynu kluczy Suna KeyStore ks = KeyStore.getInstance(”JKS”); // Ze wzgl˛edów bezpieczeństwa, każdy magazyn kluczy jest szyfrowany // za pomoca˛ hasła, które należy podać, zanim załadujemy magazyn // z dysku. Hasło jest przechowywane w tablicy char[], aby można // było je szybko usunać ˛ z pami˛eci, zamiast czekać na źbieracza // odpadków". Oczywiście, użycie znakowego literału pozbawia // to rozwiazanie ˛ jego zalet. 9 char[] password = ”2andnotafnord”.toCharArray(); ks.loadfnew FilelnputStream (”jnp2e19.keys”), password); kmf.init(ks, password); // context.init (kmf.getKeyManagers() , null, null); SSLServerSocketFactory factory = context.getServerSocketFactory(); SSLServerSocket server = (SSLServerSocket) factory.createServerSocket (port); // Zakończyliśmy konfiguracj˛e i możemy skupić si˛e // na rzeczywistej komunikacji try { while (true) { // To gniazdo b˛edzie zabezpieczone, // chociaż w kodzie tego nie widać. Socket connection = server.accept(); InputStream in = connection.getInputStream (); int c; while ((c = in.read(() != -1) { System.out.write (c)); } connection.close(); } } // end while } // end try catch (IOException e) { System.err.println(e); } // end catch } // end try catch (IOException e) { e.printStackTrace(); } // end catch catch (KeyManagementException e) { e.printStackTrace(); } // end catch 10 catch (KeyStoreException e) { e.printStackTrace(); } // end catch catch (NoSuchAlgorithmException e) { e.printStackTrace(); } // end catch catch (java.security.cert.CertificateException e) { e.printStackTrace(); } // end catch catch (UnrecoverableKeyException e) { e.printStackTrace (); } // end catch } // end main } // end SecureOrderTaker 11