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.cerO 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_keystoreO 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