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 antecipada (AOT) de jars bootclasspath e do servidor do sistema. Como esses artefatos são sensíveis à segurança, o Android 12 usa um recurso chamado assinatura no dispositivo para impedir 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:

  • odrefresh faz parte do módulo Mainline do ART. Ele é responsável por gerar os artefatos de tempo de execução. Ele verifica os artefatos atuais em relação à versão instalada do módulo ART, aos jars do bootclasspath e aos jars do servidor do sistema para determinar se eles estão atualizados ou precisam ser regenerados. Se precisarem ser regenerados, o odrefresh vai fazer isso e armazená-los.

  • 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 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 é chamado de resumo do arquivo. Para 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 violados.

Em condições de erro, como quando o resumo de um arquivo não corresponde, odrefresh e odsign descartam todos os artefatos existentes em /data e tentam regenerá-los. Se isso falhar, o sistema vai voltar 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 em árvores de Merkle. Ao ativar o fs-verity em um arquivo, o sistema de arquivos cria uma árvore de Merkle com os dados do arquivo usando hashes SHA-256, armazena em um local oculto ao lado do arquivo e marca o arquivo como somente leitura. O fs-verity verifica automaticamente os dados do arquivo em relação à á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 de resumo do arquivo fs-verity, e garante que todos os dados lidos do arquivo sejam consistentes com esse resumo.

O odsign usa o fs-verity para melhorar o desempenho da 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 hash de todos os dados do artefato na inicialização. Em vez disso, os dados de artefato são hashados sob demanda pelo fs-verity à medida que são usados, bloco por bloco.

Em dispositivos cujo kernel não é compatível com 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. Portanto, 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

O odsign armazena os resumos dos arquivos dos artefatos em um arquivo separado chamado odsign.info. Para garantir que odsign.info não seja adulterado, odsign.info é assinado com uma chave de assinatura que tem propriedades de segurança importantes. Em particular, a chave pode ser gerada e usada apenas durante a inicialização antecipada, quando 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 vai garantir que os arquivos não foram adulterados desde que foram gerados. O odsign faz isso verificando os resumos dos arquivos. Primeiro, ele verifica a assinatura de odsign.info. Se a assinatura for válida, o odsign vai verificar se o resumo de cada arquivo corresponde ao resumo 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 questões de segurança:

  • O que impede um invasor de usar nossa chave de assinatura para assinar a própria versão de 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 de 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 um 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 evita 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 já aumentou além de 30, e o Keystore se recusa a realizar operações que usam a chave.
  • Os invasores não podem criar uma nova chave porque, quando têm a chance de executar um código malicioso, o nível de inicialização já aumentou 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 vai rejeitá-la.

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

Implementação do Keymaster 4.0

Versões diferentes do Keymaster processam a implementação de chaves de estágio de inicialização de maneiras diferentes. Em dispositivos com um TEE/StrongBox 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 poderá ser gerada do 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 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 anterior é apagada da memória, e as chaves associadas a 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 sempre ocorre pelo menos uma transição de nível de inicialização (para o nível de inicialização final).

Quando um cliente do Keystore, como odsign, solicita a criação de uma chave no nível de inicialização i, o blob é 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 etapas posteriores de inicialização.

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 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 de inicialização, quando nenhum código não confiável está em execução. Isso oferece um nível adicional 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 sua própria chave MAX_USES_PER_BOOT=1 para assinar artefatos. Esse tipo de 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 comprometer o sistema de arquivos poderá 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 controle dele.

Para evitar esse ataque, o odsign cria outra chave HMAC 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 em disco. Em 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 forem iguais, a chave pública será confiável, porque a chave HMAC só pode ser usada em níveis de inicialização antecipada e, portanto, não pode ter sido criada por um invasor.