¿Java admite los certificados Let''s Encrypt?
ssl keystore (4)
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í?
Para los JDK que aún no admiten los certificados Let''s Encrypt, puede agregarlos a los
cacerts
JDK siguiendo este proceso (gracias a
this
).
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
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 todavía 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 precompilado, 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 1.8.0_101 (no publicada en el momento de escribir este artículo), 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
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
[ 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 letsencrypt.org/docs/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, da como resultado
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 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 tendrá que 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);
}
}
}