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

164

Um módulo que estou adicionando ao nosso grande aplicativo Java precisa 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 intermediário e preciso incorporar esse certificado ao 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 qualquer manipulação adicional em vigor para o certificado autoassinado, ele morre em 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 este certificado autoassinado, para este ponto no aplicativo e em nenhum outro lugar.

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

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

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

skiphoppy
fonte
correção pequena e funcional: rgagnon.com/javadetails/…
20
@ Hasenpriester: por favor não sugira esta página. Desativa toda a verificação de confiança. Você não apenas aceitará o certificado autoassinado que deseja, como também aceitará qualquer certificado que um invasor do MITM lhe apresente.
Bruno

Respostas:

168

Crie SSLSocketvocê mesmo uma fábrica e defina-a HttpsURLConnectionantes de conectar.

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

Você deseja criar um SSLSocketFactorye 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 precisar de ajuda para criar o armazenamento de chaves, comente.


Aqui está um exemplo de carregamento do armazenamento de chaves:

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

Para criar o armazenamento de chaves com um certificado no formato PEM, você pode escrever seu próprio código usando CertificateFactoryou apenas importá-lo keytooldo JDK (o keytool não funcionará para uma "entrada de chave", mas é adequado para uma "entrada confiável" )

keytool -import -file selfsigned.pem -alias server -keystore server.jks
erickson
fonte
3
Muito obrigado! Os outros caras me ajudaram a apontar na direção certa, mas no final a sua é a abordagem que eu tomei. Eu tinha uma penosa tarefa de converter o arquivo de certificado PEM para um arquivo de armazenamento de chave JKS Java, e eu achei assistência para esse aqui: stackoverflow.com/questions/722931/...
skiphoppy
1
Estou feliz que deu certo. Sinto muito por você ter lutado com o armazenamento de chaves; Eu deveria ter incluído na minha resposta. Não é muito difícil com o CertificateFactory. Na verdade, acho que vou fazer uma atualização para quem vier mais tarde.
214 erickson
1
Tudo o que esse código faz é replicar o que você pode realizar, definindo três propriedades do sistema descritas no JSSE Refernence Guide.
Marquês de Lorne
2
@EJP: Isso foi há um tempo atrás, então não me lembro com certeza, mas acho que a lógica era que, em um "aplicativo Java grande", é provável que outras conexões HTTP sejam estabelecidas. A configuração de propriedades globais pode interferir nas conexões de trabalho ou permitir que essa parte falsifique servidores. Este é um exemplo de uso do gerenciador de confiança interno, caso a caso.
Erickson 31/05
2
Como o OP disse, "meu código precisa ensinar Java a aceitar este certificado autoassinado, para este ponto no aplicativo e em nenhum outro lugar".
Erickson 31/05
18

Eu li vários lugares online para resolver isso. Este é o código que escrevi para fazê-lo funcionar:

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 cadeia de certificados, se for autoassinado, desde que mantenha a estrutura exata acima. Eu obtive a sequência de certificados com a linha de comando do terminal do meu laptop.

Josh
fonte
1
Obrigado por compartilhar @ Josh. Criei um pequeno projeto Github que demonstra seu código em uso: github.com/aasaru/ConnectToTrustedServerExample
Master Drools
14

Se criar SSLSocketFactoryuma opção não for, apenas importe a chave para a JVM

  1. Recupere a chave pública: $openssl s_client -connect dev-server:443e crie um arquivo dev-server.pem com a aparência

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

  3. Reiniciar JVM

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

user454322
fonte
1
Acho que isso não resolve a questão original, mas resolveu o meu problema, então obrigado!
Ed Norris
Também não é uma boa idéia fazer isso: agora você tem esse certificado autoassinado extra-aleatório em todo o sistema, no qual todos os processos Java confiam por padrão.
user268396
12

Copiamos o armazenamento confiável do JRE e adicionamos nossos certificados personalizados a esse armazenamento confiável e, em seguida, instruímos o aplicativo a usar o armazenamento confiável personalizado com uma propriedade do sistema. Dessa forma, deixamos o armazenamento confiável JRE padrão em paz.

A desvantagem é que, quando você atualiza o JRE, seu novo armazenamento confiável não é automaticamente mesclado ao seu personalizado.

Talvez você possa lidar com esse cenário com uma rotina de instalação ou inicialização que verifique o armazenamento confiável / jdk e verifique se há uma incompatibilidade ou atualize 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.

Sr. Brilhante e Novo 安 宇
fonte
12

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

Isso se resume a ter nosso próprio SSLSocketFactory, que cria soquetes SSL a partir do nosso SSLContext local, configurado para ter apenas o TrustManager local associado a ele. Você não precisa chegar nem perto de um keystore / certstore.

Portanto, isso está em nossa 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 métodos de implementação do SecureProtocolSocketFactory. LocalSSLTrustManager é a implementação do gerenciador de confiança fictícia mencionada acima.

araqnid
fonte
8
Se você desabilitar toda a verificação de confiança, não vale a pena usar o SSL / TLS em primeiro lugar. Não há problema em testar localmente, mas não se você quiser se conectar fora.
Bruno
Recebo essa exceção ao executá-lo no Java 7. javax.net.ssl.SSLHandshakeException: nenhum conjunto de cifras em comum Você pode ajudar?
Uri Lukach