Аутентификация

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

Примечание

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

  • 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 сессии.

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

// Аутентификация пользователя
$("#loginButton").click(function () {

    // Если не выбран сертификат, выдать ошибку
    if ($("#certSelect").attr("selectedIndex") == -1) {
        jAlert("Выберите сертификат");
        return;
    }

    // Получить объект, ассоциированный с элементом списка сертификатов
    var certHandle = $.data($("#certSelect option:selected")[0], "jcWebClientData");

    try {
        // Прочитать открытый ключ (пример вызова функции)
        var publicKey = JCWebClient().readPublicKey(certHandle.tokenID,
                                                    certHandle.certID);

        // Если было выполнено предъявление PIN-кода, отменить авторизацию
        if (JCWebClient().getLoggedInState()[0] != STATE_NOT_BINDED) {
            JCWebClient().unbindToken();
        }

        // Предъявить PIN-код
        JCWebClient().bindToken(certHandle.tokenID, $('#pinInput').val());

        // Начать установку защищенного соединения
        var clientData = JCWebClient().establishSChannelBegin(certHandle.certID);

        // Данные для отправки на сервер
        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 (JCWebClient().getLoggedInState()[0] != STATE_SECURE_CHANNEL_ESTABLISHED)
        {

            // Продолжить установку канала
            clientData = JCWebClient().establishSChannelContinue(dataFromServer,
                                                                 connectionID);

            // Если  защищенный канал установлен выйти из цикла
            if (JCWebClient().getLoggedInState()[0] == STATE_SECURE_CHANNEL_ESTABLISHED)
            {
                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) {
        jAlert(JCWebClient().getErrorMessage(JCWebClient().getLastError()));
        //jAlert("Произошла ошибка" + error);
        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. Сервер посылает запрос на сертификат и случайное число. Клиент отправляет в ответ сертификат и подписанное случайное число сервера.

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

// Односторонняя аутентификация пользователя
$("#uniauthButton").click(function () {

    // Если не выбран сертификат, выдать ошибку
    if ($("#uniauthCertSelect").attr("selectedIndex") == -1) {
        jAlert("Выберите сертификат");
        return;
    }
    // Получить объект, ассоциированный с элементом списка сертификатов
    var certHandle = $.data($("#uniauthCertSelect option:selected")[0],
                              "jcWebClientData");

    try {
        // Если было выполнено предъявление PIN-кода, отменить авторизацию
        if (JCWebClient().getLoggedInState()[0] != STATE_NOT_BINDED) {
            JCWebClient().unbindToken();
        }
        // Предъявить PIN-код
        JCWebClient().bindToken(certHandle.tokenID, $('#uniauthPinInput').val());

        // Начать одностороннюю аутентификацию
        var clientData = JCWebClient().unilateralAuthenticationBegin(certHandle.certID);

        // Данные для отправки на сервер
        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 (JCWebClient().getLoggedInState()[0] !=
               UNILATERAL_AUTHENTICATION_COMPLETE) {

            // Продолжить аутентификацию
            clientData = JCWebClient().unilateralAuthenticationContinue(dataFromServer,
                                                                        connectionID);
            // Если аутентификацию прошла успешно выйти из цикла
            if (JCWebClient().getLoggedInState()[0] ==
                UNILATERAL_AUTHENTICATION_COMPLETE) {
                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) {
        jAlert(JCWebClient().getErrorMessage(JCWebClient().getLastError()));
        //jAlert("Произошла ошибка" + error);
        return;
    }
    jAlert("Аутентификация прошла успешно");
    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;
}