TECH 8 хв

Дія.Підпис для бізнесу: технічні виклики інтеграції з державним сервісом

ECDSA + SHA256 для хешування. Redis key mismatch між start та verify. QR-код і deep link. Оновлення даних ФОП/ТОВ при кожному логіні. 4 фікси за добу. Реальна історія інтеграції з Дією — без прикрас.

Дія.Підпис для бізнесу: технічні виклики інтеграції з державним сервісом

Реальна історія: як ми інтегрували Дію.Підпис і що пішло не так (і як ми це полагодили).


Навіщо Дія.Підпис

Google OAuth — зручний, але не юридично значущий. Для LegalTech-платформи це проблема: ми маємо знати, що користувач — це конкретна фізична особа або ФОП/ТОВ, а не просто власник Gmail-акаунту.

Дія.Підпис (Diia.Sign) вирішує це:

Підключення до Дії — обов'язковий крок для будь-якої платформи, яка працює з юридичними документами в Україні.


Архітектура: як це працює

Флоу автентифікації

Користувач → LEX (натискає "Увійти через Дію")
     ↓
LEX Backend → Diia API: POST /api/v2/auth/acquirer/branch/offer/request
     ↓
Diia API → LEX: { deeplink, requestId }
     ↓
LEX Frontend → показує QR-код (з deeplink) або редіректить на deep link
     ↓
Користувач → сканує QR у Дії, підтверджує
     ↓
Diia API → LEX Callback: POST /api/diia/callback
     ↓
LEX Backend → розшифровує дані, створює/оновлює користувача
     ↓
LEX Backend → видає JWT, редіректить на фронтенд

Ключові компоненти

Компонент Технологія
Хешування requestId ECDSA + SHA256, Base64
Зберігання стану Redis (TTL 5 хвилин)
Розшифрування даних AES-256-CBC (ключ від Дії)
Сесія JWT з даними користувача

Проблема 1: ECDSA хешування requestId

Що було

Дія вимагає, щоб requestId був підписаний ECDSA з SHA-256 і закодований у Base64. Документація мінімальна. Наша перша реалізація:

// ❌ Не працювало
const hash = crypto.createHash('sha256')
  .update(requestId)
  .digest('hex');

Що пішло не так

Дія очікувала не просто хеш, а підпис ECDSA з приватним ключем acquirerToken. Формат підпису — DER, закодований у Base64. Hex-хеш — це зовсім інше.

Як полагодили

// ✅ Правильно
const sign = crypto.createSign('SHA256');
sign.update(requestId);
const signature = sign.sign(privateKey, 'base64');

Ключовий момент: приватний ключ — це не acquirerToken напряму, а PEM-ключ, який генерується при реєстрації в порталі Дії.


Проблема 2: Redis key mismatch

Що було

Флоу: /start створює запит і зберігає requestId в Redis. Коли callback приходить від Дії — бекенд шукає цей requestId в Redis, щоб зіставити з сесією.

// /start endpoint
await redis.set(`diia:request:${requestId}`, sessionData, 'EX', 300);

// /callback endpoint
const session = await redis.get(`diia:auth:${requestId}`);
// 💥 null — ключі не збігаються!

Що пішло не так

Два різних префікси: diia:request: при створенні, diia:auth: при верифікації. Класичний copy-paste баг. Callback приходив, але Redis повертав null, і автентифікація мовчки фейлилась.

Як полагодили

Уніфікували префікс:

const REDIS_PREFIX = 'diia:auth:';

// /start
await redis.set(`${REDIS_PREFIX}${requestId}`, sessionData, 'EX', 300);

// /callback
const session = await redis.get(`${REDIS_PREFIX}${requestId}`);

Проблема 3: Оновлення даних бізнесу

Що було

При першому логіні через Дію ми створювали запис ФОП/ТОВ:

Але ці дані можуть змінюватись: компанія переїхала, змінила назву, оновила контактний email.

Що пішло не так

Другий і подальші логіни ігнорували нові дані від Дії — ми просто знаходили існуючий запис по ЄДРПОУ і пропускали оновлення. Результат: застарілі адреси, старі назви офертів.

Як полагодили — 4 PR за добу

PR #1117 — оновлення назв бранчів та офертів:

// Раніше: створювали тільки якщо не існує
// Тепер: UPDATE при кожному логіні
await db.query(`
  UPDATE diia_branches
  SET name = $2, address = $3, updated_at = NOW()
  WHERE acquirer_id = $1
`, [acquirerId, branchName, address]);

PR #1118 — оновлення існуючих бранчів при ініціалізації: Додали ON CONFLICT DO UPDATE для ідемпотентності.

PR #1119 — оновлення назв офертів: Назви офертів теж могли змінюватись (наприклад, "Авторизація" → "Вхід через Дію").

PR #1120 — оновлення назви компанії та email:

await db.query(`
  UPDATE organizations
  SET name = $2, email = $3, updated_at = NOW()
  WHERE edrpou = $1
`, [edrpou, companyName, contactEmail]);

Проблема 4: Nginx proto override

Що було

Дія відправляє callback на наш URL. У продакшні — за Cloudflare та Nginx. Nginx проксює запит на бекенд, але губить оригінальний протокол.

Що пішло не так

Бекенд генерував redirect URL з http:// замість https://:

# Дія callback → Nginx → Backend
# Backend бачив: req.protocol = 'http'
# Генерував: http://legal.org.ua/auth/callback
# Браузер: mixed content error

Як полагодили

Nginx конфіг:

proxy_set_header X-Forwarded-Proto $scheme;

Express middleware:

app.set('trust proxy', 1);
// Тепер req.protocol читає X-Forwarded-Proto

Поточний стан інтеграції

Параметр Значення
Тип підпису Дія.Підпис (КЕП)
Хешування ECDSA + SHA-256 + Base64
Стан сесії Redis, TTL 5 хв
Шифрування AES-256-CBC
JWT RS256, 7 днів
Оновлення даних При кожному логіні

Що отримуємо від Дії

При успішній автентифікації Дія повертає:

Ці дані автоматично синхронізуються з нашою базою при кожному логіні.


Уроки

  1. Документація Дії — мінімальна. Готуйтесь до reverse engineering. Тестове середовище працює інакше, ніж продакшн.

  2. Redis-ключі мають бути константами. Один prefix, один файл з константами. Ніякого дублювання рядків.

  3. Дані потрібно оновлювати при кожному логіні. Не тільки створювати, а й синхронізувати. Бізнес-дані змінюються.

  4. Тестуйте весь флоу end-to-end. Unit-тести не покривають: "callback приходить, але Redis ключ інший". Тільки повний прогін від /start до JWT видає баг.

  5. Nginx — невидимий убивця. X-Forwarded-Proto, X-Real-IP, trust proxy — конфігуруйте до того, як інтеграція піде в продакшн.


Дія.Підпис — це правильний вибір для юридичної платформи. Але шлях від "документація виглядає просто" до "все працює в проді" — це 4 PR за добу і купа нетривіальних багів.


Реєстрація: legal.org.ua