Android / Kotlin SDK
Integrate TrustPin into your Android or JVM application for native certificate pinning.
Current version: cloud.trustpin:kotlin-sdk 6.3.0
Platform Requirements
| Platform | Minimum Version |
|---|---|
| Android | API 25+ (full feature support) |
| JVM | Java 11+ |
Kotlin Version: 2.3.0+
Note: The Maven Central artifact is an Android AAR. For server-side JVM, desktop, or Compose Multiplatform targets, request access to the hardened JVM JAR via support@trustpin.cloud.
Installation
Gradle (Kotlin DSL)
Add TrustPin to your build.gradle.kts:
dependencies {
implementation("cloud.trustpin:kotlin-sdk:6.3.0")
}Gradle (Groovy)
Add to your build.gradle:
dependencies {
implementation 'cloud.trustpin:kotlin-sdk:6.3.0'
}Maven
Add to your pom.xml:
<dependency>
<groupId>cloud.trustpin</groupId>
<artifactId>kotlin-sdk</artifactId>
<version>6.3.0</version>
</dependency>Optional Client Adapters: OkHttp / Ktor
Thin, optional integration artifacts published to Maven Central alongside the SDK, versioned in lockstep with it. Each contains only public-API glue, and neither pulls in the SDK or the HTTP client transitively. Your app supplies both:
dependencies {
implementation("cloud.trustpin:trustpin-okhttp:6.3.0") // OkHttp
implementation("cloud.trustpin:trustpin-ktor:6.3.0") // Ktor (OkHttp engine); includes trustpin-okhttp
}Quick Start
1. Get Your Credentials
Sign in to the TrustPin Dashboard and retrieve:
- Organization ID
- Project ID
- Public Key (Base64-encoded)
2. Initialize TrustPin
Ship a trustpin.json asset in your app and load it with TrustPinConfiguration.fromAssets(context). Credentials stay out of source.
Place trustpin.json at app/src/main/assets/trustpin.json:
{
"organization_id": "your-org-id",
"project_id": "your-project-id",
"public_key": "your-base64-public-key",
"mode": "strict"
}Build-variant overrides follow standard Android source-set merging. Drop a different file under src/debug/assets/trustpin.json or src/staging/assets/trustpin.json to use per-flavor credentials.
| Key | Type | Required | Notes |
|---|---|---|---|
organization_id | String | Yes | Non-empty |
project_id | String | Yes | Non-empty |
public_key | String | Yes | Base64-encoded ECDSA P-256 public key |
mode | String | No | "strict" (default) or "permissive" |
configuration_url | String | No | Must be HTTPS. Overrides the default CDN endpoint |
embedded_configuration_asset | String | No | Asset name of a bundled signed configuration, read from assets/. See Embedded Configuration |
Then load it during Application.onCreate():
import android.app.Application
import cloud.trustpin.kotlin.sdk.TrustPin
import cloud.trustpin.kotlin.sdk.TrustPinConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
CoroutineScope(Dispatchers.IO).launch {
try {
val config = TrustPinConfiguration.fromAssets(this@MyApplication)
TrustPin.setup(config)
TrustPin.awaitConfiguration()
println("TrustPin initialized")
} catch (e: Exception) {
println("TrustPin setup failed: ${e.message}")
}
}
}
}setup() is non-blocking. It starts loading the configuration and returns without waiting. TrustPin.awaitConfiguration() is the fail-closed gate: call it once after setup, immediately before constructing any HTTP client that depends on pinning, to suspend until a validated configuration is loaded (default timeout: 30s) and throw if it didn’t. For synchronous call sites use TrustPin.awaitConfigurationBlocking(), or check TrustPin.isConfigurationLoaded for a non-throwing status read.
Don’t forget to register your Application class in AndroidManifest.xml:
<application
android:name=".MyApplication"
...>
</application>3. Add Network Permission
Ensure your AndroidManifest.xml includes:
<uses-permission android:name="android.permission.INTERNET" />Embedded Configuration
TrustPin fetches its signed pinning configuration online and keeps the last validated one on the device. For the one case where neither exists, an app’s very first start while every configuration source is unreachable, you can ship a signed configuration inside the app as a last-resort fallback.
Download the signed configuration for your project from the dashboard and place it at app/src/main/assets/trustpin-seed.b64. With trustpin.json, reference it by adding "embedded_configuration_asset": "trustpin-seed.b64" and leave the call site unchanged. Programmatically, pass an EmbeddedConfiguration:
import cloud.trustpin.kotlin.sdk.EmbeddedConfiguration
// Android: the asset is read by the Context-aware setup path.
TrustPin.setup(
TrustPinConfiguration(
organizationId = "your-org-id",
projectId = "your-project-id",
publicKey = "your-base64-public-key",
embeddedConfiguration = EmbeddedConfiguration.Asset("trustpin-seed.b64"),
).withAndroidStorage(context)
)
// JVM: a classpath resource, or a file inside the installation.
TrustPin.setup(
TrustPinConfiguration(
organizationId = "your-org-id",
projectId = "your-project-id",
publicKey = "your-base64-public-key",
embeddedConfiguration = EmbeddedConfiguration.File("config/trustpin-seed.b64"),
)
)| Variant | Resolves against | Available on |
|---|---|---|
EmbeddedConfiguration.Asset(name) | Android assets/, or the JVM classpath | Android and JVM |
EmbeddedConfiguration.File(path) | A filesystem path | JVM only |
On Android, EmbeddedConfiguration.Asset requires one of the Context-aware setup paths (fromAssets(context) or .withAndroidStorage(context)).
Requirements
- Use it only in apps protected by RASP (runtime application self-protection) that guards bundled resources against modification. An unprotected app must not ship an embedded configuration.
- The file must be the unmodified signed payload downloaded from the dashboard for this project. It is verified against
publicKeyduring setup, andsetup()throwsTrustPinError.InvalidProjectConfigif the file is not a bundled resource, cannot be read, or does not verify. - Regenerate it in CI on every release, so it is never older than the app that ships it. Pins expire on their own schedule, and an embedded configuration whose pins have all expired is equivalent to having no fallback.
trustpin-cli projects jwswrites the currently published payload to a file, so a release pipeline can refresh the bundled copy on every build.
Behaviour
- It is never preferred over an online source, or over a configuration the SDK has already fetched and validated.
- It is subject to the same integrity checks as any other configuration: a device that has already trusted a newer configuration will not accept an older embedded one.
- The iOS, Flutter, and React Native SDKs expose the equivalent option. The file format is identical across platforms.
Integration Approaches
| Approach | Best For | Setup Complexity |
|---|---|---|
| OkHttp Integration (Recommended) | Most Android apps | 🟢 Low |
| Retrofit Integration | REST API clients (uses OkHttp under the hood) | 🟢 Low |
| Ktor Client Integration | Ktor-based apps and KMP shared modules | 🟡 Medium |
| Manual Verification | Custom transports, non-OkHttp stacks | 🟠 High |
OkHttp Integration (Recommended)
With the optional trustpin-okhttp adapter , one line replaces the manual SSL wiring and guarantees the factory/trust-manager pair belongs to the same TrustPin instance:
val client = OkHttpClient.Builder()
.trustPin() // or .trustPin(TrustPin.instance("payments"))
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()// Java
OkHttpClient client = TrustPinOkHttp.trustPin(new OkHttpClient.Builder()).build();Call after TrustPin.setup(...).
Without the adapter, wire the SSL socket factory manually. TrustPinSSLSocketFactory.create() returns an SSL socket factory wired to your TrustPin configuration. Pass it, along with its trust manager, to OkHttpClient.Builder:
import cloud.trustpin.kotlin.sdk.ssl.TrustPinSSLSocketFactory
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
val sslSocketFactory = TrustPinSSLSocketFactory.create()
val httpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.sslSocketFactory(sslSocketFactory, sslSocketFactory.trustManager())
.build()Retrofit Integration
Retrofit uses OkHttp under the hood, so share the same TrustPin-backed client:
class ApiClient {
private val okHttpClient by lazy {
val sslSocketFactory = TrustPinSSLSocketFactory.create()
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.sslSocketFactory(sslSocketFactory, sslSocketFactory.trustManager())
.build()
}
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}Ktor Client Integration
With the optional trustpin-ktor adapter (OkHttp engine only, since TLS configuration is engine-specific in Ktor):
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
val ktorClient = HttpClient(OkHttp) {
engine { trustPin() }
}A Ktor
preconfiguredOkHttpClient bypasses engine configuration. Pin it directly with the OkHttp adapter instead.
Without the adapter, plug TrustPin into Ktor’s OkHttp engine manually:
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import okhttp3.OkHttpClient
private val httpClient by lazy {
val sslSocketFactory = TrustPinSSLSocketFactory.create()
HttpClient(OkHttp) {
engine {
preconfigured = OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, sslSocketFactory.trustManager())
.build()
}
}
}Manual Verification
For custom transports, validate a domain/certificate pair directly with the suspending API:
import java.security.cert.X509Certificate
val certificate: X509Certificate = /* ... */
TrustPin.verify("api.example.com", certificate)Named Instances (Multi-Tenant)
The SDK supports independent named instances via TrustPin.instance(id) for apps that talk to multiple TrustPin projects (for example, separate consumer and admin backends). Because the bundled-asset path loads a single trustpin.json per build, multi-tenant setups currently require the programmatic configuration API. See the upstream Kotlin SDK docs for usage.
Logging
Set the desired verbosity before calling setup() to capture initialization logs:
import cloud.trustpin.kotlin.sdk.TrustPinLogLevel
TrustPin.setLogLevel(TrustPinLogLevel.INFO)Available levels: NONE, ERROR, INFO, DEBUG.
Custom Log Sink
To route SDK log output into your own logging pipeline, install a global TrustPinLogSink. One sink serves all instances and receives every message after per-instance level filtering, tagged with the producing instance id:
TrustPin.setLogSink { level, instanceId, message ->
myLogger.log("[$instanceId] $message")
}
TrustPin.setLogSink(null) // restore the default sinkSinks are called synchronously from SDK internals, including TLS-handshake threads: keep them fast and non-blocking, don’t perform I/O inline, and never call back into TrustPin from a sink.
Monitoring Pin Validation
To feed pin-validation verdicts into your security monitoring, for example reporting suspected MITM attempts to your backend, install a global TrustPinValidationListener:
import cloud.trustpin.kotlin.sdk.TrustPinValidationListener
import java.security.cert.X509Certificate
TrustPin.setValidationListener(object : TrustPinValidationListener {
override fun onValidationFailure(
instanceId: String,
domain: String,
error: TrustPinError,
presentedCertificate: X509Certificate,
) {
// Fires only for definitive verdicts: PinsMismatch, AllPinsExpired,
// DomainNotRegistered (strict mode). `presentedCertificate` is the
// leaf as received from the network. Treat it as untrusted input.
}
override fun onValidationSuccess(instanceId: String, domain: String) {
// Optional. The default implementation does nothing.
}
})
TrustPin.setValidationListener(null) // detachThe listener is observe-only: it is invoked strictly after the verdict is decided and cannot veto, approve, or alter a connection. Transient conditions (configuration fetch failures, timeouts) and permissive-mode connections to unregistered domains produce no callbacks. Like log sinks, listeners are called synchronously from TLS-handshake threads, so keep them non-blocking and never call back into TrustPin.
Error Handling
TrustPinError is a sealed class with the following variants:
| Variant | Meaning |
|---|---|
DomainNotRegistered | Strict mode and the host isn’t in the configuration |
PinsMismatch | Server certificate doesn’t match any active pin |
AllPinsExpired | Configuration is stale. Rotate pins in the dashboard |
InvalidServerCert | Server returned an unparseable certificate |
InvalidProjectConfig | Bad credentials or invalid configuration |
ErrorFetchingPinningInfo | Network failure while loading the configuration |
ConfigurationValidationFailed | JWS signature didn’t verify against the project’s public key |
ConfigIntegrityError | Configuration failed an integrity check. Hard stop |
NotInitialized | An API was called before setup() completed successfully |
AlreadyInitialized | setup() was called a second time on the same instance |
SetupInProgress | An operation raced a setup() that hadn’t finished yet |
LockTimeout | An internal lock couldn’t be acquired in time |
Timeout | An operation (e.g. awaitConfiguration) exceeded its timeout |
SSLContextSetupFailed | The pinned SSLContext / socket factory couldn’t be created |
UnsupportedDevice | The runtime environment doesn’t support the required security primitives |
try {
TrustPin.setup(config)
} catch (e: TrustPinError.InvalidProjectConfig) {
// Bad credentials
} catch (e: TrustPinError.ErrorFetchingPinningInfo) {
// Network failure during setup
} catch (e: TrustPinError.NotInitialized) {
// setup() wasn't called, or it failed silently. Guard with awaitConfiguration()
} catch (e: TrustPinError) {
// Any other TrustPin failure (UnsupportedDevice, etc.)
}Best Practices
Setup & Initialization
- Initialize in
Application.onCreate()for app-wide coverage. - Use a coroutine scope for async setup. The API is suspend-first (blocking variants are available for synchronous call sites).
- Call
TrustPin.awaitConfiguration()aftersetup(), before constructing any HTTP client that depends on pinning.setup()is non-blocking. - Set the log level before
setup()to capture initialization output. - Handle setup errors gracefully. Don’t block app launch.
Security
- Use
TrustPinMode.STRICTin production. - Prefer SPKI pinning; rotate pins in the dashboard before they expire.
- Monitor pin validation failures via
TrustPin.setValidationListener(...)or logging. - Keep credentials outside source control. Prefer
TrustPinConfiguration.fromAssets(context)with per-flavortrustpin.jsonfiles, or fetch them at runtime and usewithAndroidStorage(context). - Use HTTPS for all pinned domains.
- Ship an embedded configuration only if the app is protected by RASP, and regenerate it in CI on every release.
Performance
- Configuration is cached for 10 minutes with a stale-while-revalidate fallback.
- Reuse
OkHttpClientinstances rather than creating one per request. - Use minimal log levels in production.
Complete Documentation
For the full API reference, ProGuard/R8 rules, and additional integration patterns, visit:
Resources
- Repository: github.com/trustpin-cloud/kotlin.sdk
- API Reference: trustpin-cloud.github.io/kotlin.sdk
- Maven Coordinates:
cloud.trustpin:kotlin-sdk:6.3.0 - Dashboard: app.trustpin.cloud
- Support: support@trustpin.cloud