´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

Podobne dokumenty