Prove it's you
Sharing a credential proves the phone holds it. For a credential like the Emirates ID you may also want proof that the person holding the phone is the person on it. The face check does that in about four seconds, on the phone, without sending any image anywhere.
sequenceDiagram
autonumber
actor H as Holder
participant W as Wallet (your app)
participant V as Verifier
V->>W: Request (in person, website or same device)
W->>H: Names the verifier, lists the fields
H->>W: Share
W->>H: Camera: "Put your face in the oval", then "Blink"
H->>W: Blinks
W->>W: Matches the live face with the credential's photo
W->>V: Signed answer (only after a match)
What the holder sees
Section titled “What the holder sees”- The consent sheet, as always. They tap Share.
- The front camera opens over the same screen, dimmed except for an oval. One instruction at a time guides them in: Put your face in the oval, Move closer, Centre your face. The oval turns green when the face is placed, and a ring around it fills as each step passes.
- One short challenge: Blink or Smile.
- Hold still, then Checking…: the face is compared with the photo on the credential.
- On a match a tick appears and sharing continues on its own. Otherwise: Try again with a short tip, and the camera comes back by itself. After the last try: Not shared.
Order on every path, in person or online: consent → face check → sign → send. If the check does not pass, nothing is signed; the verifier sees a cancelled exchange.
Turn it on
Section titled “Turn it on”The face check is optional. A wallet without it shares credentials on the holder’s consent alone, needs no camera permission for sharing, and doesn’t ship the model file. To add it:
-
Add the library. In
app/build.gradle.kts, next to the other Wallet SDK lines, addimplementation("com.wallet.sdk:sdk-face:<version>")and click Sync Now. It declares the camera permission itself. -
Add the model. Copy
models/facenet_512.tflitefrom the SDK zip intoapp/src/main/assets/(create the folder with New → Folder → Assets Folder if you don’t have one). -
Turn the check on where you create the SDK: one
shareGuardline.ProveItsYou.kt package samplesimport android.content.Contextimport com.wallet.Walletimport com.wallet.WalletSdkimport com.wallet.config.IssuerConfigimport com.wallet.config.TrustedVerifiersimport com.wallet.face.FaceCheckPolicyimport com.wallet.face.FaceModelimport com.wallet.face.FaceShareGuardimport com.wallet.model.SdkResult/** Before the Emirates ID is shared, the holder proves it's them: liveness + face match against the card's photo. */object ProveItsYou {fun create(context: Context, signedReaderList: ByteArray): WalletSdk {// The face model from the SDK zip (models/facenet_512.tflite), copied into app/src/main/assets.val model = FaceModel.fromBytes(context.assets.open(FaceModel.FILE_NAME).use { it.readBytes() })return Wallet.createWallet(issuer = IssuerConfig("https://issuer.example.com", "my-wallet", "mywallet://callback"),trustedVerifiers = TrustedVerifiers(signedList = signedReaderList),credentialTypes = listOf(EmiratesIdType.type),// The only line that turns the check on. Other credential types are shared as before.shareGuard = FaceShareGuard(FaceCheckPolicy(docTypes = setOf(EmiratesIdType.type.docType)), model),)}/** Optional. Right after the credential arrives: can its photo be used? Warn the holder early if not. */suspend fun checkPhoto(sdk: WalletSdk, credentialId: String): String? =when (val r = sdk.wallet.prepareForSharing(credentialId)) {is SdkResult.Success -> nullis SdkResult.Failure -> r.error.message // e.g. "The photo on this credential can't be used for a face check"}} -
Run it on a phone (the check needs a front camera) and share the Emirates ID: the camera opens over the consent sheet. Nothing else to host; the check attaches itself to whatever screen is presenting.
-
Use the face variant of the framework. It is the same
Walletframework plus the face check, and needs Google ML Kit and TensorFlow Lite, which ship only as CocoaPods. Remove the plainWallet.xcframeworkfrom the target, then add aPodfilenext to your.xcodeproj:platform :ios, '16.0'target 'YourApp' douse_frameworks!pod 'WalletFace', :path => 'vendor/wallet-sdk/ios/face'endRun
pod installin Terminal and from now on open the.xcworkspace, not the.xcodeproj. -
Add the model. Drag
models/facenet_512.tflitefrom the SDK zip into the project navigator; tick Copy items if needed and your app target. -
Explain the camera. In the target’s Info tab, set
NSCameraUsageDescription, e.g. “Confirm it’s you before sharing your ID.” -
Turn the check on where you create the SDK:
let path = Bundle.main.path(forResource: "facenet_512", ofType: "tflite")!let model = FaceModel.companion.fromFile(path: path)let sdk = Wallet.shared.createWallet(issuer: IssuerConfig(baseUrl: "https://issuer.example.com", clientId: "my-wallet",redirectUri: "mywallet://callback", dpopPolicy: .allowBearerFallback),trustedVerifiers: TrustedVerifiers(signedList: signedReaderList),credentialTypes: [EmiratesIdType.type],// The only line that turns the check on. Other credential types are shared as before.shareGuard: FaceShareGuard(policy: FaceCheckPolicy(docTypes: ["org.iso.23220.photoid.1"], liveness: .quick,similarityThreshold: 0.7, maxAttempts: 3),model: model)) -
Run it on an iPhone (the check needs a front camera; there is no simulator build). The ready-made wallet screen (
WalletViewControllersKt.WalletViewController(sdk:)) shows the check by itself. If you build your own SwiftUI screens instead, presentFaceCheckHostViewController()once over your root view.
How the match works
Section titled “How the match works”The check uses FaceNet, the face-recognition method published by Google researchers:
Florian Schroff, Dmitry Kalenichenko, James Philbin. FaceNet: A Unified Embedding for Face Recognition and Clustering. IEEE Conference on Computer Vision and Pattern Recognition (CVPR), 2015. arXiv:1503.03832
FaceNet turns a face into a short list of numbers, an embedding, trained so that two photos of the same person land close together and photos of different people land far apart. In the paper it reached 99.63% accuracy on the standard Labeled Faces in the Wild benchmark.
On the phone, in three steps:
- Find the face. A face detector locates the face and the eyes, in the camera image and in the credential’s photo.
- Turn each face into numbers. Each face is levelled, cropped, scaled to 160 × 160 pixels and run through the model, which returns 512 numbers.
- Compare. The two lists are compared by cosine similarity, a score from 0 to 1. After the liveness step, up to four frames with a neutral expression are compared, a moment apart, and the best score counts; one blurred or blinking frame doesn’t fail the check. At or above the threshold (0.7 by default), it’s a match.
The model is a FaceNet-style network (Inception-ResNet-v1) in TensorFlow Lite format. It is an openly licensed reimplementation of the paper, not Google’s original model, so the paper’s figure describes the method rather than this file. Measure it on your own users’ photos before you choose a threshold.
The model file
Section titled “The model file”The model is facenet_512.tflite, about 23 MB. It takes a 160 × 160 colour image and returns 512 numbers.
It is in the SDK zip under models/, not inside any library, so your app decides how to ship it: bundled
as above, or downloaded on first run. Load it once with FaceModel.fromBytes or FaceModel.fromFile.
Everything runs on the phone’s own processor through TensorFlow Lite. No server or internet connection is
involved, so the check works offline.
Settings
Section titled “Settings”FaceCheckPolicy |
Meaning | Default |
|---|---|---|
docTypes |
which credential types are protected; every other type is shared as before | required |
liveness |
NONE (match only, about 1 s; a printed photo would pass), QUICK (blink or smile), STRICT (both), FULL (the complete selfie ritual, 20 to 40 s) |
QUICK |
similarityThreshold |
the match score a live face must reach, 0 to 1 | 0.7 |
maxAttempts |
retries before the share is refused | 3 |
Lower the threshold a little if genuine holders with a changed look (a new beard, glasses) are refused; raise it where a false match costs more than a retry.
Check early (optional)
Section titled “Check early (optional)”Call sdk.wallet.prepareForSharing(credentialId) right after a protected credential arrives. It fails
with FACE_CHECK_UNAVAILABLE when the credential’s photo has no usable face, so you can warn the holder
before they are standing at a counter.
If you skip it, nothing else changes: a credential whose photo can’t be used is refused when the holder tries to share it, with the message “The photo on this credential can’t be used for a face check”.
Turn it off
Section titled “Turn it off”Remove the shareGuard argument (and the line that loads the model). Every credential is then shared on
consent alone. You can also remove the face library (sdk-face on Android, the face variant of the
framework on iOS) and the model file, which makes the app about 23 MB smaller.
What is stored
Section titled “What is stored”Nothing. The credential’s photo is turned into numbers in memory for the comparison and discarded with the process; camera frames are never written or sent. The verifier receives the same fields it would have without the check.
Android and iOS
Section titled “Android and iOS”| Android | iOS | |
|---|---|---|
| Library | com.wallet.sdk:sdk-face |
the face variant of Wallet.xcframework (ios/face/), via CocoaPods |
| Extra requirements | CAMERA permission (declared by the library) |
NSCameraUsageDescription, iOS 16, a Podfile (Google ML Kit and TensorFlow Lite have no Swift Package) |
| Hosting | automatic | automatic in the ready-made screens; FaceCheckHostViewController() over your own |
| iOS Digital Credentials sheet | n/a | not offered for protected types: an extension has no camera |
Next: Go live.