Как мы познакомились с КриптоПРО или Работаем с документами, подписанными УКЭП

6 8

Задача: Дать пользователю универсальный инструмент для работы с подписью, ее выгрузкой, установкой, передачей и загрузкой (если на то будет необходимость). Это необходимо для дальнейшего использования и выгрузок подписанных документов с УКЭП.

Как ни странно, но информации по способам подписания документов было более чем достаточно в сети, а вот конкретики по настройке или хотя бы идеи о том, как сопрягать имеющиеся сертификаты и УКЭП с файлами - кот наплакал. Однако собрать воедино кое-что получилось.

Пришлось искать информацию и собирать все по кусочкам. 

Итак. Кусочек первый:

Определение командной строки для подписания. Тут все просто - форум КриптоПро в помощь:

'chcp 1251
cd "C:\Program Files (x86)\Crypto Pro\CSP"
csptest.exe -sfsign -sign -in "ИмяДокумента" -out "ИмяДокумента.sig" -my "Sertificate" -addsigtime -add -detached
'

Вроде бы все понятно. Облачаем это во временный файл для запуска:

TextInCMD = 'chcp 1251
    cd "C:\Program Files (x86)\Crypto Pro\CSP"
    csptest.exe -sfsign -sign -in "' & DocName & '" -out "' & DocName & '.sig" -my "' & Certificate & '" -addsigtime -add -detached'
    CMDFile = GetTempFolder() & 'sig.bat'
    ФайлЗаписать(CMDFile;'N';TextInCMD)
    CMD = CreateObject("WScript.Shell")                                  
    CMD.Run(CMDFile;1;True)
    ФайлУдалить(CMDFile)

Далее. Кусочек два.

Отлично, теперь нам надо передать в эту строку Имя файла и указатель на конкретный сертификат. С именем все понятно, там по классике: DocName = GetTempFolder() & Doc.SYSREQ_EDOC_NAME & '.' & Doc.Info.Editor.Extension, а вот с сертификатом возникли сложности, так как их может быть много, а значит нам надо передавать только тот, который будет подписывать внешний файл. Для этого в списке сертификатов пользователя мы, пометив нужный нам сертификат тэгом "Внешний", и будем получать его ИД:

И в коде:

UserRef = References.SYSREF_USERS_REFERENCE.GetObjectByID(tasks.CurrentUser.ID)
DDS2 = UserRef.DetailDataSet(2)
foreach Sert in DDS2
  if Sert.ISBCertificateInfo == "Внешний"
    Certificate = Sert.СодержаниеТ2 // -------- То что нам и надо!
  endif
endforeach

Теперь у нас есть все необходимое для того, чтобы получить строку для подписания документа находящегося во "внешнем мире", осталось только добавить Экспорт документа, Импорт подписанного документа и связи между ними. Плюс уборка за собой.

Reslt = 0  
UserRef = References.SYSREF_USERS_REFERENCE.GetObjectByID(tasks.CurrentUser.ID)
DDS2 = UserRef.DetailDataSet(2)
foreach Sert in DDS2
  if Sert.ISBCertificateInfo == CertID
    Certificate = Sert.СодержаниеТ2
    Doc = EDocuments.GetObjectByID(DocID)
    DocName = GetTempFolder() & Doc.SYSREQ_EDOC_NAME & '.' & Doc.Info.Editor.Extension
    LastVersion = _LastVersionDoc(Doc)
    Doc.Export(LastVersion;DocName;;;;)
    TextInCMD = 'chcp 1251
    cd "C:\Program Files (x86)\Crypto Pro\CSP"
    csptest.exe -sfsign -sign -in "' & DocName & '" -out "' & DocName & '.sig" -my "' & Certificate& '" -addsigtime -add -detached'
    CMDFile = GetTempFolder() & 'sig.bat'
    ФайлЗаписать(CMDFile;'N';TextInCMD)
    CMD = CreateObject("WScript.Shell")                                  
    CMD.Run(CMDFile;1;True)
    ФайлУдалить(CMDFile)
    CryptoFile = DocName & '.SIG'
    if ФайлСуществует(CryptoFile)
      FileSign = EDocuments.CreateNewFromFile('Файл_типа_Sign';'Д000099';'Crypt';CryptoFile)
      FileSign.EDocument = Doc.ID
      FileSign.Save
      ФайлУдалить(CryptoFile)
      EDocuments.BindTo(Doc.Info;FileSign.Info)
      Reslt = FileSign.ID
    else
      Reslt = 0
    endif
  endif
endforeach
Result = Reslt

Далее. Кусочек три.

Рассмотрим вариант, когда у нас уже есть документы подписанные УКЭП в самом Директуме. Подпись надо достать и из него. Ищем информацию, получаем на выходе:

Doc = EDocuments.GetObjectByID(DocID)       
  FileName = GetTempFolder() & Doc.SYSREQ_EDOC_NAME & '.' & Doc.Info.Editor.Extension
  FileNamesig = FileName & '.sig'
  VerNumber = _LastVersionDoc(Doc)
  Doc.Export(VerNumber; FileName; FALSE; FALSE; TRUE)  
  ESDDoc = CreateObject('MSXML.DOMDocument') 
  ESDDoc.Load(FileName) 
  Root = ESDDoc.DocumentElement
  DeleteFile(FileName)
  SignaturesNode = Root.SelectSingleNode('DigitalSignatures') 
  SignNodeCount = SignaturesNode.ChildNodes.length 
  I = 0
  while I < SignNodeCount
    SignNode = SignaturesNode.ChildNodes.Item(I)
    DataNode = SignNode.ChildNodes.Item(1)
    WriteFile(FileNamesig;; DataNode.Text)
    I = I + 1
    Reslt = 1
  endwhile  

Замечательно.

Теперь мы должны к обоим алгоритмам предусмотреть вариант с выбором конечного адреса и отказом от удаления документа, чтобы файл подписи остался там, куда мы его отправили. А так же быть готовыми к варианту, что у пользователя нет УКЭП в Директум, но есть на внешнем носителе (Certificate = '1'). Ну и в параметры добавим Режим работы (MODE).

Затем собираем все в кучу. Немного магии, и вот мы уже имеем функцию с 3 параметрами: "DocID", "CertID" и MODE:

Описание:

Функция для выгрузки подписанного документа. 
Имеется 2 режима работы: Подписание в момент выгрузки или выгрузка ранее подписанного документа.
При запуске срабатывает Диалог - путь, куда нужно выгрузить подписанные документы. Если не выбирать каталог - будет выбрано временное хранилище и файлы после выгрузки будут удалены. 
Результат выполнения:
0 - системе не удалось загрузить подписанный документ.
1 - Система успешно выгрузила подписанный ранее документ.
ИД документа - Система выгрузила документ и загрузила подписанную версию в вид документа типа SIG, вернув его ИД для работы. 

PathK = CreateFolderDialog()
PathK.Execute
if Assigned(PathK.Result)
  Path = PathK.Result & '\'
  Load = FALSE 
else
  Path = GetTempFolder()
  Load = TRUE
endif
if Mode
  Reslt = 0  
  UserRef = References.SYSREF_USERS_REFERENCE.GetObjectByID(tasks.CurrentUser.ID)
  DDS2 = UserRef.DetailDataSet(2)
  if DDS2.RecordCount = 0
  try
    Certificate = '1'
    Doc = EDocuments.GetObjectByID(DocID)
    DocName = Path & Doc.SYSREQ_EDOC_NAME & '.' & Doc.Info.Editor.Extension
    LastVersion = _LastVersionDoc(Doc)
    Doc.Export(LastVersion;DocName;;;;)
    TextInCMD = 'chcp 1251
    cd "C:\Program Files (x86)\Crypto Pro\CSP"
    csptest.exe -sfsign -sign -in "' & DocName & '" -out "' & DocName & '.sig" -my "' & Certificate & '" -addsigtime -add -detached'
    CMDFile = GetTempFolder() & 'sig.bat'
    ФайлЗаписать(CMDFile;'N';TextInCMD)
    CMD = CreateObject("WScript.Shell")                                  
    CMD.Run(CMDFile;1;True)
    ФайлУдалить(CMDFile)
    if Load
      ФайлУдалить(DocName)
    endif
    CryptoFile = DocName & '.SIG'
    if ФайлСуществует(CryptoFile)
      FileSign = EDocuments.CreateNewFromFile('TFD_Sign';'Д000099';'Crypt';CryptoFile)
      FileSign.EDocument = Doc.ID
      FileSign.Save
      //if Load
      //  ФайлУдалить(CryptoFile)
      //endif
      EDocuments.BindTo(Doc.Info;FileSign.Info)
      Reslt = FileSign.ID
    else
      Reslt = 0
    endif
  except
    ShowMessage('Не обнаружены сертификаты пригодняе для подписания документа.')
    exit()  
  endexcept    
  else
    foreach Sert in DDS2
      if Sert.ISBCertificateInfo == CertID
        Certificate = Sert.СодержаниеТ2
        Doc = EDocuments.GetObjectByID(DocID)
        DocName = Path & Doc.SYSREQ_EDOC_NAME & '.' & Doc.Info.Editor.Extension
        LastVersion = _LastVersionDoc(Doc)
        Doc.Export(LastVersion;DocName;;;;)
        TextInCMD = 'chcp 1251
        cd "C:\Program Files (x86)\Crypto Pro\CSP"
        csptest.exe -sfsign -sign -in "' & DocName & '" -out "' & DocName & '.sig" -my "' & Certificate & '" -addsigtime -add -detached'
        CMDFile = GetTempFolder() & 'sig.bat'
        ФайлЗаписать(CMDFile;'N';TextInCMD)
        CMD = CreateObject("WScript.Shell")                                  
        CMD.Run(CMDFile;1;True)
        ФайлУдалить(CMDFile)
        if Load
            ФайлУдалить(DocName)
        endif
        CryptoFile = DocName & '.SIG'
        if ФайлСуществует(CryptoFile)
          FileSign = EDocuments.CreateNewFromFile('TFD_Sign';'Д000099';'Crypt';CryptoFile)
          FileSign.EDocument = Doc.ID
          FileSign.Save          
          EDocuments.BindTo(Doc.Info;FileSign.Info)
          Reslt = FileSign.ID
        else
          Reslt = 0
        endif
      endif
    endforeach
  endif
  Result = Reslt
else
  Reslt = 0
  Doc = EDocuments.GetObjectByID(DocID)       
  FileName = GetTempFolder() & Doc.SYSREQ_EDOC_NAME & '.' & Doc.Info.Editor.Extension
  FileNamesig = FileName & '.sig'
  VerNumber = _LastVersionDoc(Doc)
  Doc.Export(VerNumber; FileName; FALSE; FALSE; TRUE)  
  ESDDoc = CreateObject('MSXML.DOMDocument') 
  ESDDoc.Load(FileName) 
  Root = ESDDoc.DocumentElement
  if Load
    DeleteFile(FileName)
  endif 
  SignaturesNode = Root.SelectSingleNode('DigitalSignatures') 
  SignNodeCount = SignaturesNode.ChildNodes.length 
  I = 0
  while I < SignNodeCount
    SignNode = SignaturesNode.ChildNodes.Item(I)
    DataNode = SignNode.ChildNodes.Item(1)
    WriteFile(FileNamesig;; DataNode.Text)
    I = I + 1
    Reslt = 1
  endwhile  
endif

Вроде так. Может кому пригодится. 

Андрей Михайлов

А обратную ситуацию с загрузкой в СЭД пришедшей из вне пары документ-подпись исследовали? Я про импорт такой пары с образованием версии документа и подписи в системе отображаемой в информации о подписях документа из штатного диалога системы?

Тарас Асачёв

Андрей, нет. только практика хранения связки Документ-Подпись(sig) в паре для единицы Хранилища (Справочника). Но сама мысль интересная - добавить проверку подписи и загрузку в СЭД.

Алексей Семакин

1. Тарас, насколько я понял, ваше решение по требованию пользователя экспортирует документы из системы, попутно вычисляя ЭП, и складывает все это в папку выгрузки. При этом вычисленная ЭП импортируется в систему в новый документ, связанный с подписываемым. Документы в системе при этом могут быть как уже подписанными, так и неподписанными.

Вопрос: если вы импортируете отделяемую подпись в систему, то как обеспечить контроль ее валидности в системе (а иначе зачем она там)?

Неподписанный документ в системе может быть легко изменен, и в этот самый момент импортированная вами ЭП превратится в тыкву. Для подписанных в системе документов ситуация чуть лучше, так как их изменение запрещено системой. Но создание новой версии приведет все к ситуации с неподписанным документом, и получится, что УКЭП в системе есть (в связанном документе), но какая версия была подписана ею, непонятно.

Рассматривали ли подписание документов сертификатами КриптоПро непосредственно в системе с последующим экспортом подписей в выгрузку? Это решило бы задачу контроля валидности подписи (переложило бы ее на плечи системы). Про то как экспортировать подпись документа из системы в файл, можно посмотреть в материале Андрея Шестакова.

2. Забавный кусок кода:

  while I < SignNodeCount
    SignNode = SignaturesNode.ChildNodes.Item(I)
    DataNode = SignNode.ChildNodes.Item(1)
    WriteFile(FileNamesig;; DataNode.Text)
    I = I + 1
    Reslt = 1
  endwhile

То есть вы пробегаетесь по всем подписям и сливаете их в один и тот же файл с перезаписью? Почему бы не экспортнуть только последнюю?

Алексей Семакин: обновлено 26.02.2021 в 11:04
Алексей Семакин

Андрей, посмотрите материал Андрея Шестакова — там есть про импорт отсоединенной подписи.

Наталия Рвачева

Уточните, пожалуйста, а почему используется csptest, а не cryptcp? Судя по всему csptest - внутренняя утилита для тестирования определенных функций CSP.

Тарас Асачёв

Наталия, для периода тестирования и до ввода материала в продуктивную среду мне было достаточно утилиты тестирования, поэтому выбрал ее, но ее, как Вы заметили - легко заменить.

Тарас Асачёв

Алексей,

1. Данное решение работает во вспомогательном справочнике, где у нас хранятся как раз связи документа и его приложений, в том числе и его отделяемая подпись для тех случаев, когда ее необходимо выгрузить сотруднику без ЭЦП. Контроль обеспечивается через внутренний реквизит, который х-ранит информацию о версии к которой выгружен документ типа SIG и на Экспорте, будет делать Экспорт необходимой версии или не выгрузит sig вообще.  Другого контроля и не нужно - процесс делается для узкого круга лиц и там все люди образованные чтобы просто подменять документы. 

2. Это просто конечный продукт 2 направлений. Сначала было решение выгружать все в отдельные файлы, затем приняли решение оставить только последнюю, но код переписывать было немного лениво, поэтому просто перестали менять имя файла до поры (а то вдруг и это отменят)

Дмитрий Зайцев

Тарас, как всегда актуально и вовремя.  Тоже на работе мучают подобным вопросом...и ваша статья  - отличный материал на эту тему.

Маленькая поправка: в последних версиях системы Директум вместо:

VerNumber = _LastVersionDoc(Doc)

надо использовать: 

VerNumber = GetLastActiveEDocumentVersionNum(Doc)

И еще замечание: если в переменной FileName будут плохие символы или длинное имя и т.д., то ваш вариант получения имени  выдаст ошибку на экспорте документа, т.е. Doc.Export() не сработает:

FileName = GetTempFolder() & Doc.SYSREQ_EDOC_NAME & '.' & Doc.Info.Editor.Extension

Я бы наверно сделал так, используя ID документа: 

FileName = GetTempFolder() &Doc.ID& '.' &Doc.Info.Editor.Extension

Или так 

FileName = GetTempFolder() & ClearFileName(Doc.SYSREQ_EDOC_NAME)& '.' & Doc.Info.Editor.Extension
 
Дмитрий Зайцев: обновлено 06.06.2022 в 09:41

Авторизуйтесь, чтобы написать комментарий