Руководство по использованию myDSSSDK

Термины и определения

Тип Описание
DSSUser Объект, содержащий всю необходимую информацию для выполнения действий от имени пользователя. Содержит, в том числе, информацию для доступа к экземпляру DSS (url, корневой сертификат и пр.), "вектор аутентификации" для подтверждения действий и пр. С точки зрения DSS данный объект является устройством, подключенным к учетной записи пользователя DSS
DSSDevice Объект, содержащий информацию об устройствах, подключенных к той же учетной записи, что и объект DSSUser. Данный объект не содержит "вектор аутентификации" и не может использоваться для подтверждения операций или выполнения других действий
DSSOperation Операция DSS, для которой требуется подтверждение/отклонение. Операция может содержать несколько документов. При подтверждении/отклонении операции все содержащиеся в ней документы будут подписаны/отклонены. Операция создается на сервере DSS
DSSOperation.DSSDocument Документ, входящий в операцию, либо обрабатываемый самостоятельно
Document Description "Сниппет" документа, сформированный сервером DSS на основе шаблона для сниппета и содержания документа
Document Preview Визуализированный в человеко-читаемую форму документ, сформированный сервером DSS на основе шаблона для визуализации и содержания документа
Document RawPDF "Сырое" содержание документа (например, текстового файла), преобразованное в формат PDF
DSSCertificate Сертификат, привязанный к ключу подписи в учетной записи на DSS

Установка

Xcode 14+ Swift 5.7

Для использования myDSSSSD необходимо добавить в проект:

  • Фреймворк
  • Ресурсы
  • Корневые сертификаты

Добавление фреймворка

Swift Package Manager

Добавьте пакет myDSSSDK из репозитория:

https://repo.paycontrol.org/git/ios/mydss.git

CocoaPods

Добавьте в файл Podfile зависимости:

pod 'myDSSSDK',           :git => 'https://repo.paycontrol.org/git/ios/mydss.git'

Ручная установка

Добавьте в проект myDSSSDK.xcframework и myDSSSDKSupplement.xcframework.

Carthage

Т.к. Carthage не поддерживает распространение бинарных фреймворков, данный метод мы не используем.

Ресурсы

Ресурсы лежат в бандле фреймворка в папке Resources. Их необходимо прикрепить к приложению. Папку locale нужно добавить с флагом Create folder references.

При интеграции с помощью SPM/CocoaPods фреймворк можно найти в Project Navigator.

  • SPM: Package Dependencies -> myDSSSDK -> Referenced Binaries -> myDSSSDK.xcframework
  • CocoaPods: Pods -> Pods -> myDSSSDK -> Frameworks -> myDSSSDK.xcframework

Корневые сертификаты

Для взаимодействия с серверами в ресурсы приложения в папку root-certs нужно поместить сертификаты:

AppName.app/root-certs/
    cert_1.crt
    cert_2.cer
    ...
    cert_N.certificate

Дополнительные действия

В процесс сборки таргета необходимо добавить новую скриптовую фазу (Build Phases -> New Run Script Phase), которая запускает скрипт ConfigureApplication внутри бандла фреймворка. Примеры для различных способов установки:

# SPM
sh ${BUILD_DIR%Build/*}/SourcePackages/artifacts/mydss/myDSSSDK.xcframework/Tools/ConfigureApplication

# CocoaPods
sh ${PROJECT_DIR}/Pods/myDSSSDK/myDSSSDK.xcframework/Tools/ConfigureApplication

# Ручная установка
sh myDSSSDK.xcframework/Tools/ConfigureApplication

Так же можно переместить содержимое папки myDSSSDK.xcframework/Tools в любое удобное место и указать нужный путь.

Инициализация

Инициализировать библиотеку нужно перед вызовом внутренних функций SDK. Можно выставить уровень логирования и режим работы.

import UIKit
import myDSSSDK

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        // Установка уровня логирования и инициализация библиотеки
        #if DEBUG
        myDSS.setLogLevel([.debug, .keys])
        myDSS.initialize(debugMode: true)
        #else
        myDSS.initialize()
        #endif

        ...

    }

}

При debugMode: true проверка сертификатов на отзыв отключена, при запуске приложения будет пояляться экран с напоминанием о работе в режиме разработчика.

Объекты и функции

Для работы c УКЭП можно использовать:

  • DSSUsersManager: Создаёт и управляет учётными записями пользователя на устройстве
  • DSSOperationsManager: Получает информацию и подписывает операции и документы
  • DSSCertificatesManager: Управляет сертификатами пользователей
  • DSSDevicesManager: Управляет устройствами, привязанными к учётной записи
  • DSSPolicyManager: Получает информацию о сервере DSS

Для работы c УНЭП можно использовать любые объекты и функции. Есть классы, которые включают в названии NonQual, в них есть расширенный набор методов:

  • DSSUsersManagerNonQual: Создаёт и управляет учётными записями пользователя на устройстве
  • DSSOperationsManagerNonQual: Получает информацию и подписывает операции и документы
  • DSSCertificatesManagerNonQual: Управляет сертификатами пользователей
  • DSSKeysManagerNonQual: Управляет сертификатами пользователей.

Регистрация

Перед тем, как начать подписывать документы и операции, нужно создать пользователя. Есть 3 способа это сделать:

  1. Зарегистрировать онлайн
  2. Зарегистрировать с использованием QR-кода
  3. Привязать к другому устройству

Сценарии УКЭП

Зарегистрировать онлайн

DSSUsersManager.createDSSUser(
    serviceURL: serverURL,          // Адрес сервера DSS
    name: name,                     // Имя для сохранения учетной записи
    pushNotificationsData: data,    // Данные для отправки пуш-уведомлений
    deviceName: deviceName,         // Отображаемое дружественное имя устройства
    externalId: externalId,         // Внешний идентификатор
    alias: alias,                   // Человекочитаемый идентификатор устройства
    requirePassword: requirePwd     // Требуется ли установка пароля
) { result in

    // Обрабатываем результат
    switch result {
        case let .success(user): ...    // Пользователь создан и сохранён в хранилище
        case let .failure(error): ...
    }

}

Пользователь создан и сохранён в хранилище, его статус — .installed. Теперь нужно дождаться, когда его статус на сервере DSS сменится на .notVerified. Чтобы проверить текущий статус:

DSSUsersManager.updateStatus(
    user: user
) { result in
    switch result {
        case let .success(user):                // Сведения о пользователе обновлены
            if user.state == .notVerified {     // Проверяем статус

                ...
            }
        case let .failure(error): ...
    }

}

Как только статус пользователя станет .notVerified, мы сможем его верифицировать:

DSSUsersManager.acceptAccountChanges(
    user: user
) { result in

    switch result {
        case let .success(user): ...    // Пользователь подтверждён
        case let .failure(error): ...
    }

}

При верификации может потребоваться QR-код. Это зависит от настроек политики сервера.

Теперь статус пользователя .active, с ним можно работать дальше.

Зарегистрировать с использованием QR-кода

DSSUsersManager.createDSSUserWithInitQR(
    name: name,                     // Имя для сохранения учетной записи
    pushNotificationsData: data,    // Данные для отправки пуш-уведомлений
    deviceName: deviceName,         // Отображаемое дружественное имя устройства
    externalId: externalId,         // Внешний идентификатор
    alias: alias,                   // Человекочитаемый идентификатор устройства
    requirePassword: true           // Требуется ли установка пароля
) { result in

    switch result {
        case let .success(user): ...    // Пользователь создан и сохранён в хранилище
        case let .failure(error): ...
    }

}

При создании пользователя данным способом потребуется отсканировать QR-код, сгенерированый сервером. После создания статус пользователя — .active, с ним можно работать дальше.

Привязать к другому устройству

Данный способ применяется, когда уже есть устройство с подтверждённым пользователем и нужно привязать новое устройство. Потребуется передать идентификатор данного пользователя на новое устройство.

Получить идентификатор можно у экземпляра пользователя:

// Идентификатор подтверждённого пользователя, к которому привязываем новое устройство
let uid = user.dssUserID

На новом устройстве вызываем метод:

DSSUsersManager.createDSSUserWithApproval(
    serviceURL: serverURL,          // Адрес сервера DSS
    uid: uid,                       // Идентификатор пользователя, к которому привязываем устройство
    name: name,                     // Имя для сохранения учетной записи
    pushNotificationsData: data,    // Данные для отправки пуш-уведомлений
    deviceName: deviceName,         // Отображаемое дружественное имя устройства
    externalID: externalID,         // Внешний идентификатор
    alias: alias,                   // Человекочитаемый идентификатор устройства
    requirePassword: true           // Требуется ли установка пароля
) { result in

    switch result {
        case let .success(newUser): ...    // Пользователь создан и сохранён в хранилище
        case let .failure(error): ...
    }

}

Мы создали нового пользователя, его статус — .approveRequired, требуется подтверждение с основного устройства. На экране отобразится QR-код, который нужно отсканировать основным устройством.

На основном устройстве вызываем метод processAwaitingDevice:

DSSDevicesManager.processAwaitingDevice(
    user: user  // Пользователь к которому привязываем устройство
) { result in
    switch result {
        case .success: ...    // Новое устройство подтверждено/отклонено
        case let .failure(error): ...
    }
}

Далее, на новом устройстве проверяем статус:

DSSUsersManager.checkApprovalStatus(
    user: newUser
) { result in
    switch result {
        case let .success(approvedUser): ...    // Пользователь добавлен
        case let .failure(error): ...           // Пользователь отклонён/не обработан
    }
}

Теперь новое устройство подтверждено, статус пользователя — .active и с ним можно работать на новом устройстве.

Сценарии УНЭП

В сценариях УНЭП интерфейс SDK не используется, применяются дополнительные методы для работы с данными и используется класс DSSUsersManagerNonQual.

Зарегистрировать онлайн

Для начала регистрации мы вызываем метод createDSSUser:

DSSUsersManagerNonQual.createDSSUser(
    serviceURL: serverURL,          // Адрес сервера DSS
    pushNotificationsData: data,    // Данные для отправки пуш-уведомлений
    deviceName: deviceName,         // Отображаемое дружественное имя устройства
    externalId: externalId,         // Внешний идентификатор
    alias: alias,                   // Человекочитаемый идентификатор устройства
) { result in

    // Обрабатываем результат
    switch result {
        case let .success(user): ...    // Пользователь создан
        case let .failure(error): ...
    }

}

После регистрации будет создан DSSUser в статусе created. Для дальнейшей работы его нужно сохранить в хранилище.


DSSUsersManagerNonQual.store(
   user: user,        // Новый пользователь 
   name: name,        // Имя для хранения
   password: password // Пароль для хранения
) { result in

    // Обрабатываем результат
    switch result {
    case let .success(user): ...    // Пользователь сохранён
    case let .failure(error): ...
    }

}

После успешного сохранения у пользователя сменится статус на .installed. Далее, необходимо дождаться от сервера смены статуса на .notVerified. Для проверки статуса нужно вызвать DSSUsersManager.updateStatus:

DSSUsersManager.updateStatus(
    user: user
) { result in

    switch result {
        case let .success(user):                // Сведения о пользователе обновлены
            if user.state == .notVerified {     // Проверяем статус
                ...
            }
        case let .failure(error): ...
    }

}

После смены статуса на .notVerified необходимо вызвать метод DSSUsersManagerNonQual.acceptAccountChanges. Данный метод требует, чтобы перед выполнением user.isReadyToSign == true.

Так же, для присоединения может потребоваться QR-код: если user.verification == .qrRequired. Его необходимо отсканировать и распарсить заранее, а содержимое передать в параметре verificationQRValue.

Персональные данные клиента для присоединения можно проверить в DSSUser.profile.

if !user.isReadyToSign {
    // Предъявляем пароль
    try DSSUsersManagerNonQual.submitPassword(user: user, password: password)
}

if user.verification == .qrRequired {
    // Парсим QR-код
    let qrValue = ...
    // Проверяем
    let verificationQRCode: DSSQRCodeVerification = try myDSS.analyzeQR(qrValue)
}

// Присоединяем к УЗ
DSSUsersManagerNonQual.acceptAccountChanges(
  user: user,                             // Пользователь
  verificationQRCode: verificationQRCode  // QR-код
) { result in

    // Обрабатываем результат
    switch result {
    case let .success(user): ...    // Пользователь присоединён у УЗ
    case let .failure(error): ...
    }

}

Зарегистрировать с использованием QR-кода

Для начала регистрации с QR кодом его нужно получить и распарсить. После этого его можно передать в метод DSSUsersManagerNonQual.createDSSUserWithInitQR.

QR-код может требовать активации. Код активации предоставляет сервер. Длину кода активации можно узнать, вызвав DSSPolicyManager.getDSSParams, в переменной DSSParams.activationCodeLength.


// Парсим QR-код
let registrationQRCode: DSSQRCodeKinit = try myDSS.analyzeQR(qrValue)

// Проверяем активацию
if !qrCode.isActivated {
    let activatedQRCode = try myDSS.activate(qrCodeKinit: qrCode, code: activationCode)
}

// Регистрируем
DSSUsersManagerNonQual.createDSSUserWithInitQR(
    qrCode: qrCode,                               // QR-код
    pushNotificationsData: pushNotificationsData, // Данные для отправки пуш-уведомлений
    deviceName: deviceName,                       // Отображаемое дружественное имя устройства
    externalId: externalId,                       // Внешний идентификатор
    alias: alias,                                 // Человекочитаемый идентификатор устройства
) { result in

    switch result {
        case let .success(user): ...    // Пользователь зарегистрирован
        case let .failure(error): ...
    }

}

После регистрации будет создан DSSUser в статусе created. Для дальнейшей работы его нужно сохранить в хранилище.

DSSUsersManagerNonQual.store(
   user: user,        // Новый пользователь 
   name: name,        // Имя для хранения
   password: password // Пароль для хранения
) { result in

    // Обрабатываем результат
    switch result {
    case let .success(user): ...    // Пользователь сохранён
    case let .failure(error): ...
    }

}

Привязать к другому устройству

Для привязки к другому устройству требуется знать uid и serviceURL уже существующего на другом устройствке пользователя. На новом устройстве нужно вызвать метод DSSUsersManagerNonQual.createDSSUserWithApproval с этими данными:

 DSSUsersManagerNonQual.createDSSUserWithApproval(
    serviceURL: serviceURL,                        // Адрес для взаимодействия существующего пользователя
    uid: userId,                                   // UserID существующего пользователя 
    pushNotificationsData: pushNotificationsData,  // Данные для отправки пуш-уведомлений
    deviceName: deviceName,                        // Отображаемое дружественное имя устройства
    externalId: externalId,                        // Внешний идентификатор
    alias: alias,                                  // Человекочитаемый идентификатор устройства
 ) { result in

    switch result {
    case let .success(user): ... // Пользователь зарегистрирован
    case let .failure(error): ...
    }

}

После регистрации будет создан DSSUser в статусе created. Для дальнейшей работы его нужно сохранить в хранилище.

DSSUsersManagerNonQual.store(
   user: user,        // Новый пользователь 
   name: name,        // Имя для хранения
   password: password // Пароль для хранения
) { result in

    // Обрабатываем результат
    switch result {
    case let .success(user): ...    // Пользователь сохранён
    case let .failure(error): ...
    }

}

После сохранения статус сменится на .approveRequired — присоединение данного пользователя нужно подтвердить или отклонить на основном устройстве. Для этого нужно вызвать DSSDevicesManagerNonQual.approve или DSSDevicesManagerNonQual.reject.

Если для присоединения требуется отобразить персональные данные нового пользователя, то их можно найти в unapprovedUser.qrCode. В сценарии УКЭП на новом устройстве отображается этот QR-код, старое устройство его сканирует и отображает содержимое на экране.

// Получаем список устройств
DSSDevicesManager.listDevices(
    user: user // Основной активный пользователь
) { result in

    switch result {
    case let .success(devices): ...    // Список устройств
    case let .failure(error): ...
    }
}

// Находим неподтверждённое устройство
let notApprovedDevice = devices.first(where { $0.state == .approveRequired })

if !user.isReadyToSign {
    // Предъявляем пароль
    try DSSUsersManagerNonQual.submitPassword(user: user, password: password)
}

// Подтверждаем присоединение
DSSDevicesManagerNonQual.approve(
   device: notApprovedDevice,   // Устройство, ожидающее подтверждения
   user: user                   // Основной активный пользователь
) { result in
    switch result {
    case .success: ... // Присоединение устройства одобрено
    case let .failure(error): ...
    }
}

// ... или отклоняем
DSSDevicesManagerNonQual.reject(
   device: notApprovedDevice,   // Устройство, ожидающее подтверждения
   user: user                   // Основной активный пользователь
) { [weak self] result in
    switch result {
    case .success: ... // Присоединение устройства отклонено
    case let .failure(error): ...
    }
}

После этого на новом устройстве нужно проверить и обновить статус пользователя:

DSSUsersManagerNonQual.checkApprovalStatus(
    user: userToCheck // Новый пользователь
) { result in
    switch result {
    case let .success(user): ... // Пользователь присоединён
    case let .failure(error):
        switch error {
        case DSSError.approveRequired: ...  // По-прежнему ожидаем подтверждение на основном устройстве
        default: ...
        }
    }
}

Устройства и пользователи

Управления пользователями на текущем устройстве осуществляется с помощью классов DSSUsersManager и DSSUsersManagerNonQual.

Управление устройствами осуществляется с помощью DSSDevicesManager и DSSDevicesManagerNonQual.

Пользователей, установленных на устройстве, можно получить в DSSUsersManager.users.

Связанные устройства (в том числе и ожидающие подтверждения) можно получить, вызвав DSSDevicesManager.listDevices:

DSSDevicesManager.listDevices(
    user: user
) { result in
    switch result {
        case let .success(devices): ... // Все устройства
        case let .failure(error): ...
    }
}

Сертификаты

Для подтверждения операций и подписи документов нужны действующие сертификаты. Управлять сертификатами можно с помощью классов DSSCertificatesManager и DSSCertificatesManagerNonQual.

Получение списка сертификатов

DSSCertificatesManager.listCertificates(
    user: user // Пользователь, запрашивающий информацию
) { result in
    switch result {
        case let .success(certificates): ... // Вернётся массив сертификатов и запросов
        case let .failure(error): ...
    }
}

// Отфильтруем выпущеные сертификаты
let issuedCertificates = certificates.filter { $0.type == .certificate }

Создание запроса на сертификат

// Получаем параметры сервера подписания
DSSPolicyManager.getDSSSignServerParams(
    user: user  // Пользователь, запрашивающий параметры сервера подписания
) { result in
    switch result {
        case let .success(signServerParams): ... // Вернутся параметры сервера подписания
        case let .failure(error): ...
    }
}

...

// Подготавливаем параметры. Значения даны для примера
let policyId = signServerParams.caPolicies.first(where: { $0.caType == .DSSOutOfBandEnroll }).id
let templateId = policy.ekuTemplates.first?.value.first
let testDN = ["2.5.4.3": "Test"]

// Создаём запрос на сертификат
DSSCertificatesManager.createCertificate(
    user: user,             // Пользователь, создающий запрос на сертификат
    dn: testDN,             // Различительное имя субъекта
    templateId: templateId, // Идентификатор шаблона сертификата
    caId: policyId          // Идентификатор обработчика УЦ
) { result in
    switch result {
        case let .success(certRequest): ... // Вернётся запрос на сертификат
        case let .failure(error): ...
    }
}

Сертификат с ключами на устройстве

Для работы с ключами на устройстве необходимо сначала подписать запрос на сертификат у которого параметр isClient: true. Данный запрос создаётся на сервере.

// Находим неподписаный запрос
let requestToSign = certificates.first {
    $0.type == .request && $0.isClient = true && $0.dssCertificateId == nil
}

// Подписываем
DSSCertificatesManagerNonQual.sign(
    certificateRequest: requestToSign,
    user: user
) { result in

    switch result {
    case .success: ... // Запрос подписан
    case let .failure(error): ...
    }
}

После этого необходимо дождаться выпуска сертификата и установить его:


// Отсеиваем сертификаты, недоступные на этом устройстве
// (например, у которых запрос был подписан на другом устройстве)
let availableCertificates = certificates
    .filter { $0.type == .certificate && DSSCertificatesManagerNonQual.checkIfAccessibleOnThisDevice(certificate: $0, for: user) }

// Получаем неустановленные сертификаты
let certificateToInstall = availableCertificates
    .filter { $0.isClient && !DSSCertificatesManagerNonQual.checkIfInstalled(certificate: $0, for: user) }
    .first

// Устанавливаем
DSSCertificatesManagerNonQual.install(
    certificate: certificateToInstall,
    user: user
) {
    switch result {
    case .success: ... // Сертификат установлен
    case let .failure(error): ...
    }
}

Теперь данный сертификат можно использовать для подтверждения операций и подписания документов.

Управление сертификатами

С остальными доступными методами можно ознакомиться в описании API DSSCertificates​Manager и DSSCertificates​ManagerNonQual.

Операции и документы

Получение списка операций

DSSOperationsManager.getOperationsList(
    user: user,         // Пользователь, для которого проверяются операции
    operationType: nil, // Фильтр операций по типу
    operationID: nil    // Фильтр по идентификатору
) { result in
    switch result {
        case let .success(operations): ...   // Получен список операций
        case let .failure(error): ...
    }
}

Подтверждение операции

DSSOperationsManager.confirmOperation(
    operation: operation,   // Операция для подтверждения
    user: user,             // Пользователь, подтверждающий операцию
    signMode: .online       // Способ подтверждения. В данном случае — online
) { result in
    switch result {
        case let .success(approveRequest): ...   // Операция подтверждена, вернули
        case let .failure(error): ...
    }
}

Загрузка документов

DSSOperationsManager.uploadDocument(
    documentContent: documentData,      // Бинарные данные документа
    title: documentTitle,               // Заголовок документа
    snippetTemplate: documentSnippet,   // HTML-сниппет документа
    previewTemplate: documentPreview,   // HTML-превью документа
    user: user                          // Пользователь-владелец документа
) { result in
    switch result {
        case let .success(documentID):  // Вернётся идентификатор документа
        case let .failure(error): ...
    }
}

Подпись документов


// Формируем параметры подписания
let templateId = signServerParams.processingTemplates.first.id
let certificateId = certificate.dssCertificateId
let signParams = SignParams(
    signTemplateId: templateId, // Идентификатор шаблона подписи
    certId: certificateId,      // Идентификатор сертификата, которым будем подписывать
    pinCode: ""                 // Пин-код от сертификата. Если пин-код не установлен, то укажите пустую строку
)

DSSOperationsManager.signDocuments(
    documentsIDs: confirmedDocuments,           // Документы для подписания
    user: user,                                 // Пользователь, подписывающий документы
    signParams: signParams                      // Параметры подписания
) { result in
    switch result {
        case let .success(signingResults):      // Вернутся результаты подписания
        case let .failure(error): ...
    }
}

Дополнительная информация

Параметры подписания

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

// Получаем параметры сервера подписания
DSSPolicyManager.getDSSSignServerParams(
    user: user  // Пользователь, запрашивающий параметры сервера подписания
) { result in
    switch result {
        case let .success(signServerParams): ... // Вернутся параметры сервера подписания
        case let .failure(error): ...
    }
}

...

// Получаем сертификат
DSSCertificatesManager.listCertificates(
    user: user  // Пользователь, запрашивающий сертификаты
) { result in
    switch result {
        case let .success(certificates): ... // Вернутся сертификаты и запросы на сертификаты
        case let .failure(error): ...
    }
}

...

// Выбираем сертификат, например, первый в списке. Запросы на сертификат игнорируем
let certificate: DSSCertificate = certificates.first(where: {
    $0.type == .certificate
})

...

С остальными доступными методами можно ознакомиться в описании API DSSOperations​Manager и DSSOperations​Manager​Non​Qual.

Резервное копирование

Создание и восстановление резервных копий может быть полезно при смене устройства или если пользователь забыл пароль к своему профилю. Для создания резервной копии и восстановления понадобится придумать пароль для восстановления — recovery password.

Резервная копия профиля пользователя


// Создание резервной копии
let userBackupData: Data = try DSSUsersManagerNonQual.createBackup(
    user: user,
    recoveryPassword: recoveryPassword
)

// Восстановление
let restoredUser: DSSUser = try DSSUsersManagerNonQual.restoreFromBackup(
    backupData: userBackupData,
    recoveryPassword: recoveryPassword
)

DSSUsersManagerNonQual.store(
    user: restoredUser,
    name: username,
    password: password
) { storingResult in
    switch result {
        case let .success(storedUser):  // Сохранённый пользователь
        case let .failure(error): ...
    }
}

Резервная копия userBackupData после создания находится в памяти. Необходимо самостоятельно сохранить её на устройство любым возможным способом.

Резервая копия ключей подписи на устройстве


// Создание резервной копии
let keyBackupData: Data = try DSSKeysManagerNonQual.createBackup(
    keyInfo: keyInfo,
    recoveryPassword: recoveryPassword
)

// Восстановление
let restoredKeyInfo: DSSKeyInfo = try DSSKeysManagerNonQual.restoreFromBackup(
    backupData: keyBackupData,
    recoveryPassword: recoveryPassword
)

Резервная копия keyBackupData после создания находится в памяти. Необходимо самостоятельно сохранить её на устройство любым возможным способом.

Архивирование ключей подписи на сервере DSS

Ключи, созданные с флагом isExportable могут быть архивированы на сервере DSS. Начиная с версии 2.1.267, можно создать ключи с таким флагом:


// При подписи сертификата
DSSCertificatesManagerNonQual.sign(
    certificateRequest: request,
    user: user,
    isExportable: true,
    completion: { ... } 
)

// При создании ключей
let keyInfo = try DSSKeysManagerNonQual.createKeyPair(
    for: user,
    pin: pin,
    isExportable: true
)

Для создания резервной копии сертификат ключей подписи нужно передать в метод DSSCertificatesManagerNonQual.exportPfx(...). Метод требует ПИН-код от контейнера с ключевой информацией. Если передать nil, то будет использован пин-код по умолчанию.

После архивации ключей их можно восстановить как на том же самом устройстве, так и на другом устройстве, где есть объект DSSUser, привязанный к той же учётной записи.


// Экспорт

// Получаем список сертификатов
DSSCertificatesManager.listCertificates(
    user: user, 
    сompletion: { ... }
) 

// // Выбираем сертификат для экспорта
let certificateToExport = certificatesList.first {
    $0.type == .certificate &&
    !$0.isArchived &&
    DSSCertificatesManagerNonQual.checkIfAccessibleOnThisDevice(certificate: $0, for: user)
}

// Выгружаем архив
DSSCertificatesManagerNonQual.exportPfx(
    user: user,
    certificate: certificateToExport, // Сертификат, ключи которого архивируем
    pin: pin,    // Пин-код от контейнера ключей
    pfxPin: nil, // Дополнительный пин-код, на котором будет зашифрован архив (опционально)
    completion: { ... }
)

...

// Импорт

// Получаем список сертификатов
DSSCertificatesManager.listCertificates(
    user: user,
    completion: { ... }
)

// Находим доступный для импорта сертификат
let archivedCertificate = certificatesList.first {
    $0.type == .certificate && 
    $0.isArchived
}

// Импортируем. После импорта на устройство будут загружены ключи
DSSCertificatesManagerNonQual.importPfx(
    user: user,
    certificate: archivedCertificate,
    pin: newPin, // Новый пин-код для контейнера ключей
    pfxPin: nil, // Пин-код для расшифрования архива
    completion: { ... }
)


Для удаления архивной копии на сервере доступен метод DSSCertificatesManagerNonQual.removePfx(user:certificate:completion:).

Обновление профиля устройства

Для обновление профиля устройства и симметричных ключей можно воспользоваться методом DSSUsersManager.renew(user:callback:). После использования метода старый DSSUser будет заменён новым.


let userToRenew: DSSUser = ... // Пользователь, которого хотим обновить

DSSUsersManagerNonQual.renew(
    user: userToRenew
) { renewingResult in
    switch result {
        case let .success(renewedUser):  // Обновлённый пользователь
        case let .failure(error): ...
    }
}

Работа с NFC картами и токенами Рутокен

Для работы с NFC требуется отдельная сборка myDSSSDK_NFC

Установка сборки с NFC

Swift Package Manager

Добавьте пакет myDSSSDK_NFC из репозитория:

https://repo.paycontrol.org/git/ios/mydssnfc.git

CocoaPods

Добавьте в файл Podfile зависимости:

pod 'myDSSSDK_NFC',           :git => 'https://repo.paycontrol.org/git/ios/mydssnfc.git'

Ручная установка

Добавьте в проект myDSSSDK_NFC.xcframework.

Ресурсы для NFC

Необходимо добавить в проект ресурсы аналогично инструкции.

Настройка проекта

Для работы с картами и токенами Рутокен нужно дополнительно настроить проект по документации Рутокен.

Убедитесь, что в проект добавлены все необходимые библиотеки и указаны все идентификаторы карт.

Для работы с NFC токенами доступно 2 сценария:


Импорт сертификата с карты

Для импорта сертификата сначала нужно получить с карты все доступные сертификаты, а потом нужный загрузить на сервер.

При перечислении сертификатов будет возвращён массив [Result<DSSCertificate, Error>]. Считывание данных сертификата — отдельная операция. Поэтому возвращаются результаты всех считываний по-отдельности.

После перечисления необходимо выбрать нужный сертификат и выгрузить его в DSS. Для этого нужно передать его в метод DSSCertificateManager.setCertificate(user:certificate:).

После выгрузки сертификата NFC-токен можно использовать для подписании документов.

// Перечисление доступных сертификатов
DSSCertificatesManager.listExternalCertificates { listingResult in
    switch listingResult {
        case let .success(certificatesResults): ... // Результаты считывания сертификатов
        case let .failure(error): ...               // Ошибка при работе с картой
    }
}

let certificateToImport = ... // Выбираем сертификат для импорта

// Загружаем сертификат на сервер
DSSCertificatesManager.setCertificate(
    user: user,
    certificate: certificateToImport
) { settingResult in
    switch settingResult {
        case .success: ...            // Выгрузили и установили на сервере
        case let .failure(error): ... // Ошибка загрузки
    }
}

Создание ключей на карте

Создание ключей происходит при вызове метода DSSCertificatesManagerNonQual.signCertificateRequest. Место для создания ключей выбирается параметром keysSource. По-умолчанию выбрано KeysSourceIdentifier.localGeneric — ключи создаются на устройстве. Для NFC-токенов необходимо выбрать KeysSourceIdentifier.rutokenNFC.

После создания ключей и подписания запроса на сертификат, нужно дождаться выпуска сертификата. Текущие сертификаты проверяем методом DSSCertificatesManager.listCertificates(user:callback:).

Как только сертификат будет выпущен, его можно установить на устройство методом DSSCertificatesManager.install.

После установки сертификата NFC-токен можно использовать для подписания документов.


// Подписываем запрос на сертификат
public static func sign(
    certificateRequest: certificateRequest,
    user: user,
    keysSource: .rutokenNFC // Используем NFC-токен Рутокен
) { signingResult in
    switch signingResult {
        case .success: ...            // Запрос подписан, ключи созданы
        case let .failure(error): ... // Ошибка при работе с карто
    }
}

// Получаем список сертификатов
let certificates = DSSCertificatesManager.listCertificates(...)

// Проверяем, выпущен ли сертификат
let certificateToInstall: DSSCertificate = certificates.first {
    // Сертификат активный
    $0.state == .active && 
    // Ключи хранятся на клиенте
    $0.isClient &&
    // Ключи хранятся на текущем устройстве
    DSSCertificatesManagerNonQual.checkIfAccessibleOnThisDevice(certificate: $0, for: user) &&
    // Сертификт не установлен
    !DSSCertificatesManagerNonQual.checkIfInstalled(certificate: $0, for: user)
} 

// Устанавливаем выпущеный сертификат
public static func install(
    certificate: certificateToInstall,
    user: user
) { installingResult in
    switch signingResult {
        case .success: ...            // Сертификат установлен
        case let .failure(error): ... // Ошибка при установке сертификата
    }
}

Логирование

У фреймворка есть возможность подключить внешний логгер. Для этого логгер должен наследовать протокол DSSLoggerProtocol.

public protocol DSSLoggerProtocol {

    func debug(_ message: String, category: DSSLoggingCategory)
    func error(_ message: String, category: DSSLoggingCategory)
    func sensitive(_ message: String, category: DSSLoggingCategory)

}


/// Пример логгера

struct MyLogger: DSSLoggerProtocol {

    func debug(_ message: String, category: DSSLoggingCategory) {
        ...
    }

    func error(_ message: String, category: DSSLoggingCategory) {
        ...
    }

    func sensitive(_ message: String, category: DSSLoggingCategory) {
        ...
    }

}

После определения логгера, его нужно передать в метод myDSS.setLogger(...):

let myLogger = MyLogger()
// myDSS.setLogLevels([.debug, .keys]) <- Теперь можно убрать из кода приложения
myDSS.setLogger(myLogger, options: [.debug, .sensitive])

После этого сообщения из SDK будут передаваться в установленный логгер.


Сохранение логов в файл

В SDK нет встроенных методов для сохранения логов файл. Но можно подключить стороннюю библиотеку, например CocoaLumberjack:

import CocoaLumberjack
import CocoaLumberjackSwift

...

// Настраиваем CocoaLumberjack
let fileLogger = DDFileLogger()
fileLogger.logFileManager.maximumNumberOfLogFiles = 2  // Храним только 2 файла
fileLogger.maximumFileSize = 256 * 1024                // не более 256kb каждый
fileLogger.rollingFrequency = 60 * 60 * 24             // с сообщениями за последние 24 часа
DDLog.add(fileLogger)

...

// Описываем методы логгера
final class MyLogger: DSSLoggerProtocol {

    func debug(_ message: String, category: DSSLoggingCategory) {
        DDLogInfo("[SDK][\(category)] \(message)")
    }

    func error(_ message: String, category: DSSLoggingCategory) {
        DDLogError("[SDK][\(category)] \(message)")
    }

    func sensitive(_ message: String, category: DSSLoggingCategory) {
        #if DEBUG
        DDLogInfo("[SDK][Sensitive][\(category)] \(message)")
        #endif
    }

}

...

// Назначаем логгер
myDSS.setLogger(MyLogger(), options: [.debug, .sensitive])

Чтобы получить логи, используем API CocoaLumberjack:

fileLogger
    .logFileManager
    .sortedLogFileInfos
    .compactMap { (info: DDInfo) -> Data in
        FileManager.default.contents(atPath: info.filePath) // Читаем логи из файла
    }
    .forEach { (logData: Data) in
        ... // Обрабатываем логи каждого файла
    }

Оформление интерфейса

За внешний вид интерфейса отвечают файлы экранов *.xib и ассеты myDSSSDK_Assets.xcassets.

Xib файлы

Xib Описание
myDSSSDK_MainNavigationController.xib Контроллер навигации всех экранов
myDSSSDK_MainNavigationController.xib Контроллер навигации всех экранов
myDSSSDK_CameraNavigationController.xib Контроллер навигации экрана с камерой
myDSSSDK_OverlayViewController.xib Оверлей с индикатором активности. Закрывает экран приложения при взаимодействии с фреймворком.
myDSSSDK_CameraViewController.xib Экран с камерой
myDSSSDK_ApprovingQRViewController.xib Экран с QR-кодом для присоединения другого устройства
myDSSSDK_PasswordViewController.xib Экран с вводом и созданием ПИН-кода
myDSSSDK_BiometrySuggestionViewController.xib Экран с предложением использовать биометрию
myDSSSDK_DocumentsListViewController.xib Экран со списком документов
myDSSSDK_DocumentPreviewCell.xib Ячейка с превью документа
myDSSSDK_PDFViewController.xib Просмотр PDF документа
myDSSSDK_AcceptAccountActionsViewController.xib Действия при подтверждении присоединения к УЗ
myDSSSDK_ProcessDeviceActionsViewController.xib Действия при подтверждении присоединения к другому устройству
myDSSSDK_DocumentsActionsViewController.xib Действия при подписании документов
myDSSSDK_OperationActionsViewController.xib Действия при работе с операцией с документами
myDSSSDK_InfoViewController.xib Общие экран с таблицей значений
myDSSSDK_KeyValueCell.xib Ячейка с заголовком и значением
RndmBioViewController.xib Генератор случайных чисел CPROCSP
RndmBioViewControllerIPhone.xib Генератор случайных чисел CPROCSP для iPhone

Ассеты

В ассетах myDSSSDK_Assets.xcassets лежат цвета и изображения по-умолчанию.

Совместимость с DSSAppearance

Для обратной совместимости созданы классы с префиксом Custom. Они применяют к элементам стили, описаные в myDSS.appearance.*:

  • CustomView
  • CustomImageView
  • CustomLabel
  • CustomButton
  • CustomKeyboardButton
  • CustomGradientView
  • CustomPDFView

Если сбросить класс компонентов на стандартный (UIView, UILabel, ...), то стили DSSAppearance применяться не будут.

Работа с распространёнными ошибками

DSSError

Данные ошибки возвращает SDK.

При возникновении ошибки DSSError.networkError, необходимо смотреть на параметр code:

12002

The request has timed out

Превышено ожидание ответа от сервера.

12007

The server name could not be resolved.

Не удаётся подключиться к ендпоинту. Возможные причины: - отсутствие интернета - некорректный адрес сервера - сервер недоступен

12175

WinINet failed to perform content decoding on the response. For more information, see the Content Encoding topic.

Возникла проблема с сертификатами. Возможные причины:

  • В бандле приложения нет папок root-certs/prod с корневыми сертификатами сервера
  • Корневые сертификаты имеют некорректный формат. Например, у них расширение .cer, а не .crt. Либо они не в формате PEM.
  • Среди сертификатов нет подходящих для работы с данным сервером
  • Сервер не настроен должным образом

Работа с UI

Методы SDK в которых есть интерфейс, могут возвращать ошибку DSSError.canceled — она означает, что пользователь закрыл UI.

DSSNetworkError

Данные ошибки возвращает сервер. При обработке статуса пользователя можно наткнуться на следующие ошибки:

  • .userNotFound: Пользователь был удалён на сервере
  • .deviceBlocked: Устройство заблокировано на сервере
  • .keyExpiredOrNotYetValid: Срок действия ключей истёк ещё не наступил

Т.е. например при вызове DSSUsersManager.updateStatus мы не получим DSSUser со статусом .expired, а метод вернёт ошибку DSSNetworkError.keyExpiredOrNotYetValid

История изменений

2.1.324

  • Исправлена проблема с поиском ресурсов SDK

2.1.323

  • SDK теперь делится на версию NFC и без NFC.
  • Исправлена ошибка «Неверный HMAC», возникающая на iOS 17.
  • При подписании документов кнопка подписать будет неактивной, если у пользователя нет сертификатов.
  • Добавлена дополнительная информация в структуру DSSOperationHistory.
  • Внутренние оптимизации и улучшения.

2.1.312

  • Добавлена поддержка работы с NFC-токенами.
  • Обновили методы DSSCertificatesManagerNonQual для работы с NFC.
  • Исправлено дублирование страниц PDF документов.
  • Добавлена поддержка ошибок prov_type_not_defined, invalid_chv при работе с NFC.
  • Исправлена видимость ошибок CPROCSPError.
  • Исправлена кнопка «Закрыть» при подписании запроса на сертификат.
  • Исправления локализации.
  • Исправлены ошибки при работе с новыми экранами ввода пин-кодов и паролей.
  • Исправлено падение на iPad при попытке загрузить документ.
  • Внутренние оптимизации и улучшения.

2.1.288

  • Небольшие исправления локализации

2.1.287

  • Добавлена поддержка парольных политик. Теперь пароль может содержать буквы и символы. Для сложных паролей добавлен отдельный тип экранов.
  • Если пользователь запретил доступ к камере, то теперь ему будет предложено перейти в настройки.
  • Исправлены условия проверки активных сертификатов при подписании

2.1.276

  • Добавлена проверка сертификатов при подписании документов. Если нет активных сертификатов, то кнопка «Подписать» не будет активной.

2.1.274

  • Добавлена возможность скачать документ при подписании

2.1.272

  • Ошибка DSSNetworkError переименована в DSSServerError.
  • Добавлена серверная ошибка operation_expired.

2.1.271

  • Обновлены параметры ошибки DSSError.confirmationError и DSSError.signingError.