Apakah Java mendukung sertifikat Let's Encrypt?

125

Saya sedang mengembangkan aplikasi Java yang meminta REST API di server jarak jauh melalui HTTP. Untuk alasan keamanan, komunikasi ini harus dialihkan ke HTTPS.

Sekarang Let's Encrypt memulai beta publik mereka, saya ingin tahu apakah Java saat ini berfungsi (atau dipastikan akan berfungsi di masa depan) dengan sertifikat mereka secara default.

Let's Encrypt mendapat tanda tangan silang perantara mereka oleh IdenTrust , yang seharusnya menjadi kabar baik. Namun, saya tidak dapat menemukan salah satu dari keduanya di output dari perintah ini:

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

Saya tahu bahwa CA tepercaya dapat ditambahkan secara manual di setiap mesin, tetapi karena aplikasi saya seharusnya dapat diunduh dan dijalankan secara gratis tanpa konfigurasi lebih lanjut, saya mencari solusi yang berfungsi "di luar kotak". Apakah Anda punya kabar baik untuk saya?

Hexaholic
sumber
1
Seseorang juga dapat memeriksa kompatibilitas Let's Encrypt di sini letsencrypt.org/docs/certificate-compatibility
potame
@potame "dengan Java 8u131 Anda masih harus menambahkan sertifikat Anda ke truststore" jadi jika Anda mendapatkan sertifikat dari Let's Encrypt, Anda perlu menambahkan sertifikat yang Anda dapatkan ke truststore? Bukankah seharusnya cukup memasukkan CA mereka?
mxro
1
@mxro Hai - terima kasih telah menarik perhatian saya ke hal ini. Komentar saya di atas tidak benar sama sekali (sebenarnya masalahnya lebih rumit dari itu dan terkait dengan infrastruktur kami) dan saya akan menghapusnya karena memang hanya menimbulkan kebingungan. Jadi, jika Anda memiliki jdk> Java 8u101, sertifikat Let's Encrypt harus berfungsi dan dikenali serta dipercaya dengan benar.
potame
@potame Itu luar biasa. Terimakasih atas klarifikasinya!
mxro

Jawaban:

142

[ Pembaruan 2016-06-08 : Menurut https://bugs.openjdk.java.net/browse/JDK-8154757 , IdenTrust CA akan disertakan dalam Oracle Java 8u101.]

[ Pembaruan 2016-08-05 : Java 8u101 telah dirilis dan memang menyertakan IdenTrust CA: catatan rilis ]


Apakah Java mendukung sertifikat Let's Encrypt?

Iya. Sertifikat Let's Encrypt hanyalah sertifikat kunci publik biasa. Java mendukungnya (menurut Let's Encrypt Certificate Compatibility , untuk Java 7> = 7u111 dan Java 8> = 8u101).

Apakah Java mempercayai sertifikat Let's Encrypt di luar kotak?

Tidak / itu tergantung pada JVM. Truststore Oracle JDK / JRE hingga 8u66 tidak berisi Let's Encrypt CA secara khusus maupun IdenTrust CA yang menandatanganinya. new URL("https://letsencrypt.org/").openConnection().connect();misalnya menghasilkan javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException.

Namun Anda dapat memberikan validator Anda sendiri / menentukan keystore khusus yang berisi CA root yang diperlukan atau mengimpor sertifikat ke truststore JVM.

https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/10 juga membahas topik tersebut.


Berikut beberapa contoh kode yang menunjukkan cara menambahkan sertifikat ke truststore default pada waktu proses. Anda hanya perlu menambahkan sertifikat (diekspor dari firefox sebagai .der dan dimasukkan ke classpath)

Berdasarkan Bagaimana cara mendapatkan daftar sertifikat dasar tepercaya di Java? dan 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
sumber
Satu hal lagi: File mana yang perlu saya gunakan? Let's Encrypt menawarkan satu root dan empat sertifikat perantara untuk diunduh. Saya mencoba isrgrootx1.derdan lets-encrypt-x1-cross-signed.der, tetapi tidak satu pun dari mereka tampaknya yang benar.
Hexaholic
@Hexaholic Tergantung pada apa yang ingin Anda percayai dan bagaimana situs yang Anda gunakan memiliki konfigurasi sertifikat. Pergi ke https://helloworld.letsencrypt.orgmisalnya dan periksa rantai sertifikat di browser (mengklik ikon hijau). Untuk yang satu ini, Anda memerlukan sertifikat khusus situs, sertifikat perantara X1 (ditandatangani silang oleh IdenTrust) atau yang DSTRootCAX3. Tidak ISRG Root X1berfungsi untuk situs helloworld karena tidak ada dalam rantai, itu adalah rantai alternatif. Saya akan menggunakan DSTRoot, cukup diekspor melalui browser karena saya belum melihatnya untuk diunduh di mana pun.
zapl
1
Terima kasih untuk kodenya. Jangan lupa untuk menutup InputStream di keyStore.load (Files.newInputStream (ksPath)).
Michael Wyraz
3
@ adapt-dev Tidak, mengapa perangkat lunak harus mempercayai sistem yang tidak dikenal untuk memberikan sertifikat yang baik? Perangkat lunak harus dapat mempercayai sertifikat yang dipercayai. Ini akan menjadi lubang keamanan jika ini berarti bahwa program java saya dapat menginstal sertifikat untuk program orang lain. Tapi itu tidak terjadi di sini, kode hanya menambahkan sertifikat selama runtime itu sendiri
zapl
3
1 untuk menampilkan kode yang tidak hanya menipu dengan mengatur javax.net.ssl.trustStoreproperti sistem, tetapi -1 untuk kemudian mengatur default JVM SSLContext. Akan lebih baik untuk membuat yang baru SSLSocketFactorydan kemudian menggunakannya untuk berbagai koneksi (misalnya HttpUrlConnection) daripada mengganti konfigurasi SSL di seluruh VM. (Saya menyadari perubahan ini hanya efektif untuk menjalankan JVM dan tidak bertahan atau mempengaruhi program lain. Saya hanya berpikir itu adalah praktik pemrograman yang lebih baik untuk secara eksplisit tentang di mana konfigurasi Anda berlaku.)
Christopher Schultz
65

Saya tahu OP meminta solusi tanpa perubahan konfigurasi lokal, tetapi jika Anda ingin menambahkan rantai kepercayaan ke keystore secara permanen:

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

sumber: https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/13

Jan Berkel
sumber
6
Ya, OP tidak memintanya, tapi sejauh ini solusi paling sederhana untuk saya!
Auspex
Saya mengimpor sertifikat ini letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt ke dalam java 8 build lama saya dan layanan web yang menggunakan sertifikat lets encrypt tidak dapat dijangkau! Solusi ini cepat dan mudah.
ezwrighter
56

Jawaban mendetail bagi kami yang ingin membuat perubahan konfigurasi lokal yang mencakup mencadangkan file konfigurasi:

1. Uji apakah itu berfungsi sebelum perubahan

Jika Anda belum memiliki program pengujian, Anda dapat menggunakan program ping SSLPing java saya yang menguji handshake TLS (akan bekerja dengan port SSL / TLS apa pun, bukan hanya HTTPS). Saya akan menggunakan SSLPing.jar prebuilt, tetapi membaca kode dan membuatnya sendiri adalah tugas yang cepat dan mudah:

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

Karena versi Java saya lebih awal dari 1.8.0_101 (tidak dirilis pada saat penulisan ini), sertifikat Let's Encrypt tidak akan diverifikasi secara default. Mari kita lihat seperti apa kegagalan sebelum menerapkan perbaikan:

$ 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. Impor sertifikat

Saya menggunakan Mac OS X dengan kumpulan variabel lingkungan JAVA_HOME. Perintah selanjutnya akan menganggap variabel ini disetel untuk instalasi java yang Anda ubah:

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

Buat cadangan file cacerts yang akan kami modifikasi sehingga Anda dapat mencadangkan perubahan apa pun tanpa menginstal ulang JDK:

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

Unduh sertifikat penandatanganan yang perlu kami impor:

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

Lakukan impor:

$ 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. Verifikasi bahwa ini berfungsi setelah perubahan

Pastikan Java sekarang dengan senang hati terhubung ke port SSL:

$ java -jar SSLPing/dist/SSLPing.jar helloworld.letsencrypt.org 443
About to connect to 'helloworld.letsencrypt.org' on port 443
Successfully connected
dimalinux.dll
sumber
8

Untuk JDK yang belum mendukung sertifikat Let's Encrypt, Anda dapat menambahkannya ke JDK cacertssetelah proses ini (terima kasih untuk ini ).

Unduh semua sertifikat di https://letsencrypt.org/certificates/ (pilih format der ) dan tambahkan satu per satu dengan perintah seperti ini (contoh untuk 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.
sumber
Ini memperbaiki situasi tetapi kemudian saya mendapatkan kesalahan Sambungan: javax.net.ssl.SSLException: java.lang.RuntimeException: Tidak dapat menghasilkan DH keypair
nafg