Authentification biométrique améliorée sur iOS et Android avec des clés liées au matériel
Le Face ID seul ne prouve rien à votre serveur. Utilisez Secure Enclave et les clés Android Keystore pour signer des défis uniques afin d’obtenir une authentification renforcée réelle.
Sources: Biometrics, the Secure Enclave, and Auth Tokens: Three Ideas That Sound the Same But Aren’t
Les spécifications des produits indiquent souvent « Face ID déverrouille l’application », et pour les utilisateurs, cette description suffit. Cependant, lorsqu’il s’agit de créer des systèmes de paiement, des dossiers médicaux, des consoles administratives ou un flux de connexion, cela cache l’élément essentiel : un scan biométrique ne parvient jamais à votre backend, ne génère jamais de jeton, et ne transmet jamais à votre code de modèle de visage ou d’empreinte digitale. Ce qu’une application bien conçue obtient réellement, c’est l’autorisation d’utiliser un clé privée que votre code ne peut pas lire, afin que cette clé puisse signer un défi unique que le serveur vérifie ensuite. Ce guide distingue les trois concepts qui ont tendance à être confondus (les demandes biométriques, les clés soutenues par du matériel et les jetons d’authentification) et explique en détail un flux de registration-suivi-de-signature sur les deux plateformes.
Pourquoi un jeton seul constitue une promesse insuffisante sur un appareil mobile
Néanmoins, un point d’entrée sensible doit faire confiance à une affirmation précise : l’appel provient d’un téléphone possédant une clé que le serveur a enregistrée précédemment, et utilisée par une personne il y a peu de temps.
Comparez cela à l’affirmation « cette demande contient un jeton ». Toute valeur stockée en mémoire ou dans le stockage de l’application peut être copiée et reproduite depuis un autre endroit. Une clé privée générée à l’intérieur de matériel protégé et qui ne peut jamais être exportée constitue une cible bien plus difficile, car la voler signifie attaquer le matériel plutôt que de simplement lire une chaîne de caractères.
- Demande biométrique : le téléphone accepte Face ID, Touch ID ou un biométrique fiable d’Android.
- Cle matérielle sécurisée : une clé privée protégée génère une signature.
- Tokén d’authentification : votre serveur accorde une session ou un certain niveau d’accès.
Une conception robuste utilise les trois méthodes, chacune pour sa propre fonction.
HTTPS protège le canal, pas l’identité
HTTPS chiffre les données échangées entre l’application et votre API, de sorte que quiconque se connecte au réseau Wi-Fi d’un café ne peut pas lire ou modifier facilement les requêtes.
phone ── encrypted HTTPS connection ──► server
C’est obligatoire, mais cela ne suffit pas à lui seul. Si un attaquant a déjà obtenu un tokén valide, TLS transmettra fidèlement et en toute sécurité ce tokén volé à votre serveur. Rien dans la connexion chiffrée ne permet au système backend de déterminer si le tokén provient du dispositif légitime ou d’une copie.
Donc, les principes de base sont les suivants : utiliser HTTPS pour chaque appel, valider les certificats de la manière habituelle, maintenir la pile TLS à jour, puis ajouter un mécanisme d’authentification qui fournisse des informations vérifiables concernant le dispositif et l’action de l’utilisateur.
Le pinning des certificats est un outil restreint
Le pinning limite l’application à un certificat ou une identité de clé publique connue. Dans les applications strictement contrôlées, il peut réduire certains scénarios de type « homme du milieu ».
Cependant, il ne permet pas à un jeton porteur de se comporter comme une clé d’authentification gérée par du matériel. De plus, il comporte un véritable risque opérationnel : si le pinning est incorrect ou si une rotation de certificat n’est pas prise en compte, toutes les copies installées de l’application cessent immédiatement de communiquer avec votre API. L’adopter uniquement si votre modèle de menaces l’exige et que votre processus de déploiement permet une rotation fiable des clés, et ne jamais le considérer comme un remplacement de l’authentification liée au dispositif.
Une demande biométrique n’est qu’un oui ou non local
Au iOS, l’application demande à un LAContext de présenter la page d’authentification système ; sur Android, l’équivalent est BiometricPrompt. Le système d’exploitation effectue le scan et indique s’il a réussi ou échoué. Le fragment ci-dessous est en Swift : il évalue la politique basée uniquement sur les biométriques avec une chaîne d’explication et lance le transfert si la vérification est réussie.
let context = LAContext()
let policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics
context.evaluatePolicy(policy, localizedReason: "Approve this transfer") { success, error in
guard success else { return }
startTransfer()
}
Votre code ne voit jamais la géométrie du visage ni les images des empreintes digitales. Apple décrit LocalAuthentication comme fonctionnant avec le Secure Enclave et ne renvoyant à l’application que un résultat de réussite ou d’échec (référence du framework LocalAuthentication).
Cela est tout à fait suffisant pour verrouiller du contenu privé à l’intérieur de l’application. En tant que preuve destinée à un serveur, cela est insuffisant pour trois raisons :
- Le résultat indique uniquement « l’appareil a accepté le contrôle » ; il ne contient aucune information sur l’identité de la personne ou du compte concerné.
- Le système backend ne dispose d’aucun moyen de vérifier une valeur booléenne générée du côté client.
- Si le processus de l’application est compromis, un attaquant peut ignorer ou falsifier la réponse de rappel et réutiliser simplement un jeton déjà existant.
La leçon à retenir de ce schéma est que success n’est qu’un indicateur dans votre propre espace mémoire. Une conception plus solide utilise le contrôle biométrique pour déverrouiller une seule opération cryptographique dont le serveur peut vérifier l’output de manière indépendante.
Les stockages de clés matérielles maintiennent la clé privée en dehors de votre processus
Les appareils Apple disposent du Secure Enclave, un sous-système isolé capable de générer et d’utiliser des clés privées sans jamais exposer le matériel clé brut au processeur principal de l’application. Selon la documentation d’Apple, la clé reste à l’intérieur de l’Enclave et l’application ne reçoit que des résultats tels que des signatures (Guide d’Apple sur le Secure Enclave).
L’équivalent sur l’autre plateforme est le Android Keystore. Selon le matériel, les clés peuvent être protégées par un environnement d’exécution fiable (TEE) ou par un élément sécurisé dédié appelé StrongBox. Faites attention à la nomenclature : « Titan M » désigne le chip présent dans les téléphones Google Pixel, et non une API Android générale ; vous ne devez pas supposer que tous les appareils en disposent. Du point de vue de l’application, ce qui compte, c’est la manière dont la clé est protégée et les règles d’utilisation que vous lui associez.
Pour une action à haute valeur, la clé doit respecter quatre règles :
- Elle est générée sur cet appareil spécifique.
- Chaque utilisation nécessite une nouvelle validation biométrique fiable.
- La partie privée ne doit jamais être exportée.
- Seule la clé publique est stockée sur le serveur.
Sur Android, BiometricPrompt.CryptoObject ne peut autoriser une opération cryptographique que lorsque la classe d’authentification est BIOMETRIC_STRONG, et que la clé peut être configurée pour exiger une nouvelle approbation à chaque utilisation (Référence BiometricPrompt, Aperçu du Keystore).
Le protocole : enregistrer une clé une fois, signer de nouveaux défis par la suite
Tout le schéma comporte deux phases : l’enregistrement et l’utilisation.
Enregistrement
- L’application génère une paire de clés soutenue par du matériel.
- Elle télécharge la clé publique vers le serveur via HTTPS.
- Le serveur enregistre la clé publique accompagnée du compte et d’un identifiant de dispositif.
- La clé privée reste sur le téléphone, à l’intérieur d’un matériel sécurisé.
L’envoi de la clé publique est inoffensif : elle peut vérifier les signatures mais ne peut pas les générer. Seule la clé privée peut signer, et elle ne quitte jamais le matériel protégé.
Approuver une demande sensible
- L’application demande au serveur un défi aléatoire unique.
- L’utilisateur répond à la demande biométrique.
- Le matériel sécurisé signe le défi.
- L’application renvoie l’ID du défi ainsi que la signature.
- Le serveur vérifie la signature à l’aide de la clé publique stockée.
- Le serveur accepte ou rejette l’action, et peut ensuite émettre un jeton.
C’est là que la plupart des implémentations commettent des erreurs. Le jeton doit être aléatoire, expirer rapidement et ne pouvoir être utilisé qu’une seule fois. Il doit également être lié au compte, à la clé du dispositif spécifique ainsi qu’à l’action qui est approuvée (par exemple, « transférer 200 EUR vers le compte X »). En omettant l’un de ces éléments, une signature capturée peut être réutilisée ultérieurement pour une demande différente, ce qui constitue un classique attaque de replay. Le lien avec l’action empêche également qu’une signature collectée pour une opération inoffensive ne soit redirigée vers une opération dangereuse.
iOS : une clé Secure Enclave qui exige Face ID à chaque utilisation
Sur iOS, on crée la clé directement à l’intérieur du Secure Enclave et on y associe un objet de contrôle d’accès qui exige des données biométriques chaque fois que la clé est utilisée. Dans le code Swift ci-dessous, on note trois options possibles. kSecAttrAccessibleWhenUnlockedThisDeviceOnly empêche la clé de migrer vers des sauvegardes ou d’autres appareils. Les flags .privateKeyUsage et .biometryCurrentSet exigent une validation biométrique pour signer et lient la clé aux visages ou doigts actuellement enregistrés. Enfin, kSecAttrTokenIDSecureEnclave est ce qui place la clé dans le matériel ; le Secure Enclave ne prend en charge que des clés à courbe elliptique de 256 bits (P-256), c’est pourquoi le type et la taille de la clé sont définis de cette manière.
// 1. Create access control requiring Face ID / Touch ID for every use
var error: Unmanaged<CFError>?
guard let accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage, .biometryCurrentSet],
&error
) else { return }
// 2. Generate the key pair in the Secure Enclave
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: "com.example.keys.transfer",
kSecAttrAccessControl as String: accessControl
]
]
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else { return }
.biometryCurrentSet est que l’enregistrement d’une nouvelle empreinte digitale ou le réinitialisation de Face ID rendent la clé invalide. C’est généralement ce que l’on souhaite pour les paiements, mais cela signifie que votre application doit gérer correctement le cas où la clé fait défaut et amener l’utilisateur à se réenregistrer.
let algorithm: SecKeyAlgorithm = .ecdsaSignatureMessageX962SHA256
var signError: Unmanaged<CFError>?
// 3. This call automatically triggers the Face ID prompt because of the accessControl flags
guard let signature = SecKeyCreateSignature(
privateKey,
algorithm,
challengeData as CFData,
&signError
) else { return }
api.approve(challengeId, signature)
SecKeyCreateSignature renvoie nil et il n’existe aucune signature à envoyer, donc il n’y a pas de flag de succès que quelqu’un pourrait modifier.
Android : une clé du Keystore déverrouillée via BiometricPrompt
Le Kotlin ci-dessous crée une clé EC uniquement destinée à la signature dans le fournisseur AndroidKeyStore. setUserAuthenticationRequired(true) rend l’authentification obligatoire, et setUserAuthenticationParameters(0, AUTH_BIOMETRIC_STRONG) définit la durée de validité à zéro seconde, ce qui signifie que chaque utilisation nécessite une nouvelle vérification biométrique solide plutôt qu’une période de grâce après le dernier déverrouillage.
val spec = KeyGenParameterSpec.Builder(
"transfer-key",
KeyProperties.PURPOSE_SIGN
).setDigests(KeyProperties.DIGEST_SHA256)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(
0,
KeyProperties.AUTH_BIOMETRIC_STRONG
).build()
val generator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
"AndroidKeyStore"
)
generator.initialize(spec)
val publicKey = generator.generateKeyPair().public.encoded
Envoyez la publicKey encodée au backend après que l’utilisateur se soit connecté normalement, puis stockez-la avec le dossier de l’utilisateur et du dispositif.
Lorsque le serveur enverra plus tard une demande de transfert, initialisez un objet Signature, enveloppez-le dans un CryptoObject, et transmettez-le à l’interface d’authentification. Ce n’est qu’à l’intérieur de onAuthenticationSucceeded que vous devez insérer les octets de la demande et générer la signature.
val signature = Signature.getInstance("SHA256withECDSA")
signature.initSign(privateKey)
val crypto = BiometricPrompt.CryptoObject(signature)
prompt.authenticate(promptInfo, crypto)
override fun onAuthenticationSucceeded(result: AuthenticationResult) {
val signed = result.cryptoObject!!.signature!!
.apply { update(challenge) }.sign()
api.approve(challengeId, signed)
}
Le dialogue n’est pas la frontière de sécurité ; c’est le CryptoObject qui l’est. Le Keystore refusera d’exécuter l’opération de signature à moins que la vérification biométrique associée n’ait réussi, et le matériau clé reste lui-même à l’intérieur du stockage protégé. Votre application ne conserve que la signature résultante.
Sur le serveur, vérifiez cette signature à l’aide de la clé publique enregistrée. Si elle correspond à un défi non expiré et non utilisé, approuvez le transfert ou émettez un jeton à durée de vie courte spécifié pour cette action.
Où appartiennent encore les tokens d’authentification
Vérifier une signature prise en charge par le matériel ne supprime pas les jetons. Après cette vérification, il est tout à fait raisonnable d’émettre un jeton d’accès normal, car les jetons restent le moyen pratique pour autoriser la majeure partie du trafic API. Si vous concevez la partie liée aux jetons du système, les compromis décrits dans notre guide de stratégie pour les jetons de renouvellement s’appliquent également ici.
Gardez ces jetons dans un stockage protégé par la plateforme : le Keychain sur iOS et le stockage géré par Keystore sur Android. Les jetons de renouvellement à longue durée de vie ne doivent jamais être stockés dans des préférences simples, des fichiers ordinaires ou un stockage accessible depuis JavaScript, ce qui est particulièrement important dans les applications React Native où AsyncStorage constitue une option par défaut facile d’accès mais non chiffrée.
La question de conception devient alors celle de la fréquence à laquelle il faut demander une nouvelle signature matérielle :
- Au cours de chaque appel API : presque toujours trop lent et trop agaçant.
- Lors du lancement de chaque application : un choix judicieux pour un verrouillage d’application privé.
- Pour les paiements, les modifications de profil, les exportations de données ou l’approbation d’un nouvel appareil : souvent le compromis idéal.
- Au moment de chaque utilisation d’un token de mise à jour : cela en vaut la peine pour les comptes à haut risque, à condition que l’expérience utilisateur puisse gérer les demandes affichées.
Pensez à la signature comme une preuve renforcée et au token comme l’autorisation à courte durée qui en découle.
Limites des clés soutenues par du matériel
Cette approche est solide, mais elle n’est pas une solution miracle :
- Elle ne sert à rien contre un utilisateur légitime qui déverrouille son propre téléphone et approuve une action nuisible, par exemple sous la pression de l’ingénierie sociale.
Planifiez dès le début le parcours de récupération, en le rendant délibérément plus lent et soumis à des vérifications plus strictes que le parcours habituel. Lorsqu’une clé de dispositif disparaît, que l’écran verrouillé est réinitialisé ou que les informations biométriques changent, votre application doit être prête à enregistrer une nouvelle clé et votre serveur doit annuler l’ancienne. La documentation d’Android indique que les clés liées à l’authentification peuvent être invalidées de manière permanente lorsque l’écran verrouillé sécurisé est modifié ou que les informations biométriques changent, en fonction de la manière dont la clé a été configurée (référence KeyProtection.Builder).
Comment les clés de passe se rapportent à cette conception
Les clés de connexion restent basées sur le même principe : une paire de clés publique/privée et un défi envoyé par le serveur, signé avec la clé privée. Pour se connecter à des sites web ou des comptes, elles constituent généralement la meilleure solution disponible, car le système d’exploitation et la plateforme s’occupent de la majeure partie des tâches complexes. Les clés de connexion sont un protocole standardisé, multiplateforme, qui s’appuie sur ce type de flux de signature soutenu par du matériel. Une nuance : de nombreuses clés de connexion sont synchronisées entre les appareils d’un utilisateur via le gestionnaire de identifiants de la plateforme, ce qui signifie qu’elles ne sont pas toujours liées à un seul téléphone, contrairement aux clés de type Keystore ou Secure Enclave qui ne peuvent pas être exportées.
Cette distinction suggère une séparation simple : optez pour une clé d’application gérée par le matériel lorsque l’application native a besoin d’une preuve liée au dispositif pour une action spécifique. Préférez les clés d’accès pour des connexions sans mot de passe modernes, là où votre produit les prend en charge. Dans tous les cas, la tâche du serveur reste identique : vérifier une signature à l’aide d’une clé publique en laquelle il a déjà confiance.
Choisir l’outil adapté à chaque besoin
- Protéger les données en transit : HTTPS.
- Garder l’utilisateur connecté : un jeton d’accès à durée limitée ainsi qu’un jeton de renouvellement protégé.
- Verrouiller le contenu privé lors de l’ouverture de l’application : Face ID, Touch ID ou BiometricPrompt.
- Approuver un paiement ou une autre action à haut risque : une signature matérielle contrôlée par des données biométriques.
- Connexion sans mot de passe : clés d’accès.
- Réduire le risque d’interception TLS : pinning de certificat.
Points clés
Le Face ID et les scans d’empreintes digitales ne valident pas une demande backend. Ils autorisent l’utilisation d’une clé ; le matériel sécurisé utilise cette clé pour signer un défi serveur frais ; et le serveur vérifie la signature avant de décider des actions autorisées à l’utilisateur.
- Biométrique : approbation locale, invisible pour le serveur.
- Clé matérielle : preuve de possession qui ne peut être copiée hors du dispositif.
- Tokén : autorisation à court terme accordée par le serveur.
Maintenez ces rôles distincts, associez chaque défi à un compte, une clé et une action, et prévoyez la désactivation et le récupération des clés. Le résultat est un système d’authentification mobile plus facile à comprendre et considérablement plus difficile à falsifier.