Привет, habr!

Хочу поделиться опытом работы с API системой маркировки товаров «Честный Знак» (ЧЗ) / МДЛП (маркировка лекарственных препаратов), в части аутентификация и авторизация пользователей.

Не раз сталкивался с интеграцией различных источников данных в аналитические системы, но конкретно эта задача показалась достойной публикации по той причине, что в процессе работы особо не нашлось готовых гайдов в интернете, хотя такой инструмент в бизнесе может быть довольно широко востребован – и при этом его реализация может наткнуться на некоторые подводные камни.

Задача: авторизоваться и забрать в ЧЗ статистические данные (информация о движении единиц продукции между юридическими лицами, статистика конечных продаж с адресами, и тп.)

В рамках данной статьи опускается вопрос о том, что такое система «Честный знак», и каковы основы ее работы – если вы начали читать эту статью, наверняка основы вам уже известны.

Итак, по порядку…

Для работы нам понадобится:

  1. КРИПТОГРАФИЧЕСКОЕ ПО - в нашем случае -  КриптоПро. Включает в себя следующие элементы:

Чтобы проверить, все ли стоит на нашем компе ЧЗ предоставляет небольшую страничку для проверки https://markirovka.crpt.ru/plugins/cryptopro

  1. Сам сертификат КЭП. И не просто сертификат, а тот который зарегистрирован в ЧЗ.

  2. Ну и Python. Хотя в большинстве своем мы обращаемся к com-Объектам и х можно дергать из любого языка который вам по душе.

Собственно, решение вопросов с КЭП – это основное, чем хочу поделиться в рамках данной статьи.

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

После того, как вы установили Сертификат, КриптоПро и все его плагины, было бы не лишним перейти в личный кабинет (ЛК) на сайте, поскольку оттуда нам необходимо взять креды для авторизации. На текущий момент в документации (https://mdlp.crpt.ru/static/document/api_mdlp_ru.pdf) к API сказано, что для авторизации вам необходимо client_id, client_secret, user_id, первые два мы как раз таки смотрим в ЛК в разделе Администрирование – Учетные Системы:

Рекомендую также скопировать серийный номер, он нам тоже вскоре понадобиться.

Теперь переходим к самому интересному - аутентификация и авторизация. Происходит это чудо в 3 этапа. Сначала мы получаем «код аутентификации» потом подписываем его сертификатом и отправляем обратно в ЧЗ и потом можем получить авторизационный токен.

Отдельно отметим, что в документации к API ЧЗ нет ни слова о том, как подписывать этот «код аутентификации».

Тут нам пришлось идти в документацию от КриптоПро https://docs.cryptopro.ru/cades/usage

И так если ваш проект на Ubuntu вам повезло для вас сделали «Сборку расширения для языка Python», что там они насобирали и работает ли оно я не проверял. Ну а если вы на Windows далее актуально для вас.

В документации от КриптоПро есть раздел «Интерфейс COM» он то нам и нужен там даже есть примеры на VBScript, которые помогли нам отчасти. Для авторизации на нужны следующие объекты:

  • "CAdESCOM.Store" – поможет в поиске нужного сертификата в хранилище сертификатов

  • "CAdESCOM.CPSigner" – объект для подписания

  • "CAdESCOM.CPAttribute" – объект который добавляет атрибут к подписи

  • "CAdESCOM.CadesSignedData" – объект усовершенствованной подписи

Для работы нам нужно установить библиотеку win32com, установка pywin32, которую написал Mark Hammond храни его Господь. Еще одна рекомендация: держите под рукой ссылку на страницу  Tim Golden http://timgolden.me.uk/python/ на этом ресурсе есть еще много подробностей о том, как использовать Python в Windows для автоматизации и других административных задач.

И так по порядку что нужно сделать чтобы получить токен или, как он называется в документации ЧЗ, «Ключ сессии»

  1. Импортируем нужные библиотеки

import win32com.client
import os, sys
import json
import datetime
import base64
  1. Объявляем константы для методов КриптоПро

CADES_BES = 1
CADES_DEFAULT = 0
CAPICOM_ENCODE_BASE64 = 0
CAPICOM_CURRENT_USER_STORE = 2
CAPICOM_MY_STORE = 'My'
CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED = 2
  1. Ищем нужный сертификат в хранилище. Тут то нам и нужно передать, сохранённый ранее, серийный номер

oStore = win32com.client.Dispatch("CAdESCOM.Store")
oStore.Open(CAPICOM_CURRENT_USER_STORE, CAPICOM_MY_STORE, CAPICOM_STORE_OPEN_MAXIMUM_ALLOWED)
for val in oStore.Certificates:
    if val.SerialNumber == sSerialNumber:
        oCert = val
oStore.Close
  1. Вызываем объект для подписания и объект передачи атрибутов

oSigner = win32com.client.Dispatch(CAdESCOM.CPSigner")
oSigner.Certificate = oCert
oSigningTimeAttr = win32com.client.Dispatch("CAdESCOM.CPAttribute")
oSigningTimeAttr.Name = 0
oSigningTimeAttr.Value = datetime.datetime.now()
oSigner.AuthenticatedAttributes2.Add(oSigningTimeAttr)
  1. Получаем код аутентификации, а вот тут важно заменить особенность что нам не удалось достучаться до ЧЗ через всем известную библиотеку requests, на этом этапе мы потеряли немало времени, пока не догадались, что нужно использовать COM объекты Windows "WinHTTP.WinHTTPRequest.5.1"

url = "https://api.mdlp.crpt.ru/api/v1/auth"
 
params = {
    'client_id': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
    'client_secret':'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
    'user_id':'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
    'auth_type':'SIGNED_CODE'
        } 
win_http = win32com.client.Dispatch('WinHTTP.WinHTTPRequest.5.1')
win_http.Open("POST", url, False)
win_http.SetRequestHeader("Content-Type","application/json;charset=UTF-8")
win_http.SetRequestHeader("Accept","application/json;charset=UTF-8")
win_http.Send(json.dumps(params))
win_http.WaitForResponse()
print(win_http.ResponseText)
items  = json.loads(win_http.ResponseText)
CodeAuth = items['code']
  1. Далее вызываем объект усовершенствованной подписи и её создание. Данный пункт у нас тоже отнял не мало сил и поэтому я перечислю наши ошибки, чтобы вы их не повторили:

  • Обязательно убедитесь, что вы передали в переменную из полученного ответа сам код, а не весь json, потому что вы с легкостью подпишете его, а потом в ответ раз за разом вам будет прилетать ошибка авторизации.

  • Обязательно закодируйте в base64 код

  • И проверьте настройки времени, которые атрибутом добавляются к подписи на предыдущем шаге

oSignedData = win32com.client.Dispatch("CAdESCOM.CadesSignedData")
oSignedData.ContentEncoding = 1
message = CodeAuth
message_bytes = message.encode('ascii')
base64_bytes = base64.b64encode(message_bytes)
base64_message = base64_bytes.decode('ascii')
oSignedData.Content = base64_message
sSignedData = oSignedData.SignCades(oSigner, CADES_BES, False, CAPICOM_ENCODE_BASE64)
  1. Ну и заключительный шаг - получение ключа сессии

url = "https://api.mdlp.crpt.ru/api/v1/token"
paramskey ={
  'code': CodeAuth,
  'signature': sSignedData
}
print(json.dumps(paramskey))
win_http.Open("POST", url, False)
win_http.SetRequestHeader("Content-Type","application/json;charset=UTF-8")
win_http.SetRequestHeader("Accept","application/json;charset=UTF-8")
win_http.Send(json.dumps(paramskey))
win_http.WaitForResponse()
print(win_http.ResponseText)

Если в результате вы получили:

{"token":"0d90d966-5027-416f-a0cd-0697db8c79f3","life_time":600}

- поздравляю, вы сделали все верно!

В данной статье не стоит цель переписать всю документацию КриптоПро и ЧЗ, а только рассказать о решении тех проблем, которые заняли у нас больше всего времени. Если у кого-то остались вопросы – буду рад ответить в комментариях.

Комментарии (10)


  1. shark14
    00.00.0000 00:00
    -1

    Это та самая штука, из-за которой у всей минералки и кефиров приходится на кассе по 2 раза пробивать штрихкоды?
    Кто-нибудь знающий может пояснить, зачем это вообще нужно, хотя бы в случае продуктов?


    1. sepetov
      00.00.0000 00:00
      +3

      Да, это она самая. По поводу сканирования обоих штрихкодов (старый EAN + новый Datamatrix) - это криворукость или лень того, кто дорабатывал ПО на кассе под маркировку. Дело в том, что в новом штрихкоде уже есть те 13 цифр, которые были в старом штрихкоде. Их специально оставили для совместимости, чтобы не сканировать старый штрихкод. Но где-то в 1С:Рознице (могу ошибиться с названием) старый EAN гвоздями приколочен к определению товара и переделывать это не стали, так и живут. Сделали только дополнительную формочку, куда ещё Datamatrix в довесок сканировать заставили.


      1. Didimus
        00.00.0000 00:00

        Во вкусвиле при покупке надо сканировать оба кода, при возврате только квадратный...


        1. sepetov
          00.00.0000 00:00
          +1

          Ужас. Лишнюю работу заставляют делать. Я вот не могу понять - почему это даже у крупных интеграторов встречается? Взять строку и выковырять из неё 13 символов, начиная с четвёртого - это не архисложное действие. Я когда на работе добавил возможность сканировать Datamatrix-ы, мне даже в голову не пришло, что я для этого буду городить отдельную пристройку.


    1. sepetov
      00.00.0000 00:00

      Забыл ответить на вопрос: "Зачем это вообще нужно?". В интернете написано много, но вкратце - это часть внедрения цифровой экономики. На каждую пачку товара нужно приложить некий набор электронных документов. Для лекарств - регистрационное удостоверение, где уже прописано количество пачек, которое по нему произведено. Из-за этого труднее незаметно сбывать товар, произведённый мимо "плана" в чёрную. Для молочки прикладывается ветеринарный документ, чтобы из 1 литра молока производитель не сделал 30-40 килограммов масла. Для воды привязывается какой-то документ на скважину, чтобы из скважины с дебетом 5 тонн/сутки нельзя было разлить бутылок с 15-ю тоннами воды.

      Многословно, но в целом доступно? :-)


      1. klis
        00.00.0000 00:00
        +2

        Это нужно, чтобы в конечную цену включить еще один побор, который упадет в чей-то лично карман, как было с платоном, НДС +2%, 1% Михалкову с каждой флешки и компа и т.д. и т.п., больше ни для чего.


        1. sepetov
          00.00.0000 00:00

          58 копеек с каждой упаковки, если точнее. И ещё нужны вложения на оборудование (промышленный маркиратор может стоить и 250 тыс., и 1 млн., а они могут быть нужны на каждую линию, а не 1 на весь завод). А ещё ПО нужно создать или доработать. При этом API у "Честного знака" ещё периодически меняется, поэтому нужна поддержка. В таких условиях это и воспринимается как поборы.


          1. Didimus
            00.00.0000 00:00
            +1

            Как всегда, цель хорошая, реализация не очень


            1. sepetov
              00.00.0000 00:00

              Особенно плохо в организационном плане.

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

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


  1. MaxDown
    00.00.0000 00:00

    Аналогично писал на C#

    Сейчас же работает на Airflow с Python, где просто подключили криптопро