Pull to refresh

Электронная цифровая подпись в веб-шаблонах SAP

Статья посвящена выполненной мной достаточно нетривиальной задаче по интеграции электронной цифровой подписи в веб-шаблоны 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 происходит автоматически.

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.

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.