Czy Java obsługuje certyfikaty Let's Encrypt?

125

Tworzę aplikację Java, która wysyła zapytania do interfejsu API REST na zdalnym serwerze za pośrednictwem protokołu HTTP. Ze względów bezpieczeństwa ta komunikacja powinna zostać przełączona na HTTPS.

Teraz, gdy Let's Encrypt rozpoczęło swoją publiczną wersję beta, chciałbym wiedzieć, czy Java obecnie działa (lub jest potwierdzona, że ​​będzie działać w przyszłości) z ich certyfikatami domyślnie.

Let's Encrypt otrzymało pośredni podpis krzyżowy przez IdenTrust , co powinno być dobrą wiadomością. Jednak nie mogę znaleźć żadnego z tych dwóch w danych wyjściowych tego polecenia:

keytool -keystore "..\lib\security\cacerts" -storepass changeit -list

Wiem, że zaufane urzędy certyfikacji można dodać ręcznie na każdym komputerze, ale ponieważ moja aplikacja powinna być bezpłatna do pobrania i wykonywalna bez dalszej konfiguracji, szukam rozwiązań, które działają „po wyjęciu z pudełka”. Czy masz dla mnie dobrą wiadomość?

Heksaholic
źródło
1
Można również sprawdzić kompatybilność Let's Encrypt tutaj letsencrypt.org/docs/certificate-compatibility
potame
@potame "z Javą 8u131 nadal musisz dodać swój certyfikat do swojego truststore", więc jeśli otrzymasz certyfikat z Let's Encrypt, będziesz musiał dodać otrzymany certyfikat do truststore? Czy nie powinno wystarczyć uwzględnienie ich CA?
mxro
1
@mxro Cześć - dziękuję za zwrócenie na to mojej uwagi. Moje powyższe komentarze wcale nie są prawdziwe (w rzeczywistości problem był bardziej skomplikowany i związany z naszą infrastrukturą) i zamierzam je usunąć, ponieważ w rzeczywistości prowadzą one tylko do zamieszania. Jeśli więc masz jdk> Java 8u101, certyfikat Let's Encrypt powinien działać i być odpowiednio rozpoznawany i zaufany.
potame
@potame To świetnie. Dziękuję za wyjaśnienie!
mxro

Odpowiedzi:

142

[ Aktualizacja 2016-06-08 : Według https://bugs.openjdk.java.net/browse/JDK-8154757, IdenTrust CA zostanie uwzględniony w Oracle Java 8u101.]

[ Aktualizacja 2016-08-05 : wydano Java 8u101 i rzeczywiście zawiera ona IdenTrust CA: informacje o wersji ]


Czy Java obsługuje certyfikaty Let's Encrypt?

Tak. Certyfikat Let's Encrypt to zwykły certyfikat klucza publicznego. Java to obsługuje (zgodnie z Let's Encrypt Certificate Compatibility , dla Java 7> = 7u111 i Java 8> = 8u101).

Czy Java ufa certyfikatom Let's Encrypt po wyjęciu z pudełka?

Nie / to zależy od JVM. Truststore Oracle JDK / JRE do 8u66 nie zawiera ani konkretnego urzędu certyfikacji Let's Encrypt, ani urzędu certyfikacji IdenTrust, który go podpisał. new URL("https://letsencrypt.org/").openConnection().connect();na przykład powoduje javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException.

Można jednak udostępnić własny walidator / zdefiniować niestandardowy magazyn kluczy zawierający wymagany główny ośrodek certyfikacji lub zaimportować certyfikat do magazynu zaufanych certyfikatów maszyny JVM.

https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/10 również omawia ten temat.


Oto przykładowy kod, który pokazuje, jak dodać certyfikat do domyślnego magazynu zaufanych certyfikatów w czasie wykonywania. Musisz tylko dodać certyfikat (wyeksportowany z przeglądarki Firefox jako .der i wstawić ścieżkę klas)

Na podstawie Jak mogę uzyskać listę zaufanych certyfikatów głównych w Javie? i http://developer.android.com/training/articles/security-ssl.html#UnknownCa

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManagerFactory;

public class SSLExample {
    // BEGIN ------- ADDME
    static {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            Path ksPath = Paths.get(System.getProperty("java.home"),
                    "lib", "security", "cacerts");
            keyStore.load(Files.newInputStream(ksPath),
                    "changeit".toCharArray());

            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            try (InputStream caInput = new BufferedInputStream(
                    // this files is shipped with the application
                    SSLExample.class.getResourceAsStream("DSTRootCAX3.der"))) {
                Certificate crt = cf.generateCertificate(caInput);
                System.out.println("Added Cert for " + ((X509Certificate) crt)
                        .getSubjectDN());

                keyStore.setCertificateEntry("DSTRootCAX3", crt);
            }

            if (false) { // enable to see
                System.out.println("Truststore now trusting: ");
                PKIXParameters params = new PKIXParameters(keyStore);
                params.getTrustAnchors().stream()
                        .map(TrustAnchor::getTrustedCert)
                        .map(X509Certificate::getSubjectDN)
                        .forEach(System.out::println);
                System.out.println();
            }

            TrustManagerFactory tmf = TrustManagerFactory
                    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(keyStore);
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, tmf.getTrustManagers(), null);
            SSLContext.setDefault(sslContext);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    // END ---------- ADDME

    public static void main(String[] args) throws IOException {
        // signed by default trusted CAs.
        testUrl(new URL("https://google.com"));
        testUrl(new URL("https://www.thawte.com"));

        // signed by letsencrypt
        testUrl(new URL("https://helloworld.letsencrypt.org"));
        // signed by LE's cross-sign CA
        testUrl(new URL("https://letsencrypt.org"));
        // expired
        testUrl(new URL("https://tv.eurosport.com/"));
        // self-signed
        testUrl(new URL("https://www.pcwebshop.co.uk/"));

    }

    static void testUrl(URL url) throws IOException {
        URLConnection connection = url.openConnection();
        try {
            connection.connect();
            System.out.println("Headers of " + url + " => "
                    + connection.getHeaderFields());
        } catch (SSLHandshakeException e) {
            System.out.println("Untrusted: " + url);
        }
    }

}
zapl
źródło
Jeszcze jedno: którego pliku muszę użyć? Let's Encrypt oferuje do pobrania jeden główny i cztery pośrednie certyfikaty . Próbowałem isrgrootx1.deri lets-encrypt-x1-cross-signed.der, ale żaden z nich nie wydaje się być właściwy.
Hexaholic
@Hexaholic Zależy od tego, czemu chcesz ufać i jak witryna, której używasz, ma skonfigurowane certyfikaty. Idź https://helloworld.letsencrypt.orgna przykład i sprawdź łańcuch certyfikatów w przeglądarce (klikając zieloną ikonę). Do tego potrzebny jest certyfikat specyficzny dla witryny, pośredni certyfikat X1 (podpisany krzyżowo przez IdenTrust) lub certyfikat DSTRootCAX3. ISRG Root X1Nie działa na stronie helloworld ponieważ nie jest w łańcuchu, który jest alternatywą łańcuch. Użyłbym DSTRoot, po prostu wyeksportowanego przez przeglądarkę, ponieważ nigdzie nie widziałem go do pobrania.
zapl
1
Dziękuję za kod. Nie zapomnij zamknąć InputStream w keyStore.load (Files.newInputStream (ksPath)).
Michael Wyraz
3
@ adapt-dev Nie, dlaczego oprogramowanie powinno ufać nieznanemu systemowi, który dostarcza dobre certyfikaty? Oprogramowanie musi ufać certyfikatom, którym faktycznie ufa. Byłaby to luka w zabezpieczeniach, gdyby oznaczało to, że mój program java mógł zainstalować certyfikaty dla czyjegoś programu. Ale tak się nie dzieje, kod dodaje certyfikat tylko podczas jego własnego działania
zapl
3
+1 za pokazanie kodu, który nie tylko oszukuje, ustawiając javax.net.ssl.trustStorewłaściwość systemową, ale -1 za to ustawienie wartości domyślnej maszyny JVM SSLContext. Lepiej byłoby utworzyć nową, SSLSocketFactorya następnie użyć jej dla różnych połączeń (np. HttpUrlConnection), Zamiast zastępować konfigurację SSL dla całej maszyny wirtualnej. (Zdaję sobie sprawę, że ta zmiana jest skuteczna tylko dla działającej maszyny JVM i nie utrzymuje się ani nie wpływa na inne programy. Myślę tylko, że lepszą praktyką programistyczną jest wyraźne określenie, gdzie ma zastosowanie twoja konfiguracja.)
Christopher Schultz
65

Wiem, że OP poprosił o rozwiązanie bez lokalnych zmian konfiguracji, ale na wypadek, gdybyś chciał trwale dodać łańcuch zaufania do magazynu kluczy:

$ keytool -trustcacerts \
    -keystore $JAVA_HOME/jre/lib/security/cacerts \
    -storepass changeit \
    -noprompt \
    -importcert \
    -file /etc/letsencrypt/live/hostname.com/chain.pem

źródło: https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/13

Jan Berkel
źródło
6
Tak, OP nie prosił o to, ale było to dla mnie zdecydowanie najprostsze rozwiązanie!
Auspex
Zaimportowałem ten certyfikat letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt do mojej starszej kompilacji java 8, a usługa sieciowa korzystająca z certyfikatu lets encrypt jest nieosiągalna! To rozwiązanie było szybkie i łatwe.
ezwrighter
57

Szczegółowa odpowiedź dla tych z nas, którzy chcą dokonać lokalnych zmian konfiguracyjnych, które obejmują utworzenie kopii zapasowej pliku konfiguracyjnego:

1. Sprawdź, czy działa przed zmianami

Jeśli nie masz jeszcze programu testowego, możesz użyć mojego programu ping SSLPing java, który testuje uzgadnianie TLS (będzie działać z dowolnym portem SSL / TLS, nie tylko HTTPS). Użyję gotowego pliku SSLPing.jar, ale czytanie kodu i samodzielne tworzenie go to szybkie i łatwe zadanie:

$ git clone https://github.com/dimalinux/SSLPing.git
Cloning into 'SSLPing'...
[... output snipped ...]

Ponieważ moja wersja Java jest starsza niż 1.8.0_101 (nie została wydana w momencie pisania tego tekstu), certyfikat Let's Encrypt nie będzie domyślnie weryfikowany. Zobaczmy, jak wygląda awaria przed zastosowaniem poprawki:

$ java -jar SSLPing/dist/SSLPing.jar helloworld.letsencrypt.org 443
About to connect to 'helloworld.letsencrypt.org' on port 443
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
[... output snipped ...]

2. Zaimportuj certyfikat

Pracuję na Mac OS X z ustawioną zmienną środowiskową JAVA_HOME. Późniejsze polecenia przyjmą, że ta zmienna jest ustawiona dla modyfikowanej instalacji Java:

$ echo $JAVA_HOME 
/Library/Java/JavaVirtualMachines/jdk1.8.0_92.jdk/Contents/Home/

Utwórz kopię zapasową pliku cacerts, który będziemy modyfikować, aby móc cofnąć wszelkie zmiany bez ponownej instalacji JDK:

$ sudo cp -a $JAVA_HOME/jre/lib/security/cacerts $JAVA_HOME/jre/lib/security/cacerts.orig

Pobierz certyfikat podpisujący, który musimy zaimportować:

$ wget https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.der

Wykonaj import:

$ sudo keytool -trustcacerts -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit -noprompt -importcert -alias lets-encrypt-x3-cross-signed -file lets-encrypt-x3-cross-signed.der 
Certificate was added to keystore

3. Sprawdź, czy działa po zmianach

Sprawdź, czy Java jest teraz zadowolona z połączenia z portem SSL:

$ java -jar SSLPing/dist/SSLPing.jar helloworld.letsencrypt.org 443
About to connect to 'helloworld.letsencrypt.org' on port 443
Successfully connected
dimalinux
źródło
8

W przypadku JDK, które jeszcze nie obsługują certyfikatów Let's Encrypt, możesz je dodać do JDK cacertspo tym procesie (dzięki temu ).

Pobierz wszystkie certyfikaty z https://letsencrypt.org/certificates/ (wybierz format der ) i dodawaj je jeden po drugim za pomocą tego rodzaju polecenia (przykład dla letsencryptauthorityx1.der):

keytool -import -keystore PATH_TO_JDK\jre\lib\security\cacerts -storepass changeit -noprompt -trustcacerts -alias letsencryptauthorityx1 -file PATH_TO_DOWNLOADS\letsencryptauthorityx1.der
Anthony O.
źródło
Poprawia to sytuację, ale
pojawia