Реализация электронной подписи в мобильном приложении
Электронная подпись в мобильном приложении — это не картинка с подписью поверх документа. Это криптографическая операция: документ подписывается приватным ключом пользователя, верификатор проверяет подпись публичным ключом и гарантирует, что документ не изменился после подписания и что подписал именно этот пользователь.
В зависимости от юрисдикции и требований различают квалифицированную (КЭП — с токеном и сертификатом от аккредитованного УЦ), усиленную неквалифицированную (НЭП — своя PKI) и простую электронную подпись (ПЭП — логин/пароль, SMS-код). Для большинства коммерческих кейсов достаточно НЭП; КЭП нужна для госуслуг и юридически значимого документооборота с госорганами.
PKI на мобильном устройстве
Самый распространённый подход для НЭП: приватный ключ генерируется на устройстве и хранится в Keychain (iOS) или Android Keystore. Публичный ключ регистрируется на сервере. Подписание происходит на устройстве — приватный ключ никогда не покидает Secure Enclave/TEE.
Генерация ключевой пары на Android:
val keyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"
)
keyPairGenerator.initialize(
KeyPairGeneratorSpec.Builder(context)
.setAlias("user_signing_key")
.setKeyType("EC")
.setKeySize(256)
.setSubject(X500Principal("CN=User"))
.setSerialNumber(BigInteger.ONE)
.setStartDate(startDate)
.setEndDate(endDate)
.build()
)
val keyPair = keyPairGenerator.generateKeyPair()
// публичный ключ регистрируем на сервере
val publicKeyBase64 = Base64.encode(keyPair.public.encoded)
Для подписания документа:
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val privateKey = keyStore.getKey("user_signing_key", null) as PrivateKey
val signature = Signature.getInstance("SHA256withECDSA")
signature.initSign(privateKey)
signature.update(documentBytes)
val signatureBytes = signature.sign()
iOS, Swift:
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: "com.example.signing".data(using: .utf8)!
]
]
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
// обработать ошибку
}
let publicKey = SecKeyCopyPublicKey(privateKey)!
kSecAttrTokenIDSecureEnclave — ключ создаётся внутри Secure Enclave и не может быть извлечён даже при компрометации основного процессора.
Подписание с биометрической аутентификацией
Для юридически значимых операций ключ подписания должен быть защищён подтверждением личности. Привязка к биометрии через Keystore/Keychain: ключ доступен только после успешной биометрической аутентификации в рамках текущей сессии.
Android:
keyGenParameterSpec = KeyGenParameterSpec.Builder(...)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
.build()
При каждом подписании:
val biometricPrompt = BiometricPrompt(activity, executor, callback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Подпишите документ")
.setSubtitle("Используйте биометрию для подтверждения")
.setNegativeButtonText("Отмена")
.build()
// привязываем CryptoObject с нашим Signature объектом
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(signature))
В onAuthenticationSucceeded callback получаем result.cryptoObject?.signature — разблокированный объект подписи, готовый к работе.
Формат подписи и верификация на сервере
Для стандартизации используем CMS (Cryptographic Message Syntax, RFC 5652) или JWS (JSON Web Signature, RFC 7515). JWS удобнее для REST API:
Header.Payload.Signature
Header: {"alg": "ES256", "kid": "user_key_id"}
Payload: base64url-encoded документ или его хэш
Signature: ES256 подпись
На сервере верификация через любую JWT библиотеку с ES256 поддержкой. Python: PyJWT, cryptography. Node.js: jose, jsonwebtoken. Java: nimbus-jose-jwt.
Долгосрочная валидность и timestamp
Если документ должен оставаться валидным годами (после истечения сертификата подписанта), используем RFC 3161 Trusted Timestamping. Timestamp Authority (TSA) подписывает хэш документа + время своим ключом. Даже если ключ пользователя позже скомпрометирован, timestamp доказывает, что подпись была создана в определённый момент времени.
Публичные TSA: Freetsa.org, DigiCert TSA. Интеграция: bouncycastle на Android, Security.framework + RFC 3161 запрос на iOS.
Варианты интеграции с внешними КЭП-провайдерами
Для квалифицированной подписи в России — КриптоПро, Рутокен, ViPNet. У каждого есть мобильные SDK. Интеграция через CryptoPro CSP SDK для Android/iOS: подписание на стороне токена (физического или программного), приложение только передаёт данные SDK.
Для международных кейсов — DocuSign SDK, Adobe Sign API, HelloSign. Они абстрагируют криптографию, но привязывают к конкретному провайдеру.
Сроки реализации НЭП с генерацией ключей в Keystore/Keychain, биометрическим подтверждением и серверной верификацией — 3–5 дней. Интеграция с КЭП-провайдером или внешним документооборотным сервисом — индивидуальная оценка после анализа API провайдера.







