¿Java admite los certificados Let's Encrypt?

124

Estoy desarrollando una aplicación Java que consulta una API REST en un servidor remoto a través de HTTP. Por razones de seguridad, esta comunicación debe cambiarse a HTTPS.

Ahora que Let's Encrypt comenzó su versión beta pública, me gustaría saber si Java funciona actualmente (o se confirma que funcionará en el futuro) con sus certificados de forma predeterminada.

Let's Encrypt consiguió su firma cruzada intermedia por IdenTrust , lo que debería ser una buena noticia. Sin embargo, no puedo encontrar ninguno de estos dos en la salida de este comando:

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

Sé que las CA de confianza se pueden agregar manualmente en cada máquina, pero dado que mi aplicación debería poder descargarse y ejecutarse sin ninguna configuración adicional, estoy buscando soluciones que funcionen "fuera de la caja". ¿Tienes buenas noticias para mí?

Hexaholico
fuente
1
También se puede verificar la compatibilidad de Let's Encrypt aquí letsencrypt.org/docs/certificate-compatibility
potame
@potame "con Java 8u131 todavía tiene que agregar su certificado a su almacén de confianza", por lo que si obtiene un certificado de Let's Encrypt, ¿deberá agregar el certificado que obtuvo al almacén de confianza? ¿No debería ser suficiente que su CA esté incluida?
mxro
1
@mxro Hola, gracias por llamar mi atención sobre esto. Mis comentarios anteriores no son ciertos en absoluto (de hecho, el problema era más complicado que eso y estaba relacionado con nuestra infraestructura) y los eliminaré porque en realidad solo están causando confusión. Entonces, si tiene un jdk> Java 8u101, el certificado Let's Encrypt debería funcionar y ser reconocido y confiable correctamente.
potame
@potame Eso es excelente. ¡Gracias por la aclaración!
mxro

Respuestas:

142

[ Actualización 2016-06-08 : De acuerdo con https://bugs.openjdk.java.net/browse/JDK-8154757, IdenTrust CA se incluirá en Oracle Java 8u101.]

[ Actualización 2016-08-05 : Java 8u101 ha sido lanzado y de hecho incluye IdenTrust CA: notas de la versión ]


¿Java admite los certificados Let's Encrypt?

Si. El certificado Let's Encrypt es solo un certificado de clave pública normal. Java lo admite (según Let's Encrypt Certificate Compatibility , para Java 7> = 7u111 y Java 8> = 8u101).

¿Java confía en los certificados Let's Encrypt listos para usar?

No / depende de la JVM. El almacén de confianza de Oracle JDK / JRE hasta 8u66 no contiene el Let's Encrypt CA específicamente ni el IdenTrust CA que lo firmaron. new URL("https://letsencrypt.org/").openConnection().connect();por ejemplo resultados en javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException.

Sin embargo, puede proporcionar su propio validador / definir un almacén de claves personalizado que contenga la CA raíz requerida o importar el certificado al almacén de confianza JVM.

https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/10 también discute el tema.


Aquí hay un código de ejemplo que muestra cómo agregar un certificado al almacén de confianza predeterminado en tiempo de ejecución. Solo necesitará agregar el certificado (exportado desde Firefox como .der y puesto en classpath)

Basado en ¿Cómo puedo obtener una lista de certificados raíz de confianza en Java? y 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
fuente
Una cosa más: ¿Qué archivo necesito usar? Let's Encrypt ofrece una raíz y cuatro certificados intermedios para descargar. Lo intenté isrgrootx1.dery lets-encrypt-x1-cross-signed.der, pero ninguno de ellos parece ser el correcto.
Hexaholic
@Hexaholic Depende de en qué desee confiar y de cómo el sitio que utiliza tiene certificados configurados. Vaya https://helloworld.letsencrypt.orgpor ejemplo e inspeccione la cadena de certificados en el navegador (haciendo clic en el icono verde). Para este necesita el certificado específico del sitio, el X1 intermedio (con signo cruzado por IdenTrust) o el DSTRootCAX3. El ISRG Root X1no funciona para el sitio helloworld porque no está en la cadena, esa es la cadena alternativa. Usaría el DSTRoot uno, simplemente exportado a través del navegador porque no lo he visto para descargar en ningún lado.
zapl
1
Gracias por el codigo. No olvide cerrar InputStream en keyStore.load (Files.newInputStream (ksPath)).
Michael Wyraz
3
@ adapt-dev No, ¿por qué el software debe confiar en un sistema desconocido para proporcionar buenos certificados? El software necesita poder confiar en los certificados en los que realmente confía. Sería un agujero de seguridad si esto significara que mi programa Java podría instalar certificados para el programa de otra persona. Pero eso no sucede aquí, el código solo agrega el certificado durante su propio tiempo de ejecución
zapl el
3
+1 para mostrar código que no solo hace trampa al establecer la javax.net.ssl.trustStorepropiedad del sistema, sino -1 para luego establecer el valor predeterminado de la JVM SSLContext. Sería mejor crear uno nuevo SSLSocketFactoryy luego usarlo para varias conexiones (por ejemplo HttpUrlConnection) en lugar de reemplazar la configuración SSL de toda la VM. (Me doy cuenta de este cambio sólo es eficaz para la JVM en ejecución y no persisten o afectan a otros programas simplemente creo que es una mejor práctica de programación a ser explícito acerca de dónde se aplica la configuración..)
Christopher Schultz
65

Sé que el OP solicitó una solución sin cambios de configuración local, pero en caso de que desee agregar la cadena de confianza al almacén de claves de forma permanente:

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

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

Jan Berkel
fuente
66
Sí, el OP no lo solicitó, ¡pero esta fue, con mucho, la solución más simple para mí!
Auspex
Importé este certificado letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt en mi versión anterior de java 8 y el servicio web que está usando el certificado permite cifrar no es accesible. Esta solución fue rápida y fácil.
ezwrighter
56

Respuesta detallada para aquellos de nosotros que estamos dispuestos a hacer cambios de configuración local que incluyen hacer una copia de seguridad del archivo de configuración:

1. Pruebe si está funcionando antes de los cambios

Si aún no tiene un programa de prueba, puede usar mi programa de ping SSLPing de Java que prueba el protocolo de enlace TLS (funcionará con cualquier puerto SSL / TLS, no solo HTTPS). Usaré SSLPing.jar preconstruido, pero leer el código y construirlo usted mismo es una tarea rápida y fácil:

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

Dado que mi versión de Java es anterior a la 1.8.0_101 (no publicada al momento de escribir esto), un certificado Let's Encrypt no se verificará de manera predeterminada. Veamos cómo se ve la falla antes de aplicar la corrección:

$ 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. Importar el certificado

Estoy en Mac OS X con el conjunto de variables de entorno JAVA_HOME. Los comandos posteriores supondrán que esta variable está establecida para la instalación de Java que está modificando:

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

Haga una copia de seguridad del archivo cacerts que modificaremos para que pueda hacer una copia de seguridad de cualquier cambio sin reinstalar el JDK:

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

Descargue el certificado de firma que necesitamos importar:

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

Realizar la importación:

$ 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. Verifique que esté funcionando después de los cambios

Verifique que Java ahora esté contento de conectarse al puerto SSL:

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

Para los JDK que aún no admiten los certificados Let's Encrypt, puede agregarlos al JDK cacertssiguiendo este proceso (gracias a esto ).

Descargue todos los certificados en https://letsencrypt.org/certificates/ (elija el formato der ) y agréguelos uno por uno con este tipo de comando (ejemplo para 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.
fuente
Esto mejora la situación, pero luego aparece el error de conexión: javax.net.ssl.SSLException: java.lang.RuntimeException: no se pudo generar el par de claves DH
nafg