Wednesday, April 15, 2020

Segurança com Java: Certificados

Nesse primeiro post falamos um pouco sobre criptografia com Java. Agora vamos nos aprofundar e falar sobre certificados.

Se você quiser que sua aplicação web (e-commerce, página estática, back-end, etc) viabilize a comunicação HTTPS com seu cliente, você precisa instalar um certificado TLS/SSL no seu web-server. Dessa forma toda a sessão entre a aplicação web e o cliente poderá ser estabelecida de forma segura, isto é, criptografada por meio de chaves públicas e privadas.



TLS/SSL

Os protocolos TLS (Transport Layer Security) e SSL (Secure Socket Layer) são frequentemente usados para se referir à mesma coisa: comunicação HTTPS. Ambos visam a segurança, integridade e privacidade durante a comunicação cliente-servidor. O SSL porém é considerado depreciado e sua versão moderna é o TLS 1.3.

A espinha dorsal do TLS é uma infraestrutura de chaves públicas que viabiliza a confiança mútua entre duas partes (cliente e servidor) que nunca trabalharam juntas. Isso tudo é realizado por um sistema de Certificate Authorities (CAs) que assinam certificados que passam a servir de credenciais para servidores, atestando sua identidade. Assim, quando um cliente tenta se conectar a esse servidor via HTTPS (TLS), esse servidor apresenta seu certificado ao cliente. Se a autoridade que assinou o certficado desse servidor estiver na lista de autoridades confiáveis do cliente, então um canal de comunicação segura é estabelecido entre esse cliente e o respectivo servidor.

Quando o cliente é uma aplicação java, a lista de autoridades confiáveis é chamada de truststore. Do lado do servidor [quando ele também é uma aplicação java], seu certificado (e as respectivas chaves) fica armazenado em uma keystore.

Geralmente, quando você é um servidor, trabalha mais com o keystore; quando você é cliente, está mais interessado no seu truststore.

O server apresenta seu certificado ao cliente, que confere no seu truststore se o certificado é confiável

Truststores do JDK

O Truststore na prática é um conjunto de certificados armazenados no JDK que a aplicação utiliza para determinar se confia ou não no servidor TLS que ela está acessando. Todo JDK vem com uma lista de CAs pré-definidos no Truststore.

O keytool é um programa em linha de comando que permite trabalhar com truststores e keystores.  Como dissemos, você trabalha mais com o keystore se for um server; ou trabalha mais com o truststore, se for um cliente.

O seguinte comando lista todas as autoridades pré-aceitas no JDK (o password default é changeit para rodar o comando):

keytool -list -keystore $JAVA_HOME/jre/lib/security/cacerts


Como exemplo, no browser Mozila, podemos checar o certificado de docs.amazon.aws clicando no icone do cadeado:


Em seguida clicando em more information:


Na pop-up que aparecer clique em view certficate:


Selecione o fingerprint desse certificado:


E confira se o JDK confia nesse CA:

keytool -list -keystore $JAVA_HOME/jre/lib/security/cacerts | grep ' fingerprint '


Como esse CA consta no truststore do JDK, podemos acessar essa url via código sem problemas:

String hostURL = "https://docs.aws.amazon.com";
URL url = new URL(hostURL);
HttpsURLConnection conn;
conn = (HttpsURLConnection)url.openConnection();
InputStream is = conn.getInputStream();

Experimente trocar por uma url cujo certificado não confiável, como por exemplo https://self-signed.badssl.com, e o código acima lança uma exception

Exception in thread "main" 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
 at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
 at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1946)
 at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:316)
 at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:310)
 at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1639)
 at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:223)
 at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1037)
 at sun.security.ssl.Handshaker.process_record(Handshaker.java:965)
 at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1064)
 at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
 at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
 at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
 at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
 at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
 at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1570)
 at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1498)
 at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:268)
 at ssl.Main03.main(Main03.java:17)

 javax.net.ssl.SSLHandshakeException Basicamente nos diz que o certificado desse host não é reconhecido pelo truststore default do JDK e que, portanto, não é possível estabelecer uma conexão segura com ele via TLS.

É comum que muitas vezes queiramos ignorar essa advertência e acessar o host mesmo assim (cenário comum em ambientes de teste). Neste caso podemos criar nossa própria truststore e   adicionar manualmente qualquer certficado.

O código abaixo cria uma nova TrsutStore e adiciona nela o certificado de https://self-signed.badssl.com, fazendo com que a aplicação consiga acessar a URL via HTTPS sem erros:

public class AdicionaCertificado {

    public static void main(String args[]) throws Exception {

        String hostURL = "https://self-signed.badssl.com";
        //String hostURL = "https://docs.aws.amazon.com";
        loadNewCertificate("/home/rafael/Library/Blog/Certs/badssl-com.pem");

        URL url = new URL(hostURL);
        HttpsURLConnection conn;
        conn = (HttpsURLConnection)url.openConnection();
        InputStream is = conn.getInputStream();
    }

    static void loadNewCertificate(String newCertficateFile) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {

        String trustStorePass = "123456";
        String clientTrutStorePath = "/home/rafael/Library/Blog/Certs/myNewTrustStore";
        String alias = "badssl.com";

        // criando uma nova trustStore (diferente da default cacerts)
        KeyStore clientTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());

        char[] password = trustStorePass.toCharArray();

        // para criar uma nova truststore vazia, passe null no 1o argumento...
        clientTrustStore.load(null, password);

        // ...e crie o arquivo fisico vazio
        FileOutputStream fos = new FileOutputStream(clientTrutStorePath);
        clientTrustStore.store(fos, password);
        fos.close();

        // como a keystore já existe, desta vez não passamos null ao metodo load
        FileInputStream in = new FileInputStream(clientTrutStorePath);
        clientTrustStore.load(in, password);
        in.close();

        // CertficateFactory é usada para gerar novos certificados
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        InputStream serverCertstream = new FileInputStream(newCertficateFile);

        // gera um objeto certificado e o inicializa com base nos dados do InputStream
        Certificate serverCertificate =  cf.generateCertificate(serverCertstream);

        // adiciona o novo certificado a nova keystore, com um apelido
        clientTrustStore.setCertificateEntry(alias, serverCertificate);

        // salvamos as alteracoes na nova keystore
        FileOutputStream out = new FileOutputStream(clientTrutStorePath);
        clientTrustStore.store(out, password);
        out.close();

        // dinamicamente alteramos o trsutstore default desta App para a nova trsutStore criada
        System.setProperty("javax.net.ssl.trustStore", clientTrutStorePath);
        System.setProperty("javax.net.ssl.trustStorePassword", trustStorePass);
    }
}

Você pode criar seu próprio certificado para posteriormente apresentá-lo aos clientes de sua aplicação. O comando a seguir cria uma chave privada e um certificado contendo a chave pública:
keytool -genkeypair -alias meu-alias -keyalg RSA -validity 7 -keystore minha-keystore

O próximo comando extrai o certificado do arquivo minha-keystore com a extensão .cer:
keytool -exportcert -alias meu-alias -keystore minha-keystore -rfc -file meu-certificado.cer

Nota: um detalhe para se estar ciente e que pode causar confusão é que, da perspectiva do keytool e do Java, keystores e truststores são arquivos keystore. Eles apenas contêm diferentes tipos de chaves. Tanto que a referência a clientTrustore no código foi representada pela classe java.security.KeyStore.


"Os fariseus perguntaram a Jesus sobre o momento em que chegaria o Reino de Deus. Jesus respondeu: 'O Reino de Deus não vem ostensivamente. Nem se poderá dizer: 'Está aqui' ou: 'está alí', porque o Reino de Deus está no meio de vocês.'"

Lucas 17:20-21