Решение Рутокен WEB позволяет реализовать строгую аутентификацию для web-ресурсов, используя электронную подпись по ГОСТ Р 34-10.2001. Более подробно про алгоритмы можно прочитать в этой статье. Здесь покажем как сделан действующий вариант использования Рутокен WEB на сайтах под управлением Asp.net и приведем инструкцию по сборке.
Сделать так, чтобы все работало, действительно просто.
Решение Рутокен WEB состоит из следующих компонентов:
- USB-токена Рутокен WEB (не требует установки драйверов)
- клиентских кроссплатформенных мультибраузерных плагинов
- клиентских скриптов для работы с плагином
- серверных компонентов
Плагин для браузера можно скачать тут. Осталось сделать серверный компонент, клиентский javascript и все это соединить.
Алгоритм аутентификации
Аутентификация пользователя, не зависимо от платформы, подразумевает предъявление некоего идентификатора субъекта, проверку идентификатора и принятия решения о доступе. Например, предъявление логина с паролем, проверка данных по базе, и установка аутентифицирующей cookie в случае успеха.
В нашем случае идентификатором будет ЭЦП, сформированная на клиенте. Проверять будем корректность подписи данных. При успешной проверке, считаем аутентификацию прошедшей успешно. В общем, используем классическое рукопожатие.
Реализация алгоритма
Подготовка.
Для того чтобы сервер мог проверять подпись, ему необходимо знать открытый ключ клиента.
Поэтому, первым делом сформируем на клиенте, в его устройстве Рутокен WEB, контейнер, содержащий ключевую пару, и передадим на сервер открытый ключ и уникальный идентификатор устройства Рутокен WEB. Закрытый ключ является не извлекаемым, соответственно, не покидает устройство.
Контейнер назовем тоже не абы как, а по схеме {логин}#%#{sitename}{port}. Например, yandex@gmail.com#%#dotnet.rutokenweb.ru:80. Название будет использовано в дальнейшем, при отображении списка логинов на токене.
На сервере получаем открытый ключ и id токена и привязываем их существующему клиенту. Мы же должны знать, кто попытается получить доступ.
Этап подготовки закончен, можно аутентифицировать клиентов.
Аутентификация.
- Клиент отправляет на сервер запрос, содержащий идентификатор и признак того, что нужно аутентифицироваться.
- Сервер генерирует случайные данные, например, строку; хэширует данные, запоминает в сессии и отправляет клиенту. Назовем эти данные s1.
- Клиент получает хэш данных, генерирует свои случайные данные (s2), формирует хэш суммы строк и подписывает данный хэш (получаем ЭЦП). Далее клиент передает на сервер те данные, что сгенерировал сам (s2) и ЭЦП суммы строк.
- Сервер получает случайные данные клиента (s2)и ЭЦП, аналогично формирует хэш суммы случайных данных клиента (s2) и данных, сформированных в начале сервером (s1).
- В результате на сервере есть данные (хэш s1 + s2) и подпись этих данных. Остается только проверить корректность подписи.
Пример реализации на C#
В моем случае аутентификацию по Рутокен WEB нужно было прикрутить на 3 сайта. 2 из них используют аутентификацию Forms, еще один работает с Windows Identity Foundation, использует STS сервис для аутентификации. Все три сайта работают на WebForms.
Сделаем для них WebControl с нужным функционалом, на самом деле два контрола. Один будет использоваться при аутентификации, другой для управления привязками Рутокен WEB, например — в личном кабинете.
Все запросы на сервер будут ajax запросами, без полного постбэка. Таким образом контролы нужны, по большому счету, для представления на странице необходимых элементов и javascript-ов, а обработкой аякс-запросов займется httpHandler. Он же будет отдавать клиенту локализованный javascript.
И, наконец, с остальным сайтом контрол и хэндлер будут взаимодействовать с помощью объекта, реализующего интерфейс ITokenProcessor, где объявлены специфичные для каждого конкретного сайта методы, нужные нашему решению. Например, получение открытого ключа, получение имени пользователя и прочее.
Схематично все это выглядит так:
Подготовка к аутентификации, как уже говорилось, сводится к формированию на Рутокен WEB контейнера с закрытым и открытым ключом и передаче на сервер открытого ключа и id токена, с привязкой данных к аккаунту пользователя. Данная операция должна быть доступна уже аутентифицированным пользователям, а сам контрол с функционалом можно разместить например в личном кабинете. Этим будет заниматься контрол с редким названием Administration, а контрол с названием Login займется процессом аутентификации.
Реализация httpHandler
Задачи нашего обработчика:
1. Обработать ajax запрос клиента с Рутокен WEB.
Хэндлер будет обрабатывать ajax запросы только с известными ему headers ('X-Requested-With','XhrRutoken').
Сделаем класс для разбора запроса (CMessageRequest) и класс для формирования ответа (CMessageResponse). При запросе создаем экземпляр класса для разбора запроса, присваивая его мемберу хэндлера. Разбор происходит в конструкторе.
_mRequest = new CMessageRequest(context);
В запросе передается название метода, который и запускаем, если конечно найдем, рефлексией.
GetType().InvokeMember(_mRequest.act, BindingFlags.InvokeMethod, null, this, new object[] {});
В методе запрос обрабатывается, в результате создаем экземпляр класса с ответом. В конце концов ответ сериализуется в json и передается в Response.
2. Отдать локализованный javascript на страницу.
Javascript добавляется на страницу так —
<script src=" /RutokenWebSTS/rutokenweb/ajax.rtw?getRutokenJavaLocal=1" type="text/javascript"></script>
Разметку выдает контрол (об этом ниже). В случае запроса с getRutokenJavaLocal=1 опять задействуем наш хэндлер, на этот раз для отдачи javascript.
Все javascript-ы добавлены в сборку как Embedded Resource. Можно было бы ограничиться простым добавлением ресурса. Вначале так и было. Но вот появился заказчик из белоарабии и захотел возможность локализации. Поэтому добавляем не простую, а золотую, локализованную версию, так:
private void SendLocalizeScript()
{
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(
"RutokenWebPlugin.javascript.tokenadmin.js"))
{
if (stream != null)
{
var reader = new StreamReader(stream);
HttpContext.Current.Response.Write(Utils.LocalizeScript(reader.ReadToEnd()));
}
}
}
LocalizeScript выдает уже локализованную версию скрипта, для чего парсит скрипт и выдает нужную нам строку, заменяя все вхождения LOCALIZE(ключ_ресурса) на строку из ресурсного файла RutokenLocalText.resx
private static Regex REGEX = new Regex(@"LOCALIZE\(([^\))]*)\)", RegexOptions.Singleline | RegexOptions.Compiled);
…
public static string LocalizeScript(string text)
{
var matches = REGEX.Matches(text);
foreach (Match match in matches)
{
string strResourceStringID = match.Groups[1].Value;
string str = (string)HttpContext.GetGlobalResourceObject("RutokenLocalText", strResourceStringID) ?? strResourceStringID;
text = str != strResourceStringID ? text.Replace(match.Value, MakeValidString(str)) : text.Replace(match.Value, string.Format("'LOCALIZE.{0}'", str));
}
return text;
}
Ресурсы есть в исходниках примера.
ITokenController
Наши контролы и хэндлер будут взаимодействовать с сайтом/приложением посредством интерфейса ITokenController. Методы интерфейса подробно расписаны в исходниках. Они реализуют специфичный для сайта функционал. Например, получение/сохранение ключей, получение имени пользователя и т.п.
Чтобы все заработало, объект, реализующий этот интерфейс, нужно передавать в метод контролов, например:
class CustomTokenProcessor : ITokenProcessor
...
// tokenLogin - контрол
tokenlogin.SetRequired(new CustomTokenProcessor(), returnurl);
метод фактически помещает объект в сессию
public void SetRequired(ITokenProcessor processor, string successurl)
{
var session = HttpContext.Current.Session;
if (session != null)
{
if (session["TokenProcessor"] == null)
{
session["TokenProcessor"] = processor;
}
session["SuccessUrl"] = successurl;
}
}
И объект становится доступным хэндлеру.
Так же в хэндлере используется event OnSuccessAuth, срабатывающий при успешной аутентификации. Причем на событие можно подписываться в контроле, а не в хэндлере. Сделано так для возможности доступа к сессии в методе, который добавлен к евенту. При этом методу передается объект сесии
if ((OnSuccessAuth = (EventHandler) _mContext.Session["OnSuccessAuth"]) != null)
{
OnSuccessAuth(_mContext.Session, new EventArgs());
_mContext.Session["OnSuccessAuth"] = null;
}
А в методе получаем сессию так
private void tokenlogin_OnSuccessAuth(object sender, EventArgs e)
{
HttpSessionState session = (HttpSessionState) sender;
if (session != null)
{
// используем сессию
session["dssVerify"] = true;
}
}
Реализация контролов.
Для начала сделаем родителя обоих контролов. Его основные задачи следующие:
1. Обеспечение возможности задать Template
Делаем так
[TemplateContainer(typeof (AdministrationData)), TemplateInstance(TemplateInstance.Single)]
public virtual ITemplate Template { get; set; }
2. Добавление на страницу объекта для работы с Рутокен WEB
Работа с браузерным плагином сводится к вызовам методов специально объявленного object. Объявляется в таком формате:
<object id="cryptoPlugin" type="application/x-rutoken" width="0" height="0"></object>
для этого в onLoad контрола делаем
private void EnsureRutokenPlugin()
{
var rtObject = new HtmlGenericControl("object") {ClientIDMode = ClientIDMode.Static, ID = JStokenObjectID};
rtObject.Attributes.Add("type", "application/x-rutoken");
rtObject.Attributes.Add("width", "0");
rtObject.Attributes.Add("height", "0");
var rtParam = new HtmlGenericControl("param") {TagName = "onload"};
rtParam.Attributes.Add("value", "pluginit");
rtObject.Controls.Add(rtParam);
// ищем контрол с возможностью добавить и кидаем объект туда
bool bControlAdded = false;
if (Page.Form == null)
{
throw new Exception("define 'Form' tag on page!");
}
foreach (PlaceHolder control in Page.Form.Controls.OfType<PlaceHolder>())
{
(control).Controls.Add(rtObject);
bControlAdded = true;
break;
}
if (!bControlAdded)
{
throw new Exception("define an empty 'PlaceHolder' tag after the tag 'Form'");
}
// объект токена
Utils.IdToJavaScript(rtObject, JScontrolVar, "token", Page);
// объект с настройками
Page.ClientScript.RegisterStartupScript(typeof(Control), "settings",
string.Format("{0}.settings = {{}}; {0}.settings.mainurl = '{1}/rutokenweb/ajax.rtw';",
JScontrolVar, HttpContext.Current.Request.ApplicationPath)
, true);
}
Здесь есть одна особенность. Объект плагина не должен находиться в скрытом элементе, у которого display:none; на пример, а то работать отказывается. А размещать мы его будем в PlaceHolder-е, который специально для этого объявим на основной странице с нашими контролами. Если используется masterpage, то на ней, причем сразу за тэгом Form.
<form id="form1" runat="server">
<asp:PlaceHolder ID="tokenPlaceHolder" runat="server"></asp:PlaceHolder>
Это и позволит избежать непреднамеренного попадания объекта токена в скрытый элемент страницы.
Теперь займемся реализацией контролов – наследников. Один для управления токенами, второй для аутентификации клиентов. Контролы templated, так что нужно им задать разметку на странице, причем в шаблоне обязательно должны присутствовать определенные элементы разметки с определенными именами. Кнопки, надписи и т.д. Наличие проверяется в коде.
В обоих контролах переопределим CreateChildControls:
protected override void CreateChildControls()
{
if (Template != null) // задан темплэйт
{
Controls.Clear();
administrationData = new AdministrationData();
Template.InstantiateIn(administrationData);
...
Дальше в методе найдем кнопки, таблицы и прочее, выставим им свойства если надо. Например кнопка привязки токена:
var rtwConnect = (Button)administrationData.FindControl("rtwConnect");
а также добавим на страницу переменные – указатели на эти dom-объекты, как свойства глобальной javascript переменной $grd_ctrls
IdToJavaScript(rtwConnect, JScontrolVar, "rtwConnect", Page);
public static void IdToJavaScript(Control ctrl, string jsvar, string field, Page page)
{
page.ClientScript.RegisterStartupScript(typeof (Control), field,
jsvar + "." + field + " = rtwGID('" + ctrl.ClientID +
"'); ", true);
}
Итого, у нас будет нужная разметка и ссылки на эти элементы разметки как свойства $grd_ctrls.
Рассмотрим шаблоны контролов:
Administration
Разметка у этого контрола достаточно громоздкая. Зато все данные есть.
<token:Administration ID="backoffice" runat="server" Port="12345">
<template>
<label>Список токенов:</label>
<asp:GridView runat="server" ID="rtwEnable" CssClass="OrdersGr" AutoGenerateColumns="False" GridLines="None" ShowHeaderWhenEmpty="False">
<EmptyDataTemplate>
Нет привязанных токенов
</EmptyDataTemplate>
<Columns>
<asp:TemplateField HeaderText="Token Id">
<ItemTemplate>
<%# ((uint)Container.DataItem) %>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Активен">
<ItemTemplate>
<asp:Label ID="rtwEnabledToken" runat="server"></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Управление">
<ItemTemplate>
<asp:Button runat="server" ID="rtwEnableSwitch" OnClientClick="return false;"/>
<asp:Button ID="rtwRemove" runat="server" Text="Отвязать токен" OnClientClick="return false;" ClientIDMode="Predictable"/>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
<br />
<label>Связка с Рутокен Web:</label>
<asp:Button ID="rtwConnect" runat="server" Text="Привязать токен"/>
<br />
<asp:Image ID="rtwAjaxImg" runat="server" ImageUrl="~/ajax_loader.gif" />
<br />
<asp:Label ID="rtwErrorMessage" runat="server" CssClass="errLabel" />
<asp:Label ID="rtwMessage" runat="server" CssClass="status ok" />
</template>
</token:Administration>
В принципе, здесь, всего лишь, таблица с токенами, кнопки привязки, отвязки и переключения токенов, а также два span – для информационных сообщений и сообщений об ошибках.
Данные для таблицы токенов отдает метод интерфейса ITokenController GetUserTokens
// List<uint> GetUserTokens(string login);
rtwEnable.DataSource = m_tokenProcessor.GetUserTokens(m_tokenProcessor.GetUserName());
rtwEnable.DataBind();
Login, Remember
Контрол для аутентификации или восстановления доступа. Восстановление возможно без использования токена, нужно ввести свой логин и код восстановления, указанный на карточке Рутокен WEB (поставляется в комплекте с токеном)
Пример разметки логина:
<aktivlogin:Login ID="tokenlogin" runat="server" LoginType="Login">
<Template>
<asp:Literal ID="rtwUsers" runat="server" />
<asp:Label ID="rtwErrorMessage" runat="server" CssClass="rutoken error" style="display: block; color: #c00;" />
<asp:Label ID="rtwMessage" runat="server" CssClass="rutoken message" style="display: block; color: green;" />
<asp:Button ID="rtwLogin" runat="server" OnClientClick="return false;" Text="Войти" style="margin-top:12px;" />
<asp:Image ID="rtwAjaxImg" runat="server" ImageUrl="~/ajax_loader.gif" />
</Template>
</aktivlogin:Login>
Здесь есть контрол типа Literal, который в результате будет выдавать select. Можно было бы использовать DropDownList, но в селект мы будем javascript-ом добавлять список логинов на токене и если будет postback, EventValidation страницы ругнется. Чтобы не выключать его, нарисуем select сами.
rtwUsers.Text = "<select id=\"rtwUsers\"></select>";
Пример разметки восстановления доступа:
<aktivlogin:Login ID="tokenlogin" runat="server" LoginType="Remember">
<Template>
Логин: <asp:TextBox ID="rtwRepairUser" runat="server" /><br />
Код восстановления: <asp:TextBox ID="rtwRepair" runat="server" /><br />
<asp:Label ID="rtwErrorMessage" runat="server" style="display: block; color: #c00;" />
<asp:Label ID="rtwMessage" runat="server" style="display: block; color: green;" />
<asp:Button ID="rtwRepairBtn" runat="server" OnClientClick="return false;" Text="Войти" style="margin-top:12px;" />
<asp:Image ID="rtwAjaxImg" runat="server" ImageUrl="/ajax_loader.gif" />
</Template>
</aktivlogin:Login>
Как видно, они отличаются указанием LoginType = Login или Remember.
Javascript
Основной javascript расположен в tokenadmin.js, его отдает хэндлер. Скрипт связывает элементы пользовательского интерфейса, плагин и сервер.
Элементы интерфейса привязаны к свойствам глобальной переменной $grd_ctrls, привязываем в коде контролов, помещая переменные на страницу с помощью page.ClientScript.RegisterStartupScript. Объект плагина — $grd_ctrls.token.
Tokenadmin.js делает следующее: в начале проверяем, доступен ли плагин и есть ли токен(если это логин). Затем делаем обработку запросов пользователя с колбэками. Например, при аутентификации сначала скрипт считывает все логины на токене и добавляет их в select (rtwUsers).
var containerCount = g.token.rtwGetNumberOfContainers();
for (i = 0; i < containerCount; i++) {
var contName = g.token.rtwGetContainerName(i);
g.rtwUsers.options[i] = new Option(contName.replace("#%#", " - "), contName);
}
Пользователь выбирает нужный логин и жмет кнопку «Войти».
Посылаем на сервер запрос с командой rnd и id токена. Если все ок, получаем в ответ json вида
{«text»:«94156e9a6642d42a47fc94c6f4b1b8c000dab4bfd24f321f5976e4d3a5a4e994»,«type»:«Notify»}
Это сгенерированная сервером последовательность, к которой по алгоритму нам надо прибавить свои случайные данные. Колбэк функция генерирует эти данные, делает конкатенацию с тем что прислал сервер, считает хэш и подписывает в плагине браузера. Подпись данных требует ввода пинкода. Пользователь вводит пин. Если все ок и пин корректен, отправляем на сервер подпись и случайные данные. Сервер производит конкатенацию строк и проверку подписи. Если подпись верна, получаем ответ:
{«text»:«True»,«type»:«Notify»,«url»:"\/RutokenWebSTS\/Admin\/"}
Вместе с ответом должна приехать и аутентификационная cookie, поэтому делаем редирект пользователя на присланный url. Аутентификация пройдена.
Проверка подписи
Всю криптографию вынес в отдельную dll. Наружу смотрят три метода:
- Генерация случайного хэша
- Вычисление хэша строки
- Проверки подписи
Вы можете сделать свою реализацию алгоритма аутентификации, используя только эту сборку.
И в заключение короткая инструкция по сборке.
(.net 4.0, тестировалось под iis 7.5)
1. Добавить сборки RutokenWebPlugin.dll и Rutoken.dll в проект
2. Добавляем httpHandler в Web.config
<system.webServer>
<handlers>
<add name="AjaxHandler" path="/RutokenWebSTS/rutokenweb/ajax.rtw" verb="*" type="RutokenWebPlugin.TokenAjaxHandler" resourceType="Unspecified" requireAccess="Script" preCondition="integratedMode" />
</handlers>
</system.webServer>
Path обязательно должен заканчиваться на '/rutokenweb/ajax.rtw'. Если сайт/приложение установлено в виртуальный каталог, как в примере выше, включите его в путь.
И при необходимости надо сделать хэндлер доступным всем
<location path="rutokenweb">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>
3. Реализуем интерфейс ITokenProcessor
public class CustomTokenProcessor : ITokenProcessor
{
…..
Самый ответственный момент, пример реализации с комментариями есть в исходниках
4. Добавляем контрол для управления токенами (личный кабинет)
<%@ Register TagPrefix="token" Namespace="RutokenWebPlugin" Assembly="RutokenWebPlugin" %>
И шаблон контрола (пример шаблона был в статье)
<token:Administration ID="backoffice" runat="server" Port="12345" >
<Template>
…..
Порт указываем, если приложение работает не на 80 порту.
5. В codebehind контрола администрирования добавляем объект, реализующий ITokenProcessor
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// CustomTokenProcessor : ITokenProcessor
var processor = new CustomTokenProcessor();
// метод процессора покажет данные токенов на странице
backoffice.TokenProcessor = processor;
// объект станет доступен хэндлеру
backoffice.SetRequired(processor, "/");
}
6. Добавляем контрол для аутентификации на страницу логина
<%@ Register TagPrefix="aktivlogin" Namespace="RutokenWebPlugin" Assembly="RutokenWebPlugin" %>
и его шаблон
<aktivlogin:Login ID="tokenlogin" runat="server" SuccessUrl="http://localhost/Secured/" LoginType="Login">
<Template>
…….
8. В Codebehind контрола с логином добавляем объект, реализующий ITokenProcessor
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// returnurl определяет урл, на который переходим после аутентификации
tokenlogin.SetRequired(new CustomTokenProcessor(), returnurl);
}
После этого на странице авторизации появится вход по токену, а в личном кабинете будет возможность управлять токенами — привязывать к аккаунту, переключать активность. Что и требовалось сделать.
Приведенный пример не составит труда доработать под свои нужды, либо можно использовать библиотеку с проверкой подписи и сделать все «с нуля».
Исходники примера с тестовым сайтом и скриптом для базы данных можно скачать здесь