ATENÇÃO: O blog está de casa e cara nova.
O novo endereço agora é http://www.jjocenio.com.

Se você não for redirecionado automaticamente, por favor, clique link acima.

Conexão SSL em aplicações JAVA

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.

0 comentários:

Postar um comentário