Skip to content

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 applicationId and 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 catches AttestationUnavailableException gracefully 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
// app/build.gradle.kts
dependencies {
    implementation(project(":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 MainActivity must be a FragmentActivity (or AppCompatActivity) — 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:

val auth = rememberBiometricAuthenticator()   // eIDMongolia's etalon/core/BiometricAuth.kt

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 assembleReleasebuild/outputs/aar/...-release.aar.

The build does not break when google-services.json is 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 nonce
  • POST /v3/enrollment/init · /v3/enrollment/complete
  • GET /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).

  • 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