A Kotlin/Android library for generating and verifying zero-knowledge proofs for two RS circuits (cert_chain_rs4096 and device_sig_rs2048) using native Rust code via UniFFI and JNI.
To get this library from GitHub using JitPack:
Step 1. Add the JitPack repository to your settings.gradle.kts at the end of repositories:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}Step 2. Add the dependency to your build.gradle.kts:
dependencies {
implementation("com.github.zkmopro:OpenACKotlin:0.2.0")
}Checkout the JitPack page for more available versions.
Note: If you're using an Android template from mopro create, comment out these UniFFI dependencies in your build file to prevent duplicate class errors.
// // Uniffi
// implementation("net.java.dev.jna:jna:5.13.0@aar")
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")All functions are in the uniffi.mopro package. The library supports two circuits:
cert_chain_rs4096— Certificate chain verificationdevice_sig_rs2048— Device signature verification
import uniffi.mopro.generateCertChainRs4096Input
import uniffi.mopro.setupKeys
import uniffi.mopro.proveCertChainRs4096
import uniffi.mopro.proveDeviceSigRs2048
import uniffi.mopro.verifyCertChainRs4096
import uniffi.mopro.verifyDeviceSigRs2048
import uniffi.mopro.linkVerify
import uniffi.mopro.runCompleteBenchmark
import uniffi.mopro.BenchmarkResults
import uniffi.mopro.ProofResult
import uniffi.mopro.ZkProofExceptionGenerates JSON input files for both circuits from raw certificate data.
val status: String = generateCertChainRs4096Input(
certb64 = "<base64-encoded certificate>",
signedResponse = "<signed response string>",
tbs = "<to-be-signed data>",
issuerCertPath = "/path/to/issuer.crt",
smtServer = null, // optional SMT server URL
issuerId = "<issuer-id>",
outputDir = context.filesDir.absolutePath
)Writes two files to outputDir:
cert_chain_rs4096_input.jsondevice_sig_rs2048_input.json
Returns a status string on success. Throws ZkProofException on failure.
Instead of generating keys locally with setupKeys, you can download pre-built keys from the zkID releases and place them in documentsPath/keys/.
Download and extract the following files:
| File | Download URL |
|---|---|
cert_chain_rs4096_proving.key |
cert_chain_rs4096_proving.key.gz |
cert_chain_rs4096_verifying.key |
cert_chain_rs4096_verifying.key.gz |
device_sig_rs2048_proving.key |
device_sig_rs2048_proving.key.gz |
device_sig_rs2048_verifying.key |
device_sig_rs2048_verifying.key.gz |
After extracting, place all .key files into documentsPath/keys/. Example using OkHttp and GZIPInputStream:
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.util.zip.GZIPInputStream
suspend fun downloadKeys(documentsPath: String) {
val keysDir = File(documentsPath, "keys").also { it.mkdirs() }
val client = OkHttpClient()
val baseUrl = "https://github.com/zkmopro/zkID/releases/download/latest"
val files = listOf(
"cert_chain_rs4096_proving.key",
"cert_chain_rs4096_verifying.key",
"device_sig_rs2048_proving.key",
"device_sig_rs2048_verifying.key"
)
for (name in files) {
val dest = File(keysDir, name)
if (dest.exists()) continue
val response = client.newCall(Request.Builder().url("$baseUrl/$name.gz").build()).execute()
GZIPInputStream(response.body!!.byteStream()).use { input ->
dest.outputStream().use { output -> input.copyTo(output) }
}
}
}Alternatively, generate keys locally from R1CS files. Requires cert_chain_rs4096.r1cs and device_sig_rs2048.r1cs to be present in documentsPath. This is slow and only needed if you cannot use the pre-built keys above.
val documentsPath: String = context.filesDir.absolutePath
val result: String = setupKeys(documentsPath)Returns a status string on success. Throws ZkProofException on failure.
Generates a ZK proof for the cert_chain_rs4096 circuit. Reads cert_chain_rs4096_input.json from documentsPath.
val result: ProofResult = proveCertChainRs4096(documentsPath)
println("Prove time: ${result.proveMs} ms")
println("Proof size: ${result.proofSizeBytes} bytes")Generates ZK proofs for both the cert_chain_rs4096 and device_sig_rs2048 circuits. Reads both input JSON files from documentsPath.
val result: ProofResult = proveDeviceSigRs2048(documentsPath)
println("Prove time: ${result.proveMs} ms")
println("Proof size: ${result.proofSizeBytes} bytes")ProofResult fields:
| Field | Type | Description |
|---|---|---|
proveMs |
ULong |
Time to generate the proof in milliseconds |
proofSizeBytes |
ULong |
Size of the proof in bytes |
Both prove functions throw ZkProofException.SetupRequired if keys have not been set up yet.
Verifies the proof for the cert_chain_rs4096 circuit stored in documentsPath.
val isValid: Boolean = verifyCertChainRs4096(documentsPath)Verifies the proof for the device_sig_rs2048 circuit stored in documentsPath.
val isValid: Boolean = verifyDeviceSigRs2048(documentsPath)Verifies proofs for both circuits together.
val isValid: Boolean = linkVerify(documentsPath)All verify functions return true if the proof is valid, false otherwise. Throws ZkProofException on error.
Runs the full pipeline (setup → prove → verify) for both circuits and returns timing and size metrics.
val results: BenchmarkResults = runCompleteBenchmark(documentsPath)
println("Setup: ${results.setupMs} ms")
println("Prove: ${results.proveMs} ms")
println("Verify: ${results.verifyMs} ms")
println("Proving key: ${results.provingKeyBytes} bytes")
println("Verifying key: ${results.verifyingKeyBytes} bytes")
println("Proof: ${results.proofBytes} bytes")
println("Witness: ${results.witnessBytes} bytes")BenchmarkResults fields:
| Field | Type | Description |
|---|---|---|
setupMs |
ULong |
Time for key setup in milliseconds |
proveMs |
ULong |
Time for proof generation in milliseconds |
verifyMs |
ULong |
Time for verification in milliseconds |
provingKeyBytes |
ULong |
Size of the proving key in bytes |
verifyingKeyBytes |
ULong |
Size of the verifying key in bytes |
proofBytes |
ULong |
Size of the proof in bytes |
witnessBytes |
ULong |
Size of the witness in bytes |
All functions (except the verify functions) throw ZkProofException on failure. The sealed class variants are:
| Variant | Description |
|---|---|
ZkProofException.FileNotFound |
A required file could not be found |
ZkProofException.ProofGenerationFailed |
Proof generation encountered an error |
ZkProofException.VerificationFailed |
Proof verification encountered an error |
ZkProofException.InvalidInput |
The provided input is invalid |
ZkProofException.SetupRequired |
Keys must be set up before proving |
ZkProofException.IoException |
An I/O error occurred |
try {
val proof = proveCertChainRs4096(documentsPath)
} catch (e: ZkProofException.SetupRequired) {
// Run setupKeys first
} catch (e: ZkProofException) {
println("ZK error: ${e.message}")
}import uniffi.mopro.*
val documentsPath = context.filesDir.absolutePath
// 1. Generate circuit inputs from certificate data
generateCertChainRs4096Input(
certb64 = certBase64,
signedResponse = signedResponse,
tbs = tbs,
issuerCertPath = issuerCertPath,
smtServer = null,
issuerId = issuerId,
outputDir = documentsPath
)
// 2. Download pre-built keys (only needed once)
downloadKeys(documentsPath)
// Or generate locally: setupKeys(documentsPath)
// 3. Generate proofs
val certProof = proveCertChainRs4096(documentsPath)
println("cert_chain proved in ${certProof.proveMs} ms")
val deviceProof = proveDeviceSigRs2048(documentsPath)
println("device_sig proved in ${deviceProof.proveMs} ms")
// 4. Verify proofs
val valid = linkVerify(documentsPath)
println("Proofs valid: $valid")This package relies on bindings generated by the Mopro CLI. To learn how to build Mopro bindings, refer to the Getting Started section.
Use mopro-cli and choose the appropriate circuit:
mopro init
mopro buildChoose android and build for aarch64-linux-android and x86_64-linux-android architectures.
Then replace the bindings in:
lib/src/main/java/uniffilib/src/main/jniLibs
Or copy them with:
cp -r MoproAndroidBindings/uniffi lib/src/main/java
cp -r MoproAndroidBindings/jniLibs lib/src/mainThis work was initially sponsored by a joint grant from PSE and 0xPARC. It is currently incubated by PSE.