Histórico de um único handshake SSL

Olá Habr!

Recentemente, tive que estragar o SSL com autenticação mútua no Spring Reactive Webclient. Parece uma questão simples, mas resultou em uma perambulação nas fontes do JDK com um final inesperado. Foi adquirida experiência em um artigo inteiro, que pode ser útil para os engenheiros nas tarefas diárias ou na preparação para uma entrevista.


Declaração do problema


Existe um serviço REST no lado do cliente que funciona através de HTTPS.
Você deve acessá-lo a partir de um aplicativo Java cliente.

A primeira coisa que me foi dada nessa missão foram 2 arquivos com a extensão .pem - um certificado de cliente e uma chave privada. Verifiquei o desempenho deles usando o Postman: especifiquei os caminhos para eles nas configurações e, após receber uma solicitação, verifiquei se o servidor respondia com 200 OK e um corpo de resposta significativo. Em separado, verifiquei que, sem um certificado de cliente, o servidor retorna um status HTTP 500 e uma mensagem curta no corpo da resposta de que uma "exceção de segurança" ocorreu com um código específico.

Em seguida, você deve configurar corretamente o aplicativo Java do cliente.

Para solicitações REST, usei o Spring Reactive WebClient com E / S sem bloqueio.
A documentação tem um exemplo de como ela pode ser personalizada, lançando um objeto SslContext, que apenas armazena certificados e chaves privadas.

Minha configuração em uma versão simplificada foi quase copiar e colar da documentação:

SslContext sslContext = SslContextBuilder .forClient() .keyManager(…) /*  ,   .pem      (, , ). PEM      . /    openssl .     KeyManagerFactory. */ .build(); ClientHttpConnector connector = new ReactorClientHttpConnector( builder -> builder.sslContext(sslContext)); WebClient webClient = WebClient.builder() .clientConnector(connector).build(); 

Seguindo o princípio do TDD, também escrevi um teste que usa o WebTestClient em vez do WebClient, que exibe várias informações de depuração. A primeira afirmação foi assim:

 webTestClient .post() .uri([  ]) .body(BodyInserters.fromObject([ ,   ,  ])) .exchange() .expectStatus().isOk() 

Esse teste simples não passou imediatamente: o servidor retornou 500 com o mesmo corpo como se o Postman não especificasse um certificado de cliente.

Separadamente, observo que, durante a depuração, ativei a opção "não verificar certificado do servidor", ou seja, passei a instância InsecureTrustManagerFactory como TrustManager for SslContext. Essa medida foi redundante, mas excluiu metade das opções com certeza.

As informações de depuração no teste não esclareceram o problema, mas parecia que algo deu errado no estágio de handshake SSL, então decidi comparar com mais detalhes como a conexão ocorre nos dois casos: para Postman e para o cliente Java. Tudo isso pode ser visto usando o Wireshark - este é um analisador de tráfego de rede tão popular. Ao mesmo tempo, vi como o handshake SSL acontece com a autenticação bidirecional, por assim dizer, ao vivo (eles gostam de perguntar isso durante as entrevistas):

  • Primeiro de tudo, o cliente envia uma mensagem Hello do cliente contendo meta-informações como a versão do protocolo e a lista de algoritmos de criptografia que ele suporta
  • Em resposta, o servidor envia imediatamente um pacote das seguintes mensagens: Servidor Olá, Certificado, Troca de Chaves do Servidor, Solicitação de Certificado, Servidor Olá Concluído .
    O Server Hello indica o algoritmo de criptografia selecionado pelo servidor (conjunto de criptografia). Dentro do certificado está o certificado do servidor. O Server Key Exchange carrega algumas informações necessárias para criptografia, dependendo do algoritmo escolhido (não estamos interessados ​​nos detalhes agora, portanto assumimos que essa é apenas uma chave pública, embora esteja incorreta!). Além disso, no caso de autenticação bidirecional, na mensagem Solicitação de certificado , o servidor solicita um certificado de cliente e explica em quais formatos ele suporta e em quais emissores confia.
  • Após receber essas informações, o cliente verifica o certificado do servidor e envia seu certificado, sua "chave pública" e outras informações nas seguintes mensagens: Certificado, Troca de Chaves do Cliente, Verificação de Certificado . A última é a mensagem ChangeCipherSpec , indicando que toda a comunicação adicional ocorrerá na forma criptografada
  • Por fim, depois de todos esses embaralhamento, o servidor verificará o certificado do cliente e, se estiver tudo bem, fornecerá uma resposta.

Após quinze minutos de penetração no tráfego, notei que o cliente Java, em resposta à solicitação de certificado do servidor, por algum motivo não envia seu certificado, ao contrário do cliente Postman. Ou seja, há uma mensagem de certificado, mas está vazia.

Em seguida, eu precisaria examinar primeiro a especificação do protocolo TLS , que diz literalmente o seguinte:
Se a lista certificate_authorities na mensagem de solicitação de certificado não estiver vazia, um dos certificados da cadeia de certificados DEVE ser emitido por uma das CAs listadas.
Estamos falando da lista certificate_authorities especificada na mensagem Solicitação de certificado vinda do servidor. Um certificado de cliente (pelo menos uma das cadeias) deve ser assinado por um dos emissores listados nesta lista. Chamamos isso de verificação X.

Eu não conhecia essa condição e a encontrei quando atingi as profundezas do JDK na depuração (eu tenho JDK9). O HttpClient da Netty, subjacente ao Spring WebClient, usa SslEngine do JDK por padrão. Como alternativa, você pode alterná-lo para o provedor OpenSSL adicionando as dependências necessárias, mas eu finalmente não precisei disso.
Portanto, defini pontos de interrupção na classe sun.security.ssl.ClientHandshaker e no manipulador da mensagem serverHelloDone, encontrei uma verificação X que não passou: nenhum dos emissores na cadeia de certificados do cliente estava na lista de emissores nos quais o servidor confia ( da mensagem Solicitação de certificado do servidor).

Voltei-me para o cliente em busca de um novo certificado, mas o cliente objetou que tudo funcionava bem para ele e entregou ao Python um script, com o qual ele normalmente verificava os certificados quanto à funcionalidade. O script nada mais fez do que enviar uma solicitação HTTPS usando a biblioteca Requests e retornou 200 OK. Finalmente fiquei surpreso quando o bom e velho cacho também retornou 200 OK. Imediatamente me lembrei da piada: "A empresa inteira sai do caminho, um tenente dá um passo na perna".

Curl é, obviamente, um utilitário respeitável, mas o padrão TLS também não é um pedaço de papel higiênico. Sem saber mais o que verificar, subi sem rumo pela documentação do curl e no Github, onde encontrei um bug tão famoso.

O repórter descreveu exatamente o teste X: em curl com o back-end padrão (OpenSSL), ele não foi executado, ao contrário do curl com o back-end do GnuTLS. Eu não era muito preguiçoso, peguei o cacho da fonte com a opção --with-gnutls e enviei uma solicitação longínqua. E, finalmente, outro cliente, além do JDK, retornou o status HTTP 500 junto com a "exceção de segurança"!

Escrevi sobre isso para o cliente em resposta ao argumento "Bem, a curvatura funciona" e recebi um novo certificado, regenerado e instalado corretamente no servidor. Com isso, minha configuração para o WebClient funcionou bem. Final feliz.

O épico SSL levou mais de duas semanas com toda a correspondência (incluiu o estudo de logs Java detalhados, escolhendo o código de outro projeto que funciona para o cliente e apenas escolhendo o nariz).

O que me deixou desconcertado por um longo tempo, além da diferença no comportamento do cliente, foi que o servidor foi configurado de maneira que o certificado solicitou, mas não foi verificado. No entanto, existem explicações para isso na especificação:
Além disso, se algum aspecto da cadeia de certificados for inaceitável (por exemplo, não foi assinado por uma CA confiável e conhecida), o servidor PODE, a seu critério, continuar o handshake (considerando o cliente não autenticado) ou enviar um alerta fatal.

Source: https://habr.com/ru/post/pt414909/


All Articles