TrustPin is a modern, lightweight, and secure Kotlin library for SSL Certificate Pinning in Android applications, with a hardened JVM JAR available by request for server and desktop customers. Built with Kotlin Coroutines and following OWASP recommendations, it prevents man-in-the-middle (MITM) attacks by validating server authenticity at the TLS level.
Available on Maven Central for Android as an AAR: cloud.trustpin:kotlin-sdk.
JVM customers should request access to the hardened JVM JAR via email at support@trustpin.cloud.
- ✅ Android AAR on Maven Central with bundled R8/ProGuard consumer rules
- ✅ Hardened JVM JAR available by request for desktop / server customers
- ✅ Strict or permissive pinning — enforce in production, relax during development
- ✅ Drop-in integration —
TrustManagerandSSLSocketFactoryfor OkHttp /HttpsURLConnection - ✅ Managed configuration delivered from TrustPin and cached locally
- ✅ Coroutine- and Java-friendly APIs
- ✅ Minimal dependencies — Kotlin stdlib + coroutines only
- Platform Requirements
- Installation
- Quick Setup
trustpin.jsonreference (Android)- Usage Examples
- Pinning Modes
- Error Handling
- Logging
- Best Practices
- API Reference
- Suspend vs Blocking API
- Testing
- Troubleshooting
- Documentation, License & Support
| Platform | Minimum Version | Notes |
|---|---|---|
| Android | API 25+ | Full feature support |
| JVM | Java 11+ | Hardened JAR available by request |
| Kotlin | 2.3.0+ | Built with Kotlin 2.3.0 |
dependencies {
implementation("cloud.trustpin:kotlin-sdk:5.0.0")
}dependencies {
implementation 'cloud.trustpin:kotlin-sdk:5.0.0'
}<dependency>
<groupId>cloud.trustpin</groupId>
<artifactId>kotlin-sdk</artifactId>
<version>1.2.0</version>
</dependency>The Maven Central artifact is an Android AAR. For JVM / server / desktop use, request the hardened JVM JAR at support@trustpin.cloud.
The Android AAR ships consumer rules automatically. Apps should not need to add TrustPin-specific keep rules. The SDK keeps the documented public API stable while obfuscating internal and private implementation details.
On Android, setup must go through a Context-aware entry point so the SDK can persist its integrity-check state under Context.getNoBackupFilesDir(). There are two equivalent recommended paths.
⚠️ Do not use the contextlessTrustPinConfiguration(orgId, projId, publicKey)constructor on Android. It works, but the SDK has noContextand falls back to an in-memory integrity-check backend that does not survive a process restart. A one-shotinfolog is emitted so the regression is operator-visible.
⚠️ Decorated configurations are single-use on Android. ATrustPinConfigurationproduced byfromAssets(context)or.withAndroidStorage(context)MUST be passed to exactly oneTrustPin.setupcall. The Android storage backend is consumed on first use; reusing the same configuration object silently falls back to in-memory storage on the second instance. Build a fresh decorated configuration for each TrustPin instance:// ✓ Correct TrustPin.instance("payments").setup(TrustPinConfiguration.fromAssets(context)) TrustPin.instance("analytics").setup(TrustPinConfiguration.fromAssets(context)) // ✗ Wrong — second setup silently loses Android persistence val shared = TrustPinConfiguration.fromAssets(context) TrustPin.default.setup(shared) TrustPin.instance("payments").setup(shared)The restriction does not apply on JVM.
Ship a trustpin.json in your app's assets/ directory; the loader reads it and automatically attaches the Android-persistent storage backend for you.
import android.content.Context
import cloud.trustpin.kotlin.sdk.TrustPin
import cloud.trustpin.kotlin.sdk.TrustPinConfiguration
import cloud.trustpin.kotlin.sdk.fromAssets
suspend fun initializeTrustPin(context: Context) {
// fromAssets(context) reads assets/trustpin.json AND calls
// .withAndroidStorage(context) internally.
TrustPin.setup(TrustPinConfiguration.fromAssets(context))
}From Java:
import cloud.trustpin.kotlin.sdk.TrustPin;
import cloud.trustpin.kotlin.sdk.TrustPinConfiguration;
import cloud.trustpin.kotlin.sdk.TrustPinConfigurationAssets;
void initializeTrustPin(Context context) {
TrustPinConfiguration config = TrustPinConfigurationAssets.fromAssets(context);
TrustPin.getDefault().setupBlocking(config);
}See trustpin.json reference for the file format and per-flavor / per-build-type overrides.
When you can't ship a bundled JSON (credentials come from runtime config, A/B-test routing, MDM channel, etc.), build the configuration programmatically and chain .withAndroidStorage(context) to attach the persistent storage backend.
import android.content.Context
import cloud.trustpin.kotlin.sdk.TrustPin
import cloud.trustpin.kotlin.sdk.TrustPinConfiguration
import cloud.trustpin.kotlin.sdk.TrustPinMode
import cloud.trustpin.kotlin.sdk.withAndroidStorage
suspend fun initializeTrustPin(context: Context) {
val config = TrustPinConfiguration(
organizationId = "your-org-id",
projectId = "your-project-id",
publicKey = "your-base64-public-key",
mode = TrustPinMode.STRICT,
).withAndroidStorage(context) // ← required on Android
TrustPin.setup(config)
}withAndroidStorage(context) captures context.applicationContext (no Activity leak), registers the storage backend in an SDK-internal binding, and returns the same configuration so the call chains cleanly.
JVM customers (server-side, desktop, embedded) construct the configuration directly — there is no Context, and the SDK keeps integrity-check state in-memory for the lifetime of the JVM process.
import cloud.trustpin.kotlin.sdk.TrustPin
import cloud.trustpin.kotlin.sdk.TrustPinConfiguration
import cloud.trustpin.kotlin.sdk.TrustPinMode
suspend fun initializeTrustPin() {
TrustPin.setup(
TrustPinConfiguration(
organizationId = "your-org-id",
projectId = "your-project-id",
publicKey = "your-base64-public-key",
mode = TrustPinMode.STRICT,
)
)
}💡 Find your credentials in the TrustPin Dashboard.
⚙️ TrustPin provides both suspend functions (recommended for Kotlin coroutines) and blocking functions (for Java interop and non-coroutine contexts). See Suspend vs Blocking API.
The default TrustPin instance can be configured from a JSON file in your application's assets/ directory — same pattern as google-services.json. The loader reads the file and automatically chains .withAndroidStorage(context) so the returned configuration is production-ready.
app/src/main/assets/trustpin.json:
{
"organization_id": "your-org-id",
"project_id": "your-project-id",
"public_key": "MFkwEwYH...",
"mode": "strict",
"configuration_url": "https://custom.example.com/config/signed.b64"
}| Field | Required | Notes |
|---|---|---|
organization_id |
✅ | Organization identifier |
project_id |
✅ | Project identifier |
public_key |
✅ | Base64-encoded public key |
mode |
❌ | "strict" (default) or "permissive" |
configuration_url |
❌ | Optional custom source (HTTPS only) |
Unknown top-level keys are ignored for forward compatibility.
See §1a above. A custom filename can be supplied:
TrustPinConfiguration.fromAssets(context, fileName = "my-config.json")Per-variant overrides use Android's standard asset-merging — no Gradle plugin required:
app/src/main/assets/trustpin.json ← default
app/src/debug/assets/trustpin.json ← overrides for the `debug` build type
app/src/ff/assets/trustpin.json ← overrides for the `ff` product flavor
- Default instance only. File-based setup configures
TrustPin.default. Named instances (TrustPin.instance(id)) continue to use programmaticTrustPinConfiguration(...). - Android only. The hardened JVM JAR does not include
fromAssets. - The file is bundled into the APK —
public_keyis the verification key for signed pin payloads, not key material. - Failure modes (file missing, malformed JSON, invalid
mode, non-HTTPSconfiguration_url, etc.) all surface asTrustPinError.InvalidProjectConfig. A descriptive reason is written to logcat under the[TrustPin]tag. - No log-level field — log verbosity stays under programmatic control via
TrustPin.setLogLevel(...).
The recommended integration pattern for OkHttp applications:
import cloud.trustpin.kotlin.sdk.TrustPin
import cloud.trustpin.kotlin.sdk.ssl.TrustPinSSLSocketFactory
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
class NetworkManager {
private val httpClient by lazy {
val sslSocketFactory = TrustPinSSLSocketFactory.create()
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.sslSocketFactory(sslSocketFactory, sslSocketFactory.trustManager())
.build()
}
suspend fun fetchData(url: String): String =
httpClient.newCall(Request.Builder().url(url).build()).execute().use {
it.body?.string() ?: ""
}
}import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
val sslSocketFactory = TrustPinSSLSocketFactory.create()
val okHttpClient = OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, sslSocketFactory.trustManager())
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
val sslSocketFactory = TrustPinSSLSocketFactory.create()
val client = HttpClient(OkHttp) {
engine {
preconfigured = OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, sslSocketFactory.trustManager())
.build()
}
}For custom deployment scenarios or alternative configuration endpoints:
import java.net.URI
TrustPin.setup(
organizationId = "your-org-id",
projectId = "your-project-id",
publicKey = "your-public-key",
configurationURL = URI.create("https://custom.example.com/config/signed-payload.b64").toURL(),
mode = TrustPinMode.STRICT
)For custom networking stacks or certificate inspection:
import cloud.trustpin.kotlin.sdk.TrustPin
import cloud.trustpin.kotlin.sdk.TrustPinError
import java.security.cert.X509Certificate
suspend fun verifyCertificate(domain: String, certificate: X509Certificate) {
try {
TrustPin.verify(domain, certificate)
} catch (e: TrustPinError.DomainNotRegistered) {
// Strict mode only: domain is not in your pinning configuration.
} catch (e: TrustPinError.PinsMismatch) {
// Certificate doesn't match any configured pin — potential MITM.
}
}import javax.net.ssl.HttpsURLConnection
val sslSocketFactory = TrustPinSSLSocketFactory.create()
HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory)
val connection = URL("https://api.example.com").openConnection() as HttpsURLConnection
val body = connection.inputStream.bufferedReader().readText()| Mode | Behaviour |
|---|---|
TrustPinMode.STRICT |
Throws TrustPinError.DomainNotRegistered for unregistered domains. Recommended for production. |
TrustPinMode.PERMISSIVE |
Allows unregistered domains to bypass pinning. For development / dynamic endpoints. |
import cloud.trustpin.kotlin.sdk.TrustPinError
try {
TrustPin.verify(domain = "api.example.com", certificate = cert)
} catch (e: TrustPinError.DomainNotRegistered) {
// Strict mode only: domain is not in your pinning configuration.
} catch (e: TrustPinError.PinsMismatch) {
// Certificate doesn't match configured pins — potential MITM.
} catch (e: TrustPinError.AllPinsExpired) {
// All pins for the domain have expired.
} catch (e: TrustPinError.InvalidServerCert) {
// Certificate format is invalid.
} catch (e: TrustPinError.InvalidProjectConfig) {
// Setup parameters are invalid.
} catch (e: TrustPinError.ErrorFetchingPinningInfo) {
// Transient network / configuration fetch problem.
} catch (e: TrustPinError.ConfigurationValidationFailed) {
// Configuration signature validation failed.
} catch (e: TrustPinError.ConfigIntegrityError) {
// Configuration failed an integrity check — hard stop.
}TrustPin.setLogLevel(TrustPinLogLevel.DEBUG)
// Levels: NONE, ERROR, INFO, DEBUGSet the log level before setup for complete logging coverage. Use ERROR or NONE in production.
- Always use
TrustPinMode.STRICTin production. - Rotate pins before expiration; monitor pin-validation failures.
- Use HTTPS for all pinned domains.
- Call
TrustPin.setup()exactly once at app launch. - Treat setup errors as hard stops — don't continue to construct an unpinned
OkHttpClient. - Use
requirePinned()as a guard before any pinned network operation.
- Start in
TrustPinMode.PERMISSIVEduring development; switch toSTRICTfor production. - Use
DEBUGlog level when troubleshooting; revert toERRORorNONEfor release builds.
TrustPin enforces a lightweight production-shape device gate on Android release builds. The check refuses to initialise on environments that aren't OEM-signed production user builds — non-user build types, test-keys-signed images, missing or unknown MANUFACTURER, and standard emulator markers.
Debug-built host apps (debug variants produced by AGP — android:debuggable="true" in the merged manifest) skip the gate automatically, so local dev and CI on emulators work without configuration.
The gate is intentionally light and structural. It is not a substitute for a RASP product. TrustPin does not detect root, MagiskHide, bootloader-unlock state, or runtime instrumentation. Those checks belong in a dedicated RASP layer integrated by the host application.
If your threat model includes those attack classes, layer a RASP alongside TrustPin and ensure it provides both:
- Root / bootloader / device-tamper detection —
su, Magisk modules, custom recoveries, unlocked-bootloader state, system-image tampering. - Runtime hook detection and prevention — Frida (server probes, named-pipe checks, library scans of
/proc/self/maps), Xposed / LSPosed (module enumeration, classloader inspection), inline-hook detection on ART, code-integrity checks on critical native libraries.
A RASP that only does root detection is insufficient — the in-process hook surface (Frida attaching at runtime to a non-rooted device, malicious dependency loaded by the host) is the more realistic attack path for a pinning-bypass attempt, and it requires active hook detection rather than just a one-shot root scan at startup.
If you need to run a release-shaped APK (minified, release-keys) on an emulator for performance or QA work, declare a non-release buildType with isDebuggable = true in your app's Gradle config — for example a releaseStaging variant alongside release. Never upload a debuggable APK to Play.
Main SDK interface with dual API design.
class TrustPin {
companion object {
val default: TrustPin
// id must match [a-zA-Z0-9._-]+ — reverse-DNS ("com.example.app")
// and the bare "default" reserved name are the two notable cases.
fun instance(id: String): TrustPin
}
// ── Suspend API (recommended for Kotlin) ──────────────────────────────
suspend fun setup(configuration: TrustPinConfiguration)
fun requirePinned()
suspend fun verify(domain: String,
certificate: X509Certificate,
timeout: Long = 30_000)
suspend fun verify(domain: String,
certificate: String,
timeout: Long = 30_000)
suspend fun verify(domain: String,
chain: List<X509Certificate>,
timeout: Long = 30_000)
suspend fun fetchCertificate(host: String,
port: Int = 443,
timeout: Long = 30_000): String
// ── Blocking API (for Java interop) ───────────────────────────────────
fun setupBlocking(configuration: TrustPinConfiguration)
fun verifyBlocking(domain: String, certificate: X509Certificate, timeout: Long = 30_000)
fun verifyBlocking(domain: String, certificate: String, timeout: Long = 30_000)
fun verifyBlocking(domain: String, chain: List<X509Certificate>, timeout: Long = 30_000)
fun fetchCertificateBlocking(host: String, port: Int = 443, timeout: Long = 30_000): String
// ── SSL integration ───────────────────────────────────────────────────
fun makeSSLSocketFactory(): SSLSocketFactory
fun makeTrustManager(): X509TrustManager
// ── Logging ───────────────────────────────────────────────────────────
fun setLogLevel(level: TrustPinLogLevel)
}timeout is in milliseconds, default 30 000, clamped to [10 000, 120 000].
data class TrustPinConfiguration(
val organizationId: String,
val projectId: String,
val publicKey: String,
val mode: TrustPinMode = TrustPinMode.STRICT,
val configurationURL: URL? = null
)
// Android-only extensions
fun TrustPinConfiguration.withAndroidStorage(context: Context): TrustPinConfiguration
fun TrustPinConfiguration.Companion.fromAssets(
context: Context,
fileName: String = "trustpin.json"
): TrustPinConfigurationenum class TrustPinMode {
STRICT, // Throws for unregistered domains (production)
PERMISSIVE // Allows unregistered domains to bypass pinning (development)
}enum class TrustPinLogLevel(val value: Int) {
NONE(0), ERROR(1), INFO(2), DEBUG(3)
}sealed class TrustPinError : Exception() {
object InvalidProjectConfig : TrustPinError()
object SetupInProgress : TrustPinError()
object LockTimeout : TrustPinError()
object NotInitialized : TrustPinError()
object ErrorFetchingPinningInfo : TrustPinError()
object InvalidServerCert : TrustPinError()
object PinsMismatch : TrustPinError()
object AllPinsExpired : TrustPinError()
object ConfigurationValidationFailed : TrustPinError()
object DomainNotRegistered : TrustPinError()
object ConfigIntegrityError : TrustPinError()
}Context-aware SSLSocketFactory with built-in TrustPin certificate validation.
class TrustPinSSLSocketFactory : SSLSocketFactory() {
companion object {
fun create(): TrustPinSSLSocketFactory
}
fun trustManager(): X509TrustManager
}val sslSocketFactory = TrustPinSSLSocketFactory.create()
val client = OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, sslSocketFactory.trustManager())
.build()TrustPin provides two API styles:
| Feature | Suspend API | Blocking API |
|---|---|---|
| Best for | Kotlin coroutines | Java interop, legacy code |
| Performance | Non-blocking | Blocks the calling thread |
| Cancellation | Supports coroutine cancellation | None |
| Usage context | suspend fns / coroutine builders |
Any function context |
// Suspend (recommended for Kotlin)
suspend fun initialize() {
TrustPin.setup(configuration)
}
// Blocking (Java interop)
fun initializeFromJava() {
TrustPin.getDefault().setupBlocking(configuration)
}Blocking APIs must not be called from the Android main thread; they throw IllegalStateException if you try.
import kotlinx.coroutines.test.runTest
import org.junit.Test
class NetworkTest {
@Test
fun `secure network request succeeds`() = runTest {
TrustPin.setup(
TrustPinConfiguration(
organizationId = "test-org",
projectId = "test-project",
publicKey = "test-key",
mode = TrustPinMode.PERMISSIVE
)
)
val result = SecureNetworkClient().fetchData()
assert(result.isNotEmpty())
}
}import okhttp3.mockwebserver.MockWebServer
class TrustPinIntegrationTest {
private lateinit var mockServer: MockWebServer
@Before fun setUp() { mockServer = MockWebServer().also { it.start() } }
@After fun tearDown() { mockServer.shutdown() }
@Test fun `pinning with mock server`() = runTest {
TrustPin.setup(
TrustPinConfiguration(
organizationId = "test-org",
projectId = "test-project",
publicKey = "test-key",
mode = TrustPinMode.PERMISSIVE // allow mock server
)
)
// Test networking code against mockServer.url("/")
}
}- Verify organization ID, project ID, and public key against the dashboard.
- Check for stray whitespace or newlines in credentials.
- Ensure the public key is properly base64-encoded.
- Confirm the domain is registered in the TrustPin dashboard.
- Check the certificate format (must be valid X.509).
- Verify pins haven't expired.
- Test with
TrustPinMode.PERMISSIVEfirst.
- Ensure TrustPin is initialized before creating
OkHttpClient. - Use
TrustPinSSLSocketFactory.create()with OkHttp'ssslSocketFactory(factory, trustManager). - Always pass both the
SSLSocketFactoryand theTrustManagerto OkHttp.
- Enable debug logging:
TrustPin.setLogLevel(TrustPinLogLevel.DEBUG) - Test in
TrustPinMode.PERMISSIVEfirst - Re-verify credentials in the dashboard
- Check network connectivity to
cdn.trustpin.cloud
- API reference: docs.trustpin.cloud/sdk/kotlin
- Dashboard: app.trustpin.cloud
- Support: trustpin.cloud
This project is licensed under the TrustPin Binary License Agreement — see LICENSE.
Commercial License: for enterprise licensing or custom agreements, contact contact@trustpin.cloud.
Attribution required: applications using this software must display "Uses TrustPin™ technology – https://trustpin.cloud".
Built with ❤️ by the TrustPin team