Skip to content

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 (or rawTwoManRuleKey) 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 decrypt encryptedIdentity;
  • SSKS: an instance of Seald SDK Key Storage, it will store encryptedIdentity, not decryptable without the twoManRuleKey.
  • 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 the appId 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.

Go to the SDK management page
1. Go to the SDK management page
Generating the key
2. In the "SSKS-2MR backend key" section, click on the "Generate a key" button
Key generated
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:

  1. initial identity protection, which normally occurs only once immediately after generation;
  2. 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:

uml diagram

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:

uml diagram

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 at true, 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:

  1. uses the API endpoint POST https://{SSKS_URL}/tmr/back/challenge_send/ to send to SSKS:
  • create_user: set it at true to create the user on SSKS in the same request, otherwise you'll need to make another request before ;

  • user_id: is the userId 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 the challenge will be sent;
  • force_auth: if you want to force authentication of the user, even if they never stored an identity with this auth_factor, that is force the server to set mustAuthenticate to true;

  • subject: (in the case of email) the subject line of the email to use;

  • template: the email template to be used in HTML format for email auth_factor, or the message content for SMS ones. Must contain $$CHALLENGE$$ which will be replaced by the challenge.

    SSKS will return to the backend sessionId and mustAuthenticate.

    If mustAuthenticate is true, SSKS will send a message in template format to the auth_factor containing challenge, valid for 6h;

  1. 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.

  1. returns to the frontend sessionId and twoManRuleKey.

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:

js
// 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
})
// 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:

  1. uses the API endpoint POST https://{SSKS_URL}/tmr/back/challenge_send/ to send to SSKS:
  • create_user: set it at False, at this step, the user should already be created on SSKS;

  • user_id: is the userId 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 the challenge will be sent;
  • template: the email template to be used in HTML format for email auth_factor, or the message content for SMS ones. Must contain $$CHALLENGE$$ which will be replaced by the challenge.

    SSKS will send a message in template format to the auth_factor containing challenge, valid for 6h, and return the backend sessionId;

  1. retrieves the twoManRuleKey associated with this user;
  2. returns to the frontend sessionId and twoManRuleKey.

Then the frontend can use the method retrieveIdentity as follows:

js
// 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
})
// 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

  1. 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).
  2. 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.

Customising the sender of the challenge

Email

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.

SMS

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
<?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 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)
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 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)