Autoryzacja biometryczna na poziomie wzrostu stopy w iOS i Android z kluczami powiązanymi z sprzętem
Sam Face ID nic nie dowodzi na korzyść twojego serwera. Użyj kluczy Secure Enclave oraz Android Keystore do podpisywania jednorazowych wyzwań w celu zapewnienia rzeczywistej, zaostrzonej autoryzacji.
Źródła: Biometrics, the Secure Enclave, and Auth Tokens: Three Ideas That Sound the Same But Aren’t
Specyfikacje produktów często podają, że „Face ID odblokowuje aplikację”, a dla użytkowników to opis jest wystarczający. Jednak w przypadku aplikacji do zarządzania płatnościami, dokumentacją medyczną, konsolami administracyjnymi lub procesem logowania, ukrywa to to, co jest najważniejsze: skan biometryczny nigdy nie trafia do twojego backendu, nigdy nie tworzy tokena i nigdy nie przekazuje twojemu kodowi szablonu twarzy czy odcisku palca. W rzeczywistości dobrze zaprojektowana aplikacja uzyskuje zezwolenie na korzystanie przez zabezpieczony sprzęt z klucza prywatnego, którego twój kod nie może odczytać, aby ten klucz mógł podpisać jednorazowy wyzwanie, które następnie serwer weryfikuje. Ten przewodnik rozróżnia trzy koncepcje, które często są ze sobą mylone (prośby o dane biometryczne, klucze wspierane sprzętem oraz tokeny autoryzacyjne), i opisuje proces rejestracji a następnie podpisu na obu platformach.
Dlaczego sam token to słaba gwarancja na urządzeniu mobilnym
Aplikacja mobilna działa na sprzęcie, którym kontroluje użytkownik, a nie ty. Telefony się gubią, ekrany blokujące można obejść, podczas rozwoju instalowane są narzędzia do debugowania, a urządzenie z rootem lub jailbreakiem może ujawnić o wiele więcej informacji na temat twoich procesów, niż byś chciał.
Mimo to wrażliwy punkt końcowy musi ufać precyzyjnemu stwierdzeniu: połączenie pochodzi z telefonu posiadającego klucz wcześniej zarejestrowany przez serwer, a osoba ta akceptowała używanie tego klucza chwilę temu.
Porównaj to z stwierdzeniem „ta prośba zawiera token”. Każdą wartość przechowywaną w pamięci lub plikach aplikacji można skopiować i odtworzyć z innego miejsca. Klucz prywatny generowany w zabezpieczonym sprzęcie i niepodlegający eksportowi stanowi znacznie trudniejszy cel, ponieważ jego kradzież oznacza atak na sam sprzęt, a nie tylko odczytanie ciągu znaków.
Zanim cokolwiek zbudujesz, przydatne jest określenie, co dowodzi każda z tych elementów:
- Zapytanie biometryczne: telefon akceptuje Face ID, Touch ID lub zaawansowane funkcje biometryczne Androida.
- Zabezpieczony klucz sprzętowy: chroniony klucz prywatny służy do tworzenia podpisów.
- Tok autoryzacyjny: twój serwer udziela dostępu do sesji lub określonych funkcji.
Zaawansowany projekt wykorzystuje wszystkie trzy metody, z których każda pełni swoją specyficzną rolę.
HTTPS chroni kanał komunikacji, a nie tożsamość
HTTPS szyfruje ruch pomiędzy aplikacją a twoją API, dzięki czemu osoba korzystająca z sieci Wi-Fi w kawiarni nie może łatwo odczytać ani zmodyfikować żądań.
phone ── encrypted HTTPS connection ──► server
Jest to obowiązkowe, ale samo w sobie nie wystarcza. Jeśli atakujący już uzyskał ważny token, TLS wiernie i bezpiecznie dostarczy ten skradziony token do twojego serwera. Nic w szyfrowanym połączeniu nie informuje backendu, czy token pochodzi z prawdziwego urządzenia, czy z kopii.
Zatem podstawowe zasady to: używanie HTTPS przy każdym połączeniu, weryfikacja certyfikatów w standardowy sposób, utrzymywanie aktualizowanego stacka TLS oraz dodanie na wierzch mechanizmu autoryzacji, który dostarcza wiarygodnych informacji na temat urządzenia i działań użytkownika.
Pinning certyfikatów to narzędzie o ograniczonym zastosowaniu
Pinning ogranicza aplikację do określonego certyfikatu lub tożsamości klucza publicznego. W aplikacjach pod ścisłą kontrolą może zmniejszyć ryzyko ataków typu man-in-the-middle.
Nie może jednak sprawić, by token nośny zachowywał się jak kredencjały wspierane sprzętowo. Ponadto niesie ze sobą rzeczywiste ryzyko operacyjne: błędny pin lub zaniedbanie rotacji certyfikatów może sprawić, że wszystkie zainstalowane kopie aplikacji natychmiast przestaną komunikować się z twoją API. Należy go stosować tylko wtedy, gdy model zagrożeń na to pozwala i proces wydawania aplikacji umożliwia niezawodną rotację kluczy, a nigdy nie traktować go jako zamiennika autoryzacji powiązanej z urządzeniem.
Zapytanie biometryczne to jedynie lokalna odpowiedź „tak” lub „nie”
W systemie iOS aplikacja prosi LAContext o wyświetlenie okna autoryzacji systemowej; w Androidzie odpowiednikiem jest BiometricPrompt. System operacyjny przeprowadza skan i informuje o sukcesie lub niepowodzeniu. Poniższy fragment jest napisany w języku Swift: ocenia politykę opartą wyłącznie na danych biometrycznych wraz z informacją o przyczynie i rozpoczyna transfer, jeśli sprawdzenie przejdzie pomyślnie.
let context = LAContext()
let policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics
context.evaluatePolicy(policy, localizedReason: "Approve this transfer") { success, error in
guard success else { return }
startTransfer()
}
Twój kod nigdy nie ma dostępu do informacji o geometrii twarzy ani obrazów odcisków palców. Apple opisuje LocalAuthentication jako narzędzie pracujące z Secure Enclave i zwracające aplikacji jedynie wynik sprawdzenia – sukces lub niepowodzenie (referencja frameworku LocalAuthentication).
To jest w pełni wystarczające do zamykania prywatnych treści wewnątrz aplikacji. Jako dowód dla serwera jest to niewystarczające z trzech powodów:
- Wynik pokazuje jedynie „urządzenie zaakceptowało sprawdzenie”; nie zawiera żadnych informacji o tym, kto to zrobił ani który konto zostało użyte.
- Część serwerowa nie ma możliwości weryfikacji wartości logicznej wygenerowanej na stronie klienta.
- Jeśli proces aplikacji zostanie skompromitowany, atakujący może pominąć lub sfałszować funkcję powrotną i po prostu ponownie użyć już istniejącego tokena.
Główny wniosek z tego wzorca jest taki, że success to po prostu flaga w twoim własnym przestrzeni pamięci. Lepszy projekt wykorzystuje sprawdzenie biometryczne do odblokowania pojedynczej operacji kryptograficznej, której wynik serwer może samodzielnie zweryfikować.
Magazyny kluczy sprzętowych chronią klucz prywatny przed dostępem z procesu aplikacji
Urządzenia Apple zawierają Secure Enclave – odizolowany podsystem, który może generować i używać kluczy prywatnych, nie ujawniając nigdy surowych danych klucza głównemu procesorowi aplikacji. Zgodnie z dokumentacją Apple, klucz pozostaje wewnątrz Secure Enclave, a aplikacja otrzymuje jedynie wyniki takie jak podpisy (Przewodnik Apple dotyczący Secure Enclave).
Odpowiednikiem na innej platformie jest Android Keystore. W zależności od sprzętu klucze mogą być zabezpieczane za pomocą środowiska zaufanego wykonywania (TEE) lub dedykowanego bezpiecznego elementu StrongBox. Należy uważać przy nadawaniu nazw: „Titan M” to chip w telefonach Google Pixel, a nie ogólna API Android, więc nie należy zakładać, że każde urządzenie go posiada. Z punktu widzenia aplikacji istotne jest to, w jaki sposób klucz jest zabezpieczany oraz jakie reguły użytkowania do niego przypisano.
Dla operacji o wysokiej wartości klucz powinien spełniać cztery zasady:
- Jest generowany na tym konkretnym urządzeniu.
- Każde użycie wymaga nowego, silnego potwierdzenia biometrycznego.
- Część prywatna nigdy nie może zostać wyeksportowana.
- Tylko klucz publiczny jest przechowywany na serwerze.
W systemie Android BiometricPrompt.CryptoObject może umożliwić wykonywanie operacji kryptograficznych tylko wtedy, gdy klasa autoryzatora to BIOMETRIC_STRONG, a klucz może być skonfigurowany tak, aby wymagał nowej akceptacji przy każdym użyciu (dokumentacja BiometricPrompt, przegląd skarbca kluczy).
Protokół: rejestracja klucza raz, podpisywanie nowych wyzwań później
Cały schemat składa się z dwóch faz: rejestracji i użycia.
Rejestracja
- Aplikacja generuje parę kluczy wspieraną sprzętem.
- Wgrzewa klucz publiczny do serwera przez HTTPS.
- Serwer zapisuje klucz publiczny wraz z kontem i identyfikatorem urządzenia.
- Klucz prywatny pozostaje w telefonie, w bezpiecznym sprzęcie.
Wysyłanie klucza publicznego nie stanowi zagrożenia: może on weryfikować podpisy, ale nie może ich tworzyć. Tylko klucz prywatny może podpisywać, a nie opuszcza on zabezpieczonego sprzętu.
Aprobata wrażliwej prośby
- Aplikacja żąda od serwera jednorazowego, losowego wyzwania.
- Użytkownik wykonuje polecenie biometryczne.
- Zabezpieczony sprzęt podpisuje to wyzwanie.
- Aplikacja zwraca identyfikator wyzwania oraz podpis.
- Serwer weryfikuje podpis przy użyciu przechowywanego klucza publicznego.
- Serwer akceptuje lub odrzuca żądanie, a następnie może wydać token.
W tym miejscu większość implementacji popełnia błędy. Hasło musi być losowe, szybko wygasać i mogło zostać użyte dokładnie raz. Powinno być również powiązane z kontem, konkretnym kluczem urządzenia oraz akcją, która ma zostać zatwierdzona (na przykład „przelej 200 EUR na konto X”). Jeśli pominie się któryś z tych elementów, pozyskany podpis można będzie ponownie wykorzystać do innej prośby, co stanowi klasyczny atak replay. Powiązanie z akcją zapobiega również przekierowaniu podpisu zebranego przy nieszkodliwej operacji na taką, która jest niebezpieczna.
iOS: klucz Secure Enclave wymagający użycia Face ID przy każdym użyciu
W systemie iOS klucz tworzy się bezpośrednio w Secure Enclave i dołącza obiekt kontroli dostępu, który wymaga danych biometrycznych za każdym razem, gdy klucz jest używany. W poniższym kodzie w języku Swift zauważ można trzy opcje. kSecAttrAccessibleWhenUnlockedThisDeviceOnly zapobiega przenoszeniu klucza do kopii zapasowych lub innych urządzeń. Flagi .privateKeyUsage i .biometryCurrentSet wymagają zatwierdzenia biometrycznego przy podpisywaniu i łączą klucz z aktualnie zarejestrowanymi twarzami lub palcami. Natomiast kSecAttrTokenIDSecureEnclave umieszcza klucz w sprzęcie; Secure Enclave obsługuje jedynie klucze eliptyczne o długości 256 bitów (P-256), dlatego typ i rozmiar klucza są ustawione w taki sposób.
// 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 }
Praktyczną konsekwencją użycia .biometryCurrentSet jest to, że rejestracja nowego odcisku palca lub sformatowanie Face ID unieważnia klucz. Zazwyczaj jest to pożądane przy dokonywaniu płatności, ale oznacza to, że aplikacja musi w sposób uprzejmy radzić sobie z brakiem klucza i poprosić użytkownika o ponowną rejestrację.
Zapisz klucz publiczny po tym, jak użytkownik zaloguje się do konta. Później, gdy serwer przekaże losowy wyzwanie do przekazania środków, podpisz je kluczem prywatnym. Sam nie pokazuj żadnego komunikatu – dzięki flagom kontroli dostępu operacja podpisywania automatycznie uruchamia Face ID lub 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)
Jeśli użytkownik odwoła skanowanie lub ono się nie powiedzie, SecKeyCreateSignature zwróci wartość nil i nie będzie żadnego podpisu do wysłania, więc nie ma flagi sukcesu, którą atakujący mógłby zmienić.
Android: klucz w Keystore odblokowany za pomocą BiometricPrompt
Kotlin poniżej tworzy klucz EC przeznaczony wyłącznie do podpisywania w dostawcy AndroidKeyStore. Metoda setUserAuthenticationRequired(true) sprawia, że autoryzacja staje się obowiązkowa, a setUserAuthenticationParameters(0, AUTH_BIOMETRIC_STRONG) ustawia okno ważności na zero sekund, co oznacza, że przy każdym użyciu konieczna jest nowa, silna weryfikacja biometryczna zamiast okresu karencji po ostatnim odblokowaniu.
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
Po tym, jak użytkownik normalnie się zaloguje, należy wysłać zakodowany publicKey do serwera backendowego i przechować go razem z danymi użytkownika i urządzenia.
Gdy serwer później wyśle żądanie przekazania danych, należy zainicjować obiekt Signature, umieścić go wewnątrz CryptoObject i przekazać to do dalszych działań. Dopiero w metodzie onAuthenticationSucceeded należy podać dane z żądania i utworzyć podpis.
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)
}
Rozmowa nie stanowi granicy bezpieczeństwa; takim elementem jest CryptoObject. Skarbiec kluczy odrzuci operację podpisywania, chyba że sprawdzenie biometryczne do niego przypisane zakończyło się pomyślnie, a sam materiał klucza pozostaje w zabezpieczonym skarbcu. Twoja aplikacja przechowuje jedynie uzyskany podpis.
Na serwerze sprawdź ten podpis za pomocą zarejestrowanego klucza publicznego. Jeśli będzie odpowiadał niewygasłemu, niewykorzystanemu wyzwaniu, zatwierdź transfer lub wydaj token o krótkim czasie trwania, dostosowany do tej akcji.
Gdzie nadal powinny znajdować się tokeny autoryzacyjne
Weryfikacja podpisu opartego na sprzęcie nie eliminuje tokenów. Po tej weryfikacji wydanie zwykłego tokena dostępu jest całkowicie uzasadnione, ponieważ tokeny pozostają praktycznym sposobem na autoryzację większości ruchu w API. Jeśli projektujesz element systemu związany z tokenami, rozważania zawarte w naszym przewodniku strategii tokenów odświeżających mają tu również zastosowanie.
Zachowuj te tokeny w chronionym przez platformę magazynie: Keychain w iOS oraz Keystore w Android. Tokeny odświeżające o długim czasie życia nigdy nie powinny znajdować się w zwykłych ustawieniach, zwykłych plikach ani w magazynie dostępnym dla JavaScript, co jest szczególnie ważne w aplikacjach React Native, gdzie AsyncStorage stanowi łatwy, lecz niezaszyfrowany standard.
Następnie pojawia się pytanie projektowe dotyczące częstotliwości żądania nowego podpisu sprzętowego:
- Przy każdym wywołaniu API: prawie zawsze zbyt wolne i irytujące.
- Przy każdym uruchamianiu aplikacji: rozsądny wybór dla prywatnego zamykania aplikacji.
- Przy płatnościach, edycji profilu, eksportowaniu danych lub zatwierdzaniu nowego urządzenia: często optymalne rozwiązanie.
- Przy każdym użyciu tokena odświeżania: warto to zastosować w przypadku kont o wyższym ryzyku, pod warunkiem że doświadczenie użytkownika pozwala na radzenie sobie z powiadomieniami.
Traktuj podpis jako dodatkowy dowód, a token jako krótkotrwałe uprawnienie wynikające z niego.
Ograniczenia kluczy wspieranych przez sprzęt
To podejście jest skuteczne, ale nie jest rozwiązaniem na wszystko:
- Nie chroni przed prawowitym użytkownikiem, który odblokuje własny telefon i zatwierdzi szkodliwą czynność, na przykład pod presją socjotechniczną.
Z góry zaplanuj ścieżkę odzyskiwania i spraw, by była celowo wolniejsza oraz wymagała bardziej rygorystycznych weryfikacji niż zwykła ścieżka. Gdy klucz urządzenia znika, ekran blokady jest resetowany lub zmienia się proces rejestracji danych biometrycznych – twoja aplikacja powinna być gotowa do zarejestrowania nowego klucza, a twój serwer powinien anulować stary. Dokumentacja Androida wskazuje, że klucze związane z procesem autoryzacji mogą zostać trwale unieważnione po zmianie bezpiecznego ekranu blokady lub po zmianie procesu rejestracji danych biometrycznych, w zależności od sposobu konfiguracji klucza (dokumentacja KeyProtection.Builder).
Jak passkeys wpisują się w tę architekturę
Klucze przejściowe opierają się na tej samej zasadzie: parze kluczy publicznego/prywatnego oraz wyzwaniu serwera podpisanym kluczem prywatnym. Do logowania na strony internetowe i konta są zazwyczaj najlepszym rozwiązaniem, ponieważ system operacyjny i platforma zajmują się większością trudnych aspektów. Klucze przejściowe to standaryzowany, międzyplatformowy protokół oparty na tym stylu procesu podpisywania wspieranego sprzętem. Jeden szczegół: wiele kluczy przejściowych jest synchronizowanych pomiędzy urządzeniami użytkownika za pośrednictwem menedżera haseł platformy, więc nie zawsze są powiązane z jednym telefonem, w przeciwieństwie do kluczy nieeksportowalnych z Keystore lub Secure Enclave.
To rozróżnienie wskazuje na prosty podział. Użyj klucza aplikacyjnego wspieranego sprzętowo, gdy aplikacja natywna potrzebuje dowodu powiązanego z urządzeniem do wykonania określonej czynności. Wybierz klucze passkey do nowoczesnego logowania bez hasła tam, gdzie twój produkt je obsługuje. W obu przypadkach zadanie serwera jest takie samo: weryfikacja podpisu za pomocą klucza publicznego, któremu już ufa.
Wybór odpowiedniego narzędzia do każdej potrzeby
- Ochrona danych w transporcie: HTTPS.
- Podtrzymanie zalogowania użytkownika: krótkotrwały token dostępu w połączeniu z chronionym tokenem odnowienia.
- Zamknięcie dostępu do treści prywatnych podczas otwierania aplikacji: Face ID, Touch ID lub BiometricPrompt.
- Autorizacja płatności lub innej czynności o wysokim ryzyku: podpis sprzętowy zabezpieczony metodami biometrycznymi.
- Logowanie bez hasła: klucze passkey.
- Zmniejszenie ryzyka przechwytywania danych TLS: certificate pinning.
Główne wnioski
Skanowanie twarzy i odcisków palców nie autoryzuje żądania wysyłanego do serwera. Służą one jedynie do uzyskania dostępu do klucza; bezpieczne urządzenia używają tego klucza do podpisywania nowych wyzwań od serwera, a serwer weryfikuje tę podpis przed decydowaniem o tym, co użytkownik może robić.
- Biometria: lokalna akceptacja, niewidoczna dla serwera.
- Klucz sprzętowy: dowód posiadania, którego nie można skopiować z urządzenia.
- Token: krótkoterminowe upoważnienie udzielane przez serwer.
Należy zachować odrębność tych elementów, powiązać każde wyzwanie z konkretnym kontem, kluczem i akcją, a także zaplanować procedury unieważniania i odzyskiwania kluczy. Dzięki temu projekt autoryzacji w aplikacjach mobilnych jest łatwiejszy do zrozumienia i znacznie trudniejszy do sfałszowania.