Java รองรับใบรับรอง Let's Encrypt หรือไม่


125

ฉันกำลังพัฒนาแอปพลิเคชัน Java ที่สืบค้น REST API บนเซิร์ฟเวอร์ระยะไกลผ่าน HTTP เพื่อความปลอดภัยควรเปลี่ยนการสื่อสารนี้เป็น HTTPS

ตอนนี้Let's Encryptเริ่มเบต้าสาธารณะแล้วฉันต้องการทราบว่า Java ใช้งานได้ในขณะนี้ (หรือได้รับการยืนยันว่าจะใช้งานได้ในอนาคต) พร้อมใบรับรองโดยค่าเริ่มต้น

Let's Encrypt มีการลงนามแบบไขว้ระดับกลางโดย IdenTrustซึ่งน่าจะเป็นข่าวดี อย่างไรก็ตามฉันไม่พบสองสิ่งนี้ในผลลัพธ์ของคำสั่งนี้:

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

ฉันรู้ว่า CA ที่เชื่อถือได้สามารถเพิ่มได้ด้วยตนเองในแต่ละเครื่อง แต่เนื่องจากแอปพลิเคชันของฉันควรดาวน์โหลดและเรียกใช้งานได้ฟรีโดยไม่ต้องกำหนดค่าใด ๆ เพิ่มเติมฉันจึงกำลังมองหาโซลูชันที่ "ใช้งานได้ทันที" คุณมีข่าวดีสำหรับฉันหรือไม่?


1
คุณยังสามารถตรวจสอบความเข้ากันได้ของ Let's Encrypt ได้ที่นี่allowencrypt.org/docs/certificate-compatibility
potame

@potame "ด้วย Java 8u131 คุณยังต้องเพิ่มใบรับรองของคุณไปยัง Truststore ของคุณ" ดังนั้นหากคุณได้รับใบรับรองจาก Let's Encrypt คุณจะต้องเพิ่มใบรับรองที่คุณได้รับไปยัง truststore หรือไม่? มันควรจะเพียงพอที่จะรวม CA ของพวกเขาหรือไม่?
mxro

1
@mxro สวัสดี - ขอบคุณที่ดึงความสนใจของฉันมาที่สิ่งนี้ ความคิดเห็นของฉันข้างต้นไม่เป็นความจริงเลย (อันที่จริงปัญหานั้นซับซ้อนกว่านั้นและเกี่ยวข้องกับโครงสร้างพื้นฐานของเรา) และฉันจะลบออกเพราะมันนำไปสู่ความสับสนเท่านั้น ดังนั้นหากคุณมี jdk> Java 8u101 ใบรับรอง Let's Encrypt ควรใช้งานได้และได้รับการยอมรับและเชื่อถืออย่างเหมาะสม
potame

@potame ยอดเยี่ยมมาก ขอบคุณสำหรับคำชี้แจง!
mxro

คำตอบ:


142

[ อัปเดต 2016-06-08 : อ้างอิงจากhttps://bugs.openjdk.java.net/browse/JDK-8154757 IdenTrust CA จะรวมอยู่ใน Oracle Java 8u101]

[ อัปเดต 2016-08-05 : Java 8u101 ได้รับการเผยแพร่และมี IdenTrust CA: บันทึกประจำรุ่น ]


Java รองรับใบรับรอง Let's Encrypt หรือไม่

ใช่. ใบรับรอง Let's Encrypt เป็นเพียงใบรับรองคีย์สาธารณะทั่วไป Java รองรับ (ตามมาตรฐานLet's Encrypt Certificate Compatibilityสำหรับ Java 7> = 7u111 และ Java 8> = 8u101)

Java เชื่อถือ Let's Encrypt ใบรับรองนอกกรอบหรือไม่

ไม่ / ขึ้นอยู่กับ JVM Truststore ของ Oracle JDK / JRE สูงสุด 8u66 ไม่มีทั้ง Let's Encrypt CA โดยเฉพาะหรือ IdenTrust CA ที่เซ็นชื่อข้าม new URL("https://letsencrypt.org/").openConnection().connect();ตัวอย่างเช่นผลลัพธ์ในjavax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException.

อย่างไรก็ตามคุณสามารถระบุตัวตรวจสอบความถูกต้องของคุณเอง / กำหนดที่เก็บคีย์แบบกำหนดเองที่มี root CA ที่ต้องการหรืออิมพอร์ตใบรับรองไปยัง JVM truststore

https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/10กล่าวถึงหัวข้อนี้ด้วย


นี่คือโค้ดตัวอย่างบางส่วนที่แสดงวิธีการเพิ่มใบรับรองไปยัง truststore เริ่มต้นที่รันไทม์ คุณจะต้องเพิ่มใบรับรอง (ส่งออกจาก firefox เป็น. เดอร์และใส่ใน classpath)

ขึ้นอยู่กับฉันจะรับรายชื่อใบรับรองหลักที่เชื่อถือได้ใน 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);
        }
    }

}

อีกอย่างหนึ่ง: ฉันต้องใช้ไฟล์ใด Let's Encrypt มี ใบรับรองระดับกลางหนึ่งรายการและใบรับรองระดับกลางสี่รายการสำหรับการดาวน์โหลด ฉันพยายามisrgrootx1.derและlets-encrypt-x1-cross-signed.derแต่ไม่ใช่ของพวกเขาน่าจะเป็นหนึ่งที่เหมาะสม
Hexaholic

@Hexaholic ขึ้นอยู่กับสิ่งที่คุณต้องการเชื่อถือและเว็บไซต์ที่คุณใช้มีการกำหนดค่าใบรับรองอย่างไร ไปที่https://helloworld.letsencrypt.orgตัวอย่างและตรวจสอบห่วงโซ่ใบรับรองในเบราว์เซอร์ (คลิกไอคอนสีเขียว) สำหรับใบรับรองนี้คุณต้องมีใบรับรองเฉพาะไซต์ X1 ตัวกลาง (ลงนามโดย IdenTrust) หรือ DSTRootCAX3 ใช้ISRG Root X1ไม่ได้กับไซต์ helloworld เนื่องจากไม่ได้อยู่ในห่วงโซ่นั่นคือห่วงโซ่ทางเลือก ฉันจะใช้ DSTRoot อันเดียวส่งออกผ่านเบราว์เซอร์เพราะฉันไม่เห็นมันให้ดาวน์โหลดที่ไหนเลย
zapl

1
ขอบคุณสำหรับรหัส อย่าลืมปิด InputStream ใน keyStore.load (Files.newInputStream (ksPath))
Michael Wyraz

3
@ adapt-dev ไม่ทำไมซอฟต์แวร์จึงควรเชื่อถือระบบที่ไม่รู้จักเพื่อจัดหาใบรับรองที่ดี ซอฟต์แวร์ต้องสามารถเชื่อถือใบรับรองที่เชื่อถือได้จริง มันจะเป็นช่องโหว่ด้านความปลอดภัยถ้านั่นหมายความว่าโปรแกรม java ของฉันสามารถติดตั้งใบรับรองสำหรับโปรแกรมอื่น ๆ ของคนอื่นได้ แต่นั่นไม่ได้เกิดขึ้นที่นี่รหัสจะเพิ่มใบรับรองในระหว่างรันไทม์ของตัวเองเท่านั้น
zapl

3
+1 สำหรับการแสดงรหัสที่ไม่เพียง แต่โกงโดยการตั้งค่าjavax.net.ssl.trustStoreคุณสมบัติของระบบ แต่ -1 SSLContextแล้วการตั้งค่าเริ่มต้นของ จะเป็นการดีกว่าถ้าสร้างใหม่SSLSocketFactoryแล้วใช้สิ่งนั้นสำหรับการเชื่อมต่อต่างๆ (เช่นHttpUrlConnection) แทนที่จะแทนที่การกำหนดค่า SSL แบบกว้าง VM (ฉันตระหนักดีว่าการเปลี่ยนแปลงนี้มีผลเฉพาะกับ JVM ที่ทำงานอยู่และไม่คงอยู่หรือส่งผลกระทบต่อโปรแกรมอื่น ๆ ฉันแค่คิดว่าการเขียนโปรแกรมที่ดีกว่าที่จะอธิบายอย่างชัดเจนเกี่ยวกับตำแหน่งที่ใช้การกำหนดค่าของคุณ)
Christopher Schultz

65

ฉันรู้ว่า OP ขอวิธีแก้ปัญหาโดยไม่มีการเปลี่ยนแปลงการกำหนดค่าภายในเครื่อง แต่ในกรณีที่คุณต้องการเพิ่มห่วงโซ่ความเชื่อถือในที่เก็บคีย์อย่างถาวร:

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

แหล่งที่มา: https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/13


6
ใช่ OP ไม่ได้ขอ แต่นี่เป็นวิธีแก้ปัญหาที่ง่ายที่สุดสำหรับฉัน!
Auspex

ฉันนำเข้าใบรับรองนี้allowencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txtลงในโครงสร้าง java 8 ที่เก่ากว่าของฉันและบริการเว็บที่ใช้ใบรับรองการเข้ารหัสช่วยไม่สามารถเข้าถึงได้! วิธีนี้ทำได้ง่ายและรวดเร็ว
ezwrighter

57

คำตอบโดยละเอียดสำหรับพวกเราที่เต็มใจทำการเปลี่ยนแปลงการกำหนดค่าในเครื่องซึ่งรวมถึงการสำรองไฟล์ config:

1. ทดสอบว่าใช้งานได้หรือไม่ก่อนการเปลี่ยนแปลง

หากคุณยังไม่มีโปรแกรมทดสอบคุณสามารถใช้โปรแกรม java SSLPing ping ของฉันซึ่งทดสอบการจับมือ TLS (จะทำงานกับพอร์ต SSL / TLS ใด ๆ ไม่ใช่แค่ HTTPS) ฉันจะใช้ SSLPing.jar ที่สร้างไว้ล่วงหน้า แต่การอ่านโค้ดและสร้างด้วยตัวเองนั้นเป็นงานที่ง่ายและรวดเร็ว:

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

เนื่องจากเวอร์ชัน Java ของฉันเก่ากว่า 1.8.0_101 (ไม่ได้เผยแพร่ในขณะที่เขียน) ใบรับรอง Let's Encrypt จะไม่ตรวจสอบตามค่าเริ่มต้น มาดูกันว่าความล้มเหลวมีลักษณะอย่างไรก่อนใช้การแก้ไข:

$ 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. นำเข้าใบรับรอง

ฉันใช้ Mac OS X พร้อมชุดตัวแปรสภาพแวดล้อม JAVA_HOME คำสั่งในภายหลังจะถือว่าตัวแปรนี้ถูกตั้งค่าสำหรับการติดตั้ง java ที่คุณกำลังแก้ไข:

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

ทำการสำรองไฟล์ cacerts ที่เราจะแก้ไขเพื่อให้คุณสามารถสำรองการเปลี่ยนแปลงใด ๆ โดยไม่ต้องติดตั้ง JDK ใหม่:

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

ดาวน์โหลดใบรับรองการลงนามที่เราต้องนำเข้า:

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

ดำเนินการนำเข้า:

$ 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. ตรวจสอบว่าใช้งานได้หลังจากการเปลี่ยนแปลง

ตรวจสอบว่า Java มีความสุขในการเชื่อมต่อกับพอร์ต SSL:

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

8

สำหรับ JDK ที่ยังไม่รองรับใบรับรอง Let's Encrypt คุณสามารถเพิ่มใบรับรองเหล่านั้นลงใน JDK ได้cacertsตามขั้นตอนนี้ (ขอบคุณสิ่งนี้ )

ดาวน์โหลดใบรับรองทั้งหมดบนhttps://letsencrypt.org/certificates/ (เลือกรูปแบบder ) และเพิ่มทีละรายการด้วยคำสั่งประเภทนี้ (ตัวอย่างletsencryptauthorityx1.der):

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

สิ่งนี้ช่วยปรับปรุงสถานการณ์ แต่แล้วฉันได้รับข้อผิดพลาดการเชื่อมต่อ: javax.net.ssl.SSLException: java.lang.RuntimeException: ไม่สามารถสร้าง
คีย์คู่
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.