https://lafor.ge/feed.xml

En route vers Biscuit (Partie 1)

2022-08-25

Bonjour à toutes et tous 😀

Pour ceux qui suivent ce blog, vous savez que j’aime expliquer des trucs. 😅

J’ai dĂ©couvert un nouveau jouet qui me permet d’aborder plein de sujets, comme la cryptographie asymĂ©trique, les chaĂźnes de confiance et les langages logiques.

Aujourd’hui, je vous propose ainsi de dĂ©couvrir un nouvel outil appelĂ© Biscuit. đŸ„ 

Il s’agit d’un token d’autorisation et d’authentification pouvant ĂȘtre attĂ©nuĂ© sans avoir besoin de secret partagĂ©.

Si ce que j’ai Ă©crit juste au-dessus est de l’hĂ©breu pour vous, l’article qui va suivre va tenter de vous donner toutes les bases pour ĂȘtre Ă  mĂȘme de la comprendre.

Si vous voulez directement entrer dans le vif du sujet, un second article sera disponible, ici.

Pour ceux/celles qui sont resté(e)s, je vous propose une rétrospective de la gestion des données utilisateur éphémÚres.

On parlera d’abord, du concept des sessions et on exposera leur limites.

Puis, nous analyserons les tokens d’authentification sans Ă©tat.

Bon, c’est parti pour un trùs, trùs, long voyage. 😁

Authentification avec Ă©tat

Lorsqu’il est venu Ă  l’idĂ©e de personnaliser le contenu des utilisateurs. Il a fallu ĂȘtre capable de savoir qui Ă©tait en train de visiter le site, pour pouvoir lui afficher le contenu qui lui correspond.

Une autre volontĂ© Ă©tait de permettre Ă  l’utilisateur de possĂ©der un compte sur le site. Cela permettant Ă  l’utilisateur de s’engager sur les contenus, et de pouvoir plus facilement le connaĂźtre, lui et ses habitudes. (Oui le cĂŽtĂ© Sombre de la Force est partout)

Mais qui dit compte utilisateur, dit authentification. Qui dit authentification, dit mire de login.

Pour la premiĂšre connexion de sa session de visite, il ne verra pas d’inconvĂ©nient Ă  se connecter avec son mot de passe. Mais s’il doit le faire Ă  chaque fois, cela risque d’ĂȘtre plutĂŽt agaçant pour lui et nous risquons potentiellement de ne jamais le revoir.

C’est pour cette raison que deux concepts ont vu le jour, ceci travaillant de pair :

  • les cookies
  • les sessions

Sessions

L’idĂ©e est simple pour savegarder les donnĂ©es d’un utilisateur, cĂŽtĂ© serveur, nous lui ouvrons une session. Il s’agit soit d’un emplacement dans la mĂ©moire du serveur, soit d’un fichier qui va contenir toutes les informations nĂ©cessaires.

Par exemple lorsque l’utilisateur est bien connectĂ© et qu’il n’est pas nĂ©cessaire de lui redemander son mot de passe d’une page Ă  l’autre.

Nous pouvons reprĂ©senter nos sessions sous la forme d’un tableau ayant pour clef les ID de session et comme valeur les donnĂ©es que l’on veut stocker dans la session.

schéma représentant des sessions sur le serveur

CĂŽtĂ© client, nous allons Ă©galement sauvegarder des informations sur le navigateur de l’utilisateur, il s’agit d’un petit fichier qui se nomme un cookie.

un cookie informatique

Ce cookie peut contenir des informations.

un cookie informatique avec des données

 

Information supplémentaire

Si vous voulez plus de dĂ©tails sur ce concept je vous conseille la vidĂ©o d’Hubert SablonniĂšre sur le sujet.

Si c’est la premiùre visite, l’on demande à l’utilisateur de se connecter.

session inexistante et cookie innexistant

Si l’authentification est un succĂšs. Nous crĂ©ons une session sur le serveur et l’on vient gĂ©nĂ©rer un cookie avec l’ID de session nouvellement crĂ©Ă©.

création du cookie et de la session

Ce cookie est alors transmis Ă  l’utilisateur par retour de requĂȘte.

Lorsque l’utilisateur rĂ©alise une requĂȘte vers une ressource privilĂ©giĂ©e sur le serveur, l’on vient lire l’ID de cette session, si celle-ci existe et est valide, on laisse passer l’utilisateur, sinon on lui demande de s’authentifier.

requĂȘte avec cookie

Ainsi, nous avons rĂ©glĂ© le problĂšme, merci au revoir ! 😛

Sauf que


Scissions de sessions


 Non ❌

Dans une infrastructure mĂȘme vĂ©tuste, lorsque le trafic augmente, une seule machine ne suffit pas, on entre alors dans le monde merveilleux de l’équilibrage de charge. đŸ€©

Sur le papier cela semble simple, on met une machine qui sert d’agent de la circulation et qui rĂ©partit les requĂȘtes sur 2 ou plus serveurs.

Cet agent de circulation est appelé un load balancer.

load balancer

C’est Ă  ce moment prĂ©cis que les choses se gĂątent. Rappelez-vous les sessions sont des fichiers sur le serveur ou des cases dans sa mĂ©moire. Ce qui signifie que ce qui se passe sur le serveur 1 est totalement inconnu au serveur 2 et vice-versa.

Lorsque la premiĂšre requĂȘte arrive, pas de problĂšme. On est dans l’état prĂ©cĂ©dent, la requĂȘte va ĂȘtre routĂ©e vers le serveur 1 par exemple, une session va ĂȘtre crĂ©Ă©e et un cookie dĂ©posĂ©.

load balancer redirige vers serveur 1

Et maintenant Ă  50/50, la prochaine requĂȘte arrivera soit sur le serveur 1, soit sur le serveur 2.

Si c’est le serveur 1, pas de problĂšme, l’utilisateur prĂ©cĂ©demment connectĂ©, le reste.

Par contre, si c’est le serveur 2 qui reçoit la requĂȘte, il va lire le cookie, voir que la session n’existe pas. Demander Ă  l’utilisateur de se connecter.

load balancer redirige vers serveur 2

Si celui-ci s’exĂ©cute, une nouvelle session est construite sur le serveur 2, et le cookie est rĂ©Ă©crit avec le numĂ©ro de session sur le serveur 2.

session créé sur le serveur 2

La situation est dĂ©sormais inverse. Si la requĂȘte tombe sur le serveur 1, celui-ci dĂ©connectera l’utilisateur et ainsi de suite


session créé sur le serveur 2

L’utilisateur a Ă  prĂ©sent 50% de chance de se faire dĂ©connecter Ă  chaque requĂȘte ! đŸ˜«

Bon il faut faire quelque chose, on ne peut pas laisser la situation ainsi.

Pour cela nous allons partager les sessions.

Sessions partagées

L’idĂ©e est de remplacer des sessions stockĂ©es localement par des sessions sur le rĂ©seau.

Cela peut ĂȘtre, soit via un montage NFS (ce n’est pas bien, ne faites pas ça 😛) soit via une base de donnĂ©es qui va contenir nos diffĂ©rentes sessions.

La base la plus utilisĂ©e pour ce genre d’usage est Redis, mais il en existe plein d’autres.

Les diffĂ©rents serveurs sont alors connectĂ©s Ă  cette base de donnĂ©es et l’interrogent pour rĂ©cupĂ©rer la session indexĂ©e par l’ID contenu dans le cookie de requĂȘte.

sessions partagées

Et maintenant grĂące Ă  ces sessions dĂ©portĂ©es, on peut augmenter ou diminuer le nombre de serveurs, sans jamais risquer de dĂ©connecter un utilisateur. 😎

En Bonus

Redis est fait pour travailler en cluster, ce qui signifie que si votre nombre de serveurs explose, vous pouvez rajouter des nƓuds sur votre cluster Redis pour accueillir la charge supplĂ©mentaire de requĂȘtes de sessions.

Le souci de cela, c’est la complexitĂ© de l’infrastructure, on a une collection de frontaux, une collection de serveurs et un cluster de sessions.

Ce serait intĂ©ressant de pouvoir de se passer d’état tout court. C’est ce que l’on va voir tout de suite. 😀

Authentification sans Ă©tat

Si vous vous rappelez, les cookies sont des fichiers locaux au navigateur de l’utilisateur et gĂ©nĂ©rĂ©s par le serveur.

Pourquoi ne pas utiliser ce concept pour y stocker la session de l’utilisateur ? đŸ˜ș

Reprenons notre situation de tout Ă  l’heure, l’utilisateur se connecte Ă  un serveur, le serveur vĂ©rifie les cookies qui lui sont fournis.

S’il n’y a pas de cookies ou que le cookie n’a pas d’information valide. Le serveur gĂ©nĂšre un cookie.

Au lieu d’y stocker seulement l’ID de la session, nous allons y coller toute la session prĂ©cĂ©demment stockĂ©e soit sur le serveur soit sur la BDD de sessions.

un cookie avec des données de session

Lorsque l’utilisateur revient avec son cookie, le serveur vient lire le cookie et y rĂ©cupĂšre la session de l’utilisateur.

Ici notre session dĂ©finit les droits de l’utilisateur, ainsi que son user ID.

cas d'une connexion légitime

Comme la session est stockĂ©e dans le cookie et non sur un serveur en particulier, le cookie peut alors ĂȘtre traitĂ© par n’importe quel serveur.

On en revient donc Ă  la situation de la session partagĂ©e. 😀

Sauf que là, encore
 Non ❌

Je vous prĂ©sente Jaba, Jaba est un pirate, son plaisir dans la vie, c’est de devenir administrateur sur une plateforme.

un pirate voulant devenir admin

Les cookies sont, je le rappelle, des fichiers locaux au navigateur, Jaba n’a pas besoin d’avoir accùs au serveur.

Et il ne va pas se priver de le faire. 😈

cas d'une connexion illégitime

Le voici administrateur !! 😭

Nous allons devoir mettre des bĂątons dans les roues de Jaba !

Signatures Numériques

Pour cela nous allons introduire un concept supplémentaire, celui-ci se nomme la signature numérique. Elle consiste à valider les données qui transitent. En clair, seul le serveur doit avoir la capacité de le faire.

Il existe deux maniÚres de signer des données

  • la signature par secret
  • la signature par clef asymĂ©trique

Signature par secret

Voici un secret, il s’agit d’un grand nombre qui doit comme son nom l’indique, doit rester
 eh bien
 secret. 🔐

un secret

On peut utiliser ce secret pour venir signer nos données.

signature par secret

Puis lorsque l’on a le besoin, nous pouvons vĂ©rifier avec ce mĂȘme secret la validitĂ© des donnĂ©es.

signature validée

Si les données sont altérées, alors la signature ne correspondra pas et la vérification lÚvera une erreur.

signature invalide car données altérée

Mais, si la signature et les donnĂ©es sont modifiables par l’utilisateur et qu’il fournit une signature compatible avec les donnĂ©es

Comment fait-on pour ĂȘtre certain que les donnĂ©es ne sont pas altĂ©rĂ©es ?

Si la signature ne correspond pas avec le secret, la punition sera la mĂȘme.

signature invalide

Ces deux mĂ©canismes imposent que seul le serveur est Ă  mĂȘme de pouvoir modifier les donnĂ©es, ou en tout cas si celle-ci ou la signature est altĂ©rĂ©e. Le serveur s’en rendra compte tout de suite.

Code python
import hashlib, hmac, binascii

def hmac_sha256(secret, message):
  return binascii.hexlify(hmac.new(secret, message, hashlib.sha256).digest())

def sign_message(secret, message):
    signature = hmac_sha256(secret, message)
    return {'message': message, 'signature': signature}

def verify(message, secret):
    computed_signature = hmac_sha256(secret, message['message'])
    if hmac_sha256(secret, computed_signature) == hmac_sha256(secret, message['signature']):
        print("La signature est valide")
    else:
        print("La signature n'est pas valide")

# On génÚre des secret
secret = b"12345"
pirate_secret = b"6789"

# On génÚre des messages à signer
message = b"Mon message"
pirate_message = bytes("Message altéré", encoding="utf-8")

# La signature corespond au message
genuine_message = sign_message(secret, message)
# On altĂšre le message
altered_message = genuine_message.copy()
altered_message['message'] = pirate_message
# On altĂšre la signature
altered_signature = sign_message(pirate_secret, pirate_message)

verify(genuine_message, secret)
verify(altered_message, secret)
verify(altered_signature, secret)

Le code est disponible ici.

Celui-ci renvoie.

La signature est valide
La signature n'est pas valide
La signature n'est pas valide

On voit bien ici que seul une correspondance du triplet (secret, données non falsifiées, signature valide), permet au systÚme de valider les données reçues.

Le souci est que pour vérifier, il faut obligatoirement partager le secret.

C’est pour cette raison qu’un autre type de signature a Ă©tĂ© mis en place, il s’agit des clefs asymĂ©triques.

Signature par clefs asymétriques

MĂȘme principe ici nous allons gĂ©nĂ©rer un secret. Celui-ci va porter un nom diffĂ©rent, il s’agit d’une clef privĂ©e.

clef privée

Contrairement au secret qui était relativement libre de choix, la clef privée respecte des rÚgles mathématiques.

Elle est donc généralement créée par un algorithme.

Cette clef privée a la capacité de générer un pendant, celui-ci se nomme clef publique.

dérivation de la clef publique

Les mathĂ©matiques nous certifient (jusqu’à preuve du contraire) qu’il est impossible de retrouver la clef privĂ©e en connaissant la clef publique.

Le couple de clefs privĂ©e/publique est appelĂ©e un Keypair en cryptographie, j’utiliserai en consĂ©quence ce terme dans la suite.

une paire de clef asymétrique

Le mĂ©canisme de signature reste le mĂȘme que dans le cadre d’un secret. Nous utilisons la clef privĂ©e pour signer nos donnĂ©es.

signature asymétrique

Par contre, la vérification est réalisée via la clef publique et non la clef privée.

vérification réussite

Ici de mĂȘme, si les donnĂ©es sont altĂ©rĂ©es

vérification échouées car les données sont altérées

La vérification échouera.

Ainsi que lorsque la signature n’a pas Ă©tĂ© gĂ©nĂ©rĂ©e par la clef privĂ©e d’origine.

vérification échouées car la signature est altérée

Voici un petit morceau de python pour vous expliquer le fonctionnement.

On installe, la dépendance :

pip install ed25519
Code python
import ed25519

def verify(public_key, signature, message):
  try:
    public_key.verify(signature, message, encoding='hex')
    print("La signature est valide.")
  except:
    print("La signature a été altérée")

# On génÚre la paire de clef
private_key, public_key = ed25519.create_keypair()
print("Clef privée originelle:", private_key.to_ascii(encoding='hex'))
print("Clef publique originelle", public_key.to_ascii(encoding='hex'))

## On génÚre également une paire de clef pirates
pirate_private_key, pirate_public_key = ed25519.create_keypair()
print("Clef privée pirate:", privKey.to_ascii(encoding='hex'))
print("Clef publique pirate", pubKey.to_ascii(encoding='hex'))

## On signe un message avec la clef privée originel 
message = b'Voici mon message'
signature = private_key.sign(message, encoding='hex')
print("Signature :", signature)

## On fait de mĂȘme mais avec une clef privĂ©e diffĂ©rente 
pirate_message = b'Message altéré'
pirate_signature = pirate_private_key.sign(pirate_message, encoding='hex')
print("Signature pirate :", pirate_signature)

### Cas nominal
verify(public_key, signature, message)

### Les données ont été altérées
verify(public_key, signature, pirate_message)

### Les données et la signature ont été altérées
verify(public_key, pirate_signature, pirate_message)

Le code ici.

La signature est valide.
La signature a été altérée.
La signature a été altérée.

On voit ici encore une fois que seul une correspondance du triplet (clef publique, données non falsifiées, signature valide), permet au systÚme de valider les données reçues.

Exemple d’utilisation

Prenons un exemple un peu plus concret qui va démontrer la plus grande flexibilité des clefs asymétriques par rapport aux secrets.

Imaginons que vous ayez une Entreprise A, celle-ci possĂšde les informations des utilisateurs, ainsi que leurs identifiants de connexion.

Une Entreprise B veut pouvoir accepter les utilisateurs de l’Entreprise A, les utilisateurs n’ont aucune envie de crĂ©er un compte dans l’Entreprise B.

L’Entreprise B a confiance dans les tokens qui sont dĂ©livrĂ©s par l’Entreprise A.

Elle souhaite pouvoir les vérifier.

On ne voudrait pas qu’un Jaba malĂ©fique falsifie le token, si ?
 😈

Par contre l’Entreprise A n’a aucune confiance en l’Entreprise B, en tout cas pas suffisamment pour lui laisser signer des tokens en se faisant passer pour l’Entreprise A.

Avec un secret ce ne serait pas possible, mais avec une paire asymĂ©trique, ce n’est pas un problĂšme.

L’Entreprise A peut en toute confiance partager sa clef publique avec l’Entreprise B, celle-ci ne pourra que vĂ©rifier des signatures avec.

Les choses se passent ainsi :

  1. L’utilisateur s’identifie sur Entreprise A
  2. Entreprise A vérifie les identifiants
  3. Entreprise A gĂ©nĂšre un token qu’il signe avec sa clef privĂ©e
  4. L’utilisateur reçoit le token signĂ© de l’Entreprise A.
  5. L’utilisateur transmet le token à l’Entreprise B
  6. L’Entreprise B vĂ©rifie le token avec la clef publique de l’Entreprise A
  7. Si le token est valide, l’utilisateur est connectĂ© sur l’Entreprise B
exemple d'utilisation des clefs asymétriques

L’avantage de cette technique par rapport au secret, est que la clef publique peut librement ĂȘtre envoyĂ©e dans la nature, car il est impossible de retrouver la clef privĂ©e Ă  partir de la clef publique et que seule la clef publique est nĂ©cessaire Ă  la vĂ©rification d’une signature. 😎

La clef privĂ©e n’a donc besoin d’ĂȘtre baladĂ©e qu’aux endroits nĂ©cessitant la signature.

Maintenant que vous avez les bases de la cryptographie, nous pouvons passer au token cryptographique.

Token

Le Token cryptographique est la rĂ©union de donnĂ©es et d’une signature numĂ©rique de celles-ci.

token cryptographique

L’idĂ©e est de pouvoir stocker des informations dans un cookie sans risque de les voir falsifier au retour de celui-ci.

exemple de token

Si le token demeure inchangé, la signature sera validée.

token non-falsifié

De ce fait, si Jaba a pour idĂ©e de modifier les informations de notre Token, alors le serveur s’en rendra compte immĂ©diatement, car les donnĂ©es ne correspondront plus avec la signature.

token falsifié

Si Jaba tente de modifier la signature, le serveur s’en rendra Ă©galement compte, parce que la signature ne correspondra pas au secret ou Ă  la clef privĂ©e qui a signĂ© le Token.

signature falsifiée

Et lĂ , Jaba est bien embĂȘtĂ©, il ne peut plus se faire passer pour un administrateur de la plateforme. 😛

Les Tokens c’est bien, le souci, c’est que ce n’est pas standardisĂ© et donc chacun doit mettre en place sa propre logique de vĂ©rification et de crĂ©ation des tokens.

JWT

Le JWT est une initiative de standardisation des tokens.

JWT est l’abrĂ©viation de JSON Web Token, comme son nom l’indique : il s’agit d’un JSON, mais comme son nom ne l’indique pas il contient par ailleurs une signature numĂ©rique.

Je ne traiterai que du JWS, si vous voulez en apprendre plus sur le JWE, cet article est parfait

Le JWT est composé de 3 parties :

  • un entĂȘte qui dĂ©finit quel type de signature est utilisĂ© (secret ou clefs asymĂ©triques) au format JSON
  • les donnĂ©es en elles-mĂȘmes au format JSON
  • la signature

Attention!

Lorsque vous crĂ©z des JWT, attention au choix de l’algorithme de signature, celui-ci vous est libre. Mais mal choisi, il peut occasionner des problĂšmes de sĂ©curitĂ©.

Ces différentes informations sont alors encodées en base64 séparé par des points.

Ce qui donne par exemple :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.          // en-tĂȘte
eyJ1c2VyIjoxLCJyaWdodCI6WyJyZWFkIl19.          // données
tqwenw0ShYJdia5zfeDiUvKNmD0mSf6pAkzHJ67-nFs    // signature

Le format des donnĂ©es est lui-mĂȘme normalisĂ©, les champs du JSON de donnĂ©es sont appelĂ©s des claims.

Il y a des champs standards qui permettent aux bibliothùques d’agir dans certaines circonstances :

Le champ exp par exemple permet de forcer l’expiration d’un JWT.

Il en existe plein d’autres, que vous pouvez retrouver ici.

En plus de ces champs normalisés, vous pouvez en rajouter autant que vous le voulez.

Un petit code d’exemple en utilisant un secret :

import jose
from jose import jwt
import base64
import json

def verify(token, secret):
    try:
        jwt.decode(token, secret, algorithms=['HS256'])
        print("Signature valide")
    except jose.exceptions.JWTError:
        print("Signature invalide")
        

def create_token(data, secret):
    return jwt.encode(data, secret, algorithm='HS256')


message = {
    "user" : 666,
    "rights": ["read"]
}

pirate_message = {
    "user" : 1,
    "rights": ["read", "admin"]
}

secret = "12345"
pirate_secret = "6789"

# On créé un token valide
token = create_token(message, secret)

# On altÚre les données sans modifier la signature
token_parts = token.split(".")
pirate_message_encoded = json.dumps(
                pirate_message,
                separators=(",", ":"),
            ).encode("utf-8")

# On modifie la signature pour la faire correspondre aux données
token_with_altered_signature = create_token(pirate_message, pirate_secret)


token_parts[1] = base64.urlsafe_b64encode(pirate_message_encoded).replace(b"=", b"").decode("utf-8") 
pirate_token = ".".join(token_parts)

verify(token, secret)
verify(pirate_token, secret)
verify(token_with_altered_signature, secret)

Ce code nous renvoie :

Signature valide
Signature invalide
Signature invalide

Le code est disponible ici.

Le souci du JWT est qu’il est impossible d’attĂ©nuer les droits.

Prenons l’exemple suivant, dans le JWT on indique l’ID de l’utilisateur, par dĂ©finition celui-ci peut alors permettre d’agir sur toutes les ressources appartenant Ă  cet utilisateur.

Si nous voulons dĂ©lĂ©guer des droits Ă  un service, mais ne restreindre qu’à un certain pĂ©rimĂštre de droits.

Exemple, uniquement en lecture sur les données qui se trouve dans le bucket films.

Comment peut-on attĂ©nuer les droits du JWT ?

Pour cela l’on doit rĂ©gĂ©nĂ©rer un autre JWT qui possĂšde cette attĂ©nuation de droits.

atténuation d'un jwt

GĂ©nĂ©ralement on met en place un systĂšme d’authentification centralisĂ© qui se charge de conserver les secrets et les clefs privĂ©es. Son rĂŽle est de vĂ©rifier les tokens ou de distribuer les clefs publiques.

validation d'un jwt

Lorsque le service 1 veut faire atténuer un JWT, il transmet le nouveau payload, et reçoit un nouveau JWT. Et le transmet au service 2.

demande de JWT

Le service 2, fait alors une nouvelle demande de vérification sur ce nouveau token.

vérification du JWT atténué

Ici j’ai reprĂ©sentĂ© le processus de vĂ©rification via un secret. Mais on aurait tout aussi bien pu le faire via une clef publique.

La vérification aurait été réalisée par Service 1, Service 2; et non le Service Auth.

Par contre, rien ne dit que ce token a vraiment “moins” de droits que l’autre.

Le JWT ne certifie qu’une seule chose : les donnĂ©es n’ont pas Ă©tĂ© falsifiĂ©es.

C’est pour cette raison qu’un nouveau systĂšme est entrĂ© dans la course.

Il s’agit du Macaron !

Macaron

Le Macaron est projet issu des laboratoires de Google.

Son idĂ©e est de crĂ©er une chaĂźne de confiance de blocs qui s’inter-vĂ©rifient comme le fait une chaĂźne de certificats SSL par exemple.

Un macaron est composé de champs normalisés

un macaron
  • location : est une simple chaĂźne, elle est stockĂ©e seulement Ă  titre indicatif
  • identifier : est un champ libre permettant de dĂ©finir quel secret a Ă©tĂ© utilisĂ© pour gĂ©nĂ©rer le macaron
  • cid : est un caveat, c’est l’équivalent des claims de JWT, il peut y en avoir de 0 Ă  plusieurs, il est fortement recommandĂ© d’en mettre au moins un sinon ce macaron est le joker ultime.
  • signature : la signature cryptographique du macaron, nous allons voir en dĂ©tails comment celle-ci est crĂ©Ă©e

Signature

Le macaron est également un token et donc signé.

signature d'un macaron

Étant donnĂ© que nous avons ici encore une signature numĂ©rique, Jaba ne pourra pas falsifier les caveats (les droits), donc de ce point de vue on est bons Ă©galement.

rejet d'un macaron falsifié

La mĂ©thode de validation d’un macaron est standardisĂ©e.

VĂ©rification des caveats

Pour chaque droit que l’on dĂ©sire valider, on vient faire un ”string exact” sur chaque caveat.

vérification d'un caveat

Si la signature et les caveats sont validés alors le macaron est validé.

Nous avons ici une mĂ©thode standardisĂ©e de vĂ©rification des droits en plus de la vĂ©rification de l’exactitude de ceux-ci.

Par contre, pour des droits qui nĂ©cessitent des opĂ©rations plus complexes qu’un “string equal”, il faut dĂ©velopper un parser qui vient analyser le caveat.

vérification d'un caveat plus complexe

Maintenant voyons comment les macarons respectent les deux conditions de gestion : chaĂźne de confiance d’attĂ©nuation de droits et de validation de ces chaĂźnes.

ChaĂźne de confiance

Le macaron a opté pour le choix du secret au lieu des clefs cryptographiques.

On rĂ©alise la signature via une fonction appelĂ©e HMAC. Elle certifie qu’il est impossible de remonter au secret depuis la signature.

C’est la mĂȘme qu’ici.

fonction HMAC

Par contre, cela signifie que le secret qui signe le macaron doit ĂȘtre distribuĂ© pour permettre la vĂ©rification du macaron.

On choisit un secret. De prĂ©fĂ©rence ce secret doit ĂȘtre unique au macaron.

On dĂ©finit aussi un identifier, celui-ci est un indice permettant de dĂ©terminer quel secret doit-ĂȘtre utilisĂ© lors de la vĂ©rification.

On réalise alors la signature de couple (secret, identifier)

signature initiale

Prenons un macaron avec les caveats suivants :

cid user = 666
cid time < 2022-09-01

On utilise le premier caveat ainsi que la signature 0 pour générer une signature 1.

signature caveat 1

Puis l’on fait de mĂȘme avec le deuxiĂšme caveat et la signature 1

signature caveat 9

On peut poursuivre ce mĂ©canisme de chaĂźne avec autant de caveats que l’on veut

signature caveat n

VĂ©rification de la chaĂźne de confiance

Pour vérifier le macaron, il est nécessaire de reconstituer la chaßne de signatures en partant du secret.

On vient successivement recalculer toutes les signatures en passant en revue chaque caveat.

La fonction HMAC Ă©tant stable, pour un secret donnĂ© et un caveat donnĂ©, la signature rĂ©sultante sera toujours la mĂȘme.

vérification de la chaßne de confiance

Si en bout de chaßne, la signature calculée est égale à la signature du macaron.

vérification de la chaßne de confiance

Alors le macaron, n’a pas Ă©tĂ© falsifiĂ©.

Lorsque le macaron est transmis à l’utilisateur, il lui est totalement impossible de modifier les caveats.

L’unique champ qui peut ĂȘtre considĂ©rĂ© comme provenant de l’AutoritĂ© de signature est l’identifier. En effet mĂȘme le premier caveat peut-ĂȘtre considĂ©rĂ© comme une attĂ©nuation.

Il est impossible de déterminer à postériori si le caveat est issu du macaron originel ou de son atténuation.

Car une Ă©volution du caveat imposerait de connaĂźtre le secret.

Simulons rapidement avec un peu de code.

Code python
import hashlib, hmac, binascii

def hmac_sha256(secret, message):
  return binascii.hexlify(hmac.new(secret, message, hashlib.sha256).digest())


class Macaron:
    def __init__(self, secret, identifier):
        self.caveats = []
        self.identifier = identifier
        self.signature = hmac_sha256(bytes(secret, encoding="utf-8"), bytes(identifier, encoding="utf-8"))

    def add_caveat(self, caveat):
        self.signature = hmac_sha256(self.signature, caveat.encode("utf-8"))
        self.caveats.append(caveat)

    def verify(self, secret):
        
        signature = hmac_sha256(bytes(secret, encoding="utf-8"), bytes(self.identifier, encoding="utf-8"))
        
        for caveat in self.caveats:
            signature = hmac_sha256(signature, caveat.encode("utf-8"))

        if self.signature == signature:
            print("Macaron valide")
        else:
            print("Macaron invalide")

    def __str__(self):
        data = f"Macaron\n \nidentifier: {self.identifier}\n---\ncaveats\n---\n"
        for caveat in self.caveats:
            data += f"cid {caveat}\n"
        data += f"\nsignature: {self.signature}\n\n###\n"
        return data

secret = "12345"

macaron = Macaron(secret, "simple")
print(macaron)
macaron.add_caveat("user = 666")
print(macaron)
macaron.add_caveat("time < 2022-09-01")
print(macaron)

Le code est disponible ici.

À chaque ajout d’un caveat

La signature Ă©volue

Code python
Macaron
 
identifier: simple
---
caveats
---

signature: b'07f6ce323d25ab7cd0af1527e1b850532eedb0f665ed48281c09542dc39b5627'

###

Macaron
 
identifier: simple
---
caveats
---
cid user = 666

signature: b'de18c7cc78ff83eaecbab893b0127d9dde26f3961828509f8e30af73627be838'

###

Macaron
 
identifier: simple
---
caveats
---
cid user = 666
cid time < 2022-09-01

signature: b'9f148339a6baca2bffade97f92eadbf7713a24e7ab7d2e2bd3d1332de28c9a8e'

###

Si l’on tente de falsifier un caveat, alors la signature calculĂ©e va diffĂ©rer.

macaron.verify(secret)

# on falsifie un caveat
macaron.caveats[0] = "user = 1"

macaron.verify(secret)

Ce macaron falsifié vaut :

Macaron
 
identifier: simple
---
caveats
---
cid user = 1
cid time < 2022-09-01

signature: b'9f148339a6baca2bffade97f92eadbf7713a24e7ab7d2e2bd3d1332de28c9a8e'

###

Bien que les signatures soient identiques. Le premier sera validé, mais pas le second.

Macaron valide
Macaron invalide

Or, une fois que le macaron est crĂ©Ă©, les seules informations que l’on connait de l’extĂ©rieur du macaron sont :

  • l’identifier
  • les caveats
  • la signature

Reprenons, notre bon Jaba qui tente désespérément de devenir admin.

falsification de macaron

De fait de la propriété de la fonction HMAC, il est impossible depuis la signature du macaron de revenir à la signature précédente qui a signé le dernier caveat du macaron.

hmac reverse caveat 2

Comme notre caveat concernant l’utilisateur est le premier, cela signifie que l’on doit remonter 2 fois la signature. Les probabilitĂ©s d’y arriver sont quasi nulles.

hmac reverse caveat 1

De mĂȘme que de reconstruire le macaron en connaissant seulement les caveats et pas le secret.

hmac secret

Un secret différent créera une signature différente.

Un macaron peut donc ĂȘtre considĂ©rĂ© comme immuable une fois crĂ©Ă©.

Il peut ainsi ĂȘtre partagĂ© avec un tiers qui peut lui-mĂȘme rajouter ses caveats, sans pour autant pouvoir supprimer ou modifier les caveats prĂ©cĂ©dents.

Voyons justement ce processus de rajout de caveats.

Atténuation

L’attĂ©nuation est l’ajout de nouveaux caveats dans le but de crĂ©er un macaron avec moins de droits (Ă  comprendre au minimum les caveats prĂ©cĂ©dents).

Reprenons l’exemple de tout Ă  l’heure : comment attĂ©nuer le macaron pour n’accepter que les appels sur le bucket films.

Il suffit de rajouter un caveat.

cid path begins_with /bucket/films/

Un peu de code pour tester tout cela.

On définit nos Verifiers.

Le premier fait une comparaison classique de chaĂźne de caractĂšres.

class StringEqual:
    def __init__(self, caveat):
        self.caveat = caveat
        
    def verify(self, caveat):
        return self.caveat == caveat

Le second vĂ©rifie qu’un certain pattern est respectĂ©, ici un ”starts_with”

class BeginWith:
    def __init__(self, path, pattern):
        self.pattern = pattern
        self.path = path
        
    def verify(self, caveat):
        regex = r"(.*)\s+begins_with\s+(.*)"
        matches = re.findall(regex, caveat)
        if matches is None:
            return False
        path = matches[0][0]
        pattern = matches[0][1]
        if path != self.path:
            return False

        return self.pattern.startswith(pattern)

Puis, on crée un objet Verifier qui prend un secret et le macaron en entrée :

class Verifier:
    def __init__(self, secret, macaron):
        self.secret = secret
        self.verifiers = []
        self.macaron = macaron

    def verify(self):
        
        for caveat in self.macaron.caveats:
            verified = False
            for verifier in self.verifiers:
                if verifier.verify(caveat):
                    verified = True
                    break
                else:
                    continue
            if not verified:
                print("Macaron invalide")
                return

        self.macaron.verify(self.secret)

Son rĂŽle va ĂȘtre d’itĂ©rer sur chaque caveat et de vĂ©rifier la signature.

On va par exemple dire que l’utilisateur 666 souhaite accĂ©der au film “Ratatouille” (trĂšs bien d’ailleurs, je le recommande ^^ ).

Nous allons vĂ©rifier que le caveat concernant le champ ”path” commence bien par ”/bucket/films/ratatouille”. On connait le chemin Ă©tant donnĂ© que c’est la ressource qui tente d’ĂȘtre accĂ©dĂ©e.

Le code est disponible ici.

Testons sans atténuation :

# le macaron provient de la requĂȘte
macaron = Macaron(secret, "simple")
macaron.add_caveat("user = 666")

verifier_success = Verifier(secret, macaron)
verifier_success.satisfy_exact("user = 666")

verifier_fail = Verifier(secret, macaron)
verifier_fail.satisfy_exact("user = 667")

verifier_success.verify()
verifier_fail.verify()

Ce qui donne le résultat suivant :

Macaron valide
Macaron invalide

Maintenant atténuons ses droits :

macaron.add_caveat("path begins_with /bucket/films/")

VĂ©rifions la restriction.

# la ressource que l'on tente d'atteindre
ressource = "/bucket/films/ratatouille"

verifier_right_path = Verifier(secret, macaron)
verifier_right_path.satisfy_exact("user = 666")
verifier_right_path.satisfy_general(BeginWith("path", ressource))

verifier_right_path.verify()
Macaron valide ✅
# la ressource que l'on tente d'atteindre
ressource = "/bucket/compta/facture_12"

verifier_right_path = Verifier(secret, macaron)
verifier_right_path.satisfy_exact("user = 666")
verifier_right_path.satisfy_general(BeginWith("path", ressource))

verifier_right_path.verify()
Macaron invalide ❌

Le macaron est donc attĂ©nuable sans avoir besoin de connaĂźtre le secret. On appelle ça de l’attĂ©nuation offline.

Par contre, pour vérifier ce macaron atténué, il faut connaßtre le secret.

Si le macaron a Ă©tĂ© fourni Ă  un tiers, cela impose d’exposer une API d’authentification qui se chargera de valider le macaron.

Prenons l’attĂ©nuation suivante

atténuation d'un macaron

Le service 1, reçoit un macaron, le fait valider par l’API d’Auth.

vérification du macaron

Puis le transmet au service 2.

Celui-ci, lui-mĂȘme le fait vĂ©rifier.

vérification d'un macaron atténué

L’attĂ©nuation est offline, mais pas sa vĂ©rification, le secret reste centralisĂ© et donc doit soit ĂȘtre distribuĂ©, soit nĂ©cessite un service d’authentification tiers.

Biscuit est une proposition qui vise à la fois régler les problÚmes du JWT et ceux des Macarons.

Biscuit

Je sais que vous ĂȘtes lĂ  pour Biscuit, mais c’est encore vraiment trop complexe pour que ça puisse tenir ici.

On verra dans la partie 2, l’anatomie d’un Biscuit ainsi que les mĂ©canismes qui permettront Ă  la fois de le sĂ©curiser et de l’attĂ©nuer.

La suite sera lĂ .

Conclusion

Si vous ĂȘtes arrivĂ© jusqu’ici, je vous en fĂ©licite ! 😄

Un petit rĂ©capitulatif de ce que l’on a vu.

Les sessions cÎté serveur sont bien, mais exigent de conserver un état et donc une persistance.

Lorsque les infrastructures grandissent, le besoin de passer sur des mĂ©canismes d’authentification sans Ă©tat peut se faire sentir.

Les cookies ne pouvant pas ĂȘtre fournis sans sĂ©curitĂ©, le mĂ©canisme de signature a Ă©tĂ© inventĂ©, ce qui a fait apparaĂźtre le standard JWT.

Macaron par la suite, a été une tentative de rendre ces tokens immuables et atténuables.

Mais en introduisant 2 problĂšmes, le premier est la nĂ©cessitĂ© d’une centralisation de la vĂ©rification et le second l’absence de normalisation de la vĂ©rification du Macaron.

CritĂšresCookieTokenJWTMacaronBiscuit
SĂ©curisĂ©âŒâœ…âœ…âœ…â”
Payload normalisĂ©âŒâŒâŒâŒâ”
VĂ©rification normalisĂ©e❌❌❌❌❔
Immuable❌❌❌✅❔
AttĂ©nuation offline❌❌❌❌❔
VĂ©rifiĂ© par clef publique❌✅✅❌❔
Peut contenir des donnĂ©es non utiles pour la vĂ©rification✅✅✅❌❔

Biscuit a pour but de mettre une ✅ sur chaque ligne.

Un grand merci à Geoffroy Couprie et Clément Delafargue, qui ont bien voulu prendre de leur temps pour m'expliquer les concepts que je ne comprenais pas et effectuer la relecture attentive de cet article.

Ils sont accessoirement derriĂšre Biscuit. ^^

Je vous remercie encore une fois de n’avoir lu et je vous donne rendez-vous dans la partie 2. ❀

avatar

Auteur: Akanoa

Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.

Ce travail est sous licence CC BY-NC-SA 4.0.