Title: TLS Check Fails on iPhone for React Native Expo App
Body:
Hello Stack Overflow Community,
I am developing a React Native Expo app that needs to configure a device with WiFi. The app works fine on Android after implementing the following network_security_config.xml
:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<base-config>
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config>
<domain includeSubdomains="true">192.168.x.x</domain>
<trust-anchors>
<certificates src="@raw/mycert"
tools:ignore="NetworkSecurityConfig" />
</trust-anchors>
</domain-config>
</network-security-config>
However, I am facing issues with iOS. I have tried using the react-native-ssl-pinning
library both with and without my cert.pem
file, but the TLS check always fails.
Additionally, I attempted to write Swift code to bypass the SSL check and use the certificate, but it also fails.
Here is the error message I received using openssl
:
SSL handshake has read 601 bytes and written 465 bytes
Verification error: EE certificate key too weak
The certificate is 512 bits, which seems to be weak.
Questions:
- Is the weak certificate the primary reason for the TLS failure on iOS?
- How can I use this certificate or bypass this issue temporarily while developing?
- Are there any best practices for handling such situations in React Native Expo for iOS?
Thank you in advance for any suggestions or guidance!
iOS Network Configuration Issue: Certificate Invalid Error [-1202]
Application requires making network requests to a local server (192.168.x.x). I have configured my Info.plist
to allow for insecure HTTP loads and added an exception domain for the local IP. Here is the configuration:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>192.168.x.x</key>
<dict>
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
Swift Code Using Alamofire
I tried using Alamofire to make requests and have bypassed SSL checks with a custom certificate ServerTrustEvaluator
:
import Foundation
import Alamofire
class DisabledTrustEvaluator: ServerTrustEvaluating {
func evaluate(_ trust: SecTrust, forHost host: String) throws {
// Bypass all trust checks
print("SSL trust evaluation bypassed for host: (host)")
}
}
@objc(ConfigureMonidrop)
class ConfigureMonidrop: NSObject {
private var session: Session!
@objc static func requiresMainQueueSetup() -> Bool {
return false
}
override init() {
super.init()
// Configure Alamofire with a custom ServerTrustManager to bypass SSL checks
let serverTrustManager = ServerTrustManager(evaluators: [
"192.168.x.x": DisabledTrustEvaluator()
])
session = Session(serverTrustManager: serverTrustManager)
}
@objc(makeRequest:withResolver:withRejecter:)
func makeRequest(urlString: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
guard let url = URL(string: urlString) else {
reject("INVALID_URL", "Invalid URL", NSError(domain: "", code: -1, userInfo: nil))
return
}
session.request(urlString).validate().responseString { response in
switch response.result {
case .success(let responseData):
resolve(["ok": true, "status": 200, "data": responseData])
case .failure(let error):
print("Request failed with error: (error)")
if let data = response.data, let responseString = String(data: data, encoding: .utf8) {
print("Response data: (responseString)")
}
if let httpResponse = response.response {
resolve(["ok": false, "status": httpResponse.statusCode, "error": error.localizedDescription])
} else {
reject("REQUEST_FAILED", "Request failed: (error.localizedDescription)", error as NSError)
}
}
}
}
@objc
static func moduleName() -> String! {
return "ConfigureMonidrop"
}
}
Swift Code Using URLSession and Certificate Pinning
Alternatively, I attempted certificate pinning with the following approach:
// MySessionDelegate.swift
import Foundation
class MySessionDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if let serverTrust = challenge.protectionSpace.serverTrust {
let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)
// Load the local certificate
if let pathToCert = Bundle.main.path(forResource: "mycert", ofType: "pem"),
let localCertificateData = NSData(contentsOfFile: pathToCert) {
let certificateData = SecCertificateCopyData(certificate!) as Data
// Check if the server's certificate matches the local one
if certificateData == localCertificateData as Data {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
return
}
}
}
// The server's certificate didn't match the local one
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
// RNCertificatePinning.swift
import Foundation
@objc(RNCertificatePinning)
class RNCertificatePinning: NSObject {
@objc static func requiresMainQueueSetup() -> Bool {
return false
}
@objc(makeRequest:withResolver:withRejecter:)
func makeRequest(urlString: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
guard let url = URL(string: urlString) else {
reject("INVALID_URL", "Invalid URL", nil)
return
}
let session = URLSession(configuration: .default, delegate: MySessionDelegate(), delegateQueue: nil)
let task = session.dataTask(with: url) { (data, response, error) in
if let error = error {
reject("REQUEST_FAILED", "Request failed", error)
} else if let data = data {
resolve(String(data: data, encoding: .utf8))
}
}
task.resume()
}
@objc
static func moduleName() -> String! {
return "RNCertificatePinning"
}
@objc
static func requiresMainQueueSetup() -> Bool {
return false
}
}
React Native Code
Here’s my React Native code using react-native-ssl-pinning
:
import { fetch } from "react-native-ssl-pinning";
export const iosConfigureMonidrop = async (SSID, PASSWORD, TOKEN) => {
const BASE_URL = "https://192.168.x.x/init";
const URL = "httpsxxxdata";
const urlString = `${BASE_URL}?ssid=${encodeURIComponent(SSID)}&pwd=${encodeURIComponent(PASSWORD)}&token=${encodeURIComponent(TOKEN)}&url=${URL}`;
try {
const response = await fetch(urlString, {
method: "GET",
timeoutInterval: 3000,
disableAllSecurity: true,
sslPinning: {
certs: ["deviceDrive"]
},
headers: {
Accept: "application/json; charset=utf-8",
"Access-Control-Allow-Origin": "*",
e_platform: "mobile",
},
});
if (response.status < 200 || response.status >= 300) {
console.error(`HTTP error! status: ${response.status}`);
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error during fetch with SSL pinning", error);
throw error;
}
};
Error Encountered
All of these configurations result in the following error:
Task <6A32A4AE-D06D-4A40-9D82-52936CB0F377>.<7> finished with error [-1202] Error Domain=NSURLErrorDomain Code=-1202 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “192.168.4.1” which could put your confidential information at risk." UserInfo={NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?
Request for Help
How can I resolve this certificate issue and ensure my app can securely communicate with the local server? Is there a better approach to handle SSL pinning or trust evaluation in this context? React Native solution could be best so I dont need manage native code at all.
Any help or guidance would be greatly appreciated. Thank you!
Aki-Petteri Kuivas is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.