https://lafor.ge/feed.xml

En route vers Biscuit (Partie 2)

2023-02-26

Bonjour à toutes et tous! 😀

Le moment tant attendu de la partie 2 de notre série sur le Biscuit est arrivé.

D'abord petit rappel de ce qu'est Biscuit.

Biscuit est un token cryptographique signé numériquement par clef asymétrique et atténuable en droits.

Si la phrase encadrée vous est incompréhensible, je vous conseil la lecture de la partie 1 et puis revenir ici. 🙂

Si vous êtes toujours là c'est que vous êtes au fait de ces concepts on va donc pouvoir charger la mule avec d'autres. 😈

Dans cet article nous allons réaliser une courte introduction aux langages logiques et à la notion de Blockchain.

Je vous propose le plan suivant:

  1. La cryptographie d'un Biscuit
  2. Le Datalog
  3. Les usages d'un Biscuit

C'est parti !

Tous les playgrounds sont interactifs 😀

La cryptographie d'un Biscuit

C'est peut-être ce qui est le plus important dans un token cryptograpique, c'est bien sa cryptographie.

Mais c'est quoi la cryptographie ?

Étymologiquement c'est l'art de cacher des choses.

Dans un Biscuit, on ne veut pas forcément cacher des choses mais plus les sécuriser.

Et dans notre cas ce qui va venir servir de verrou, c'est l'aléatoire, ou plus particulièrement la qualité de l'aléatoire.

Keypair

Cela va permettre de générer un élément cryptographique que l'on nomme une paire de clef ou Keypair.

missing alt

L'intégralité de la sécurité est basée sur l'imprédictibilité de la Keypair. Si deux Keypair sont identiques, alors les risques d'usurpations d'identités sont très élevés.

Une Keypair est composée d'une paire de clefs:

  • La clef privée
  • La clef publique

Mais pas n'importe lesquelles.

Celles-ci ont une relation qui les lie.

missing alt

Il est très facile de passer de la clef privée à la clef publique, autrement dit à partir d'une clef privée on peut reconstituer la clef publique.

Par contre, il doit être impossible à partir d'une clef publique de retrouver la clef privée de la paire de clef.

Ainsi, il n'y a aucun danger à laisser traîner une clef publique dans la nature. Chose qui nous sera très pratique par la suite.

Nous allons maintenant faire ce que l'on a appris dans la partie 1 signer numériquement et plus particulièrement par clefs assymétriques.

Sauf que l'on ne va pas utiliser une clef privée pour signer. Nous allons plutôt utiliser deux paires de clefs différentes.

Une paire de clefs racine. Celle-ci est soit générée aléatoirement, soit récupérée depuis une base de clefs chiffrement.

missing alt

Et une deuxième paire, elle complètement aléatoire.

missing alt

Créer un Biscuit

A partir de cette soupe de paires de clef et des données que l'on souhaite sécuriser (ou reviendra dessus plus tard 😅), nous générons une signature.

missing alt

Mais pas seulement, on également la paire de clef aléatoire que l'on déconstruit en deux entités:

missing alt
  • La next key 0
  • La preuve 0 qui est en fait la clef privée de la next key 0

Bon, il est temps de fabriquer notre premier Biscuit ! 🤩

Il est composé d'un bloc que l'on nomme une Autorité.

Dedans, nous allons y retrouver

  • les données à sécuriser
  • la clef publique de la paire de clef aléatoire
  • la signature générée à partir des données, de la paire de clef racine et de la paire de clef aléatoire

Puis, et c'est là que généralement les gens pètent un câble, une clef privée directement insérée dans le token en tant que preuve.

missing alt

Mais aucun risque, puisque cette clef privée est dans les faits une clef totalement aléatoire qui ne peut pas servir à usurper d'une quelconque manière une identité, car elle ne porte pas ce genre d'information.

Et peut donc, tout comme une clef publique être laissée dans la nature sans risque de sécurité.

(En tout cas, tout autant que le token lui-même)

Vérifier un Biscuit

Nous avons signé notre Biscuit, il est temps de le vérifier.

Pour cela, nous avons besoin:

  • du Biscuit
  • de la clef publique racine

Celle-ci provient de la paire de clef racine que nous avons utilisé pour créer notre Biscuit.

Mais ce qui est génial, c'est que seul la clef publique nous est nécessaire.

missing alt

Autrement dit, nous n'avons pas le souci du Macaron qui nécessitait que le secret ayant servi à la signature du Macaron soit connu de la personne qui allait vérifier cette même signature.

missing alt

Pour effectuer la vérification du Biscuit nous avons besoin d'un dernier ingrédient.

La clef publique dérivée de la preuve.

missing alt

La vérification est décomposée en deux parties:

La première consiste à vérifier l'authenticité des données en comparant la signature vérifiée par la clef publique racine.

La seconde vérifie que le bloc d'Autorité concerne bien la preuve. Pour cela, nous comparons la clef publique du bloc d'Autorité à la clef publique de la preuve.

Les deux doivent coïncider, si ce n'est pas le cas c'est que potentiellement le bloc d'Autorité n'est plus le même.

missing alt

Bon et du coup, comment est-on certain que les données contenues dans le Biscuit n'ont pas été falsifiées ?

Et bien, si un pirate fait évoluer les données, alors il y aura incohérence entre les données et la signature.

missing alt

Le seul moyen de rendre cohérent la signature serait de la falsifier elle aussi.

missing alt

Or! Ceci est également impossible par le mécanisme même de signature du Biscuit.

Le pirate peut connaître :

  • les données à falsifier
  • remonter à la paire de clef aléatoire au travers de la preuve
  • connait la clef racine publique

Mais, pour peu qu'elle n'ait pas fuité, il lui est impossible de reconstituer la clef racine privée.

Et donc, impossible de créer la paire de clefs racine.

Et donc impossible de créer une signature qui serait vérifiée par la clef racine publique.

missing alt

Et donc impossible de créer un Biscuit en ne connaissant pas la clef privée racine.

Blockchain

De la Blockchain ?? Comment ça, je pensais pas qu'on parlerai de crypto-monnaie ici !!

C'est parce que la Blockchain est bien plus qu'un outil de revendication d'indépendance monétaire. C'est à avant tout un moyen de stocker de manière sûre de la données sans faire confiance aux participants.

La conception de la chaîne de blocs, va consister à introduire un ou plusieurs nouveaux blocs, au bloc d'Autorité que l'on possède déjà. Et s'assurer que ces nouvelles données ne puissent pas être fasifiées et que l'on ne puisse pas non plus falsifier le bloc d'Autorité.

Et dernière contrainte et pas des moindres. Il ne faut pas avoir accès à la clef privée racine.

missing alt

Du coup comment va-t-on s'en sortir ?

C'est à ce moment que l'existence de la preuve devient magique ! 🧙‍♂️

La preuve étant une clef privée, on peut en reconstituer une paire de clef.

missing alt

Puis en générer une seconde paire aléatoirement.

missing alt

De cette paire de clef aléatoire on peut en créer une seconde preuve et la clef publique du bloc pour vérifier le prochain.

missing alt

Ce qui nous permet de signer les nouvelles données sans avoir besoin de clef privée racine.

On se sert à la place de notre paire de clef venant de la preuve précédante.

missing alt

Et on construit ainsi notre Biscuit atténué.

missing alt

Nous avons désormais un bloc d'Autorité suivi d'un bloc de données.

Nous avons également remplacé la preuve précédente qui est écrasée à tout jamais.

Vous verrez que ça aussi c'est fait à dessin. 😀

Vérifier le bloc supplémentaire

D'abord, petit rappel de comment est signé notre Biscuit.

missing alt
  • Le bloc d'autorité est signé par la clef racine privée.
  • Le bloc 1 est signé par la clef privée de la preuve 0.

Donc pour vérifier, nous utilisons ces clefs publiques.

missing alt
  • La signature du bloc d'autorité est vérifiée par la clef racine publique
  • La signature du bloc 1 est vérifiée par la clef publique de la preuve 0

Or, la clef publique de la preuve 0, est également la clef publique du bloc d'Autorité

missing alt

Donc la véritable vérification du bloc 1, se fait avec la clef publique du bloc d'Autorité

missing alt

Finalement, vérifier notre Biscuit, consiste à

missing alt

D'abord vérifier la signature du bloc d'autorité avec la clef racine publique comme nous l'avons fait précédemment.

Puis utiliser, la clef publique du bloc d'autorité pour vérifier le bloc 1.

Et finalement vérifier que la clef publique de preuve 1 correspond à la clef publique du bloc 1.

Falsification de bloc

Oui, d'ailleurs, ce dernier test, semble bien inutile. La clef publique du dernier bloc est toujours la clef publique de la preuve.

Oui, mais... Non...

Imaginons que les données qui soit dans le bloc 1, interdisent à une certaine personne d'accéder à une ressource. Il serait bien tentant de supprimer ce bloc gênant. 😈

missing alt

Bon, voilà c'est réglé, par contre la vérification ne va pas être aussi simple ...

missing alt

Autant, le bloc d'autorité est très bien vérifié.

Autant, la dernière clef publique de bloc connue est celle de l'autorité.

Or, celle-ci est différente de la clef publique de la preuve 1 du Biscuit.

Donc, la validation ne passe pas ! ❌

Ok, donc pour retirer un bloc, faut falsifier la preuve. Bon, allons-y!

missing alt

Ah, oui, mais non ...

Tout ce qu'on connaît de la preuve 0, c'est que c'est la clef privée de la clef publique du bloc d'autorité.

Or, il est impossible de remonter à la clef privée en partant d'une clef publique.

missing alt

Donc encore perdu ! ❌

Et de même, il est également impossible de falsifier les données du bloc 1, car nous ne pouvons pas remonter à la preuve 0, qui a servi à créer la signature qui sera vérifiée par la clef publique du bloc d'autorité.

missing alt

A partir du moment où la preuve 0 est remplacée par la preuve 1, le coffre fort se referme!

Il est impossible de retirer ou de modifier un bloc sans avoir au préalable la clef racine privée permettant de reconstituer toutes les preuves!

Plus de blocs !

Maintenant que nous avons la logique, nous pouvons commencer à enchaîner les blocs.

missing alt

Le principe reste identique.

On utilise la preuve 1 pour créer la paire de clef de preuve 1.

On génère une paire de clef aléatoire qui deviendra la clef publique du bloc 2 et la preuve 2.

On signe nos données avec la même formule qui fait intervenir les deux paires de clefs.

Et on remplace la preuve 1, par la preuve 2.

Il est désormais impossible de recréer la preuve 1.

A part la rechercher dans un Biscuit ne comportant que le bloc 1. Mais l'on verra que cela n'a aucune importance, lorsque l'on abordera le contenu des données.

Bon et du coup, même principe pour N blocs.

missing alt

La seule différence est la preuve qui est la preuve ${n-1}$

Et pour vérifier cela devient mécanique.

missing alt

On part du bloc d'autorité que l'on vérifie avec la clef publique racine, puis on utilise la clef publique du bloc d'autorité pour vérifier le bloc 1, puis on utilise la clef publique du bloc 1, pour vérifier le bloc 2, etc ...

Pour finalement vérifier le bloc $n$, en utilisant la clef publique du bloc $n-1$.

Et bien évidemment, on s'assure de la conformité de la chaîne en vérifiant que la preuve du Biscuit correspond bien à la clef publique du dernier bloc.

Le Datalog de Biscuit

Bon c'est magnifique, nous avons un coffre-fort, il est temps d'y mettre des bijoux et des documents précieux ! 🤑

Mais au lieu d'y mettre un simple clef/valeur comme nous avons pu le faire avec le JWT ou le Macaron. Nous allons introduire une sémantique.

Pourquoi faire ceci ?

Et bien, le problème du Macaron outre l'obligation de vérification au moyen d'un secret partagé, est l'absence de formalisme défini lors de la vérifications des caveats.

Autrement dit, chacun peut implémenter comme il le désire ses règles, en fonctions des données qui lui sont fournies.

Il existe des bibliothèques toute faites pour générer ses Verifiers mais le Macaron reste un clef-valeur sans aucune intelligence.

Biscuit, prend le pari d'utiliser un langage de programmation pour définir le contenu cette intelligence.

Ce langage se nomme le Datalog.

Oui, l'article Wikipedia fait mal à la tête ^^

Essayons de comprendre pas à pas le fonctionnement de celui-ci. Et pourquoi c'est cool une fois qu'on a saisi son intérêt! 😁

Faits

Le premier concept à assimiler est le Fait.

Un fait est de l'information.

Dire qu'un utilisateur possède l'identifiant #1 est un fait.

Dire qu'un utilisateur est dans le groupe admin est également un fait.

Dire que le Biscuit a été créer le mercredi 1 mars 2023 à 10:10 est aussi un fait.

En Datalog un fait est représenté par un identifiant, suivi entre parenthèses de la valeur de ce fait.

missing alt

Par exemple, ici, je représente un fait user de valeur 1.

user(1)

Il est possible de mettre autant de fait que l'on désire

missing alt

Ici, je représente les faits énoncé plus haut.

user(1); group("admin"); time(2023-03-01T10:10:00+01:00)

Un même fait peut apparaître plusieurs fois.

missing alt

Par exemple pour représenter qu'un utilisateur est dans deux groupes à la fois.

user(1); group("admin"); group("publisher");

Un fait peut également contenir plusieurs valeurs.

user_group(1, "admin");

Donc de manière générale un fait peut-être défini par un identifiant suivi de 1 à plusieurs valeurs.

missing alt

 

Attention

Un fait doit posséder au moins une valeur.

Le fait bad sans valeur est incorrect.

bad()

On peut alors s'amuser à construire des tables de données.

missing alt

Ce terme de table n'est pas anodin.

Il faut réellement visualiser les fait comme des lignes dans une table d'une base de données SQL.

Prenons ce jeu de faits.

//membres des groupes user_group(12, "admin"); user_group(12, "it"); user_group(13, "it"); user_group(14, "compta"); // utilisateurs user(12); user(13); user(14); user(15); // groupes group("admin"); group("it"); group("guest"); group("compta");

Nous alors sommes face à 3 tables:

  • user
  • group
  • user_group
missing alt

gardez bien en tête cette représentation sous forme de tables, elle nous sera bien utile. 😁

Fait dynamique

Un autre concept de Datalog est la capacité à créer des fait au travers d'autres faits.

Je vais nommer ça des Faits dynamiques

missing alt

Un fait dynamique se construit en venant piocher des informations venant d'autres faits.

missing alt

Exemple, nous voulons recréer les faits user_group à partir des faits user et group.

//--- Déclarations des faits // utilisateurs user(12); user(13); user(14); // groupes group("admin"); group("it"); group("compta"); // --- Déclaration dynamique des membres des groupes user_group($user, $group) #<= user($user), group($group);

Alors que s'est-il passé ?

En haut la déclaration du Datalog.

Les tables et la définitions de la manière de créer le fait user_group et donc la table correspondante.

En bas, l'ensemble des entrées par tables.

Tout se passe comme si vous effectuiez une opération de INNER JOIN entre deux tables.

SELECT user.user, user.group FROM user INNER JOIN group;

Ce qui créé la table virtuelle user_group.

missing alt

Qui vient s'additionner au précédentes qui était user et group.

Explication du produit cartésien

Pour rappel, un INNER JOIN sans clause ON est l'application d'un produit cartésien entre plusieurs ensembles d'entités.

Et qu'est ce qu'un produit cartésien ? Il s'agit de l'ensemble des combinaisons possibles entre les éléments des différents paquets d'entités.

Exemple les paquets nombres et lettres.

On vient réaliser les différentes ramification partant de nombres pour aller à lettres.

missing alt

Fait dynamique contraint

Tout cela c'est parfait, mais on manque de contrôle sur ce qui se passe.

Peut-on empêcher la création de faits si par exemple, certaines contraintes ne sont pas respectées ?

La réponse est oui.

Pour cela, penchons-nous un peu plus sur la manière dont on génère un fait à partir d'un autre. D'ailleurs ce processus s'appelle une Règle.

Ici, nous avons un fait x avec une valeur de true.

La règle que l'on défini est x est vrai.

missing alt

Lorsque cela est véridique alors le fait x est vrai existe.

x(true); x_est_vrai($X) #<= x($X), $X;

Par contre, si ce n'est pas la réalité alors le fait x est vrai n'existera pas.

Par exemple si le fait x n'existe pas.

missing alt

Ou que la valeur ne correspond pas.

missing alt

Dans le premier cas, l'évaluation se stoppe car le fait x ne peut pas être trouvé, dans le deuxième cas, le fait x existe, mais sa valeur est false.

C'est là que l'on découvre une autre facette de l'évaluation.

Il est possible soit de faire du pattern matching sur des faits, soit d'utiliser les valeurs des faits qui ont été matchées.

Pour chaque analyse, chacune séparées par une virgule ,. Soit ça match, soit ça renvoie true.

y(true); x(false); x_est_vrai($X) #<= x($X), $X;

Dans les deux cas, on remarque que le fait x est vrai n'est pas créé.

Une fois que l'on a compris la logique, on peut alors l'utiliser pour venir vérifier des comportements plus complexes, comme filtrer de la donnée.

En cas positif

missing alt

Ou négatif

missing alt
// déclaration des faits x(12); x(9); below_10($X) #<= x($X), $X < 10;

On voit alors que seul below_10(9) a été généré, le x(12) a été filtré.

Avec ce que l'on a appris, nous pouvons faire des choses intéressantes, comme par exemple grouper les utilisateurs.

//--- Déclarations des faits // utilisateurs user(12); user(13); user(14); user(15); // groupes user_group("admin", 12); user_group("it", 12); user_group("it", 13); user_group("compta", 14); // --- Déclaration dynamique des membres des groupes compta($user) #<= user($user), user_group($group, $user), $group == "compta"; it($user) #<= user($user), user_group($group, $user), $group == "it"; admin($user) #<= user($user), user_group($group, $user), $group == "admin";

Nous avons bien deux utilisateurs it, et un compta et un admin.

Le user(15) n'appartenant à aucun groupe, n'apparaît dans aucun des faits.

En SQL

Pour le fait compta nous aurions

SELECT u.user
    FROM user u INNER JOIN user_group ug
    ON u.user = ug.user
    WHERE ug."group" == 'compta'

Nous faisons le JOIN en nous rattachant sur l'indenticité via le ON u.user = ug.user des valeurs de colonnes entre les tables, puis nous effectuons une vérification sur les valeurs qui correspondent.

Et on peut faire de même pour le fait it.

SELECT u.user
    FROM user u CROSS JOIN user_group ug
    ON u.user = ug.user
    WHERE ug."group" == 'it'

Il existe une syntaxe alternative qui se base sur le fonctionnement même de Datalog.

//--- Déclarations des faits // utilisateurs user(12); user(13); user(14); user(15); // groupes user_group("admin", 12); user_group("it", 12); user_group("it", 13); user_group("compta", 14); // --- Déclaration dynamique des membres des groupes compta($user) #<= user($user), user_group("compta", $user); it($user) #<= user($user), user_group("it", $user); admin($user) #<= user($user), user_group("admin", $user);

Celle-ci repose sur la capacité de Datalog de venir matcher des faits selon des informations précises.

Pour le fait it.

On match tous les faits user_group dont la première valeur est "it", puis l'on vérifie que la valeur du fait user($user) correspond au user_group("it", $user).

Si ces deux conditions sont remplies, alors le fait it($user) peut exister et prend la valeur du $user correspondant.

On parle d'unification.

missing alt

Je vais être tout à fait honnête avec vous, c'est encore une notion très vague dans mon esprit, mais ça ne nous empêche pas de l'utiliser 😁

Attention!

Le Datalog peut réserver quelques surprises !

On peut se dire, du coup si je veux toutes les personnes non-it il suffit de faire :

//--- Déclarations des faits it(12); it(13); // utilisateurs user(12); user(13); user(14); // --- Déclaration du fait dynamique non_it($u) #<= user($u), it($i), $u != $i;

Mais comme vous pouvez le voir ça ne marche pas.

Pourquoi, et bien rappelez-vous, avant d'évaluer les valeurs, on construit d'abord l'ensemble des possibilités.

missing alt

Ce qui fait, que certaines combinaisons, génère des faits, alors que l'on ne le voudrait pas.

Pour cela, il faut ruser, et réfléchir différemment.

Au lieu de déclarer plusieurs faits user_group si celui-ci appartient à plus d'un groupe. Nous allons plutôt définir un tableau de droits par utilisateur.

//--- Déclarations des faits user_groups(12, ["admin", "it"]); user_groups(13, ["it"]); user_groups(14, ["compta"]); user_groups(15, []); user_groups(18, ["it"]); // --- Déclaration du fait dynamique non_it($user) #<= user_groups($user, $groups), !$groups.contains("it"); guest($user) #<= user_groups($user, []);

On peut alors vérifier facilement la non-existence de la valeur "it" dans le tableau $groups et l'opérateur de négation !.

De même, avec cette nouvelle typologie, nous pouvons également vérifier si un utilisateur n'appartient à aucun groupe. Pour cela on match sur le tableau vide [].

Bref, Datalog est puissant mais demande de réfléchir d'une manière différente.

Mais c'est ça qu'est bon ! 🤩

Vérifier des faits

Pour le moment, on écrit des faits et on en génère à partir d'autres mais on ne fait pas grand chose côté validation. C'est quand même la promesse que je vous ai fait par rapport au Macaron.

Remédions à cette situation, en introduisant encore un nouveau concept.

Il s'agit des Checks.

Un Check prend un fait et vérifie sa cohérence. Par cohérence, j'entends est-ce qu'il existe ou non ?

missing alt

Bien entendu, un Check vérifiera également un Fait dynamique.

missing alt

Ou juste, un enchaînement de faits, dynamique ou non d'ailleurs.

missing alt

Prenons un exemple extrêmement simple. Nous voulons vérifier que le fait x existe, quel que soit sa valeur.

x(5); check if x($x);

Ok, easy!

Maintenant plus compliqué.

On cherche à savoir si la valeur du fait x est inférieur à 3.

x(5); check if x($X), $X < 3;

Et oui, c'est la même syntaxe que lors de la génération des faits dynamiques contraints.

Ici, la valeur est de 5 ce qui excède 3. Cela signifie que le Check ne passe pas.

Et lorsqu'un Check ne passe pas, le Biscuit est automatiquement rejeté.

Voyons comment ce se déroule lorsqu'il y a plusieurs checks dans une validation.

missing alt
fait_1(5); check if fait_1($x), $x < 10; check if fait_1($x), $x > 3;

Tout se passe bien.

missing alt
fait_1(2); check if fait_1($x), $x < 10; check if fait_1($x), $x > 3;

Le deuxième échoue.

missing alt
fait_1(3); check if fait_1($x), $x > 3; check if fait_1($x), $x < 10;

Le troisième également.

Par contre, il y a un message: No policy matched.

Police: papiers s'il vous plaît

Nous allons encore rajouté un dernier concept, il s'agit des Policies qui veut dire "stratégies" en bon français. C'est à dire comment doit-on évaluer un Biscuit.

Tout comme les checks, nous pouvons évaluer tout un tas de choses.

Il existe deux variantes possibles:

Les Allows qui sont vrais si le contenu évaluer l'est.

missing alt

Et les Denies qui marchent à l'inverse.

missing alt

Mais sa manière de fonctionner est différente.

Si un seul check était en échec, l'ensemble de l'évaluation était en échec.

Pour une police c'est différent.

missing alt
fait_1(5); allow if fait_1($x), $x < 10; allow if fait_1($x), $x > 3; deny if true;

Si une police ne correspond pas, on passe à la suivante.

missing alt
fait_1(5); allow if fait_1($x), $x > 10; allow if fait_1($x), $x > 3; deny if true;

Et ainsi de suite.

missing alt
fait_1(5); allow if fait_1($x), $x > 10; allow if fait_1($x), $x < 4; deny if true;

Jusqu'à atteindre ici le deny qui est toujours vrai, mais seulement évalué quand c'est le dernier choix possible.

Il est également possible de mélanger les allow et le deny pour modifier les comportements

missing alt
fait_1(5); allow if fait_1($x), $x > 10; deny if fait_1($x), $x < 6; deny if true;

Et finalement, pouvoir tout mélanger: des checks et des policy.

missing alt
fait_1(8); check if fait_1($x), $x < 10; check if fait_1($x), $x > 3; allow if fait_1($x), $x > 7; deny if true;

En résumé, tous les Checks doivent être valides, mais seulement une seul Policy valide est nécessaire.

Attention, tout de fois, à l'ordonnencement des policy, en effet le premier qui match, sera celui qui définira l'état de validation.

fait_1(5); check if fait_1($x), $x < 10; check if fait_1($x), $x > 3; allow if true; allow if fait_1($x), $x > 7; deny if true;
fait_1(5); check if fait_1($x), $x < 10; check if fait_1($x), $x > 3; allow if fait_1($x), $x > 7; deny if true;

Le allow if true court-circuite les autres évaluations.

Construire un Biscuit

Nous avons toutes les pièces du puzzle. Plus qu'à mettre de l'ordre dans tout ça et organiser ce joyeux système de concepts.

Tout d'abord petite remise au point sur ce qu'est un Biscuit.

C'est une structure de données qui est formée d'une Autorité et optionnellement de blocs.

missing alt

Maintenant regardons plus en détails ces différents composants.

Autorité

Nous allons commencer simple. Nous allons faire un Biscuit, qui pourrait s'apparenter à un token JWT avec du Datalog dedans.

Pour ce faire nous allons remplir l'Autorité.

Elle peut être composées:

  • de faits globaux
  • de checks
  • de rules (faits dynamiques)
missing alt

Prenons par exemple le Biscuit suivant, un Biscuit qui correspond à l'utilisateur #12 et qui est limité dans son utilisation aux services "compta" et "banque".

missing alt

Ce Biscuit n'est pas valide car le fait service , n'existe pas.

Par contre, nous sommes en mesure de déterminer l'authenticité des données, car seul un possesseur de la clef privée racine peut avoir signé ce Biscuit.

user(12); is_allowed($s) #<= service($s), ["compta", "banque"].contains($s); check if is_allowed($s);

Pour le moment, ce Biscuit ne fait rien par lui-même, il définit des faits et des checks.

Mais rien ne met en mouvement ce Datalog.

Ce mouvement va provenir d'un dernier acteur.

Authorizer

L'Authorizer. Son rôle est de valider ce qui doit l'être.

missing alt

Il prend en entré un Biscuit, et le valide.

Pour réaliser la validation, l'Authorizer se base également sur du Datalog.

Mais un Datalog qui possède plus d'outils.

  • Faits globaux
  • Rules
  • Checks
  • Policy
missing alt

On peut alors implémenter cet Authorizer et le Biscuit à vérifier.

Ici nous nous retrouvons avec un Biscuit qui contient le même identifiant utilisateur #12 et un fait dynamique is_allowed vrai si le service est soit la "compta" soit la "banque".

La différence est que le check a été déporté dans l'Authorizer sous la forme d'une Policy allow.

Cet Authorizer définit également le fait service, ici à banque.

user(12); is_allowed($s) #<= service($s), ["compta", "banque"].contains($s); service("banque"); check if user($u), $u != 1; allow if is_allowed($x); deny if true;

Comment se passe la validation ?

missing alt
  • On voit que l'Authorizer a besoin du fait user. Il n'existe pas de fait user dans l'Authorizer.

  • Celui-ci se trouve dans l'Autorité. Il est donc récupéré pour être utilisé dans le check check if user($u), $u != 1.

  • L'Authorizer a également besoin du fait is_allowed, celui-ci est défini dans l'Autorité.

  • Or le fait is_allowed est dynamique, pour être construit, il demande le fait service, qui se trouve dans l'Authorizer.

  • Le fait service est donc injecté dans l'Autorité, qui peut alors générer le fait is_allowed.

  • Le fait is_allowed existant désormais dans l'Autorité, il peut alors être utilisé par le allow if is_allowed

  • Le Biscuit est validé

Notez bien

Nous pouvons faire ces opérations car nous avons confiances dans les données de l'Autorité du Biscuit ❤

Chose irréalisable sur des caveats de Macaron, par exemple, car on ne peut être certain de leur authenticité !

Atténuation

Nouveau cas d'usage.

Au lieu de définir, le check de service dans l'Authorizer, nous souhaitons faire les choses différemment.

Imaginez, vous possédez un Biscuit qui possède un fait comportant votre ID utilisateur.

missing alt

Celui-ci, vous donne accès de manière illimité à tout vos services.

L'Authorizer du service possède un fait service, avec le nom du service en question et c'est tout.

L'authorisation est inexitante, on ne réalise qu'une authentification. Nous verrons l'autorisation de ressource dans l'exemple suivant. 😀

On croit le Biscuit, comme étant l'utilisateur 12, on lui ouvre une session, et c'est bon. 🙂

Bien sûr, vous ne pouvez pas accéder au session d'un autre utilisateur. 😎

user(12) service("banque"); allow if true;

Maintenant, pour une raison qui vous est propre. Vous souhaitez partager votre session avec un ami.

Vous lui donnez le Biscuit, et il se retrouve à pouvoir accéder à tous les services de votre utilisateur !! 😱

missing alt

Vous ne pouvez pas modifier l'Autorité de votre Biscuit, car vous n'avez pas la clef privée racine en votre possession.

Par contre, Biscuit est atténuable par bloc.

Un bloc peut posséder:

  • des Faits locaux
  • des Checks
  • des Rules

On revient juste après sur la notion de "faits locaux".

missing alt

On peut alors rajouter un bloc qui va venir limiter les services accessibles.

missing alt

C'est ce Biscuit atténué en droits que vous allez transmettre à votre ami.

missing alt

Ce qui nous donne côté Authorizer la même chose, mais pourtant nous avons pu dégrader nos droits et les déléguer à une autre personne.

Cet ami, pourra alors lui-même atténuer les droits qu'il possède en rajoutant un nouveau bloc, et ainsi de suite ! 😍

user(12); check if service($s), ["compta", "banque"].contains($s); service("banque"); allow if true;

Si notre ami tente d'accéder à un service dont on ne veut pas, voici ce qui se passe.

user(12); check if service($s), ["compta", "banque"].contains($s); service("it"); allow if true;

Les portes restent closes. 😈

Sécurité sur l'atténuation

Alors, pourquoi parler de faits locaux dans le cadres des blocs qui ne sont pas l'Autorité ?

Prenons ce Biscuit

missing alt

Il possède un certain ID.

Si on croie tous les faits qu'importe leur provenance.

Qu'est ce qui empêche de rajouter un bloc avec l'ID que l'on désire ?

missing alt

Heureusement, les concepteurs de Biscuit ne sont pas fous et ne permettent pas de faire ce genre de choses.

missing alt

Seuls les faits de l'Autorité sont crus.

user(12); user(1) allow if user(1); deny if true;

Exemple complet

Allez, dernier effort!

Un exemple complet pour récapituler tout et donner plus de concret à ce que j'ai déroulé depuis le début de cet article. 😁

Comme dans les pièces de théâtres, présentons les acteurs.

Nous avons:

  • Le Service A qui authentifie les utilisateurs et émet des Biscuit
  • Le Service B qui possède des ressources et valide des Biscuits
  • Le Pirate qui tente d'accéder à des ressources dont il n'a pas les droits sur le Service B

Le Service A signe ses Biscuit avec une clef privée A.

Le Service A, n'a qu'une confiance limitée du Service B, mais peut sans crainte lui transmettre la clef publique A.

Le Service B, ne croit pas le Pirate qui lui n'a pas connaissance de la clef privée A.

missing alt

Vous en tant qu'utilisateur abonné au Service B, vous tentez d'accéder à une ressource qui vous appartient.

Vous réalisez une requête en lecture sur le film matrix.

missing alt

Mais vous vous faites jeter, il vous manque le Biscuit approprié.

Vous vous authentifiez donc sur le Service A, qui a condition d'un mot de passe correct, vous délivre un Biscuit.

Celui-ci possède un fait user représentant votre identifiant utilisateur.

missing alt

Vous refaites la même requête mais avec le Biscuit.

missing alt

La première chose que le Service B va réaliser c'est de vérifier l'authenticité du Biscuit en utilisant la clef publique A.

S'il a bien été signé par la clef privée A, alors la vérification passe.

missing alt

Une fois que cette vérification est passée, rien n'empêche d'utiliser le Biscuit comme un token JWT et venir récupérer le fait user et surtout sa valeur qui est l'identifiant de l'utilisateur.

missing alt

Nous pouvons faire cela, car nous avons la certitude que seul le Service A a pu définir cet ID. (à condition que la clef privée n'est pas leak).

Pourquoi faisons-nous ça ?

Et bien, nous pouvons avoir des millions d'utilisateurs abonnés. Or nous stockons en base de données les droits associés à ces ressources.

missing alt

Et nous allons être encore plus malin, en ciblant précisément la ressource que l'utilisateur souhaite joindre.

Or, la requête d'API, nous fourni cette information.

missing alt

Nous pouvons ainsi créer une requête SQL la plus optimisée, pour ne renvoyer que les résultats voulus.

En combinant l'ID récupéré du Biscuit et la ressource venant de la requête.

missing alt

On peut alors transformer les résultats de la requête SQL en faits rights.

missing alt

Et nous avons un autre fait operation que l'on obtient, là aussi de la requête API.

Finalement, nous pouvons générer l'Authorizer suivant.

Il vérifie que l'opération demandé par l'utilisateur est bien possible sur la ressource.

missing alt

Nous validons ensuite le Biscuit avec cet Authorizer.

missing alt

Finalement, le Service B répond à l'utilisateur avec son film.

missing alt

Si l'utilisateur, tente d'accéder à une resource dont il n'a pas les droits ou qui n'existe pas.

Il sera rejeté.

missing alt

Si un pirate, tente d'accéder à vos ressources en se faisant passer pour vous, il pourra créer un Biscuit, avec l'Autorité qu'il faut oui.

Mais pas avec la bonne signature.

La validation d'authenticité du Biscuit échoue et on renvoie une erreur au pirate.

missing alt

Maintenant, vous avez un ami, vous voulez lui montrer votre super collections de films mais pas autre chose.

Vous lui passez un Biscuit, mais pas n'importe lequel.

Un Biscuit Films.

missing alt

Celui-ci est atténué en droits par l'ajout d'un bloc Check qui vérifie que la resource commence par "/films/".

missing alt

Ce nouveau Biscuit est alors celui-ci. Et vous avez pu faire cette atténuation par vous même, aucun besoin de le demander au Service A de réemmettre un Biscuit différent.

Vous êtes autonome !! 😎

missing alt

Votre ami fait alors une requête sur le Service B avec son Biscuit tout chaud.

missing alt

On valide alors le Biscuit Films avec la clef publique A, comme le Biscuit originel a été édité par la clef privée A, la validation passe.

missing alt

On unifie les mondes du Biscuit et ce de l'Authorizer.

Le Datalog se déroule.

missing alt

Et on répond à l'ami de l'utilisateur avec le film.

missing alt

S'il tente d'accéder à une ressource dont il n'a pas les droits, il sera jeté.

missing alt

Vos comptes en banque sont en sécurité ^^

Je vous propose maintenant en guise de "devoir maison" et de récompense pour être arrivé jusqu'ici.

Un petit exercice.

Voici un Playgound, modélisez l'exemple qu'on vient d'étudier.

A vous de bosser ^^

Je donnerai la réponse sur twitter et plus tard dans cet article.

Conclusion

Bon maintenant que l'on a éffleuré la surface de Biscuit, nous allons ...

Je déconne ! 😁

Cet article est déjà bien trop long !

Dans la prochaine partie, la 3 donc.

Nous verrons un certain nombres de concepts manquants:

  • Révocation de Biscuit : si la clef privée leak, mais on verra que c'est bien plus puissant que ça ^^
  • Block 3rd party : pour que l'Authorizer fasse confiance à des faits situés dans un (ou plusieurs) des blocks du biscuit (et pas que dans l'autorité)
  • les bonnes pratiques
  • le fonctionnement de la sérialisation.

Un grand merci à ceux et celles qui auront lu cet article et je vous dit à la prochaine ❤️

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.