Pular para o conteúdo
ecdsa.com
← Todos os artigos

Chaves em hardware: Secure Enclave, StrongBox, TPM, PIV

Criamos chaves no Secure Enclave deste Mac, inclusive pós-quânticas, e em um TPM de software com atestado, e lemos as regras de atestado do Android e da Yubico. A promessa em comum é limitada, mas valiosa: a chave não pode ser copiada.

Resposta curta. Uma chave protegida por hardware é gerada dentro de um chip separado ou de um processador isolado e nunca sai dele em texto claro; seu código guarda um handle da chave e pede ao hardware que assine. Isso garante bem uma coisa: a chave privada não pode ser copiada para fora do dispositivo, então uma imagem de disco, um backup ou um dump de memória roubados não têm utilidade. Isso não impede que um malware no mesmo dispositivo peça ao hardware para assinar, a menos que você exija a presença do usuário ou biometria a cada uso. Os algoritmos compatíveis são poucos (P-256 em todas as plataformas, RSA em algumas, os pós-quânticos ML-DSA e ML-KEM no Secure Enclave da Apple desde as versões 26). O atestado permite que um servidor verifique se uma chave está mesmo nesse tipo de hardware, por meio de uma cadeia de certificados até o fabricante: as raízes do Google para o Android, as da Yubico para as YubiKeys, as do fabricante do TPM para os TPMs. Perder o dispositivo significa perder a chave, então planeje desde o início o registro de várias chaves e a rotação delas.

As plataformas

Onde a chave ficaAlgoritmos típicosAtestadoVerificações do usuário
Secure Enclave da AppleUm coprocessador dedicado no Apple siliconP-256; ML-KEM-768/1024 e ML-DSA-65/87 (CryptoKit, OS 26+)App Attest no iOS; nenhum para chaves arbitráriasTouch ID, Face ID, código do dispositivo via controle de acesso
Android Keystore, TEEO ambiente de execução confiável do SoC principalRSA, EC, AES, HMACAtestado de chave até as raízes do GoogleCredencial da tela de bloqueio ou biometria, a cada uso ou por período
Android StrongBoxUm elemento seguro embutido ou um enclave seguro integrado (Android 9+)RSA 2048, P-256, AES, HMAC-SHA256, 3DESIgual, com nível de segurança StrongBoxIgual
TPM 2.0Um chip discreto, um TPM de firmware ou um TPM virtualRSA 2048+, P-256, P-384 (depende do fabricante)AK certificada pela EK, certificado da EK emitido pelo fabricanteValores de autorização e políticas
YubiKey PIVUm applet de cartão inteligente no tokenRSA 1024/2048, P-256, P-384; RSA 3072/4096, Ed25519 e X25519 a partir do firmware 5.7Certificado de atestado do slot f9, encadeado até a YubicoPolítica de PIN e política de toque por chave
HSM em nuvemHSMs operados pelo provedorVeja assinatura com KMS em nuvemDeclarações de atestado do Google Cloud HSMIAM, não pessoas

As chaves de acesso (passkeys) são o uso mais comum de tudo isso: um autenticador de plataforma é uma das quatro primeiras linhas da tabela com uma interface WebAuthn (chaves de acesso para desenvolvedores).

O Secure Enclave da Apple, testado neste Mac

O Secure Enclave é o processador de chaves isolado da Apple. Com o CryptoKit, criar uma chave leva uma linha. Rodamos isto em um Apple M4 Max com macOS 27.0.1 e Swift 6.4, como uma ferramenta de linha de comando simples, sem entitlements especiais:

main.swift: uma chave P-256 do Secure Enclave ao lado de uma chave de software
// Secure Enclave from a command-line tool: create a P-256 signing key, sign, export the public key,
// and look at what the "private key" object actually contains.
import CryptoKit
import Foundation

print("SecureEnclave.isAvailable: \(SecureEnclave.isAvailable)")

let key = try SecureEnclave.P256.Signing.PrivateKey()
let message = Data("release 1.4.2".utf8)
let signature = try key.signature(for: message)

print("public key (x9.63): \(key.publicKey.x963Representation.count) bytes")
print("signature (DER): \(signature.derRepresentation.count) bytes, raw r||s: \(signature.rawRepresentation.count) bytes")
print("verifies with the public key: \(key.publicKey.isValidSignature(signature, for: message))")

// dataRepresentation is an opaque blob: the key encrypted by the Secure Enclave for this device.
let blob = key.dataRepresentation
print("dataRepresentation: \(blob.count) bytes (opaque, usable only by this device's Secure Enclave)")
let restored = try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: blob)
print("restored from the blob signs too: \(restored.publicKey.isValidSignature(try restored.signature(for: message), for: message))")

// A software key from the same API family, for comparison: its raw bytes are the private scalar.
let software = P256.Signing.PrivateKey()
print("software key rawRepresentation: \(software.rawRepresentation.count) bytes (the private scalar itself)")
Saída (macOS 27.0.1, Apple M4 Max; a chave pública PEM foi omitida)
SecureEnclave.isAvailable: true
public key (x9.63): 65 bytes
signature (DER): 71 bytes, raw r||s: 64 bytes
verifies with the public key: true
dataRepresentation: 284 bytes (opaque, usable only by this device's Secure Enclave)
restored from the blob signs too: true
software key rawRepresentation: 32 bytes (the private scalar itself)

Os 284 bytes de dataRepresentation são o que um app armazena (no Keychain ou em um arquivo) para usar a chave de novo mais tarde; esse blob vem criptografado pelo Secure Enclave, e só o enclave desse dispositivo consegue usá-lo. Não há nenhuma forma de pedir o escalar privado: um código que tente fazer isso não compila.

swiftc em um arquivo que lê key.rawRepresentation de uma chave do Secure Enclave
export.swift:5:11: error: value of type 'SecureEnclave.P256.Signing.PrivateKey' has no member 'rawRepresentation'

O guia da Apple Como proteger chaves com o Secure Enclave, escrito para a API SecKey do framework Security, ainda diz que ele “funciona apenas com chaves de curva elíptica NIST P-256”. O CryptoKit foi além: SecureEnclave.MLDSA65, MLDSA87, MLKEM768 e MLKEM1024 estão disponíveis a partir do iOS e do macOS 26. Neste Mac:

pq.swift: chaves pós-quânticas no Secure Enclave
let mldsa = try SecureEnclave.MLDSA65.PrivateKey()
let signature = try mldsa.signature(for: message)
print("ML-DSA-65 in the Secure Enclave: public key \(mldsa.publicKey.rawRepresentation.count) bytes, signature \(signature.count) bytes")
print("verifies: \(mldsa.publicKey.isValidSignature(signature, for: message))")

let mlkem = try SecureEnclave.MLKEM768.PrivateKey()
let encapsulated = try mlkem.publicKey.encapsulate()
let shared = try mlkem.decapsulate(encapsulated.encapsulated)
print("ML-KEM-768 in the Secure Enclave: ciphertext \(encapsulated.encapsulated.count) bytes, shared secrets match: \(shared == encapsulated.sharedSecret)")
Saída
ML-DSA-65 in the Secure Enclave: public key 1952 bytes, signature 3309 bytes
verifies: true
ML-KEM-768 in the Secure Enclave: ciphertext 1088 bytes, shared secrets match: true

Os tamanhos são os valores do FIPS 204 e do FIPS 203. Para chaves arbitrárias, a Apple não oferece nenhum atestado que um servidor possa verificar; no iOS, o App Attest (DeviceCheck) atesta o app e uma chave do Secure Enclave que ele cria para esse fim.

Android Keystore: TEE e StrongBox

No Android, as chaves geradas no Android Keystore ficam “não exportáveis”: “o material da chave nunca entra no processo do aplicativo” e, quando o hardware seguro do dispositivo oferece suporte ao algoritmo e aos parâmetros da chave, “o material da chave nunca é exposto fora do hardware seguro”. Existem dois níveis de hardware:

  • TEE (KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT): um ambiente isolado no processador principal.
  • StrongBox (Android 9 ou posterior, setIsStrongBoxBacked(true)): “SEs embutidos ou Secure Enclaves integrados (iSE), que oferecem isolamento e resistência a adulteração mais fortes que o TEE”, com CPU, armazenamento, gerador de números aleatórios e temporizador seguro próprios. Ele aceita apenas “RSA 2048, AES 128 e 256, ECDSA, ECDH P-256, HMAC-SHA256” e Triple DES, e o Google avisa que ele “é mais lento, tem mais restrições de recursos” e que, “para a maioria dos apps, o StrongBox não é necessário”. Uma solicitação que ele não aceita lança StrongBoxUnavailableException.

Verifique onde a chave foi parar com KeyInfo.getSecurityLevel() (API 31+; isInsideSecureHardware() em versões anteriores) e vincule o uso ao usuário com setUserAuthenticationParameters(), seja por operação, seja por uma janela de tempo após o desbloqueio. Uma ressalva da mesma página: o hardware “normalmente não impõe autorizações com intervalo de validade temporal, porque em geral não tem um relógio de tempo real independente e seguro”.

O atestado de chave prova isso para o seu servidor. O dispositivo retorna uma cadeia de certificados para a chave; o primeiro certificado que traz a extensão de atestado (OID 1.3.6.1.4.1.11129.2.1.17) descreve a chave, as propriedades dela e o nível de segurança. As regras de atestado de chave que vale a pena copiar para o seu verificador:

  • Verifique em um servidor, nunca no próprio dispositivo.
  • A raiz precisa ser uma das raízes de atestado do Google, publicadas em android.googleapis.com/attestation/root. Uma raiz ECDSA P-384, a “Key Attestation CA1”, “vai começar a assinar cadeias de certificados de atestado em 1º de fevereiro de 2026”, ao lado da raiz RSA de longa data; aceite as duas.
  • Confira cada certificado na lista de revogação do Google, em android.googleapis.com/attestation/status.
  • “Somente a primeira ocorrência da extensão na cadeia pode ser considerada confiável”; as seguintes podem vir de um invasor que estendeu a cadeia.
  • Exija attestationSecurityLevel igual a TrustedEnvironment ou StrongBox; uma cadeia até qualquer outra raiz, ou um atestado de software, não diz nada sobre o hardware.

O Google recomenda a própria biblioteca de verificação em Kotlin em vez de verificadores personalizados. A assinatura C2PA da câmera do Pixel é um exemplo construído sobre isso: as chaves de assinatura de claims são “geradas e armazenadas com o Android StrongBox no chip de segurança Titan M2”, e os certificados só são emitidos depois do atestado de chave (credenciais de conteúdo C2PA).

TPM 2.0: chaves, políticas e atestado em um TPM de software

O TPM é o equivalente disso em PCs e servidores. Para mostrar o mecanismo sem hardware, o laboratório rodou o swtpm 0.10.2 (um TPM de software, que por construção não protege nada) com o tpm2-tools 5.8 em debian:forky-20261005-slim. A chave de assinatura é criada sob uma chave primária, com atributos que a prendem a este TPM:

lab.sh (trecho): uma chave de assinatura P-256 não exportável, uma assinatura e a verificação com OpenSSL
tpm2_createprimary -Q -C o -g sha256 -G ecc256 -c primary.ctx
tpm2_create -Q -C primary.ctx -G ecc256:ecdsa-sha256 -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign" -u sig.pub -r sig.priv
tpm2_load -Q -C primary.ctx -u sig.pub -r sig.priv -c sig.ctx
tpm2_readpublic -c sig.ctx -f pem -o sig.pem
tpm2_sign -Q -c sig.ctx -g sha256 -f plain -o msg.sig msg.txt
openssl dgst -sha256 -verify sig.pem -signature msg.sig msg.txt
Saída (filtrada)
attributes:
  value: fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign
sig.priv: 128 bytes (encrypted by the TPM, usable only under this parent)
Verified OK

fixedtpm e fixedparent significam que a chave nunca pode ser duplicada para outro TPM ou outra chave pai; sensitivedataorigin, que o TPM a gerou. sig.priv é a chave criptografada sob a chave pai e pode ser guardada em disco com segurança. A assinatura é ECDSA comum em DER, verificada pelo OpenSSL com a chave pública exportada.

O atestado usa uma chave de atestado (AK) separada, com o atributo restricted. tpm2_certify faz a AK assinar uma declaração sobre outra chave, incluindo o nonce do verificador:

lab.sh (trecho): certificar a chave de assinatura com uma chave de atestado restrita
tpm2_create -Q -C primary.ctx -G ecc256:ecdsa-sha256:null -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|sign" -u ak.pub -r ak.priv
tpm2_load -Q -C primary.ctx -u ak.pub -r ak.priv -c ak.ctx
tpm2_readpublic -Q -c ak.ctx -f pem -o ak.pem
printf 'server-nonce-7f3a' > nonce.bin
tpm2_certify -Q -c sig.ctx -C ak.ctx -g sha256 -q nonce.bin -o attest.bin -s attest.sig -f plain
openssl dgst -sha256 -verify ak.pem -signature attest.sig attest.bin
tpm2_print -t TPMS_ATTEST attest.bin
Saída (filtrada)
Verified OK
magic: ff544347
type: 8017
extraData: 7365727665722d6e6f6e63652d37663361
    name: 000b2d7a257558828e1caf174b67daabff261f8c02077768266be3cdc58015de4bee
    qualifiedName: 000b6d68f45fa311b20dc86b37e1659c1f06055e5641aa6a03e28cbb2caad6646fcd
name of the signing key: 000b2d7a257558828e1caf174b67daabff261f8c02077768266be3cdc58015de4bee

A estrutura começa com o número mágico 0xff544347 (“TPM_GENERATED”), o tipo 0x8017 indica uma declaração de certificação, extraData é o nosso nonce em hexadecimal e name é o hash da área pública e dos atributos da chave certificada: ele bate com o da chave de assinatura. Por que confiar nisso? Porque uma chave restrita se recusa a assinar dados que começam com o número mágico TPM_GENERATED, a menos que o próprio TPM tenha produzido esses dados:

A mesma chave de atestado: dados comuns e, depois, um atestado forjado (um byte alterado)
ordinary data (msg.txt): signed by the attestation key
ERROR: Esys_Sign(0x3E0) - tpm:parameter(3):invalid ticket

Em uma implantação real, a cadeia continua: a AK está vinculada à chave de endosso (endorsement key) do TPM, cujo certificado foi emitido pelo fabricante do TPM, e o verificador confere essa cadeia; o formato de declaração de atestado tpm do WebAuthn funciona assim. Um TPM de software não tem esse certificado, e é exatamente por isso que não se deve confiar no atestado dele.

YubiKey PIV e atestado

O applet PIV de uma YubiKey guarda chaves em slots (9a autenticação, 9c assinatura, 9d gerenciamento de chaves, 9e autenticação do cartão), com uma política de PIN e uma política de toque fixadas no momento da geração. O atestado PIV mostra “que uma determinada chave assimétrica foi gerada no dispositivo, e não importada”: a YubiKey assina um certificado para a chave com a própria chave de atestado, guardada no slot f9, que vem com um certificado emitido por uma CA PIV da Yubico. Os comandos, do yubico-piv-tool 2.7.3 (não temos um token no laboratório, então só conferimos o --help):

yubico-piv-tool (não executado em um token)
yubico-piv-tool -a generate -s 9c -A ECCP256 --pin-policy=always --touch-policy=always -o 9c.pub
yubico-piv-tool -a attest -s 9c > 9c-attest.pem
yubico-piv-tool -a read-certificate -s f9 > f9-intermediate.pem

O certificado de atestado traz extensões da Yubico: versão do firmware (1.3.6.1.4.1.41482.3.3), número de série (…3.7), política de PIN e de toque (…3.8), fator de forma (…3.9) e status FIPS ou CSPN (…3.10, …3.11). O verificador confere a cadeia do certificado do slot, passando pelo certificado f9, até a raiz da Yubico, e depois lê as políticas: “touch always” significa que uma pessoa esteve fisicamente presente em cada assinatura. A Yubico está trocando de raiz: “as YubiKeys com firmware versão 5.7.4 e posteriores serão assinadas pela nossa nova CA raiz”, então os verificadores precisam das duas raízes. O CA/Browser Forum aceita exatamente esse tipo de prova para chaves de assinatura de código (Authenticode), e o OpenSSH usa os mesmos tokens, só que via FIDO2 (ssh-keygen).

O que as chaves em hardware não oferecem

  • Proteção contra código executado como o usuário. Um malware no dispositivo pode pedir ao hardware que assine; ele só não consegue levar a chave embora. Exija presença do usuário (toque), verificação do usuário (PIN, biometria) ou uma confirmação para as chaves que importam.
  • Disponibilidade. Um dispositivo perdido, apagado ou quebrado leva a chave junto, e limpar um TPM (o que algumas atualizações de firmware e reinstalações fazem) invalida as chaves que estão sob ele. Registre pelo menos dois autenticadores, mantenha um caminho de recuperação e planeje a rotação.
  • Agilidade de algoritmos. Você fica com o que o chip implementa. Muitas plataformas ainda oferecem só P-256 e RSA; se um protocolo precisa de Ed25519 ou secp256k1, confira antes de projetar o sistema em torno do hardware.
  • Confiança no que o hardware assina. Uma chave segura assina o que o sistema operacional entregar a ela; a tela que mostra “aprovar pagamento” pertence ao mesmo sistema operacional, possivelmente comprometido. É por isso que existem carteiras de hardware com tela própria.
  • Prova sem atestado. Se o seu servidor não verifica uma cadeia de atestado, “a chave está em hardware” é só o que o cliente diz.

Lista de verificação para usar chaves em hardware

  1. Gere as chaves no dispositivo; nunca importe uma chave que você queira chamar de protegida por hardware.
  2. Escolha o nível de que você precisa: Secure Enclave ou TEE para a maioria dos apps, StrongBox ou um elemento seguro para chaves de alto valor, um HSM para chaves de servidor.
  3. Vincule o uso ao usuário (biometria ou PIN a cada uso) quando uma assinatura autoriza algo.
  4. Verifique o atestado no servidor: raízes (a atual e a nova, após a rotação), revogação, nonce, nível de segurança, propriedades da chave.
  5. Armazene apenas chaves públicas e handles opacos; parta do princípio de que chaves vão desaparecer e permita um novo registro.

Fontes