Integrate 2-man-rule on your backend
As explained in the identity management guide, protection with 2-man rule requires some modifications on your backend.
More precisely, two things need to be modified:
- the generation and storage of a secret
twoManRuleKey
(orrawTwoManRuleKey
) associated to a user of your application, which can be retrieved when authenticated; - an interaction with the Seald SDK Key Storage (SSKS) service so that the user can authenticate to SSKS.
We note:
- User: the user of the application whose identity is to be protected;
- Identity: a Seald cryptographic identity of the User (described in the dedicated guide);
- Frontend: the frontend of the application in which the SDK is integrated;
- Backend: the backend of the application in which the SDK is integrated, it will store
twoManRuleKey
, the key to decryptencryptedIdentity
; - SSKS: an instance of Seald SDK Key Storage, it will store
encryptedIdentity
, not decryptable without thetwoManRuleKey
. - Authentication Factor : The means of authentication used by the user on SSKS. Currently, email and SMS authentication are available ;
The goal is to give the User the two "pieces" of his Seald identity ( twoManRuleKey
and encryptedIdentity
) by authentication, while preventing SSKS and the Backend from being able to independently access the user's identity.
TIP
When protecting the user's identity in 2-man rule, you can use either the twoManRuleKey
argument, or the rawTwoManRuleKey
argument, to encrypt the identity stored on SSKS.
The two are very similar in both idea and usage, the only difference being that twoManRuleKey
can be of any format (but having sufficient randomness is still important), while rawTwoManRuleKey
must absolutely be the base64 encoding of a 64-byte cryptographically random buffer.
Technically, this avoids deriving the twoManRuleKey
with scrypt
, so using rawTwoManRuleKey
makes storing and retrieving the identity a bit faster, but twoManRuleKey
may be a bit easier to use.
For more details, see the dedicated paragraph in the guide identities guide.
In this guide, we will say twoManRuleKey
only, but to refer to one of the two.
Requirements
To communicate with the SSKS server, your Backend will need:
keyStorageURL
: the SSKS server URL, it can be retrieved from the administration dashboard;ssksAppId
: corresponds to theappId
used in the SDK;ssksAppKey
: allows your Backend to authenticate itself to the SSKS server.
This ssksAppKey
can be generated or renewed on the administration dashboard.
1. Go to the SSKS management page |
2. Click on the "Generate a key" button |
3. You can copy the generated key. Warning, you will not have access to it later! Save it now! |
Protocol
The protocol is carried out in two steps:
- initial identity protection, which normally occurs only once immediately after generation;
- identity recovery, which happens every time the User wants to recover his identity locally.
TIP
The protection of an identity in 2-man rule is usually coupled with a persistent local database to "cache" the identity of the User, and avoid the need to retrieve it each time he opens the application.
The protocol for performing the initial protection of a User's identity is as follows:
TIP
In the case where an identity has previously been stored onto SSKS with the same authentication factor, or if the request contained force_auth: true
, the SSKS server will then demand an authentication with a challenge, by sending mustAuthenticate
at true
. The user will then receive a challenge
valid for 6h by email or text message, and they will need to repeat this code into the front-end in order to be able to authenticate on SSKS before being able to store their new identity.
In the case where no identity has ever been stored on SSKS with this authentication factor, the server will instead respond with mustAuthenticate: false
(unless the request contained force_auth: true
). The user will then receive no challenge
, and can directly store their identity.
To retrieve the identity
of this User in another session, the protocol is as follows:
The red arrows in the diagram indicate the operations to be implemented in the application that uses the Seald SDK and the 2-man rule protection mode. The black arrows indicate what is already implemented by the SDK, the 2-man rule protection plugin @seald-io/sdk-plugin-ssks-2mr
or SSKS.
WARNING
Email addresses and phone numbers sent to the SSKS server must be normalized.
Identity protection
Before calling the saveIdentity
method exposed by the plugin @seald-io/sdk-plugin-ssks-2mr
, you need the following elements:
userId
: a unique identifier of the user in the application;authFactor
: user's email address or phone number, used for challenge authentication, must be repeated or verified by the user to ensure that the backend is not trying to perform an MITM attack;twoManRuleKey
: the key known by the backend allowing to encrypt / decrypt the identity;sessionId
: the session identifier generated by SSKS and transmitted by the backend to the SDK;- if the server sent
mustAuthenticate
attrue
,challenge
: a random token sent by SSKS by email or text message to the user to authenticate them, valid for 6h, never to be revealed to the backend;
To do this, we need to implement an authenticated getSSKSSession
API point on the backend that:
- uses the API endpoint
POST https://{SSKS_URL}/tmr/back/challenge_send/
to send to SSKS:
create_user
: set it attrue
to create the user on SSKS in the same request, otherwise you'll need to make another request before ;user_id
: is theuserId
in@seald-io/sdk-plugin-ssks-2mr
;auth_factor
:type
: Authentication factor type. 'EM' for e-mail, or 'SMS' for phone number;value
: the user's e-mail address or phone number to which thechallenge
will be sent;
force_auth
: if you want to force authentication of the user, even if they never stored an identity with thisauth_factor
, that is force the server to setmustAuthenticate
totrue
;subject
: (in the case of email) the subject line of the email to use;template_id
: the ID of the template to be used, defined on the dashboard.SSKS will return to the backend
sessionId
andmustAuthenticate
.If
mustAuthenticate
istrue
, SSKS will send a message following the template oftemplate_id
to theauth_factor
containingchallenge
, valid for 6h;
- generates
twoManRuleKey
(64 cryptographically robust random bytes) and stores it associated with this user;
WARNING
If you want to use a rawTwoManRuleKey
, it must be the base64 encoding of a cryptographically random 64 byte buffer.
If you use a simple twoManRuleKey
, the format is free, but it must still contain enough randomness.
- returns to the frontend
sessionId
andtwoManRuleKey
.
TIP
In the case where mustAuthenticate
is true
, the frontend must block and wait for the user to enter the challenge which was sent to them before calling the method saveIdentity
.
In the case where mustAuthenticate
is false
, the frontend can call the method saveIdentity
directly.
Then the frontend can use the method saveIdentity
as follows:
// We make an API call to the application server to get the `sessionId` and the `twoManRuleKey`.
const { sessionId, twoManRuleKey } = APIClient.getSSKSSession() // API endpoint to develop
await seald.ssks2MR.saveIdentity({
userId: 'myUserId',
sessionId,
// the user's email address or phone number must be repeated to prevent MITM by your application server
auth_factor: {
type: 'EM',
value: 'user@domain.com'
},
challenge: 'XXXXXXXXX', // `challenge` sent via message if `mustAuthenticate` is `true`, otherwise `null` or omitted
twoManRuleKey // `twoManRuleKey` is the key stored by your application server to secure this user's identity
})
TIP
For optimal security, it is recommended to display in frontend the authentication factor value that is about to be sent to SSKS before calling saveIdentity
, in order to make sure the backend has indeed created the SSKS user with the correct value, and that we are about to store the user's identity linked to an authentication factor they control.
Identity retrieval
Before calling the retrieveIdentity
method exposed by the @seald-io/sdk- plugin-ssks-2mr
plugin, you need the same elements as for the saveIdentity
method described in the previous paragraph.
To do this, we reuse the same getSSKSSession
point described in the previous section, except that it does not generate a new twoManRuleKey
but returns the one already generated:
- uses the API endpoint
POST https://{SSKS_URL}/tmr/back/challenge_send/
to send to SSKS:
create_user
: set it atFalse
, at this step, the user should already be created on SSKS;user_id
: is theuserId
in@seald-io/sdk-plugin-ssks-2mr
;auth_factor
:type
: Authentication factor type. 'EM' for e-mail, or 'SMS' for phone number;value
: the user's e-mail address or phone number to which thechallenge
will be sent;
template_id
: the ID of the template to be used, defined on the dashboard.SSKS will send a message following the template of
template_id
to theauth_factor
containingchallenge
, valid for 6h, and return to the backendsessionId
;
- retrieves the
twoManRuleKey
associated with this user; - returns to the frontend
sessionId
andtwoManRuleKey
.
Then the frontend can use the method retrieveIdentity
as follows:
// We make an API call to the application server to get the `sessionId` and the `twoManRuleKey`.
const { sessionId, twoManRuleKey } = APIClient.getSSKSSession() // API endpoint to develop
await seald.ssks2MR.retrieveIdentity({
userId: 'myUserId',
sessionId,
// the user's email address or phone number must be repeated to prevent MITM by your application server
auth_factor: {
type: 'EM',
value: 'user@domain.com'
},
challenge: 'XXXXXXXXX', // `challenge` sent via the auth_factor
twoManRuleKey // `twoManRuleKey` is the key stored by your application server to secure this user's identity
})
WARNING
It is recommended not to retrieve the same identity with ssks2MR.retrieveIdentity
on multiple devices at the same time, at the same exact instant, for example during automated tests. Please wait until one of the devices has finished retrieving the identity before starting the retrieval on another device.
Test environment
In a test environment, your server can, when calling POST https://{SSKS_URL}/tmr/back/challenge_send/
, add a fake_otp: true
argument. This will have the effect of not actually sending a challenge by email or SMS. The challenge will then be set to 'aaaaaaaa'
.
You can use this to speed up your local development, for automated tests, ...
WARNING
Be careful not to send fake_otp: true
in a production environment. The server would respond with a 406 Not Acceptable
error.
Security
The security of such a protocol is based on:
- Secure storage of at least one piece of the identity: unlike storage at a single trusted third party (digital safe, KMS, Cloud HSM, ...), this protocol is robust to partial breach. One would have to breach both the Backend and SSKS simultaneously to breach the users' identities.
- Authentication independence: SSKS and the Backend must not rely on the same authentication protocols. If both use the same SSO for example, then only the SSO server has to be malicious to completely breach the security of the protocol. The same applies to social logins.
- Non-collusion of servers: it is necessary that SSKS and the Backend cannot have access to the other secret, and especially not by API, otherwise breaching one server would be enough to get both secrets.
- the secure "splitting" of the identity: the protocol chosen by Seald to "split" the identity ensures that knowing a "piece" does not accelerate a brute force attack, contrary to a naive slicing of a string in two.
The limitations of this protocol are:
- if both protocols use the same factors to authenticate the user (e.g., email), then breaching the user's factors is sufficient to breach the identity (as with SSO).
- If SSKS and the Backend were breached (or subpoenaed) simultaneously, the users' identities could be reconstructed by the attacker.
WARNING
This protocol is not strictly "end-to-end", in the sense that the users' identities can be reconstructed without their intervention or the intervention of their devices.
Customizing email and SMS templates
Defining a template
You can define email and SMS templates on your dashboard.
1. Go to the SSKS management page |
2. Click on the "Create a template" button in the section corresponding to emails or SMS (here emails) |
3. Fill out the fields required to create a template, not forgetting to put $$CHALLENGE$$ in the field corresponding to the template.You can also add extra template arguments, the following way: $$ARG_NAME$$ . The argument names ARG_NAME may only contain uppercase letters, numbers, and underscores (A-Z0-9_ ), limited to 32 characters. You may use at most 10 extra arguments. |
4. The template created now appears in the list. In production environment, it is in "Awaiting validation" status after being created, and you must wait for our team to validated it (up to 2 business days). In staging environment, it is directly "Validated". |
5. Once the template is "Validated", you can copy its ID, and use it as the template_id argument when calling POST https://{SSKS_URL}/tmr/back/challenge_send/ . |
Customizing the email sender address
By default, email challenges are sent from no-reply@seald.io
.
It is possible to change the sender to an email address that belongs to your own domain name. In order to do this, contact us.
Actions on your DNS zone will be required.
Customizing the SMS sender
By default, SMS challenges are sent from SEALD.IO
.
It is possible to change the sender with a name including your company name. In order to do this, contact us.
Examples
The functions to be implemented being relatively simple, and their implementation varying considerably from one backend technology to another, we do not provide a library implementing these functions.
On the other hand, this part gathers several examples of implementation in several languages.
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_id' => '00000000-0000-0000-0000-000000000000',
'template_extra_params' => array(
'USER_FIRST_NAME' => 'firstname',
'USER_LAST_NAME' => 'lastname',
),
)));
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_id': '00000000-0000-0000-0000-000000000000',
'template_extra_args': {
'USER_FIRST_NAME': 'firstname',
'USER_LAST_NAME': 'lastname'
}
},
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 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': {
'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)