In a React Native payments app, how do you implement public-key pinning for the API on iOS and Android, and why ship a backup pin?
answer
- pin the key hash, not the file
- Android: pin-set with SHA-256 pins
- iOS 14+: NSPinnedDomains in Info.plist
- or OkHttpClientProvider with CertificatePinner
- backup key pinned before it is used
basics
~20 sPin the SHA-256 hash of the API's public key in native config: a pin-set in Android's network security config and NSPinnedDomains in iOS's Info.plist, or a pinning module. Always include a backup key's pin so rotation needs no emergency release.
solid answer
~40 sPinning has to happen in the native networking layer, because `fetch` is only a JavaScript facade over `NSURLSession` and OkHttp. On **Android**, add a `pin-set` to the network security config for the API domain, with `<pin digest="SHA-256">` entries holding base64 SPKI hashes; alternatively, register a factory with React Native's `OkHttpClientProvider.setOkHttpClientFactory` that adds OkHttp's `CertificatePinner`. On **iOS 14 and later**, declare `NSPinnedDomains` under `NSAppTransportSecurity`, with `NSPinnedLeafIdentities` or `NSPinnedCAIdentities` entries carrying `SPKI-SHA256-BASE64` values. Pin the **public key** rather than the certificate so a renewal with the same key keeps working, and ship at least one **backup pin** for a key you hold offline, so a compromised or lost key can be replaced without locking users out.
code
kotlin · 19 linesimport com.facebook.react.modules.network.OkHttpClientFactory
import com.facebook.react.modules.network.OkHttpClientProvider
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
class PinnedClientFactory : OkHttpClientFactory {
override fun createNewNetworkModuleClient(): OkHttpClient {
val pinner = CertificatePinner.Builder()
.add("api.payments.example", "sha256/CURRENT_SPKI_HASH_BASE64=")
.add("api.payments.example", "sha256/BACKUP_SPKI_HASH_BASE64=")
.build()
return OkHttpClientProvider.createClientBuilder()
.certificatePinner(pinner)
.build()
}
}
// In MainApplication.onCreate(), before React Native starts:
// OkHttpClientProvider.setOkHttpClientFactory(PinnedClientFactory())go deeper
Remember that pinning is configured natively, per platform, and that a pin is a hash of a public key.
Explain Android's pin-set, iOS's NSPinnedDomains and React Native's OkHttpClientProvider hook, and what each covers.
Choose what to pin, leaf or intermediate key, always with a backup pin, scoped to your own API domains and testable in debug builds.
Tie the pin strategy to key-management practice: who generates and stores backup keys, and how pin choices constrain CA migrations.
## Why pinning lives in native code Ordinary HTTPS trusts any certificate that chains to one of the device's trusted certificate authorities. **Pinning** adds a second condition: the connection is accepted only if the server's chain contains a public key the app already knows. The React Native security guide recommends it for exactly the case where a malicious CA has been installed on the device. In React Native, `fetch` and `XMLHttpRequest` are JavaScript facades over the native networking module, which uses `NSURLSession` on iOS and OkHttp on Android. JavaScript never sees the TLS handshake, so pins must be enforced natively: by platform configuration, by a hook in React Native's networking setup, or by a pinning native module. ## Android options 1. **Network security config.** In `res/xml/network_security_config.xml`, add a `domain-config` for the API host containing a `pin-set`. Each `<pin digest="SHA-256">` holds the base64 SHA-256 hash of a certificate's SubjectPublicKeyInfo. A `pin-set` may carry an `expiration` date after which pins are no longer enforced. The platform checks the pins after normal chain validation, and OkHttp honours it, so `fetch` is covered. 2. **A custom OkHttp client.** React Native exposes `OkHttpClientProvider.setOkHttpClientFactory(factory)`. A factory implementing `OkHttpClientFactory.createNewNetworkModuleClient()` can start from `OkHttpClientProvider.createClientBuilder()` and add OkHttp's `CertificatePinner`. Register it in `MainApplication` before any request is made. ## iOS options 1. **`NSPinnedDomains`** (iOS 14 and later) in `Info.plist` under `NSAppTransportSecurity`. For each domain, list `NSPinnedLeafIdentities` (pins on the server's own key) or `NSPinnedCAIdentities` (pins on an issuing CA's key), each an array of dictionaries with an `SPKI-SHA256-BASE64` value; `NSIncludesSubdomains` extends a domain entry. Because React Native uses `NSURLSession`, the system enforces these for `fetch`. 2. **A pinning native module** that implements the check in a URL session delegate, for teams that need logic the plist cannot express. ## What to pin | Choice | Survives a certificate renewal with the same key | Survives a key change | Scope of trust | |---|---|---|---| | Whole leaf certificate | no | no | one certificate | | Leaf public key (SPKI hash) | yes | only if a backup pin matches | your server's key | | Intermediate or CA public key | yes | yes, while the CA key is unchanged | every certificate that CA issues | For a payments API that you control, a **leaf or intermediate public-key pin plus a backup** is the usual balance. Pinning a CA you do not control ties your uptime to its issuance changes. ## The backup pin A **backup pin** is the hash of a key pair that is generated in advance, kept offline and not yet in use. Shipping it means the server can switch to that key at any time, for example after the current key is compromised, and every installed app still connects. Without it, a forced key change breaks every installed version until users update from the store. ## Proving the pins work A pin that is never tested may be silently ignored or silently wrong. Before release: - Point a debug build at an interception proxy **without** the debug-only trust and confirm the API calls fail. - Serve the API on staging with a certificate whose key is **not** pinned and confirm a release build refuses it. - Serve staging with the **backup** key and confirm the release build still connects, which proves the backup pin is correct. - Compute pins from the actual certificates with a scripted step in the release process rather than pasting hashes by hand. ## Checklist - Pin in native configuration or code, never "in JavaScript". - At least two pins per domain: current plus backup. - Scope pins to your own API domains; third-party SDK endpoints change keys on their own schedule. - Keep debug builds inspectable through debug-only trust rather than by removing pins in code.
- Why can't you implement pinning in JavaScript on top of `fetch`?`fetch` never exposes the server's certificate chain to JavaScript; the TLS handshake happens inside `NSURLSession` or OkHttp and is finished before a response object exists. The check has to run natively, through platform config, React Native's OkHttp factory hook, or a native module.
- What does pinning an intermediate CA key trade away compared with the leaf key?It survives changes of your own server key, since any certificate that CA issues still matches, but it trusts every certificate that CA issues for your domain. It also breaks if you or your provider move to a different issuing CA without shipping the new pin first.
saying these in an interview costs you the question
- Pinning can be done by checking the certificate from JavaScript.
- Pinning the whole certificate survives a routine renewal.
- One pin is enough if it matches the current certificate.
- iOS pinning always requires a third-party module.
- Pins should cover every third-party SDK domain too.