skip to Main Content

Title: SSL Evaluation Fails on iPhone for React Native Expo App

Body:

I am developing a React Native Expo app that needs to configure a device with local WiFi without internet connection. The app works fine on Android by original fetch 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 and rn-fetch-blob 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 and self-signed. I found that apple needs 2048bit certificate.

Questions:

  1. Is the weak certificate the primary reason for the TLS failure on iOS?
  2. How can I use this certificate or bypass this issue temporarily while developing?
  3. 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]

The application requires making network requests to a local server (https://192.168.x.x/…). I have configured my Info.plist to allow arbitrary loads and have tried adding an exception domain for the local IP. Here is the current configuration for every address:

    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>

Swift Code

This is my last attempt to ignore a weak certificate for development purposes. Later, we can probably upgrade the certificates on the configured devices, but for now, I need to establish a connection to the IP address and ignore the weak certificate.

import Foundation

@objc(RNSecureRequest)
class RNSecureRequest: NSObject {

    @objc(performDebugRequest:reject:)
    func performDebugRequest(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        guard let url = URL(string: "https://192.168.x.x/init/...data") else {
            reject("Invalid URL", "The URL provided is invalid", nil)
            return
        }

        let sessionConfig = URLSessionConfiguration.default
        let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)

        let task = session.dataTask(with: url) { data, response, error in
            if let error = error {
                reject("Request Error", error.localizedDescription, error)
                return
            }

            guard let data = data else {
                reject("No Data", "No data received from the server", nil)
                return
            }

            let responseString = String(data: data, encoding: .utf8)
            resolve(responseString)
        }

        task.resume()
    }
}

extension RNSecureRequest: URLSessionDelegate {
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        if let serverTrust = challenge.protectionSpace.serverTrust {
            // Create a policy to allow the connection to proceed despite the weak certificate.
            let policy = SecPolicyCreateSSL(true, challenge.protectionSpace.host as CFString)
            SecTrustSetPolicies(serverTrust, policy)

            // Evaluate the trust object using the modern API.
            var error: CFError?
            let isServerTrusted = SecTrustEvaluateWithError(serverTrust, &error)

            if isServerTrusted {
                let credential = URLCredential(trust: serverTrust)
                completionHandler(.useCredential, credential)
            } else {
                // Even if the evaluation fails, you can bypass it if needed.
                let credential = URLCredential(trust: serverTrust)
                completionHandler(.useCredential, credential)
            }
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
}

Error Encountered

All of these configurations result in the following error:

Connection 15: default TLS Trust evaluation failed(-9807)
Connection 15: TLS Trust encountered error 3:-9807
Connection 15: encountered error(3:-9807)
Task <F30FCF7F-8C61-42D7-8F64-DF19C7D426DF>.<4> HTTP load failed, 0/0 bytes (error code: -1202 [3:-9807])
Task <F30FCF7F-8C61-42D7-8F64-DF19C7D426DF>.<4> 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.x.x” which could put your confidential information at risk." UserInfo={NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, NSErrorPeerCertificateChainKey=(
    "<cert(0x14703a800) s: DeviceDrive i: DeviceDrive>"
), NSErrorClientCertificateStateKey=0, NSErrorFailingURLKey=https://192.168.x.x/init?ssid=&pwd=&token=url=data, NSErrorFailingURLStringKey=https://192.168.x.x/init?ssid=&pwd=&token=url=data, NSUnderlyingError=0x30118c5d0 {Error Domain=kCFErrorDomainCFNetwork Code=-1202 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x302ec0dc0>, _kCFNetworkCFStreamSSLErrorOriginalValue=-9807, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9807, kCFStreamPropertySSLPeerCertificates=(
    "<cert(0x14703a800) s: DeviceDrive i: DeviceDrive>"
)}}, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalDataTask <F30FCF7F-8C61-42D7-8F64-DF19C7D426DF>.<4>"
), _kCFStreamErrorCodeKey=-9807, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <F30FCF7F-8C61-42D7-8F64-DF19C7D426DF>.<4>, NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x302ec0dc0>, NSLocalizedDescription=The certificate for this server is invalid. You might be connecting to a server that is pretending to be “192.168.x.x” which could put your confidential information at risk.}

Request for Help

How can I resolve this certificate issue and ensure my app can communicate with the local server? Is there a better approach to handle SSL pinning or trust evaluation? React Native solution could be best so I dont need manage native code at all.

Any help or guidance would be greatly appreciated.

2

Answers


  1. Chosen as BEST ANSWER

    Solution:

    I noticed that my urlSession method was never called when it was in my own function file, so I started to investigate where the issue was occurring.

    To resolve this, I added a custom urlSession(_:didReceive:completionHandler:) function to the URLSessionSessionDelegateProxy class in the ExpoRequestInterceptorProtocol.swift file. This ensures that the app can properly handle server trust challenges.

    Here’s the code I added:

    // MARK: - Custom URL Session Delegate Function
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        if let serverTrust = challenge.protectionSpace.serverTrust {
            let credential = URLCredential(trust: serverTrust)
            print("Accepting server trust in URLSessionSessionDelegateProxy")
            completionHandler(.useCredential, credential)
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
    

    Location:

    I added this function after the following code block in node_modules/expo-modules-core/ios/DevTools/ExpoRequestInterceptorProtocol.swift:

    /private class URLSessionSessionDelegateProxy: NSObject, URLSessionDataDelegate {
        private var requestIdProvider = RequestIdProvider()
        private var delegateMap: [String: URLSessionDataDelegate] = [:]
        private let dispatchQueue = ExpoRequestCdpInterceptor.shared.dispatchQueue
    

    Outcome:

    With this change, the app now correctly handles server trust challenges, allowing it to connect to servers with self-signed certificates without issues. It also restores the functionality of the original React Native fetch method.

    Question:

    Does anyone know how I could handle certificates without modifying the Expo code? Or is this even possible? My next task is to add a certificate and specify the IP address where this function should take effect.


  2. Issue:

    When implementing a custom URLSessionDelegate in Expo module, delegate was being overridden by Expo’s default behavior. This resulted in your custom delegate not being utilized during network requests.

    Solution:

    The issue arises because Expo may register its own custom URLProtocol classes globally, which can interfere with your custom URLSessionDelegate. To prevent this, you can explicitly clear any globally registered custom URLProtocol classes in your URLSessionConfiguration.

    Updated Code:

    Here’s the relevant code snippet with the solution applied:

    import ExpoModulesCore
    
    public class FetchData_module: Module {
      
      public func definition() -> ModuleDefinition {
        Name("FetchData_module")
    
        Constants([
          "PI": Double.pi
        ])
    
        Events("onChange")
    
        AsyncFunction("fetchData") { (url: String) -> String in
            return try await fetchData(from: url)
        }
      }
    
      private class CustomURLSessionDelegate: NSObject, URLSessionDelegate {
          func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
              print("CustomURLSessionDelegate: Received authentication challenge")
              
              if let serverTrust = challenge.protectionSpace.serverTrust {
                  let credential = URLCredential(trust: serverTrust)
                  print("CustomURLSessionDelegate: Accepting server trust")
                  completionHandler(.useCredential, credential)
              } else {
                  print("CustomURLSessionDelegate: Performing default handling")
                  completionHandler(.performDefaultHandling, nil)
              }
          }
      }
    
        private func fetchData(from urlString: String) async throws -> String {
            guard let url = URL(string: urlString) else {
                throw NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])
            }
            
            print("Fetching data from URL: (url)")
    
            let configuration = URLSessionConfiguration.default
            
            configuration.protocolClasses = [] 
    
            configuration.timeoutIntervalForRequest = 5.0
            configuration.timeoutIntervalForResource = 10.0
    
            let sessionDelegate = CustomURLSessionDelegate()
            let session = URLSession(configuration: configuration, delegate: sessionDelegate, delegateQueue: nil)
    
            let (data, _) = try await session.data(from: url)
    
            if let str = String(data: data, encoding: .utf8) {
                return str
            } else {
                throw NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Unable to decode data"])
            }
        }
    }
    

    Explanation:

    • Issue: Expo may register custom URLProtocol classes that override your custom URLSessionDelegate. This results in your delegate not being called as expected.
    • Solution: To avoid this, set configuration.protocolClasses = [] in your URLSessionConfiguration. This clears any globally registered custom URLProtocol classes, ensuring that your delegate is used.

    By applying this change, you can ensure that your custom URLSessionDelegate handles network requests as intended, without interference from Expo’s default behavior.


    Important Security Note:

    The CustomURLSessionDelegate class in this implementation is configured to accept all HTTPS connections, even those with self-signed or invalid certificates. This means that the delegate will trust any server, potentially exposing your application to man-in-the-middle (MITM) attacks.

    If you use this code in production, make sure to implement proper security checks, such as validating the server’s certificate against a known certificate authority or pinning the server’s certificate. Accepting all server certificates without validation should be limited to development environments or controlled scenarios where you are fully aware of the security implications.

    Including this note will help others understand the security risks involved and encourage them to take appropriate precautions if they intend to use the code in a production environment.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search