π Production-ready Mutual TLS (mTLS) client certificate authentication for Expo/React Native applications
This Expo module provides secure, hardware-backed mTLS client certificate authentication for mobile applications. It supports both P12 (PKCS#12) and PEM certificate formats with enterprise-grade security features.
- π Hardware-backed Security: iOS Keychain & Android Keystore integration
- π± Cross-platform: Native iOS (Swift) and Android (Kotlin) implementations
- π― Simple API: Easy-to-use utility functions for common operations
- π Multiple Formats: Support for P12/PKCS#12 and PEM certificate formats
- π Biometric Auth: Optional biometric/device credential requirements
- π Rich Events: Debug logging, error handling, and certificate expiry warnings
- β‘ Performance: Optimized for production workloads
- π‘οΈ Enterprise Ready: Comprehensive certificate validation and security
npx expo install '@a-cube-io/expo-mutual-tls'import ExpoMutualTls from '@a-cube-io/expo-mutual-tls';
// Configure for P12 certificates
await ExpoMutualTls.configureP12('my-keychain-service', true);
// Store P12 certificate
await ExpoMutualTls.storeP12(p12Base64Data, 'certificate-password');
// Make authenticated mTLS request
const response = await ExpoMutualTls.request('https://api.example.com/secure', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: 'example' })
});Configure the module for P12/PKCS#12 certificate format.
const result = await ExpoMutualTls.configureP12(
'my-p12-service', // Optional: keychain service name (default: 'client.p12')
true // Optional: enable debug logging (default: false)
);Configure the module for PEM certificate format.
const result = await ExpoMutualTls.configurePEM(
'cert-service', // Optional: certificate service name
'key-service', // Optional: private key service name
true // Optional: enable debug logging
);Store a P12/PKCS#12 certificate in secure storage.
await ExpoMutualTls.storeP12(
'MIIKXgIBAzCCCh...', // Base64-encoded P12 data
'my-certificate-password'
);Store PEM certificate and private key in secure storage.
await ExpoMutualTls.storePEM(
'-----BEGIN CERTIFICATE-----\n...', // PEM certificate
'-----BEGIN PRIVATE KEY-----\n...', // PEM private key
'optional-passphrase' // Optional: passphrase for encrypted key
);Check if certificates are stored.
const hasStoredCert = await ExpoMutualTls.hasCertificate();Remove stored certificates from secure storage.
await ExpoMutualTls.removeCertificate();Make an authenticated mTLS request.
const result = await ExpoMutualTls.request('https://api.example.com', {
method: 'POST',
headers: { 'Authorization': 'Bearer token' },
body: JSON.stringify({ key: 'value' })
});
console.log('Status:', result.statusCode);
console.log('TLS Version:', result.tlsVersion);
console.log('Response:', result.body);Test mTLS connection to a URL (HEAD request).
const result = await ExpoMutualTls.testConnection('https://secure-api.example.com');Check if the module is configured.
if (ExpoMutualTls.isConfigured) {
// Module is ready for certificate operations
}Get the current module state.
console.log('Current state:', ExpoMutualTls.currentState);
// Possible values: 'notConfigured', 'configured', 'error'The module provides comprehensive event utilities for monitoring mTLS operations, debugging, and certificate lifecycle management.
Listen for debug log events including network requests, certificate operations, and system information.
const debugSubscription = ExpoMutualTls.onDebugLog(event => {
console.log(`[${event.type}] ${event.message}`);
// Access additional event data
if (event.method) console.log('HTTP Method:', event.method);
if (event.url) console.log('Request URL:', event.url);
if (event.statusCode) console.log('Status Code:', event.statusCode);
if (event.duration) console.log('Duration:', event.duration + 'ms');
});
// Remember to remove the listener when done
debugSubscription.remove();Event Types:
certificate_storage- Certificate store/retrieve operationsnetwork_request- HTTP/HTTPS requestskeychain_operation- Keychain access operationstls_handshake- TLS/SSL handshake information
Listen for error events from all module operations.
const errorSubscription = ExpoMutualTls.onError(event => {
console.error('mTLS Error:', event.message);
// Handle specific error codes
if (event.code) {
switch (event.code) {
case 'CERTIFICATE_NOT_FOUND':
console.log('Action: Store a certificate first');
break;
case 'SSL_HANDSHAKE_FAILED':
console.log('Action: Check certificate validity');
break;
case 'KEYCHAIN_ACCESS_DENIED':
console.log('Action: Check app permissions');
break;
default:
console.error('Error Code:', event.code);
}
}
});
// Remove listener when done
errorSubscription.remove();Listen for certificate expiry warnings and notifications.
const expirySubscription = ExpoMutualTls.onCertificateExpiry(event => {
const expiryDate = new Date(event.expiry);
console.warn('Certificate Expiry Warning:');
console.warn('Subject:', event.subject);
console.warn('Expires:', expiryDate.toLocaleDateString());
if (event.alias) {
console.warn('Alias:', event.alias);
}
if (event.warning) {
console.warn('β οΈ Certificate expires soon!');
}
// Calculate days until expiry
const daysUntilExpiry = Math.ceil((event.expiry - Date.now()) / (1000 * 60 * 60 * 24));
console.warn(`Days until expiry: ${daysUntilExpiry}`);
});
// Remove listener when done
expirySubscription.remove();Remove all active event listeners at once.
// Remove all event listeners
ExpoMutualTls.removeAllListeners();import { useEffect } from 'react';
import ExpoMutualTls from '@a-cube-io/expo-mutual-tls';
export default function MyComponent() {
useEffect(() => {
// Set up all event listeners
const debugSubscription = ExpoMutualTls.onDebugLog((event) => {
const message = event.message || '';
const method = event.method ? ` [${event.method}]` : '';
const url = event.url ? ` ${event.url}` : '';
const statusCode = event.statusCode ? ` (${event.statusCode})` : '';
const duration = event.duration ? ` ${event.duration}ms` : '';
console.log(`π Debug [${event.type}]: ${message}${method}${url}${statusCode}${duration}`);
});
const errorSubscription = ExpoMutualTls.onError((event) => {
const code = event.code ? ` [${event.code}]` : '';
console.error(`β Error: ${event.message}${code}`);
// Show user-friendly error messages
if (event.code === 'CERTIFICATE_NOT_FOUND') {
alert('Please store a certificate first');
}
});
const expirySubscription = ExpoMutualTls.onCertificateExpiry((event) => {
const expiryDate = new Date(event.expiry).toLocaleDateString();
const alias = event.alias ? ` (${event.alias})` : '';
const warning = event.warning ? ' β οΈ' : '';
console.warn(`π
Certificate Expiry${warning}: ${event.subject}${alias} - expires ${expiryDate}`);
if (event.warning) {
alert(`Certificate expiring soon: ${event.subject}`);
}
});
// Cleanup all listeners on unmount
return () => {
debugSubscription.remove();
errorSubscription.remove();
expirySubscription.remove();
};
}, []);
// Component JSX...
}For advanced use cases, you can use the raw module interface:
import { ExpoMutualTlsModuleRaw, MutualTlsConfig } from '@a-cube-io/expo-mutual-tls';
const config: MutualTlsConfig = {
certificateFormat: 'p12',
keychainServiceForP12: 'custom.p12.service',
keychainServiceForPassword: 'custom.password.service',
enableLogging: true,
requireUserAuthentication: true, // Require biometric/device auth
userAuthValiditySeconds: 300, // Auth validity duration
expiryWarningDays: 30 // Days before expiry to warn
};
const result = await ExpoMutualTlsModuleRaw.configure(config);Enable biometric or device credential authentication:
const config: MutualTlsConfig = {
certificateFormat: 'p12',
requireUserAuthentication: true,
userAuthValiditySeconds: 300, // 5 minutes
// ... other options
};The module performs comprehensive certificate validation:
- β Certificate expiry checking
- β Extended Key Usage (EKU) validation for client authentication
- β Private key/certificate pairing verification
- β Certificate chain validation
- β Hardware-backed key storage
- Security Framework: Uses iOS Security Framework APIs
- Keychain Integration: Secure keychain storage with hardware backing
- Certificate Parsing: Native PEM and P12 parsing
- TLS Integration: URLSession with custom SSL context
- Android Keystore: Hardware-backed key storage when available
- BouncyCastle: PEM certificate parsing and cryptographic operations
- OkHttp Integration: mTLS-enabled HTTP client
- Biometric Support: Android Biometric API integration
The module provides detailed error information:
try {
await ExpoMutualTls.request('https://api.example.com');
} catch (error) {
console.error('Request failed:', error.message);
// Handle specific error types
if (error.code === 'CERTIFICATE_NOT_FOUND') {
// Certificate is not stored
} else if (error.code === 'SSL_HANDSHAKE_FAILED') {
// mTLS handshake failed
}
}| Code | Description | Solution |
|---|---|---|
NOT_CONFIGURED |
Module not configured | Call configure method first |
CERTIFICATE_NOT_FOUND |
No certificate stored | Store certificate before making requests |
INVALID_CERTIFICATE_FORMAT |
Certificate format invalid | Verify certificate data and format |
SSL_HANDSHAKE_FAILED |
mTLS handshake failed | Check certificate validity and server configuration |
KEYCHAIN_ACCESS_DENIED |
Keychain access denied | Check app permissions or retry with authentication |
import React, { useEffect, useState } from 'react';
import ExpoMutualTls from '@a-cube-io/expo-mutual-tls';
import { Asset } from 'expo-asset';
import * as FileSystem from 'expo-file-system';
export default function App() {
const [logs, setLogs] = useState<string[]>([]);
const [status, setStatus] = useState('Ready');
const addLog = (message: string) => {
const timestamp = new Date().toLocaleTimeString();
setLogs(prev => [`[${timestamp}] ${message}`, ...prev.slice(0, 19)]);
};
// Comprehensive event listeners setup
useEffect(() => {
// Debug logging with detailed information
const debugSubscription = ExpoMutualTls.onDebugLog((event) => {
const message = event.message || '';
const method = event.method ? ` [${event.method}]` : '';
const url = event.url ? ` ${event.url}` : '';
const statusCode = event.statusCode ? ` (${event.statusCode})` : '';
const duration = event.duration ? ` ${event.duration}ms` : '';
addLog(`π Debug [${event.type}]: ${message}${method}${url}${statusCode}${duration}`);
console.log(`Debug [${event.type}]:`, message, {
method: event.method,
url: event.url,
statusCode: event.statusCode,
duration: event.duration
});
});
// Error handling with user-friendly messages
const errorSubscription = ExpoMutualTls.onError((event) => {
const code = event.code ? ` [${event.code}]` : '';
addLog(`β Error: ${event.message}${code}`);
console.error('mTLS Error:', event.message, event.code ? `Code: ${event.code}` : '');
// Provide user guidance based on error codes
if (event.code === 'CERTIFICATE_NOT_FOUND') {
setStatus('Please store a certificate first');
} else if (event.code === 'SSL_HANDSHAKE_FAILED') {
setStatus('Certificate validation failed');
}
});
// Certificate expiry monitoring
const expirySubscription = ExpoMutualTls.onCertificateExpiry((event) => {
const expiryDate = new Date(event.expiry).toLocaleDateString();
const alias = event.alias ? ` (${event.alias})` : '';
const warning = event.warning ? ' β οΈ' : '';
addLog(`π
Certificate Expiry${warning}: ${event.subject}${alias} - expires ${expiryDate}`);
console.warn('Certificate expiry warning:', {
subject: event.subject,
alias: event.alias,
expiry: expiryDate,
warning: event.warning
});
if (event.warning) {
setStatus(`Certificate expires soon: ${event.subject}`);
}
});
// Cleanup listeners on unmount
return () => {
debugSubscription.remove();
errorSubscription.remove();
expirySubscription.remove();
};
}, []);
const setupP12Certificate = async () => {
try {
setStatus('Setting up P12 certificate...');
// Configure for P12 with logging enabled
await ExpoMutualTls.configureP12('demo-service', true);
addLog('β
P12 configuration completed');
// Load P12 certificate from assets
const [asset] = await Asset.loadAsync(require('./assets/client.p12'));
const p12Data = await FileSystem.readAsStringAsync(asset.localUri!, {
encoding: FileSystem.EncodingType.Base64,
});
// Store certificate
await ExpoMutualTls.storeP12(p12Data, 'certificate-password');
addLog('β
P12 certificate stored successfully');
// Test connection
const result = await ExpoMutualTls.request('https://secure-api.example.com', {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (result.success) {
addLog(`β
Connection successful! Status: ${result.statusCode}, TLS: ${result.tlsVersion}`);
setStatus(`Connected successfully (${result.statusCode})`);
} else {
addLog('β Connection failed');
setStatus('Connection failed');
}
} catch (error) {
addLog(`β Setup failed: ${error}`);
setStatus('Setup failed');
console.error('Setup failed:', error);
}
};
return (
<div>
<h1>mTLS P12 Demo</h1>
<p>Status: {status}</p>
<button onClick={setupP12Certificate}>Setup P12 Certificate</button>
<h2>Activity Logs</h2>
<div style={{ height: '200px', overflow: 'auto', border: '1px solid #ccc' }}>
{logs.map((log, index) => (
<div key={index} style={{ fontSize: '12px', fontFamily: 'monospace' }}>
{log}
</div>
))}
</div>
<button onClick={() => ExpoMutualTls.removeAllListeners()}>
Clear All Event Listeners
</button>
</div>
);
}import React, { useEffect } from 'react';
import ExpoMutualTls from '@a-cube-io/expo-mutual-tls';
import { Asset } from 'expo-asset';
import * as FileSystem from 'expo-file-system';
const PEMCertificateDemo = () => {
useEffect(() => {
// Set up comprehensive event monitoring
const debugSubscription = ExpoMutualTls.onDebugLog((event) => {
console.log(`π [${event.type}] ${event.message}`);
if (event.url) console.log(` URL: ${event.url}`);
if (event.duration) console.log(` Duration: ${event.duration}ms`);
});
const errorSubscription = ExpoMutualTls.onError((event) => {
console.error(`β mTLS Error: ${event.message}`);
if (event.code) console.error(` Code: ${event.code}`);
});
const expirySubscription = ExpoMutualTls.onCertificateExpiry((event) => {
console.warn(`π
Certificate "${event.subject}" expires on ${new Date(event.expiry).toLocaleDateString()}`);
});
return () => {
debugSubscription.remove();
errorSubscription.remove();
expirySubscription.remove();
};
}, []);
const setupPEMCertificates = async () => {
try {
// Configure for PEM with debug logging
console.log('Configuring PEM certificate format...');
await ExpoMutualTls.configurePEM('cert-service', 'key-service', true);
// Load PEM files from assets
console.log('Loading PEM certificate files...');
const [certAsset, keyAsset] = await Asset.loadAsync([
require('./assets/client.pem'),
require('./assets/client.key')
]);
const certificate = await FileSystem.readAsStringAsync(certAsset.localUri!);
const privateKey = await FileSystem.readAsStringAsync(keyAsset.localUri!);
// Store certificates
console.log('Storing PEM certificates...');
await ExpoMutualTls.storePEM(certificate, privateKey);
// Verify certificates are stored
const hasCerts = await ExpoMutualTls.hasCertificate();
console.log('Certificate verification:', hasCerts ? 'β
Present' : 'β Missing');
if (hasCerts) {
// Make authenticated request
console.log('Making authenticated mTLS request...');
const response = await ExpoMutualTls.request('https://api.example.com/data', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ action: 'getData', timestamp: Date.now() })
});
if (response.success) {
console.log('β
API Request successful!');
console.log(` Status: ${response.statusCode} ${response.statusMessage}`);
console.log(` TLS Version: ${response.tlsVersion}`);
console.log(` Cipher Suite: ${response.cipherSuite}`);
console.log(' Response:', JSON.parse(response.body));
} else {
console.log('β API Request failed');
}
}
} catch (error) {
console.error('β PEM setup failed:', error);
// Handle specific error scenarios
if (error.code === 'INVALID_CERTIFICATE_FORMAT') {
console.error(' Solution: Check PEM file format and encoding');
} else if (error.code === 'KEYCHAIN_ACCESS_DENIED') {
console.error(' Solution: Check app keychain permissions');
}
}
};
return (
<div>
<h1>mTLS PEM Demo</h1>
<button onClick={setupPEMCertificates}>
Setup PEM Certificates & Test
</button>
</div>
);
};
export default PEMCertificateDemo;iOS Build Errors:
- Ensure iOS deployment target is 11.0 or higher
- Add required iOS frameworks in your app configuration
Android Build Errors:
- Verify Android API level 24 (Android 7.0) or higher
- Ensure BouncyCastle dependencies are properly resolved
Certificate Issues:
- Verify certificate format and encoding
- Check certificate expiry dates
- Ensure a private key matches a certificate public key
Network Issues:
- Verify server supports mTLS client certificate authentication
- Check server certificate authority trust chain
- Ensure proper network connectivity
Enable comprehensive logging:
// Enable debug logging during configuration
await ExpoMutualTls.configureP12('service', true);
// Listen for debug events
ExpoMutualTls.onDebugLog(event => {
console.log(`[${event.type}] ${event.message}`);
if (event.url) console.log(`URL: ${event.url}`);
if (event.statusCode) console.log(`Status: ${event.statusCode}`);
if (event.duration) console.log(`Duration: ${event.duration}ms`);
});- Certificates are stored in hardware-backed secure storage when available
- iOS: Uses iOS Keychain with hardware encryption
- Android: Uses Android Keystore with hardware security module (HSM)
- Enable biometric authentication for sensitive applications
- Use short authentication validity periods
- Implement certificate rotation procedures
- Monitor certificate expiry dates
- Validate server certificates properly
- Supports enterprise security requirements
- Hardware-backed cryptographic operations
- Audit-friendly debug logging
- Secure credential lifecycle management
The v0.1.x release introduces simplified utility functions:
Before (v0.0.x):
import ExpoMutualTlsModule, { MutualTlsConfig } from '@a-cube-io/expo-mutual-tls';
const config: MutualTlsConfig = {
certificateFormat: 'p12',
keychainServiceForP12: 'service',
enableLogging: true
};
await ExpoMutualTlsModule.configure(config);After (v0.1.x):
import ExpoMutualTls from '@a-cube-io/expo-mutual-tls';
await ExpoMutualTls.configureP12('service', true);The raw module interface is still available for advanced use cases via ExpoMutualTlsModuleRaw.
Contributions are very welcome! Please refer to guidelines described in the contributing guide.
- Clone the repository
- Install dependencies:
npm install - Build the module:
npm run build - Run example app:
cd example && npx expo run:ios
MIT License - see LICENSE file for details.
- π GitHub Issues
- π Documentation
Made with β€οΈ for secure mobile applications