O uso de SSL para criptografar a comunicação entre o cliente e as aplicações é muito comum, principalmente em webservices. Em JAVA, isso acaba causando problemas que, apesar de serem simples de solucionar, são pouco documentados ou a documentação é de difícil acesso. Por isso, resolvi escrever este post para ajudar na solução desses problemas.
Na comunicação via SSL, ao enviar a primeira requisição o cliente recebe o certificado do servidor, que contem uma chave pública. O cliente então utiliza essa chave pública para criptografar os dados antes de cada requisição para o servidor, que por sua vez utiliza a sua chave privada para decriptografar os dados antes de processá-lo.
Em Java, existe o conceito de certificados confiáveis, que são aqueles armazenados em um repositório de chaves, chamado keystore. Ao tentar estabelecer a requisição, a JVM utiliza o gerenciador de certificados para aceitar ou não a conexão. Caso o certificado não esteja importado ou o keystore não esteja presente a conexão será rejeitada.
Para resolver esse problema pode-se usar duas abordagens. A primeira é importar manualmente o certificado e informar a JVM qual keystore utilizar. Para importar o certificado, é necessário primeiro fazer o download (.cer), o que pode ser feito pelo browser, e então utilizar o utilitário
keytool para importá-lo.
keytool -importcert -alias certificado_do_servidor -keystore arquivo_keystore -file arquivo.cer
O keytool solicitará a senha do keystore para armazenar o certificado.
Feito isso, agora falta informar a JVM sobre o keystore que deve ser utilizado. Isso é feito por meio de variáveis de ambiente que podem ser passadas para a JVM pela linha de comando, utilizando a notação -Dnome=valor, ou dentro do seu código, utilizando o método System.setProperty. As variáveis que devem ser definidas são:
javax.net.ssl.trustStore=/caminho/arquivo_keystore
javax.net.ssl.trustStorePassword=senha_keystore
O problema dessa solução é que você precisa primeiro importar o certificado manualmente antes de distribuir sua aplicação e, no caso de uma mudança do certificado, sua aplicação pode parar de funcionar, apresentando o erro SSLHandshakeException, e aí você precisaria importar o novo certificado. A segunda abordagem é um pouco mais complicada, mas evita que a aplicação quebre quando houver mudanças de certificados.
Ao estabelecer uma conexão SSL a JVM utilizará um contexto SSL. O que faremos é criar um novo contexto SSL, substituindo o gerenciador de certificados por uma implementação customizada, que importará os novos certificados automaticamente.
A seguir, está a classe customizada para gerenciamento dos certificados.
/**
* Classe responsável pelo gerenciamento dos certificados
*/
public class CustomTrustManager implements X509TrustManager {
// Nome da variável de ambiente que contem a senha
// do arquivo de keystore
private static final String SENHA_KEYSTORE_VAR =
"javax.net.ssl.trustStorePassword";
// Nome da variável de ambiente que contem o caminho
// para o arquivo de keystore
private static final String TRUST_STORE_VAR =
"javax.net.ssl.trustStore";
// O caminho para o arquivo de keystore
private String keyStorePath;
// A senha do arquivo de keystore
private String senhaKeystore;
// O objeto keystore que será carregado do arquivo
private KeyStore keyStore;
// O gerenciador padrão, que será utilizado pela nossa classe
// evitando que precisemos reimplementar toda a
// lógica de gerenciamento
private X509TrustManager trustManager;
public CustomTrustManager() throws Exception {
super();
// Carrega os certificados armazenados
setKeyStorePath();
// Inicia o gerenciador padrão
getTrustManager();
}
// Método obrigatório para implementar a interface do gerenciador
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
// Utiliza o gerenciador padrão para executar a verificação
trustManager.checkClientTrusted(chain, authType);
}
// Método obrigatório para implementar a interface do gerenciador
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
try {
// Utiliza o gerenciador padrão para executar a verificação
trustManager.checkClientTrusted(chain, authType);
} catch (CertificateException e) {
// No caso de erro, o certificado é importado e
// a verificação é refeita
adicionarCertificado(chain);
trustManager.checkClientTrusted(chain, authType);
}
}
// Método obrigatório para implementar a interface do gerenciador
public X509Certificate[] getAcceptedIssuers() {
// Utiliza o gerenciador padrão recuperar os certificados
return trustManager.getAcceptedIssuers();
}
// Método responsável por importar o certificado
private void adicionarCertificado(X509Certificate[] chain)
throws CertificateException {
// É comum que o certificado contenha toda a rede de certificação.
// Nesse caso, toda ela será carregada
for (X509Certificate cert : chain) {
try {
// Armazena o certificado com o alias Certificado_X_Y,
// onde X é o serial e Y é o Current Time Millis.
// Isso evita a duplicidade de alias.
keyStore.setCertificateEntry("Certificado_"
+ cert.getSerialNumber() + "_"
+ System.currentTimeMillis(), cert);
} catch (KeyStoreException e) {
throw new CertificateException("Erro ao aceitar o certificado.", e);
}
}
try {
// Armazena os certificados no arquivo informado pela
// variável de ambiente com a senha específica
OutputStream streamKeyStore = new FileOutputStream(
new File(keyStorePath));
keyStore.store(streamKeyStore, senhaKeystore.toCharArray());
getTrustManager();
} catch (Exception e) {
throw new CertificateException("Erro ao aceitar o certificado.", e);
}
}
// Método responsável por iniciar o gerenciador de certificados
protected void getTrustManager() throws Exception {
// Recupera o algoritmo default de certificados
String algoritmo = TrustManagerFactory.getDefaultAlgorithm();
// Recupera a fabrica de gerenciadores para o algoritmo padrão
TrustManagerFactory fabricaTM =
TrustManagerFactory.getInstance(algoritmo);
// Inicia a fábrica com o keystore
fabricaTM.init(this.keyStore);
// Recupera todos os gerenciadores do sistema para encontrar o
// responsável pelos certificados
TrustManager tms[] = fabricaTM.getTrustManagers();
for (TrustManager tm : tms) {
if (tm instanceof X509TrustManager) {
this.trustManager = (X509TrustManager) tm;
return;
}
}
// Caso não seja encontrado um gerenciador para os certificados,
// lança a exceção
throw new NoSuchAlgorithmException("TrustManager não encontrado!");
}
// Método responsável por carregar o keystore do arquivo especificado
protected void setKeyStorePath() throws Exception {
// Recupera o caminho do arquivo e a senha do keystore
this.keyStorePath = System.getProperty(TRUST_STORE_VAR);
this.senhaKeystore = System.getProperty(SENHA_KEYSTORE_VAR);
if (this.keyStorePath == null) {
throw new KeyStoreException("O caminho do keystore não pode " +
" ser null. Informe em " + TRUST_STORE_VAR);
}
// Obtem o keystore para o tipo padrão do sistema
this.keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
// Inicia o stream para leitura dos certificados já importados
InputStream streamKeyStore =
new FileInputStream(new File(keyStorePath));
try {
// Carrega os certificados já importados utilizando
// a senha informada
this.keyStore.load(streamKeyStore, senhaKeystore.toCharArray());
} finally {
if (streamKeyStore != null) {
streamKeyStore.close();
}
}
}
}
Perceba que os métodos obrigatórios da interface são implementados por delegação para um gerenciador que foi obtido do sistema. Dessa forma evitamos a implementação de toda a lógica de um gerenciador padrão e somente fazemos o tratamento necessário no momento de carregar os certificados que ainda não foram importados. Note também que é obrigatório que já exista um arquivo de keystore.
Vejamos agora como utilizar o nosso gerenciador para realizar a conexão com um servidor https do qual não possuímos o certificado.
public class TesteHttpsURLConnection {
public static void main(String[] args) throws Exception {
// Define a variável de ambiente com o caminho do keystore
System.setProperty("javax.net.ssl.trustStore", "/tmp/keystore");
// Define a variável de ambiente com a senha do keystore
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
// Recupera o contexto SSL
SSLContext context = SSLContext.getInstance("SSL");
// Define o nosso gerenciador como o gerenciador default do contexto
context.init(null,
new TrustManager[] { new CustomTrustManager() }, null);
// Cria a conexão com o endereço https
URL httpsURL = new URL("https://internetbanking.caixa.gov.br/");
HttpsURLConnection connection =
(HttpsURLConnection) httpsURL.openConnection();
// Define a fábrica de sockets do nosso contexto
connection.setSSLSocketFactory(context.getSocketFactory());
// Realiza a conexão
connection.connect();
// Exibe as chaves dos certificados do servidor
Certificate[] certs = connection.getServerCertificates();
for (Certificate cert: certs) {
System.out.println(cert.getPublicKey());
}
}
}
Como demonstração, você pode executar o main sem a linha que define o socket factory para ver que será lançada a exceção SSLHandshakeException.
Se você utilizar o utilitário
keytool para verificar os certificados no arquivo de keystore, verá que todos os certificados do servidor foram armazenados lá. Evitando que seja necessário importá-los novamente.