O Java suporta certificados Let's Encrypt?

124

Estou desenvolvendo um aplicativo Java que consulta uma API REST em um servidor remoto por HTTP. Por motivos de segurança, essa comunicação deve ser alterada para HTTPS.

Agora que o Let's Encrypt começou sua versão beta pública, eu gostaria de saber se o Java atualmente funciona (ou se está confirmado que está funcionando no futuro) com seus certificados por padrão.

Vamos Encrypt ter seu intermediário assinado pela IdenTrust , o que deve ser uma boa notícia. No entanto, não consigo encontrar nenhum desses dois na saída deste comando:

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

Sei que CAs confiáveis ​​podem ser adicionadas manualmente em cada máquina, mas como meu aplicativo deve ser gratuito para download e executável sem nenhuma configuração adicional, estou procurando soluções que funcionem "prontas para uso". Você tem boas notícias para mim?

Hexaholic
fonte
1
Pode-se também verificar Vamos compatibilidade Criptografar aqui letsencrypt.org/docs/certificate-compatibility
potame
@potame "com o Java 8u131, você ainda precisa adicionar o seu certificado ao seu armazenamento confiável". Portanto, se você obtiver um certificado do Let's Encrypt, precisará adicionar o certificado que obteve ao armazenamento confiável? Não deveria ser suficiente que a CA deles fosse incluída?
Mxro
1
@mxro Oi - obrigado por chamar minha atenção para isso. Meus comentários acima não são verdadeiros (na verdade, o problema era mais complicado que isso e relacionado à nossa infraestrutura) e vou removê-los porque eles realmente estão apenas levando à confusão. Portanto, se você possui um certificado jdk> Java 8u101, o Let's Encrypt deve funcionar e ser adequadamente reconhecido e confiável.
potame
@potame Isso é excelente. Obrigado pelo esclarecimento!
Mxro

Respostas:

142

[ Atualização 2016-06-08 : de acordo com https://bugs.openjdk.java.net/browse/JDK-8154757, a CA do IdenTrust será incluída no Oracle Java 8u101.]

[ Atualização 05/08/2016 : O Java 8u101 foi lançado e inclui de fato a CA IdenTrust: notas de versão ]


O Java suporta certificados Let's Encrypt?

Sim. O certificado Let's Encrypt é apenas um certificado de chave pública comum. O Java suporta (de acordo com Let's Encrypt Certificate Compatibility , para Java 7> = 7u111 e Java 8> = 8u101).

O Java confia nos certificados Let's Encrypt fora da caixa?

Não / depende da JVM. O armazenamento confiável do Oracle JDK / JRE até 8u66 não contém a CA Let's Encrypt criptografada especificamente nem a CA IdenTrust que a assinou cruzadamente. new URL("https://letsencrypt.org/").openConnection().connect();por exemplo, resulta em javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException.

No entanto, você pode fornecer seu próprio validador / definir um keystore personalizado que contenha a CA raiz necessária ou importar o certificado para o armazenamento confiável da JVM.

https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/10 também discute o tópico.


Aqui está um código de exemplo que mostra como adicionar um certificado ao armazenamento confiável padrão no tempo de execução. Você só precisará adicionar o certificado (exportado do firefox como .der e colocar no caminho de classe)

Baseado em Como posso obter uma lista de certificados raiz confiáveis ​​em Java? e 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
fonte
Mais uma coisa: qual arquivo eu preciso usar? Let's Encrypt oferece um certificado raiz e quatro intermediários para download. Eu tentei isrgrootx1.dere lets-encrypt-x1-cross-signed.der, mas nenhum deles parece ser o caminho certo.
Hexaholic
@ Hexaholic Depende do que você deseja confiar e de como o site que você usa possui certs configurados. Vá para, https://helloworld.letsencrypt.orgpor exemplo, e inspecione a cadeia de certificados no navegador (clicando no ícone verde). Para este, você precisa do certificado específico do site, o intermediário X1 (assinado em cruz por IdenTrust) ou o DSTRootCAX3. O ISRG Root X1não funciona no site helloworld porque não está na cadeia, é a cadeia alternativa. Eu usaria o DSTRoot, simplesmente exportado pelo navegador, porque não o vi para download em nenhum lugar.
zapl
1
Obrigado pelo código. Não se esqueça de fechar o InputStream em keyStore.load (Files.newInputStream (ksPath)).
Michael Wyraz 22/02
3
@ adapt-dev Não, por que o software deve confiar em um sistema desconhecido para fornecer bons certificados? O software precisa poder confiar nos certificados nos quais realmente confia. Seria uma falha de segurança se isso significasse que o meu programa java poderia instalar certificados para o programa de outra pessoa. Mas isso não acontece aqui, o código só acrescenta o certificado durante o seu próprio tempo de execução
zapl
3
+1 por mostrar o código que não apenas trapaceia ao definir a javax.net.ssl.trustStorepropriedade do sistema, mas -1 por definir o padrão da JVM SSLContext. Seria melhor criar um novo SSLSocketFactorye depois usá-lo para várias conexões (por exemplo HttpUrlConnection), em vez de substituir a configuração SSL em toda a VM. (Sei que esta alteração só é eficaz para a JVM em execução e não persistir ou afetar outros programas que eu só acho que é uma melhor prática de programação para ser explícito sobre onde sua configuração se aplica..)
Christopher Schultz
65

Sei que o OP solicitou uma solução sem alterações na configuração local, mas caso você queira adicionar a cadeia de confiança ao keystore permanentemente:

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

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

Jan Berkel
fonte
6
Sim, o OP não pediu, mas essa foi de longe a solução mais simples para mim!
Auspex
Eu importei esse certificado letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt na minha versão mais antiga do java 8 e o serviço da web que está usando o certificado permite a criptografia não está acessível! Esta solução foi rápida e fácil.
ezwrighter
56

Resposta detalhada para aqueles de nós dispostos a fazer alterações locais na configuração, incluindo o backup do arquivo de configuração:

1. Teste se está funcionando antes das alterações

Se você ainda não possui um programa de teste, pode usar meu programa ping SSLPing java que testa o handshake TLS (funcionará com qualquer porta SSL / TLS, não apenas HTTPS). Usarei o SSLPing.jar predefinido, mas ler o código e construí-lo você mesmo é uma tarefa rápida e fácil:

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

Como minha versão do Java é anterior a 1.8.0_101 (não liberada no momento da redação deste documento), um certificado Let's Encrypt não será verificado por padrão. Vamos ver como é a falha antes de aplicar a correção:

$ 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. Importe o certificado

Estou no Mac OS X com o conjunto de variáveis ​​de ambiente JAVA_HOME. Os comandos posteriores assumirão que esta variável está definida para a instalação do java que você está modificando:

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

Faça um backup do arquivo cacerts que modificaremos para que você possa fazer o backup de qualquer alteração sem reinstalar o JDK:

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

Faça o download do certificado de assinatura que precisamos importar:

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

Execute a importação:

$ 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 se está funcionando após as alterações

Verifique se o Java está feliz em se conectar à porta SSL:

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

Para o JDK que ainda não suporta certificados Let's Encrypt, você pode adicioná-los ao JDK cacertsapós este processo (graças a isso ).

Faça o download de todos os certificados em https://letsencrypt.org/certificates/ (escolha o formato der ) e adicione-os um a um com este tipo de comando (exemplo 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.
fonte
Isso melhora a situação, mas eu recebo o erro de conexão: javax.net.ssl.SSLException: java.lang.RuntimeException: não foi possível gerar o par de chaves DH
nafg