Skip to Content
App AttestationSDK IntegrationReact Native (iOS & Android)

App attestation (React Native)

For React Native developers adding attestation to an app that already uses TrustPin pinning (@trustpin/react-native). At the end of this page, your requests carry a token your backend can verify.

How it works

@trustpin/react-native-attest is a thin wrapper of the native attestation modules, TrustPinAttestKit on iOS and kotlin-attest on Android. Like pinning, it starts in native code before JavaScript runs. The device creates a key and sends evidence about the app and the device to the attestation verifier you run: Apple App Attest, or Android Key Attestation and Google Play Integrity, as your policy asks. The verifier answers with a short-lived signed token, which the SDK renews cheaply with the same key.

React Native’s networking is native, so fetch, XMLHttpRequest and axios carry the TrustPin-Attestation header to the hosts you choose with nothing to change. Your backend verifies it and decides what an unattested request may do.

The SDK never blocks or fails your requests. When no token is available in a short, bounded time, the request goes out without the header.

Requirements

ItemRequirement
React Native0.85 or later (New Architecture only)
@trustpin/react-nativeSet up in your app, at the same version as @trustpin/react-native-attest
iOS15 or later, with the App Attest capability
AndroidAPI 25 or later, Kotlin 2.3.0 or later
VerifierDeployed, reachable over HTTPS, with a published policy
DevicesiOS: a device that supports App Attest. Android: hardware-backed key attestation, and Google Play only if your policy asks for Play Integrity

Expo Go cannot run attestation, it is native code: use a development build.

SDK and verifier compatibility

SDKVerifier
@trustpin/react-native-attest 7.1.0Contract version 2 or newer: verifier 1.0.0 or later

With an older verifier the state is failed with VERIFIER_INCOMPATIBLE and no header is sent until the verifier is upgraded. The app recovers on its own at the next attempt, with no update or reinstall.

Before you write code: the policy

Your verifier checks every attestation against a signed policy, which you manage on the project’s Attestation page in the TrustPin console . List each platform’s app: the Team ID and Bundle ID for iOS, the Package name and every Signing certificate SHA-256 for Android. The settings are described on Setting up apps and the policy, and the Android evidence options on the Android page.

  • iOS: the App Attest capability. Its entitlement is development for builds run from Xcode and production for TestFlight and the App Store. The Expo plugin adds it.
  • Android with Play Integrity: you need the number of the Google Cloud project linked to the app in Play Console.
  • Register your verifier’s domain in your TrustPin project. Calls to the verifier are pinned, and in strict mode an unregistered host is never reached.

Install

npm install @trustpin/react-native @trustpin/react-native-attest

Both packages must be on the same version. They bring the native 7.1.x modules with them.

Setup with Expo

List the attestation plugin with the pinning one:

{ "expo": { "plugins": [ ["@trustpin/react-native", { "organizationId": "…", "projectId": "…", "publicKey": "…" }], ["@trustpin/react-native-attest", { "verifierUrl": "https://attest.example.com", "attestedHosts": ["api.example.com"], "appAttestEnvironment": "development" }] ] } }

npx expo prebuild adds the attestation keys to the configuration files the pinning plugin writes, adds the App Attest entitlement, and starts attestation right after pinning on both platforms.

PropTypeNotes
verifierUrlstringYour verifier, https:// on a host name, without credentials, query or fragment. Set it with attestedHosts, or neither when your pinning configFiles already carry the keys
attestedHostsstring[]The hosts that receive the header: exact names, or *.example.com for exactly one label. No other host ever receives it
googleCloudProjectNumberstringAndroid, when the policy uses Play Integrity
appAttestEnvironmentdevelopment | productionThe iOS entitlement’s value. Defaults to production
logLevelnone | error | warning | info | debugPassed to the native init helper

Setup with bare React Native

1. Add the attestation keys to the files the pinning setup reads.

iOS, TrustPin-Info.plist:

<key>AttestVerifierURL</key> <string>https://attest.example.com</string> <key>AttestedHosts</key> <array> <string>api.example.com</string> </array>

Android, android/app/src/main/assets/trustpin.json:

"attest_verifier_url": "https://attest.example.com", "attested_hosts": ["api.example.com"]

Add "google_cloud_project_number" only when your policy uses Play Integrity: with a number, the app contacts Google Play services from setup on.

2. Start attestation right after pinning, in native code.

iOS, AppDelegate.swift:

import TrustPinReactNative import TrustPinReactNativeAttest TrustPinReactNative.start() TrustPinReactNativeAttest.start() // or: .start(logLevel: .debug)

Android, MainApplication.kt:

import cloud.trustpin.reactnative.TrustPinReactNative import cloud.trustpin.reactnative.attest.TrustPinReactNativeAttest override fun onCreate() { TrustPinReactNative.start(this) TrustPinReactNativeAttest.start(this) // or: .start(this, TrustPinAttestLogLevel.DEBUG) super.onCreate() loadReactNative(this) }

Then cd ios && pod install, and rebuild. Setup makes no verifier call: it validates the configuration and loads a device key from an earlier launch. A failure never crashes the app; it shows in the log and the state, and requests go out without the header.

Bind tokens to the user’s session

import TrustPinAttest from '@trustpin/react-native-attest'; await TrustPinAttest.bindToHeader('Authorization'); TrustPinAttest.prefetch();

Each token is then bound to the request’s Authorization value: only its SHA-256 travels, and a token taken from one session is useless with another. setTokenBinding(value) binds to a fixed value instead. A binding change renews the token with the existing device key; it is not a new attestation.

If your policy has Require token binding on, no token exists until a binding is known: the state is failed with BINDING_REQUIRED and no retry time. That is expected before sign-in.

Send the header

Nothing to change. fetch, XMLHttpRequest and axios requests to the attested hosts carry the header, added natively.

  • A header the request already carries is kept.
  • A redirect never carries the header to another host: each redirect gets the header for its own URL only.
  • For other HTTP clients, TrustPinAttest.attestationHeaders(url, { headers }) returns the headers to add.
  • For transports React Native’s networking doesn’t carry (gRPC, some WebSocket clients), send await TrustPinAttest.getToken() in the TrustPin-Attestation header.

Sensitive operations

For an operation that must come from this device, such as a payment, send an assertion next to the token. It binds the device’s attested key to a SHA-256 digest of your choice, typically the request body:

const body = JSON.stringify({ amount: '1.00', to: 'demo' }); const headers = { Authorization: `Bearer ${session}`, 'Content-Type': 'application/json' }; Object.assign(headers, await TrustPinAttest.assertionHeaders(sha256(body))); // 32 bytes await fetch(url, { method: 'POST', headers, body });

assertionHeaders takes the digest as 32 bytes (Uint8Array or ArrayBuffer) or 64 hex characters, and returns both headers: the token and TrustPin-Assertion. Each call produces a new assertion: one per operation. Your backend passes both headers and its own digest to your verifier. See Assertions for sensitive operations. React Native has no built-in digest API; use your crypto library of choice. Any other digest rejects with INVALID_DIGEST.

Verify the token in your API

The header carries a signed JWT issued by your verifier. Your API checks its signature against the verifier’s published keys, its type, issuer, audience, expiry and project, and its binding, and refuses debug tokens in production. See Verifying tokens in your API.

Simulators, emulators and development

Simulators and emulators cannot provide evidence your policy accepts. For development, your verifier’s operator can issue a debug token. The policy (Allow debug tokens) and the verifier must both allow it. Pass it from the build, never from a file:

  • iOS simulator: set TRUSTPIN_DEBUG_TOKEN in the Xcode scheme’s environment, or pass TrustPinReactNativeAttest.start(debugToken:) from a debug-only build setting. Ignored on devices.
  • Android: pass TrustPinReactNativeAttest.start(this, level, debugToken) from a debug-only BuildConfig field, as on the Android page. Used only in debuggable builds.

Debug tokens carry tp.env debug, which production APIs refuse.

State, errors and logging

const subscription = TrustPinAttest.onStateChange(state => { switch (state.status) { case 'unavailable': return console.log('No token yet'); case 'attesting': return console.log('Attesting…'); case 'ready': return console.log('Token ready until', new Date(state.expiresAtMs)); case 'failed': return console.log('Failed:', state.error.code, state.retryAtMs ?? 'after restart'); } });

onStateChange delivers the current state first, then every change. After a failure no timer runs: nothing is tried before retryAtMs, and the next request, getToken() or prefetch() after it starts a new attempt. A null retryAtMs means no new attempt in this launch, until reset() or a restart, except BINDING_REQUIRED, which resumes once a binding is set.

Errors are TrustPinAttestError with a stable code:

CodeMeaningWhat to do
INVALID_CONFIGURATIONA configuration value was rejected, or the file is missingFix the configuration
ALREADY_INITIALIZEDSetup ran twiceStart once, natively
NOT_SET_UPThe native init helper never ranAdd TrustPinReactNativeAttest.start(), or the Expo plugin
UNSUPPORTED_DEVICEApp Attest is unsupported on this device (iOS)Expected on some hardware
EVIDENCE_UNAVAILABLEThe device can’t produce the evidence the policy requiresReview Evidence, or configure the project number
BINDING_REQUIREDThe policy requires a binding and none is set yetExpected before sign-in
ATTESTATION_FAILEDThe verifier refused; see reasonsRead reasons, such as app_not_allowed
VERIFIER_UNAVAILABLEThe verifier couldn’t be reached, including when strict pinning refuses its hostCheck connectivity and the verifier. In strict mode, register its domain
VERIFIER_INCOMPATIBLEThe verifier is older than contract version 2Upgrade the verifier
INVALID_DIGESTassertionHeaders got a digest that isn’t 32 bytesHash the payload with SHA-256 first

New codes can appear in a minor release: handle unknown ones.

Logging

Logging is off by default: setLogLevel('debug') in development, and onLogEvent to route messages. Log lines never contain tokens, key certificates, bound values or debug tokens.

Events

TrustPinAttest.onEvent(listener) delivers events for your metrics: keys generated or replaced, attestations and renewals with their durations, refusals and waits with their end, unsupported devices, an unreachable verifier, and assertions. Events from before JavaScript was running replay to the first subscriber.

TrustPinAttest.onEvent(event => { if (event.type === 'attestationSucceeded') metrics.timing('attest', event.durationMs); });

Events never carry a token, a key or a device identifier. New event types can appear in a minor release: keep a default case.

Reset

TrustPinAttest.reset() discards the device key, so the next request attests a new key and the token’s sub changes. Use it for support flows, not routinely. It does nothing before setup.

Troubleshooting

SymptomLikely causeFix
Requests arrive without the headerThe host is not in the attested hosts, the URL is http://, the native init helper never ran, or there is no token yetCheck the host list and the native setup, then turn on debug logging
Every call rejects with NOT_SET_UPTrustPinReactNativeAttest.start() isn’t called, or the app wasn’t rebuiltAdd it after the pinning start (or the Expo plugin) and rebuild
ATTESTATION_FAILED with app_not_allowedThe app or its signing certificate is not in the policyAdd them. Android builds from Play use the Play app signing key
Failed with VERIFIER_UNAVAILABLE every timeThe verifier’s domain is not registered, the verifier is down, or its URL is wrongRegister the domain, then check the URL and the verifier

Privacy

Evidence and tokens go only to your own verifier, nothing to TrustPin. Device keys and key certificates are never backed up or synced: a restored app attests again. On iOS the native framework ships a privacy manifest; on Android, with a Google Cloud project number configured, declare your use of the Play Integrity API in Play Console’s Data safety form.

Next steps