Как стать автором
Поиск
Написать публикацию
Обновить

Применение ЭЦП в web-шаблонах SAP NetWeaver

Статья посвящена выполненной мной достаточно нетривиальной задаче по интеграции электронной цифровой подписи в веб-шаблоны SAP (Business Explorer Web Application), работающие в SAP NetWeaver. Заранее извиняюсь, если в статье будут допущены ошибки в терминологии или в логике, так как с SAP работаю только 5 месяцев.

Общие сведение об ЭЦП можно получить в википедии ЭЦП

В чем заключалась задача


Есть SAP NetWeaver в роли хранилища данных Business Warehouse.
Все данные хранятся в кубах. В кубах же хранятся документы. Документом, по сути, является набор строк куба, имеющих одинаковый признак – номер документа. Работа с данными построена на базе веб-шаблонов Business Explorer Web Application. Содержимое документов отображаются в компоненте analisys item.

Несколько слов для незнакомых с Bex Web. Технология веб-шаблонов (веб форм) по сути напоминает собой ASP.NET. В дизайнере создаешь макет формы, используя компоненты, схожие с ASP (dataGrid, button и прочее). Навешиваешь с помощью мастеров обработчики событий (это могут быть определенные команды или произвольный ABAP код). При запуске веб-формы – она обрабатывается на сервере и клиенты отдается HTML страничка с JS. Реакция на действия пользователя – производиться на стороне сервера при обновлении страницы. В коде веб-шаблона обычно нет необходимости генерировать HTML, как в PHP.

В некой web-форме пользователи вводят данные в таблицу, представленную analisys item. Введенные данные сохраняются в куб. После ввода данных пользователь должен поменять их статус (например, со статуса «Новый» на «Обработано»: перевод статуса происходит с помощью функции repost по значениям признака хранящего статусы данных; этот признак также находится в этом же кубе).
Так вот, необходимо подписывать введенные данные с помощью ЭЦП после ввода/сохранения данных и перед тем, как пользователь переведет эти данные со статуса «Новый» на «Обработано» (подписывать должен тот пользователь, который ввел данные).

Поиск в сети показал, что использовать ЭЦП не так то просто, как хотелось бы. В большинстве стран существуют собственные законодательные акты, регулирующие применение средств криптозащиты. Федеральный закон Российской Федерации от 10 января 2002 г. N 1-ФЗ «Об электронной цифровой подписи»
В частности устанавливаются алгоритмы, которые должны применяться при шифровании и генерации подписи. Например, алгоритм формирования и проверки электронной цифровой подписи ГОСТ Р 34.10-2001

Конечно же, не имеет смысла самим пытаться реализовать данные алгоритмы, поэтому смотрим, что предлагается на рынке.
Например, решения «ЛИССИ»
http://www.lissi.ru/solution/

Позиционируют себя спасителями на белом коне для SAP’овцев.Комплекс софта от них обойдется в сумму, превышающую 300 000 рублей. Программное обеспечение представляет собой API для продуктов SAP, обращаться к которому можно посредством ABAP.
Проблема в том, что данные продукты подразумевают подписание данных посредством кода ABAP. На клиенте же мы имеем только веб-страницу c JS. Исполнить код ABAP можно только на сервере, например с помощью AJAX запроса. Но возникает проблема – закрытый ключ пользователя доступен только на клиенте. Его пересылка на сервер не должна осуществляться. Решение «ЛИССИ» подразумевает работу на клиентской машине полновесного, не тонкого, клиента SAP, в котором возможно выполнение ABAP.
Поэтому я отказался от готовых решений и реализовал ЭЦП через CAPICOM CAPICOM

Реализация ЭЦП


Здесь описание того, как реализовал ЭЦП

1 Порядок применения ЭЦП

1) Администратор безопасности регистрирует сертификат в базе сертификатов. Сертификат необходимо получить от подлинного удостоверяющего центра.

2) Пользователь работает в системе, создает документ и подписывает его, используя свой секретный ключ на внешнем носителе. При этом:
а) Создается «слепок» документа (выбирается все его содержание).
б) Над содержимом производятся криптографические операции подписания, в результате получаем подпись.
в) Из сертификата подписывающего извлекается отпечаток и сравнивается с отпечатком, зарегистрированным на этого пользователя. В случае совпадения – подпись сохраняется в базе, иначе подпись отменяется.

3) При последующих просмотрах документа, подпись проверяется при открытии документа. Подпись извлекается из БД. Над подписью и содержимым документа проводятся криптографические операции верификации подписи.

4) Администратор безопасности может добавлять сертификаты пользователей в базу сертификатов, приостанавливать временно или постоянно их действие.

2 Реализация хранения данных

Подписи хранятся в плоской таблице «Подписи».
image

База сертификатов – набор из двух плоских таблиц:

image

Ключи – собственно сертификаты. В таблице храниться привязанный к ключу пользователь, дата начала и конца действия ключа, сам ключ, статус (блокирован или нет), описание.

Приостановки – набор возможных приостановок действия ключа. Хранит дату начало, конца и описание приостановки. Также хранит ID приостановленного ключа.

3 Архитектура системы цифровой подписи

Механизм цифровой подписи построен на основе следующих компонентов.
1) ActiveX компонент для доступа к криптографическому API. (CAPICOM)
2) С помощью JS получаем содержимое документа
3) Вызовом метода ActiveX компонента подписать данные.
4) Отправить подпись на сервер (классу ABAP) с целью разместить в базе подписей.

image

CAPICOM – библиотека от MS, предоставляющая интерфейс к крипто провайдерам.
1 – посредством JS кода, происходят обращения к библиотеке CAPICOM
2 – Веб шаблон формирует данные для подписи (XML, описывающий DataProvider).
3 – Полученная подпись, посредством AJAX передается ABAP классу, осуществляющему сохранение подписи в плоскую таблицу.
4 – взаимодействие крипто провайдера с eToken происходит автоматически.

Следует отметить, что использование CAPICOM полностью снимает головную боль о том, какие алгоритмы применяются. При создании сертификата удостоверяющим центром — в сертификат прошиваются требования к алгоритмам. CAPICOM же при обработке сертификата ищет данные алгоритмы (точнее криптопровайдеры, их реализующие) в системе и использует их.

4 Реализация API

image

Класс Signer – реализует пользовательские методы –
Подписать, проверить подпись, получить последнюю подпись
Класс CryptoProvider – враппер для Capicom.
ZCL_AJAX_DIG_SIGN – реализация интерфейсных методов через Ajax.
Z_DIGITAL_SIGNER – реализация методов сохранения и поиска подписи, методов проверки действительности публичного ключа по базе ключей.

5 Дополнительно словесное описание

Рассмотрим порядок подписания\проверки документа.

Пользователь жмет на форме кнопку «Утвердить(сохранить) документ». JS собирает с с html кода шаблона контент документа, предварительно выгруженный туда. Обращается к CAPICOM, который просит у человека выбрать нужный сертификат. При выборе сертификата сделанного под криптоПро специально для работы в системе – CAPICOM обратиться к провайдеру КриптоПРО, тот же попросит токен с закрытым ключом. Когда токен вставят – контент документа будет подписан. Подпись по AJAX кидается в BSP приложение, оно передает подпись в интерфейсный класс Z_DIGITAL_SIGNER. Класс проверит сертификат из подписи, факт того, что именно такой сертификат привязан к данному залогинившемуся пользователю. В случае успеха проверки – запишет подпись в базу подписей. На форме произойдут изменения – появиться отметка о успешной подписи.

При открытии документа другим пользователем –появиться статус подписания. Это произойдет следующим образом. JS по AJAX запросит подписи для документа, получит подпись (априорно – она сделана нужным человеком и подпись сделана сертификатом из базы разрешенных сертификатов). Затем js дергает CAPICOM — метод «верификация подписи» с параметрами «подпись» и «контент документа». Если с документом и подписью все в порядке – метод вернет true, следовательно, документ подписан и корректен.
Также есть GUI для администратора безопасности – ведение базы активных сертификатов.

Подключение ЭЦП к веб-шаблону



1) подключить в XHTML веб шаблона ActiveX компонент CAPICOM, например
  1. <object id="CapicomObj" codebase="bwmimerep:///sap/bw/mime/Customer/JS/bin/capicom.cab" classid="clsid:A996E48C-D3DC-4244-89F7-AFA33EC60679" VIEWASTEXT="" />
* This source code was highlighted with Source Code Highlighter.



2) Создать новый провайдер данных с тем же запросом, что и основной. То есть, сделать копию провайдера. Таким образом получим выгруженный документ в HTML, который будем подписывать. Нельзя подписывать провайдер, который выводит документ в таблицу пользователя, потому что, при сортировки или фильтрации таблицы — данные в провайдеры будут меняться, а нам нужен документ в начальном виде.

3) Разместить на форме компонент «провайдер данных-информация».
Назовем его DATA_PROVIDER_TO_SIGN.
image
Синим- компонент «провайдер данных-информация», красным — он же в палитре компонентов, желтым — провайдер данных, поставляющий контент документа

4) Укажем в настройках DATA_PROVIDER_TO_SIGN:
Провайдер данных: Укажем созданную в шаге 2 копию провайдера.
Статус навигации — вывод: Off
Данные отчета: вывод: On

5) Размещаем на форме код
Здесь уже все зависит от вашей фантазии. Не буду постить ВЕСЬ свой код, включающий AJAX, ABAP, JavaScript, оставлю только простенький врапер для CAPICOM, который я сделал на основе примеров с сайта Microsoft.
  1. function CryptoProvider(OBJECT1)
  2. {
  3.   // CAPICOM constants
  4.   //Const to verify
  5.   CryptoProvider.prototype.CAPICOM_ACTIVE_DIRECTORY_USER_STORE = 3;
  6.   CryptoProvider.prototype.CAPICOM_AUTHENTICATED_ATTRIBUTE_DOCUMENT_DESCRIPTION = 2;
  7.   CryptoProvider.prototype.CAPICOM_AUTHENTICATED_ATTRIBUTE_DOCUMENT_NAME = 1;
  8.   CryptoProvider.prototype.CAPICOM_AUTHENTICATED_ATTRIBUTE_SIGNING_TIME = 0;
  9.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_APPLICATION_POLICY = 7;
  10.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_CERTIFICATE_POLICY = 8;
  11.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_EXTENDED_PROPERTY = 6;
  12.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_EXTENSION = 5;
  13.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_ISSUER_NAME = 2;
  14.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_KEY_USAGE = 12;
  15.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_ROOT_NAME = 3;
  16.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_SHA1_HASH = 0;
  17.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_SUBJECT_NAME = 1;
  18.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_TEMPLATE_NAME = 4;
  19.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_TIME_EXPIRED = 11;
  20.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_TIME_NOT_YET_VALID = 10;
  21.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_FIND_TIME_VALID = 9;
  22.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_INCLUDE_CHAIN_EXCEPT_ROOT = 0;
  23.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_INCLUDE_END_ENTITY_ONLY = 2;
  24.   CryptoProvider.prototype.CAPICOM_CERTIFICATE_INCLUDE_WHOLE_CHAIN = 1;
  25.   CryptoProvider.prototype.CAPICOM_CURRENT_USER_STORE = 2;
  26.   CryptoProvider.prototype.CAPICOM_DIGITAL_SIGNATURE_KEY_USAGE = 0x00000080;
  27.   CryptoProvider.prototype.CAPICOM_E_CANCELLED = -2138568446;
  28.   CryptoProvider.prototype.CAPICOM_ENCODE_ANY = 0xffffffff;
  29.   CryptoProvider.prototype.CAPICOM_ENCODE_BASE64 = 0;
  30.   CryptoProvider.prototype.CAPICOM_ENCODE_BINARY = 1;
  31.   CryptoProvider.prototype.CAPICOM_INFO_SUBJECT_SIMPLE_NAME = 0;
  32.   CryptoProvider.prototype.CAPICOM_KEY_STORAGE_DEFAULT = 0;
  33.   CryptoProvider.prototype.CAPICOM_LOCAL_MACHINE_STORE = 1;
  34.   CryptoProvider.prototype.CAPICOM_PROPID_KEY_PROV_INFO = 2;
  35.   CryptoProvider.prototype.CAPICOM_SMART_CARD_USER_STORE = 4;
  36.   CryptoProvider.prototype.CAPICOM_STORE_OPEN_READ_ONLY = 0;
  37.   CryptoProvider.prototype.CAPICOM_VERIFY_SIGNATURE_AND_CERTIFICATE = 1
  38.   CryptoProvider.prototype.CAPICOM_VERIFY_SIGNATURE_ONLY = 0;
  39.   CryptoProvider.prototype.CERT_KEY_SPEC_PROP_ID = 6;
  40.  
  41.  
  42.   //CryptoProvider.prototype.CertThumbprint = "";
  43.   CryptoProvider.prototype.CertValue = "";
  44.   CryptoProvider.prototype.CertHash = "";
  45.   CryptoProvider.prototype.ErrorStack = "";
  46.   CryptoProvider.prototype.ErrorState = 0;
  47.   CryptoProvider.prototype.VerifySert = false;
  48.  
  49.   CryptoProvider.prototype.oCAPICOM = OBJECT1;
  50.  
  51.   //CryptoProvider.prototype.Init();
  52. }
  53.  
  54. // объявляем, инициализируем, реализуем свойства и методы
  55.  
  56. CryptoProvider.prototype.IsCAPICOMInstalled = function ()
  57. {
  58.   if (typeof (this.oCAPICOM) == "object")
  59.   {
  60.     if ((this.oCAPICOM.object != null))
  61.     {
  62.       //alert(" We found CAPICOM!");
  63.       return true;
  64.     }
  65.   }
  66. }
  67.  
  68. CryptoProvider.prototype.Init = function ()
  69. {
  70.   var FilteredCertificates = this.FilterCertificates();
  71.   if (FilteredCertificates)
  72.   {
  73.     if (FilteredCertificates.Count == 1)
  74.     {
  75.       this.CertValue = FilteredCertificates.Item(1).GetInfo(this.CAPICOM_INFO_SUBJECT_SIMPLE_NAME);
  76.       this.CertHash = FilteredCertificates.Item(1).Thumbprint;
  77.     }
  78.     else
  79.     {
  80.       this.CertValue = "";
  81.       this.CertHash = "";
  82.       this.SelectCertificate(FilteredCertificates);
  83.     }
  84.     FilteredCertificates = null;
  85.   }
  86.   else
  87.   {
  88.     this.ErrorStack += "У Вас нет действующих сертификатов.\n";
  89.     this.ErrorState = 13;
  90.   }
  91. }
  92.  
  93.  
  94. CryptoProvider.prototype.FilterCertificates = function ()
  95. {
  96.   var MyStore = new ActiveXObject("CAPICOM.Store");
  97.   var FilteredCertificates = new ActiveXObject("CAPICOM.Certificates");
  98.   try
  99.   {
  100.     //MyStore.Open(this.CAPICOM_CURRENT_USER_STORE, "My", this.CAPICOM_STORE_OPEN_READ_ONLY);
  101.     MyStore.Open(this.CAPICOM_CURRENT_USER_STORE, "MY");
  102.   }
  103.   catch (e)
  104.   {
  105.     if (e.number != this.CAPICOM_E_CANCELLED)
  106.     {
  107.       this.ErrorStack += "Ошибка при открытии хранилища сертификатов.\n";
  108.       this.ErrorState = 11;
  109.       return false;
  110.     }
  111.   }
  112.   // find all of the certificates that:
  113.   //  * Are good for signing data
  114.   //  * Have PrivateKeys associated with then - Note how this is being done :)
  115.   //  * Are they time valid
  116.   //var FilteredCertificates = MyStore.Certificates.Find(this.CAPICOM_CERTIFICATE_FIND_KEY_USAGE, this.CAPICOM_DIGITAL_SIGNATURE_KEY_USAGE).Find(this.CAPICOM_CERTIFICATE_FIND_TIME_VALID).Find(this.CAPICOM_CERTIFICATE_FIND_EXTENDED_PROPERTY, this.CERT_KEY_SPEC_PROP_ID);
  117.   var FilteredCertificates = MyStore.Certificates.Find(this.CAPICOM_CERTIFICATE_FIND_KEY_USAGE, this.CAPICOM_DIGITAL_SIGNATURE_KEY_USAGE).Find(this.CAPICOM_CERTIFICATE_FIND_TIME_VALID).Find(this.CAPICOM_CERTIFICATE_FIND_EXTENDED_PROPERTY, this.CERT_KEY_SPEC_PROP_ID);
  118.   //var FilteredCertificates = MyStore.Certificates.Find(this.CAPICOM_CERTIFICATE_FIND_KEY_USAGE, this.CAPICOM_DIGITAL_SIGNATURE_KEY_USAGE).Find(this.CAPICOM_CERTIFICATE_FIND_TIME_VALID);
  119.   return FilteredCertificates;
  120.   MyStore = null;
  121.   FilteredCertificates = null;
  122. }
  123.  
  124. CryptoProvider.prototype.FindCertificateByHash = function (szThumbprint)
  125. {
  126.   // instantiate the CAPICOM objects
  127.   var MyStore = new ActiveXObject("CAPICOM.Store");
  128.   // open the current users personal certificate store
  129.   try
  130.   {
  131.     MyStore.Open(this.CAPICOM_CURRENT_USER_STORE, "My", this.CAPICOM_STORE_OPEN_READ_ONLY);
  132.   }
  133.   catch (e)
  134.   {
  135.     if (e.number != this.CAPICOM_E_CANCELLED)
  136.     {
  137.       this.ErrorStack += "Ошибка при открытии хранилища сертификатов.\n";
  138.       this.ErrorState = 12;
  139.       return false;
  140.     }
  141.   }
  142.  
  143.   // find all of the certificates that have the specified hash
  144.   var FilteredCertificates = MyStore.Certificates.Find(this.CAPICOM_CERTIFICATE_FIND_SHA1_HASH, szThumbprint);
  145.   return FilteredCertificates.Item(1);
  146.  
  147.   // Clean Up
  148.   MyStore = null;
  149.   FilteredCertificates = null;
  150. }
  151.  
  152. CryptoProvider.prototype.SelectCertificate = function (Serts)
  153. {
  154.   var ret;
  155.   var FilteredCertificates = Serts;
  156.   try
  157.   {
  158.     // Pop up the selection UI
  159.     var SelectedCertificate = FilteredCertificates.Select();
  160.     if (SelectedCertificate)
  161.     {
  162.       this.CertValue = SelectedCertificate.Item(1).GetInfo(this.CAPICOM_INFO_SUBJECT_SIMPLE_NAME); ;
  163.       this.CertHash = SelectedCertificate.Item(1).Thumbprint;
  164.       ret = true;
  165.     }
  166.     else
  167.     {
  168.       this.CertValue = "";
  169.       this.CertHash = "";
  170.       this.ErrorStack += "Вы не выбрали сертификат.\n";
  171.       this.ErrorState = 20;
  172.       ret = false;
  173.     }
  174.   }
  175.   catch (e)
  176.   {
  177.     this.CertValue = "";
  178.     this.CertHash = "";
  179.     this.ErrorStack += e.description + "\n";
  180.     this.ErrorState = 19;
  181.     ret = false;
  182.   }
  183.   SelectedCertificate = null;
  184.   FilteredCertificates = null;
  185.   return ret;
  186. }
  187.  
  188. CryptoProvider.prototype.SignedData = function (toSign)
  189. {
  190.   // instantiate the CAPICOM objects
  191.   var SignedData = new ActiveXObject("CAPICOM.SignedData");
  192.   var Signer = new ActiveXObject("CAPICOM.Signer");
  193.   var TimeAttribute = new ActiveXObject("CAPICOM.Attribute");
  194.   // only do this if the user selected a certificate
  195.   if (this.CertHash != "")
  196.   {
  197.     try
  198.     {
  199.       if (toSign == "")
  200.       {
  201.         throw new userException('Отсутствуют данные для подписи.');
  202.       }
  203.       SignedData.Content = toSign;
  204.       // Set the Certificate we would like to sign with
  205.       Signer.Certificate = this.FindCertificateByHash(this.CertHash);
  206.       
  207.       // Set the time in which we are applying the signature
  208.       var Today = new Date();
  209.       TimeAttribute.Name = this.CAPICOM_AUTHENTICATED_ATTRIBUTE_SIGNING_TIME;
  210.       TimeAttribute.Value = Today.getVarDate();
  211.       Today = null;
  212.       Signer.AuthenticatedAttributes.Add(TimeAttribute);
  213.       // Do the Sign operation
  214.       var szSignature = SignedData.Sign(Signer, true, this.CAPICOM_ENCODE_BASE64);
  215.     }
  216.  
  217.     catch (e)
  218.     {
  219.       if (e.number != this.CAPICOM_E_CANCELLED)
  220.       {
  221.         this.ErrorStack += "Ошибка доступа к подписываемому содержимому: " + e.description + "\n";
  222.         this.ErrorState = 10;
  223.         return "";
  224.       }
  225.       else
  226.       {
  227.         this.ErrorStack += e.description + "\n";
  228.         this.ErrorState = 15;
  229.         return "";
  230.       }
  231.     }
  232.     return szSignature;
  233.   }
  234.   else
  235.   {
  236.     this.ErrorStack += 'Не был выбран сертификат.\n';
  237.     this.ErrorState = 16;
  238.     return "";
  239.   }
  240. }
  241.  
  242. CryptoProvider.prototype.VerifySig = function (toVer, sign)
  243. {
  244.   // instantiate the CAPICOM objects
  245.   var SignedData = new ActiveXObject('CAPICOM.SignedData');
  246.   try
  247.   {
  248.     SignedData.Content = toVer;
  249.     var mode;
  250.     this.VerifySert ? mode = this.CAPICOM_VERIFY_SIGNATURE_AND_CERTIFICATE : mode = this.CAPICOM_VERIFY_SIGNATURE_ONLY;
  251.     SignedData.Verify(sign, true, mode);
  252.   }
  253.   catch (e)
  254.   {
  255.     this.ErrorStack += e.description + "\n";
  256.     return false;
  257.   }
  258.   return true;
  259. }
* This source code was highlighted with Source Code Highlighter.



И пример его использования
Подписание
  1. SignerProv = new CryptoProvider(this.CapicomObj);
  2.   if (SignerProv.IsCAPICOMInstalled())
  3.     {
  4.       SignerProv.Init();
  5.       Sign== SignerProv.SignedData(DataToSign);    
  6.      }
* This source code was highlighted with Source Code Highlighter.



Проверка подписи

  1.   SignerProv = new CryptoProvider(this.CapicomObj);
  2.   SignerProv.VerifySert = true;//false – если не надо проверять сам сертификат на подлинность
  3.   if (SignerProv.IsCAPICOMInstalled())
  4.   {
  5.       var SRes = SignerProv.VerifySig(ContentToVerif, SignToVerify);
  6.    }
* This source code was highlighted with Source Code Highlighter.



Заключение


Таким образом без особых сложностей с применением пары костылей и мелких фишек была реализована ЭЦП. На вопрос о ее юридической значимости ответить пока не могу, так как тут ключевым является не реализация, а наличие договоров между сторонами, принимающих участие в документообороте. Надеюсь, информация может оказаться полезной.
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.