π 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