A engenharia de plataforma não se trata apenas de ferramentas
28 de março de 2024Por que a IA não consegue protegê-lo contra ataques gerados por IA
28 de março de 2024QUIC é a camada de transporte padrão para o protocolo HTTP/3, mas quando decidimos construir suporte para QUIC no NGINX, rapidamente percebemos que o desafio seria tudo menos padrão. O cerne da questão era o suporte ao QUIC na biblioteca OpenSSL, ou melhor, a falta dele. Embora outras bibliotecas SSL como BoringSSL, QuicTLS e LibreSSL apresentem suporte QUIC, nenhuma é tão popular quanto OpenSSL, a biblioteca de fato para transporte seguro pela Internet.
O NGINX tem uma reputação de longa data como proxy e servidor web de alto desempenho, mas nossas opções para uma implementação QUIC análoga pareciam limitadas por forças fora de nosso controle. Confrontados com uma decisão de compromisso aparentemente impossível, esforçámo-nos a pensar de formas inovadoras. Isso nos levou a desenvolver uma solução que se sobrepõe ao OpenSSL como uma interface entre ele e o NGINX. Isso nos permitiu aproveitar a velocidade e a segurança líderes de mercado do OpenSSL com os mais recentes avanços em tecnologia web. Chamamos esse recurso de Camada de Compatibilidade OpenSSL e está disponível no NGINX desde a versão 1.25.0.
Vamos explorar os insights técnicos e as decisões de compromisso que lançaram as bases para a camada de compatibilidade OpenSSL, incluindo suas vantagens e limitações.
O aperto de mão QUIC
A proteção de pacotes QUIC é baseada em TLS 1.3. Isso significa que o QUIC possui um handshake semelhante ao do TLS 1.3, incluindo criptografia de pacotes com uma cifra simétrica e uma chave gerada durante a troca de chaves no handshake. É importante notar que, uma vez que uma chave esteja disponível, a criptografia de pacotes QUIC é simples, portanto não abordaremos isso aqui. Em vez disso, nosso foco será o próprio handshake QUIC.
Ao estabelecer uma conexão TLS com TCP, simplesmente chamamos SSL_do_handshake()
, que lê e grava os registros TLS de handshake. No entanto, o fluxo de mensagens de handshake QUIC não é empacotado em registros TLS. Em vez disso, ele é incorporado em quadros QUIC CRYPTO e requer tratamento especial pela biblioteca SSL. BoringSSL e OpenSSL abordam handshakes QUIC de maneira diferente, então vamos explorar cada método com mais detalhes.
Apertos de mão QUIC com BoringSSL
O BoringSSL fornece uma maneira de ignorar o empacotamento de registros TCP I/O e TLS com funções e retornos de chamada adicionais. Com essa abordagem, passamos um fluxo de mensagens de handshake TLS bruto para dentro e para fora da biblioteca SSL, que é a única responsável pelo handshake. Enquanto isso, somos responsáveis pela implementação do empacotamento de pacotes e molduras. Em teoria, este método abre as portas para a implementação QUIC mais eficiente.
Apertos de mão QUIC com OpenSSL
O objetivo do OpenSSL é ocultar todos os componentes internos do QUIC dentro da biblioteca. Esses componentes internos incluem handshake, fluxos QUIC, controle de congestionamento e muito mais. Com esta abordagem, usamos uma API TLS padrão para QUIC retornar SSL*
objetos que fazem referência a uma conexão ou fluxo QUIC.
No entanto, existem preocupações quanto à quantidade significativa de trabalho necessária para implementar todo o protocolo QUIC no OpenSSL. Com a maior parte desse trabalho aparentemente não relacionado ao TLS, um dos maiores problemas da abordagem OpenSSL é o tempo de desenvolvimento. O roteiro atual programa suporte completo ao QUIC para a versão 3.4, mas pode demorar um pouco até que esta versão seja lançada e chegue aos usuários. Diante disso, nosso instinto inicial foi confiar na API BoringSSL QUIC.
A API BoringSSL QUIC
Vamos explorar a API BoringSSL QUIC, como ela funciona e por que não era a solução ideal para a implementação do NGINX QUIC. A API é atualmente suportada pelas seguintes bibliotecas:
- ChatoSSL
- LibreSSL
- QuickTLS
Observação: QuicTLS é um fork do OpenSSL que fornece uma API QUIC semelhante ao BoringSSL.
As principais funções da API BoringSSL QUIC são:
SSL_set_quic_method()
– Conjuntos Clique aqui para inserir text.callbacksset_read_secret()
eset_write_secret()
que permite segredos de criptografia para criptografia e descriptografia de pacotes QUIC, bem como o retorno de chamadaadd_handshake_data()
para acesso a mensagens de handshake de saída brutas.SSL_provide_quic_data()
– Passa mensagens de handshake de entrada brutas do handshake QUIC.SSL_set_quic_transport_params()
eSSL_get_peer_quic_transport_params()
– Definir e recuperar parâmetros de transporte QUIC.
Ao implementar o QUIC no lado do servidor, após definir os retornos de chamada com SSL_set_quic_method()
precisamos ligar SSL_provide_quic_data()
para cada quadro CRYPTO recebido, seguido por SSL_do_handshake()
como visto no código abaixo. Durante a execução de SSL_do_handshake()
a biblioteca invocará o add_handshake_data()
ligar de volta. Os dados passados para o retorno de chamada devem ser empacotados em um quadro CRYPTO e enviados ao peer. A biblioteca também ligará para o set_read_secret()
e set_write_secret()
retornos de chamada com os segredos para criptografar e descriptografar pacotes QUIC nos níveis de handshake e aplicativo.
No entanto, as bibliotecas que suportam a API BoringSSL QUIC não são tão difundidas quanto o OpenSSL. A maioria dos usuários de Linux e NGINX já possui OpenSSL instalado a partir de pacotes. Construir outra biblioteca SSL a partir da fonte e instalá-la em todos os servidores que precisam de suporte QUIC não é uma solução prática para todos. Com opções limitadas, decidimos construir a camada de compatibilidade OpenSSL, agora fornecida por padrão em todas as distribuições oficiais do NGINX. O recurso permite que os usuários executem NGINX com QUIC usando bibliotecas OpenSSL existentes.
API QUIC sobre API TLS
Como o QUIC usa as mesmas mensagens de handshake do TLS 1.3, era tentador criar uma camada que implementasse a API BoringSSL QUIC sobre uma API OpenSSL TLS normal. No entanto, fazer isso complicaria o código e criaria estes desafios:
- Passando mensagens de handshake TLS brutas dentro e fora do OpenSSL
- Obtendo chaves para os níveis de handshake e criptografia de aplicativos do OpenSSL
- Passando parâmetros de transporte QUIC dentro e fora do OpenSSL
As seções a seguir explicam como esses desafios foram resolvidos no NGINX. Observe que os trechos de código fornecidos são simplificados para maior clareza. O código-fonte completo está disponível aqui.
Mensagens de aperto de mão
A maneira mais fácil de obter entrada e saída de uma conexão OpenSSL é usar BIO, uma API fornecida por OpenSSL para abstração básica de E/S. Com esta API, é possível construir (e analisar) registros TLS com mensagens de handshake, enganando o OpenSSL fazendo-o pensar que estabeleceu uma conexão TLS real. Embora essa abordagem funcione perfeitamente com pacotes iniciais de texto simples, como ClientHello e ServerHello, o OpenSSL espera que os registros TLS de solicitação e resposta subsequentes sejam criptografados. Isso significa que precisaríamos criptografar novamente todos os registros TLS de entrada pós-inicial (como um cliente real faria) e descriptografar a saída antes de passar os dados para a camada QUIC.
Felizmente, houve uma solução melhor para o segundo problema que não requer descriptografia. OpenSSL oferece uma opção para definir um retorno de chamada de mensagem usando SSL_set_msg_callback()
, que retorna mensagens de handshake TLS de saída não criptografadas. Ao usar esse retorno de chamada, simplesmente ignoramos toda a saída do handshake definindo um BIO nulo.
Por outro lado, parecia que não havia um atalho eficaz para entrada. Isso significava que precisaríamos criptografar os registros TLS seguindo sua entrada inicial da seguinte maneira:
Segredos de criptografia
Um recurso útil da API BoringSSL QUIC é a capacidade de definir retornos de chamada set_read_secret()
e set_write_secret()
, que retornam segredos para criptografar e descriptografar pacotes QUIC em diferentes níveis. Da mesma forma, podemos obter essas chaves do OpenSSL com SSL_CTX_set_keylog_callback()
. O retorno de chamada definido por esta função recebe segredos gerados durante uma sessão TLS. Os nomes e valores dos segredos são passados em forma textual. Este é o mesmo método normalmente usado por analisadores de pacotes como o Wireshark para descriptografar sessões.
Para implementar o QUIC, precisamos dos seguintes segredos:
CLIENT_HANDSHAKE_TRAFFIC_SECRET
SERVER_HANDSHAKE_TRAFFIC_SECRET
CLIENT_TRAFFIC_SECRET_0
SERVER_TRAFFIC_SECRET_0
Conseguimos isso da seguinte maneira:
Parâmetros de Transporte
A API BoringSSL QUIC fornece duas funções que obtêm e definem parâmetros de transporte QUIC. Embora o OpenSSL não ofereça esse benefício, ele inclui diversas funções genéricas para lidar com novas extensões TLS. Uma dessas funções é SSL_CTX_add_custom_ext() e conseguimos aproveitá-la da seguinte maneira:
O problema 0-RTT
Anteriormente, abordamos que uma diferença importante entre handshakes QUIC e handshakes TLS está na maneira como as mensagens de handshake são empacotadas. Infelizmente, essa não é a única diferença. Uma mensagem específica de handshake TLS foi proibida de ser usada com QUIC: o EndOfEarlyData
mensagem. Por esse motivo e pelo fato de o QUIC 0-RTT ser processado de forma diferente do TLS, estamos limitados a lidar com handshakes QUIC sem quaisquer dados iniciais. Uma solução seria injetar EndOfEarlyData
no handshake TLS durante a conversão do handshake QUIC. No entanto, fazer isso alteraria o hash da transcrição da sessão TLS e, assim, causaria um MAC finalizado e um fichário PSK quebrados. Embora esses valores possam ser recalculados, isso adicionaria um nível significativo de complexidade ao código. No momento, estamos trabalhando em uma solução para esse problema. A partir de agora, 0-RTT está desabilitado na camada de compatibilidade OpenSSL.
Conclusão
A equipe e a comunidade NGINX conhecem bem a inovação. Quando confrontados com um desafio técnico sem soluções ideais, fizemos o que sabemos fazer melhor: inventar uma nova abordagem e partilhá-la com o resto do mundo. Ser aberto e transparente é fundamental para o nosso espírito na NGINX. Isso é o que a camada de compatibilidade OpenSSL representa para nós e estamos entusiasmados em compartilhar isso com você.
Fique conectado em nginx.org enquanto avançamos nesse recurso e em outras inovações interessantes no NGINX. Enquanto isso, adoraríamos ouvir suas perguntas, sugestões e opiniões nos comentários.
A postagem Como adicionamos suporte QUIC ao OpenSSL sem patches ou reconstruções apareceu pela primeira vez em The New Stack.