Przewodnik programisty Java po certyfikatach SSL

Przegląd

Tworząc aplikacje internetowe często potrzebujemy integracji z innymi aplikacjami wykorzystującymi SSL. Może to dotyczyć różnych protokołów, takich jak HTTPS, IMAPS lub LDAPS.

W tym artykule omówimy, co programiści Java powinni wiedzieć o certyfikatach SSL.

Łańcuch certyfikatów

Połączenie SSL powiedzie się tylko wtedy, gdy klient może zaufać serwerowi. Przyjrzyjmy się, jak działa ten model zaufania.

W przeglądarce Chrome przejdź na stronę google.com i otwórz Narzędzia programistyczne (F12 w systemie Windows, Cmd+Option+i na komputerze Mac).

Na karcie Bezpieczeństwo kliknij przycisk Wyświetl certyfikat, aby wyświetlić szczegółowe informacje o certyfikacie.

Widzimy, że certyfikat witryny jest częścią łańcucha. Ten konkretny łańcuch składa się z 3 certyfikatów.

Certyfikat witryny został wystawiony przez certyfikat o nazwie Google Internet Authority G2. To jest certyfikat pośredni. Z kolei certyfikat pośredni wydawany jest przez certyfikat głównyGeoTrust Global CA.

Kiedy nawiążemy połączenie poprzez HTTPS, serwer WWW odpowie, podając swoją witrynę i certyfikaty pośrednie. Następnie do klienta należy dokończenie łańcucha poprzez posiadanie certyfikatu głównego. Ta weryfikacja łańcucha jest konieczna, aby klient mógł zaufać witrynie.

Ponieważ Chrome ma certyfikat główny GeoTrust Global CA w swojej bazie certyfikatów, nasze połączenie powiedzie się i nie zostaną wyświetlone żadne błędy ani ostrzeżenia.

Uwaga: w zależności od lokalizacji w łańcuchu może znajdować się więcej niż jeden certyfikat pośredni.

Certyfikaty z podpisem własnym

Certyfikaty nie wystawione przez znany urząd certyfikacji, ale przez serwer hostujący certyfikat, nazywane są certyfikatami samopodpisanymi.

Są one często używane w wewnętrznych środowiskach programistycznych, które nie są skierowane do klienta.

Certyfikaty główne dla nich będą nieobecne w magazynie certyfikatów przeglądarki.

Przykład certyfikatu z podpisem własnym znajduje się na stronie https://self-signed.badssl.com. Widzimy, że zostało to wydane przez Niezaufany urząd certyfikacji Avast, którego przeglądarka nie rozpoznaje, więc wyświetla ostrzeżenie.

Magazyn zaufanych certyfikatów Java i magazyn kluczy

W tej sekcji omówimy, gdzie znajdują się certyfikaty w systemie, w którym jest zainstalowane JDK/JRE.

Sklep zaufania

Truststore to plik zawierający certyfikaty główne dla urzędów certyfikacji (CA), które wydają certyfikaty, takie jak GoDaddy, Verisign, Network Solutions i inne.

Magazyn zaufanych certyfikatów jest dostarczany w pakiecie z JDK/JRE i znajduje się w $JAVA_HOME/lib/security/cacerts.

Magazyn zaufanych certyfikatów jest używany za każdym razem, gdy nasz kod Java nawiązuje połączenie przez SSL.

Magazyn kluczy

Plik kluczy to plik używany przez serwer aplikacji do przechowywania jego klucza prywatnego i certyfikatu witryny.

Jeśli więc uruchamialibyśmy aplikację internetową za pośrednictwem protokołu SSL pod adresem tomcat.codebyamir.com, plik magazynu kluczy o nazwie keystore.jks zawierałby dwa wpisy — jeden dotyczący klucza prywatnego i jeden dotyczący certyfikatu.

Magazyn kluczy jest używany przez serwery aplikacji Java, takie jak Tomcat, do obsługi certyfikatów.

Uwaga: większość serwerów aplikacji Java odczytuje zawartość tych plików tylko podczas uruchamiania. Oznacza to, że wszelkie aktualizacje pliku wymagają ponownego uruchomienia, aby zaczęły obowiązywać.

Główne narzędzie

Keytool to narzędzie dołączone do środowiska JRE służące do zarządzania parami kluczy i certyfikatami. Dzięki temu możemy przeglądać/modyfikować/tworzyć bazy certyfikatów w świecie Java.

Wyświetl listę certyfikatów w magazynie zaufanych certyfikatów

keytool -list -keystore $JAVA_HOME/lib/security/cacerts

Zostaniemy poproszeni o podanie hasła do magazynu zaufanych certyfikatów. Domyślne hasło to „changeit”.

Ten magazyn zaufanych certyfikatów zawiera 104 wpisy, a każdy wpis ma unikalny alias i odcisk palca. Dla zwięzłości skróciliśmy poniższe dane wyjściowe.

Keystore type: JKS
Keystore provider: SUN
Your keystore contains 104 entries
verisignclass2g2ca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): B3:EA:C4:47:76:C9:C8:1C:EA:F2:9D:95:B6:CC:A0:08:1B:67:EC:9D
digicertassuredidg3 [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): F5:17:A2:4F:9A:48:C6:C9:F8:A2:00:26:9F:DC:0F:48:2C:AB:30:89
verisignuniversalrootca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): 36:79:CA:35:66:87:72:30:4D:30:A5:FB:87:3B:0F:A7:7B:B7:0D:54
digicerttrustedrootg4 [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): DD:FB:16:CD:49:31:C9:73:A2:03:7D:3F:C8:3A:4D:7D:77:5D:05:E4
verisignclass1g3ca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): 20:42:85:DC:F7:EB:76:41:95:57:8E:13:6B:D4:B7:D1:E9:8E:46:A5
identrustpublicca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): BA:29:41:60:77:98:3F:F4:F3:EF:F2:31:05:3B:2E:EA:6D:4D:45:FD
utnuserfirstobjectca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): E1:2D:FB:4B:41:D7:D9:C3:2B:30:51:4B:AC:1D:81:D8:38:5E:2D:46
geotrustuniversalca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): E6:21:F3:35:43:79:05:9A:4B:68:30:9D:8A:2F:74:22:15:87:EC:79
digicertglobalrootg3 [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): 7E:04:DE:89:6A:3E:66:6D:00:E6:87:D3:3F:FA:D9:3B:E8:3D:34:9E
deutschetelekomrootca2 [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): 85:A4:08:C0:9C:19:3E:5D:51:58:7D:CD:D6:13:30:FD:8C:DE:37:BF
...

Korzystając z wcześniejszego przykładu google.com, przyjrzyjmy się odciskowi palca GeoTrust Global CA z naszej przeglądarki:

Odcisk palca SHA-1 to DE:28:F4:A4:FF:E5:B9:2F:A3:C5:03:D1:A3:49:A7:F9:96:2A:82:12

Poszukajmy tego w naszym zaufanym sklepie:

keytool -list -keystore $JAVA_HOME/lib/security/cacerts | grep -B1 -i DE:28
Enter keystore password:  changeit
geotrustglobalca [jdk], Aug 25, 2016, trustedCertEntry,
Certificate fingerprint (SHA1): DE:28:F4:A4:FF:E5:B9:2F:A3:C5:03:D1:A3:49:A7:F9:96:2A:82:12

Dane wyjściowe mówią nam, że certyfikat znajduje się w magazynie zaufanych certyfikatów:

Co to oznacza dla programisty Java?

Oznacza to, że kod łączący się z https://www.google.com nie wygeneruje wyjątku z powodu błędu uzgadniania SSL.

Dodaj certyfikat do magazynu zaufanych certyfikatów

Dodanie certyfikatu do zaufanego magazynu jest konieczne, jeśli chcemy zaufać certyfikatowi wystawionemu przez urząd certyfikacji, którego nie ma w dołączonym magazynie zaufanych certyfikatów.

keytool -import -trustcacerts -file [certificate] -alias [alias] -keystore $JAVA_HOME/lib/security/cacerts

Przykład kodu

Poniżej znajduje się kod Java, który połączy się z adresem URL i wydrukuje zawartość strony na ekranie.

Łączenie się z witryną za pomocą zaufanego certyfikatu

Wypróbujmy nasz kod na innej stronie z ważnym certyfikatem SSL.

Kod

Zastąp linię 12 kodu następującą linią:

private static final String URL = "https://httpbin.org/user-agent";

Wyjście

Widzimy, że wynik kodu pomyślnie pokazuje, że nasz ciąg agenta użytkownika to nasza wersja Java.

{
  "user-agent": "Java/1.8.0_131"
}

Łączenie się z witryną przy użyciu niezaufanego certyfikatu

Wypróbujmy nasz kod na stronie z certyfikatem z podpisem własnym.

Kod

Zastąp linię 12 kodu następującą linią:

private static final String URL = "https://self-signed.badssl.com";

Wyjście

Kod zgłasza SSLHandshakeException, ponieważ Java mu nie ufa.

Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
	at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
	at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949)
	at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302)
	at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296)
	at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1514)
	at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216)
	at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1026)
	at sun.security.ssl.Handshaker.process_record(Handshaker.java:961)
	at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062)
	at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)
	at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
	at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1546)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474)
	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)
	at com.codebyamir.ssl.App.main(App.java:18)

Jeśli chcielibyśmy zaufać certyfikatowi z podpisem własnym z poprzedniego przykładu, moglibyśmy dodać jego certyfikat główny do naszego magazynu zaufanych certyfikatów, korzystając z polecenia opisanego wcześniej w sekcji narzędzia kluczy.

Po dodaniu certyfikatu ponowne uruchomienie kodu pomyślnie wyświetli zawartość strony:

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="shortcut icon" href="/pl/icons/favicon-red.ico"/>
  <link rel="apple-touch-icon" href="/pl/icons/icon-red.png"/>
  <title>self-signed.badssl.com</title>
  <link rel="stylesheet" href="/pl/style.css">
  <style>body { background: red; }</style>
</head>
<body>
<div id="content">
  <h1 style="font-size: 12vw;">
    self-signed.<br>badssl.com
  </h1>
</div>
</body>
</html>

Typowe wyjątki podczas sprawdzania poprawności protokołu SSL

Certyfikat wygasł

Łącząc się z witryną z wygasłym certyfikatem SSL, zobaczymy następujący wyjątek:

java.security.cert.CertPathValidatorException: timestamp check failed

Niewłaściwa nazwa zwyczajowa (CN)

Łącząc się z witryną o nazwie certyfikatu innej niż nazwa hosta, zobaczymy następujący wyjątek:

java.security.cert.CertificateException: No subject alternative DNS name matching wrong.host.badssl.com found.

Często zadawane pytania

Czy magazyn zaufanych certyfikatów JRE cacerts jest aktualizowany?

Tak, nowe wersje Oracle JDK/JRE będą w razie potrzeby dodawać nowe certyfikaty do magazynu zaufanych certyfikatów.

Jak mogę powiedzieć Javie, aby korzystała z niestandardowego magazynu zaufanych certyfikatów?

Dodaj następującą właściwość JVM podczas uruchamiania aplikacji:

-Djavax.net.ssl.trustStore=/app/security/truststore.jks

Jeśli hasło zaufanych certyfikatów jest inne niż „changeit”, podaj także hasło:

-Djavax.net.ssl.trustStorePassword=myTrustStorePassword

Jak mogę sprawdzić, czy witryna wysyła certyfikat pośredni?

Aby to sprawdzić, możemy użyć narzędzia openssl w systemie Linux:

openssl s_client -showcerts -connect google.com:443