Afin d’améliorer la sécurité, de plus en plus d’entreprises souhaitent utiliser des certificats numériques pour chaque utilisateur de terminaux Android. Il est alors possible d’ouvrir des connexions HTTPS (SSL v3) avec le serveur, avec une authentification mutuelle forte. Le client est certain du serveur qu'il consomme. Le serveur est certain du client qu'il alimente.
Pour rappel, les certificats numériques sont des fichiers binaires possédant la clef publique d'une bi-clef et d'autres informations. La clef privée permet de prouver que l'utilisateur est bien celui qu'il prétend être.
L’utilisation de certificat numérique a pour objectif d’augmenter la sécurité par rapport à l’utilisation d’un couple utilisateur/mot de passe. En effet, les utilisateurs ont tendance à utiliser le même mot de passe pour des usages différents. Une compromission du mot de passe de l’utilisateur permet généralement au pirate d’obtenir un accès à tous les services qu’il utilise.
L'utilisation de certificat numérique permet de passer du modèle « je connais » (le mot de passe) à « je possède » (un certificat). Souvent, une combinaison des deux est appliquée : je connais le mot de passe qui permet d'avoir accès au certificat que je possède. Dans ce scénario, le mot de passe est validé localement, lors du déchiffrement du certificat.
Cela permet également de ne pas avoir à maintenir une base de données avec les mots de passe des utilisateurs. Par principe, tous les certificats utilisateurs signés par une autorité sont valides. Ainsi, une compromission du serveur ne révèle pas les secrets des utilisateurs. Seuls les numéros des certificats doivent être mémorisés afin de pouvoir les révoquer.
La révocation du certificat client permet de fermer l'accès aux services de l'entreprise.
Les certificats peuvent également être utilisés pour chiffrer des données dans le téléphone. Ainsi, le vol du terminal ne révélera pas les informations sensibles.
Dans l’idéal, les certificats numériques doivent être générés par l’utilisateur avant d’être validés par l’autorité de certification. La génération peut être réalisée en software ou par une puce sécurisée. La signature du certificat proposé par le client ne doit s’effectuer qu’après certaines vérifications. Ainsi, l’utilisateur ne peut pas répudier ses activités.
Si le certificat est généré sur le serveur, il existe des scénarios permettant à l’entreprise d’effectuer des actions au nom de l’utilisateur, à son insu. En effet, à un moment du processus, le certificat avec la clef privée de l’utilisateur a été présent sur le serveur. En mémorisant ces informations, le serveur est capable de se faire passer pour l’utilisateur.
L’utilisation de certificat client est plus ou moins bien intégrée dans les terminaux Android, suivant les versions du système d’exploitation.
Nous allons étudier les différentes technologies de sécurité présentes dans Android, publiques ou cachées, nous permettant d’exploiter une authentification forte, avec le maximum de sécurité. Ces technologies peuvent également être exploitées pour protéger tout secret applicatif.
Nous devons résoudre plusieurs challenges pour utiliser ces technologies dans les meilleures conditions. Nous serons capables d'importer et d'installer un certificat dans le mobile ; le sauver dans le périphérique dans un espace parfaitement sécurisé, résistant au vol du terminal et réduire au maximum le nombre d'applications ayant besoin d'avoir accès aux secrets.
1. Approche traditionnelle
Android est un système d'exploitation qui simule l'exécution simultanée de nombreuses applications. Techniquement, ce n'est pas le cas. Seules les dernières applications utilisées sont encore présentes en mémoire. Les plus anciennes sont tuées après avoir sauvegardé leurs contextes.
Il est possible de demander à l'utilisateur le mot de passe permettant de déchiffrer un certificat numérique et de le garder en mémoire. Mais, à chaque destruction de l'application par l'OS (lors de la réception d'un appel téléphonique, par exemple), la mémoire est perdue. Lorsque l'application reprend, il faut à nouveau demander le mot de passe de l'utilisateur.
De par la structure de l'OS, il n'est pas concevable de demander un mot de passe dès qu'une application a besoin d'exploiter un certificat numérique.
Il est donc nécessaire de mémoriser le certificat ou le mot de passe de l'utilisateur sans chiffrement, directement dans la mémoire statique du téléphone. Cela l'expose à différentes attaques. Est-ce qu'un pirate peut analyser la mémoire statique du téléphone pour retrouver le certificat ?
Une approche subtile consiste à placer la clef privée ou le mot de passe dans un extra de l'Intent de l'activité courante où dans le onSaveInstanceState(). Ainsi la clef est mémorisée par le framework Android, dans le processus system_app. Il faut faire cela dans toutes les activités. Techniquement, les données peuvent alors être sauvées sur disque par le framework, exposant alors la clef ou le mot de passe.
Cette approche n'est pas satisfaisante au niveau sécurité, au niveau développeur ou au niveau de l'expérience utilisateur.
Pour gérer un certificat numérique, nous devons résoudre plusieurs challenges techniques :
- Comment installer ou générer le certificat sur le terminal ?
- Comment protéger le certificat d’un vol du terminal ou de l'exploitation d'une faille ?
- Comment réduire au maximum l'exposition des secrets ?
Nous allons étudier toutes les solutions qui s'offrent à nous pour cela.
2. Protéger le certificat dans le périphérique
Les certificats doivent être installés dans l'Android. Est-ce que les certificats sont correctement sécurisés ?
2.1 Secure Element (SE)
Un SE est un composant électronique capable de recevoir des mini-applications et de communiquer via des trames binaires. C'est essentiellement un mini ordinateur sur un simple composant, avec CPU, ROM, EEPROM, RAM et des ports I/O. Les dernières générations sont équipées de co-processeurs cryptographiques capables d'implémenter les algorithmes standards comme DES, AES et RSA. Ces composants utilisent des techniques pour résister à différentes attaques physiques, afin de rendre impossible l'extraction de données (Figure 1).
Typiquement, il s’agit de composants compatibles Java Card Runtime Environment (JCRE). Suivant les versions des téléphones, il peut exister plusieurs éléments de sécurité. Un SE peut être présent dans la puce du téléphone, ajouté à l’aide du port SD du téléphone ou directement embarqué dans le périphérique.
Dans ce dernier scénario, le composant est généralement lié au composant NFC. Dans ce cas, il existe trois modes d’utilisation. Soit le composant est désactivé, soit il est directement en écoute des trames NFC venant de l’extérieur du téléphone. Les applications embarquées dans le SE sont alors capables de simuler une carte bancaire ou de transport. Soit le composant est visible des applications comme s’il s’agissait d’une communication avec une carte bancaire externe au téléphone (Figure 2).
Les communications NFC peuvent fonctionner en trois modes : simulation de tag passif, peer-to-peer ou simulation de carte à puce. À ce jour, ce dernier mode n'est pas disponible par soft, sauf à utiliser une version spéciale de Cyanogen.
Les SE servent généralement à mémoriser les secrets comme les clefs privées, les accès VPN, ou les générateurs de mot de passe à usage unique (One Time Password – OTP).
Il existe des applications Javacard pour offrir un environnement permettant de gérer une PKI via un SE. Le projet M.U.S.C.L.E.[1] en est un exemple. Il permet de connecter les API SSL avec le SE pour la protection de la clef privée et la génération des clefs symétriques.
Malheureusement, Android ne propose pas d’API pour communiquer avec ces composants. Le projet seek-for-android propose une librairie complémentaire pour Android, permettant d’accéder à ces différents composants. Elle est parfois présente sur certains terminaux[2] comme le Sony Xperia S et les Samsung Galaxy S3 et S2 NFC. Pour y accéder, il est nécessaire de lier l’application à la librairie, via un marqueur <use-library/>.
<uses-library android:name="org.simalliance.openmobileapi" android:required="true" />
Cette approche n'étant pas standard, nous décidons de ne pas l'utiliser.
2.1.1 SE dans la puce 3G
Android est découpé en deux parties distinctes. Un processeur est chargé du système d'exploitation. Un autre processeur économe en ressource est chargé des communications radios. Cette partie du logiciel n'est pas publique. Les deux composants communiquent entre eux à l'aide de commandes textuelles, préfixées par AT. Cela vient des modems Hayes[3] à l'initiative de cette norme, permettant de séparer les données destinées aux modems des données destinées aux réseaux téléphoniques, à l'époque où les modems étaient connectés via des liaisons séries type RS232.
Pour communiquer avec la puce 3G, il faut enrichir le vocabulaire AT du module radio[4] conformément aux spécifications 3GPP TS 27.007[5].
- AT+CSIM (Generic SIM access)
- AT+CCHO (Open Logical Channel)
- AT+CCHC (Close Logical Channel)
- AT+CGLA (Generic UICC Logical Channel Access)
Sans intégration dès l'origine de ces commandes par le constructeur, il n'est pas possible de communiquer avec le SE présent dans la puce du téléphonique. Les opérateurs font du forcing pour autoriser cela, afin de capter le marché des applications dans les puces SIM. Ils facturent l'installation d'applications dans la puce, voire chaque transaction. Comment tuer un marché naissant ?
Une autre approche consiste à exploiter le Single Wire Protocol[6] pour communiquer avec la puce, via la connexion NFC. C'est le cas des Galaxy Nexus et Nexus S, mais désactivé par défaut.
2.1.2 SE intégré
Android 2.3.4 et supérieur propose une API non publique pour manipuler le SE intégré aux téléphones ou tablettes récents. Malheureusement, cette API nécessite un privilège system, qui ne peut être accordé qu'au fournisseur du téléphone ou à Google pour la famille Nexus. L'application doit être signée par le signataire de la plate-forme.
Android 4.0.4 (API niveau 15) modifie cela en utilisant à la place une liste blanche de signatures autorisées (/etc/nfcee_access.xml). Cette liste est figée sur la plate-forme. Elle peut être mise à jour Over The Air (OTA) par le constructeur. Ce fichier indique la signature et la liste des packages valides.
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<signer android:signature="30820...90">
<package android:name="org.foo.nfc.app">
</package></signer>
</resources>
Avec un accès root, il est possible de remonter la partition en mode lecture/écriture pour modifier ce fichier. Seule la permission NFC est alors nécessaire. L'application doit également ajouter une librairie.
<uses-library
android:name="com.android.nfc_extras"
android:required="true" />
Ensuite, une API minimaliste permet de communiquer des trames binaires avec le SE.
NfcAdapterExtras adapterExtras =
NfcAdapterExtras.get(NfcAdapter.getDefaultAdapter(context));
NfcExecutionEnvironment nfceEe = adapterExtras.getEmbeddedExecutionEnvironment();
nfcEe.open();
byte[] response = nfcEe.transceive(command);
nfcEe.close();
Google utilise cela pour Google Wallet. Il s'agit du portefeuille sécurisé de Google, pour y placer toutes les cartes de crédit, données particulièrement sensibles.
Trois applications sont installées sur le SE : l'applet de contrôle du portefeuille, une applet de gestion MIFARE, et bien sûr, l'applet de simulation de carte bancaire, compatible avec les terminaux PayPass[7]. La première applet active ou désactive l'applet de simulation de carte suivant la saisie d'un code PIN par l'utilisateur. La deuxième applet sert de conteneur aux offres et tickets numériques (métro de Londres par exemple). La dernière simule une EMV classique comme les cartes bancaires. Cette dernière est active ou non suivant le verrouillage du téléphone. Le NFC est paramétré pour faire transiter les trames vers le SE correspondant.
Ce composant est très intéressant pour pouvoir sauver notre clef privée, ou mieux encore, pour demander la création de clef symétrique lors de l'ouverture d'une communication TLS.
Nous ne pouvons pas utiliser cela, car seul le Trusted Service Manager[8] (TSM) du composant est habilité à installer des applications dans le SE. Google envisage d'étendre Google Wallet aux autres applications[9], mais ce n'est pas le cas à ce jour (avril 2013).
Nous avons exploré les techniques hardwares sans succès, car elles ne sont pas encore accessibles aux applications.
2.2 Chiffrement du disque
Depuis la version 3.x d'Android, il est possible de chiffrer l'intégralité du disque du terminal. Lors du boot de ce dernier, un mot de passe est demandé. Ce dernier permet alors de rebooter le terminal sur la partition Android, pour déchiffrer l'intégralité des secteurs à la volée. Initialement, le mot de passe de déchiffrement est identique au mot de passe de déverrouillage du téléphone. Ce dernier permet de déchiffrer le mot de passe de chiffrement du disque, présent dans le dernier secteur.
Ainsi, toutes les données présentes sur le téléphone sont protégées. Il n'est plus possible – hors force brute ou récupération de la clef en RAM[10] – de récupérer les informations. Cela peut être exigé par le gestionnaire du parc des téléphones.
Cette approche présente néanmoins quelques inconvénients. Il est nécessaire d'avoir un mot de passe alpha-numérique pour débloquer la session de l'utilisateur ; le démarrage du terminal est particulièrement fastidieux, car il faut lancer deux fois de suite un OS Android (la première fois pour pouvoir demander la saisie du mot de passe, la deuxième fois pour l'OS avec déchiffrement). Les utilisateurs préfèrent alors utiliser un mot de passe relativement simple, exposant ainsi le terminal à une attaque par force brute. Les performances sont légèrement dégradées, car il faut chiffrer/déchiffrer tous les accès disques.
Avec un téléphone rooté, il est possible d’utiliser un mot de passe de déchiffrement du disque, différent du mot de passe de déverrouillage du téléphone.
$ su -c vdc cryptfs changepw newpass
200 0 0
L'application Cryptfs password[11] propose une interface utilisateur pour cela, avec un téléphone rooté.
Avec beaucoup de temps, si le mode debug est actif, il est possible d'essayer différents mots de passe pour le déverrouillage du téléphone, en injectant des touches.
adb shell input text PASSWORD
adb shell input keyevent 66
Pour débloquer un téléphone et le passer éventuellement en mode root, il faut généralement l’éteindre une fois. Pour des raisons de sécurité, cela efface l'intégralité des données. C'est la procédure standard pour débloquer un téléphone de la famille Nexus, via l'utilitaire fastboot.
fastboot oem unlock
Cet état s’identifie par le petit cadenas qui est ouvert lors du boot du téléphone, sur le premier écran.
Le chiffrement est une protection contre l'extinction du téléphone. C'est généralement nécessaire pour passer un téléphone quelconque à root. Donc, cela protège les données.
Par contre, si le téléphone est déjà rooté, c'est plus facile pour l'attaquant. Un adb shell lui permet d'avoir accès à toutes les données déchiffrées. Les dernières versions de l'OS ne permettent pas d'accrocher un adb au device, sans l'accord de ce dernier. Il faut débloquer le téléphone pour pouvoir brancher un adb, donc il faut avoir l'utilisateur à côté de soi. Ainsi, même en root, il n'est plus possible d'attaquer un téléphone avec le port USB.
Le chiffrement ne protège pas contre une vulnérabilité de l'OS. En effet, toutes les applications ont un accès déchiffré aux données. Tant que le téléphone n'est pas éteint ni verrouillé, il est possible d'installer une application exploitant une faille permettant de devenir root, et d'avoir ainsi accès à toutes les données. Les dernières versions de l’OS ne possèdent pas de vulnérabilité publique permettant d’être root “à chaud”.
Nous pouvons sauvegarder nos certificats dans les données des applications, mais rien ne garantit que l'utilisateur chiffre le disque. Nous ne pouvons pas l'imposer. Cette approche est donc à éliminer pour protéger nos données.
2.3 KeyStore
Contrairement à iOS, il n'existe pas officiellement d'API pour protéger les secrets des applications. Chaque application peut sauver des informations dans son espace protégé dans la mémoire statique. Les autres applications ne peuvent pas y accéder. Mais, en cas de vol du téléphone, une analyse de la mémoire de masse, en dehors de l'OS, permet de retrouver tous les secrets.
Il existe pourtant un composant bien caché, permettant de protéger les données. En effet, le système Android lance un service interne codé en C, permettant de protéger les différents certificats numériques. Les applications peuvent communiquer avec ce dernier, via un socket local, pour demander le déblocage du conteneur, y stocker des secrets ou de les retrouver. Les secrets sont isolés par application.
Ce conteneur est chiffré à l'aide d'un dérivé d'un mot de passe, appliqué suffisamment de fois pour résister à une attaque en force brute.
Le comportement est différent suivant les versions de l'OS.
Pour les versions 2.x, le menu sécurité propose à l'utilisateur d'indiquer un mot de passe pour ce conteneur. Il est alors possible d'y stocker des données. C'est ici que se trouvent les certificats numériques permettant une identification Wifi ou VPN. Lorsqu'une application a besoin d'accéder à ces certificats, l'utilisateur doit saisir au moins une fois le mot de passe afin de débloquer le conteneur (Figure 3).
Pour les versions suivantes d'Android, le mot de passe n'est pas spécifique au conteneur. Il s'agit d'un dérivé du verrou du téléphone. Si le téléphone est verrouillé, le conteneur est bloqué. Lorsque l'utilisateur déverrouille le téléphone, le conteneur est ouvert aux applications. Il n'est donc pas possible d'utiliser ce conteneur sans verrou du téléphone.
L'API n'est pas publique, car elle impose de sauvegarder la clef privée dans le conteneur. Google veut se réserver la possibilité de sauvegarder la clef privée dans un composant type SE. Pour ne pas proposer d'API qu'il sera difficile de maintenir dans le temps, Google préfère ne pas exposer ce code.
En attendant, c'est un module très sympathique pour y placer notre clef privée.
Pour communiquer avec ce composant, il faut ouvrir un LockSocket avec le nom keystore. Ensuite, il faut envoyer des ordres en respectant un protocole propriétaire. Pour ne pas avoir à tout coder, il est facile d'extraire des sources d'Android, la classe android.security.KeyStore et de renommer le package. Cela fonctionne parfaitement avec les versions d'Android supérieures à 2.0. Au-delà de la version ICE CREAM SANDWICH, il existe une autre approche officielle pour les certificats clients, même si l'approche KeyStore fonctionne toujours. Nous pouvons supposer que lorsque la classe KeyStore sera supprimée, une approche alternative sera disponible.
Cette classe propose plusieurs méthodes intéressantes. Certaines sont accessibles à toutes les applications, d'autres non. Nous pouvons demander l'état du composant (state()) pour savoir s'il est bloqué ; sécuriser un tableau de byte (put()) et le récupérer (get()). Mais, impossible de débloquer le KeyStore par l'Api.
Il faut utiliser une autre approche consistant à déclencher une activité pour cela. Ainsi l'utilisateur garde le contrôle (Figure 4).
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB)
context.startActivity(new Intent("android.credentials.UNLOCK"));
else
context.startActivity(new Intent("com.android.credentials.UNLOCK"));
Comme cette activité n'invoque pas setResult(), il faut gérer un petit automate à état dans votre activité. Avant de demander l'accès à une donnée, il faut vérifier si le conteneur est bloqué. Si c'est le cas, placez votre activité dans l'état d'attente de déblocage et lancez l'activité UNLOCK. N'oubliez pas de sauver l'état dans la méthode onSaveInstanceState() et de le récupérer dans onCreate().
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
if (savedInstanceState!=null)
{
mState=savedInstanceState.getInt(EXTRA_STATE);
}
}
@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putInt(EXTRA_STATE, mState);
}
En effet, l'activité déclenchée pour déverrouiller le conteneur fait partie d'une autre application. La vôtre peut donc être tuée par l'OS.
Dans le onResume() de votre activité, si l'état courant est en attente de déblocage et que le conteneur n'est plus bloqué, vous pouvez demander à nouveau d'avoir accès à la donnée protégée. Sinon, l'utilisateur n'a pas été capable de débloquer le conteneur.
Après trois échecs de saisie du mot de passe du KeyStore, le conteneur est effacé.
3. API de certificats numériques sous Android
Android est capable de gérer des conteneurs de certificats clients. L'intégration n'est pas excellente, mais s'améliore de version en version.
Pour les versions 2.x (API version 5+), seuls les certificats permettant d'identifier l'utilisateur pour une connexion WIFI ou pour ouvrir un VPN sont possibles. La procédure d'installation du certificat n'est pas très simple, mais elle fonctionne. Il s'agit de déposer un fichier PKCS12 chiffré dans la racine de la carte SD. Ensuite, le menu Paramètre/Sécurité permet de déclencher l'analyse de ce répertoire. Il faut ensuite sélectionner le certificat à installer. Après saisie d'un mot de passe valide, le certificat est installé sans mot de passe dans le terminal et effacé de la carte SD. Comme le certificat n'est pas lui-même protégé, il doit être placé dans un conteneur lui-même chiffré. C'est le sens du « mot de passe de stockage des identifiants » que nous avons vu pour le KeyStore (Figure 5).
Le risque de cette approche est que la carte SD possède une version officiellement effacée, mais accessible. En récupérant ce fichier, il est envisageable d'effectuer une attaque en force brute off-line, pour récupérer la clef privée.
Pour les versions 3 et suivantes d'Android (API level 11+), il est possible d'installer des certificats clients pour une utilisation Web. Il est même possible de sélectionner les autorités de certifications. Lors de la navigation sur un site Web demandant une authentification mutuelle, une boite de dialogue demande le certificat à utiliser pour ouvrir la connexion.
Les certificats clients sont utilisés par le navigateur par défaut. Les navigateurs Opera ou Firefox ne savent pas exploiter ce conteneur pour récupérer les certificats clients à utiliser. Chrome l'exploite à partir de la version beta 27 sortie le 10 avril 2013.
Depuis ICE CREAM SANDWICH (API level 14+), des APIs sont disponibles. Elles permettent de donner l'autorisation aux applications d'avoir accès à certaines clefs privées, sous le contrôle de l'utilisateur.
Il est à noter que l'API VpnService permet aux applications d'imposer un VPN privé pour leur utilisation. Les autres applications peuvent continuer à exploiter le réseau, sans le VPN. Le projet ICS-openvpn[12] est un exemple d'implémentation. C'est une approche souvent ignorée, et pourtant très pertinente pour séparer les flux pro et perso dans l'utilisation d'un BYOD.
3.1 KeyChain
Depuis l'API niveau 14 d'Android, la classe KeyChain permet d'exploiter les certificats clients présents dans le terminal. Le principe est le suivant. Si une application a besoin d'un certificat, elle déclenche un service pour demander à l'utilisateur d'en sélectionner un. L'application est alors enregistrée comme étant habilitée à avoir accès à la clef privée du certificat sélectionné. L'application peut alors la demander pour ouvrir une connexion TLS. Cela est mémorisé dans une base de données pour limiter les accès aux autres certificats. C'est l'utilisateur qui a la responsabilité d'indiquer le ou les certificats qu'une application peut consommer.
En pratique, cela donne :
KeyChain.choosePrivateKeyAlias(this,
new KeyChainCallBack()
{
@Override
public void alias(String alias)
{
mAlias=alias;
}
},
new String[] {"RSA"}, // List of acceptable key types. null for any
null, // issuer, null for any
"internal.example.com", // host name of server requesting the cert
443, // port of server requesting the cert, -1 if unavailable
null); // alias to preselect, null if unavailable
La call-back alias() est alors invoquée pour signaler le nom symbolique du certificat que l'application peut exploiter. La plupart des informations de cette méthode ne sont là qu'à titre informatif pour le client.
Puis, deux méthodes permettent de consommer le certificat client.
KeyChain.getCertificateChain(this, mAlias);
KeyChain.getPrivateKey(this, mAlias);
Ces deux méthodes peuvent être judicieusement exploitées dans les paramètres TLS.
Les certificats sont mémorisés dans le Keystore. Ce dernier est protégé avec la clef de verrouillage de l'écran. Ainsi, il n'est plus nécessaire d'avoir un mot de passe spécifique. Si l'utilisateur débloque l'écran, le Keystore est déverrouillé.
Il existe quelques techniques pour débloquer l'écran, si le mode débuggage est actif. Il est possible d'intervenir directement sur la base de données des paramètres.
adb shell
cd /data/data/com.android.providers.settings/databases
sqlite3 settings.db
update system set value=0 where name='lock_pattern_autolock';
update system set value=0 where name='lockscreen.lockedoutpermanently';
.quit
reboot
sqlite3 n'est pas toujours disponible. Il est alors possible de supprimer le fichier du schéma.
adb shell rm /data/system/gesture.key
reboot
Au reboot, un schéma est demandé, mais il est possible d'indiquer n'importe quoi.
Ces techniques ne fonctionnent pas toujours, car maintenant, il faut être root pour intervenir et avoir le privilège de connecter un adb. Néanmoins, avec un émulateur, vous pouvez vous amuser à tester cela.
Quel est l'impact sur la protection du KeyStore ? Et bien, dans ce cas, un mot de passe est demandé à l'utilisateur.
Avec un accès à la mémoire via l'interface électronique JTAG, il est possible de brute-forcer le verrou du téléphone[13] ou de retrouver en mémoire les clefs privées. Il faut récupérer le fichier /data/system/password.key et le sel présent dans la base de données settings.db sous la clef lockscreen.password_salt. Contre cela, il n'y a rien à faire :-(
3.2 Installation de certificats
Pour exploiter le KeyChain, il faut installer les certificats numériques dans le conteneur sécurisé. Nous avons vu qu'il est possible de faire cela en plaçant un fichier PKCS12 dans la racine de la sdcard (ou sa simulation), puis en déclenchant une activité. Cela correspond à l’appui sur la commande « Installer depuis la carte SD » du paramétrage d'Android.
Intent intent = new Intent("android.credentials.INSTALL");
intent.putExtra(EXTRA_NAME, CERT_NAME); // Controle le nom du certificat
startActivityForResult(intent, RESULT_CODE);
Il faut avouer que cela n'est pas très propre. En effet, rien n'indique qu'il n'y a pas d'autres certificats déjà présents dans le répertoire ; le fichier est effacé dans la carte SD, mais uniquement logiquement. Il est possible de le récupérer pour essayer une attaque en force brute.
Il est possible de faire autrement. KeyChain offre différents EXTRA pour injecter le contenu d'un certificat à installer.
Intent intent =KeyChain.createInstallIntent();
intent.putExtra(KeyChain.EXTRA_NAME, CERT_NAME); // Controle le nom du certificat
intent.putExtra(KeyChain.EXTRA_PKCS12, out.toByteArray());
startActivityForResult(intent, RESULT_CODE);
Cela fonctionne uniquement lorsque la classe KeyChain est disponible (API Level 14). Cette approche impose à l'utilisateur de saisir le mot de passe du certificat puis de confirmer son alias.
Pour installer directement une autorité de confiance, il faut en obtenir une version en mémoire, puis la donner dans le paramètre EXTRA_CERTIFICATE.
Intent intent =KeyChain.createInstallIntent();
intent.putExtra(KeyChain.EXTRA_NAME, CERT_NAME);
intent.putExtra(KeyChain.EXTRA_CERTIFICATE,ca.getEncoded());
startActivityForResult(intent, RESULT_CODE);
Après validation de l'alias par l'utilisateur, l’autorité est installée.
Notez que les certificats clients ne sont pas présents dans l'interface de paramétrage des certificats. Seules les autorités sont visibles.
Nous pouvons donc installer un certificat client, puis demander le droit de l'exploiter à l'utilisateur et le présenter dans une connexion TLS avec authentification mutuelle.
4. android-keychain-backport
Nous avons maintenant tous les éléments pour proposer une approche compatible avec les différentes versions d'Android (enfin, supérieures à la version 7). Pourquoi ne pas proposer une librairie de compatibilité qui permet de marier le meilleur des deux mondes ?
L'idée est de simuler la classe KeyChain pour les versions inférieures à 14 d'Android.
Nous utiliserons différents services pour installer un certificat dans le KeyStore ou le KeyChain suivant les cas. Nous sommes contraints de gérer également le déblocage du KeyStore pour les anciennes versions. Nous proposons alors d'ajouter une méthode isUnlock() et unlock(Activity) à notre classe KeyChain.
Comme nous avons besoin d'un Context, nous ne pouvons pas proposer strictement la même API que KeyChain. Nous ne pouvons pas utiliser de méthodes statiques comme le propose l'interface officielle.
Notre nouvelle classe de compatibilité a alors la forme suivante :
public class KeyChain
{
public KeyChain(Context context) { … }
public boolean isUnLocked() { … }
public void unlock(Activity context) { … }
public Intent createInstallIntent() { … }
public void choosePrivateKeyAlias( … ) { … }
public X509Certificate[] getCertificateChain( … ) { … }
public PrivateKey getPrivateKey(Context context,String alias)( … ) { … }
}
Elle est très similaire à la classe KeyChain officielle.
Vous trouverez le code de la librairie Android ici : https://github.com/pprados/android-keychain-backport
Il est également nécessaire de déclarer quelques Activity dans le fichier AndroidManifest.xml.
<!-- You MUST add this three activities in your application. -->
<!-- Activity to install a certificat after enter the password. -->
<activity
android:name="android.support.v7.security.impl.CertInstaller"
android:configChanges="orientation|keyboardHidden"
android:theme="@style/KeyChain_Transparent" />
<!-- Activity to select a certificate to use -->
<activity
android:name="android.support.v7.security.impl.CertChooser"
android:configChanges="orientation|keyboardHidden"
android:theme="@style/KeyChain_Transparent" />
<!-- Activity to unlock the local container. Do nothing. -->
<activity android:name="android.support.v7.security.impl.UnlockActivity" />
En effet, il n'est pas encore possible d'hériter de paramètres décrits dans le AndroidManifest.xml d'une librairie Android.
Cette classe simule la classe officielle si elle n'est pas disponible. Sinon, elle délègue simplement les méthodes.
Avant d'utiliser cela, il faut vérifier que le conteneur n'est pas bloqué par un appel à isUnlocked(). Si c'est le cas, il faut demander le déblocage du conteneur via unlock(). Cela déclenche une nouvelle activité. Au retour, dans onResume(), il est possible de reprendre le traitement.
Voici des copies d'écrans d'un exemple d'utilisation avec les différentes boites de dialogues (Figure 6 à 9).
Et voici un extrait du code. Je n'ai laissé que le strict minimum. Le reste est avec les sources.
public class MainActivity extends Activity
{
...
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
if (savedInstanceState!=null)
mState=savedInstanceState.getInt(EXTRA_STATE);
}
@Override
protected void onResume()
{
super.onResume();
// Switch the current state, after unlock the key store, continue the job
switch (mState)
{
case STATE_INSTALL:
installCertificate(null);
break;
case STATE_USE:
useCertificate(null);
break;
}
}
@Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putInt(EXTRA_STATE, mState);
}
public void installCertificate(View view)
{
if (!mKeyChain.isUnLocked())
{
// Key store is locked. Start an activity to unlock it.
mState=STATE_INSTALL;
mKeyChain.unlock(this);
}
else
{
mState=0;
...
}
}
public void chooseCertificate(View view)
{
mKeyChain.choosePrivateKeyAlias(this,
new KeyChainCallBack()
{
@Override
public void alias(final String alias)
{
if (alias!=null)
{
mAlias=alias;
// Save last alias
mPreference.edit().putString(ALIAS_KEY, alias).commit();
}
},
null,null,null,-1,null);
}
public void useCertificate(View view)
{
if (!mKeyChain.isUnLocked())
{
// Key store is locked. Start an activity to unlock it.
mState=STATE_USE;
mKeyChain.unlock(this);
return;
}
mState=0;
...
}
}
Le code est le même quelque soit les versions d'Android.
Côté serveur, vous pouvez utiliser une instance Tomcat et la paramétrer pour accepter les connexions TLS avec authentification mutuelle. Avec les sources, dans le répertoire tomcat_conf, vous trouverez les paramètres pour accepter le certificat client présent dans l'application de démonstration.
Il y a quand même quelques petites modifications à l'usage. Avant ICS, les certificats sont installés localement à l'application. Chaque application doit alors installer le certificat dont elle a besoin. Ce dernier ne peut pas être utilisé pour une connexion Web via le navigateur classique.
Pour les versions ICS et suivantes, le certificat est installé globalement pour toutes les applications et pour le navigateur Web standard. Les autres applications peuvent demander à utiliser directement le certificat installé.
5. Utilisation via un AccountAuthenticator
Android propose un cadre de travail pour ajouter un nouveau type de compte, généralement compatible OAuth2. Ce mécanisme permet à l’utilisateur d’ajouter un compte depuis la console de paramétrage du téléphone. Il est possible d’utiliser ce framework pour gérer les certificats clients (Figure 10).
La documentation proposée par Google pour utiliser cela n’est pas très claire ni très juste. Il est possible de s’inspirer de l’exemple SampleSyncAdapter mais avec précaution.
Pour offrir un nouveau type de compte, il faut proposer un service qui répond à l’intention android.accounts.AccountAuthenticator. Le service doit indiquer des méta-données sous forme de fichier XML.
<service
android:exported="true"
android:name=".CertificateAuthenticationService"
>
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/certificate_authenticator"/>
</service>
Le fichier XML décrit le type de compte, ainsi que les icônes à utiliser pour ce dernier.
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="fr.prados.android.account.certificate"
android:icon="@drawable/ic_launcher"
android:smallIcon="@drawable/ic_launcher"
android:label="@string/authenticator_certificate_label"
/>
Le service est simple à proposer. Il doit juste renvoyer une instance dérivée de Authenticator dans la méthode onBind().
public class CertificateAuthenticationService extends Service
{
private Authenticator mAuthenticator;
@Override
public void onCreate()
{
mAuthenticator = new Authenticator(this,new CertificateProvider(this));
}
@Override
public IBinder onBind(Intent intent)
{
return mAuthenticator.getIBinder();
}
}
L’instance Authenticator hérite de AbstractAccountAuthenticator et répond aux différentes méthodes. Le principe est le suivant. Chacune peut soit retourner des valeurs, soit demander une intervention de l’utilisateur pour pouvoir les retourner. Par exemple, l’utilisateur peut être sollicité pour saisir son nom et son mot de passe afin de pouvoir retourner un token de connexion. Les méthodes retournent un Bundle avec les valeurs de retours sous différentes clefs, ou bien un Intent permettant de démarrer une activité via la clef KEY_INTENT. Si une méthode peut retourner une information, elle le fait immédiatement. Sinon, si elle a besoin de demander des précisions à l’utilisateur (mot de passe, validation des privilèges), la méthode construit un Intent avec toutes les informations permettant de retourner le résultat lorsque l’utilisateur aura validé son formulaire.
Le framework standard d’Android se charge d’invoquer l’instance Authenticator, puis de déclencher ou non l’activité correspondante si un Intent est retourné.
L’intent de l’activité doit posséder la clef KEY_ACCOUNT_AUTHENTICATOR_RESPONSE avec l’instance response présente dans les paramètres des méthodes. Cela permettra à l’activité d’informer du résultat du formulaire.
Pour simplifier l’implémentation, les activités doivent hériter de AccountAuthenticatorActivity. Cela permet de bénéficier de la méthode setAccountAuthenticatorResult() pour déposer le résultat de l’activité.
Avec ce mécanisme, il est possible de l’exploiter pour installer un certificat client. La difficulté consiste à réussir à transmettre la clef privée et la chaîne des certificats aux applications.
Il n’est pas difficile d’ajouter une confirmation de l’utilisateur lors de l’implémentation de getAuthToken(). Comme le fait Google, un écran affiche les applications souhaitant avoir accès au token. Cela est alors mémorisé dans une base de données SQLite pour ne plus demander cela à nouveau. Il faut également réagir à un broadcast PACKAGE_REMOVED pour nettoyer la base de données lors de la suppression d’une application.
Côté client, pour utiliser cette API, il faut utiliser la méthode getAuthToken() de AccountManager. Une instance dérivée de AccountManagerCallback<Bundle> permet alors de récupérer les informations. Par ce canal, il est possible de récupérer la clef privée et la chaîne de certificats. Il suffit d’utiliser des clefs spécifiques.
Attention, comme Android garde en cache que le password, il faut que l'application invalide le dernier authToken avant de demander à nouveau le token avec les certificats. En effet, comme nous utilisons des clefs complémentaires, les données ne sont pas mémorisées dans le cache.
mAccountManager.invalidateAuthToken(mAccountType, mAuthToken);
mAccountManager.getAuthToken(
mAccount, // Account retrieved using getAccountsByType()
mAuthTokenType, // Auth scope
mOptions, // Authenticator-specific options
this, // Your activity
mAccountManagerCallback, // Callback
mHandler); // Callback called if an error occurs
Pour interdire aux applications ne venant pas de l'entreprise, d’exploiter ce mécanisme pour voler la clef privée, nous devons ajouter une nouvelle permission.
<permission
android:name="fr.prados.USE_CREDENTIEL"
android:label="@string/permission_use_certificate_label"
android:description="@string/permission_use_certificate_desc"
android:protectionLevel="signature"
android:permissionGroup="android.permission-group.ACCOUNTS"
/>
Celle-ci est indispensable aux applications souhaitant se binder sur notre service.
<uses-permission android:name="fr.prados.USE_CREDENTIEL"/>
Comme la nouvelle permission utilise un protectionLevel de type signature, seules les applications signées numériquement avec le même certificat numérique peuvent en bénéficier.
Mais, nous rencontrons une difficulté pour vérifier les privilèges de l'appelant. En effet, notre application n'invoque pas directement notre AccountAuthenticator. L'application invoque le framework Android qui lui invoque notre AccountAuthenticator. Au passage, le numéro du processus client est perdu.
Pour régler cela, Android propose, depuis la version 11, deux clefs permettant de récupérer ces informations lors de l'invocation d'une méthode de l'AccountAuthenticator. Mais, comme cela est important pour la sécurité, en regardant les sources de la version Gingerbread, on découvre qu'un backport est disponible. Impossible de connaître la version exacte à partir de laquelle cela est disponible.
Bon, pour régler cela, proposons des alias suivant les versions.
public static final String KEY_CALLER_UID =
(VERSION.SDK_INT>=VERSION_CODES.HONEYCOMB)
? AccountManager.KEY_CALLER_UID : "callerUid";
public static final String KEY_CALLER_PID =
(VERSION.SDK_INT>=VERSION_CODES.HONEYCOMB)
? AccountManager.KEY_CALLER_PID : "callerPid";
Puis, dans la méthode getAuth(), nous pouvons vérifier les privilèges de l'appelant si cela est disponible.
if (USE_PERMISSION)
{
if (options==null)
return errorDenied();
int pid=options.getInt(KEY_CALLER_PID);
int uid=options.getInt(KEY_CALLER_UID);
if (mContext.checkPermission(PERMISSION, pid, uid)
==PackageManager.PERMISSION_DENIED)
return errorDenied();
}
L’implémentation de l'AccountAuthenticator utilise bien entendu, le conteneur sécurisé proposé par Android. En effet, il est possible de sauver un mot de passe dans le framework, via la méthode setPassword() d’un AccountManager, mais ce dernier est alors mémorisé dans une base de données SQLite, accessible uniquement par root. La base n’est pas chiffrée. Un téléphone rooté peut alors révéler les secrets.
Cette approche permet de partager l'authentification de l'utilisateur par toutes les applications de l'entreprise. Nous avons proposé cela à un grand opérateur de logistique, pour authentifier tous les livreurs.
6. Utilisation TLS
Maintenant que nous savons comment installer un certificat numérique depuis une application, comment le protéger et comment le récupérer, nous pouvons exploiter cela pour initialiser une connexion TLS. Cela s'effectue via la création de deux classes spécifiques.
final KeyManager[] keyManagers=new KeyManager[]
{
new X509KeyManager()
{
…
@Override
public String[] getClientAliases(String keyType, Principal[] issuers)
{
return new String[]{mAlias};
}
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket)
{
return mAlias;
}
@Override
public X509Certificate[] getCertificateChain(String alias)
{
return mKeyChain.getCertificateChain(MainActivity.this, mAlias);
}
@Override
public PrivateKey getPrivateKey(String alias)
{
return mKeyChain.getPrivateKey(MainActivity.this, mAlias);
}
}
};
final X509TrustManager[] X509TrustManager=new X509TrustManager[]
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
{
MessageDigest.getInstance("MD5").digest(chain[0].getEncoded());
if (!Arrays.equals(DIGEST, currentDigest))
throw new CertificateException("Invalid server certificate");
}
...
};
Suivi d'une ouverture de connexion TLS.
if (mSocketFactory==null)
{
final SSLContext sslcontext =
SSLContext.getInstance(TLS_IMPLEMENTATION_ALGORITHM)
SecureRandom random=SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM);
sslcontext.init(
keyManagers,
X509TrustManager,
random);
mSocketFactory=sslcontext.getSocketFactory();
}
// Open socket
SSLSocket socket=(SSLSocket)mSocketFactory.createSocket();
socket.connect(new InetSocketAddress(mHost,mPort),SOCKET_TIMEOUT);
// Write HTTP request
PrintWriter out=new PrintWriter(socket.getOutputStream());
BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream()));
out.write("GET / HTTP/1.0\n\n");
out.flush();
// Read HTTP response
System.out.println(in.readLine());
socket.close();
Notez qu'il est très important de recycler autant que possible le SocketFactory. En effet, lors de l'ouverture d'une connexion TLS, une clef symétrique est négociée entre les parties. Cette clef est gardée en cache dans le contexte SSL. Ainsi, lors d'une prochaine connexion, la clef symétrique est proposée au serveur. S'il l'accepte, cela améliore notablement les performances. De plus, initialiser un SecureRandom prend un temps certain, afin de garantir l'entropie. Comme l'instance peut être partagée par plusieurs tâches, il est préférable d'utiliser un singleton pour cela.
7. Utilisation HTTPS
Pour utiliser cela en HTTPS, rien de plus simple. Il suffit d'indiquer le SSLSocketFactory à utiliser pour les connexions HTTPS et de valider éventuellement tous les hosts.
HttpsURLConnection.setDefaultSSLSocketFactory(mSocketFactory);
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier()
{
@Override
public boolean verify(String hostname, SSLSession session)
{
return true;
}
BufferedReader in=new BufferedReader(new InputStreamReader(con.getInputStream()));
in.readLine();
Pour des raisons de sécurité, ce n'est pas bien d'accepter tous les hosts sans vérifier leurs signatures.
8. Comment ne pas exposer la clef privée ?
Toutes ces approches présentent l'inconvénient de donner accès à la clef privée aux applications qui en ont besoin. L'approche avec plusieurs APK permet de limiter cette exposition, mais toutes les applications de l'entreprise ont bien accès à la clef privée. Est-ce vraiment nécessaire ? Il serait sympathique que la clef privée ne soit pas visible par les applications qui ont simplement besoin d'ouvrir un socket authentifié.
8.1 ClientAuth="want"
Côté serveur, il est possible de demander un certificat client, mais de tolérer qu'il ne soit pas fourni par l'utilisateur. Avec le paramètre clientAuth à want de Tomcat, il est possible d'imaginer un scénario n'exposant pas la clef privée aux applications. L'astuce est la suivante.
Une application qui est la seule à avoir accès à la clef privée ouvre une connexion vers le serveur Web avec une authentification mutuelle. Le serveur constate que le client ne possède pas de cookie. Il exige de vérifier l'identité de l'utilisateur via un certificat client. Si le certificat présenté est valide, le serveur génère un cookie de session et le retourne au client.
L'application est alors en capacité de donner la valeur du cookie aux applications désirant ouvrir une connexion vers le serveur. Si le cookie est signé et possède l'identité de l'utilisateur, cela fonctionne très bien.
Les autres applications récupèrent le cookie et ouvrent une connexion TLS mais sans présenter de certificat client. Le serveur, constatant que le cookie est valide, ignore le fait que le certificat client est absent. Il extrait l'identité de l'utilisateur du cookie.
Ainsi, la clef privée est protégée des vulnérabilités présentes dans les applications de l'entreprise. Cela exige, côté serveur, d'accepter des communications sans certificat client. Cela peut avoir un impact sur la traçabilité des actions des utilisateurs.
Cette approche est très similaire à OAuth. Les applications peuvent exploiter les communications, sans connaître les secrets.
8.2 Approche Ninja
Encore plus fort : une approche permettant d'avoir toujours un certificat client, sans propager le secret de la clef privée.
Les sockets sont ouverts via des handles de fichiers Linux. Android permet d'envoyer un handle de fichier à une autre application. Un dup() est alors effectué par le noyau. Il est donc possible d'ouvrir un socket dans une application et l'utiliser dans une autre ! La difficulté réside dans la création d'une instance socket java propre, simplement à partir du handle du socket.
Il faut analyser comment la classe est construite pour reconstruire une instance utilisable, depuis un autre processus.
Nous sommes toujours dans une architecture avec plusieurs APK. L'un possède la clef privée et est en charge d'ouvrir une connexion avec authentification mutuelle. Les autres APK souhaitent continuer la communication, sans avoir accès à la clef privée. Elle ne souhaite pas ouvrir une nouvelle communication, mais continuer avec le même socket.
Côté application qui ouvre le socket, il faut extraire le numéro du handle du socket. Nous avons la méthode cachée getFileDescriptor$() pour cela. Elle sert normalement à faire la liaison avec les IO asynchrones. La méthode getInt$() du FileDescriptor permet alors de récupérer la valeur du handle.
Socket socket=new Socket("10.0.2.2",8080);
FileDescriptor fd=(FileDescriptor)socket.getClass()
.getMethod("getFileDescriptor$").invoke(socket);
int fdi=(Integer)fd.getClass().getMethod("getInt$").invoke(fd);
Il faut alors construire un ParcelFileDescriptor à partir de ce handle.
ParcelFileDescriptor fd=ParcelFileDescriptor.adoptFd(fdi);
Ce dernier peut être retourné au processus appelant. La couche AIDL d'Android se charge d'effectuer un dup() sur le handle de fichier, lors de la transmission des données entre les processus (voilà à quoi sert de lire le code du framework d'Android ;-).
Pour cela, nous déclarons une interface AIDL.
package com.example.sharesocket;
import android.os.ParcelFileDescriptor;
interface ShareSocket
{
ParcelFileDescriptor openSocket();
}
Cette dernière a besoin d'indiquer que ParcelFileDescriptor doit être envoyé par valeur. Cela s'effectue en ajoutant un autre fichier AIDL dans le package android.os de votre projet.
package android.os;
parcelable ParcelFileDescriptor;
Et un petit service pour permettre le binding sur l'implémentation de l'AIDL :
public class ShareSocketService extends Service
{
@Override
public IBinder onBind(Intent intent)
{
return new ShareSocketImpl();
}
}
avec une implémentation de l'AIDL comme décrite ci-dessus.
Côté consommateur du socket ouvert par l'autre processus, il faut construire une instance PlainSocketmpl avec un FileDescriptor, puis le donner au constructeur du socket. Il reste à placer le drapeau isConnected à true et le socket est utilisable. Le code théorique est celui-ci :
int fdi=shareSocket.openSocket().getFd();
FileDescriptor fd=new FileDescriptor();
fd.descriptor=fdi;
Socket socket=new Socket(new java.net.PlainSocketImpl(fd));
socket.isConnected=true;
Comme les méthodes ne sont pas toutes accessibles, nous utilisons l'introspection.
ParcelFileDescriptor pfd=shareSocket.openSocket();
int fdi=pfd.getFd();
FileDescriptor fd=new FileDescriptor();
Field field;
field=fd.getClass().getDeclaredField("descriptor");
field.setAccessible(true);
field.set(fd, fdi);
Class<?> clPlainSocketImpl=Class.forName("java.net.PlainSocketImpl");
Object psimpl=clPlainSocketImpl.getConstructor(FileDescriptor.class).newInstance(fd);
constructor=Socket.class.getDeclaredConstructor(SocketImpl.class);
constructor.setAccessible(true);
Socket socket=(Socket)constructor.newInstance(psimpl);
field=Socket.class.getDeclaredField("isConnected");
field.setAccessible(true);
field.setBoolean(socket, true);
Nous pouvons alors utiliser le socket comme s'il avait été ouvert par notre processus ! (Ninja j'ai dis)
PrintWriter out=new PrintWriter(socket.getOutputStream());
BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream()));
out.println("GET / HTTP/1.0\n\n");
out.flush();
String line=in.readLine();
System.out.println(line);
Pour les connexions TLS, c'est plus complexe, car il faut de plus, récupérer les clefs symétriques négociées, pour pouvoir continuer à exploiter le flux et reconstruire toute la grappe d'objets pour reprendre une connexion sécurisée.
De plus, rien ne garantit la persistance des méthodes suivant les différentes versions de l'OS. Nous laissons le soin au lecteur de continuer ces travaux.
9. Enrôlement
Nous savons maintenant comment utiliser un certificat numérique, comment le protéger et comment l'installer. Il nous manque à concevoir un mécanisme permettant de créer et télécharger en toute sécurité le certificat signé sur le téléphone. L'approche par carte SD n'est vraiment pas conviviale.
Il faut faire très attention à nos scénarios, car un pirate peut les exploiter pour obtenir un certificat indu. Suivant les exigences, nous devons imaginer des scénarios différents. Par exemple, le même certificat peut être installé sur différents terminaux. Au contraire, un certificat par terminal peut être généré localement, puis signé par le certificat de l'utilisateur. La chaîne de certification permet alors d'identifier le terminal d'un utilisateur. Il est alors plus facile de révoquer le certificat du terminal, sans révoquer le certificat de l'utilisateur. Cela évite également de sauver sur le terminal, le certificat de l'utilisateur. Il est récupéré en mémoire, juste le temps d'effectuer la signature du certificat du terminal. Une approche plus simple consiste à créer un certificat dans le terminal et à demander à une autorité de certification de le signer.
Verisign a développé pour CISCO, le projet Simple Certificate Enrollment Protocol[14] (SCEP). C'est un projet qui est maintenant abandonné, mais il est possible de trouver des implémentations en différents langages. La couche transport est HTTP. Des messages sécurisés sont échangés pour permettre la récupération de certificats client ou de les renouveler. Un serveur SCEP peut être une autorité de certification (CA) ou une autorité d’enrôlement, qui délègue la signature à la CA. Les certificats demandés peuvent être signés immédiatement ou être mis en attente pour une validation humaine.
Pour initier une première demande de certificat, le client doit connaître un mot de passe spécifique, éventuellement à usage unique. Ensuite, il peut exploiter son certificat courant pour le renouveler.
SCEP est proposé par Microsoft depuis 2003. Apple l'utilise[15] dans iOS 4 avec un mot de passe dérivé des paramètres du terminal. Comme le mot de passe est alors statique, les serveurs SCEP doivent être paramétrés pour autoriser le recyclage du mot de passe initial. Cela fragilise[16] l'architecture globale.
Cette approche présente une faiblesse, car la connaissance de ce mot de passe permet d'obtenir un certificat valide. Nous allons proposer une approche pour combler cette lacune.
Nous allons nous placer dans un cas simple, où nous voulons installer un certificat pour un utilisateur identifié, sur un téléphone identifié avec une puce GSM identifiée. Nous ne voulons pas demander à l'utilisateur de saisir un mot de passe. C'est particulièrement pénible sur un mobile. Seule la saisie de son identifiant sera nécessaire pour obtenir un certificat valide.
Pour valider l’installation d’un certificat sur un terminal, il est nécessaire d’utiliser un deuxième canal de communication. Ce dernier permet de faire transiter une information complémentaire et nécessaire à l’installation. Ce dernier peut être un e-mail, un SMS, un scan de QR Code, etc. Il permet d’identifier l’utilisateur et/ou le terminal. Ainsi, un pirate doit être capable d’attaquer les deux canaux afin d’obtenir un certificat valide.
Le canal complémentaire sélectionné est la connexion 3G. L’utilisateur capable de recevoir un SMS particulier est autorisé à installer un certificat.
Pour associer le certificat au terminal, le code IMEI, unique à chaque téléphone est également nécessaire à la validation du certificat. En ayant connaissance de l’IMEI d’un terminal, il est toutefois possible d’effectuer la procédure d’enrôlement sur un autre terminal. Il faut néanmoins posséder la puce téléphonique correspondante (Figure 11).
Avant l’enrôlement par l’utilisateur, l’administrateur :
- Enregistre ou importe l’association IMEI du terminal avec le numéro de téléphone mobile correspondant dans le serveur SCEP. Cela permet d’identifier le parc de matériel de l'entreprise, habilité à demander un certificat numérique.
- Enregistre l'identité de l’utilisateur avec le couple IMEI, numéro de téléphone.
Puis, l’utilisateur :
- Installe l’application et la démarre ;
- L’application demande l’identifiant de l’utilisateur ;
- L’application interroge le serveur SCEP avec une requête hors protocole contenant le nom de l'utilisateur. Une requête Certificat Enrollement avec un challengePassword vide peut faire l'affaire ;
- Le serveur vérifie la validité de la demande (est-ce que l'utilisateur est connu et habilité à demander un certificat ?) ;
- Le serveur SCEP génère alors un challengePassword aléatoire et l'envoie par SMS à l'utilisateur (si possible, sur un port SMS différent de zéro) ;
- Le serveur SCEP ajoute l'IMEI du téléphone au mot de passe et l'enregistre dans la base de données du SCEP, avec les mots de passe valides ;
- Le client attend le SMS technique avec le mot de passe à usage unique ;
- Le client génère un certificat et formate une demande de signature au serveur SCEP ;
- Le client fourni alors un mot de passe composé du token récupéré par SMS, accolé à l'IMEI du périphérique comme identifiant ;
- Le serveur SCEP vérifie que la demande est valide (validation du mot de passe) ;
- Le serveur signe le certificat du client ;
- Le serveur enregistre le numéro du certificat dans sa base de données pour pouvoir le révoquer ;
- Le serveur retourne le certificat signé à l’application ;
- Une version non chiffrée du certificat est enregistrée dans le téléphone dans un conteneur, autant que possible sécurisé ; si le certificat est installé dans le conteneur officiel d'Android 16, l'utilisateur doit saisir un mot de passe complémentaire. En effet, il n'est pas possible à ce jour d'installer un certificat client non chiffré. Un ticket est ouvert pour cela.
- L’application peut maintenant exploiter le certificat.
Ce scénario présente quelques avantages. Le certificat n'est pas connu par le serveur, car la clef privée reste dans le téléphone ; l'enrôlement est automatique, après la saisie de l'identifiant de l'utilisateur ; le certificat est associé au terminal et non à l'utilisateur ; cela garantit que le certificat est installé via une puce GSM connu de l'entreprise, sur un téléphone identifié.
Néanmoins, l’utilisateur associé à l’IMEI du téléphone ainsi que le numéro de téléphone doit être enregistré avant l’enrôlement. Cela n'est pas toujours possible ; il peut exister des terminaux fantômes, préparés à l’enrôlement, mais jamais enrôlés ; le serveur doit avoir la capacité à envoyer rapidement un SMS ; le vol de la puce permet de récupérer le certificat sur un autre téléphone, en modifiant la valeur de l’IMEI à condition de la connaître ; l’installation d’un cheval de Troie sur le téléphone peut capturer le SMS et l’envoyer vers le pirate. Il peut alors récupérer le certificat. Le niveau de priorité de traitement du SMS peut limiter cette attaque ; cela ne fonctionne qu’avec des terminaux pouvant recevoir des SMS et impose la signature du certificat par le serveur, qui doit donc avoir accès à la clef privée de l’autorité.
Suivant les besoins, d'autres scénarios peuvent être imaginés, utilisant un QRCode présenté sur la console de l'administrateur du parc, une puce NFC, etc.
10. Renouvellement
Tant que le certificat courant est valide, il est facile d'ouvrir une communication authentifiée pour demander un nouveau certificat. Cela peut s'effectuer en tâche de fond, le dernier mois de validité du certificat. Ensuite, deux possibilités : soit on laisse l'ancien certificat finir sa vie tranquillement, pour ne pas interrompre les communications en cours, soit l'ancien certificat est révoqué dans 24 heures. Cette dernière approche est plus lourde, car tous les certificats seront un jour révoqués.
Les requêtes SCEP permettent cela, en exploitant le certificat valide précédant.
11. Certificat Pinning
Les certificats numériques, c'est bien sympathique. Mais finalement, la sécurité dépend des règles réellement appliquées par les différentes autorités. Certaines se sont fait abuser et ont signé des vrais/faux certificats. Avec 650 CA, ce n'est pas étonnant. Certains gouvernements ont utilisé une de leurs CA pour générer des certificats pour Gmail et ainsi, pouvoir récupérer tous les flux en clair.
Pour améliorer cela, Google Chrome 13+ propose une nouvelle approche. L'idée est de maintenir une liste d'autorités ou de certificats valides pour chaque site. L'extension PKPE ( Public Key Pinning Extension for HTTP[17]) propose d'ajouter un en-tête Public-Key-Pin avec le hash des clefs publiques et une durée de vie. Cette information est gardée en cache par le navigateur lors de la première visite. Ainsi, plus tard, si un certificat valide est présenté, ne faisant pas partie de la liste, il est refusé. La première connexion est implicitement valide.
Android 4.2 gère maintenant cela via le keychain.
Au niveau applicatif, il est envisageable d'exploiter ces informations pour qualifier les connexions web. La librairie AndroidPinning[18] propose une implémentation.
Conclusion
Pour résumer, voici les différentes technologies disponibles et les différentes approches pour les utiliser.
Technologies de sauvegarde des secrets :
- Sauvegarde dans le contexte de l'application : Les données ne sont pas chiffrées. Elles sont vulnérables au vol du téléphone.
- SE dans la carte à puce : Inaccessible aux applications.
- SE dans le terminal : Inaccessible aux applications.
- Chiffrement du disque : Non obligatoire. Ne protège pas des vulnérabilités des applications ou du téléphone allumé.
- KeyStore : Conteneur sécurisé, mais non officiel. Il peut être modifié dans les prochaines versions d'Android. À ce jour, le meilleur endroit où sauver les secrets.
- KeyChain : Gestion officielle de gestion des certificats clients. Non disponible avant l'API 14. Nous proposons une librairie de compatibilité pour les versions comprises entre 7 et 14.
Architecture d'utilisation :
- Certificat en clair en mémoire : Exige le mot de passe à chaque rappel de l'application.
- Certificat client partagé, installé dans le périphérique et disponible pour toutes les applications qui en font la demande : Possible à partir de l'API 14, après accord de l'utilisateur. Demande le mot de passe du certificat uniquement lors de l'installation. Tous les flux utilisent une authentification mutuelle. Risque d'exploitation de la clef privée sélectionnée par erreur par l'utilisateur, par une application vulnérable ou malveillante.
- Certificat client partagé par des applications de confiance. Application qui expose la clef privée via une interface AIDL : Ne demande pas forcément le mot de passe du certificat qui peut être importé dans l'application par tous moyens. Nécessite une application complémentaire pour protéger le certificat. Les autres applications consomment le certificat si elles ont le privilège. Tous les flux utilisent une authentification mutuelle. Risque d'exploitation d'une vulnérabilité des applications habilitées à utiliser le certificat.
- Certificat client non partagé. Le certificat est optionnel côté serveur (authClient="want"). Ouverture d'un socket avec authentification mutuelle, puis de sockets avec cookies par les autres applications. Approche similaire à OAuth : Ne demande pas forcément le mot de passe du certificat qui peut être importé dans l'application par tous moyens. Limitation du risque à la seule application qui possède l’accès à la clef privée. Exploitation par des applications de confiance si elles ont le privilège. Une partie des flux utilise une authentification mutuelle.
- Certificat client non partagé. Une application ouvre un socket avec authentification mutuelle, puis le duplique vers les applications consommatrices : Limitation du risque à la seule application qui possède l'accès à la clef privée. Exploitation par des applications de confiance si elles ont le privilège. Tous les flux utilisent une authentification mutuelle. Très difficile à implémenter sur tous les modèles de téléphones.
Nous comprenons maintenant pourquoi il est dangereux d'avoir un téléphone rooté, débloqué ou avec le mode debug activé. Des logiciels sont en effet capables de dumper toute la mémoire[19]. C'est pour cela que la version 4.2.2 d'Android impose maintenant une association entre le PC de déverminage et le terminal. Il n'est plus possible de brancher le téléphone sur un port USB pour y avoir accès.
Il y a des vulnérabilités avec les versions utilisant les processeurs Exynos 4 équipant les Samsungs[20] permettant d’avoir un accès complet à la mémoire, corrigé depuis par les fournisseurs. Une autre vulnérabilité pour ICS et JB a été découverte[21]. Il est également possible de découvrir le code de déverminage en détectant les mouvements subtils du téléphone lors de la saisie, par analyse via la caméra et/ou le gyroscope[22].
Nous voici bien équipés pour proposer des applications Intranet à l'ensemble des commerciaux ou autres employés en mobilité. Pour les autres plate-formes comme iOS ou Windows Phone 8, il faudra étudier comment protéger le certificat client correctement.
Si cela vous semble trop complexe ou subtil, je me ferais un plaisir de venir vous aider ;-)
Références
[1] http://www.musclecard.com/
[2] http://code.google.com/p/seek-for-android/wiki/Devices
[3] http://fr.wikipedia.org/wiki/Commandes_Hayes
[4] http://code.google.com/p/seek-for-android/wiki/UICCSupport
[5] http://www.3gpp.org/ftp/Specs/html-info/27007.htm
[6] http://en.wikipedia.org/wiki/Single_Wire_Protocol
[7] http://www.mastercard.us/paypass.html#/home/
[8] http://en.wikipedia.org/wiki/Trusted_service_manager
[10] http://forensics.spreitzenbarth.de/2013/02/14/cracking-androids-full-disk-encryption/
[11] https://play.google.com/store/apps/details?id=org.nick.cryptfs.passwdmanager
[12] https://github.com/kghost/ics-openvpn
[13] http://forensics.spreitzenbarth.de/2012/02/28/cracking-pin-and-password-locks-on-android/
[14] http://tools.ietf.org/html/draft-nourse-scep-23
[16] http://www.css-security.com/wp-content/uploads/2012/05/SCEP-and-Untrusted-Devices.pdf
[17] http://tools.ietf.org/html/draft-ietf-websec-key-pinning-04
[18] https://github.com/moxie0/AndroidPinning/
[19] https://www.google.fr/webhp?q=live%20forensics%20android
[20] http://forum.xda-developers.com/showthread.php?t=2057818
[21] http://forum.xda-developers.com/showpost.php?p=31545627
[22] https://en.wikipedia.org/wiki/Smudge_attack