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
(ourawTwoManRuleKey
) 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échiffrerencryptedIdentity
; - SSKS : une instance de Seald SDK Key Storage, il stockera
encryptedIdentity
non déchiffrable sans latwoManRuleKey
; - 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 auappId
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.
1. Aller sur la page de gestion du SDK |
2. Dans la partie "Clé serveur SSKS-2MR", cliquer sur le bouton "Générer la clé" |
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 :
- la protection initiale de l'identité qui n'arrive normalement qu'une seule fois immédiatement après sa génération ;
- 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 :
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 :
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 :
- utilise le point d'API
POST https://{SSKS_URL}/tmr/back/challenge_send/
pour envoyer à SSKS :
create_user
: donnertrue
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 auuserId
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ù lechallenge
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 lechallenge
.SSKS va retourner au backend
sessionId
etmustAuthenticate
.Si
mustAuthenticate
esttrue
, SSKS va envoyer un email ou un SMS au format detemplate
au facteur d'authentification contenantchallenge
, valable 6h ;
- 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.
- renvoie au frontend
sessionId
,mustAuthenticate
, ettwoManRuleKey
.
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 :
// 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 :
- utilise le point d'API
POST https://{SSKS_URL}/tmr/back/challenge_send/
pour envoyer à SSKS :
create_user
: donnerFalse
, à cette étape l'utilisateur est déjà crééuser_id
: correspond auuserId
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ù lechallenge
sera envoyé ;
force_auth
: si vous voulez forcer l'authentification de l'utilisateur, même si il n'a jamais stocké d'identité avec ceauth_factor
, c'est à dire forcer le serveur à mettremustAuthenticate
à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 lechallenge
.SSKS va envoyer un email ou un SMS au format de
template
au facteur d'authentification contenantchallenge
, valable 6h, et retourner au backendsessionId
;
- récupère la
twoManRuleKey
stockée associée à cet utilisateur (différent de la partie précédente) ; - renvoie au frontend
sessionId
ettwoManRuleKey
.
Ensuite, le frontend peut utiliser la méthode retrieveIdentity
de la façon suivante :
// 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 :
- 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.
- 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.
- 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.
- 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 :
- 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).
- 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
// 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
:
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)