Falhas de handshake SSL

Falhas de handshake SSL

1. Visão geral

O Secure Socket Layer (SSL) é um protocolo criptográfico que fornece segurança na comunicação pela rede. In this tutorial, we’ll discuss various scenarios that can result in an SSL handshake failure and how to it.

Observe que nossoIntroduction to SSL using JSSE cobre os fundamentos do SSL com mais detalhes.

2. Terminologia

É importante observar que, devido a vulnerabilidades de segurança, o SSL como padrão é substituído pelo Transport Layer Security (TLS). A maioria das linguagens de programação, incluindo Java, possui bibliotecas para suportar SSL e TLS.

Desde o início do SSL, muitos produtos e linguagens como OpenSSL e Java tinham referências ao SSL, que eles mantinham mesmo depois que o TLS assumiu. Por esse motivo, no restante deste tutorial, usaremos o termo SSL para nos referirmos geralmente a protocolos criptográficos.

3. Configuração

Para o propósito deste tutorial, criaremos um servidor simples e aplicativos cliente usandothe Java Socket API para simular uma conexão de rede.

3.1. Criando um cliente e um servidor

Em Java, podemos usarsockets to establish a communication channel between a server and client over the network. Os soquetes fazem parte da Java Secure Socket Extension (JSSE) em Java.

Vamos começar definindo um servidor simples:

int port = 8443;
ServerSocketFactory factory = SSLServerSocketFactory.getDefault();
try (ServerSocket listener = factory.createServerSocket(port)) {
    SSLServerSocket sslListener = (SSLServerSocket) listener;
    sslListener.setNeedClientAuth(true);
    sslListener.setEnabledCipherSuites(
      new String[] { "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" });
    sslListener.setEnabledProtocols(
      new String[] { "TLSv1.2" });
    while (true) {
        try (Socket socket = sslListener.accept()) {
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            out.println("Hello World!");
        }
    }
}

O servidor definido acima retorna a mensagem "Hello World!" para um cliente conectado.

A seguir, vamos definir um cliente básico, que conectaremos ao nossoSimpleServer:

String host = "localhost";
int port = 8443;
SocketFactory factory = SSLSocketFactory.getDefault();
try (Socket connection = factory.createSocket(host, port)) {
    ((SSLSocket) connection).setEnabledCipherSuites(
      new String[] { "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256" });
    ((SSLSocket) connection).setEnabledProtocols(
      new String[] { "TLSv1.2" });

    SSLParameters sslParams = new SSLParameters();
    sslParams.setEndpointIdentificationAlgorithm("HTTPS");
    ((SSLSocket) connection).setSSLParameters(sslParams);

    BufferedReader input = new BufferedReader(
      new InputStreamReader(connection.getInputStream()));
    return input.readLine();
}

Nosso cliente imprime a mensagem retornada pelo servidor.

3.2. Criação de certificados em Java

O SSL fornece sigilo, integridade e autenticidade nas comunicações da rede. Os certificados desempenham um papel importante no que diz respeito ao estabelecimento da autenticidade.

Normalmente, esses certificados são adquiridos e assinados por uma autoridade de certificação, mas para este tutorial, usaremos certificados autoassinados.

Para conseguir isso, podemos usarkeytool, que vem com o JDK:

$ keytool -genkey -keypass password \
                  -storepass password \
                  -keystore serverkeystore.jks

O comando acima inicia um shell interativo para coletar informações para o certificado, como Nome Comum (CN) e Nome Distinto (DN). Quando fornecemos todos os detalhes relevantes, ele gera o arquivoserverkeystore.jks, que contém a chave privada do servidor e seu certificado público.

Observe queserverkeystore.jks  é armazenado no formato Java Key Store (JKS), que é proprietário do Java. These days, keytool will remind us that we ought to consider using PKCS#12, which it also supports.

Podemos ainda usarkeytool para extrair o certificado público do arquivo keystore gerado:

$ keytool -export -storepass password \
                  -file server.cer \
                  -keystore serverkeystore.jks

O comando acima exporta o certificado público do armazenamento de chaves como um arquivoserver.cer. Vamos usar o certificado exportado para o cliente, adicionando-o ao seu armazenamento confiável:

$ keytool -import -v -trustcacerts \
                     -file server.cer \
                     -keypass password \
                     -storepass password \
                     -keystore clienttruststore.jks

Agora, geramos um keystore para o servidor e o armazenamento confiável correspondente para o cliente. Examinaremos o uso desses arquivos gerados quando discutirmos possíveis falhas de handshake.

E mais detalhes sobre o uso do armazenamento de chaves do Java podem ser encontrados em nossoprevious tutorial.

4. Handshake SSL

Os handshakes SSL sãoa mechanism by which a client and server establish the trust and logistics required to secure their connection over the network.

Este é um procedimento muito orquestrado e a compreensão dos detalhes pode ajudar a entender por que geralmente falha, o que pretendemos abordar na próxima seção.

As etapas típicas de um handshake SSL são:

  1. O Cliente fornece uma lista de possíveis versões SSL e conjuntos de cifras para usar

  2. O servidor concorda com uma suíte de cifras e versões SSL específica, respondendo de volta com seu certificado

  3. O cliente extrai a chave pública do certificado responde com uma "chave pré-mestre" criptografada

  4. O servidor descriptografa a “chave pré-master” usando sua chave privada

  5. Cliente e servidor calculam um "segredo compartilhado" usando a "chave pré-master" trocada

  6. Cliente e servidor trocam mensagens confirmando a criptografia e descriptografia bem-sucedidas usando o "segredo compartilhado"

Embora a maioria das etapas seja a mesma para qualquer handshake SSL, há uma diferença sutil entre SSL unidirecional e bidirecional. Vamos revisar rapidamente essas diferenças.

4.1. O handshake em SSL unilateral

Se nos referirmos às etapas mencionadas acima, a segunda etapa menciona a troca de certificados. O SSL unidirecional exige que um cliente possa confiar no servidor por meio de seu certificado público. Esteleaves the server to trust all clients que solicita uma conexão. Não há como um servidor solicitar e validar o certificado público de clientes, o que pode representar um risco à segurança.

4.2. O handshake em SSL bidirecional

Com o SSL unidirecional, o servidor deve confiar em todos os clientes. Porém, o SSL bidirecional adiciona a capacidade do servidor de estabelecer clientes confiáveis ​​também. Durante um handshake bidirecional,both the client and server must present and accept each other’s public certificates antes que uma conexão bem-sucedida possa ser estabelecida.

5. Cenários de falha de handshake

Tendo feito essa revisão rápida, podemos olhar para os cenários de falha com maior clareza.

Um handshake SSL, na comunicação unidirecional ou bidirecional, pode falhar por vários motivos. Analisaremos cada um desses motivos, simularemos a falha e entenderemos como evitar esses cenários.

Em cada um desses cenários, usaremos osSimpleClienteSimpleServer que criamos anteriormente.

5.1. Certificado de servidor ausente

Vamos tentar executar oSimpleServer e conectá-lo por meio doSimpleClient. Enquanto esperamos ver a mensagem "Hello World!", Somos apresentados com uma exceção:

Exception in thread "main" javax.net.ssl.SSLHandshakeException:
  Received fatal alert: handshake_failure

Agora, isso indica que algo deu errado. OSSLHandshakeException acima, de maneira abstrata,is stating that the client when connecting to the server did not receive any certificate.

Para resolver esse problema, usaremos o keystore gerado anteriormente, passando-os como propriedades do sistema para o servidor:

-Djavax.net.ssl.keyStore=clientkeystore.jks -Djavax.net.ssl.keyStorePassword=password

É importante observar que a propriedade do sistema para o caminho do arquivo de armazenamento de chave deve ser um caminho absoluto ou o arquivo de armazenamento de chave deve ser colocado no mesmo diretório de onde o comando Java é chamado para iniciar o servidor. Java system property for keystore does not support relative paths.

Isso nos ajuda a obter o resultado esperado? Vamos descobrir na próxima subseção.

5.2. Certificado de servidor não confiável

Conforme executamosSimpleServereSimpleClient novamente com as mudanças na subseção anterior, o que obtemos como saída:

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

Bem, não funcionou exatamente como esperávamos, mas parece que falhou por um motivo diferente.

Essa falha específica é causada pelo fato de que nosso servidor está usando um certificadoself-signed que não é assinado por uma Autoridade de Certificação (CA).

Really, any time the certificate is signed by something other than what is in the default truststore, we’ll see this error. O armazenamento confiável padrão no JDK normalmente é fornecido com informações sobre CAs comuns em uso.

Para resolver esse problema aqui, teremos que forçarSimpleClient a confiar no certificado apresentado porSimpleServer. Vamos usar o armazenamento confiável que geramos anteriormente, passando-os como propriedades do sistema para o cliente:

-Djavax.net.ssl.trustStore=clienttruststore.jks -Djavax.net.ssl.trustStorePassword=password

Observe que essa não é a solução ideal. In an ideal scenario, we should not use a self-signed certificate but a certificate which has been certified by a Certificate Authority (CA) which clients can trust by default.

Vamos para a próxima subseção para descobrir se obtemos nossa saída esperada agora.

5.3. Certificado de cliente ausente

Vamos tentar mais uma vez executar o SimpleServer e o SimpleClient, tendo aplicado as alterações das subseções anteriores:

Exception in thread "main" java.net.SocketException:
  Software caused connection abort: recv failed

Novamente, não é algo que esperávamos. OSocketException aqui nos diz que o servidor não pode confiar no cliente. Isso ocorre porque configuramos um SSL bidirecional. Em nossoSimpleServer we temos:

((SSLServerSocket) listener).setNeedClientAuth(true);

O código acima indica que umSSLServerSocket é necessário para autenticação do cliente por meio de seu certificado público.

Podemos criar um keystore para o cliente e um armazenamento confiável correspondente para o servidor de maneira semelhante à que usamos ao criar o keystore e o armazenamento confiável anteriores.

Reiniciaremos o servidor e transmitiremos as seguintes propriedades do sistema:

-Djavax.net.ssl.keyStore=serverkeystore.jks \
    -Djavax.net.ssl.keyStorePassword=password \
    -Djavax.net.ssl.trustStore=servertruststore.jks \
    -Djavax.net.ssl.trustStorePassword=password

Em seguida, reiniciaremos o cliente passando estas propriedades do sistema:

-Djavax.net.ssl.keyStore=clientkeystore.jks \
    -Djavax.net.ssl.keyStorePassword=password \
    -Djavax.net.ssl.trustStore=clienttruststore.jks \
    -Djavax.net.ssl.trustStorePassword=password

Finalmente, temos a saída que desejamos:

Hello World!

5.4. Certificados Incorretos

Além dos erros acima, um handshake pode falhar devido a vários motivos relacionados à maneira como criamos os certificados. Um erro comum está relacionado a uma CN incorreta. Vamos explorar os detalhes do armazenamento de chaves do servidor que criamos anteriormente:

keytool -v -list -keystore serverkeystore.jks

Quando executamos o comando acima, podemos ver os detalhes do keystore, especificamente o proprietário:

...
Owner: CN=localhost, OU=technology, O=example, L=city, ST=state, C=xx
...

O CN do proprietário deste certificado está definido como localhost. O CN do proprietário deve corresponder exatamente ao host do servidor. Se houver alguma incompatibilidade, isso resultará emSSLHandshakeException.

Vamos tentar regenerar o certificado do servidor com CN como algo diferente de localhost. Quando usamos o certificado regenerado agora para executarSimpleServereSimpleClient, ele falha imediatamente:

Exception in thread "main" javax.net.ssl.SSLHandshakeException:
    java.security.cert.CertificateException:
    No name matching localhost found

O rastreamento de exceção acima indica claramente que o cliente estava esperando um certificado com o nome de host local que não foi encontrado.

Observe queJSSE does not mandate hostname verification by default. Habilitamos a verificação do nome do host emSimpleClient por meio do uso explícito de HTTPS:

SSLParameters sslParams = new SSLParameters();
sslParams.setEndpointIdentificationAlgorithm("HTTPS");
((SSLSocket) connection).setSSLParameters(sslParams);

A verificação de nome de host é uma causa comum de falha e, em geral, e sempre deve ser aplicada para maior segurança. Para obter detalhes sobre a verificação de nome de host e sua importância na segurança com TLS, consultethis article.

5.5. Versão SSL incompatível

Atualmente, existem vários protocolos criptográficos, incluindo diferentes versões de SSL e TLS em operação.

Como mencionado anteriormente, o SSL, em geral, foi substituído pelo TLS por sua força criptográfica. O protocolo criptográfico e a versão são um elemento adicional com o qual um cliente e um servidor devem concordar durante um handshake.

Por exemplo, se o servidor usa um protocolo criptográfico de SSL3 e o cliente usa TLS1.3, eles não podem concordar com um protocolo criptográfico e umSSLHandshakeException será gerado.

Em nossoSimpleClient, vamos mudar o protocolo para algo que não seja compatível com o protocolo definido para o servidor:

((SSLSocket) connection).setEnabledProtocols(new String[] { "TLSv1.1" });

Quando executarmos nosso cliente novamente, obteremos umSSLHandshakeException:

Exception in thread "main" javax.net.ssl.SSLHandshakeException:
  No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

O rastreamento de exceção nesses casos é abstrato e não nos diz o problema exato. To resolve these types of problems it is necessary to verify that both the client and server are using either the same or compatible cryptographic protocols.

5.6. Suite Cipher Incompatível

O cliente e o servidor também devem concordar com o conjunto de códigos que eles usarão para criptografar as mensagens.

Durante um aperto de mão, o cliente apresentará uma lista de possíveis cifras a serem usadas e o servidor responderá com uma cifra selecionada da lista. O servidor irá gerar umSSLHandshakeException if não pode selecionar uma cifra adequada.

Em nossoSimpleClient, vamos mudar o pacote de criptografia para algo que não seja compatível com o pacote de criptografia usado por nosso servidor:

((SSLSocket) connection).setEnabledCipherSuites(
  new String[] { "TLS_RSA_WITH_AES_128_GCM_SHA256" });

Quando reiniciarmos nosso cliente, obteremos umSSLHandshakeException:

Exception in thread "main" javax.net.ssl.SSLHandshakeException:
  Received fatal alert: handshake_failure

Novamente, o rastreamento de exceção é bastante abstrato e não nos diz o problema exato. A solução para esse erro é verificar os conjuntos de criptografia habilitados usados ​​pelo cliente e pelo servidor e garantir que haja pelo menos um conjunto comum disponível.

Normalmente, clientes e servidores são configurados para usar uma ampla variedade de conjuntos de criptografia, portanto, é menos provável que ocorra esse erro. If we encounter this error it is typically because the server has been configured to use a very selective cipher. Um servidor pode escolher impor um conjunto seletivo de cifras por razões de segurança.

6. Conclusão

Neste tutorial, aprendemos sobre a configuração do SSL usando soquetes Java. Em seguida, discutimos os handshakes SSL com SSL unidirecional e bidirecional. Por fim, examinamos uma lista de possíveis razões pelas quais os handshakes SSL podem falhar e discutimos as soluções.

Como sempre, o código dos exemplos está disponívelover on GitHub.