Skip to content

Intégrer le 2-man-rule sur votre backend

Comme expliqué dans le guide de gestion des identités la protection en 2-man rule nécessite quelques modifications sur votre backend.

Plus précisément, deux choses doivent être modifiées :

  • la génération et stockage d'une clé twoManRuleKey (ou rawTwoManRuleKey) secrète associée à un utilisateur de votre application, remise lors de l'authentification ;
  • une interaction avec le service Seald SDK Key Storage (SSKS) pour que l'utilisateur puisse s'authentifier auprès de SSKS.

On notera :

  • Utilisateur : l'utilisateur de l'application dont on veut protéger l'identité ;
  • Identité : une identité cryptographique Seald de l'Utilisateur (décrite dans le guide dédié) ;
  • Frontend : le frontend de l'application dans laquelle le SDK est intégré ;
  • Backend : le backend de l'application dans laquelle le SDK est intégré, il stockera twoManRuleKey, la clé permettant de déchiffrer encryptedIdentity ;
  • SSKS : une instance de Seald SDK Key Storage, il stockera encryptedIdentity non déchiffrable sans la twoManRuleKey ;
  • Facteur d'authentification : le moyen d'authentifiaction utilisé par l'utilisateur sur SSKS. Actuellement l'authentification par email et par SMS sont disponible ;

L'objectif est de donner à l'Utilisateur les deux "morceaux" de son identité Seald (twoManRuleKey et encryptedIdentity) par authentification, tout en empêchant SSKS et le Backend de pouvoir chacun indépendamment accéder à l'identité de l'utilisateur.

TIP

Lors de la protection de l'identité en 2-man rule, vous pouvez utiliser soit l'argumenttwoManRuleKey, soit l'argument rawTwoManRuleKey, pour chiffrer l'identité stockée sur SSKS.

Les deux sont très similaires à la fois sur l'idée et sur la manière de les utiliser, la seule différence étant que twoManRuleKey est d'un format libre (mais avoir un aléa suffisant est toujours important), alors que rawTwoManRuleKey doit absolument être l'encodage en base64 d'un buffer cryptographiquement aléatoire de 64 octets.

Techniquement, cela évite de dériver la twoManRuleKey avec scrypt, donc utiliser rawTwoManRuleKey rend le stockage et la récupération de l'identité un peu plus rapides, mais twoManRuleKey peut être un peu plus facile à utiliser.

Pour plus de détails, voir le paragraphe dédié du guide sur les identités.

Dans ce guide, nous dirons twoManRuleKey seulement, mais pour évoquer l'une des deux.

Préalable

Pour communiquer avec le serveur SSKS, votre Backend aura besoin de :

  • keyStorageURL : l'URL du serveur SSKS, elle peut être récupérée sur le tableau d'administration ;
  • ssksAppId : correspond au appId utilisé pour le SDK ;
  • ssksAppKey : permet à votre Backend de s'authentifier auprès du serveur SSKS.

Cette ssksAppKey peut être générée ou renouvelée sur le tableau d'administration.

Aller sur la page de gestion du SDK
1. Aller sur la page de gestion du SDK
Générer la clé
2. Dans la partie "Clé serveur SSKS-2MR", cliquer sur le bouton "Générer la clé"
Clé générée
3. Vous pouvez copier la clé générée. Attention, vous n'y aurez plus accès plus tard ! Enregistrez-la maintenant !

Protocole

Le protocole s'effectue en deux étapes :

  1. la protection initiale de l'identité qui n'arrive normalement qu'une seule fois immédiatement après sa génération ;
  2. la récupération de l'identité qui arrive à chaque fois que l'Utilisateur veut récupérer son identité localement.

TIP

La protection d'une identité en 2-man rule est usuellement couplée à une base de données locale persistante pour "mettre en cache" l'identité de l'Utilisateur et éviter qu'il ait à nouveau besoin de la récupérer à chaque fois qu'il ouvre l'application.

Le protocole pour effectuer la protection initiale d'une identité notée identity de l'Utilisateur est le suivant :

uml diagram

TIP

Dans le cas où une identité a déjà été stockée sur SSKS avec ce facteur d'authentification, ou si la requête contenait force_auth: true, le serveur SSKS demandera alors une authentification par challenge, en envoyant mustAuthenticate à true. L'utilisateur recevra un challenge par email ou SMS, valable 6h, et devra le répéter dans le front-end pour pouvoir s'authentifier sur SSKS avant de stocker sa nouvelle identité.

Dans le cas où aucune identité n'a jamais été stockée sur SSKS avec ce facteur d'authentification, le serveur répondra au contraire avec mustAuthenticate: false (sauf si la requête contenait force_auth: true). L'utilisateur ne recevera alors aucun challenge, et peut directement stocker son identité.

Pour récupérer l'identité identity de cet Utilisateur lors d'une autre session, le protocole est le suivant :

uml diagram

Les flèches rouges dans le schéma indiquent les opérations à implémenter dans l'application utilisant le SDK Seald et le mode de protection en 2-man rule. Les flèches noires indiquent ce qui est déjà implémenté par le SDK, le plugin de protection en 2-man rule @seald-io/sdk-plugin-ssks-2mr ou SSKS.

WARNING

Les adresses emails et numéros de téléphones envoyés au serveur SSKS doivent absolument être normalisés.

Protection de l'identité

Avant d'appeler la méthode saveIdentity exposée par le plugin @seald-io/sdk-plugin-ssks-2mr, il faut les éléments suivants :

  • userId : un identifiant unique de l'utilisateur dans l'application ;
  • authFactor : adresse e-mail ou numéro de telephone de l'utilisateur utilisée pour l'authentification par challenge, doit être répété ou vérifié par l'utilisateur pour s'assurer que le backend n'essaye pas de réaliser une attaque MITM ;
  • twoManRuleKey : la clé connue du backend permettant de chiffrer / déchiffrer l'identité ;
  • sessionId : l'identifiant de session généré par SSKS et transmis par le backend au SDK ;
  • si le serveur à envoyé mustAuthenticate à true, challenge : un jeton aléatoire envoyé par SSKS par e-mail ou SMS à l'utilisateur pour l'authentifier, valable 6h, ne doit jamais être révélé au backend ;

Pour cela, il faut implémenter un point d'API getSSKSSession authentifié sur le backend qui :

  1. utilise le point d'API POST https://{SSKS_URL}/tmr/back/challenge_send/ pour envoyer à SSKS :
  • create_user : donner true pour créer en même temps l'utilisateur sur SSKS sinon cela devra être fait avec une requête préaable ;

  • user_id : correspond au userId dans @seald-io/sdk-plugin-ssks-2mr ;

  • auth_factor : un object contenant deux entrées:

    • type : le type de facteur d'authentification. 'EM' pour un email, ou 'SMS' pour un numéro de téléphone ;
    • value : l'adresse e-mail ou le numéro de téléphone de l'utilisateur où le challenge sera envoyé ;
  • template : le template d'email à utiliser au format HTML, ou le contenu du SMS, et qui doit contenir $$CHALLENGE$$ qui sera remplacé par le challenge.

    SSKS va retourner au backend sessionId et mustAuthenticate.

    Si mustAuthenticate est true, SSKS va envoyer un email ou un SMS au format de template au facteur d'authentification contenant challenge, valable 6h ;

  1. génère twoManRuleKey (64 octets aléatoires robustes cryptographiquement) et la stocke associé à cet utilisateur ;

WARNING

Dans le cas où vous voulez utiliser une rawTwoManRuleKey, celle-ci doit absolument être l'encodage en base64 d'un buffer cryptographiquement aléatoire de 64 octets.

Si vous utiliser une twoManRuleKey simple, le format est libre, mais celle-ci doit quand même contenir suffisamment d'aléa.

  1. renvoie au frontend sessionId, mustAuthenticate, et twoManRuleKey.

TIP

Dans le cas où mustAuthenticate est true, le frontend doit bloquer et attendre que l'utilisateur entre le challenge qui lui a été envoyé avant d'appeler la méthode saveIdentity.

Dans le cas où mustAuthenticate est false, le frontend peut appeler la méthode saveIdentity directement.

Ensuite, le frontend peut utiliser la méthode saveIdentity de la façon suivante :

js
// On fait un appel API au serveur de l'application pour récupérer le `sessionId` et la `twoManRuleKey`
const { sessionId, twoManRuleKey } = APIClient.getSSKSSession() // point d'API à développer de votre côté

await seald.ssks2MR.saveIdentity({
  userId: 'myUserId',
  sessionId,
  // l'adresse e-mail de l'utilisateur doit être répétée pour éviter un MITM par votre serveur applicatif
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  challenge: 'XXXXXXXXX', // `challenge` envoyé au facteur d'authentification  si `mustAuthenticate` est `true`, `null` ou non-passé sinon.
  twoManRuleKey // `twoManRuleKey` est la clé stockée par votre serveur applicatif pour protéger l'identité de cet utilisateur
})
// On fait un appel API au serveur de l'application pour récupérer le `sessionId` et la `twoManRuleKey`
const { sessionId, twoManRuleKey } = APIClient.getSSKSSession() // point d'API à développer de votre côté

await seald.ssks2MR.saveIdentity({
  userId: 'myUserId',
  sessionId,
  // l'adresse e-mail de l'utilisateur doit être répétée pour éviter un MITM par votre serveur applicatif
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  challenge: 'XXXXXXXXX', // `challenge` envoyé au facteur d'authentification  si `mustAuthenticate` est `true`, `null` ou non-passé sinon.
  twoManRuleKey // `twoManRuleKey` est la clé stockée par votre serveur applicatif pour protéger l'identité de cet utilisateur
})

TIP

Pour une sécurité optimale, il est recommandé d'afficher en frontend le facteur d'authentification qui va être envoyée à SSKS avant d'appeler le saveIdentity, afin de s'assurer que le backend a bien créé l'utilisateur SSKS avec la bonne valeur, et qu'on va bien stocker l'identité de l'utilisateur associée à un facteur d'authentification qu'il maitrise.

Récupération de l'identité

Avant d'appeler la méthode retrieveIdentity exposée par le plugin @seald-io/sdk- plugin-ssks-2mr, il faut les mêmes éléments que pour la méthode saveIdentity décrite au paragraphe précédent.

Pour cela, on réutilise le même point getSSKSSession décrit à la partie précédente, sauf qu'il ne génère pas une nouvelle twoManRuleKey mais renvoie celle déjà générée :

  1. utilise le point d'API POST https://{SSKS_URL}/tmr/back/challenge_send/ pour envoyer à SSKS :
  • create_user : donner False, à cette étape l'utilisateur est déjà créé

  • user_id : correspond au userId dans @seald-io/sdk-plugin-ssks-2mr ;

  • auth_factor : un object contenant deux entrées:

    • type : le type de facteur d'authentification. 'EM' pour un email, ou 'SMS' pour un numéro de téléphone ;
    • value : l'adresse e-mail ou le numéro de téléphone de l'utilisateur où le challenge sera envoyé ;
  • force_auth : si vous voulez forcer l'authentification de l'utilisateur, même si il n'a jamais stocké d'identité avec ce auth_factor, c'est à dire forcer le serveur à mettre mustAuthenticate à true ;

  • subject : (dans le cas d'un email) la ligne d'objet de l'email à utiliser ;

  • template : le template d'email à utiliser au format HTML, ou le contenu du SMS, et qui doit contenir $$CHALLENGE$$ qui sera remplacé par le challenge.

    SSKS va envoyer un email ou un SMS au format de template au facteur d'authentification contenant challenge, valable 6h, et retourner au backend sessionId ;

  1. récupère la twoManRuleKey stockée associée à cet utilisateur (différent de la partie précédente) ;
  2. renvoie au frontend sessionId et twoManRuleKey.

Ensuite, le frontend peut utiliser la méthode retrieveIdentity de la façon suivante :

js
// On fait un appel API au serveur de l'application pour récupérer le `sessionId` et la `twoManRuleKey`
const { sessionId, twoManRuleKey } = APIClient.getSSKSSession() // point d'API à développer de votre côté

await seald.ssks2MR.retrieveIdentity({
  userId: 'myUserId',
  sessionId,
  // l'adresse e-mail de l'utilisateur doit être répétée pour éviter un MITM par votre serveur applicatif
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  challenge: 'XXXXXXXXX', // `challenge` envoyé au facteur d'authentification
  twoManRuleKey // `twoManRuleKey` est la clé stockée par votre serveur applicatif pour protéger l'identité de cet utilisateur
})
// On fait un appel API au serveur de l'application pour récupérer le `sessionId` et la `twoManRuleKey`
const { sessionId, twoManRuleKey } = APIClient.getSSKSSession() // point d'API à développer de votre côté

await seald.ssks2MR.retrieveIdentity({
  userId: 'myUserId',
  sessionId,
  // l'adresse e-mail de l'utilisateur doit être répétée pour éviter un MITM par votre serveur applicatif
  authFactor: {
    type: 'EM',
    value: 'user@domain.com'
  },
  challenge: 'XXXXXXXXX', // `challenge` envoyé au facteur d'authentification
  twoManRuleKey // `twoManRuleKey` est la clé stockée par votre serveur applicatif pour protéger l'identité de cet utilisateur
})

WARNING

Il est déconseillé de récupérer la même identité avec ssks2MR.retrieveIdentity sur plusieurs appareils en même temps, au même instant exact, par exemple lors de tests automatisés. Veuillez attendre que l'un des appareils ait fini de récupérer l'identité avant de lancer la récupération sur un autre appareil.

Environnement de test

En environnement de test, votre serveur peut, lors de l'appel à POST https://{SSKS_URL}/tmr/back/challenge_send/, rajouter un argument fake_otp: true. Ceci aura pour effet de ne pas envoyer réellement de challenge par email ou SMS. Le challenge sera alors fixé à 'aaaaaaaa'.

Vous pouvez utiliser ceci pour accélérer votre développement local, pour des tests automatisés, ...

WARNING

Faites bien attention de ne pas envoyer fake_otp: true en environnement de production. Le serveur vous répondrait avec une erreur 406 Not Acceptable.

Sécurité

La sécurité d'un tel protocole se fonde sur :

  1. le stockage sécurisé d'au moins un des morceaux de l'identité : contrairement à un stockage chez un tiers de confiance unique (coffre-fort numérique, KMS, Cloud HSM, ...), ce protocole est robuste à une compromission partielle. Il faudrait compromettre Backend et SSKS simultanément pour compromettre les identités des utilisateurs.
  2. l'indépendance des authentifications : il faut que SSKS et le Backend ne reposent pas sur les mêmes protocoles d'authentification. Si tous deux utilisent le même SSO par exemple, il suffit alors que le serveur de SSO devienne malveillant pour compromettre totalement la sécurité du protocole. Il en va de même pour les social logins.
  3. non-collusion des serveurs : il faut que SSKS et le Backend ne puissent pas avoir accès à l'autre secret, et surtout pas par API, sinon il suffit de compromettre un serveur et son secret pour obtenir l'autre secret.
  4. le "découpage" sécurisé de l'identité : le protocole choisi par Seald pour "découper" l'identité fait que connaître un "morceau" n'aide pas à accélérer une attaque par force brute, contrairement à un découpage naïf d'un string en deux.

Les limites de ce protocole sont les suivantes :

  1. si les deux protocoles utilisent les mêmes facteurs pour authentifier l'utilisateur (par exemple l'email), il suffit alors de compromettre le facteur de l'utilisateur pour compromettre l'identité (comme pour le SSO).
  2. si SSKS et Backend étaient compromis (ou perquisitionnés) simultanément, les identités des utilisateurs seraient reconstituées par l'attaquant.

WARNING

Ce protocole n'est pas strictement "de bout-en-bout", dans la mesure où l'identité de l'utilisateur peut être reconstituée sans son intervention ou l'intervention de ses appareils.

Personnalisation de l’émetteur du challenge

Email

Par défaut, les challenges emails sont envoyés depuis l'adresse no-reply@seald.io.

Il est possible de le changer par une adresse email provenant de votre nom de domaine. Pour cela, contactez-nous. L'opération nécessitera d'effectuer des opérations sur votre zone DNS.

SMS

Par défaut, les challenges emails sont envoyés depuis un émetteur SEALD.IO.

Il est possible de le changer par un émetteur au nom de votre entreprise. Pour cela, contactez-nous.

Exemples

Les fonctions à implémenter étant relativement simples, et leur implémentation variant considérablement d'une technologie de backend à une autre, nous ne fournissons pas de bibliothèque implémentant ces fonctions.

En revanche, cette partie regroupe plusieurs exemples d'implémentation dans plusieurs langages.

PHP (vanilla)

get_ssks_session.php :

php
<?php
// Retrieve user on database from session
$query = "SELECT * FROM users where id=:user_id";
$statement = $db->prepare($query);
$statement->execute(['user_id'=>$_SESSION['user_id']]);
$count = $statement->rowCount();
if($count == 0) die("Not connected");
$user = $statement->fetch(PDO::FETCH_OBJ);

// Make SSKS API call
// It will send an email to the user, and generate a SSKS Session
$curl = curl_init();
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt( $ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'X-SEALD-APPID: CHANGEME',  // To change
    'X-SEALD-APIKEY: CHANGEME',  // To change
));
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode(array(
    'create_user' => True,
    'user_id' => $user->id,
    'auth_factor' => array(
        'type' => 'EM'
        'value' => $user->email
        )
    'template' => '<html><body>Please confirm your identity. Code: $$CHALLENGE$$</body></html>'
)));
curl_setopt($curl, CURLOPT_URL, "https://SSKS_URL/tmr/back/challenge_send");
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$result = json_decode(curl_exec($curl));
curl_close($curl);

// Send the user SSKS information
print(json_encode(array([
    'session_id' => $result["session_id"],  // SSKS Session ID
    'must_authenticate' => $result["must_authenticate"],  // Whether or not a challenge was sent by email
    'user_id' => $user->id,  // Internal identifier of the user, it
                             // can be an id, a username, or anything unique for the user
                             
    'auth_factor' => array(
        'type' => 'EM'
        'value' => $user->email  // User's email address. It must be the same for every API call regarding
                                 // this user and cannot be changed
        )                     
    'two_man_rule_key' => $user->two_man_rule_key,  // This field must be a per-user random field
])));
?>
<?php
// Retrieve user on database from session
$query = "SELECT * FROM users where id=:user_id";
$statement = $db->prepare($query);
$statement->execute(['user_id'=>$_SESSION['user_id']]);
$count = $statement->rowCount();
if($count == 0) die("Not connected");
$user = $statement->fetch(PDO::FETCH_OBJ);

// Make SSKS API call
// It will send an email to the user, and generate a SSKS Session
$curl = curl_init();
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt( $ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'X-SEALD-APPID: CHANGEME',  // To change
    'X-SEALD-APIKEY: CHANGEME',  // To change
));
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode(array(
    'create_user' => True,
    'user_id' => $user->id,
    'auth_factor' => array(
        'type' => 'EM'
        'value' => $user->email
        )
    'template' => '<html><body>Please confirm your identity. Code: $$CHALLENGE$$</body></html>'
)));
curl_setopt($curl, CURLOPT_URL, "https://SSKS_URL/tmr/back/challenge_send");
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$result = json_decode(curl_exec($curl));
curl_close($curl);

// Send the user SSKS information
print(json_encode(array([
    'session_id' => $result["session_id"],  // SSKS Session ID
    'must_authenticate' => $result["must_authenticate"],  // Whether or not a challenge was sent by email
    'user_id' => $user->id,  // Internal identifier of the user, it
                             // can be an id, a username, or anything unique for the user
                             
    'auth_factor' => array(
        'type' => 'EM'
        'value' => $user->email  // User's email address. It must be the same for every API call regarding
                                 // this user and cannot be changed
        )                     
    'two_man_rule_key' => $user->two_man_rule_key,  // This field must be a per-user random field
])));
?>

Python (django)

views.py :

python
import requests
from django.http import JsonResponse


def get_ssks_session(request):
  # Retrieve user on database from session
  user = request.user
  
  # Make SSKS API call
  # It will send an SMS to the user, and generate a SSKS Session
  result = requests.post(
    "https://SSKS_URL/tmr/back/challenge_send",
    json={
      'create_user': True,
      'user_id': user.id,
      'auth_factor': {
        'type': 'EM',
        'value': user.email
      },
      'template': 'Please confirm your identity. Code: $$CHALLENGE$$'
    },
    headers={
      'X-SEALD-APPID': 'CHANGEME',  # To change
      'X-SEALD-APIKEY': 'CHANGEME'  # To change
    }
  ).json()
  
  # Send the user SSKS information
  data = {
    'session_id': result["session_id"],  # SSKS Session ID
    'must_authenticate': result["must_authenticate"],  # Whether or not a challenge was sent by SMS
    'user_id': user.id,  # Internal identifier of the user, it
                         # can be an id, a username, or anything unique for the user
    'auth_factor': {
      'type': 'EM',
      'value': user.email  # User's email address. It must be the same for every API call regarding
                           # this user and cannot be changed
    }
    'two_man_rule_key': get_two_man_rule_key(user),  # This must be a per-user random field
                                          # You can either add a field with `default=random` on User model
                                          # Or generate it yourself
  }
  return JsonResponse(data)
import requests
from django.http import JsonResponse


def get_ssks_session(request):
  # Retrieve user on database from session
  user = request.user
  
  # Make SSKS API call
  # It will send an SMS to the user, and generate a SSKS Session
  result = requests.post(
    "https://SSKS_URL/tmr/back/challenge_send",
    json={
      'create_user': True,
      'user_id': user.id,
      'auth_factor': {
        'type': 'EM',
        'value': user.email
      },
      'template': 'Please confirm your identity. Code: $$CHALLENGE$$'
    },
    headers={
      'X-SEALD-APPID': 'CHANGEME',  # To change
      'X-SEALD-APIKEY': 'CHANGEME'  # To change
    }
  ).json()
  
  # Send the user SSKS information
  data = {
    'session_id': result["session_id"],  # SSKS Session ID
    'must_authenticate': result["must_authenticate"],  # Whether or not a challenge was sent by SMS
    'user_id': user.id,  # Internal identifier of the user, it
                         # can be an id, a username, or anything unique for the user
    'auth_factor': {
      'type': 'EM',
      'value': user.email  # User's email address. It must be the same for every API call regarding
                           # this user and cannot be changed
    }
    'two_man_rule_key': get_two_man_rule_key(user),  # This must be a per-user random field
                                          # You can either add a field with `default=random` on User model
                                          # Or generate it yourself
  }
  return JsonResponse(data)