• Estamos compartilhando os detalhes técnicos por trás de como a Proteção Avançada de Navegação (PA) no Messenger protege a privacidade dos links clicados nos bate-papos, ao mesmo tempo em que alerta as pessoas sobre links maliciosos.
  • Esperamos que esta postagem tenha ajudado a esclarecer alguns dos desafios de engenharia e componentes de infraestrutura envolvidos no fornecimento desse recurso aos nossos usuários..

Enquanto criptografia ponta a ponta (E2EE) no Mensageiro garante que mensagens diretas e chamadas sejam protegidas, O recurso Navegação segura do Messenger protege contra links maliciosos em mensagens e chamadas criptografadas de ponta a ponta no aplicativo. Se você receber um link inseguro por algum motivo – talvez tenha sido enviado por alguém que você não conhece ou por um amigo cuja conta foi comprometida – a Navegação segura avisa que o link aponta para um site inseguro que pode tentar roubar suas senhas ou outras informações pessoais.

Em sua configuração padrão, A Navegação segura usa modelos no dispositivo para analisar links maliciosos compartilhados em bate-papos. Mas estendemos isso ainda mais com uma configuração avançada chamada Proteção Avançada de Navegação (PA) que aproveita uma lista de observação continuamente atualizada de milhões de sites potencialmente maliciosos.

Para construir ABP, tivemos que aproveitar uma série de componentes de infraestrutura intrincados, um sistema complexo de primitivas criptográficas, todos trabalhando juntos com o objetivo de proteger a privacidade do usuário no Messenger.

Recuperação de informações privadas – o ponto de partida para ABP

ABP reflete de perto a configuração de uma primitiva criptográfica conhecida como recuperação de informações privadas (PIR). Na configuração PIR clássica, um cliente consulta um servidor (que contém um banco de dados) para saber se o assunto da consulta é ou não membro desse banco de dados. Este protocolo visa que o servidor aprenda o mínimo de informações possível (idealmente nenhuma informação) sobre a consulta do cliente.

Em um cenário teórico, o servidor poderia enviar todo o banco de dados para o cliente, permitindo que o cliente execute pesquisas de consulta subsequentes por conta própria, sem precisar mais envolver o servidor. No entanto, o banco de dados usado pela ABP precisa ser atualizado com frequência, e é muito grande para ser razoavelmente enviado ao cliente. Além disso, revelar todo o banco de dados ao cliente pode ajudar inadvertidamente os invasores que tentam contornar o sistema.

Outro trabalho sugeriu que esta abordagem pode ser melhorada usando uma função pseudoaleatória inconsciente (OPRF) e dividindo o banco de dados em vários fragmentos (ou “baldes”) para que a operação em tempo linear seja executada em uma fração do banco de dados.

Esta abordagem existente foi o ponto de partida para a nossa implementação do ABP, mas havia duas questões que precisávamos adaptar ao nosso ambiente.

  1. Um OPRF funciona bem para consultas que correspondem exatamente ao banco de dados. No entanto, As consultas de correspondência de URL não são correspondências exatas, como descreveremos com mais detalhes, em breve.
  2. Isso também significa que o cliente ainda precisa informar ao servidor qual bucket examinar. Isso introduz inerentemente uma compensação entre a privacidade do sistema versus a eficiência/largura de banda: Quanto menos granulares os baldes, menos eficiente o protocolo se torna, mas quanto menos informações vazarem da consulta do cliente para o servidor.

Existem também outras abordagens, nomeadamente construções criptográficas, que melhoram essa compensação, empregando técnicas baseadas em treliça para reduzir a quantidade de fragmentação necessária. No entanto, no momento em que escrevo, estes não pareciam ser práticos o suficiente para eliminar completamente a necessidade de fragmentação em nossa escala. Esta poderia ser uma direção futura promissora para o sistema, no entanto, e para aplicações industriais de PIR em geral.

Como o ABP trata consultas de prefixo para URLs

As entradas do banco de dados do servidor consistem em domínios de URL com (e sem) caminhos, que nem sempre correspondem às correspondências exatas do link. Por exemplo, se existisse uma entrada para “example.com” em nosso banco de dados, e o cliente envia uma consulta no formulário, “example.com/a/b/index.html” isso deve ser relatado ao cliente como uma correspondência, mesmo que o conteúdo do link não corresponda exatamente.

Em vez de, o que precisamos é de um esquema de “correspondência de URL” que preserve a privacidade entre a consulta do cliente e cada uma das entradas do banco de dados. Subdomínios também são levados em consideração aqui, mas nós os omitimos pela simplicidade deste exemplo.

Uma abordagem simples que consideramos para resolver essas consultas de prefixo foi executar uma série de consultas paralelas para PIR, um para cada prefixo de caminho do URL. Então, em nosso exemplo em execução da consulta do cliente sendo “example.com/a/b/index.html”, o cliente criaria consultas PIR para:

  • exemplo.com
  • exemplo.com/a
  • exemplo.com/a/b
  • exemplo.com/a/b/index.html

Funcionalmente, isso satisfaria a correspondência de prefixo, mas há um problema de privacidade com esta abordagem: Cada uma dessas consultas de prefixo de caminho vaza informações extras sobre o URL real do cliente. Se o esquema PIR que usamos não vazar nenhuma informação para o servidor, então isso pode ser aceitável, mas se o servidor aprender B bits da consulta do cliente, então neste esquema o servidor aprende P * B pedaços, onde P é o número de prefixos de caminho no URL. Para URLs extremamente longos, isso pode até ser suficiente para identificar exclusivamente um link de texto simples!

Para reduzir o vazamento para o servidor, em vez disso, podemos fazer com que o servidor agrupe links que compartilham o mesmo domínio. Por aqui, o cliente pode novamente solicitar apenas um bucket (o bucket correspondente ao domínio do URL), então verifique todos os componentes do caminho do URL de prefixo para associação nesse intervalo.

Na verdade, isso resolveria a questão da privacidade, para que o servidor apenas aprendesse B pedaços. Mas também cria um novo problema de eficiência: Os tamanhos dos baldes podem ficar desequilibrados. Criamos buckets fazendo hash de URLs. Se fizéssemos hash de URLs completos, poderíamos esperar que os tamanhos dos intervalos fossem aproximadamente uniformes porque cada entrada da lista de bloqueio é mapeada para um intervalo pseudo-aleatoriamente. Quando fazemos hash apenas de domínios, isso não é mais o caso. Se muitas entradas da lista de bloqueio compartilharem o mesmo domínio, todos acabam no mesmo balde.

Acontece que, na prática, muitos URLs da lista de bloqueio fazer compartilhar domínios. Por exemplo, considere serviços de encurtamento de links: Esses serviços podem hospedar muitos, muitos URLs (malicioso e benigno) que todos compartilham o mesmo domínio. Se muitos links compartilham o mesmo domínio e, por isso, pertencem ao mesmo balde, então o tamanho do intervalo pode ser muito grande para poder retornar ao cliente. E como aplicamos preenchimento aos buckets, o tamanho da resposta seria igual ao máximo em todos os intervalos!

Conjuntos de regras de pré-processamento

Para resolver este problema, fazemos com que o servidor execute uma etapa de pré-processamento na qual ele tenta equilibrar os buckets gerando um “conjunto de regras”: um conjunto de operações para processar e fazer hash de um determinado URL. O servidor calcula esse conjunto de regras e o compartilha com os clientes antecipadamente para que o cliente possa aplicar o mesmo conjunto de regras no momento da consulta..

Aqui está um exemplo de um conjunto de regras contendo três regras:

Prefixo de hash # de segmentos de caminho
08bd4dd11758b503 2
fe891588d205cf7f 1
c078e5ff2e262830 4


Cada linha é uma regra que mapeia um prefixo hash de 8 bytes para um determinado número de segmentos de caminho para anexar à consulta de URL em execução. Usando nosso exemplo do link “example.com/a/b/index.html,”o cliente começa calculando um pequeno hash do domínio: Hash(“exemplo.com”). Digamos que corresponda a um dos hashes do conjunto de regras,
08bd4dd11758b503. Em seguida, o cliente é instruído a recalcular o hash após anexar dois segmentos de caminho, o que significa que o cliente calcula o novo hash como Hash(“exemplo.com/a/b”) e verifica novamente se o conjunto de regras contém uma entrada para o novo hash. O cliente repete essas etapas até que o prefixo hash não exista no conjunto de regras, nesse ponto, ele para e gera os dois primeiros bytes desse prefixo hash como um identificador de bucket.

O servidor gera o conjunto de regras em um processo iterativo. O servidor começa com a suposição de que cada URL é hash apenas por seu domínio e calcula os buckets iniciais. Em seguida, ele identifica o maior intervalo e encontra o domínio mais comum nesse intervalo. Então, ele divide esse intervalo adicionando uma regra para anexar um ou mais segmentos de URL adicionais para esse domínio. Este processo é repetido até que todos os intervalos estejam abaixo de um limite aceitável.

Devido à forma como o conjunto de regras é gerado, qualquer URL que tenha um prefixo bloqueado tem garantia de hash para o bucket que contém essa entrada. Esta invariante é válida desde que a lista de bloqueio não contenha entradas redundantes (por exemplo, uma entrada para “example.com” e outra para “example.com/a”) e desde que a função hash usada para mapeamento do conjunto de regras não produza nenhuma colisão entre as entradas da lista de bloqueio.

No momento da pesquisa, o cliente usa o mesmo conjunto de regras para calcular o identificador de bucket do URL. O cliente envia o identificador do bucket para o servidor junto com um elemento cego do OPRF para cada segmento do caminho do link de consulta. O servidor responde com o conteúdo do bucket e as respostas ocultadas pelo OPRF. Finalmente, o cliente revela a saída do OPRF e verifica uma correspondência exata de qualquer uma das saídas do OPRF no conteúdo do bucket. Se uma correspondência for encontrada, então o URL é sinalizado.

Observe que para ocultar o número de segmentos do caminho do link de consulta do servidor, devemos preencher adequadamente até um número máximo fixo de elementos para evitar que o comprimento da solicitação revele informações sobre o link. Da mesma maneira, devemos também preencher o conteúdo do balde para que todos os baldes tenham o mesmo comprimento, para que o comprimento da resposta do servidor não revele informações sobre o link do cliente.

Protegendo as consultas dos clientes

Agora, na descrição deste protocolo até agora, o cliente ainda envia um identificador de bucket (calculado a partir do URL) ao servidor para poder processar a consulta com eficiência. Podemos usar mecanismos adicionais para reduzir ainda mais os bits de informação que um servidor hipoteticamente adversário poderia obter da consulta do cliente, que abordaremos nas seções seguintes.

Computação Confidencial

Para limitar a exposição desses prefixos hash aos servidores Meta, nós aproveitamos Tecnologia SEV-SNP da AMD para fornecer uma máquina virtual confidencial (CVM) para o qual o código do lado do servidor processa esses prefixos hash. Em alto nível, a CVM fornece um ambiente para executarmos o código do aplicativo para o qual podemos gerar relatórios de atestado. Também nos permite inicializar um canal seguro de um cliente para a CVM depois que o cliente estabelece “confiança”, verificando esses relatórios de atestado.

Um relatório de atestado contém:

  • Um manifesto de contêiner contendo resumos de hash da configuração de lançamento e dos pacotes da CVM, que atua essencialmente como um compromisso com a lógica do aplicativo em execução no CVM.
  • Uma chave pública gerada na inicialização da CVM, correspondente a uma chave privada que permanece protegida dentro do TEE.
  • Uma cadeia de certificados, com seu certificado raiz estabelecido pelo Key Distribution Service da AMD.
  • Uma assinatura do testemunha de registro de transparência, que fornece uma garantia de exclusividade que mitiga equívocos do lado do servidor

Ao receber este relatório, o cliente verifica todos os certificados/assinaturas e então usa a chave pública incorporada para estabelecer um canal seguro com a CVM. Este canal seguro é utilizado pelo cliente para transmitir o identificador do bucket para a CVM, que então usa a chave de descriptografia correspondente para descriptografar a solicitação do cliente para obter o identificador do bucket de texto simples.

Ano passado, postamos sobre nosso uso do AMD SEV-SNP para fornecer um ambiente de execução confiável para Processamento privado do WhatsApp, e muitos dos detalhes por trás da configuração do hardware são semelhantes lá.

Um aspecto que falta neste procedimento de verificação é a liberação desses artefatos para que pesquisadores de segurança externos possam validar. Nosso objetivo é fornecer uma plataforma para hospedar esses artefatos em um futuro próximo.

RAM alheia

Embora as garantias de hardware fornecidas pelo AMD SEV-SNP nos permitam reduzir a exposição desses prefixos de hash e enviá-los através de um canal criptografado, eles não são suficientes por si só para ocultar completamente esses prefixos hash de um observador que obtém privilégios administrativos do sistema host para monitorar os acessos à memória ao longo do tempo. Embora as páginas de memória sejam criptografadas por meio do Secure Nested Paging da AMD (SNP) tecnologia, os próprios padrões de acesso também devem ser mantidos privados.

Uma maneira simples de resolver isso seria carregar o banco de dados na memória da máquina na inicialização., e mediante solicitação de cada cliente, garantir que cada um dos buckets B no banco de dados seja recuperado da memória, mesmo que apenas um bucket esteja realmente incluído na resposta do servidor. Embora isso seja um desperdício de uma perspectiva puramente computacional (os acessos B-1 não são realmente levados em consideração na resposta), o servidor pode evitar o vazamento direto do índice do bucket que está sendo buscado para um adversário que pode observar seus padrões de acesso à memória ao atender solicitações do cliente.

Para um banco de dados realmente grande, esses acessos B-1 podem acabar sendo um gargalo no tempo de execução geral do servidor. Existem dois métodos que utilizamos para otimizar essa sobrecarga de desempenho sem comprometer a privacidade:

  1. Como nosso banco de dados é (no momento em que escrevo) não esmagadoramente grande, podemos colocar várias cópias diferentes do mesmo banco de dados na memória de uma única máquina. As solicitações recebidas do cliente recebem uma dessas cópias com base na disponibilidade, uma vez que a varredura linear é inerentemente sequencial por natureza.
  2. Podemos melhorar o número de acessos assintoticamente, de linear para sublinear, contando com um algoritmo chamado Caminho ORAM.

Os detalhes exatos de como o Path ORAM funciona em nosso ambiente estão além do escopo desta postagem, mas você pode encontrar mais informações sobre isso em nossa biblioteca de código aberto para Path ORAM.

Usando HTTP esquecido

Para fortalecer ainda mais as garantias de privacidade da ABP, aproveitamos um proxy de terceiros e o HTTP alheio (OHTTP) protocolo para desidentificar solicitações de clientes. O proxy de terceiros fica entre o cliente e o servidor, processar solicitações criptografadas de clientes, retirando informações de identificação deles e encaminhando essas solicitações desidentificadas para o servidor, qual, por sua vez, é capaz de descriptografar a carga útil da solicitação. Isso torna mais difícil para o servidor observar identificadores (como o endereço IP do cliente).

O ciclo de vida da solicitação ABP

O ciclo de vida geral do ABP para uma solicitação funciona da seguinte maneira:

Fase de pré-processamento/em segundo plano:

  1. Periodicamente, o servidor obtém as atualizações mais recentes do banco de dados de URL, computar iterativamente um conjunto de regras que equilibra as entradas do banco de dados em intervalos de tamanhos semelhantes.
  2. Esses baldes são então carregados em um TEE usando ORAM.
  3. O TEE gera um par de chaves, e a chave pública está incorporada em um relatório de atestado, gerado pelo hardware AMD SEV-SNP.
  4. O relatório de atestado e o conjunto de regras atual do banco de dados são fornecidos ao cliente mediante solicitação (por meio de um proxy de terceiros).
  5. O cliente verifica as assinaturas contidas no relatório de atestado, e armazena localmente uma cópia da chave pública e do conjunto de regras do banco de dados.

E então, em cada solicitação do cliente correspondente a um clique no link:

  1. O cliente, ao clicar em um link em um chat E2EE, calcula o identificador do bucket para o link aplicando as regras do “conjunto de regras” ao URL.
  2. Este identificador de bucket é criptografado para a instância CVM específica usando sua chave pública.
  3. O cliente também calcula uma série de solicitações OPRF (elementos do grupo cego), um para cada segmento de caminho do URL (acolchoado).
  4. O identificador de bucket criptografado, junto com essas solicitações OPRF, são enviados através de um proxy de terceiros para o servidor, juntamente com uma chave pública do cliente como parte do estabelecimento de um canal seguro.
  5. O servidor pré-calcula a avaliação do lado do servidor das solicitações OPRF para produzir respostas OPRF.
  6. O servidor então descriptografa o identificador do bucket, usa ORAM para procurar o conteúdo do bucket correspondente, e retorna as respostas OPRF e o conteúdo do bucket ao cliente, criptografado sob a chave pública do cliente.
  7. O cliente então descriptografa a resposta do servidor, e usa o conteúdo do intervalo junto com as respostas do OPRF para concluir a avaliação do OPRF e determinar se uma correspondência foi encontrada. Se uma correspondência foi encontrada, então o cliente exibe um aviso sobre o link de consulta.