eID Android SDK — integration guide¶
A developer guide for adding eID (Gerege Smart-ID) enrollment (enroll), authentication, and
electronic signature (signature) to an Android application. The SDK talks to the Go backend
(server/) and performs distributed keygen + 2-of-2 threshold ECDSA on the phone side. Its
crypto core is a standalone Kotlin implementation, byte-compatible with the Go server's
internal/crypto via golden vectors.
Package:
mn.eidmongolia.smartid· Client class:GeregeSmartIdClient· Module::android-sdk(Android library,.aar).
1. Overview¶
The SDK exposes the following high-level operations through a single GeregeSmartIdClient:
| Action | Function | Description |
|---|---|---|
| Enrollment | enroll(...) |
KYC consent + distributed keygen → 2 certificates (signing PIN2, authentication PIN1) |
| Approve session | approve(...) |
Sign and confirm a session arriving via push/QR with threshold ECDSA |
| Verify PIN | verifyPIN(...) / pinUnlocksSlot(...) |
Unlock and verify the stored identity with the PIN (does not contact the server) |
| Change PIN | changePIN(...) |
Unlock with the old PIN and re-encrypt with the new PIN |
| KYC | danInit/danStatus/danLiveness, gsignInit/gsignVerify, passportInit |
DAN / G-Sign / foreign-passport verification |
| Dashboard | pendingSign/pendingSession/recentActivity/sessionInfo |
Pending requests + recent activity |
2 certificates per citizen. Enroll runs keygen TWICE: Authentication (PIN1, login) and Signing (PIN2, legally-binding signature). The private key never exists fully in one place — one half on the phone, one half on the server.
2. Prerequisites¶
- minSdk 24, targetSdk 35, JDK 17, Kotlin 1.9.24 (SDK build:
compileSdk 34). - Play Integrity attestation — a fresh token is obtained on the server's nonce for each request.
The app's
applicationIdand Google Cloud project (cloudProjectNumber) must be registered on the server. - Backend URL — default
https://rp-api.eidmongolia.mn(configurable, see below).
(!) Play Integrity does not work on the emulator. Run the Go server with
SMARTID_REQUIRE_ATTESTATION=false(the dev default) — the app continues without attestation. On an emulator / environment without Play Services, the SDK catchesAttestationUnavailableExceptiongracefully and continues without attestation; on a real device, an attestation error (tamper/network) is fail-closed (the request is cancelled).
3. Installing¶
The SDK is included as the :android-sdk Gradle module (not published as a Maven coordinate — as an
adjacent module inside the repo, or as an .aar). The example app includes it as an adjacent module in
settings.gradle.kts:
// eIDMongolia/settings.gradle.kts
include(":app")
include(":android-sdk")
project(":android-sdk").projectDir = file("../android-sdk") // android/android-sdk
The SDK itself depends on the following (pulled in transitively): com.google.android.play:integrity,
androidx.security:security-crypto (EncryptedSharedPreferences), com.squareup.okhttp3:okhttp,
kotlinx-coroutines-android + -play-services.
Biometrics (required on the app side). To authorize a Keystore auth-required key, the app needs
BiometricPrompt (FragmentActivity), so add the biometric dependencies to your app:
// app/build.gradle.kts
implementation("androidx.fragment:fragment-ktx:1.8.2")
implementation("androidx.biometric:biometric:1.1.0") // BiometricPrompt requires FragmentActivity
The app's
MainActivitymust be aFragmentActivity(orAppCompatActivity) — BiometricPrompt requires this.
4. Configuration — backend URL¶
The app bakes the backend base URL into BuildConfig.GEREGE_BACKEND_URL at build time. The value is
taken from the gerege.backendUrl gradle property, falling back to the default if blank:
// app/build.gradle.kts (defaultConfig)
val backendUrl = (project.findProperty("gerege.backendUrl") as String?)
?.takeIf { it.isNotBlank() } ?: "https://rp-api.eidmongolia.mn"
buildConfigField("String", "GEREGE_BACKEND_URL", "\"$backendUrl\"")
Two ways to configure it:
# 1) Command line property:
./gradlew :app:assembleDebug -Pgerege.backendUrl=https://<go-backend>
# 2) or in eIDMongolia/gradle.properties:
# gerege.backendUrl=https://smartid-staging.example.mn
Read it in the application via AppConfig:
object AppConfig {
val backendUrl: String get() = BuildConfig.GEREGE_BACKEND_URL
const val cloudProjectNumber: Long = 399766783708L // Google Cloud project (Play Integrity)
}
To connect to a local Go server (http), you need a cleartext permission in
network_security_config(LAN testing). On a public host the SDK pins TLS to the Let's Encrypt root; on localhost/.local/ private-IP it skips pinning automatically.
5. Basic usage¶
5.1 Creating a client¶
val client = GeregeSmartIdClient(
context = this, // Context (Activity/Application)
baseUrl = AppConfig.backendUrl, // BuildConfig.GEREGE_BACKEND_URL
cloudProjectNumber = AppConfig.cloudProjectNumber, // Play Integrity
account = "default", // (optional) keystore slot name
)
5.2 Biometric authenticator (required)¶
Load operations such as approve / verifyPIN / changePIN require a BiometricAuthenticator to
unlock the Keystore's user-authentication-required key. The SDK is independent of any UI framework — the
app bridges via BiometricPrompt. In Compose there is a helper:
BiometricAuthenticator has the following contract (SDK interface):
interface BiometricAuthenticator {
suspend fun authenticate(cipher: Cipher, reason: String): Cipher
}
User cancel / biometric failure → throw (fail-closed): the unwrap/signing stops, there is no silent bypass (parity with iOS Secure Enclave
.userPresence).
5.3 Enrollment (enroll)¶
enroll runs distributed keygen with a KYC consent (danAuthCode) + PIN, obtains a certificate, and
stores it on the phone. If authPin is provided, two separate threshold keys are created for a
2-cert setup (signing + authentication):
suspend fun enroll(
danAuthCode: String,
pin: CharArray, // PIN2 — signing (non-repudiation)
pushToken: String, // FCM token
authPin: CharArray? = null, // PIN1 — authentication (if provided, 2-cert)
reviewLatin: (suspend (LatinNameProposal) -> Pair<String, String>?)? = null,
progress: ProgressHandler? = null,
): String // → documentNumber
val documentNumber = client.enroll(
danAuthCode = danCode,
pin = "1234".toCharArray(), // PIN2 (signing)
pushToken = fcmToken,
authPin = "5678".toCharArray(), // PIN1 (authentication)
)
Internally, enroll performs the server's keygen proof (rogue-key protection) and key-binding (certificate ↔ joint Q = qClient+qServer) checks — if the server returns a cert for a different key, it refuses before storing.
Prewarm (optional). Call
client.prewarmEnroll()when the enrollment screen opens to run the heavy Paillier keygen in the background ahead of time, reducing the enroll wait.
5.4 Authentication and signature (approve)¶
Approve the sessionId arriving via push/QR with a PIN. If authentication = true, the auth key
(PIN1, login) is used; otherwise the sign key (PIN2, signature) is used:
suspend fun approve(
sessionId: String,
pin: CharArray,
auth: BiometricAuthenticator,
authentication: Boolean = false, // true → PIN1/auth; false → PIN2/sign
confirmVc: String? = null, // user-confirmed verification code (WYSIWYS)
progress: ProgressHandler? = null,
): String // → status ("OK" etc.)
// Authentication (login):
val status = client.approve(sessionId, "5678".toCharArray(), auth, authentication = true)
// Signature (sign):
val status = client.approve(sessionId, "1234".toCharArray(), auth)
Inside approve, the phone performs the 3 rounds of threshold ECDSA (commit → prove → finish) and,
before sending the final signature to the server, independently ECDSA-verifies that it matches the
session's message (SIGN: digest; AUTH: ACSP_V2(rpChallenge, SPKI)) (WYSIWYS / transcript binding). If
it does not match, no signature is produced.
5.5 Verify / change PIN¶
suspend fun verifyPIN(slot: PINSlot, pin: CharArray, auth: BiometricAuthenticator): Boolean
suspend fun changePIN(slot: PINSlot, oldPin: CharArray, newPin: CharArray, auth: BiometricAuthenticator)
// slot: GeregeSmartIdClient.PINSlot.AUTH (PIN1) | .SIGN (PIN2)
Check local state (serverless): hasRegistration(): Boolean, deleteRegistration().
5.6 KYC (brief)¶
Before obtaining the danAuthCode needed for enroll, perform KYC via the SDK:
val (state, verifyUrl) = client.danInit(registrationNumber) // DAN verify URL
val verified = client.danStatus(state) // polling
val (passed, score) = client.danLiveness(state, selfieBytes) // face-match
There are also gsignInit/gsignVerify (G-Sign) and passportInit (foreign passport) variants — see
the source code.
6. Key storage and biometrics¶
The SDK encrypts the identity (xClient + Paillier private key + cert) with PBKDF2(PIN)+AES-GCM,
stores it in EncryptedSharedPreferences, and additionally wraps it with an Android Keystore
user-authentication-required (biometric) key (to block offline PIN brute-force, iOS Secure Enclave
parity). The PIN and x_client never leave the device to the server.
The deep technical details — key storage, RSA-OAEP wrap/unwrap, v1→v2 migration,
setInvalidatedByBiometricEnrollment, StrongBox fallback — are described in a separate document: Android biometric-gated keys.
7. Building / running the example app¶
Prerequisites: an Android SDK with ANDROID_HOME configured; for FCM push, a real Firebase project
app/google-services.json (the example file is app/google-services.json.example).
cd android/eIDMongolia
# For FCM push (optional): replace with a real Firebase file
cp app/google-services.json.example app/google-services.json
# Debug APK — pass the backend URL via property
./gradlew :app:assembleDebug -Pgerege.backendUrl=https://<go-backend>
# → app/build/outputs/apk/debug/app-debug.apk
Or open and run eIDMongolia in Android Studio. To build the SDK library separately:
cd android/android-sdk && gradle assembleRelease →
build/outputs/aar/...-release.aar.
The build does not break when
google-services.jsonis absent — the app compiles and runs (FCM push is disabled; Play Integrity + enroll/approve work without google-services.json).
8. Go backend endpoints used by the app¶
All map to the Go server's internal/httpapi (same as iOS):
GET /v3/attestation/challenge— attestation noncePOST /v3/enrollment/init·/v3/enrollment/completeGET /v3/mobile/session/{id}·/v3/mobile/pending/{doc}·/v3/mobile/activity/{doc}POST /v3/mobile/session/{id}/sign/{commit,prove,finish}— 2-of-2 threshold ECDSA (3 rounds)- KYC:
/v3/kyc/dan/*,/v3/kyc/gsign/*
The server returns an X-Next-Attestation-Nonce header in its response (SDK nonce-cache optimization).
9. Links (source)¶
- Android SDK client:
https://github.com/gerege-systems/eid-platform-mn/blob/main/android/android-sdk/src/main/kotlin/mn/eidmongolia/smartid/GeregeSmartIdClient.kt - Keystore storage:
https://github.com/gerege-systems/eid-platform-mn/blob/main/android/android-sdk/src/main/kotlin/mn/eidmongolia/smartid/AndroidSecureKeyStore.kt - Biometric authenticator interface:
https://github.com/gerege-systems/eid-platform-mn/blob/main/android/android-sdk/src/main/kotlin/mn/eidmongolia/smartid/BiometricAuthenticator.kt - Example app entry point:
https://github.com/gerege-systems/eid-platform-mn/blob/main/android/eIDMongolia/app/src/main/kotlin/mn/eidmongolia/example/MainActivity.kt - Android README (build/run):
https://github.com/gerege-systems/eid-platform-mn/blob/main/android/README.md - SDK README (Kotlin architecture):
https://github.com/gerege-systems/eid-platform-mn/blob/main/android/android-sdk/README.md - Biometric keys (deep technical): ANDROID_BIOMETRIC_KEYS.md