Como posso usar certificados diferentes em conexões específicas?

Um módulo que estou adicionando ao nosso grande aplicativo Java tem que conversar com o site protegido por SSL de outra empresa. O problema é que o site usa um certificado autoassinado. Eu tenho uma cópia do certificado para verificar se não estou encontrando um ataque man-in-the-middle, e preciso incorporar este certificado em nosso código de forma que a conexão com o servidor seja bem-sucedida.

Aqui está o código básico:

void sendRequest(String dataPacket) { String urlStr = "https://host.example.com/"; URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection)url.openConnection(); conn.setMethod("POST"); conn.setRequestProperty("Content-Length", data.length()); conn.setDoOutput(true); OutputStreamWriter o = new OutputStreamWriter(conn.getOutputStream()); o.write(data); o.flush(); } 

Sem nenhum tratamento adicional no local para o certificado autoassinado, isso ocorre no conn.getOutputStream () com a seguinte exceção:

 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 .... Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target .... Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target 

Idealmente, meu código precisa ensinar Java a aceitar esse certificado autoassinado, para esse ponto no aplicativo, e em nenhum outro lugar.

Eu sei que posso importar o certificado para o armazenamento de autoridade de certificação do JRE, e isso permitirá que o Java o aceite. Essa não é uma abordagem que eu quero tomar se puder ajudar; parece muito invasivo fazer em todas as máquinas dos nossos clientes por um módulo que eles não podem usar; Isso afetaria todos os outros aplicativos Java usando o mesmo JRE, e eu não gosto disso, mesmo que as chances de qualquer outro aplicativo Java acessar este site sejam nulas. Também não é uma operação trivial: no UNIX eu tenho que obter direitos de access para modificar o JRE dessa maneira.

Eu também vi que posso criar uma instância do TrustManager que faz alguma verificação personalizada. Parece que posso até mesmo criar um TrustManager que delega ao TrustManager real em todas as instâncias, exceto neste certificado. Mas parece que o TrustManager é instalado globalmente, e presumo que afetaria todas as outras conexões do nosso aplicativo, e isso também não cheira bem para mim.

Qual é a maneira preferida, padrão ou melhor de configurar um aplicativo Java para aceitar um certificado autoassinado? Posso realizar todos os objectives que tenho em mente acima, ou terei que me comprometer? Existe uma opção que envolve arquivos e diretórios e configurações, e pouco ou nenhum código?

Crie uma fábrica SSLSocket e configure-a no HttpsURLConnection antes de conectar.

 ... HttpsURLConnection conn = (HttpsURLConnection)url.openConnection(); conn.setSSLSocketFactory(sslFactory); conn.setMethod("POST"); ... 

Você vai querer criar um SSLSocketFactory e mantê-lo por perto. Aqui está um esboço de como inicializá-lo:

 /* Load the keyStore that includes self-signed cert as a "trusted" entry. */ KeyStore keyStore = ... TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(keyStore); SSLContext ctx = SSLContext.getInstance("TLS"); ctx.init(null, tmf.getTrustManagers(), null); sslFactory = ctx.getSocketFactory(); 

Se você precisar de ajuda para criar o armazenamento de chaves, por favor, comente.


Veja um exemplo de como carregar o armazenamento de chaves:

 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(trustStore, trustStorePassword); trustStore.close(); 

Para criar o armazenamento de chaves com um certificado de formato PEM, você pode escrever seu próprio código usando CertificateFactory ou apenas importá-lo com keytool do JDK (keytool não funcionará para uma “input de chave”, mas é bom para um “confiável input”).

 keytool -import -file selfsigned.pem -alias server -keystore server.jks 

Se a criação de um SSLSocketFactory não for uma opção, apenas importe a chave para a JVM

  1. Recupere a chave pública: $openssl s_client -connect dev-server:443 , então crie um arquivo dev-server.pem que se parece com

     -----BEGIN CERTIFICATE----- lklkkkllklklklklllkllklkl lklkkkllklklklklllkllklkl lklkkkllklk.... -----END CERTIFICATE----- 
  2. Importe a chave: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem . Senha: changeit

  3. Reinicie a JVM

Fonte: Como resolver javax.net.ssl.SSLHandshakeException?

Copiamos o truststore do JRE e adicionamos nossos certificados personalizados a esse armazenamento confiável e, em seguida, informamos ao aplicativo para usar o armazenamento confiável personalizado com uma propriedade do sistema. Dessa forma, deixamos o armazenamento de confiança padrão do JRE sozinho.

A desvantagem é que, quando você atualiza o JRE, não obtém seu novo truststore automaticamente mesclado ao seu personalizado.

Você poderia manipular esse cenário tendo uma rotina de instalação ou de boot que verifica o truststore / jdk e verifica a incompatibilidade ou atualiza automaticamente o armazenamento confiável. Não sei o que acontece se você atualizar o armazenamento confiável enquanto o aplicativo estiver em execução.

Esta solução não é 100% elegante ou infalível, mas é simples, funciona e não requer código.

Eu tive que fazer algo assim ao usar o commons-httpclient para acessar um servidor https interno com um certificado auto-assinado. Sim, nossa solução foi criar um TrustManager personalizado que simplesmente passasse tudo (registrando uma mensagem de debugging).

Isso se resume a ter nossa própria SSLSocketFactory que cria sockets SSL de nosso SSLContext local, que é configurado para ter apenas o nosso TrustManager local associado a ele. Você não precisa ir perto de um keystore / certstore em tudo.

Então, isso está no nosso LocalSSLSocketFactory:

 static { try { SSL_CONTEXT = SSLContext.getInstance("SSL"); SSL_CONTEXT.init(null, new TrustManager[] { new LocalSSLTrustManager() }, null); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Unable to initialise SSL context", e); } catch (KeyManagementException e) { throw new RuntimeException("Unable to initialise SSL context", e); } } public Socket createSocket(String host, int port) throws IOException, UnknownHostException { LOG.trace("createSocket(host => {}, port => {})", new Object[] { host, new Integer(port) }); return SSL_CONTEXT.getSocketFactory().createSocket(host, port); } 

Juntamente com outros methods implementando SecureProtocolSocketFactory. O LocalSSLTrustManager é a implementação do gerenciador confiável fictício mencionado anteriormente.

Eu passei por muitos lugares em SO e na web para resolver isso. Este é o código que funcionou para mim:

  ByteArrayInputStream derInputStream = new ByteArrayInputStream(app.certificateString.getBytes()); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(derInputStream); String alias = "alias";//cert.getSubjectX500Principal().getName(); KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore.load(null); trustStore.setCertificateEntry(alias, cert); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(trustStore, null); KeyManager[] keyManagers = kmf.getKeyManagers(); TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); tmf.init(trustStore); TrustManager[] trustManagers = tmf.getTrustManagers(); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagers, trustManagers, null); URL url = new URL(someURL); conn = (HttpsURLConnection) url.openConnection(); conn.setSSLSocketFactory(sslContext.getSocketFactory()); 

app.certificateString é uma String que contém o certificado, por exemplo:

  static public String certificateString= "-----BEGIN CERTIFICATE-----\n" + "MIIGQTCCBSmgAwIBAgIHBcg1dAivUzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE" + "BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE" + ... a bunch of characters... "5126sfeEJMRV4Fl2E5W1gDHoOd6V==\n" + "-----END CERTIFICATE-----"; 

Eu testei que você pode colocar qualquer caractere na string de certificado, se for auto-assinado, desde que você mantenha a estrutura exata acima. Eu obtive a string de certificado com a linha de comando do terminal do meu laptop.