Skip to content

Instantly share code, notes, and snippets.

@tripolskypetr
Created February 13, 2026 12:09
Show Gist options
  • Select an option

  • Save tripolskypetr/1497e3a2d724a568f415161dc8e5d167 to your computer and use it in GitHub Desktop.

Select an option

Save tripolskypetr/1497e3a2d724a568f415161dc8e5d167 to your computer and use it in GitHub Desktop.
# GSI Mobile SDK
## Техническая документация
**Версия документа:** 1.0
**Дата:** Декабрь 2024
---
## Глоссарий
| Термин | Расшифровка |
|--------|-------------|
| ГЦИ | Главный центр информатизации — контрагент, обслуживающий запросы мобильных SDK через прокси банков |
| ГЦП | Государственный центр персонализации — государственный орган, хранящий персональные данные граждан (аналог МВД) |
| ПИНФЛ | Персональный идентификационный номер физического лица |
| mTLS | Mutual TLS — двусторонняя аутентификация с использованием клиентских сертификатов |
| IPSec | Internet Protocol Security — протокол защиты сетевого трафика на уровне IP |
| Liveness | Проверка «живости» — подтверждение, что перед камерой находится живой человек, а не фотография или видеозапись |
| JWT | JSON Web Token — стандарт токенов для безопасной передачи данных |
---
## 1. Общая информация о продукте
GSI Mobile SDK — программный комплекс для биометрической верификации личности граждан Республики Узбекистан. SDK обеспечивает проверку соответствия лица пользователя данным в государственных реестрах с использованием технологии распознавания лиц и пассивного liveness detection.
| Параметр | Значение |
|----------|----------|
| Официальное название | gsi-mobile-sdk |
| Платформы | iOS, Android, Flutter |
| Минимальная версия iOS | 13.0 |
| Минимальная версия Android | 6.0 (API level 23) |
| Языки SDK | Kotlin (Android), Swift (iOS) |
SDK предоставляет доступ к следующим данным при успешной верификации: ПИНФЛ, серия и номер паспорта, ФИО, дата рождения, история предыдущих паспортов.
---
## 2. Архитектура решения
### 2.1 Схема взаимодействия компонентов
Архитектура системы построена на принципе сегментации сети с разделением на публичную зону (Public FQDN) и закрытую сеть.
```mermaid
flowchart LR
subgraph public["Public FQDN"]
SDK["Мобильное SDK"]
end
subgraph bank["Инфраструктура банка"]
PROXY["mTLS Proxy"]
BACKEND["Backend банка"]
end
subgraph closed["Закрытая сеть"]
GCI["ГЦИ"]
GCP["ГЦП"]
end
SDK -->|"Mutual SSL"| PROXY
PROXY -->|"IPSec VPN"| GCI
GCI -->|"Внутренний канал"| GCP
style public fill:#e8f5e9
style closed fill:#fff3e0
style bank fill:#e3f2fd
```
Мобильное SDK находится в публичной зоне и взаимодействует с прокси-сервером банка через Mutual SSL. Прокси-сервер банка передаёт запросы в закрытую сеть ГЦИ через IPSec VPN туннель. ГЦИ обращается к ГЦП для получения эталонных биометрических данных.
### 2.2 Serverless-режим с криптографической защитой
В данном режиме SDK получает подписанный ответ напрямую. Защита данных обеспечивается JWT-подписью ответа сервера ГЦИ.
```mermaid
sequenceDiagram
participant SDK as Мобильное SDK
participant Bank as mTLS Proxy банка
participant GCI as ГЦИ
participant GCP as ГЦП
SDK->>Bank: Видео + accessToken (JWT)
Bank->>GCI: Проксирование запроса (IPSec)
GCI->>GCP: Запрос эталонного фото
GCP-->>GCI: Эталонное фото
GCI->>GCI: Liveness + сравнение лиц
GCI-->>Bank: Ответ с JWT подписью (HS256)
Bank-->>SDK: Персональные данные + signature
SDK->>SDK: Отображение результата
```
Банк получает полные персональные данные в ответе. JWT-подпись позволяет верифицировать подлинность ответа на backend банка.
### 2.3 Webhook-режим с пустым body
В данном режиме SDK получает пустые строки вместо персональных данных. Реальные данные отправляются на webhook банка напрямую с прокси-сервера.
```mermaid
sequenceDiagram
participant SDK as Мобильное SDK
participant Proxy as mTLS Proxy банка
participant Backend as Backend банка
participant GCI as ГЦИ
participant GCP as ГЦП
SDK->>Proxy: Видео + accessToken (JWT с verificationId)
Proxy->>GCI: Проксирование запроса (IPSec)
GCI->>GCP: Запрос эталонного фото
GCP-->>GCI: Эталонное фото
GCI->>GCI: Liveness + сравнение лиц
GCI-->>Proxy: Персональные данные + signature
Proxy->>Proxy: Извлечение verificationId из JWT
Proxy->>Backend: Webhook с данными и verificationId
Proxy->>Proxy: Замена данных на пустые строки
Proxy-->>SDK: Пустые строки + статус OK
SDK->>SDK: Отображение успеха
Backend->>Backend: Сохранение данных по verificationId
```
Преимущества webhook-режима: персональные данные не передаются на клиентское устройство, что исключает их перехват на стороне пользователя.
---
## 3. Состав SDK и комплект поставки
### 3.1 Комплект поставки
Поставка осуществляется в виде ZIP-архива, содержащего следующие компоненты:
- `.framework` для iOS (TGFISBIN.xcframework)
- `.aar` для Android (facedetectsdk.aar)
- Flutter bindings для кроссплатформенной разработки
- OpenAPI-схема для интеграции через REST API
- Пример кода C# прокси для настройки Mutual SSL
- Демо-приложения для iOS, Android, Flutter
- Документация и changelog
Для получения обновлений предусмотрено подключение Maven-репозитория. Flutter-версия опубликована в pub.dev.
### 3.2 Внешние зависимости
**Android:**
```gradle
implementation("androidx.camera:camera-core:1.4.2")
implementation("androidx.camera:camera-lifecycle:1.4.2")
implementation("androidx.camera:camera-video:1.4.2")
implementation("androidx.camera:camera-view:1.4.2")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("com.google.mlkit:face-detection:16.1.7")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("com.scottyab:rootbeer-lib:0.1.1")
implementation("com.googlecode.mp4parser:isoparser:1.1.22")
implementation("androidx.camera:camera-camera2:1.3.0")
```
**iOS:** Используются стандартные библиотеки платформы, дополнительные зависимости не требуются.
### 3.3 Инфраструктурные требования
| Окружение | Требования |
|-----------|------------|
| Тестирование | Домен `face.mbabm.uz` (Public FQDN) |
| Production | IPSec-туннель от сервера ГЦИ до банка |
| Прокси | Mutual SSL proxy на стороне банка (nginx или C#) |
---
## 4. Функциональность Face IDS
### 4.1 Типы операций
SDK поддерживает верификацию 1:1 с поиском по серии/номеру паспорта или ПИНФЛ. Применимо для регистрации пользователей и валидации платежей.
### 4.2 Liveness Detection
Реализован пассивный liveness по видео — специальные жесты от пользователя не требуются. Система проверяет: открыты ли глаза, одно ли лицо в кадре, положение головы.
### 4.3 Требования к съёмке
| Параметр | Требование |
|----------|------------|
| Освещение | По ГОСТ для жилых помещений (не более 300 люкс) |
| Угол наклона головы | Не более 30° |
| Количество лиц | Строго одно лицо в кадре |
| Расстояние | Оптимально 30-50 см от камеры |
### 4.4 Входные данные
SDK принимает только видео. Изображения не поддерживаются. Из видео автоматически извлекается лучший кадр, центрированный по лицу.
### 4.5 Результат верификации
При успешной верификации возвращается объект со следующими полями:
| Поле | Тип | Описание |
|------|-----|----------|
| `pin` | String | ПИНФЛ |
| `firstName` | String | Имя |
| `lastName` | String | Фамилия |
| `patronym` | String | Отчество |
| `birthDate` | String | Дата рождения (DD.MM.YYYY) |
| `docSeria` | String | Серия паспорта |
| `docNumber` | String | Номер паспорта |
| `score` | Double | Confidence score (0.0 — 1.0) |
| `photo` | String | Фото лица из видео (base64) |
| `signature` | String | JWT подпись ответа |
При неуспешной верификации возвращается `null` без дополнительных данных.
### 4.6 Коды ошибок
При неуспешной верификации SDK возвращает код ошибки в поле `errorCode`. Коды сгруппированы по категориям для удобства обработки в кастомной вёрстке.
#### 4.6.1 Ошибки ГЦП (GSI API Errors)
| Код | Сообщение | Описание | Рекомендация пользователю |
|-----|-----------|----------|---------------------------|
| `GSI_API_ERROR` | ГЦП недоступен | Сервер ГЦП не отвечает или вернул ошибку | Попробуйте позже |
| `GSI_NO_DATA` | ГЦП не передал данные | ГЦП не нашёл данные по указанным реквизитам | Проверьте серию и номер паспорта |
#### 4.6.2 Ошибки Liveness-проверки
| Код | Сообщение | Описание | Рекомендация пользователю |
|-----|-----------|----------|---------------------------|
| `LIVENESS_NOT_ALIVE` | Не пройден liveness (#1) | Система определила, что перед камерой не живой человек | Повторите попытку, держите телефон ровно |
| `LIVENESS_NO_AVERAGE` | Не пройден liveness (#2) | Не удалось вычислить усреднённый кадр | Повторите попытку с лучшим освещением |
| `LIVENESS_NO_BEST_FACE` | Не пройден liveness (#3) | Не найден качественный кадр с лицом | Повторите попытку, смотрите в камеру |
| `LIVENESS_NO_NORMALIZED` | Не пройден liveness (#4) | Не удалось нормализовать изображение лица | Повторите попытку |
| `LIVENESS_SCORE_TOO_LOW` | Не пройден liveness (#5) | Низкий показатель liveness | Повторите попытку, не используйте фото |
#### 4.6.3 Ошибки детекции лица в видео
| Код | Сообщение | Описание | Рекомендация пользователю |
|-----|-----------|----------|---------------------------|
| `LIVENESS_MULTIPLE_FACES` | Несколько лиц в кадре | В видео обнаружено более одного лица | Убедитесь, что в кадре только вы |
#### 4.6.4 Ошибки положения глаз и головы
| Код | Сообщение | Описание | Рекомендация пользователю |
|-----|-----------|----------|---------------------------|
| `EYES_CLOSED_OR_HIDDEN` | Глаза закрыты или не видны | Система не может определить открытые глаза | Откройте глаза, снимите очки |
| `HEAD_TURNED` | Голова повернута | Угол наклона головы превышает допустимый | Держите голову прямо, смотрите в камеру |
#### 4.6.5 Ошибки эталонного фото из ГЦП
| Код | Сообщение | Описание | Рекомендация пользователю |
|-----|-----------|----------|---------------------------|
| `GSI_PHOTO_MULTIPLE_FACES` | Фото из ГЦП содержит несколько лиц | Эталонное фото некорректно | Обратитесь в поддержку |
| `GSI_PHOTO_NO_FACE` | Фото из ГЦП не содержит лицо | На эталонном фото не найдено лицо | Обратитесь в поддержку |
#### 4.6.6 Ошибки сравнения лиц
| Код | Сообщение | Описание | Рекомендация пользователю |
|-----|-----------|----------|---------------------------|
| `FACEIDS_API_ERROR` | Ошибка API FaceIds | Внутренняя ошибка сервиса сравнения | Повторите попытку позже |
| `SIMILARITY_SCORE_TOO_LOW` | Низкое сходство с фото из ГЦП | Лицо не совпадает с эталоном | Убедитесь, что это ваш паспорт |
#### 4.6.7 Ошибки валидации документа
| Код | Сообщение | Описание | Рекомендация пользователю |
|-----|-----------|----------|---------------------------|
| `PASSPORT_INVALID` | Паспорт недействителен | Документ аннулирован или не существует | Проверьте данные паспорта |
| `DOCUMENT_EXPIRED` | Истёк срок действия документа | Паспорт просрочен | Обновите документ |
#### 4.6.8 Ошибки валидации запроса
| Код | Сообщение | Описание | Рекомендация пользователю |
|-----|-----------|----------|---------------------------|
| `INVALID_PAYLOAD` | Неверные параметры запроса | Отсутствуют обязательные поля или неверный формат | Проверьте данные |
#### 4.6.9 Общие ошибки
| Код | Сообщение | Описание | Рекомендация пользователю |
|-----|-----------|----------|---------------------------|
| `UNKNOWN_ERROR` | Неизвестная ошибка | Непредвиденная ошибка сервера | Повторите попытку позже |
#### 4.6.10 Пример обработки ошибок в кастомной вёрстке
```typescript
const VerificationErrorMessage: Record<string, string> = {
// Ошибки ГЦП
"GSI_API_ERROR": "ГЦП недоступен. Попробуйте позже",
"GSI_NO_DATA": "ГЦП не передал данные. Проверьте серию и номер паспорта",
// Ошибки Liveness
"LIVENESS_NOT_ALIVE": "Не пройден liveness (#1)",
"LIVENESS_NO_AVERAGE": "Не пройден liveness (#2)",
"LIVENESS_NO_BEST_FACE": "Не пройден liveness (#3)",
"LIVENESS_NO_NORMALIZED": "Не пройден liveness (#4)",
"LIVENESS_SCORE_TOO_LOW": "Не пройден liveness (#5)",
// Ошибки детекции лица
"LIVENESS_MULTIPLE_FACES": "Фото содержит больше одного лица. Обратитесь в поддержку",
// Ошибки положения
"EYES_CLOSED_OR_HIDDEN": "Глаза закрыты или не видны",
"HEAD_TURNED": "Голова повернута",
// Ошибки эталонного фото
"GSI_PHOTO_MULTIPLE_FACES": "Фото из ГЦП содержит больше одного лица. Обратитесь в поддержку",
"GSI_PHOTO_NO_FACE": "Фото из ГЦП не содержит лицо. Обратитесь в поддержку",
// Ошибки сравнения
"FACEIDS_API_ERROR": "Ошибка из API FaceIds",
"SIMILARITY_SCORE_TOO_LOW": "Низкое сходство с фото из ГЦП",
// Ошибки документа
"PASSPORT_INVALID": "Паспорт недействителен",
"DOCUMENT_EXPIRED": "Истек срок действия документа",
// Ошибки запроса
"INVALID_PAYLOAD": "Неверные параметры запроса. Проверьте данные",
// Общие
"UNKNOWN_ERROR": "Произошла неизвестная ошибка",
};
function getErrorMessage(errorCode: string): string {
return VerificationErrorMessage[errorCode] || "Произошла ошибка";
}
function isRetryableError(errorCode: string): boolean {
const retryable = [
"GSI_API_ERROR",
"FACEIDS_API_ERROR",
"UNKNOWN_ERROR",
"LIVENESS_NOT_ALIVE",
"LIVENESS_NO_AVERAGE",
"LIVENESS_NO_BEST_FACE",
"LIVENESS_NO_NORMALIZED",
"LIVENESS_SCORE_TOO_LOW",
"EYES_CLOSED_OR_HIDDEN",
"HEAD_TURNED",
"LIVENESS_MULTIPLE_FACES",
];
return retryable.includes(errorCode);
}
function requiresSupport(errorCode: string): boolean {
const supportRequired = [
"GSI_PHOTO_MULTIPLE_FACES",
"GSI_PHOTO_NO_FACE",
];
return supportRequired.includes(errorCode);
}
```
---
## 5. API и методы SDK
### 5.1 Паттерн инициализации
SDK использует паттерн Builder для всех платформ. Единственный публичный метод — `startFaceDetection` / `startVerification`. Метод открывает полноэкранное activity с WebView и возвращает объект с персональными данными при успехе или `null` при неудаче.
### 5.2 Android (Kotlin)
```kotlin
class MainActivity : ComponentActivity() {
private lateinit var resultTextView: TextView
private val launcher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
result.data?.let { intent ->
val person = FaceDetectionSdk.getResultFromIntent(intent)
resultTextView.text = """
ПИНФЛ: ${person.pin}
ФИО: ${person.lastName} ${person.firstName} ${person.patronym}
Дата рождения: ${person.birthDate}
Паспорт: ${person.docSeria}${person.docNumber}
Score: ${person.score}
""".trimIndent()
}
} else {
resultTextView.text = "Верификация не пройдена"
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
resultTextView = findViewById(R.id.resultTextView)
findViewById<Button>(R.id.verifyButton).setOnClickListener {
startVerification()
}
}
private fun startVerification() {
FaceDetectionSdk.Builder(this)
.setAccessToken("YOUR_JWT_TOKEN")
.setUrl("https://your-proxy.bank.uz/")
.setLanguage("ru")
.setClientP12Base64(getClientCertificate(), "cert_password")
.setEnableMutualSSL(true)
.build()
.startFaceDetection(launcher)
}
private fun getClientCertificate(): String {
// Получить сертификат с сервера банка динамически
// КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО вшивать в APK
return "BASE64_P12_FROM_SERVER"
}
}
```
### 5.3 iOS (Swift)
```swift
import SwiftUI
import TGFISBIN
struct ContentView: View {
@State private var resultText = ""
@State private var personPhoto: UIImage?
var body: some View {
VStack(spacing: 20) {
if let photo = personPhoto {
Image(uiImage: photo)
.resizable()
.scaledToFit()
.frame(width: 150, height: 150)
.clipShape(Circle())
}
Text(resultText)
.multilineTextAlignment(.center)
Button("Начать верификацию") {
startVerification()
}
}
.padding()
}
private func startVerification() {
let sslConfig = SSLConfig(
clientCertificateBase64: getClientCertificate(),
clientCertificatePassword: "cert_password",
enableMutualSSL: true
)
let config = SDKConfig(
locale: .ru,
token: "YOUR_JWT_TOKEN",
baseUrl: "https://your-proxy.bank.uz/",
ssl: sslConfig,
uiConfig: nil,
resultType: .standard,
useNative: true,
privacyPolicyUrl: nil
)
GSI.shared.start(config: config) { result in
guard let result = result,
let person = result.standardResult else {
resultText = "Верификация не пройдена"
return
}
resultText = """
ПИНФЛ: \(person.pin)
ФИО: \(person.lastName) \(person.firstName) \(person.patronym)
Дата рождения: \(person.birthDate)
Паспорт: \(person.docSeria)\(person.docNumber)
Score: \(String(format: "%.0f%%", person.score * 100))
"""
if let photoData = Data(base64Encoded: person.photo) {
personPhoto = UIImage(data: photoData)
}
}
}
private func getClientCertificate() -> String {
// Получить сертификат с сервера банка динамически
// КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО вшивать в IPA
return "BASE64_P12_FROM_SERVER"
}
}
```
### 5.4 Flutter (Dart)
```dart
import 'package:flutter/material.dart';
import 'package:facedetect_tadi_sdk/facedetect_tadi_sdk.dart';
import 'dart:convert';
class VerificationScreen extends StatefulWidget {
const VerificationScreen({super.key});
@override
State<VerificationScreen> createState() => _VerificationScreenState();
}
class _VerificationScreenState extends State<VerificationScreen> {
final _sdk = FacedetectTadiSdk();
String _resultText = '';
MemoryImage? _personPhoto;
Future<void> _startVerification() async {
final config = SdkConfig(
accessToken: 'YOUR_JWT_TOKEN',
baseUrl: 'https://your-proxy.bank.uz/',
language: 'ru',
enableMutualSSL: true,
clientCertificateBase64: await _getClientCertificate(),
clientCertificatePassword: 'cert_password',
);
final result = await _sdk.startFaceDetection(config);
if (result == null) {
setState(() {
_resultText = 'Верификация не пройдена';
_personPhoto = null;
});
return;
}
setState(() {
_resultText = '''
ПИНФЛ: ${result.pin}
ФИО: ${result.lastName} ${result.firstName} ${result.patronym}
Дата рождения: ${result.birthDate}
Паспорт: ${result.docSeria}${result.docNumber}
Score: ${(result.score * 100).toStringAsFixed(0)}%
''';
if (result.photo.isNotEmpty) {
_personPhoto = MemoryImage(base64Decode(result.photo));
}
});
}
Future<String> _getClientCertificate() async {
// Получить сертификат с сервера банка динамически
// КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО вшивать в приложение
return 'BASE64_P12_FROM_SERVER';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Верификация')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_personPhoto != null)
CircleAvatar(radius: 75, backgroundImage: _personPhoto),
const SizedBox(height: 20),
Text(_resultText, textAlign: TextAlign.center),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _startVerification,
child: const Text('Начать верификацию'),
),
],
),
),
),
);
}
}
```
### 5.5 REST API Endpoints
SDK использует следующие endpoints для взаимодействия с сервером:
```
POST /api/v1/gsi/verify — multipart/form-data (видео файлом)
POST /api/v1/gsi/verify_b64 — JSON (видео в base64)
```
### 5.6 OpenAPI-схема
```yaml
openapi: 3.0.3
info:
title: GSI Verification API
description: API для верификации граждан через систему ГЦП с проверкой liveness
version: 1.0.0
servers:
- url: /api/v1/gsi
description: Production server
paths:
/verify_b64:
post:
summary: Верификация с видео в формате base64
description: |
Выполняет полную верификацию гражданина:
- Проверка данных в ГЦП
- Liveness-проверка видео
- Сравнение лица с фото из ГЦП
- Проверка срока действия документа
operationId: verifyB64
tags:
- Verification
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VerificationRequest'
responses:
'200':
description: Верификация успешна
content:
application/json:
schema:
$ref: '#/components/schemas/VerificationSuccessResponse'
'403':
description: Верификация не пройдена
content:
application/json:
schema:
$ref: '#/components/schemas/VerificationErrorResponse'
/verify:
post:
summary: Верификация с видео в формате multipart/form-data
operationId: verify
tags:
- Verification
requestBody:
required: true
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/VerificationRequestMultipart'
responses:
'200':
description: Верификация успешна
content:
application/json:
schema:
$ref: '#/components/schemas/VerificationSuccessResponse'
'403':
description: Верификация не пройдена
content:
application/json:
schema:
$ref: '#/components/schemas/VerificationErrorResponse'
components:
schemas:
VerificationRequest:
type: object
required:
- serviceName
- clientId
- userId
- token
- requestId
- birth_date
- doc_number
- doc_seria
- doc_pinfl
- video
properties:
serviceName:
type: string
description: Название сервиса
clientId:
type: string
description: Идентификатор клиента
userId:
type: string
description: Идентификатор пользователя
token:
type: string
description: Токен авторизации
requestId:
type: string
description: Уникальный идентификатор запроса
birth_date:
type: string
format: date
description: Дата рождения (DD.MM.YYYY)
doc_number:
type: string
description: Номер документа
doc_seria:
type: string
description: Серия документа
doc_pinfl:
type: string
description: ПИНФЛ
video:
type: string
format: byte
description: Видео в формате base64
VerificationRequestMultipart:
type: object
required:
- serviceName
- clientId
- userId
- token
- requestId
- birth_date
- doc_number
- doc_seria
- doc_pinfl
- video
properties:
serviceName:
type: string
clientId:
type: string
userId:
type: string
token:
type: string
requestId:
type: string
birth_date:
type: string
format: date
doc_number:
type: string
doc_seria:
type: string
doc_pinfl:
type: string
video:
type: string
format: binary
description: Видео файл (MP4)
VerificationSuccessResponse:
type: object
required:
- status
- clientId
- requestId
- userId
- serviceName
- data
properties:
status:
type: string
enum: [ok]
clientId:
type: string
requestId:
type: string
userId:
type: string
serviceName:
type: string
data:
$ref: '#/components/schemas/PersonData'
PersonData:
type: object
properties:
pin:
type: string
description: ПИНФЛ
doc_pinfl:
type: string
doc_seria:
type: string
doc_number:
type: string
namelat:
type: string
description: Имя (латиница)
surnamelat:
type: string
description: Фамилия (латиница)
patronymlat:
type: string
description: Отчество (латиница)
namecyr:
type: string
description: Имя (кириллица)
surnamecyr:
type: string
description: Фамилия (кириллица)
patronymcyr:
type: string
description: Отчество (кириллица)
birth_date:
type: string
format: date
birthplace:
type: string
citizenship:
type: string
citizenshipid:
type: string
gender:
type: string
description: "1 - мужской, 2 - женский"
livestatus:
type: string
description: Статус жизни
current_pinpp:
type: string
current_document:
type: string
signature:
type: string
description: JWT подпись
capture:
type: string
format: byte
description: Изображение лица (base64)
photo:
type: string
format: byte
description: Эталонное фото из ГЦП (base64)
score:
type: number
format: float
minimum: 0
maximum: 1
createdAt:
type: string
format: date-time
VerificationErrorResponse:
type: object
required:
- status
- error
- errorCode
- clientId
- requestId
- serviceName
- userId
properties:
status:
type: string
enum: [error]
error:
type: string
description: Человекочитаемое описание ошибки
errorCode:
type: string
enum:
- GSI_API_ERROR
- GSI_NO_DATA
- LIVENESS_NOT_ALIVE
- LIVENESS_NO_AVERAGE
- LIVENESS_NO_BEST_FACE
- LIVENESS_NO_NORMALIZED
- LIVENESS_SCORE_TOO_LOW
- LIVENESS_MULTIPLE_FACES
- EYES_CLOSED_OR_HIDDEN
- HEAD_TURNED
- GSI_PHOTO_MULTIPLE_FACES
- GSI_PHOTO_NO_FACE
- FACEIDS_API_ERROR
- SIMILARITY_SCORE_TOO_LOW
- PASSPORT_INVALID
- DOCUMENT_EXPIRED
- INVALID_PAYLOAD
- UNKNOWN_ERROR
clientId:
type: string
requestId:
type: string
serviceName:
type: string
userId:
type: string
```
### 5.7 Структура запроса
| Поле | Тип | Обязательное | Описание |
|------|-----|--------------|----------|
| `serviceName` | string | Да | Название сервиса |
| `clientId` | string | Да | Идентификатор клиента |
| `userId` | string | Да | Идентификатор пользователя |
| `token` | string | Да | Токен авторизации (JWT) |
| `requestId` | string | Да | Уникальный идентификатор запроса |
| `birth_date` | string | Да | Дата рождения (DD.MM.YYYY) |
| `doc_number` | string | Да | Номер документа |
| `doc_seria` | string | Да | Серия документа |
| `doc_pinfl` | string | Да | ПИНФЛ |
| `video` | binary/base64 | Да | Видео для liveness |
### 5.8 Пример успешного ответа (HTTP 200)
```json
{
"status": "ok",
"clientId": "unknown",
"requestId": "cecb73b4-0035-47bf-9cfe-cc3c8aadff41",
"userId": "unknown",
"serviceName": "unknown",
"data": {
"id": 3,
"name": "PINFL=31006996530022 BIRTH=10.06.1999",
"pin": "",
"sex": "1",
"namecyr": "ПЕТР",
"namelat": "PETR",
"pinpps_1": "31006996530022",
"doc_pinfl": "31006996530022",
"birth_date": "10.06.1999",
"birthplace": "ROSSIYA FEDERASIYASI",
"livestatus": "1",
"surnamecyr": "ТРИПОЛЬСКИЙ",
"surnamelat": "TRIPOLSKIY",
"citizenship": "РОССИЙСКАЯ ФЕДЕРАЦИЯ",
"patronymcyr": "ПЕТРОВИЧ",
"patronymlat": "PETROVICH",
"citizenshipid": "192",
"current_pinpp": "31006996530022",
"current_document": "763490940",
"foreign_documents_1_document": "763490940",
"foreign_documents_1_datebegin": "2020-10-01",
"foreign_documents_1_docgiveplace": "РОССИЯ",
"foreign_documents_1_citizenshipid": "192",
"createdDate": "2025-12-05T02:13:30.350985+05:00",
"modifiedDate": "2025-12-05T02:13:30.656538+05:00",
"photo": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQN...",
"signature": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJib2R5Ijoie1wiaWRcIjozLFwibmFtZV...",
"capture": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQD...",
"score": 0.8402466,
"createdAt": "2025-12-30T12:54:17.348Z"
}
}
```
### 5.9 Пример ответа с ошибкой (HTTP 403)
```json
{
"status": "error",
"error": "Низкое сходство с фото из ГЦП",
"errorCode": "SIMILARITY_SCORE_TOO_LOW",
"clientId": "unknown",
"requestId": "4fd3acd4-cc20-40ab-9257-a62cffdb611b",
"serviceName": "unknown",
"userId": "unknown"
}
```
### 5.10 Описание полей успешного ответа
| Поле | Тип | Описание |
|------|-----|----------|
| `status` | string | Статус операции: `ok` |
| `clientId` | string | Идентификатор клиента из запроса |
| `requestId` | string | Идентификатор запроса |
| `userId` | string | Идентификатор пользователя из запроса |
| `serviceName` | string | Название сервиса из запроса |
| `data.pin` | string | ПИНФЛ |
| `data.doc_pinfl` | string | ПИНФЛ из документа |
| `data.namelat` | string | Имя (латиница) |
| `data.surnamelat` | string | Фамилия (латиница) |
| `data.patronymlat` | string | Отчество (латиница) |
| `data.namecyr` | string | Имя (кириллица) |
| `data.surnamecyr` | string | Фамилия (кириллица) |
| `data.patronymcyr` | string | Отчество (кириллица) |
| `data.birth_date` | string | Дата рождения |
| `data.birthplace` | string | Место рождения |
| `data.citizenship` | string | Гражданство |
| `data.livestatus` | string | Статус: `1` — жив |
| `data.current_pinpp` | string | Текущий ПИНФЛ |
| `data.current_document` | string | Номер текущего документа |
| `data.photo` | string | Эталонное фото из ГЦП (base64 JPEG) |
| `data.capture` | string | Кадр из видео (base64 JPEG) |
| `data.signature` | string | JWT-подпись ответа (HS256) |
| `data.score` | number | Коэффициент сходства (0.0 — 1.0) |
| `data.createdAt` | string | Дата и время верификации (ISO 8601) |
### 5.11 Структура JWT-подписи (signature)
JWT-подпись содержит следующие claims:
| Claim | Описание |
|-------|----------|
| `body` | JSON-строка с данными гражданина |
| `score` | Коэффициент сходства |
| `serviceName` | Название сервиса |
| `clientId` | Идентификатор клиента |
| `userId` | Идентификатор пользователя |
| `requestId` | Идентификатор запроса |
| `photoHash` | SHA-256 хеш эталонного фото |
| `captureHash` | SHA-256 хеш кадра из видео |
| `videoHash` | SHA-256 хеш видео |
| `token` | Токен из запроса |
| `issuerName` | Имя банка-эмитента |
| `createdAt` | Дата создания |
| `iat` | Issued At (Unix timestamp) |
| `exp` | Expiration (Unix timestamp) |
### 5.12 История паспортов и полная схема данных гражданина
При верификации по ПИНФЛ возвращается расширенная информация о гражданине, включая историю документов. Вложенные массивы (до 5 документов) представлены в виде плоской структуры с суффиксом `_N_`, где N — порядковый номер элемента (1-5).
SDK автоматически преобразует плоские поля в массивы для удобства работы.
#### 5.12.1 Полная схема данных (HumanModel)
```typescript
/**
* Citizen Data Schema - Flattened Document Interface
* Combined schema from passport and PINFL service responses
* All fields are optional as not all appear in every response variant
* Nested arrays are flattened: documents[0].field -> documents_1_field
*/
export interface HumanModel {
// === Уникальный идентификатор записи ===
name: string; // Внутренний идентификатор записи в системе
// === Идентификация ===
pin: string; // ПИНФЛ
current_pinpp: string; // Текущий ПИНФЛ (может отличаться при смене)
pinpps_1: string; // Первичный ПИНФЛ
pinpps_2: string; // Альтернативный ПИНФЛ #2
pinpps_3: string; // Альтернативный ПИНФЛ #3
pinpps_4: string; // Альтернативный ПИНФЛ #4
document: string; // Номер документа
current_document: string; // Номер текущего документа
transaction_id: string; // ID транзакции в ГЦП
// === ФИО (латиница, стандартные поля) ===
first_name: string; // Имя
last_name: string; // Фамилия
patronym: string; // Отчество
surname: string; // Фамилия (альтернативное поле)
givenname: string; // Имя (альтернативное поле)
// === ФИО (варианты написания) ===
namelat: string; // Имя (латиница, транслит)
surnamelat: string; // Фамилия (латиница, транслит)
patronymlat: string; // Отчество (латиница, транслит)
namecyr: string; // Имя (кириллица)
surnamecyr: string; // Фамилия (кириллица)
patronymcyr: string; // Отчество (кириллица)
engname: string; // Имя (английский)
engsurname: string; // Фамилия (английский)
engpatronym: string; // Отчество (английский)
patronym_en: string; // Отчество (английский, альтернативное)
// === Текущий документ ===
doc_pinfl: string; // ПИНФЛ в документе
doc_seria: string; // Серия документа
doc_number: string; // Номер документа
date_issue: string; // Дата выдачи
date_expiry: string; // Дата окончания срока действия
give_place: string; // Код места выдачи
give_place_name: string; // Название места выдачи
// === История документов (до 5 записей) ===
// Формат: documents_N_field, где N = 1-5
documents_1_document: string; // Номер документа #1
documents_1_type: string; // Тип документа
documents_1_datebegin: string; // Дата выдачи
documents_1_dateend: string; // Дата окончания
documents_1_docgiveplace: string; // Место выдачи
documents_1_docgiveplaceid: string; // ID места выдачи
documents_1_status: string; // Статус документа
documents_2_document: string;
documents_2_type: string;
documents_2_datebegin: string;
documents_2_dateend: string;
documents_2_docgiveplace: string;
documents_2_docgiveplaceid: string;
documents_2_status: string;
documents_3_document: string;
documents_3_type: string;
documents_3_datebegin: string;
documents_3_dateend: string;
documents_3_docgiveplace: string;
documents_3_docgiveplaceid: string;
documents_3_status: string;
documents_4_document: string;
documents_4_type: string;
documents_4_datebegin: string;
documents_4_dateend: string;
documents_4_docgiveplace: string;
documents_4_docgiveplaceid: string;
documents_4_status: string;
documents_5_document: string;
documents_5_type: string;
documents_5_datebegin: string;
documents_5_dateend: string;
documents_5_docgiveplace: string;
documents_5_docgiveplaceid: string;
documents_5_status: string;
// === Иностранные документы (для неграждан, до 5 записей) ===
// Формат: foreign_documents_N_field, где N = 1-5
foreign_documents_1_document: string; // Номер иностранного документа #1
foreign_documents_1_citizenshipid: string; // ID гражданства
foreign_documents_1_datebegin: string; // Дата выдачи
foreign_documents_1_dateend: string; // Дата окончания
foreign_documents_1_docgiveplace: string; // Место выдачи
foreign_documents_2_document: string;
foreign_documents_2_citizenshipid: string;
foreign_documents_2_datebegin: string;
foreign_documents_2_dateend: string;
foreign_documents_2_docgiveplace: string;
foreign_documents_3_document: string;
foreign_documents_3_citizenshipid: string;
foreign_documents_3_datebegin: string;
foreign_documents_3_dateend: string;
foreign_documents_3_docgiveplace: string;
foreign_documents_4_document: string;
foreign_documents_4_citizenshipid: string;
foreign_documents_4_datebegin: string;
foreign_documents_4_dateend: string;
foreign_documents_4_docgiveplace: string;
foreign_documents_5_document: string;
foreign_documents_5_citizenshipid: string;
foreign_documents_5_datebegin: string;
foreign_documents_5_dateend: string;
foreign_documents_5_docgiveplace: string;
// === Информация о рождении ===
birth_date: string; // Дата рождения (DD.MM.YYYY)
birth_place: string; // Место рождения
birthplace: string; // Место рождения (альтернативное поле)
birth_country: string; // Код страны рождения
birth_country_name: string; // Название страны рождения
birthcountry: string; // Страна рождения (альтернативное)
birthcountryid: string; // ID страны рождения
// === Гражданство и национальность ===
citizenship: string; // Гражданство (название)
citizenship_name: string; // Гражданство (полное название)
citizenshipid: string; // ID гражданства
nationality: string; // Национальность (код)
nationality_name: string; // Национальность (название)
nationalityid: string; // ID национальности
// === Пол и статус ===
gender: string; // Пол (1 - мужской, 2 - женский)
gender_name: string; // Название пола
sex: string; // Пол (альтернативное поле)
subject_state: string; // Код состояния субъекта
subject_state_name: string; // Название состояния субъекта
livestatus: string; // Статус жизни (1 - жив)
}
```
#### 5.12.2 Преобразование плоских полей в массивы
SDK предоставляет функцию для преобразования плоских полей документов в массивы:
```typescript
interface DocumentRecord {
document: string;
type?: string;
datebegin: string;
dateend?: string;
docgiveplace: string;
docgiveplaceid?: string;
status?: string;
}
interface ForeignDocumentRecord {
document: string;
citizenshipid: string;
datebegin: string;
dateend?: string;
docgiveplace: string;
}
function extractDocuments(data: HumanModel): DocumentRecord[] {
const documents: DocumentRecord[] = [];
for (let i = 1; i <= 5; i++) {
const doc = data[`documents_${i}_document`];
if (doc) {
documents.push({
document: doc,
type: data[`documents_${i}_type`],
datebegin: data[`documents_${i}_datebegin`],
dateend: data[`documents_${i}_dateend`],
docgiveplace: data[`documents_${i}_docgiveplace`],
docgiveplaceid: data[`documents_${i}_docgiveplaceid`],
status: data[`documents_${i}_status`],
});
}
}
return documents;
}
function extractForeignDocuments(data: HumanModel): ForeignDocumentRecord[] {
const documents: ForeignDocumentRecord[] = [];
for (let i = 1; i <= 5; i++) {
const doc = data[`foreign_documents_${i}_document`];
if (doc) {
documents.push({
document: doc,
citizenshipid: data[`foreign_documents_${i}_citizenshipid`],
datebegin: data[`foreign_documents_${i}_datebegin`],
dateend: data[`foreign_documents_${i}_dateend`],
docgiveplace: data[`foreign_documents_${i}_docgiveplace`],
});
}
}
return documents;
}
```
#### 5.12.3 Пример данных с историей документов
```json
{
"status": "ok",
"data": {
"pin": "31006996530022",
"current_pinpp": "31006996530022",
"namelat": "PETR",
"surnamelat": "TRIPOLSKIY",
"patronymlat": "PETROVICH",
"namecyr": "ПЕТР",
"surnamecyr": "ТРИПОЛЬСКИЙ",
"patronymcyr": "ПЕТРОВИЧ",
"birth_date": "10.06.1999",
"birthplace": "ROSSIYA FEDERASIYASI",
"citizenship": "РОССИЙСКАЯ ФЕДЕРАЦИЯ",
"citizenshipid": "192",
"livestatus": "1",
"current_document": "763490940",
"foreign_documents_1_document": "763490940",
"foreign_documents_1_datebegin": "2020-10-01",
"foreign_documents_1_docgiveplace": "РОССИЯ",
"foreign_documents_1_citizenshipid": "192",
"foreign_documents_2_document": "",
"foreign_documents_2_datebegin": "",
"foreign_documents_2_docgiveplace": "",
"foreign_documents_2_citizenshipid": ""
}
}
```
#### 5.12.4 Описание полей истории документов
| Поле | Описание |
|------|----------|
| `documents_N_document` | Номер N-го документа в истории |
| `documents_N_type` | Тип документа (паспорт, ID-карта и т.д.) |
| `documents_N_datebegin` | Дата выдачи документа |
| `documents_N_dateend` | Дата окончания срока действия |
| `documents_N_docgiveplace` | Место выдачи документа |
| `documents_N_docgiveplaceid` | ID места выдачи |
| `documents_N_status` | Статус документа (действителен/недействителен) |
| Поле | Описание |
|------|----------|
| `foreign_documents_N_document` | Номер N-го иностранного документа |
| `foreign_documents_N_citizenshipid` | ID гражданства владельца документа |
| `foreign_documents_N_datebegin` | Дата выдачи документа |
| `foreign_documents_N_dateend` | Дата окончания срока действия |
| `foreign_documents_N_docgiveplace` | Место выдачи (страна) |
Где N — порядковый номер от 1 до 5. Всего может быть до 5 записей в каждой категории
---
## 6. Интеграция с mTLS
### 6.1 Управление сертификатами
Создание и управление клиентскими mTLS-сертификатами находится в зоне ответственности банка. Банк выступает в роли Certificate Authority (CA) для своих клиентов. ГЦИ не участвует в процессе выпуска сертификатов, однако требует прохождения аудита информационной безопасности.
```mermaid
flowchart TB
subgraph bank["Инфраструктура банка"]
CA["CA банка"]
AUTH["Auth Provider"]
PROXY["mTLS Proxy"]
end
subgraph client["Клиентское устройство"]
APP["Мобильное приложение"]
SDK["GSI SDK"]
end
APP -->|"1. Запрос сертификата"| AUTH
AUTH -->|"2. Генерация сертификата"| CA
CA -->|"3. P12 (base64) на 20 мин"| AUTH
AUTH -->|"4. Сертификат + accessToken"| APP
APP -->|"5. Передача в SDK"| SDK
SDK -->|"6. Запрос верификации"| PROXY
style bank fill:#e3f2fd
style client fill:#e8f5e9
```
### 6.2 Требования к сертификатам
| Параметр | Рекомендация |
|----------|--------------|
| Срок действия | Не более 20 минут |
| Алгоритм | RSA 2048+ или ECDSA P-256+ (чем выше, тем лучше) |
| Формат | P12 (PKCS#12) — рекомендуется |
| Хранение | Динамическая загрузка, запрещено вшивать в APK/IPA |
Рекомендуется выпускать отдельный сертификат для каждого пользователя со следующими данными в полях сертификата:
| Поле | Описание |
|------|----------|
| `CN` | ФИО или идентификатор пользователя |
| `O` | Название банка |
| `OU` | Подразделение |
| `C` | UZ |
| `serialNumber` | Номер паспорта или телефона |
### 6.3 Интеграция сертификатов в SDK
**Android — P12:**
```kotlin
FaceDetectionSdk.Builder(context)
.setClientP12Base64(p12CertificateBase64, p12Password)
.setEnableMutualSSL(true)
.build()
```
**Android — раздельные сертификаты:**
```kotlin
FaceDetectionSdk.Builder(context)
.setClientPrivateKeyBase64(privateKeyBase64)
.setClientCertificateChainBase64(certificateChainBase64)
.setEnableMutualSSL(true)
.build()
```
**iOS:**
```swift
let sslConfig = SSLConfig(
clientCertificateBase64: "BASE64_P12",
clientCertificatePassword: "password",
enableMutualSSL: true
)
```
**Flutter:**
```dart
final config = SdkConfig(
enableMutualSSL: true,
clientCertificateBase64: 'BASE64_P12',
clientCertificatePassword: 'password',
);
```
### 6.4 Ротация и отзыв сертификатов
При сроке действия сертификата 20 минут механизм отзыва (CRL) не является обязательным. При необходимости отзыв может быть реализован на уровне mTLS-прокси банка. В комплект поставки включён пример кода на dotnet-script для создания и ротации сертификатов.
Если сертификат истечёт во время сессии (например, пользователь выключил экран на 20+ минут), последующие верификации завершатся сетевой ошибкой.
---
## 7. Механизм защиты от MITM-атак
### 7.1 Двойная JWT-защита
Для исключения атак типа CWE-639 (Authorization Bypass Through User-Controlled Key) система использует два уровня JWT-токенов.
**accessToken (формирует банк):**
Банк кодирует `verificationId` в public claims токена. Это исключает передачу идентификатора в открытом виде и защищает от MITM-атак на стороне запроса.
```json
{
"sub": "user_id",
"verificationId": "encrypted_or_hashed_id",
"iat": 1234567890,
"exp": 1234569090
}
```
**signature (формирует ГЦИ):**
Ответ сервера ГЦИ подписывается JWT с алгоритмом HS256. Для каждого банка используется индивидуальный секретный ключ, так как банки работают с разными репликами.
Ключ подписи передаётся представителю банка лично на физическом носителе (флешка).
### 7.2 Auth Provider банка
Для реализации двойной JWT-защиты банку необходимо создать Auth Provider со следующей логикой:
```mermaid
sequenceDiagram
participant App as Приложение банка
participant Auth as Auth Provider
participant DB as База данных
App->>Auth: Запрос accessToken
Auth->>Auth: Генерация verificationId
Auth->>DB: Сохранение verificationId + метаданные
Auth->>Auth: Формирование JWT с verificationId
Auth-->>App: accessToken (JWT)
Note over App,Auth: verificationId недоступен<br/>в открытом виде
```
Auth Provider собирает необходимые данные в закрытом виде на backend и возвращает уникальную строку (JWT) в режиме только на чтение.
---
## 8. Настройка инфраструктуры банка
### 8.1 Пример Mutual SSL Proxy на C#
В комплект поставки включён пример прокси-сервера на C# с использованием dotnet-script. Реализация использует исключительно стандартную библиотеку .NET без внешних зависимостей.
```csharp
#!/usr/bin/env dotnet-script
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
var serverCert = new X509Certificate2("server.pfx", "password");
var caCert = new X509Certificate2("ca.crt");
var listener = new HttpListener();
listener.Prefixes.Add("https://+:8443/");
listener.Start();
Console.WriteLine("mTLS Proxy started on port 8443");
while (true)
{
var context = await listener.GetContextAsync();
_ = HandleRequest(context);
}
async Task HandleRequest(HttpListenerContext context)
{
try
{
var clientCert = context.Request.GetClientCertificate();
if (clientCert == null || !ValidateCertificate(clientCert))
{
context.Response.StatusCode = 403;
context.Response.Close();
return;
}
// Логирование запроса сертификата
var certInfo = new {
Subject = clientCert.Subject,
SerialNumber = clientCert.SerialNumber,
NotAfter = clientCert.NotAfter,
Thumbprint = clientCert.Thumbprint
};
Console.WriteLine($"Client cert: {JsonSerializer.Serialize(certInfo)}");
// Проксирование запроса к ГЦИ через IPSec
var gciResponse = await ForwardToGci(context.Request);
// Извлечение verificationId из accessToken
var accessToken = context.Request.Headers["Authorization"]?
.Replace("Bearer ", "");
var verificationId = ExtractVerificationId(accessToken);
// Отправка webhook (опционально)
if (!string.IsNullOrEmpty(verificationId))
{
await SendWebhook(verificationId, gciResponse);
}
// Возврат ответа (с пустыми строками в webhook-режиме)
var responseBytes = Encoding.UTF8.GetBytes(gciResponse);
context.Response.ContentType = "application/json";
await context.Response.OutputStream.WriteAsync(responseBytes);
context.Response.Close();
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
context.Response.StatusCode = 500;
context.Response.Close();
}
}
bool ValidateCertificate(X509Certificate2 cert)
{
// Проверка срока действия
if (DateTime.Now > cert.NotAfter || DateTime.Now < cert.NotBefore)
return false;
// Проверка цепочки сертификатов
var chain = new X509Chain();
chain.ChainPolicy.ExtraStore.Add(caCert);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
return chain.Build(cert);
}
async Task<string> ForwardToGci(HttpListenerRequest request)
{
// Реализация проксирования через IPSec туннель
// ...
return "{}";
}
string ExtractVerificationId(string jwt)
{
// Декодирование JWT и извлечение verificationId
// ...
return "";
}
async Task SendWebhook(string verificationId, string data)
{
// Отправка данных на webhook банка
// ...
}
```
### 8.2 Базовая конфигурация IPSec
Настройка IPSec-туннеля находится в зоне ответственности ГЦИ. Рекомендуемые параметры:
| Параметр | Значение |
|----------|----------|
| Протокол | IKEv2 |
| Шифрование | AES-256-GCM |
| Аутентификация | SHA-384 |
| DH Group | 20 (NIST P-384) или выше |
| ESP | AES-256-GCM |
Чем выше уровень защиты, тем лучше — это исключает атаки типа «intercept now, hack later».
---
## 9. Механизмы безопасности SDK
### 9.1 Защита от root/jailbreak
| Платформа | Реализация | Поведение |
|-----------|------------|-----------|
| Android | Библиотека `scottyab.rootbeer` | Отказ в работе в модальном окне |
| iOS | Проверка на Cydia.app и специфичные бинарники | Отказ в работе в модальном окне |
Проверяется наличие: Magisk, SuperSU, системных путей root-доступа, модифицированных системных файлов.
### 9.2 Защита от эмуляторов
| Платформа | Реализация | Поведение |
|-----------|------------|-----------|
| Android | Проверка характеристик эмулятора | Отказ в работе в модальном окне |
| iOS | Приложение не запустится (нет доступа к камере) | — |
### 9.3 Защита от отладки и инструментации
| Угроза | Платформа | Поведение |
|--------|-----------|-----------|
| Подключение отладчика | Android | Отказ в работе в модальном окне |
| Frida | Android | Отказ в работе в модальном окне |
### 9.4 Дополнительные меры
- SDK не сохраняет промежуточные данные на диск
- Видео и результаты верификации находятся только в оперативной памяти
- Логирование принудительно выключено для предотвращения утечки данных
- Код SDK обфусцирован
- Биометрические данные локально не хранятся — система работает stateless
---
## 10. Зоны ответственности и аудит
### 10.1 Разграничение ответственности
| Компонент | Ответственный |
|-----------|---------------|
| GSI Mobile SDK | ГЦИ |
| IPSec-туннель | ГЦИ |
| mTLS Proxy | Банк |
| CA для клиентских сертификатов | Банк |
| Auth Provider | Банк |
| Webhook endpoint | Банк |
| Rate limiter | Банк |
### 10.2 Рекомендация по аудиту
SDK предоставляет набор инструментов, который делает решение безопасным при правильном использовании. Однако банк может по халатности использовать инструменты небезопасным образом (вшить сертификаты в APK, использовать длительный срок действия сертификатов, не реализовать двойную JWT-защиту).
**Рекомендуется проведение аудита информационной безопасности со стороны ГЦИ перед запуском в production.**
Аудит должен включать проверку: механизма выдачи сертификатов, срока действия сертификатов, реализации Auth Provider, конфигурации mTLS Proxy, наличия rate limiter.
---
## 11. Требования к оборудованию
| Параметр | Требование |
|----------|------------|
| Камера | Фронтальная с автофокусом |
| FPS | > 5 кадров/сек |
| Разрешение | Желательно Full HD |
| RAM | 2 ГБ свободно |
| Процессор | ARM64 |
| Планшеты | Поддерживаются |
IR-камеры не поддерживаются — требуется цветное видео.
---
## 12. Сетевые требования
| Параметр | Значение |
|----------|----------|
| Bandwidth | ~5 МБ на верификацию × до 3 попыток |
| Timeout | 30 секунд на операцию |
| Rate limit | Не более 1 запроса в 15 секунд на пользователя |
Rate limiter должен быть установлен банком между SDK и mTLS Proxy.
---
## 13. Установка SDK
### 13.1 Android
Добавить в `build.gradle.kts`:
```gradle
repositories {
flatDir {
dirs("libs")
}
}
dependencies {
implementation(kotlin("reflect"))
implementation(files("libs/facedetectsdk.aar"))
implementation("androidx.camera:camera-core:1.4.2")
implementation("androidx.camera:camera-lifecycle:1.4.2")
implementation("androidx.camera:camera-video:1.4.2")
implementation("androidx.camera:camera-view:1.4.2")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("com.google.mlkit:face-detection:16.1.7")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("com.scottyab:rootbeer-lib:0.1.1")
implementation("com.googlecode.mp4parser:isoparser:1.1.22")
implementation("androidx.camera:camera-camera2:1.3.0")
}
```
Добавить в `AndroidManifest.xml`:
```xml
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
```
### 13.4 ProGuard/R8 правила для Android
При использовании ProGuard или R8 необходимо добавить правила, исключающие конфликты со сжатием AAR. Добавить в `proguard-rules.pro`:
```proguard
# =============================================================================
# GSI FaceDetect SDK
# =============================================================================
# Основные классы SDK
-keep public class uz.detectface.facedetectsdk.FaceDetectionSdk {
public <methods>;
public <fields>;
}
-keep public class uz.detectface.facedetectsdk.FaceDetectionSdk$Builder {
public <init>(android.content.Context);
public <methods>;
}
-keep public class uz.detectface.facedetectsdk.FaceDetectionSdk$Companion {
public <methods>;
}
-keepclassmembers class uz.detectface.facedetectsdk.FaceDetectionSdk$Companion {
public static ** getResultFromIntent(...);
public static ** getResultFromIntent$default(...);
}
# Модели данных
-keep class uz.detectface.facedetectsdk.model.** { *; }
# Enum-классы
-keep public class uz.detectface.facedetectsdk.ResultFormat {
public <init>(...);
public <methods>;
public <fields>;
public **[] values();
public ** valueOf(java.lang.String);
}
-keep public class uz.detectface.facedetectsdk.model.OverlayType {
public <init>(...);
public <methods>;
public <fields>;
public **[] values();
public ** valueOf(java.lang.String);
}
# Обфускация внутренних классов
-repackageclasses 'uz.detectface.internal.a'
-allowaccessmodification
-overloadaggressively
-keep,allowobfuscation class uz.detectface.facedetectsdk.** { *; }
-keep,allowshrinking class uz.detectface.facedetectsdk.model.OverlayType { *; }
-keep,allowshrinking class uz.detectface.facedetectsdk.ResultFormat { *; }
-keep,allowobfuscation class uz.detectface.facedetectsdk.*$* { *; }
-keep,allowobfuscation class uz.detectface.facedetectsdk.mapping.** { *; }
# =============================================================================
# Kotlinx Serialization
# =============================================================================
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
-keepclassmembers class uz.detectface.facedetectsdk.** {
*** Companion;
}
-keepclasseswithmembers class uz.detectface.facedetectsdk.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class uz.detectface.facedetectsdk.**$$serializer { *; }
-keepclassmembers @kotlinx.serialization.Serializable class uz.detectface.facedetectsdk.** {
@kotlinx.serialization.SerialName <fields>;
}
-keep class kotlinx.serialization.** { *; }
-keep interface kotlinx.serialization.** { *; }
# =============================================================================
# Сторонние зависимости
# =============================================================================
# RootBeer (проверка root)
-keep,allowobfuscation class com.scottyab.rootbeer.** { *; }
-dontwarn com.scottyab.rootbeer.**
# CameraX
-keep class androidx.camera.core.Preview { *; }
-keep class androidx.camera.view.PreviewView { *; }
-keep class androidx.camera.lifecycle.ProcessCameraProvider { *; }
-keep class androidx.camera.core.Camera { *; }
-keep class androidx.camera.core.CameraSelector { *; }
-keep class androidx.camera.core.ImageCapture { *; }
-keep class androidx.camera.core.ImageAnalysis { *; }
-keep,allowobfuscation class androidx.camera.** { *; }
-dontwarn androidx.camera.**
# ML Kit Face Detection
-keep class com.google.mlkit.vision.face.FaceDetector { *; }
-keep class com.google.mlkit.vision.face.FaceDetection { *; }
-keep class com.google.mlkit.vision.face.Face { *; }
-keep class com.google.mlkit.vision.face.FaceLandmark { *; }
-keep class com.google.mlkit.vision.face.FaceDetectorOptions { *; }
-keep class com.google.mlkit.vision.face.FaceDetectorOptions$Builder { *; }
-keep class com.google.mlkit.vision.common.InputImage { *; }
-keep,allowobfuscation class com.google.mlkit.** { *; }
-dontwarn com.google.mlkit.**
# =============================================================================
# Kotlin
# =============================================================================
-keep class kotlin.** { *; }
-keep class kotlin.Metadata { *; }
-dontwarn kotlin.**
-keepclassmembernames class kotlinx.** { *; }
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
# =============================================================================
# SSL/TLS (для mTLS)
# =============================================================================
-keep class javax.net.ssl.** { *; }
-keep class java.security.** { *; }
-keep class java.security.cert.** { *; }
-keep class java.security.spec.** { *; }
-keep class java.security.KeyStore { *; }
-keep class java.security.KeyStore$* { *; }
-keep class javax.net.ssl.SSLContext { *; }
-keep class javax.net.ssl.KeyManagerFactory { *; }
-keep class javax.net.ssl.TrustManagerFactory { *; }
-keep class javax.net.ssl.KeyManager { *; }
-keep class javax.net.ssl.X509KeyManager { *; }
-keep class javax.net.ssl.TrustManager { *; }
-keep class javax.net.ssl.X509TrustManager { *; }
-keep class java.security.Provider { *; }
-keep class java.security.Security { *; }
-keep class java.security.cert.Certificate { *; }
-keep class java.security.cert.X509Certificate { *; }
-keep class java.security.cert.CertificateFactory { *; }
-keep class java.security.KeyFactory { *; }
-keep class java.security.PrivateKey { *; }
-keep class java.security.PublicKey { *; }
# =============================================================================
# Оптимизация
# =============================================================================
-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
# Удаление логов в release
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** i(...);
}
# Сохранение информации для stack traces
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
-keepattributes LocalVariableTable,LocalVariableTypeTable
-keepattributes Signature
-keepattributes Exceptions
-keepattributes RuntimeVisibleAnnotations,RuntimeVisibleParameterAnnotations,AnnotationDefault
# Ресурсы
-adaptresourcefilenames **.properties
-adaptresourcefilecontents **.properties,META-INF/MANIFEST.MF
```
Данные правила обеспечивают корректную работу SDK при включённой минификации и обфускации кода. Правила не должны конфликтовать с существующими правилами сжатия в проекте банка
### 13.5 iOS
1. Перетащить `TGFISBIN.xcframework` в проект Xcode
2. В диалоге выбрать «Copy items if needed» и указать target
3. Проверить: Target → Frameworks, Libraries, and Embedded Content → «Embed & Sign»
Права на камеру запрашиваются автоматически в процессе работы.
### 13.6 Flutter
Добавить в `pubspec.yaml`:
```yaml
dependencies:
facedetect_tadi_sdk: ^0.0.1
```
Выполнить:
```bash
flutter pub get
```
---
## 14. Кастомная вёрстка и интеграция с нативным backend
### 14.1 Обзор
SDK использует WebView для отображения интерфейса верификации. При кастомной вёрстке необходимо обеспечить взаимодействие между JavaScript-кодом в WebView и нативным приложением. Взаимодействие осуществляется через JavaScript-интерфейсы (bridges).
### 14.2 Параметры запуска WebView
Для корректной работы SDK необходимо передать параметры в URL:
| Параметр | Тип | Описание |
|----------|-----|----------|
| `locale` | string | Язык интерфейса: `en`, `ru`, `uz` |
| `token` | string | JWT-токен авторизации |
| `native` | number | Обязательно `1` для интеграции с нативным backend |
Формат URL:
```
https://gsisdk.tadi.uz/?locale=ru&token=YOUR_JWT_TOKEN&native=1
```
Параметр `native=1` является обязательным. Без него WebView будет запрашивать доступ к камере через `getUserMedia`, как в браузере, без интеграции с нативным приложением.
### 14.3 JavaScript-интерфейсы по платформам
#### iOS
```javascript
// Отправка данных в нативное приложение
window.webkit.messageHandlers.sendDataMessage.postMessage(person: string)
// Запрос видео с камеры
window.webkit.messageHandlers.requestVideoMessage.postMessage()
```
#### Android
```javascript
// Отправка данных в нативное приложение
window.AndroidBinding.sendDataMessage(person: string)
// Запрос видео с камеры
window.AndroidBinding.requestVideoMessage()
```
### 14.4 Глобальные функции для работы с видео
При кастомной вёрстке необходимо объявить следующие глобальные функции, которые будут вызываться нативным кодом:
```typescript
declare global {
interface Window {
// Добавление чанка видео (base64)
addBase64Chunk(chunk: string): void;
// Завершение записи и отправка видео
submitVideo(): void;
// Отмена записи видео
cancelVideo(): void;
// iOS bridge
webkit: {
messageHandlers: {
sendDataMessage: {
postMessage: (data: string) => void;
};
requestVideoMessage: {
postMessage: () => void;
};
};
};
// Android bridge
AndroidBinding: {
sendDataMessage: (data: string) => void;
requestVideoMessage: () => void;
};
}
}
```
| Функция | Описание |
|---------|----------|
| `addBase64Chunk(chunk)` | Нативный код вызывает эту функцию для передачи чанков видео в формате base64 |
| `submitVideo()` | Нативный код вызывает после завершения записи видео |
| `cancelVideo()` | Нативный код вызывает при отмене записи видео пользователем |
### 14.5 Пример запроса видео с нативного backend
```typescript
import { get } from 'lodash-es';
export const VerifyNativeView = ({ onDone }: IVerifyNativeViewProps) => {
useEffect(() => {
const videoChunks: string[] = [];
// Функция для добавления чанка видео
window.addBase64Chunk = (chunk: string) => {
videoChunks.push(chunk);
};
// Функция для завершения записи и отправки видео
window.submitVideo = () => {
onDone();
const videoBlob = base64ChunksToBlob(videoChunks);
// Отправка видео на сервер верификации
sendVideoForVerification(videoBlob);
};
// Функция для отмены записи видео
window.cancelVideo = () => {
onDone();
// Обработка отмены
};
// Запрос видео на iOS
if (get(window, "webkit.messageHandlers.requestVideoMessage.postMessage")) {
window.webkit?.messageHandlers?.requestVideoMessage?.postMessage();
}
// Запрос видео на Android
if (get(window, "AndroidBinding.requestVideoMessage")) {
window.AndroidBinding?.requestVideoMessage();
}
}, []);
return <div>Записываем видео...</div>;
};
// Преобразование base64 чанков в Blob
function base64ChunksToBlob(chunks: string[]): Blob {
const base64 = chunks.join('');
const byteCharacters = atob(base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
return new Blob([byteArray], { type: 'video/mp4' });
}
```
### 14.6 Отправка результатов верификации в нативное приложение
После успешной верификации необходимо передать данные в нативное приложение:
```typescript
import { get } from 'lodash-es';
export const PersonView = ({ onDone, person }: IPersonViewProps) => {
const handleSubmit = async () => {
const personJson = JSON.stringify(person);
// Отправка данных на iOS
if (get(window, "webkit.messageHandlers.sendDataMessage.postMessage")) {
window.webkit?.messageHandlers?.sendDataMessage?.postMessage(personJson);
}
// Отправка данных на Android
if (get(window, "AndroidBinding.sendDataMessage")) {
window.AndroidBinding?.sendDataMessage(personJson);
}
onDone();
};
return (
<div>
<h2>Верификация успешна</h2>
<p>ФИО: {person.last_name} {person.first_name} {person.patronym}</p>
<p>ПИНФЛ: {person.pin}</p>
<button onClick={handleSubmit}>Подтвердить</button>
</div>
);
};
```
### 14.7 Интерфейс данных пользователя (IPerson)
При интеграции с ГЦИ используется формат snake_case:
```typescript
export interface IPerson {
// Статус субъекта
subject_state: number;
subject_state_name: string;
// Идентификация
pin: string; // ПИНФЛ
// ФИО
last_name: string;
first_name: string;
patronym: string;
surname: string;
givenname: string;
// Дата рождения и пол
birth_date: string;
gender: number;
gender_name: string;
// Документ
document: string;
doc_seria: string;
doc_number: string;
date_issue: string;
date_expiry: string;
give_place: string;
give_place_name: string;
// Место рождения
birth_country: string;
birth_country_name: string;
birth_place: string;
// Национальность и гражданство
nationality: string;
nationality_name: string;
citizenship: string;
citizenship_name: string;
// Результаты верификации
score: number; // Коэффициент сходства (0.0 — 1.0)
capture: string; // Кадр из видео (base64)
photo: string; // Эталонное фото из ГЦП (base64)
}
```
### 14.8 Закрытие WebView
После получения данных верификации нативное приложение должно закрыть WebView:
```kotlin
// Android
webView.loadUrl("about:blank")
webView.destroy()
```
```swift
// iOS
webView.load(URLRequest(url: URL(string: "about:blank")!))
webView.removeFromSuperview()
```
### 14.9 Полный пример формирования URL
```typescript
interface SDKParams {
locale: 'en' | 'ru' | 'uz';
token: string;
baseUrl: string;
}
function buildWebViewUrl(params: SDKParams): string {
const { locale, token, baseUrl } = params;
const url = new URL(baseUrl);
url.searchParams.set('locale', locale);
url.searchParams.set('token', token);
url.searchParams.set('native', '1'); // Обязательный параметр
return url.toString();
}
// Использование
const webViewUrl = buildWebViewUrl({
locale: 'ru',
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
baseUrl: 'https://gsisdk.tadi.uz/',
});
console.log(webViewUrl);
// https://gsisdk.tadi.uz/?locale=ru&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&native=1
```
---
## 15. UI/UX
SDK предоставляет базовый UI в фирменном стиле ГЦИ. Возможна полная кастомизация через замену `index.html` и сопутствующих файлов — можно запрограммировать frontend с нуля.
| Параметр | Значение |
|----------|----------|
| Локализация | Узбекский, русский, английский |
| Темы | Светлая и тёмная |
| Accessibility | Поддерживается |
Технических ограничений на кастомизацию нет. Могут быть административные ограничения со стороны ГЦИ.
---
## 16. Тестирование
### 15.1 Staging-окружение
Staging находится на отдельном домене с Public FQDN: `face.mbabm.uz`. Для тестирования доступны моковые физические лица с тестовыми ПИНФЛ и паспортными данными.
### 15.2 Режимы тестирования
- Режим без Mutual SSL для проверки на компьютере разработчика
- Staging с моковыми верификациями
- Моковый сервер для `/api/v1/verify` и `/api/v1/verify_b64`
### 15.3 Rate limiting
На staging подразумевается не более 1 запроса в 15 секунд на пользователя. Rate limiter должен быть установлен банком.
---
## 17. Производительность
| Параметр | Значение |
|----------|----------|
| Время инициализации | < 800 мс |
| Время верификации | ~15 секунд |
| Потребление RAM (форма паспорта) | До 300 МБ |
| Потребление RAM (обработка видео) | До 2 ГБ |
При сворачивании приложения модальное окно остаётся открытым до завершения или отмены верификации. Освобождение ресурсов происходит автоматически.
---
## 18. Версионирование и обновления
| Параметр | Значение |
|----------|----------|
| Обратная совместимость | Абсолютная |
| Частота обновлений | ~1 релиз в месяц |
| Changelog | Предоставляется |
| Уведомления | Telegram-чат + email |
Существующие интерфейсы API сохраняются. Новые возможности добавляются через feature model.
---
## 19. Поддержка
| Параметр | Значение |
|----------|----------|
| Каналы | Telegram-чат, прямая связь с разработчиками, выезд в офис |
| SLA на ответы | До 1 календарной недели |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment