Arquitetura de assinatura no dispositivo

No Android 12, o módulo Android Runtime (ART) é um módulo Mainline. A atualização do módulo pode exigir a recriação dos artefatos de compilação ahead-of-time (AOT) de arquivos JAR do bootclasspath e do servidor do sistema. Como esses artefatos são sensíveis à segurança, o Android 12 emprega um recurso chamado assinatura no dispositivo para evitar que eles sejam adulterados. Esta página aborda a arquitetura de assinatura no dispositivo e as interações dela com outros recursos de segurança do Android.

Design de alto nível

A assinatura no dispositivo tem dois componentes principais:

  • O odrefresh faz parte do módulo Mainline do ART. Ele é responsável por gerar os artefatos do ambiente de execução. Ele verifica os artefatos atuais em relação à versão instalada do módulo ART, arquivos JAR do bootclasspath e arquivos JAR do servidor do sistema para determinar se eles estão atualizados ou precisam ser regenerados. Se eles precisarem ser regenerados, o odrefresh os gera e armazena.

  • O odsign é um binário que faz parte da plataforma Android. Ele é executado durante a inicialização antecipada, logo após a montagem da partição /data. A principal responsabilidade dele é invocar o odrefresh para verificar se algum artefato precisa ser gerado ou atualizado. Para todos os artefatos novos ou atualizados que o odrefresh gera, o odsign calcula uma função de hash. O resultado desse cálculo de hash é chamado de resumo do arquivo. Para todos os artefatos que já existem, o odsign verifica se os resumos dos artefatos atuais correspondem aos resumos que o odsign havia calculado anteriormente. Isso garante que os artefatos não foram adulterados.

Em condições de erro, como quando o resumo de um arquivo não corresponde, o odrefresh e o odsign descartam todos os artefatos atuais em /data e tentam regenerá-los. Se isso falhar, o sistema volta para o modo JIT.

O odrefresh e o odsign são protegidos pelo dm-verity e fazem parte da cadeia de inicialização verificada do Android.

Cálculo de resumos de arquivos com fs-verity

O fs-verity é um recurso do kernel do Linux que faz a verificação de dados de arquivos com base na árvore de Merkle. A ativação do fs-verity em um arquivo faz com que o sistema de arquivos crie uma árvore de Merkle nos dados do arquivo usando hashes SHA-256, armazene-a em um local oculto ao lado do arquivo e marque o arquivo como somente leitura. O fs-verity verifica automaticamente os dados do arquivo na árvore de Merkle sob demanda à medida que são lidos. O fs-verity disponibiliza o hash raiz da árvore de Merkle como um valor chamado o resumo do arquivo fs-verity, e o fs-verity garante que todos os dados lidos do arquivo sejam consistentes com esse resumo.

O odsign usa o fs-verity para melhorar a performance de inicialização, otimizando a autenticação criptográfica de artefatos compilados no dispositivo durante a inicialização. Quando um artefato é gerado, o odsign ativa o fs-verity nele. Quando o odsign verifica um artefato, ele verifica o resumo do arquivo fs-verity em vez do hash completo do arquivo. Isso elimina a necessidade de ler e fazer o hash dos dados completos do artefato no momento da inicialização. Em vez disso, os dados do artefato são hash sob demanda pelo fs-verity à medida que são usados, bloco a bloco.

Em dispositivos cujo kernel não oferece suporte ao fs-verity, o odsign volta a calcular resumos de arquivos no espaço do usuário. O odsign usa o mesmo algoritmo de hash baseado em árvore de Merkle que o fs-verity, então os resumos são os mesmos em ambos os casos. O fs-verity é obrigatório em todos os dispositivos lançados com o Android 11 e versões mais recentes.

Armazenamento dos resumos de arquivos

odsign armazena os resumos de arquivos dos artefatos em um arquivo separado chamado odsign.info. Para garantir que o odsign.info não seja adulterado, ele é assinado com uma chave de assinatura que tem propriedades de segurança importantes.odsign.info Em particular, a chave só pode ser gerada e usada durante a inicialização antecipada, momento em que apenas o código confiável está em execução. Consulte Chaves de assinatura confiáveis para mais detalhes.

Verificação de resumos de arquivos

Em cada inicialização, se o odrefresh determinar que os artefatos atuais estão atualizados, o odsign garante que os arquivos não foram adulterados desde que foram gerados. O odsign faz isso verificando os resumos de arquivos. Primeiro, ele verifica a assinatura do odsign.info. Se a assinatura for válida, então odsign verifica se o resumo de cada arquivo corresponde ao resumo correspondente em odsign.info.

Chaves de assinatura confiáveis

O Android 12 apresenta um novo recurso do Keystore chamado chaves de estágio de inicialização, que aborda as seguintes preocupações de segurança:

  • O que impede um invasor de usar nossa chave de assinatura para assinar a própria versão do odsign.info?
  • O que impede um invasor de gerar a própria chave de assinatura e usá-la para assinar a própria versão do odsign.info?

As chaves de estágio de inicialização dividem o ciclo de inicialização do Android em níveis e vinculam criptograficamente a criação e o uso de uma chave a um nível especificado. O odsign cria a chave de assinatura em um nível inicial, quando apenas o código confiável está em execução, protegido pelo dm-verity.

Os níveis de estágio de inicialização são numerados de 0 ao número mágico 1000000000. Durante o processo de inicialização do Android, é possível aumentar o nível de inicialização definindo uma propriedade do sistema em init.rc. Por exemplo, o código a seguir define o nível de inicialização como 10:

setprop keystore.boot_level 10

Os clientes do Keystore podem criar chaves vinculadas a um determinado nível de inicialização. Por exemplo, se você criar uma chave para o nível de inicialização 10, ela só poderá ser usada quando o dispositivo estiver no nível de inicialização 10.

O odsign usa o nível de inicialização 30, e a chave de assinatura criada por ele está vinculada a esse nível de inicialização. Antes de usar uma chave para assinar artefatos, o odsign verifica se a chave está vinculada ao nível de inicialização 30.

Isso impede os dois ataques descritos anteriormente nesta seção:

  • Os invasores não podem usar a chave gerada porque, quando um invasor tem a chance de executar um código malicioso, o nível de inicialização aumenta além de 30, e o Keystore recusa operações que usam a chave.
  • Os invasores não podem criar uma nova chave porque, quando um invasor tem a chance de executar um código malicioso, o nível de inicialização aumenta além de 30, e o Keystore se recusa a criar uma nova chave com esse nível de inicialização. Se um invasor criar uma nova chave que não esteja vinculada ao nível de inicialização 30, o odsign a rejeitará.

O Keystore garante que o nível de inicialização seja aplicado corretamente. As seções a seguir detalham como isso é feito para diferentes versões do KeyMint (anteriormente Keymaster).

Implementação do Keymaster 4.0

Diferentes versões do Keymaster processam a implementação de chaves de estágio de inicialização de maneira diferente. Em dispositivos com um TEE/StrongBox do Keymaster 4.0, o Keymaster processa a implementação da seguinte maneira:

  1. Na primeira inicialização, o Keystore cria uma chave simétrica K0 com a tag MAX_USES_PER_BOOT definida como 1. Isso significa que a chave só pode ser usada uma vez por inicialização.
  2. Durante a inicialização, se o nível de inicialização for aumentado, uma nova chave para esse nível de inicialização poderá ser gerada a partir de K0 usando uma função HKDF: Ki+i=HKDF(Ki, "some_fixed_string"). Por exemplo, se você passar do nível de inicialização 0 para o nível de inicialização 10, o HKDF será invocado 10 vezes para derivar K10 de K0.
  3. Quando o nível de inicialização muda, a chave do nível de inicialização anterior é apagada da memória, e as chaves associadas aos níveis de inicialização anteriores não ficam mais disponíveis.

    A chave K0 é uma chave MAX_USES_PER_BOOT=1. Isso significa que também é impossível usar essa chave mais tarde na inicialização, porque pelo menos uma transição de nível de inicialização (para o nível de inicialização final) sempre ocorre.

Quando um cliente do Keystore, como o odsign, solicita a criação de uma chave no nível de inicialização i, o blob dela é criptografado com a chave Ki. Como Ki não está disponível após o nível de inicialização i, essa chave não pode ser criada ou descriptografada em estágios de inicialização posteriores.

Implementação do Keymaster 4.1 e do KeyMint 1.0

As implementações do Keymaster 4.1 e do KeyMint 1.0 são praticamente as mesmas da implementação do Keymaster 4.0. A principal diferença é que K0 não é uma chave MAX_USES_PER_BOOT, mas uma chave EARLY_BOOT_ONLY, que foi introduzida no Keymaster 4.1. Uma chave EARLY_BOOT_ONLY só pode ser usada durante as fases iniciais da inicialização, quando nenhum código não confiável está em execução. Isso oferece um nível extra de proteção: na implementação do Keymaster 4.0, um invasor que compromete o sistema de arquivos e o SELinux pode modificar o banco de dados do Keystore para criar a própria chave MAX_USES_PER_BOOT=1 para assinar artefatos. Esse ataque é impossível com as implementações do Keymaster 4.1 e do KeyMint 1.0, porque as chaves EARLY_BOOT_ONLY só podem ser criadas durante a inicialização antecipada.

Componente público de chaves de assinatura confiáveis

O odsign recupera o componente de chave pública da chave de assinatura do Keystore. No entanto, o Keystore não recupera essa chave pública do TEE/SE que contém a chave privada correspondente. Em vez disso, ele recupera a chave pública do próprio banco de dados no disco. Isso significa que um invasor que compromete o sistema de arquivos pode modificar o banco de dados do Keystore para conter uma chave pública que faz parte de um par de chaves pública/privada sob o controle dele.

Para evitar esse ataque, o odsign cria uma chave HMAC extra com o mesmo nível de inicialização da chave de assinatura. Em seguida, ao criar a chave de assinatura, o odsign usa essa chave HMAC para criar uma assinatura da chave pública e a armazena no disco. Nas inicializações subsequentes, ao recuperar a chave pública da chave de assinatura, ele usa a chave HMAC para verificar se a assinatura no disco corresponde à assinatura da chave pública recuperada. Se elas corresponderem, a chave pública será confiável, porque a chave HMAC só poderá ser usada em níveis de inicialização antecipados e, portanto, não poderá ter sido criada por um invasor.