expo-ssl-pinning
v0.1.6
Published
SSL certificate pinning for Expo applications. Protect against MITM attacks by validating server certificates against known public key hashes.
Maintainers
Readme
expo-ssl-pinning
SSL Certificate/Public Key Pinning for Expo applications using Expo Native Modules.
Overview
This package provides SSL certificate pinning functionality for Expo apps to protect against Man-in-the-Middle (MITM) attacks by validating server certificates against known public key hashes.
Features
- ✅ iOS Support - SPKI hash validation via URLSession
- ✅ Android Support - Certificate pinning via OkHttp
- ✅ Config Plugin - Automatic build-time configuration injection
- ✅ Multiple Hosts - Configure pinning for multiple domains
- ✅ Backup Keys - Support for multiple hashes per host (recommended for certificate rotation)
- ✅ Zero Runtime Configuration - All configuration happens at build time
Installation
npm install expo-ssl-pinning
# or
yarn add expo-ssl-pinningPrerequisites
- Expo SDK 48+ (recommended)
- EAS Build or Expo Bare workflow
- Access to native iOS and Android code
Usage
Step 1: Generate SPKI Hashes
You need to generate the SPKI (Subject Public Key Info) hash for your server's certificate.
Option A: If you have the certificate file
openssl x509 -in cert.pem -pubkey -noout | \
openssl pkey -pubin -outform DER | \
openssl dgst -sha256 -binary | \
base64Option B: If you only have the domain
openssl s_client -connect api.yourdomain.com:443 -servername api.yourdomain.com </dev/null | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform DER | \
openssl dgst -sha256 -binary | \
base64The output will be a base64-encoded string like: 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
Important: Always generate and pin at least 2 hashes:
- Your current certificate's hash (primary)
- Your backup/next certificate's hash (backup)
This prevents app lockout during certificate rotation.
Step 2: Configure in app.config.js
Add the plugin to your app.config.js or app.json:
export default {
expo: {
// ... other config
plugins: [
[
"expo-ssl-pinning",
{
sslPinning: {
hosts: {
"api.yourdomain.com": [
"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", // Primary certificate hash
"bXNvZGF0YQpkYXRhCmRhdGEK47DEQpj8HBSa5JCeuQeR=", // Backup certificate hash
],
"api2.yourdomain.com": [
"aW5jbHVkZQpkYXRhCmRhdGEK47DEQpj8HBSa5JCeuQeR=",
"ZGF0YQpkYXRhCmRhdGEK47DEQpj8HBSa5JCeuQeRkm5N=",
],
},
},
},
],
],
},
};Step 3: Build
# For EAS Build
eas build --platform ios
eas build --platform android
# For Expo Prebuild
expo prebuildConfiguration
Plugin Options
{
sslPinning: {
hosts: {
[hostname: string]: string[] // Array of base64-encoded SHA-256 SPKI hashes
}
}
}Example Configuration
{
sslPinning: {
hosts: {
// Production API
"api.production.com": [
"currentCertHash==",
"backupCertHash=="
],
// Staging API
"api.staging.com": [
"stagingCertHash==",
"stagingBackupHash=="
]
}
}
}How It Works
Build Time
- The config plugin reads your SSL pinning configuration from
app.config.js - iOS: Injects the configuration into
Info.plist - Android: Injects the configuration into
AndroidManifest.xmlas meta-data - Native modules read this configuration on app startup
Runtime
- iOS: Uses
URLSessiondelegate to intercept certificate validation - Android: Uses OkHttp
CertificatePinnerto validate certificates - When your app makes HTTPS requests, the native layer validates the server's public key hash
- If the hash matches any configured hash for that host, the connection proceeds
- If no match is found, the connection is rejected
API Reference
JavaScript Methods
import * as ExpoSslPinning from "expo-ssl-pinning";
// Check if SSL pinning is enabled
const isEnabled = ExpoSslPinning.isPinningEnabled();
// Get list of pinned hosts (iOS only)
const hosts = ExpoSslPinning.getPinnedHosts();
// Reinitialize pinning (rarely needed)
await ExpoSslPinning.reinitializePinning();Best Practices
✅ DO
- Always pin at least 2 hashes per host (primary + backup)
- Pin only your own backend domains that you control
- Test pinning in staging before production
- Monitor certificate expiration dates
- Have a certificate rotation plan
- Update backup hash before primary certificate expires
❌ DON'T
- Never pin third-party services (Firebase, Google, AWS, etc.)
- They rotate certificates frequently
- You have no control over their certificate lifecycle
- Your app will break when they update certificates
- Don't pin only one hash - you'll lock users out during rotation
- Don't pin in development - use staging/production only
- Don't forget to update backup hashes before certificate expiry
Troubleshooting
Certificate Validation Failures
Symptoms: Network requests fail with SSL errors
Solutions:
- Verify your SPKI hash is correct - regenerate it
- Check that the hostname in your config exactly matches the request URL
- Ensure you're using the latest certificate (not an expired one)
- Check logs:
- iOS: Look for "ExpoSslPinning" logs in Xcode console
- Android: Check Logcat for "ExpoSslPinning" tags
Plugin Not Working
Symptoms: No SSL pinning happening (no errors, no validation)
Solutions:
- Run
expo prebuild --cleanto regenerate native projects - Verify
plugin.jsexists in the module directory - Check that
expo-module.config.jsonincludes the plugin reference - Confirm
@expo/config-pluginsis installed
Hash Mismatch
Symptoms: All requests to a specific host fail
Solutions:
- Regenerate the SPKI hash using the commands above
- Make sure you're hashing the public key, not the certificate
- Verify you're using base64 encoding (not hex)
- Test with a tool like
openssl s_clientto ensure the server is using the expected certificate
Certificate Rotation Strategy
- Get new certificate from your CA
- Generate SPKI hash for the new certificate
- Add new hash as backup in your app config
- Release app update with both old and new hashes
- Wait for users to update (2-4 weeks recommended)
- Install new certificate on your server
- Remove old hash in next app version (optional)
Examples
Example: Development vs Production
const sslConfig = {
sslPinning: {
hosts: {},
},
};
// Only enable SSL pinning in production builds
if (process.env.APP_ENV === "production") {
sslConfig.sslPinning.hosts["api.production.com"] = [
process.env.SSL_HASH_PRIMARY,
process.env.SSL_HASH_BACKUP,
];
}
export default {
expo: {
plugins: [["expo-ssl-pinning", sslConfig]],
},
};Example: Multiple Environments
const API_CONFIGS = {
production: {
host: "api.production.com",
hashes: ["prodHash1==", "prodHash2=="],
},
staging: {
host: "api.staging.com",
hashes: ["stagingHash1==", "stagingHash2=="],
},
};
const currentEnv = process.env.APP_ENV || "staging";
const config = API_CONFIGS[currentEnv];
export default {
expo: {
plugins: [
[
"expo-ssl-pinning",
{
sslPinning: {
hosts: {
[config.host]: config.hashes,
},
},
},
],
],
},
};Security Considerations
- This is not a silver bullet - SSL pinning is one layer of defense
- Combine with other security measures (e.g., API authentication, encryption)
- Attackers with physical device access can potentially bypass pinning (jailbreak/root)
- Certificate transparency - your pins are visible in the app binary
- Keep certificates secure - protect your private keys
Requirements
- Expo SDK 48+
- React Native 0.71+
- iOS 13.0+
- Android 6.0+ (API level 23+)
Contributing
Contributions are welcome! Please open an issue or submit a PR.
License
MIT
Author
Eric Mensah (@teincsolutions)
Support
For issues and questions:
- Open an issue: https://github.com/teincsolutions/expo-ssl-pinning/issues
- Email: [email protected]
Changelog
0.1.0 (Initial Release)
- iOS SSL pinning via URLSession
- Android SSL pinning via OkHttp
- Config plugin for automatic configuration
- Support for multiple hosts and backup keys
