7.7.1 Общая информация

Система аутентификации, встроенная в JC-WebClient необходима для последующего создания защищенного соединения и собственно аутентификации. Она может быть заменена на любую аналогичную систему или не использоваться вовсе. В предложенном варианте она основана на протоколе Handshake и поддерживает два вида аутентификации: взаимную и одностороннюю.

Важно: Система аутентификации поддерживается только для токенов GOST 2 и PKI.

Для успешного взаимодействия клиента с сервером при аутентификации и защищённой передаче данных важно, чтобы и на стороне клиента, и в криптографическом компоненте сервера использовались одинаковые наборы параметров эллиптических кривых согласно RFC 4357 и методическим рекомендациям ТК26. В частности (только для токена GOST 2), в зависимости от алгоритма ключа:

  • ГОСТ Р 34.10-2001:
    • id-GostR3410-2001-CryptoPro-A-ParamSet
    • id-GostR3410-2001-CryptoPro-B-ParamSet
    • id-GostR3410-2001-CryptoPro-C-ParamSet
    • id-GostR3410-2001-CryptoPro-XchA-ParamSet
    • id-GostR3410-2001-CryptoPro-XchB-ParamSet
  • ГОСТ Р 34.10-2012 с длиной ключа 256:
    • id-GostR3410-2001-CryptoPro-A-ParamSet
    • id-GostR3410-2001-CryptoPro-B-ParamSet
    • id-GostR3410-2001-CryptoPro-C-ParamSet
    • id-GostR3410-2001-CryptoPro-XchA-ParamSet
    • id-GostR3410-2001-CryptoPro-XchB-ParamSet
    • id-tc26-gost-3410-12-256-paramSetA
    • id-tc26-gost-3410-12-256-paramSetB
    • id-tc26-gost-3410-12-256-paramSetC
    • id-tc26-gost-3410-12-256-paramSetD
  • ГОСТ Р 34.10-2012 с длиной ключа 512 (для GOST 2 версии 2.5.13):
    • id-tc26-gost-3410-12-512-paramSetA
    • id-tc26-gost-3410-12-512-paramSetB
    • id-tc26-gost-3410-12-512-paramSetC

В рамках пакета SDK поставляется готовая реализация аутентификации на стороне сервера в виде библиотеки sslServer.dll, но ее рекомендуется использовать не в качестве готового решения, а в качестве примера исходного кода. Данный раздел описывает основные принципы устройства подобной системы, которыми можно руководствоваться при создании системы аутентификации для каждого конкретного Web-приложения.

Общие принципы реализации системы аутентификации

  1. На стороне клиента используйте команды начала и продолжения сеанса: establishSChannelBegin и establishSChannelContinue для взаимной аутентификации, и unilateralAuthenticationBegin и unilateralAuthenticationContinue для односторонней соответственно.
  2. На стороне сервера необходимо реализовать функции, которые с помощью криптографического компонента преобразовывали бы полученные от клиента данные. Они необходимы для верификации подписи случайного числа вне зависимости от вида аутентификации, а также для генерации ключа согласования при взаимной.

В подразделах ниже будут рассмотрены основные особенности каждого из видов аутентификации.

7.7.2 Взаимная аутентификация

Взаимная аутентификация предполагает обмен сертификатами между клиентом и сервером и создание на их основе ключа согласования (Протокол Диффи—Хеллмана). Взаимодействие между клиентом и сервером происходит по следующей схеме с использованием команд establishSChannelBegin и establishSChannelContinue:

  1. Устанавливаются параметры безопасности, включая версию протокола, идентификатор сессии и метод компрессии. Клиент и сервер обмениваются случайными числами (client_random и server_random).
  2. Далее происходит следующее:
      • Производится обмен сертификатами для взаимной аутентификации клиента и сервера с заданными параметрами.
      • Клиент генерирует случайную величину pre_master secret, шифрует ее и передает серверу.
      • Клиент и сервер по pre_master secret, client_random и server_random формируют master secret сессии.

Пример реализации взаимной аутентификации на стороне клиента

// Идентификатор токена
var tokenID = 0;
 
// Идентификатор контейнера
var contID = 0;
 
try {
// Прочитать открытый ключ
var publicKey = JCWebClient2.readPublicKey({
args: {
tokenID: tokenID,
contID: contID
}
});
 
// Если было выполнено предъявление PIN-кода, отменить авторизацию
if (JCWebClient2.getLoggedInState().state != JCWebClient2.Vars.AuthState.notBinded) {
JCWebClient2.unbindToken();
}
 
// Предъявить PIN-код
JCWebClient2.bindToken({
args: {
tokenID: tokenID,
pin: "my pin"
}
});
 
 
// Начать установку защищенного соединения
var clientData = JCWebClient2.establishSChannelBegin({
args: {
contID: contID
}
});
 
// Данные для отправки на сервер
clientData = { clientData: clientData };
 
// Сериализовать в JSON
clientData = $.JSON.encode(clientData);
 
// Оправить данные на сервер
$.ajax({
async: false,
type: "POST",
url: "WebService.asmx/loginBegin",
data: clientData,
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (msg) {
dataFromServer = msg.d.serverData;
connectionID = msg.d.id;
ajaxError = false;
},
err: function (a, b, c) {
alert(a + ';' + b + ';' + c);
ajaxError = true;
}
});
 
if (ajaxError == true) {
throw "";
}
 
// Если защищенный канал еще не установлен
while (JCWebClient2.getLoggedInState().state != JCWebClient2.Vars.AuthState.secureChannelEstablished) {
 
// Продолжить установку канала
clientData = JCWebClient2.establishSChannelContinue({
args: {
connectionID: connectionID,
serverData: dataFromServer
}
});
 
// Если защищенный канал установлен выйти из цикла
if (JCWebClient2.getLoggedInState().state == JCWebClient2.Vars.AuthState.secureChannelEstablished) {
break;
}
 
// Данные для отправки на сервер
clientData = { clientData: clientData, connectionID: connectionID };
 
// Сериализовать в JSON
clientData = $.JSON.encode(clientData);
 
// Оправить данные на сервер
$.ajax({
async: false,
type: "POST",
url: "WebService.asmx/loginContinue",
data: clientData,
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (msg) {
dataFromServer = msg.d.serverData;
ajaxError = false;
},
err: function (a, b, c) {
alert(a + ';' + b + ';' + c);
ajaxError = true;
}
});
 
if (ajaxError == true) {
throw "";
}
}
}
catch (error) {
console.log("Произошла ошибка: " + error.message);
return;
}
 
// Перейти на страницу пользователя
window.location = "./userHome.aspx"

Пример реализации взаимной аутентификации на стороне сервера (C#)

// Начать аутентификацию пользователя.
// Обработка данных, полученных от establishSChannelBegin
[WebMethod()]
public LoginBeginReturnVal loginBegin(byte[] clientData)
{
// Получить случайный идентификатор соединения
Random rng = new Random();
int id = rng.Next();
string serverType = WebConfigurationManager.AppSettings["serverType"];
 
// Создать объект SSL-соединения
SSLServer server;
if (serverType == "OSSL")
{
serverCertFileName = String.Format(@"{0}App_Data\server.pem",
Server.MapPath("~"));
server = new SSLServer(serverCertFileName,
serverCertPassword,
SSLServer.osslServer);
}
else if (serverType == "SSPI")
{
server = new SSLServer(serverCertSubjectName, null, SSLServer.sspiServer);
}
else
{
throw new System.Exception("Invalid server configuration");
}
 
// Данные для клиента для установки защищенного соединения
byte[] serverData;
 
// Начать установку защищенного соединения
server.accept(clientData, out serverData);
 
// Создать объект соединения с клиентом
ConnectionState state = new ConnectionState(server);
 
// Сохранить созданный объект
connectionStates.Add(id, state);
 
// Сформировать выходной объект
LoginBeginReturnVal retVal = new LoginBeginReturnVal();
retVal.id = id;
retVal.serverData = serverData;
return retVal;
}
 
// Продолжить аутентификацию пользователя
// Обработка данных, полученных от establishSChannelContinue
[WebMethod()]
public LoginContinueReturnVal loginContinue(int connectionID, byte[] clientData)
{
// Получить объект соединения
ConnectionState state = getConnection(connectionID);
// Получить объект SSL-сервера
SSLServer server = state.server;
 
// Проверить не произведена ли уже аутентификация
if (state.userLoggedIn == true)
{
throw new System.Exception("Already logged in");
}
 
// Данные для клиента для установки защищенного соединения
byte[] serverData;
 
// Продолжить установку защищенного соединения
int res = server.accept(clientData, out serverData);
 
// Если защищенный канал установлен
if (res == SSLServer.tlsErrorSuccess)
{
// Получить открытый ключ клиента
byte[] pubKey = server.getPeerPublicKey();
 
// Преобразовать в base64
string encodedPubKey = Convert.ToBase64String(pubKey,
Base64FormattingOptions.None);
 
// SQL соединение
SqliteConnection conn;
// Запрос SQL
SqliteCommand cmd;
SqliteDataReader rdr;
 
// Соединение с базой Database из web.config
conn = new SqliteConnection(
DemoBank2.Global.GetConnectionString(Server.MapPath("~")));
 
// Получить имя пользователя из таблицы UserData, соответствующее открытому
// ключу
cmd = new SqliteCommand(" SELECT UserName FROM UserData WHERE PublicKey='" +
encodedPubKey +
"'", conn);
cmd.CommandType = CommandType.Text;
 
using (conn)
{
conn.Open();
rdr = cmd.ExecuteReader();
// Удалось ли прочитать имя пользователя
if (rdr.Read())
{
// Записать имя пользователя в объект соединения
state.userName = (String)rdr["UserName"];
// Аутентификация выполнена
state.userLoggedIn = true;
}
else
{
// Пользователь с таким открытым ключом не зарегистрирован
throw new System.Exception("Connection failed");
}
}
}
 
// Сформировать выходной объект
LoginContinueReturnVal retVal = new LoginContinueReturnVal();
retVal.serverData = serverData;
return retVal;
}

7.7.3 Односторонняя аутентификация

В данном случае односторонняя аутентификация – это аутентификация клиента сервером. Взаимодействие между клиентом и сервером происходит по следующей схеме с использованием команд unilateralAuthenticationBegin и unilateralAuthenticationContinue:

  1. Устанавливаются параметры безопасности, включая версию протокола, идентификатор сессии, метод компрессии и начальные случайные числа.
  2. Сервер посылает запрос на сертификат и случайное число. Клиент отправляет в ответ сертификат и подписанное случайное число сервера.

Пример реализации односторонней аутентификации на стороне клиента

// Идентификатор токена
var tokenID = 0;
 
// Идентификатор контейнера
var contID = 0;
 
try {
// Если было выполнено предъявление PIN-кода, отменить авторизацию
if (JCWebClient2.getLoggedInState().state != JCWebClient2.Vars.AuthState.notBinded) {
JCWebClient2.unbindToken();
}
 
// Предъявить PIN-код
JCWebClient2.bindToken({
args: {
tokenID: tokenID,
pin: "my pin"
}
});
 
// Начать одностороннюю аутентификацию
var clientData = JCWebClient2.unilateralAuthenticationBegin({
args: {
contID: contID
}
});
 
// Данные для отправки на сервер
clientData = { clientData: clientData };
 
// Сериализовать в JSON
clientData = $.JSON.encode(clientData);
 
// Оправить данные на сервер
$.ajax({
async: false,
type: "POST",
url: "WebService.asmx/unilateralAuthenticationBegin",
data: clientData,
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (msg) {
dataFromServer = msg.d.serverData;
connectionID = msg.d.id;
ajaxError = false;
},
err: function (a, b, c) {
alert(a + ';' + b + ';' + c);
ajaxError = true;
}
});
if (ajaxError == true) {
throw "";
}
 
// Если аутентификация не пройдена
while (JCWebClient2.getLoggedInState().state != JCWebClient2.Vars.AuthState.unilateralAuthenticationComplete) {
 
// Продолжить аутентификацию
clientData = JCWebClient2.unilateralAuthenticationContinue({
args: {
connectionID: connectionID,
serverData: dataFromServer
}
});
 
// Если аутентификацию прошла успешно выйти из цикла
if (JCWebClient2.getLoggedInState().state == JCWebClient2.Vars.AuthState.unilateralAuthenticationComplete) {
break;
}
// Данные для отправки на сервер
clientData = { clientData: clientData, connectionID: connectionID };
 
// Сериализовать в JSON
clientData = $.JSON.encode(clientData);
 
// Оправить данные на сервер
$.ajax({
async: false,
type: "POST",
url: "WebService.asmx/unilateralAuthenticationContinue",
data: clientData,
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (msg) {
dataFromServer = msg.d.serverData;
ajaxError = false;
},
err: function (a, b, c) {
alert(a + ';' + b + ';' + c);
ajaxError = true;
}
});
if (ajaxError == true) {
throw "";
}
}
}
catch (error) {
console.log("Произошла ошибка: " + error.message);
return;
}
 
// Перейти на страницу пользователя
window.location = "./userHome.aspx";

Пример реализации односторонней аутентификации на стороне сервера (C#)

// Начать одностороннюю аутентификацию пользователя.
// Обработка данных, полученных от unilateralAuthenticationBegin
[WebMethod()]
public LoginBeginReturnVal unilateralAuthenticationBegin(byte[] clientData)
{
// Получить случайный идентификатор соединения
Random rng = new Random();
int id = rng.Next();
 
string serverType = WebConfigurationManager.AppSettings["serverType"];
 
// Создать объект SSL-соединения
SSLServer server;
if (serverType == "OSSL")
{
serverCertFileName = String.Format(@"{0}App_Data\server.pem",
Server.MapPath("~"));
server = new SSLServer(serverCertFileName,
serverCertPassword,
SSLServer.osslServer);
}
else if (serverType == "SSPI")
{
server = new SSLServer(serverCertSubjectName, null, SSLServer.sspiServer);
}
else
{
throw new System.Exception("Invalid server configuration");
}
 
// Данные для клиента для установки защищенного соединения
byte[] serverData;
 
// Начать установку защищенного соединения
server.accept(clientData, out serverData);
 
// Создать объект соединения с клиентом
ConnectionState state = new ConnectionState(server);
 
// Сохранить созданный объект
connectionStates.Add(id, state);
 
// Сформировать выходной объект
LoginBeginReturnVal retVal = new LoginBeginReturnVal();
retVal.id = id;
retVal.serverData = serverData;
return retVal;
}
 
// Продолжить одностороннюю аутентификацию пользователя.
// Обработка данных, полученных от unilateralAuthenticationContinue
[WebMethod()]
public LoginContinueReturnVal unilateralAuthenticationContinue(int connectionID,
byte[] clientData)
{
// Получить объект соединения
ConnectionState state = getConnection(connectionID);
// Получить объект SSL-сервера
SSLServer server = state.server;
 
// Данные для клиента для установки защищенного соединения
byte[] serverData;
 
// Продолжить установку защищенного соединения
int res = server.accept(clientData, out serverData);
 
// Если аутентификация пройдена
if (res == SSLServer.tlsErrorSuccess)
{
// Получить открытый ключ клиента
byte[] pubKey = server.getPeerPublicKey();
 
// Преобразовать в base64
string encodedPubKey = Convert.ToBase64String(pubKey,
Base64FormattingOptions.None);
 
// SQL соединение
SqliteConnection conn;
// Запрос SQL
SqliteCommand cmd;
SqliteDataReader rdr;
 
// Соединение с базой Database из web.config
conn = new SqliteConnection(
DemoBank2.Global.GetConnectionString(Server.MapPath("~")));
 
// Получить имя пользователя из таблицы UserData, соответствующее открытому
// ключу
cmd = new SqliteCommand(" SELECT UserName FROM UserData WHERE PublicKey='" +
encodedPubKey +
"'", conn);
cmd.CommandType = CommandType.Text;
 
using (conn)
{
conn.Open();
rdr = cmd.ExecuteReader();
// Удалось ли прочитать имя пользователя
if (rdr.Read())
{
// Записать имя пользователя в объект соединения
state.userName = (String)rdr["UserName"];
// Аутентификация выполнена
state.userLoggedIn = true;
}
else
{
// Пользователь с таким открытым ключом не зарегистрирован
throw new System.Exception("Connection failed");
}
}
}
 
// Сформировать выходной объект
LoginContinueReturnVal retVal = new LoginContinueReturnVal();
retVal.serverData = serverData;
return retVal;
}