Electronic Signature Implementation in Mobile Applications
Electronic signature in a mobile app is not a picture overlaid on a document. It's a cryptographic operation: the document is signed with the user's private key, and the verifier checks the signature using the public key, guaranteeing that the document hasn't changed since signing and that this specific user signed it.
Depending on jurisdiction and requirements, there are qualified e-signature (QES — with token and certificate from accredited CA), advanced electronic signature (AES — own PKI), and simple electronic signature (SES — login/password, SMS code). For most commercial use cases, AES is sufficient; QES is needed for government services and legally significant document workflows with authorities.
PKI on a Mobile Device
The most common approach for AES: the private key is generated on the device and stored in Keychain (iOS) or Android Keystore. The public key is registered on the server. Signing happens on the device — the private key never leaves Secure Enclave/TEE.
Generating a Key Pair on Android:
val keyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"
)
keyPairGenerator.initialize(
KeyPairGeneratorSpec.Builder(context)
.setAlias("user_signing_key")
.setKeyType("EC")
.setKeySize(256)
.setSubject(X500Principal("CN=User"))
.setSerialNumber(BigInteger.ONE)
.setStartDate(startDate)
.setEndDate(endDate)
.build()
)
val keyPair = keyPairGenerator.generateKeyPair()
// register public key on server
val publicKeyBase64 = Base64.encode(keyPair.public.encoded)
To sign a document:
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val privateKey = keyStore.getKey("user_signing_key", null) as PrivateKey
val signature = Signature.getInstance("SHA256withECDSA")
signature.initSign(privateKey)
signature.update(documentBytes)
val signatureBytes = signature.sign()
iOS, Swift:
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: "com.example.signing".data(using: .utf8)!
]
]
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
// handle error
}
let publicKey = SecKeyCopyPublicKey(privateKey)!
kSecAttrTokenIDSecureEnclave — the key is created inside Secure Enclave and cannot be extracted even if the main processor is compromised.
Signing with Biometric Authentication
For legally significant operations, the signing key must be protected by identity confirmation. Bind to biometrics via Keystore/Keychain: key accessible only after successful biometric authentication within the current session.
Android:
keyGenParameterSpec = KeyGenParameterSpec.Builder(...)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
.build()
On each signing:
val biometricPrompt = BiometricPrompt(activity, executor, callback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Sign Document")
.setSubtitle("Use biometry to confirm")
.setNegativeButtonText("Cancel")
.build()
// bind CryptoObject with our Signature object
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(signature))
In onAuthenticationSucceeded callback, we get result.cryptoObject?.signature — an unlocked signature object ready to use.
Signature Format and Server Verification
For standardization, use CMS (Cryptographic Message Syntax, RFC 5652) or JWS (JSON Web Signature, RFC 7515). JWS is more convenient for REST API:
Header.Payload.Signature
Header: {"alg": "ES256", "kid": "user_key_id"}
Payload: base64url-encoded document or its hash
Signature: ES256 signature
On the server, verify through any JWT library with ES256 support. Python: PyJWT, cryptography. Node.js: jose, jsonwebtoken. Java: nimbus-jose-jwt.
Long-Term Validity and Timestamp
If a document must remain valid for years (after the signer's certificate expires), use RFC 3161 Trusted Timestamping. Timestamp Authority (TSA) signs the document hash + time with its own key. Even if the user's key is later compromised, the timestamp proves when the signature was created.
Public TSAs: Freetsa.org, DigiCert TSA. Integration: bouncycastle on Android, Security.framework + RFC 3161 request on iOS.
Integration Options with External QES Providers
For qualified signatures in Russia — CryptoPro, Rutoken, ViPNet. Each has mobile SDKs. Integration via CryptoPro CSP SDK for Android/iOS: signing happens on the token side (physical or software), the app only passes data to the SDK.
For international use cases — DocuSign SDK, Adobe Sign API, HelloSign. They abstract cryptography but tie to specific providers.
Timeline for implementing AES with key generation in Keystore/Keychain, biometric confirmation, and server verification — 3–5 days. Integration with QES provider or external document workflow service — individual assessment after analyzing provider API.







