Аутентификация на Web-сервере

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

Важно

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

Примечание

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

  • 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

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

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

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

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

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

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

digraph foo {
  n1 [label="1", shape=none];
  n2 [label="2", shape=none];
  l1 [label="Клиент", shape=none];
  r1 [label="Сервер", shape=none];
  l2 [label="establishSChannelBegin()", shape=rect];
  r2 [label="LoginBeginReturnVal()", shape=rect];
  l3 [label="establishSChannelContinue()", shape=rect];
  r3 [label="LoginContinueReturnVal()", shape=rect];
   l1 -> r1 [style=invis];
   l1 -> l2 [dir=none]
   r1 -> r2 [dir=none]
   l2 -> r2 [label="hello клиента"];
   r2 -> l2 [label="hello сервера"];
   l2 -> l3 [dir=none]
   r2 -> r3 [dir=none]
   l3 -> r3 [label="данные сервера",dir=back]
   l3 -> r3 [label="данные клиента"]
   {rank=same; l1 r1};
   {rank=same; l2 r2 n1};
   {rank=same; l3 r3 n2};
}

  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;
}

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

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

digraph foo {
  n1 [label="1", shape=none];
  n2 [label="2", shape=none];
  l1 [label="Клиент", shape=none];
  r1 [label="Сервер", shape=none];
  l2 [label="unilateralAuthenticationBegin()", shape=rect];
  r2 [label="LoginBeginReturnVal()", shape=rect];
  l3 [label="unilateralAuthenticationContinue()", shape=rect];
  r3 [label="LoginContinueReturnVal()", shape=rect];
   l1 -> r1 [style=invis];
   l1 -> l2 [dir=none]
   r1 -> r2 [dir=none]
   l2 -> r2 [label="hello клиента"];
   r2 -> l2 [label="hello сервера"];
   l2 -> l3 [dir=none]
   r2 -> r3 [dir=none]
   l3 -> r3 [label="данные сервера",dir=back]
   l3 -> r3 [label="данные клиента"]
   {rank=same; l1 r1};
   {rank=same; l2 r2 n1};
   {rank=same; l3 r3 n2};
}

  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;
}