Аутентификация с использованием биометрии на iOS и Android с ключами, привязанными к аппаратному обеспечению
Одного лишь Face ID недостаточно для подтверждения личности на вашем сервере. Используйте Secure Enclave и ключи Android Keystore для подписи одноразовых запросов, обеспечивающих дополнительную проверку личности.
Источники: Biometrics, the Secure Enclave, and Auth Tokens: Three Ideas That Sound the Same But Aren’t
В характеристиках продукта часто указано, что «Face ID разблокирует приложение», и для пользователей такое описание вполне достаточно. Однако для команд, разрабатывающих системы оплат, медицинские записи, консоли администрирования или процессы входа, это скрывает самое важное: биометрический скан никогда не попадает в ваш бэкенд, никогда не создаёт токенов и никогда не передаёт вашему коду шаблоны лица или отпечатка пальцев. На самом деле хорошо спроектированное приложение получает разрешение на использование защищённого оборудования приватного ключа, который недоступен для чтения вашим кодом; этот ключ может подписать одноразовый запрос, который затем проверяется сервером. В этом руководстве разделяются три понятия, которые часто смешиваются (биометрические запросы, ключи, поддерживаемые оборудованием, и токены аутентификации), и описывается процесс регистрации с последующей подписью на обеих платформах.
Почему один только токен — это слабая гарантия на мобильном устройстве
Мобильное приложение работает на оборудовании, которым управляет пользователь, а не вы. Телефоны теряются, экраны блокируются неправомерно, во время разработки могут быть подключены инструменты отладки, а устройство с рут-доступом или jailbreak может раскрыть гораздо больше информации о ваших процессах, чем хотелось бы.
Несмотря на все это, чувствительный конечный пункт должен доверять точному утверждению: звонок исходит с телефона, содержащего ключ, ранее зарегистрированный сервером, и человек, использующий этот ключ, был одобрен недавно.
Сравните это с фразой «в этом запросе есть токен». Любое значение, находящееся в памяти или хранилище приложения, может быть скопировано и воспроизведено из другого места. Частный ключ, генерируемый в защищенном оборудовании и никогда не могущий быть экспортирован, представляет собой гораздо более сложную цель, поскольку его кража означает атаку на само оборудование, а не просто чтение строки.
Прежде чем что-либо создавать, полезно определить, что доказывает каждый элемент.
- Биометрический метод аутентификации: телефон использует Face ID, Touch ID или современные биометрические функции Android.
- Защищенный аппаратный ключ: зашифрованный приватный ключ генерирует подпись.
- Токен аутентификации: ваш сервер предоставляет доступ к сессии или определенным ресурсам.
Надежная система использует все три метода, причем каждый из них выполняет свою функцию.
HTTPS защищает канал связи, а не личность пользователя
HTTPS шифрует данные, передаваемые между приложением и вашим API, поэтому пользователи, подключенные к общественной сети Wi-Fi, не могут легко прочитать или изменить эти запросы.
phone ── encrypted HTTPS connection ──► server
Это обязательно, но само по себе этого недостаточно. Если злоумышленник уже получил действительный токен, TLS будет надежно передавать этот украденный токен на ваш сервер. В зашифрованном соединении нет никаких данных, указывающих серверу, пришел ли токен с законного устройства или с копии.
Итак, основные принципы: использовать HTTPS при каждом запросе, проверять сертификаты стандартным способом, поддерживать обновленную версию TLS-стека, а также добавить механизм аутентификации, который позволяет проверять информацию о устройстве и действии пользователя.
Фиксация сертификатов — узкоспециализированный инструмент
Фиксация ограничивает работу приложения известным сертификатом или идентификатором публичного ключа. В строго контролируемых приложениях это может снизить риск атак типа «человек посередине».
Однако он не позволяет сделать токен-носитель похожим на учетную информацию, защищенную аппаратными средствами. Кроме того, он несет реальные операционные риски: если указать неверную информацию для фиксации или пропустить смену сертификата, все установленные копии приложения мгновенно перестанут взаимодействовать с вашим API. Применяйте его только в том случае, если это оправдано вашей моделью угроз и процесс выпуска позволяет надежно обрабатывать смену ключей, и никогда не рассматривайте его в качестве замены аутентификации, связанной с устройством.
Биометрическое запросное сообщение — это лишь локальный ответ «да» или «нет»
В iOS приложение запрашивает у LAContext отображение системного окна аутентификации; в Android аналогом является BiometricPrompt. Операционная система выполняет сканирование и сообщает о результате — успехе или неудаче. Приведённый ниже фрагмент написан на Swift: он проверяет политику использования только биометрии с указанием причины и начинает передачу данных, если проверка проходит.
let context = LAContext()
let policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics
context.evaluatePolicy(policy, localizedReason: "Approve this transfer") { success, error in
guard success else { return }
startTransfer()
}
Ваш код никогда не получает информацию о геометрии лица или изображениях отпечатков пальцев. Apple описывает LocalAuthentication как инструмент, работающий с Secure Enclave и возвращающий приложению лишь результат проверки — «успех» или «неудача» (Справка по фреймворку LocalAuthentication).
Этого вполне достаточно для защиты приватного контента внутри приложения. Однако в качестве доказательства для сервера этого недостаточно по трём причинам:
- В результате указано лишь «устройство приняло проверку»; в нем нет информации о том, кто или какой аккаунт это был.
- У серверной части нет способа проверить логическое значение, генерируемое на клиенте.
- Если процесс приложения скомпрометирован, злоумышленник может пропустить или подделать функцию возврата и просто повторно использовать уже существующий токен.
Главный вывод из этого подхода заключается в том, что переменная success — это всего лишь флаг в вашем собственном памятном пространстве. Более надежный дизайн использует биометрическую проверку для разблокировки одной криптографической операции, результат которой сервер может проверить самостоятельно.
Хранилища хардверных ключей сохраняют приватный ключ вне процесса приложения
У устройств Apple имеется Secure Enclave — изолированная подсистема, которая может генерировать и использовать приватные ключи, не позволяя сырым данным ключей попадать в основной процессор приложений. Согласно документации Apple, ключ остается внутри Secure Enclave, а приложение получает только результаты его работы, такие как подписи (Руководство Apple по использованию Secure Enclave для защиты ключей).
Аналогом на другой платформе является Android Keystore. В зависимости от аппаратного обеспечения ключи могут храниться во взаимодействии с средой надежной обработки (TEE) или в специализированном защищенном элементе StrongBox. Будьте осторожны с названиями: «Titan M» — это чип в телефонах Google Pixel, а не общий API Android, и нельзя предполагать, что каждое устройство его имеет. С точки зрения приложения важно то, как обеспечивается хранение ключа и какие правила использования к нему применяются.
Для выполнения действий высокой значимости ключ должен соответствовать четырем правилам:
- Он генерируется именно на этом устройстве.
- Для каждого использования требуется новое подтверждение с помощью биометрии.
- Частная часть ключа никогда не может быть экспортирована.
- На сервере хранится только публичный ключ.
В Android объект BiometricPrompt.CryptoObject может разрешать криптографические операции только тогда, когда класс аутентификатора равен BIOMETRIC_STRONG, и ключ может быть настроен так, чтобы требовать нового подтверждения при каждом использовании (Справка BiometricPrompt, Обзор хранилища ключей).
Протокол: регистрация ключа один раз, подпись новых запросов позже
Вся схема состоит из двух этапов: регистрации и использования.
Регистрация
- Приложение генерирует пару ключей с использованием аппаратного обеспечения.
- Оно загружает публичный ключ на сервер через HTTPS.
- Сервер сохраняет публичный ключ вместе с данными учетной записи и идентификатором устройства.
- Частный ключ остается на телефоне, в защищенном аппаратном модуле.
Отправка публичного ключа не представляет опасности: он может проверять подписи, но не создавать их. Только приватный ключ может подписывать, и он никогда не покидает защищенное аппаратное обеспечение.
Утверждение конфиденциальной заявки
- Приложение запрашивает у сервера одноразовый случайный вызов.
- Пользователь выполняет биометрическую проверку.
- Защищенное аппаратное обеспечение подписывает этот вызов.
- Приложение возвращает идентификатор вызова и подпись.
- Сервер проверяет подпись с использованием сохраненного публичного ключа.
- Сервер принимает или отклоняет действие, после чего может выдать токен.
Именно здесь большинство реализаций допускают ошибки. Сигнатура должна генерироваться случайным образом, истекать быстро и использоваться ровно один раз. Кроме того, она должна быть привязана к аккаунту, к конкретному ключу устройства и к действию, которое подлежит одобрению (например, «перевести 200 евро на счет X»). Если пренебречь хотя бы одним из этих условий, захваченная сигнатуру можно будет впоследствии использовать для другого запроса, что является классической атакой повторного использования. Привязка к действию также предотвращает перенаправление сигнатуры, собранной для безвредной операции, на опасную.
iOS: ключ Secure Enclave, требующий использования Face ID при каждом использовании
В iOS ключ создается непосредственно внутри Secure Enclave, и к нему привязывается объект управления доступом, требующий использования биометрических данных каждый раз при его использовании. В приведённом ниже коде на Swift обратите внимание на три возможных параметра. Параметр kSecAttrAccessibleWhenUnlockedThisDeviceOnly предотвращает перемещение ключа в резервные копии или на другие устройства. Флаги .privateKeyUsage и .biometryCurrentSet требуют биометрической верификации для подписи и связывают ключ с текущими зарегистрированными лицами или пальцами. А параметр kSecAttrTokenIDSecureEnclave отвечает за размещение ключа в аппаратном обеспечении; Secure Enclave поддерживает только ключи с длиной 256 бит по эллиптической кривой (P-256), поэтому тип и размер ключа устанавливаются именно таким образом.
// 1. Create access control requiring Face ID / Touch ID for every use
var error: Unmanaged<CFError>?
guard let accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage, .biometryCurrentSet],
&error
) else { return }
// 2. Generate the key pair in the Secure Enclave
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.keys.transfer",
kSecAttrAccessControl as String: accessControl
]
]
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else { return }
Практическим последствием использования .biometryCurrentSet является то, что регистрация нового отпечатка пальца или сброс Face ID приводит к аннулированию ключа. Обычно это и нужно для проведения платежей, но это означает, что ваше приложение должно корректно обрабатывать ситуацию, когда ключ отсутствует, и направлять пользователя на повторную регистрацию.
Загрузите публичный ключ после того, как пользователь завершит обычную процедуру входа в учетную запись. Позже, когда сервер вернет случайный запрос на перевод, подпишите его с помощью приватного ключа. Вам не нужно самостоятельно показывать подтверждение: благодаря флагам контроля доступа операция подписи автоматически активирует Face ID или Touch ID.
let algorithm: SecKeyAlgorithm = .ecdsaSignatureMessageX962SHA256
var signError: Unmanaged<CFError>?
// 3. This call automatically triggers the Face ID prompt because of the accessControl flags
guard let signature = SecKeyCreateSignature(
privateKey,
algorithm,
challengeData as CFData,
&signError
) else { return }
api.approve(challengeId, signature)
Если пользователь отменяет сканирование или оно проваливается, функция SecKeyCreateSignature возвращает nil, и подпись для отправки отсутствует, поэтому у злоумышленника нет возможности изменить флаг успешности.
Android: ключ хранилища, разблокированный с помощью BiometricPrompt
Приведённый ниже код на Kotlin создаёт ключ ЭЦП только для подписи в провайдере AndroidKeyStore. Метод setUserAuthenticationRequired(true) делает аутентификацию обязательной, а setUserAuthenticationParameters(0, AUTH_BIOMETRIC_STRONG) устанавливает период действия ключа равным нулю секунд, что означает, что при каждом использовании требуется новая строгая биометрическая проверка вместо периода отсрочки после предыдущего разблокирования.
val spec = KeyGenParameterSpec.Builder(
"transfer-key",
KeyProperties.PURPOSE_SIGN
).setDigests(KeyProperties.DIGEST_SHA256)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(
0,
KeyProperties.AUTH_BIOMETRIC_STRONG
).build()
val generator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
"AndroidKeyStore"
)
generator.initialize(spec)
val publicKey = generator.generateKeyPair().public.encoded
После того как пользователь войдёт в систему, отправьте закодированный publicKey на сервер и сохраните его вместе с данными пользователя и устройства.
Когда сервер позже отправит запрос на передачу данных, инициализируйте объект Signature, оберните его в объект CryptoObject и передайте полученный результат на обработку. Только внутри метода onAuthenticationSucceeded введите байты запроса и сгенерируйте подпись.
val signature = Signature.getInstance("SHA256withECDSA")
signature.initSign(privateKey)
val crypto = BiometricPrompt.CryptoObject(signature)
prompt.authenticate(promptInfo, crypto)
override fun onAuthenticationSucceeded(result: AuthenticationResult) {
val signed = result.cryptoObject!!.signature!!
.apply { update(challenge) }.sign()
api.approve(challengeId, signed)
}
Диалоговое окно не является границей безопасности; ею является CryptoObject. Хранилище ключей откажется выполнять операцию подписи, если проверка биометрических данных, связанная с ней, не пройдёт успешно, причём сам материал ключа остаётся внутри защищённого хранилища. Ваше приложение хранит только полученную в результате подпись.
На сервере проверьте эту подпись с использованием зарегистрированного публичного ключа. Если она совпадает с невыгоревшим, неиспользованным вызовом на верификацию, одобрите передачу данных или выдайте краткосрочный токен, ограниченный данным действием.
Где всё ещё должны находиться токены аутентификации
Проверка подписи, основанной на аппаратном обеспечении, не устраняет необходимость в токенах. После этой проверки полностью оправдано выдавать обычный токен доступа, поскольку токены по-прежнему остаются практичным способом авторизации большей части трафика API. Если вы разрабатываете механизм токенов в системе, здесь также применимы рекомендации по компромиссам, изложенные в нашем руководстве по стратегии обновления токенов.
Храните эти токены в защищённых платформой хранилищах: Keychain в iOS и Keystore в Android. Долговечные токены обновления никогда не должны храниться в обычных настройках, стандартных файлах или хранилищах, доступных через JavaScript, что особенно важно в приложениях React Native, где AsyncStorage является стандартным решением, но нешифрованным.
Затем возникает вопрос о том, как часто требовать новую аппаратную подпись:
- При каждом вызове API: почти всегда слишком медленно и раздражает пользователя.
- При каждом запуске приложения: разумный вариант для локального замка приложения.
- При оплате, редактировании профиля, экспорте данных или одобрении нового устройства: часто является оптимальным решением.
- При каждом использовании токена обновления: оправдано для аккаунтов с высоким уровнем риска, при условии что пользователь может справиться с поступающими уведомлениями.
Считайте подпись своего рода дополнительным доказательством, а токен — временным разрешением, выдаваемым на его основе.
Ограничения ключей, поддерживаемых аппаратным обеспечением
Этот подход эффективен, но не является универсальным решением:
- Он бесполезен против законного пользователя, который сам разблокирует свой телефон и согласится на вредоносные действия, например под давлением социальной инженерии.
С самого начала спланируйте путь восстановления, сделав его намеренно медленнее и требующим более строгой проверки по сравнению с обычным путем. Когда исчезает ключ устройства, сбрасывается экран блокировки или меняется регистрация биометрических данных, ваше приложение должно быть готово к регистрации нового ключа, а ваш сервер — отзыву старого. В документации Android указано, что ключи, связанные с процедурой аутентификации, могут быть окончательно аннулированы при изменении защищенного экрана блокировки или при изменении регистрации биометрических данных, в зависимости от способа настройки ключа (справка KeyProtection.Builder).
Как парольные ключи связаны с этой архитектурой
Пасскейсы основаны на том же принципе: паре публичного/частного ключа и запросе от сервера, подписанном частным ключом. Для входа на сайты и в учётные записи они обычно являются лучшим решением, поскольку операционная система и платформа берут на себя большую часть сложных задач. Пасскейсы — это стандартизированный межплатформенный протокол, основанный на этом подходе к подписям с использованием аппаратного обеспечения. Есть один нюанс: многие пасскейсы синхронизируются между устройствами пользователя через менеджер учетных данных платформы, поэтому они не всегда привязаны к одному телефону, как ключи из Keystore или Secure Enclave, которые нельзя экспортировать.
Это различие предполагает простое деление. Используйте приложенный ключ, поддерживаемый аппаратным обеспечением, когда нативному приложению требуется подтверждение, связанное с устройством, для выполнения определенной операции. Для современных способов входа без пароля, которые поддерживаются вашим продуктом, используйте парольные ключи. В любом случае задача сервера остается прежней: проверять подпись с помощью публичного ключа, которому он уже доверяет.
Выбор подходящего инструмента для каждой задачи
- Защита данных во время передачи: HTTPS.
- Поддержание входа пользователя: краткосрочный токен доступа плюс защищенный токен обновления.
- Защита приватного контента при запуске приложения: Face ID, Touch ID или BiometricPrompt.
- Утверждение платежа или другой операции с высоким риском: подпись с использованием аппаратных средств, защищенная биометрией.
- Вход без пароля: парольные ключи.
- Снижение риска перехвата данных по протоколу TLS: фиксация сертификата.
Основные выводы
Сканирование Face ID и отпечатков пальцев не подтверждают авторизацию запроса к бэкенду. Они лишь разрешают использование ключа; защищенное оборудование использует этот ключ для подписи нового запроса от сервера, а сервер проверяет подпись перед тем, как определить, что может делать пользователь.
- Биометрия: локальное подтверждение, невидимое для сервера.
- Аппаратный ключ: доказательство наличия ключа, которое невозможно скопировать с устройства.
- Токен: краткосрочное разрешение от сервера.
Следует сохранять четкое разделение этих ролей, связывать каждый запрос с конкретным аккаунтом, ключом и действием, а также планировать процедуры аннулирования и восстановления ключей. В результате получается система мобильной аутентификации, которая проще для понимания и значительно сложнее для подделки.