Short answer. A hardware-backed key is generated inside a separate chip or isolated processor and never leaves it in plaintext; your code holds a handle and asks the hardware to sign. That guarantees one thing well: the private key cannot be copied off the device, so a stolen disk image, backup or memory dump is useless. It does not stop malware on the same device from asking the hardware to sign, unless you require the user's presence or biometrics per use. Supported algorithms are few (P-256 everywhere, RSA on some, post-quantum ML-DSA and ML-KEM in Apple's Secure Enclave since the 26 releases). Attestation lets a server verify that a key really lives in such hardware, via a certificate chain to the manufacturer: Google's roots for Android, Yubico's for YubiKeys, the TPM vendor's for TPMs. Losing the device means losing the key, so design for enrollment of several keys and rotation from the start.
The platforms
| Where the key lives | Typical algorithms | Attestation | User checks | |
|---|---|---|---|---|
| Apple Secure Enclave | A dedicated coprocessor in Apple silicon | P-256; ML-KEM-768/1024 and ML-DSA-65/87 (CryptoKit, OS 26+) | App Attest on iOS; none for arbitrary keys | Touch ID, Face ID, passcode via access control |
| Android Keystore, TEE | The trusted execution environment of the main SoC | RSA, EC, AES, HMAC | Key attestation to Google roots | Lock screen credential or biometric per use or per time |
| Android StrongBox | An embedded secure element or integrated secure enclave (Android 9+) | RSA 2048, P-256, AES, HMAC-SHA256, 3DES | Same, with security level StrongBox | Same |
| TPM 2.0 | A discrete chip, firmware TPM or virtual TPM | RSA 2048+, P-256, P-384 (vendor-dependent) | AK certified by the EK, EK certificate from the vendor | Authorization values and policies |
| YubiKey PIV | A smart card applet on the token | RSA 1024/2048, P-256, P-384; RSA 3072/4096, Ed25519 and X25519 from firmware 5.7 | Attestation certificate from slot f9, chained to Yubico | PIN policy and touch policy per key |
| Cloud HSM | Provider-operated HSMs | See signing with cloud KMS | Google Cloud HSM attestation statements | IAM, not people |
Passkeys are the most common use of all of this: a platform authenticator is one of the first four rows with a WebAuthn interface (passkeys for developers).
Apple Secure Enclave, tested on this Mac
The Secure Enclave is Apple's isolated key processor. Through CryptoKit a key is one line. We ran this on an Apple M4 Max with macOS 27.0.1 and Swift 6.4, as a plain command-line tool without special entitlements:
// 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)")
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)
The 284-byte dataRepresentation is what an app stores (in the keychain or a file) to use the key again later; it is wrapped by the Secure Enclave and only that device's enclave can use it. There is no way to ask for the private scalar at all: code that tries does not compile.
export.swift:5:11: error: value of type 'SecureEnclave.P256.Signing.PrivateKey' has no member 'rawRepresentation'
Apple's guide Protecting keys with the Secure Enclave, written for the Security framework's SecKey API, still says it "Works only with NIST P-256 elliptic curve keys". CryptoKit went further: SecureEnclave.MLDSA65, MLDSA87, MLKEM768 and MLKEM1024 are available from iOS and macOS 26. On this Mac:
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)")
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
The sizes are the FIPS 204 and FIPS 203 values. For arbitrary keys Apple offers no attestation that a server could check; on iOS, App Attest (DeviceCheck) attests the app and a Secure Enclave key it creates for that purpose.
Android Keystore: TEE and StrongBox
On Android, keys generated in the Android Keystore stay "non-exportable": "Key material never enters the application process", and when the device's secure hardware supports the key's algorithm and parameters, "its key material is never exposed outside of secure hardware." Two levels of hardware exist:
- TEE (
KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT): an isolated environment on the main processor. - StrongBox (Android 9 and later,
setIsStrongBoxBacked(true)): "embedded SEs or integrated Secure Enclaves (iSE), providing stronger isolation and tamper resistance compared to the TEE", with its own CPU, storage, random number generator and secure timer. It supports only "RSA 2048, AES 128 and 256, ECDSA, ECDH P-256, HMAC-SHA256" and Triple DES, and Google warns that it "is slower, more resource-constrained" and "For most apps, StrongBox is not necessary." An unsupported request throwsStrongBoxUnavailableException.
Check where a key ended up with KeyInfo.getSecurityLevel() (API 31+; isInsideSecureHardware() on older versions), and bind use to the user with setUserAuthenticationParameters(), either per operation or for a time window after unlock. One caveat from the same page: hardware "doesn't usually enforce temporal validity interval authorizations, because it normally doesn't have an independent, secure real-time clock."
Key attestation proves it to your server. The device returns a certificate chain for the key; the first certificate carrying the attestation extension (OID 1.3.6.1.4.1.11129.2.1.17) describes the key, its properties and its security level. The key attestation rules worth copying into your verifier:
- Verify on a server, never on the device itself.
- The root must be one of Google's attestation roots, published at
android.googleapis.com/attestation/root. An ECDSA P-384 root, "Key Attestation CA1", "will begin signing attestation certificate chains on February 1, 2026", next to the long-standing RSA root; accept both. - Check every certificate against Google's revocation list at
android.googleapis.com/attestation/status. - "Only the first occurrence of the extension in the chain can be trusted"; later ones may come from an attacker extending the chain.
- Require
attestationSecurityLevelofTrustedEnvironmentorStrongBox; a chain to any other root, or a software attestation, says nothing about hardware.
Google recommends its Kotlin verification library over custom verifiers. Pixel's C2PA camera signing is an example built on this: claim signing keys are "generated and stored using Android StrongBox in the Titan M2 security chip", and certificates are issued only after key attestation (C2PA Content Credentials).
TPM 2.0: keys, policies and attestation, in a software TPM
A TPM is the PC and server equivalent. To show its mechanics without hardware, the lab ran swtpm 0.10.2 (a software TPM, which by construction protects nothing) with tpm2-tools 5.8 in debian:forky-20261005-slim. A signing key is created under a primary key with attributes that bind it to this TPM:
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
attributes:
value: fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign
sig.priv: 128 bytes (encrypted by the TPM, usable only under this parent)
Verified OK
fixedtpm and fixedparent mean the key can never be duplicated to another TPM or parent; sensitivedataorigin that the TPM generated it. sig.priv is the key encrypted under the parent, safe to store on disk. The signature is ordinary ECDSA in DER, verified by OpenSSL with the exported public key.
Attestation uses a separate attestation key (AK) with the restricted attribute. tpm2_certify makes the AK sign a statement about another key, including the verifier's nonce:
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
Verified OK
magic: ff544347
type: 8017
extraData: 7365727665722d6e6f6e63652d37663361
name: 000b2d7a257558828e1caf174b67daabff261f8c02077768266be3cdc58015de4bee
qualifiedName: 000b6d68f45fa311b20dc86b37e1659c1f06055e5641aa6a03e28cbb2caad6646fcd
name of the signing key: 000b2d7a257558828e1caf174b67daabff261f8c02077768266be3cdc58015de4bee
The structure starts with the magic 0xff544347 ("TPM_GENERATED"), type 0x8017 is a certify statement, extraData is our nonce in hex, and name is the hash of the certified key's public area and attributes: it matches the signing key. Why trust it? Because a restricted key refuses to sign data that starts with the TPM_GENERATED magic unless the TPM produced that data itself:
ordinary data (msg.txt): signed by the attestation key
ERROR: Esys_Sign(0x3E0) - tpm:parameter(3):invalid ticket
In a real deployment the chain continues: the AK is tied to the TPM's endorsement key, whose certificate the TPM vendor issued, and the verifier checks that chain; WebAuthn's tpm attestation statement format works this way. A software TPM has no such certificate, which is exactly why its attestation must not be trusted.
YubiKey PIV and attestation
A YubiKey's PIV applet stores keys in slots (9a authentication, 9c signature, 9d key management, 9e card authentication) with a PIN policy and a touch policy fixed at generation time. PIV attestation shows "that a certain asymmetric key has been generated on device and not imported": the YubiKey signs a certificate for the key with its attestation key in slot f9, which comes with a certificate from a Yubico PIV CA. The commands, from yubico-piv-tool 2.7.3 (we have no token in the lab, so only its --help was checked):
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
The attestation certificate carries Yubico extensions: firmware version (1.3.6.1.4.1.41482.3.3), serial number (…3.7), PIN and touch policy (…3.8), form factor (…3.9) and FIPS or CSPN status (…3.10, …3.11). A verifier checks the chain from the slot certificate through the f9 certificate to the Yubico root, then reads the policies: "touch always" means a person was physically present for every signature. Yubico is rotating its root: "YubiKeys with firmware version 5.7.4 and later will be signed by our new root CA", so verifiers need both roots. The CA/Browser Forum accepts exactly this kind of proof for code signing keys (Authenticode), and OpenSSH uses the same tokens through FIDO2 instead (ssh-keygen).
What hardware keys do not give you
- Protection from code running as the user. Malware on the device can ask the hardware to sign; it just cannot take the key with it. Require user presence (touch), user verification (PIN, biometric) or a confirmation for keys that matter.
- Availability. A lost, wiped or broken device takes the key with it, and clearing a TPM (which some firmware updates and reinstalls do) invalidates the keys under it. Register at least two authenticators, keep a recovery path, and plan rotation.
- Algorithm agility. You get what the chip implements. Many platforms still offer only P-256 and RSA; if a protocol needs Ed25519 or secp256k1, check before you design around the hardware.
- Trust in what the hardware signs. A secure key signs whatever the operating system feeds it; the screen showing "approve payment" belongs to the same possibly compromised OS. Hardware wallets with their own screen exist for this reason.
- Proof without attestation. If your server does not verify an attestation chain, "the key is in hardware" is just what the client says.
Checklist for using hardware keys
- Generate keys on the device; never import a key you want to call hardware-backed.
- Choose the level you need: Secure Enclave or TEE for most apps, StrongBox or a secure element for high-value keys, an HSM for server keys.
- Bind use to the user (biometric or PIN per use) where a signature authorizes something.
- Verify attestation on the server: roots (current and rotated), revocation, nonce, security level, key properties.
- Store only public keys and opaque handles; expect keys to disappear and support re-enrollment.
Sources
- Apple: Protecting keys with the Secure Enclave, CryptoKit
SecureEnclave.MLDSA65andSecureEnclave.MLKEM768 - Android: Android Keystore system, key attestation, attestation schema
- TCG TPM 2.0 Library specification (TPMS_ATTEST, restricted keys); tpm2-tools 5.8, swtpm 0.10.2
- Yubico: PIV attestation, YubiKey and PIV (algorithms by firmware), yubico-piv-tool 2.7.3
- Google Security Blog on Pixel C2PA signing