Article écrit par Martin Catty
La méthode d’authentification la plus courante sur le web n’a pas changée depuis des années, elle nécessite un login et un mot de passe.
Si les applications web se doivent de respecter certains critères de sécurité, il en va de même pour celui ou celle qui se situe entre le clavier et la chaise.
Si vous êtes un utilisateur aguerri, il y a des chances que vous utilisiez un gestionnaire de mot de passe tel que 1Password, Bitwarden ou LastPass.
Dans le cas contraire il y a de fortes chances que vous utilisiez un même mot de passe sur différents services, aussi sensibles soient-ils.
En sachant que tous les sites / applications proposent une fonctionnalité de type «Mot de passe oublié» je vous laisse imaginer les dégâts si quelqu’un venait à accéder à votre boite mail.
Comment faire mieux que le bon vieux couple login / mot de passe ?
Progressivement, des solutions ont été imaginées pour renforcer la sécurité pour les utilisateurs finaux sans que celles-ci ne soient trop compliquées à utiliser.
On a vu l’avènement notamment de solutions 2FA ou MFA. La première désigne l’authentification à 2 facteurs, la seconde en multi facteurs (au moins 2).
Pour accéder au service concerné il faudra donc en plus du mot de passe un complément, comme un mot de passe temporaire reçu par SMS ou un jeton.
Les gestionnaires comme 1Password savent gérer ces jetons (OTP).
Mais cela décale finalement le problème quand vous utilisez le même outil pour stocker à la fois le mot de passe et le générateur de jetons. En effet votre coffre est lui-même protégé par un… mot de passe.
Quelqu’un qui disposerait de votre mot de passe pour un service donné ne pourrait pas s’y connecter car incapable de produire un facteur d’authentification supplémentaire.
Si cet usurpateur dispose par contre du mot de passe de votre coffre il accédera à l’ensemble de vos comptes, encore plus si votre gestionnaire est en charge des OTP.
S’appuyer sur ce qui marche : le chiffrement asymétrique
On a plusieurs exemples d’outils qui s’appuient sur un chiffrement asymétrique : GPG ou SSH pour n’en citer que deux.
La logique est la suivante dans le cas de GPG : un message est encodé au moyen d’une clé (publique) et décodé au moyen d’une clé privée.
Si je veux envoyer un courriel à Tom, j’utilise sa clé publique et il pourra le déchiffrer au moyen de sa clé privée. Toute personne interceptant le message en cours de route ne pourra y voir qu’une suite de caractères incompréhensibles.
L’algorithme étant non réversible il n’est donc pas possible de deviner la partie privée depuis la partie publique.
Introduire ce mécanisme sur le web
Le W3C propose une spécification permettant de porter et utiliser ce mécanisme dans nos navigateurs : WebAuthn.
Elle a beau être en draft, le support des navigateurs est excellent.
WebAuthn s’appuie sur une architecture à 3 parties :
- le service auquel on souhaite accéder. Dans le jargon on appelle ça une relying party.
- le client accédant au service (on ne parle pas ici de l’utilisateur final mais du système qui va consommer le service, comme un navigateur par exemple)
- un mécanisme d’authentification déporté (une clé USB, un capteur Touch ID, un smartphone…). On parle ici d’authenticator.
Les mécanismes d’authentification déportés peuvent être multiples. Un service pourrait vous demander plusieurs mécanismes tiers pour y accéder mais la plupart du temps on peut en enregistrer plusieurs et utiliser l’un de ceux enregistrés.
Personnellement j’utilise une clé Yubikey comme système tiers, mais certains services proposent également Touch ID par exemple.
L’intérêt de ce mécanisme d’authentification est de gérer pour vous les paires « clé publique / clé privée » de façon transparente. Dans le cas d’une clé c’est elle qui est en charge du stockage sécurisé. Touch ID fonctionne quant à lui grâce à une enclave sécurisée qui dispose de son propre logiciel dédié, il fonctionne en dehors du système d’exploitation d’Apple.
On peut voir un exemple d’usage sur le site de démo de yubico. Après avoir enregistré le ou les mécanismes tiers dans son compte on peut s’en servir lors de la connexion.
Ici avec le capteur Touch ID :
Puis avec la clé Yubikey (après l’avoir enregistrée) :
Comment ça marche ?
Le service qui souhaite permettre une authentification avec WebAuthn doit dans un premier temps permettre à l’utilisateur disposant d’un compte d’enregistrer ce mécanisme, afin qu’il soit lié à son compte utilisateur.
Il va l’indiquer au client (navigateur) qui va être en charge de demander au mécanisme d’authentification (authenticator) la génération d’un nouveau couple de clé publique / privée.
Si tout se passe bien, la clé publique sera transmise jusqu’au service qui l’associera au compte.
Pour l’authentification au service, le client transférera les informations au mécanisme d’authentification en précisant de qui vient la requête.
Le mécanisme d’authentification vérifiera la présence de l’utilisateur, par exemple en lui demandant d’appuyer sur le bouton dans le cas d’une clé Yubikey ou en faisant une vérification biométrique dans le cas de Touch ID.
Une fois la vérification effectuée, le mécanisme d’authentification signera les données utiles à l’aide de la clé privée, identifiée par l’identifiant envoyé par le service, et renverra l’assertion au client.
Le client jouera une nouvelle fois le rôle de passe-plat vers le service qui vérifiera la signature obtenue en réponse au défi.
Pour faciliter l’implémentation, les navigateurs proposent des API permettant de gérer la génération de ces couples de clés et plus généralement des mots de passe.
const credentials = new PasswordCredential({ type: "password", id: "fuse", password: "hardc0re" }); navigator.credentials.store(credentials);
Ouvrira cette boite de dialogue bien connue :
Le code suivant permet lui de simuler la création d’un défi (challenge) permettant la création d’une clé publique.
// on crée une chaine de caractères aléatoires matérialisant le défi const challenge = new Uint8Array(32); window.crypto.getRandomValues(challenge); // l'identifiant ici viendrait du service, il ne contient pas // d'information personnelle mais permettra au service de faire le lien // entre cet identifiant unique et le compte utilisateur concerné // Ici j'ai utilisé un mécanisme très robuste à base d'encodage en base64 :) const user_id = "bWNhdHR5QHN5bmJpb3ouY29t"; const id = Uint8Array.from(window.atob(user_id), (c) => c.charCodeAt(0)); const publicKey = { challenge: challenge, rp: { name: "Mon super service.", id: "www.synbioz.com" }, user: { id: id, name: "mcatty@synbioz.com", displayName: "Martin Catty" }, // pubKeyCredParams permet de spécifier les mécanismes de chiffrement // autorisés. -7 pour SHA-256, -37 pour RSA, -257 pour RS256. pubKeyCredParams: [ { type: "public-key", alg: -7 }, { type: "public-key", alg: -37 } ] }; navigator.credentials .create({ publicKey: publicKey }) .then((credentials) => { console.log(credentials); }) .catch((error) => { console.log(error); });
Ce qui nous donne en retour :
PublicKeyCredential { rawid: ArrayBuffer(64), response: AuthenticatorAttestation Response, id: "GhXH3mKw7ayi-my Vorz LUOTDOEE6FqRc7NWCqRGENDgvDm-JpwP900E60YVDABRgyXwyptoMQfiPc9qc@yzuo", type: "public-key" } id: "GhXH3mKw7ayi-myVQrz LUẬTDOEE6FqRc7NWCqRGENDgvDm-JpwPgQQE6 ØYVDABRgykXwyptoMQfipc9qc@yZUQ" rawId: ArrayBuffer(64) response: AuthenticatorAttestationResponse {attestationObject: ArrayBuffer(226), clientDataJSON: ArrayBuffer(139) } type: "public-key" [[Prototype]]: PublicKeyCredential
Le client va transférer «l’attestation» au service qui sera en charge de vérifier son contenu (qui est chiffré) et si tout est OK il enregistrera la clé publique associée à l’utilisateur concerné.
La prochaine fois, pour permettre l’authentification de l’utilisateur concerné, le service fournira des identifiants de mécanisme d’authentification autorisés (comme vu plus haut il peut y en avoir plusieurs) et lancera un nouveau défi.
Le processus derrière sera globalement le même que celui de l’enregistrement du mécanisme d’authentification.
// l'identifiant de l'attestation récupéré à la création de la clé publique const credential_id = "kt-fb-x1WsKgTe2TW8tOtkIerPhIOVormIM_PXoAc70Uvdb92Vg8UIv3gg_Crvn6WfIQnNNB6gSr-rnpiLUiuw"; const public_key = { challenge: challenge, allowCredentials: [{ type: "public-key", id: new Uint8Array(credential_id) }] }; navigator.credentials .get({ publicKey: public_key }) .then((response) => { console.log(response); }) .catch((error) => { console.log(error); });
Conclusion
On le voit, les API à disposition sont matures et permettent déjà d’utiliser des mécanismes 2FA.
De plus en plus de sites ou applications le proposent et il est temps de le généraliser, notamment car la législation en place devient de plus en plus contraignante (on peut citer par exemple la DSP2).
Dans un prochain article nous discuterons de la possibilité d’utiliser des identifiants déportés sur un système tiers via l’API FederatedCredentials.