Головна / Статті

Біометричне підтвердження доступу на iOS та Android з ключами, прив’язаними до обладнання

Сам Face ID нічого не доводить вашому серверу. Використовуйте Secure Enclave та ключі Android Keystore для підписування одноразових викликів з метою справжньої посиленої аутентифікації.

Видавець: REACTAPP.TOP12 хв читання2762 слів

Джерела: Biometrics, the Secure Enclave, and Auth Tokens: Three Ideas That Sound the Same But Aren’t

У характеристиках продукту часто зазначається, що «Face ID розблоковує додаток», і для користувачів цього опису достатньо. Однак для команд, які створюють системи оплат, медичні записи, консолі адміністрування чи процеси входу, це приховує найважливіше: біометричний скан ніколи не потрапляє до вашого бекенду, не створює токена та не передає вашому коду шаблон обличчя чи відбитка пальця. Насправді добре спроєктований додаток отримує дозвіл на використання захищеного обладнання приватного ключа, який неможливо прочитати за допомогою коду; цей ключ може підписати одноразовий виклик, який потім перевіряє сервер. У цьому посібнику розглядаються три поняття, які часто змішуються між собою (біометричні запити, ключі, підтримувані обладнанням, та токени автентифікації), а також описується процес реєстрації та підписування на обох платформах.

Чому сам токен є слабкою гарантією на мобільному пристрої

Мобільний додаток працює на обладнанні, яким керує користувач, а не ви. Телефони губляться, екрани блокування можуть бути обійдені, під час розробки використовуються інструменти для дебаггінгу, а пристрій з рутованим чи jailbroken системним кешем може викрити набагато більше інформації про ваші процеси, ніж вам б хотілося.

Незважаючи на все це, конфіденційний кінцевий пункт має довіряти точному твердженню: дзвінок надходить з телефону, який має ключ, раніше зареєстрований на сервері, та особою, яка нещодавно схвалила використання цього ключа.

Порівняйте це з твердженням „цей запит містить токен“. Будь-яке значення, що знаходиться в пам’яті чи на зберіганні додатку, може бути скопійоване та використане з іншого джерела. Приватний ключ, створений у захищеному обладнанні та який неможливо експортувати, є значно складнішою мішенню, адже його крадіжка означає атаку на саме обладнання, а не просто читання рядка тексту.

Перш ніж щось створювати, корисно визначити, що саме доводить кожен елемент.

  • Біометричний метод автентифікації: телефон підтримує 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, і не варто припускати, що кожен пристрій його має. З точки зору додатку важливо те, як захищений ключ та які правила використання до нього застосовуються.

Для дій високої цінності ключ має відповідати чотирьом правилам:

  1. Він генерується саме на цьому пристрої.
  2. Кожне використання вимагає нової сильної біометричної підтвердження.
  3. Приватну частину ніколи не можна експортувати.
  4. На сервері зберігається лише публічний ключ.

У Android BiometricPrompt.CryptoObject може забезпечувати захист криптографічних операцій лише тоді, коли клас автентифікатора дорівнює BIOMETRIC_STRONG, і ключ може бути налаштований так, щоб вимагати нового схвалення при кожному використанні (Довідка BiometricPrompt, Огляд Keystore).

Протокол: реєструйте ключ один раз, після цього підписуйте нові виклики

Уся схема складається з двох фаз – реєстрації та використання.

Реєстрація

  1. Додаток генерує пару ключів, підтримувану апаратним забезпеченням.
  2. Він завантажує публічний ключ на сервер через HTTPS.
  3. Сервер записує публічний ключ разом із обліковим записом та ідентифікатором пристрою.
  4. Приватний ключ залишається на телефоні, у захищеному апаратному пристрої.

Надсилання публічного ключа не є шкідливим: він може перевіряти підписи, але не може створювати їх. Лише приватний ключ може ставити підпис, і він ніколи не залишає захищеного обладнання.

Схвалення конфіденційного запиту

  1. Додаток запитує у сервера одноразовий випадковий виклик.
  2. Користувач виконує біометричну дію.
  3. Захищене обладнання ставить підпис на цей виклик.
  4. Додаток повертає ідентифікатор виклику та підпис.
  5. Сервер перевіряє підпис за допомогою збереженого публічного ключа.
  6. Сервер приймає або відхиляє дію та може видати токен.

Саме тут більшість реалізацій роблять помилки. Цей токен має бути випадковим, швидко закінчувати свою дію та використовуватися лише один раз. Він також повинен бути прив’язаний до облікового запису, конкретного ключа пристрою та до дії, яка схвалюється (наприклад, „переказати 200 EUR на рахунок 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: ключ Keystore, розблокований через BiometricPrompt

Наведений нижче код на Kotlin створює ключ EC лише для підписування у провайдері 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: майже завжди занадто повільно та дратує користувача.
  • Під час кожного запуску додатка: розумний вибір для блокування приватного додатка.
  • Під час оплат, редагування профілю, експорту даних чи схвалення нового пристрою: часто є оптимальним рішенням.
  • Під час кожного використання токена оновлення: це доцільно для обліків з високим ризиком, за умови, що користувацький досвід може витримати відповідні запити.

Уявіть собі підпис як посилену перевірку, а токен — як тимчасове дозволення, що випливає з неї.

Обмеження ключів, підтримуваних апаратним забезпеченням

Цей підхід є ефективним, але не є універсальним рішенням:

  • Він не допомагає проти законного користувача, який розблоковує власний телефон та схвалює шкідливу дію, наприклад під тиском соціальної інженерії.
  • Це не робить пристрій з рутом чи jailbreak надійним; воно лише ускладнює вилучення та клонування ключа.
  • Це не замінює виявлення шахрайств на стороні сервера, обмеження кількості запитів чи моніторинг аномалій.
  • Це не усуває потреби у відновленні облікового запису, адже люди гублять телефони та знову підключають біометричні дані.
  • Це підтверджує контроль над зареєстрованим ключем після локальної затвердження, а не справжню особистість людини.
  • З самого початку сплануйте шлях відновлення та зробіть його навмисно повільнішим та суворіше перевірюваним, ніж звичайний шлях. Коли ключ пристрою зникає, екран блокування скидається або змінюється процес реєстрації біометричних даних, ваше додаток має бути готовим до реєстрації нового ключа, а ваш сервер — скасувати старий. У документації Android зазначено, що ключі, пов’язані з процесом автентифікації, можуть бути остаточно анульовані після зміни захищеного екрана блокування або після зміни процесу реєстрації біометричних даних, залежно від того, як був налаштований ключ (довідка KeyProtection.Builder).

    Як passkeys пов’язані з цією архітектурою

    Паскоди ґрунтуються на тій самій основі: парі публічного/приватного ключа та виклику від сервера, підписаному приватним ключем. Для входу на веб-сайти та облікові записи вони зазвичай є найкращим рішенням, оскільки операційна система та платформа беруть на себе більшу частину складних завдань. Паскоди — це стандартизований, міжплатформенний протокол, який ґрунтується на цьому підході до підписування з використанням апаратного забезпечення. Одна нюанс: багато паскодів синхронізуються між пристроями користувача через менеджер облікових даних платформи, тому вони не завжди прив’язані до одного телефону, на відміну від ключів Keystore чи Secure Enclave, які не можна експортувати.

    Це розрізнення вказує на простий поділ. Використовуйте ключ додатку, підтримуваний апаратним забезпеченням, коли нативний додаток потребує підтвердження, прив’язаного до пристрою, для певної дії. Використовуйте ключі доступу для сучасного входу без пароля там, де ваш продукт їх підтримує. У будь-якому разі завдання сервера залишається однаковим: перевірити підпис за допомогою загального ключа, якому він вже довіряє.

    Вибір правильного інструменту для кожної потреби

    • Захист даних під час передачі: HTTPS.
    • Збереження авторизованого статусу користувача: тимчасовий токен доступу разом із захищеним токеном оновлення.
    • Заблокування приватного контенту під час запуску додатку: Face ID, Touch ID або BiometricPrompt.
    • Схвалення оплати чи іншої дії з високим ризиком: підпис, захищений біометричними даними.
    • Вхід без пароля: ключі доступу.
    • Зменшення ризику переривання TLS-з’єднання: фіксація сертифіката.
  • Зберігання конфіденційності даних навіть від власного сервера: шифрування даних від початку до кінця.
  • Основні висновки

    Сканування Face ID та відбитків пальців не автентифікують запит до сервера. Вони дозволяють використовувати ключ; захищений обладнання використовує цей ключ для підписування нового запиту від сервера; а сервер перевіряє підпис перед тим, як вирішити, що може робити користувач.

    • Біометрія: локальна затвердження, невидима для сервера.
    • Апаратний ключ: доказ наявності, який не можна скопіювати з пристрою.
    • Токен: тимчасове дозволу від сервера.

    Тримайте ці ролі окремими, прив’язуйте кожен запит до конкретного облікового запису, ключа та дії, а також плануйте процедури анулювання та відновлення ключів. У результаті отримується дизайн мобільної автентифікації, який легше зрозуміти та значно складніший для підробки.