7.7.1 Общая информация
Система аутентификации, встроенная в JC-WebClient необходима для последующего создания защищенного соединения и собственно аутентификации. Она может быть заменена на любую аналогичную систему или не использоваться вовсе. В предложенном варианте она основана на протоколе Handshake и поддерживает два вида аутентификации: взаимную и одностороннюю.
Важно: Система аутентификации поддерживается только для токенов GOST 2 и PKI.
Для успешного взаимодействия клиента с сервером при аутентификации и защищённой передаче данных важно, чтобы и на стороне клиента, и в криптографическом компоненте сервера использовались одинаковые наборы параметров эллиптических кривых согласно RFC 4357 и методическим рекомендациям ТК26. В частности (только для токена GOST 2), в зависимости от алгоритма ключа:
- ГОСТ Р 34.10-2001:
id-GostR3410-2001-CryptoPro-A-ParamSetid-GostR3410-2001-CryptoPro-B-ParamSetid-GostR3410-2001-CryptoPro-C-ParamSetid-GostR3410-2001-CryptoPro-XchA-ParamSetid-GostR3410-2001-CryptoPro-XchB-ParamSet
- ГОСТ Р 34.10-2012 с длиной ключа 256:
id-GostR3410-2001-CryptoPro-A-ParamSetid-GostR3410-2001-CryptoPro-B-ParamSetid-GostR3410-2001-CryptoPro-C-ParamSetid-GostR3410-2001-CryptoPro-XchA-ParamSetid-GostR3410-2001-CryptoPro-XchB-ParamSetid-tc26-gost-3410-12-256-paramSetAid-tc26-gost-3410-12-256-paramSetBid-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-paramSetAid-tc26-gost-3410-12-512-paramSetBid-tc26-gost-3410-12-512-paramSetC
В рамках пакета SDK поставляется готовая реализация аутентификации на стороне сервера в виде библиотеки sslServer.dll, но ее рекомендуется использовать не в качестве готового решения, а в качестве примера исходного кода. Данный раздел описывает основные принципы устройства подобной системы, которыми можно руководствоваться при создании системы аутентификации для каждого конкретного Web-приложения.
Общие принципы реализации системы аутентификации
- На стороне клиента используйте команды начала и продолжения сеанса: establishSChannelBegin и establishSChannelContinue для взаимной аутентификации, и unilateralAuthenticationBegin и unilateralAuthenticationContinue для односторонней соответственно.
- На стороне сервера необходимо реализовать функции, которые с помощью криптографического компонента преобразовывали бы полученные от клиента данные. Они необходимы для верификации подписи случайного числа вне зависимости от вида аутентификации, а также для генерации ключа согласования при взаимной.
В подразделах ниже будут рассмотрены основные особенности каждого из видов аутентификации.
7.7.2 Взаимная аутентификация
Взаимная аутентификация предполагает обмен сертификатами между клиентом и сервером и создание на их основе ключа согласования (Протокол Диффи—Хеллмана). Взаимодействие между клиентом и сервером происходит по следующей схеме с использованием команд establishSChannelBegin и establishSChannelContinue:
- Устанавливаются параметры безопасности, включая версию протокола, идентификатор сессии и метод компрессии. Клиент и сервер обмениваются случайными числами (
client_randomиserver_random). - Далее происходит следующее:
- Производится обмен сертификатами для взаимной аутентификации клиента и сервера с заданными параметрами.
- Клиент генерирует случайную величину
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:
- Устанавливаются параметры безопасности, включая версию протокола, идентификатор сессии, метод компрессии и начальные случайные числа.
- Сервер посылает запрос на сертификат и случайное число. Клиент отправляет в ответ сертификат и подписанное случайное число сервера.
Пример реализации односторонней аутентификации на стороне клиента
// Идентификатор токена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;}
