Java có hỗ trợ chứng chỉ Let's Encrypt không?


125

Tôi đang phát triển một ứng dụng Java truy vấn API REST trên máy chủ từ xa qua HTTP. Vì lý do bảo mật, giao tiếp này nên được chuyển sang HTTPS.

Bây giờ Let's Encrypt đã bắt đầu phiên bản beta công khai của họ, tôi muốn biết liệu Java hiện đang hoạt động (hoặc được xác nhận là sẽ hoạt động trong tương lai) với các chứng chỉ của chúng theo mặc định hay không.

Let's Encrypt đã nhận được chữ ký chéo trung gian của họ bởi IdenTrust , đây hẳn là một tin tốt. Tuy nhiên, tôi không thể tìm thấy bất kỳ cái nào trong hai cái này trong đầu ra của lệnh này:

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

Tôi biết rằng các CA đáng tin cậy có thể được thêm theo cách thủ công trên mỗi máy, nhưng vì ứng dụng của tôi sẽ được tải xuống miễn phí và có thể thực thi mà không cần cấu hình thêm, tôi đang tìm kiếm các giải pháp hoạt động "hiệu quả". Bạn có tin tốt cho tôi không?


1
Người ta cũng có thể kiểm tra khả năng tương thích Hãy Encrypt đây letsencrypt.org/docs/certificate-compatibility
potame

@potame "với Java 8u131, bạn vẫn phải thêm chứng chỉ vào kho tin cậy của mình", vì vậy nếu bạn nhận được chứng chỉ từ Let's Encrypt, bạn sẽ cần thêm chứng chỉ bạn có vào kho tin cậy? Không nên bao gồm CA của họ là đủ?
mxro

1
@mxro Xin chào - cảm ơn vì đã thu hút sự chú ý của tôi vào điều này. Nhận xét của tôi ở trên không đúng chút nào (trên thực tế, vấn đề phức tạp hơn thế và liên quan đến cơ sở hạ tầng của chúng tôi) và tôi sẽ xóa chúng vì chúng thực sự chỉ dẫn đến nhầm lẫn. Vì vậy, nếu bạn có jdk> Java 8u101, chứng chỉ Let's Encrypt sẽ hoạt động và được công nhận và đáng tin cậy.
potame

@potame Thật xuất sắc. Cảm ơn bạn đã làm rõ!
mxro

Câu trả lời:


142

[ Cập nhật 2016-06-08 : Theo https://bugs.openjdk.java.net/browse/JDK-8154757 IdenTrust CA sẽ được đưa vào Oracle Java 8u101.]

[ Cập nhật 2016-08-05 : Java 8u101 đã được phát hành và thực sự bao gồm IdenTrust CA: ghi chú phát hành ]


Java có hỗ trợ chứng chỉ Let's Encrypt không?

Đúng. Chứng chỉ Let's Encrypt chỉ là một chứng chỉ khóa công khai thông thường. Java hỗ trợ nó (theo Let's Encrypt Certificate Compatibility , cho Java 7> = 7u111 và Java 8> = 8u101).

Java có tin tưởng chứng chỉ Let's Encrypt out of the box không?

Không / nó phụ thuộc vào JVM. Kho tin cậy của Oracle JDK / JRE lên đến 8u66 không chứa Let's Encrypt CA cụ thể cũng như IdenTrust CA đã ký tên vào nó. new URL("https://letsencrypt.org/").openConnection().connect();ví dụ kết quả trong javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException.

Tuy nhiên, bạn có thể cung cấp trình xác thực của riêng mình / xác định kho khóa tùy chỉnh có chứa CA gốc được yêu cầu hoặc nhập chứng chỉ vào kho tin cậy JVM.

https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/10 cũng thảo luận về chủ đề này.


Dưới đây là một số mã ví dụ hiển thị cách thêm chứng chỉ vào kho tin cậy mặc định trong thời gian chạy. Bạn chỉ cần thêm chứng chỉ (được xuất từ ​​firefox dưới dạng .der và đưa vào classpath)

Dựa trên Làm cách nào để có được danh sách các chứng chỉ gốc đáng tin cậy trong Java? 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);
        }
    }

}

Một điều nữa: Tôi cần sử dụng tệp nào? Let's Encrypt cung cấp một chứng chỉ gốc và bốn chứng chỉ trung gian để tải xuống. Tôi đã thử isrgrootx1.derlets-encrypt-x1-cross-signed.der, nhưng cả hai đều không có vẻ là đúng.
Hexaholic

@Hexaholic Phụ thuộc vào những gì bạn muốn tin tưởng và cách trang web bạn sử dụng có xác nhận cấu hình. Đi tới https://helloworld.letsencrypt.orgví dụ và kiểm tra chuỗi chứng chỉ trong trình duyệt (nhấp vào biểu tượng màu xanh lá cây). Đối với chứng chỉ này, bạn cần chứng chỉ dành riêng cho trang web, chứng chỉ trung gian X1 (được IdenTrust ký chéo) hoặc chứng chỉ DSTRootCAX3. Các ISRG Root X1không hoạt động cho các trang web helloworld vì nó không nằm trong dây chuyền, đó là chuỗi thay thế. Tôi sẽ sử dụng DSTRoot, chỉ đơn giản là xuất qua trình duyệt vì tôi chưa thấy nó để tải xuống ở bất kỳ đâu.
zapl

1
Cảm ơn bạn vì mật mã. Đừng quên đóng InputStream trong keyStore.load (Files.newInputStream (ksPath)).
Michael Wyraz

3
@ adapt-dev Không, tại sao phần mềm phải tin tưởng một hệ thống không xác định để cung cấp chứng chỉ tốt? Phần mềm cần có khả năng tin cậy các chứng chỉ mà nó thực sự tin cậy. Sẽ là một lỗ hổng bảo mật nếu điều này có nghĩa là chương trình java của tôi có thể cài đặt chứng chỉ cho chương trình elses của ai đó. Nhưng điều đó không xảy ra ở đây, mã chỉ thêm chứng chỉ trong thời gian chạy của chính nó
zapl

3
+1 để hiển thị mã không chỉ gian lận bằng cách đặt thuộc tính javax.net.ssl.trustStorehệ thống mà còn -1 để đặt mặc định của JVM SSLContext. Sẽ tốt hơn nếu tạo một cấu hình mới SSLSocketFactoryvà sau đó sử dụng nó cho các kết nối khác nhau (ví dụ HttpUrlConnection) hơn là thay thế cấu hình SSL trên toàn máy ảo. (Tôi nhận ra sự thay đổi này chỉ có hiệu quả cho JVM chạy và không tồn tại hoặc ảnh hưởng đến các chương trình khác mà tôi chỉ nghĩ đó là một thực hành lập trình tốt hơn để được rõ ràng về nơi cấu hình của bạn được áp dụng..)
Christopher Schultz

65

Tôi biết OP đã yêu cầu giải pháp mà không cần thay đổi cấu hình cục bộ, nhưng trong trường hợp bạn muốn thêm chuỗi tin cậy vĩnh viễn vào kho khóa:

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

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


6
Vâng, OP đã không yêu cầu nó, nhưng đây là giải pháp đơn giản nhất cho tôi!
Auspex

Tôi đã nhập chứng chỉ letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt này vào bản dựng java 8 cũ hơn của mình và không thể truy cập được dịch vụ web đang sử dụng chứng chỉ cho phép mã hóa! Giải pháp này nhanh chóng và dễ dàng.
ezwrighter

56

Câu trả lời chi tiết cho những người trong chúng ta sẵn sàng thực hiện các thay đổi cấu hình cục bộ bao gồm sao lưu tệp cấu hình:

1. Kiểm tra xem nó có hoạt động không trước khi thay đổi

Nếu bạn chưa có chương trình thử nghiệm, bạn có thể sử dụng chương trình ping SSLPing java của tôi để kiểm tra bắt tay TLS (sẽ hoạt động với bất kỳ cổng SSL / TLS nào, không chỉ HTTPS). Tôi sẽ sử dụng SSLPing.jar dựng sẵn, nhưng việc đọc mã và tự xây dựng mã là một nhiệm vụ nhanh chóng và dễ dàng:

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

Vì phiên bản Java của tôi cũ hơn 1.8.0_101 (không được phát hành tại thời điểm viết bài này), chứng chỉ Let's Encrypt sẽ không xác minh theo mặc định. Hãy xem lỗi trông như thế nào trước khi áp dụng bản sửa lỗi:

$ 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. Nhập chứng chỉ

Tôi đang sử dụng Mac OS X với tập biến môi trường JAVA_HOME. Các lệnh sau đó sẽ giả sử biến này được đặt cho cài đặt java mà bạn đang sửa đổi:

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

Tạo bản sao lưu của tệp cacerts mà chúng tôi sẽ sửa đổi để bạn có thể sao lưu bất kỳ thay đổi nào mà không cần cài đặt lại JDK:

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

Tải xuống chứng chỉ ký mà chúng tôi cần để nhập:

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

Thực hiện nhập:

$ 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. Xác minh rằng nó đang hoạt động sau những thay đổi

Xác minh rằng Java hiện hài lòng khi kết nối với cổng SSL:

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

8

Đối với JDK chưa hỗ trợ chứng chỉ Let's Encrypt, bạn có thể thêm chúng vào JDK cacertstheo quy trình này (nhờ vào điều này ).

Tải xuống tất cả các chứng chỉ trên https://letsencrypt.org/certificates/ (chọn định dạng der ) và thêm từng chứng chỉ một bằng loại lệnh này (ví dụ cho letsencryptauthorityx1.der):

keytool -import -keystore PATH_TO_JDK\jre\lib\security\cacerts -storepass changeit -noprompt -trustcacerts -alias letsencryptauthorityx1 -file PATH_TO_DOWNLOADS\letsencryptauthorityx1.der

Điều này cải thiện tình hình nhưng sau đó tôi nhận được lỗi kết nối: javax.net.ssl.SSLException: java.lang.RuntimeException: Không thể tạo cặp khóa DH
nafg
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.