Konfiguracja bezpiecznego połączenia w Javie
Standardowy pakiet Javy, który zapewnia warstwę abstrakcji umożliwiającą bezpieczną komunikację sieciową (zarządzanie certyfikatami, handshaking i weryfikacja) to javax.net.ssl
. Prawdopodobnie najpopularniejszym protokołem, z którym programiści mają do czynienia w tym obszarze, jest HTTPS. HTTPS jest bezpieczną wersją protokołu HTTP (RFC 2616). Może być on zaimplementowany poprzez wykorzystanie protokołu niższej warstwy modelu sieci, np. SSL bądź bezpieczniejszą i zaktualizowaną odpowiednikiem – protokołem TLS.
Protokół | Publikacja | Wsparcie stron internetowych | Bezpieczeństwo |
---|---|---|---|
SSL 1.0 | Nieopublikowany | ||
SSL 2.0 | 1995 | 1.6% | Niebezpieczny |
SSL 3.0 | 1996 | 6.7% | Niebezpieczny |
TLS 1.0 | 1999 | 65.0% | Zależy szyfru i ograniczeń po stronie klienta |
TLS 1.1 | 2006 | 75.1% | Zależy szyfru i ograniczeń po stronie klienta |
TLS 1.2 | 2008 | 96.0% | Zależy szyfru i ograniczeń po stronie klienta |
TLS 1.3 | 2018 | 18.4% | Bezpieczny |
Źródła: https://en.wikipedia.org/wiki/Transport_Layer_Security, https://www.ssllabs.com/ssl-pulse/
Obsługa różnych wersji protokołów i szyfrów w Javie jest realizowana w formie architektury bezpieczeństwa typu pluggable za pośrednictwem tzw. dostawców zabezpieczeń. Domyślnie co najmniej jeden dostawca zabezpieczeń (provider) jest dystrybuowany wraz z JRE/JDK, a w razie potrzeby możemy dodać własnego dostawcę.
Na przykład w chwili pisania tego artykułu Oracle JRE8/JDK8 nie zapewniają obsługi protokołu TLS 1.3, chociaż jest to planowane na 2020-07-14. Tymczasem możemy cieszyć się z TLS 1.3 w Javie 11 i na JVM/JDK Javy 8 od Azul Systems (Zing/Zulu).
Konfiguracja bezpiecznego połączenia
Wcześniej wspomniany security provider dostarczany jest do klasy SSLContext
, którego następnie używamy do skonfigurowania połączenia. Domyślne obsługiwane protokoły zawarte są w parametrach kontekstu SSLContext.getDefault().getSupportedSSLParameters().getProtocols()
. Aby ograniczyć listę tylko do wybranych protokołów, możemy użyć metody setEnabledProtocols(String[] protocols)
klasy SSLContext
.
Przed tym rzućmy najpierw okiem na elementy potrzebne do inicjalizacji kontekstu:
Klasa | Opis | Przykład użycia |
---|---|---|
SSL | Warstwa abstrakcji pozwalająca na konfigurację połączenia SSL/TLS przy użyciu certyfikatów zawartych w zarządzanych trust/key store'ach. |
|
Trust | Przechowuje zaufane certyfikaty z punktu widzenia klienta. | |
Key | Przechowuje certyfikat naszej tożsamości. |
|
Trust / Key | Fabryki do zarządzania magazynami kluczy, które mogą być dostarczane przez środowisko bądź zainicjalizowane z konkretnego keystore'a. Możemy również pominąć inicjalizację za pomocą fabryki, dostarczając własną implementację menadżera. |
|
Key | Udostępnia klientowi łańcuch certyfikatów wraz z kluczem publicznym oraz przechowuje klucz prywatny do odszyfrowywania danych zaszyfrowanych przez klucz publiczny. |
Wiemy, że hasła są powiązane z magazynami kluczy, ale klucze prywatne również mogą mieć hasła. Ponieważ interfejs KeyManagera nie udostępnia możliwości podania hasła dla klucza prywatnego, gdy używany jest standardowy algorytm "SunX509" przy tworzeniu samej fabryki, zakłada się, że jest ono takie samo jak hasło do keystore'a.
Jeśli jednak użyjemy algorytmu „NewSunX509”, możemy rozwiązać ten problem – szczegółowiej zostało to wyjaśniene przz Willa Argenta. |
Trust | Decyduje czy odrzucić połączenie na podstawie danych uwierzytelniających podanych przez drugą stronę. | |
Hostname | Podczas połączenia SSL/TLS w celu zapobiegania atakom MITM (Man In The Middle) zaleca się sprawdzenie, czy docelowa nazwa hosta jest taka sama jak nazwa podana w certyfikacie. |
Trzy popularne implementacje można znaleźć w bibliotece Apache HttpComponents:
|
Większość klientów HTTP obsługuje konfigurację połączenia za pomocą klasy SSLContext. Zasadniczo domyślna konfiguracja zapewniana przez JDK/JRE często wystarcza przy inicjalizacji bezpiecznego połączenia jako klient. O ile oczywiście serwer nie wymaga również od nas ważnego certyfikatu. W takim przypadku będziemy musieli przekazać naszą tożsamość za pomocą KeyManagera.
Kilka przykładów ostatecznego ogniwa między konfiguracją połączenia a klasami klientów HTTP zamieściłem poniżej:
// javax.net.ssl
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());
connection.setHostnameVerifier(hostnameVerifier);
// org.apache.httpcomponents:httpclient:4.5
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setSSLContext(sslContext)
.setHostnameVerifier(hostnameVerifier)
.build();
// com.squareup.okhttp3:okhttp:4.x
OkHttpClient okHttpClient = OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(), trustManager)
.hostnameVerifier(hostnameVerifier)
.build()
// org.glassfish.jersey.core:jersey-client:2.x
Client jerseyClient = ClientBuilder.newBuilder()
.sslContext(sslContext)
.hostnameVerifier(hostnameVerifier)
.build();
Na potrzeby zarządzania keystore'ami, tworzenia CSR (Certificate Signing Request – żądania podpisania przez urząd certyfikacji) używa się programu wiersza poleceń keytool
zawartego w katalogu bin
JRE/JDK. Listę przydatnych poleceń można podejrzeć na stronie SSL Shopper.
W razie wątpliwości, dlaczego standardowa konfiguracja nie działa, zawsze warto sprawdzić ważność certyfikatu witryny, z którą się łączymy, nazwę domeny i łańcuch poświadczeń. No chyba że certyfikat jest self-signed i zaimportowaliśmy go do trust store'a (również to powinniśmy sprawdzić).
Często jednak sprawdzanie certyfikatu na serwerach za pomocą przeglądarki nie jest wykonalnym scenariuszem, gdyż zazwyczaj nie udostępniają one powłoki graficznej. W takim przypadku możemy skorzystać z narzędzi wiersza polecenia, takich jak curl lub openssl, w celu wyodrębnienia certyfikatu.