Aceitar o certificado SSL autoassinado do servidor no cliente Java

215

Parece uma pergunta padrão, mas não consegui encontrar direções claras em nenhum lugar.

Eu tenho o código java tentando conectar a um servidor com certificado provavelmente autoassinado (ou expirado). O código relata o seguinte erro:

[HttpMethodDirector] I/O exception (javax.net.ssl.SSLHandshakeException) caught 
when processing request: sun.security.validator.ValidatorException: PKIX path 
building failed: sun.security.provider.certpath.SunCertPathBuilderException: 
unable to find valid certification path to requested target

Pelo que entendi, eu tenho que usar o keytool e dizer ao java que não há problema em permitir essa conexão.

Todas as instruções para corrigir esse problema pressupõem que eu seja totalmente competente com o keytool, como

gerar chave privada para o servidor e importá-la para o keystore

Existe alguém que possa postar instruções detalhadas?

Estou executando o unix, então o script bash seria melhor.

Não tenho certeza se é importante, mas o código é executado no jboss.

Nikita Rybak
fonte
2
Consulte Como aceito um certificado autoassinado com um Java HttpsURLConnection? . Obviamente, seria melhor se você conseguir que o site use um certificado válido.
Matthew Flaschen
2
Obrigado pelo link, não o vi durante a pesquisa. Mas as duas soluções envolvem código especial para enviar uma solicitação e eu estou usando o código existente (amazon ws client for java). Respectivamente, é o site deles que estou conectando e não consigo corrigir os problemas de certificado.
Nikita Rybak
2
@MatthewFlaschen - "Obviamente, seria melhor que o site usasse um certificado válido ..." - Um certificado autoassinado é um certificado válido se o cliente confiar nele. Muitos acham que conferir confiança ao cartel da CA / Navegador é um defeito de segurança.
jww
3
Relacionado, consulte O código mais perigoso do mundo: validando certificados SSL em software que não é o navegador . (O link é fornecido, pois você parece estar recebendo essas respostas com spam que desabilitam a validação).
JWW

Respostas:

306

Você tem basicamente duas opções aqui: adicione o certificado autoassinado ao seu armazenamento confiável da JVM ou configure seu cliente para

Opção 1

Exporte o certificado do seu navegador e importe-o no seu armazenamento confiável da JVM (para estabelecer uma cadeia de confiança):

<JAVA_HOME>\bin\keytool -import -v -trustcacerts
-alias server-alias -file server.cer
-keystore cacerts.jks -keypass changeit
-storepass changeit 

opção 2

Desativar validação de certificado:

// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[] { 
    new X509TrustManager() {     
        public java.security.cert.X509Certificate[] getAcceptedIssuers() { 
            return new X509Certificate[0];
        } 
        public void checkClientTrusted( 
            java.security.cert.X509Certificate[] certs, String authType) {
            } 
        public void checkServerTrusted( 
            java.security.cert.X509Certificate[] certs, String authType) {
        }
    } 
}; 

// Install the all-trusting trust manager
try {
    SSLContext sc = SSLContext.getInstance("SSL"); 
    sc.init(null, trustAllCerts, new java.security.SecureRandom()); 
    HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (GeneralSecurityException e) {
} 
// Now you can access an https URL without having the certificate in the truststore
try { 
    URL url = new URL("https://hostname/index.html"); 
} catch (MalformedURLException e) {
} 

Observe que eu não recomendo a opção 2 . Desativar o gerenciador de confiança derrota algumas partes do SSL e torna você vulnerável aos ataques intermediários. Prefira a opção 1 ou, melhor ainda, faça com que o servidor use um certificado "real" assinado por uma CA conhecida.

Pascal Thivent
fonte
7
Não é apenas o ataque do MIM. Isso o torna vulnerável à conexão com o site errado. É completamente inseguro. Veja a RFC 2246. Sou contra a publicação deste TrustManager o tempo todo. Nem sequer é correto escrever sua própria especificação.
Marquês de Lorne
10
@EJP Realmente não estou recomendando a segunda opção (atualizei minha resposta para deixar claro). No entanto, não publicá-lo não resolverá nada (isso é informação pública) e o IMHO não merece um voto negativo.
Pascal Thivent
135
@EJP É ensinando às pessoas que você as educa, não escondendo coisas. Portanto, manter as coisas em segredo ou na obscuridade não é uma solução. Esse código é público, a API Java é pública, é melhor falar sobre isso do que ignorá-lo. Mas eu posso viver com você não concordando.
Pascal Thivent
10
A outra opção - a que você não mencionou - é corrigir o certificado do servidor, corrigindo-o você mesmo ou ligando para o pessoal de suporte relevante. Certificados de host único são realmente muito baratos; vagar por aí com coisas autoassinadas é tolo em termos de dinheiro (ou seja, para aqueles que não estão familiarizados com esse idioma inglês, um conjunto totalmente estúpido de prioridades que custa muito para economizar quase nada).
Donal Fellows
2
@ Rich, 6 anos atrasado, mas você também pode obter o certificado do servidor no firefox clicando no ícone de cadeado -> mais informações -> Exibir certificado -> guia Detalhes -> Exportar ... Está bem oculto.
Siddhartha
9

Persegui esse problema em um provedor de certificados que não faz parte dos hosts confiáveis ​​da JVM padrão a partir de JDK 8u74. O provedor é www.identrust.com , mas esse não era o domínio ao qual eu estava tentando me conectar. Esse domínio recebeu seu certificado desse provedor. Consulte A raiz cruzada cobrirá a confiança na lista padrão no JDK / JRE? - leia algumas entradas. Consulte também Quais navegadores e sistemas operacionais suportam Let's Encrypt .

Portanto, para me conectar ao domínio em que me interessava, que tinha um certificado emitido identrust.com, executei as etapas a seguir. Basicamente, eu tive que obter o identrust.com (DST Root CA X3 certificado ) para ser confiável pela JVM. Consegui fazer isso usando o Apache HttpComponents 4.5 da seguinte forma:

1: Obtenha o certificado da indettrust em Instruções para download da cadeia de certificados . Clique no link DST Root CA X3 .

2: Salve a seqüência de caracteres em um arquivo chamado "DST Root CA X3.pem". Certifique-se de adicionar as linhas "----- COMEÇAR CERTIFICADO -----" e "----- TERMINAR CERTIFICADO -----" no arquivo no início e no final.

3: Crie um arquivo java keystore, cacerts.jks com o seguinte comando:

keytool -import -v -trustcacerts -alias IdenTrust -keypass yourpassword -file dst_root_ca_x3.pem -keystore cacerts.jks -storepass yourpassword

4: Copie o keystore cacerts.jks resultante no diretório de recursos do seu aplicativo java / (maven).

5: Use o código a seguir para carregar esse arquivo e anexá-lo ao Apache 4.5 HttpClient. Isso resolverá o problema para todos os domínios que possuem certificados emitidos a partir do indetrust.comutil oracle, incluindo o certificado no keystore padrão do JRE.

SSLContext sslcontext = SSLContexts.custom()
        .loadTrustMaterial(new File(CalRestClient.class.getResource("/cacerts.jks").getFile()), "yourpasword".toCharArray(),
                new TrustSelfSignedStrategy())
        .build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslcontext,
        new String[] { "TLSv1" },
        null,
        SSLConnectionSocketFactory.getDefaultHostnameVerifier());
CloseableHttpClient httpclient = HttpClients.custom()
        .setSSLSocketFactory(sslsf)
        .build();

Quando o projeto é desenvolvido, o cacerts.jks será copiado no caminho de classe e carregado a partir daí. Neste momento, não testei em outros sites SSL, mas se o código acima "encadear" neste certificado, eles também funcionarão, mas, novamente, não sei.

Referência: contexto SSL personalizado e Como aceito um certificado autoassinado com um Java HttpsURLConnection?

K.Nicholas
fonte
A questão é sobre um certificado autoassinado. Esta resposta não é.
Marquês de Lorne,
4
Leia a pergunta (e responda) um pouco mais de perto.
precisa saber é o seguinte
9

O Apache HttpClient 4.5 suporta a aceitação de certificados autoassinados:

SSLContext sslContext = SSLContexts.custom()
    .loadTrustMaterial(new TrustSelfSignedStrategy())
    .build();
SSLConnectionSocketFactory socketFactory =
    new SSLConnectionSocketFactory(sslContext);
Registry<ConnectionSocketFactory> reg =
    RegistryBuilder.<ConnectionSocketFactory>create()
    .register("https", socketFactory)
    .build();
HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(reg);        
CloseableHttpClient httpClient = HttpClients.custom()
    .setConnectionManager(cm)
    .build();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse sslResponse = httpClient.execute(httpGet);

Isso cria uma fábrica de soquetes SSL que usará o TrustSelfSignedStrategy , registrará com um gerenciador de conexões personalizado e, em seguida, executará um HTTP GET usando esse gerenciador de conexões.

Eu concordo com aqueles que cantam "não fazem isso na produção", no entanto, existem casos de uso para aceitar certificados autoassinados fora da produção; nós os usamos em testes de integração automatizados, para que possamos usar SSL (como na produção) mesmo quando não estiver em execução no hardware de produção.

spiffy
fonte
6

Em vez de configurar o socket factory padrão (que IMO é uma coisa ruim) - você afetará apenas a conexão atual, e não todas as conexões SSL que você tentar abrir:

URLConnection connection = url.openConnection();
    // JMD - this is a better way to do it that doesn't override the default SSL factory.
    if (connection instanceof HttpsURLConnection)
    {
        HttpsURLConnection conHttps = (HttpsURLConnection) connection;
        // Set up a Trust all manager
        TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager()
        {

            public java.security.cert.X509Certificate[] getAcceptedIssuers()
            {
                return null;
            }

            public void checkClientTrusted(
                java.security.cert.X509Certificate[] certs, String authType)
            {
            }

            public void checkServerTrusted(
                java.security.cert.X509Certificate[] certs, String authType)
            {
            }
        } };

        // Get a new SSL context
        SSLContext sc = SSLContext.getInstance("TLSv1.2");
        sc.init(null, trustAllCerts, new java.security.SecureRandom());
        // Set our connection to use this SSL context, with the "Trust all" manager in place.
        conHttps.setSSLSocketFactory(sc.getSocketFactory());
        // Also force it to trust all hosts
        HostnameVerifier allHostsValid = new HostnameVerifier() {
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };
        // and set the hostname verifier.
        conHttps.setHostnameVerifier(allHostsValid);
    }
InputStream stream = connection.getInputStream();
Jon Daniel
fonte
2
Seu checkServerTrustednão implementa a lógica necessária para realmente confiar no certificado e garantir que certificados não confiáveis ​​sejam rejeitados. Isso pode fazer uma boa leitura: O código mais perigoso do mundo: validar certificados SSL em software que não seja o navegador .
JWW
1
Seu getAcceptedIssuers()método não está em conformidade com a especificação e essa 'solução' permanece radicalmente insegura.
Marquês de Lorne
3

Existe uma alternativa melhor para confiar em todos os certificados: crie um TrustStoreque confie especificamente em um determinado certificado e use-o para criar um a SSLContextpartir do qual obter o SSLSocketFactoryconjunto HttpsURLConnection. Aqui está o código completo:

File crtFile = new File("server.crt");
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile));

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("server", certificate);

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());

Como alternativa, você pode carregar KeyStorediretamente o arquivo ou recuperar o certificado X.509 de qualquer fonte confiável.

Observe que, com esse código, os certificados cacertsnão serão usados. Este particular HttpsURLConnectionconfiará apenas neste certificado específico.

Johannes Brodwall
fonte
1

Confie em todos os certificados SSL: - Você pode ignorar o SSL se quiser testar no servidor de teste. Mas não use esse código para produção.

public static class NukeSSLCerts {
protected static final String TAG = "NukeSSLCerts";

public static void nuke() {
    try {
        TrustManager[] trustAllCerts = new TrustManager[] { 
            new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() {
                    X509Certificate[] myTrustedAnchors = new X509Certificate[0];  
                    return myTrustedAnchors;
                }

                @Override
                public void checkClientTrusted(X509Certificate[] certs, String authType) {}

                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) {}
            }
        };

        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, trustAllCerts, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String arg0, SSLSession arg1) {
                return true;
            }
        });
    } catch (Exception e) { 
    }
}

}

Por favor, chame essa função na função onCreate () em Activity ou em sua classe Application.

NukeSSLCerts.nuke();

Isso pode ser usado para o Volley no Android.

Ashish Saini
fonte
Não tenho certeza do motivo pelo qual você está sendo rejeitado, a menos que o código não funcione. Talvez seja a suposição de que o Android esteja de alguma forma envolvido quando a pergunta original não marcou o Android.
21818 Lo-Tan
1

A resposta aceita é boa, mas eu gostaria de adicionar algo a isso, pois estava usando o IntelliJ no Mac e não consegui fazê-lo funcionar usando a JAVA_HOMEvariável path.

Acontece que o Java Home era diferente ao executar o aplicativo a partir do IntelliJ.

Para descobrir exatamente onde está, você pode fazer System.getProperty("java.home")o que é de onde os certificados confiáveis ​​são lidos.

Christopher Schneider
fonte
0

Se 'eles' estiverem usando um certificado autoassinado, cabe a eles executar as etapas necessárias para tornar seu servidor utilizável. Especificamente, isso significa fornecer o certificado para você offline de maneira confiável. Então, faça com que eles façam isso. Em seguida, você importa isso para o seu armazenamento confiável usando o keytool, conforme descrito no Guia de Referência do JSSE. Nem pense no inseguro TrustManager postado aqui.

EDITAR Para o benefício dos dezessete (!) Votantes negativos e numerosos comentadores abaixo, que claramente não leram o que escrevi aqui, isso não é uma jeremia para certificados autoassinados. Não há nada de errado com certificados autoassinados quando implementados corretamente. Mas, a maneira correta de implementá-los é ter o certificado entregue com segurança por meio de um processo offline, em vez do canal não autenticado que eles serão usados ​​para autenticar. Certamente isso é óbvio? É certamente óbvio para todas as organizações com consciência de segurança em que já trabalhei, desde bancos com milhares de agências até minhas próprias empresas. A 'solução' baseada em código do lado do cliente de confiança todoscertificados, incluindo certificados autoassinados assinados por absolutamente ninguém, ou por qualquer órgão arbitrário que se estabeleça como CA, ipso facto não é seguro. Está apenas brincando com segurança. É inútil. Você está tendo uma conversa particular, à prova de adulteração, à prova de resposta e à prova de injeção com ... alguém. Qualquer pessoa. Um homem no meio. Um imitador. Qualquer pessoa. Você também pode usar apenas texto simples.

Marquês de Lorne
fonte
2
Só porque algum servidor decidiu usar https, não significa que a pessoa com o cliente dê uma porcaria sobre segurança para seus próprios fins.
Gus
3
Estou surpreso que esta resposta tenha sido tão prejudicada. Eu adoraria entender um pouco mais o porquê. Parece que o EJP está sugerindo que você não deveria fazer a opção 2 porque ela permite uma grande falha de segurança. Alguém poderia explicar por que (exceto as configurações anteriores à implantação) por que essa resposta é de baixa qualidade?
Cr1pto 16/05
2
Bem, acho que tudo isso. O que "cabe a eles tomar as medidas necessárias para tornar seu servidor utilizável" significa? O que um operador de servidor precisa fazer para torná-lo utilizável? Quais são os passos que você tem em mente? Você pode fornecer uma lista deles? Mas, voltando aos 1.000 pés, como isso se relaciona com o problema que o OP perguntou? Ele quer saber como fazer o cliente aceitar o certificado autoassinado em Java. É uma questão simples de confiar no código, pois o OP considera o certificado aceitável.
JWW
2
@EJP - Corrija-me se estiver errado, mas aceitar um certificado autoassinado é uma decisão de política do lado do cliente. Não tem nada a ver com operações do lado do servidor. O cliente deve tomar a decisão. As paridades devem trabalhar juntas para resolver o problema de distribuição de chaves, mas isso não tem nada a ver com tornar o servidor utilizável.
JWW
3
@EJP - Eu certamente não disse "confiar em todos os certificados" . Talvez esteja faltando alguma coisa (ou você esteja lendo muito sobre as coisas) ... O OP tem o certificado e é aceitável para ele. Ele quer saber como confiar nisso. Provavelmente estou partindo os cabelos, mas o certificado não precisa ser entregue offline. Uma verificação fora da banda deve ser usada, mas não é a mesma coisa que "deve ser entregue ao cliente offline" . Ele pode até ser a loja que produz o servidor e o cliente, portanto, possui o conhecimento a priori necessário.
JWW
0

Em vez de usar o keytool, conforme sugerido pelo comentário principal, no RHEL, você pode usar o update-ca-trust a partir das versões mais recentes do RHEL 6. Você precisará ter o certificado no formato pem. Então

trust anchor <cert.pem>

Edite o /etc/pki/ca-trust/source/cert.p11-kit e altere "categoria do certificado: outra entrada" para "categoria do certificado: autoridade". (Ou use sed para fazer isso em um script.) Em seguida, faça

update-ca-trust

Algumas advertências:

  • Não consegui encontrar "confiança" no meu servidor RHEL 6 e a yum não se ofereceu para instalá-lo. Acabei usando-o em um servidor RHEL 7 e copiando o arquivo .p11-kit.
  • Para fazer isso funcionar para você, você pode precisar fazer update-ca-trust enable. Isso substituirá o arquivo / etc / pki / java / cacerts por um link simbólico apontando para / etc / pki / ca-trust / extract / java / cacerts. (Portanto, convém fazer backup do primeiro.)
  • Se o seu cliente java usa cacerts armazenados em algum outro local, substitua-o manualmente por um link simbólico para / etc / pki / ca-trust / extract / java / cacerts ou substitua-o por esse arquivo.
user66660
fonte
0

Tive o problema de passar um URL para uma biblioteca que chamava de url.openConnection();adaptar a resposta de jon-daniel,

public class TrustHostUrlStreamHandler extends URLStreamHandler {

    private static final Logger LOG = LoggerFactory.getLogger(TrustHostUrlStreamHandler.class);

    @Override
    protected URLConnection openConnection(final URL url) throws IOException {

        final URLConnection urlConnection = new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile()).openConnection();

        // adapated from
        // /programming/2893819/accept-servers-self-signed-ssl-certificate-in-java-client
        if (urlConnection instanceof HttpsURLConnection) {
            final HttpsURLConnection conHttps = (HttpsURLConnection) urlConnection;

            try {
                // Set up a Trust all manager
                final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {

                    @Override
                    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }

                    @Override
                    public void checkClientTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
                    }

                    @Override
                    public void checkServerTrusted(final java.security.cert.X509Certificate[] certs, final String authType) {
                    }
                } };

                // Get a new SSL context
                final SSLContext sc = SSLContext.getInstance("TLSv1.2");
                sc.init(null, trustAllCerts, new java.security.SecureRandom());
                // Set our connection to use this SSL context, with the "Trust all" manager in place.
                conHttps.setSSLSocketFactory(sc.getSocketFactory());
                // Also force it to trust all hosts
                final HostnameVerifier allHostsValid = new HostnameVerifier() {
                    @Override
                    public boolean verify(final String hostname, final SSLSession session) {
                        return true;
                    }
                };

                // and set the hostname verifier.
                conHttps.setHostnameVerifier(allHostsValid);

            } catch (final NoSuchAlgorithmException e) {
                LOG.warn("Failed to override URLConnection.", e);
            } catch (final KeyManagementException e) {
                LOG.warn("Failed to override URLConnection.", e);
            }

        } else {
            LOG.warn("Failed to override URLConnection. Incorrect type: {}", urlConnection.getClass().getName());
        }

        return urlConnection;
    }

}

Usando esta classe, é possível criar uma nova URL com:

trustedUrl = new URL(new URL(originalUrl), "", new TrustHostUrlStreamHandler());
trustedUrl.openConnection();

Isso tem a vantagem de estar localizado e não substituir o padrão URL.openConnection.

David Ryan
fonte