How to Securely Add Fingerprint & Face ID Login to Your iOS App with ECDSA
This article walks through the design, security considerations, and step‑by‑step implementation of fingerprint and face‑recognition login for an iOS driver app, covering biometric enable/disable, server‑side verification, auth‑code replay protection, ECDSA signing, keychain management, and common pitfalls.
Background
In the Huolala driver app a driver may need to switch accounts across multiple devices; the token may expire and the SMS verification flow can be blocked by carriers. To reduce SMS cost and handle extreme cases, fingerprint/face authentication was introduced.
Exploration
Industry status
Most mainstream apps still rely on system‑provided biometric APIs; only a few, like Alipay, implement custom face recognition. The low adoption suggests many companies are still evaluating security.
Apple’s official security white‑paper confirms the safety of the Secure Enclave‑based biometric data.
Overall solution for Huolala
Local biometric authentication + one‑time auth code + ECDSA local signing & server verification.
Enabling / disabling biometric login
Biometric capability must be bound to the account while the app is already logged in. Users can later disable it, similar to binding/unbinding a third‑party login.
Using biometric login
After enabling, the user can log in quickly via biometric authentication.
Why server‑side verification?
Pure local authentication cannot guarantee data integrity; an attacker could hijack or tamper requests. Server verification ensures the data received is authentic.
Why an auth code?
An auth code (random nonce) prevents replay attacks by making each request unique and optionally time‑limited.
Why signing instead of encryption? Why ECDSA?
Signing validates integrity and source authenticity, while encryption only provides confidentiality. ECDSA (ECC‑based) offers better performance and smaller keys, and keys can be generated and stored in the Secure Enclave, which cannot be exported.
Implementation
Environment
Target iOS version >= 11.3. Add NSFaceIDUsageDescription to Info.plist for the first‑time face‑ID prompt.
Core library LocalAuthentication
Use LAContext to check capability and evaluate policy.
1. Check device capability
BOOL can = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error];Read context.biometryType after calling canEvaluatePolicy:error:.
2. Perform biometric authentication
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:@"Login with biometrics" reply:^(BOOL success, NSError *error) { /* handle result */ }];Handle success, error codes (e.g., LAErrorUserFallback, LAErrorBiometryLockout).
ECDSA key management
Creating / deleting key pair
CFErrorRef err = NULL;
SecAccessControlRef ac = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAccessControlPrivateKeyUsage, &err);
NSDictionary *params = @{ (id)kSecAttrTokenID: (id)kSecAttrTokenIDSecureEnclave,
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
(id)kSecAttrKeySizeInBits: @(256),
(id)kSecAttrLabel: @"MyKeyLabel",
(id)kSecAttrApplicationTag: @"MyKeyTag",
(id)kSecPrivateKeyAttrs: @{ (id)kSecAttrAccessControl: CFBridgingRelease(ac), (id)kSecAttrIsPermanent: @(YES) } };
SecKeyRef privateKey = SecKeyCreateRandomKey((CFDictionaryRef)params, (void *)&err);
if (privateKey) { /* success */ }Exporting public key
SecKeyRef pubKey = SecKeyCopyPublicKey(privateKey);
NSDictionary *pubParams = @{ (id)kSecClass: (id)kSecClassKey,
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
(id)kSecAttrKeySizeInBits: @(256),
(id)kSecAttrLabel: @"MyKeyLabel",
(id)kSecAttrApplicationTag: @"MyKeyTag",
(id)kSecValueRef: (__bridge id)pubKey,
(id)kSecAttrKeyClass: (id)kSecAttrKeyClassPublic,
(id)kSecReturnData: @(YES) };
CFTypeRef dataRef = NULL;
OSStatus status = SecItemAdd((CFDictionaryRef)pubParams, &dataRef);
NSData *publicData = (status == errSecSuccess) ? (__bridge_transfer NSData *)dataRef : nil;
// prepend Secp256r1 header if needed and Base64‑encodeSigning data
NSData *toSign = [stringToSign dataUsingEncoding:NSUTF8StringEncoding];
CFDataRef signed = SecKeyCreateSignature(privateKey, kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (CFDataRef)toSign, &err);
NSString *signatureBase64 = [(NSData *)CFBridgingRelease(signed) base64EncodedStringWithOptions:0];Verifying signature locally
SecKeyRef pubKey = SecKeyCreateWithData((CFDataRef)keyData, (__bridge CFDictionaryRef)attributes, &err);
Boolean ok = SecKeyVerifySignature(pubKey, kSecKeyAlgorithmECDSASignatureMessageX962SHA256, (CFDataRef)originData, (CFDataRef)signedData, &err);Remote account‑device‑biometric mapping
Maintain a server‑side table linking accounts, devices, biometric types, and stored public keys to support multiple bindings.
Data caching
Cache the random auth code, evaluatedPolicyDomainState, and binding status during the enable flow; clear caches on failure or when disabling.
Pitfalls & fixes
Incorrect caching of latestLoginUserInfo after an app upgrade caused account‑id=0 bindings, leading to “device key limit reached” or “device key verification failed” errors. The fix adds explicit cache refresh before binding and graceful handling when no binding is found (clear cache and report success for unbinding).
Conclusion
The article outlines the end‑to‑end process of integrating fingerprint/face ID login on iOS, including biometric enable/disable, server verification, auth‑code replay protection, ECDSA signing, keychain handling, data caching, and lessons learned from real‑world issues.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
