frw / react-native-ssl-public-key-pinning

Simple and secure SSL public key pinning for React Native. No native configuration needed, set up in <5 minutes.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Bug][iOS] Not working with expo-dev-client in debug build

quaos opened this issue · comments

Hi,
About 1-2 months ago, I had used this library to enforce SSL pinning in my company's app on a PoC branch, which worked as expected.

And just today, I tried to install and apply the library again in a new branch, but now it does not block any request to pinned domain when using invalid keys.

Already run cd ios && pod install then expo run:ios

UPDATE (2024-01-24):

Configuration

(Testing invalid keys case)

{
  "some-service.tech": {
    "includeSubdomains": true,
    "publicKeyHashes": [
      "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
      "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
    ]
  }
}

Logs

(iOS to MacOS Console log)

=== TrustKit: Successfully initialized with configuration {
    TSKPinnedDomains =     {
        "some-service.tech" =         {
            TSKDisableDefaultReportUri = 1;
            TSKEnforcePinning = 1;
            TSKIncludeSubdomains = 1;
            TSKPublicKeyHashes = "{(\n    {length = 32, bytes = 0x00000000 00000000 00000000 00000000 ... 00000000 00000000 },\n    {length = 32, bytes = 0x04104104 10410410 41041041 04104104 ... 04104104 10410410 }\n)}";
            kSKExcludeSubdomainFromParentPolicy = 0;
        };
    };
    TSKSwizzleNetworkDelegates = 0;
}

Versions

  • react-native-ssl-public-key-pinning: 1.1.3
  • TrustKit: 3.0.3
  • Axios: 0.25.0
  • Expo: 49.0.13
  • RN: 0.72.6
  • CocoaPods: 1.14.3
  • iOS: 16.2 (iPhone Simulator)

Thanks!

Hello @quaos , I am reviewing this library to implement it within my project and I am having the same problem on iOS. But in this case, after some testing with the project inside the library, I found the following:

Test 1
When initializing the truskit of a domain with invalid keys and making a request, the validation is done correctly

a. Initialization

Pasted Graphic

=== TrustKit: Loaded 6 SPKI cache entries from the filesystem

=== TrustKit: Successfully initialized with configuration {
TSKPinnedDomains = {
"google.com" = {
TSKDisableDefaultReportUri = 1;
TSKEnforcePinning = 1;
TSKIncludeSubdomains = 1;
TSKPublicKeyHashes = "{(\n {length = 32, bytes = 0x762195c2 25586ee6 c0237456 e2107dc5 ... bd515913 cce68332 },\n {length = 32, bytes = 0x0b9fa5a5 9eed715c 26c1020c 711b4f6e ... 7a39dad3 01c5afc3 }\n)}";
kSKExcludeSubdomainFromParentPolicy = 0;
};
};
TSKSwizzleNetworkDelegates = 0;
}

b. Validation

Pasted Graphic 1

=== TrustKit: Checking includeSubdomains configuration for google.com
=== TrustKit: Applying includeSubdomains configuration from google.com to www.google.com (new best match of 7 chars)
=== TrustKit: Checking certificate with CN: GTS Root R1
=== TrustKit: Subject Public Key Info hash was found in the cache
=== TrustKit: Testing SSL Pin {length = 32, bytes = 0x871a9194 f4eed5b3 12ff40c8 4c1d524a ... 8cf81f68 0a7adc67 }
=== TrustKit: Checking certificate with CN: GTS CA 1C3
=== TrustKit: Subject Public Key Info hash was found in the cache
=== TrustKit: Testing SSL Pin {length = 32, bytes = 0xcc24e77c bc0b29b4 bd4b6b1b a7eb85cf ... 574e827b d3b9336c }
=== TrustKit: Checking certificate with CN: www.google.com
=== TrustKit: Subject Public Key Info hash was found in the cache
=== TrustKit: Testing SSL Pin {length = 32, bytes = 0x3973ad02 3004f33d 10ffb162 56d3ee72 ... a1e9a357 8d3962cd }
=== TrustKit: Error: SSL Pin not found for www.google.com
=== TrustKit: Pin validation failed for www.google.com

Test 2

But now the problem arises when I initialize the trukit with valid keys, the initialization is done correctly and the first validation is done correctly. as can be seen in the following images and traces:

Initialization

Pasted Graphic 2

=== TrustKit: Successfully initialized with configuration {
TSKPinnedDomains = {
"google.com" = {
TSKDisableDefaultReportUri = 1;
TSKEnforcePinning = 1;
TSKIncludeSubdomains = 1;
TSKPublicKeyHashes = "{(\n {length = 32, bytes = 0x871a9194 f4eed5b3 12ff40c8 4c1d524a ... 8cf81f68 0a7adc67 },\n {length = 32, bytes = 0x4179edd9 81ef7474 77b49626 408af43d ... 1060f840 96774348 },\n {length = 32, bytes = 0x08b3a633 5fce5ef4 8f8f0e54 3986c07f ... 864bbd5b dd1f1cc9 },\n {length = 32, bytes = 0x9847e565 3e5e9e84 7516e5cb 818606aa ... 6d506988 e8d84347 },\n {length = 32, bytes = 0x55f77de4 1c037924 28f8d518 c5510422 ... 28ad653e 1ccec7bf }\n)}";
kSKExcludeSubdomainFromParentPolicy = 0;
};
};
TSKSwizzleNetworkDelegates = 0;
}

Validation

Pasted Graphic 3

=== TrustKit: Checking includeSubdomains configuration for google.com
=== TrustKit: Applying includeSubdomains configuration from google.com to www.google.com (new best match of 7 chars)
=== TrustKit: Checking certificate with CN: GTS Root R1
=== TrustKit: Subject Public Key Info hash was found in the cache
=== TrustKit: Testing SSL Pin {length = 32, bytes = 0x871a9194 f4eed5b3 12ff40c8 4c1d524a ... 8cf81f68 0a7adc67 }
=== TrustKit: SSL Pin found for www.google.com
=== TrustKit: Pin validation succeeded for www.google.com

but now if I initialize the trukit again with invalid keys and make a new request, the validation of the keys is not done and the request comes out as successful.

initialization invalid keys

Pasted Graphic 4

=== TrustKit: Successfully initialized with configuration {
TSKPinnedDomains = {
"google.com" = {
TSKDisableDefaultReportUri = 1;
TSKEnforcePinning = 1;
TSKIncludeSubdomains = 1;
TSKPublicKeyHashes = "{(\n {length = 32, bytes = 0x762195c2 25586ee6 c0237456 e2107dc5 ... bd515913 cce68332 },\n {length = 32, bytes = 0x0b9fa5a5 9eed715c 26c1020c 711b4f6e ... 7a39dad3 01c5afc3 }\n)}";
kSKExcludeSubdomainFromParentPolicy = 0;
};
};
TSKSwizzleNetworkDelegates = 0;
}

validation with invalid keys

Pasted Graphic 5

Hello @quaos , I am reviewing this library to implement it within my project and I am having the same problem on iOS. But in this case, after some testing with the project inside the library, I found the following:

Test 1 When initializing the truskit of a domain with invalid keys and making a request, the validation is done correctly

a. Initialization

Pasted Graphic

=== TrustKit: Loaded 6 SPKI cache entries from the filesystem

=== TrustKit: Successfully initialized with configuration { TSKPinnedDomains = { "google.com" = { TSKDisableDefaultReportUri = 1; TSKEnforcePinning = 1; TSKIncludeSubdomains = 1; TSKPublicKeyHashes = "{(\n {length = 32, bytes = 0x762195c2 25586ee6 c0237456 e2107dc5 ... bd515913 cce68332 },\n {length = 32, bytes = 0x0b9fa5a5 9eed715c 26c1020c 711b4f6e ... 7a39dad3 01c5afc3 }\n)}"; kSKExcludeSubdomainFromParentPolicy = 0; }; }; TSKSwizzleNetworkDelegates = 0; }

b. Validation

Pasted Graphic 1

=== TrustKit: Checking includeSubdomains configuration for google.com === TrustKit: Applying includeSubdomains configuration from google.com to www.google.com (new best match of 7 chars) === TrustKit: Checking certificate with CN: GTS Root R1 === TrustKit: Subject Public Key Info hash was found in the cache === TrustKit: Testing SSL Pin {length = 32, bytes = 0x871a9194 f4eed5b3 12ff40c8 4c1d524a ... 8cf81f68 0a7adc67 } === TrustKit: Checking certificate with CN: GTS CA 1C3 === TrustKit: Subject Public Key Info hash was found in the cache === TrustKit: Testing SSL Pin {length = 32, bytes = 0xcc24e77c bc0b29b4 bd4b6b1b a7eb85cf ... 574e827b d3b9336c } === TrustKit: Checking certificate with CN: www.google.com === TrustKit: Subject Public Key Info hash was found in the cache === TrustKit: Testing SSL Pin {length = 32, bytes = 0x3973ad02 3004f33d 10ffb162 56d3ee72 ... a1e9a357 8d3962cd } === TrustKit: Error: SSL Pin not found for www.google.com === TrustKit: Pin validation failed for www.google.com

Test 2

But now the problem arises when I initialize the trukit with valid keys, the initialization is done correctly and the first validation is done correctly. as can be seen in the following images and traces:

Initialization

Pasted Graphic 2

=== TrustKit: Successfully initialized with configuration { TSKPinnedDomains = { "google.com" = { TSKDisableDefaultReportUri = 1; TSKEnforcePinning = 1; TSKIncludeSubdomains = 1; TSKPublicKeyHashes = "{(\n {length = 32, bytes = 0x871a9194 f4eed5b3 12ff40c8 4c1d524a ... 8cf81f68 0a7adc67 },\n {length = 32, bytes = 0x4179edd9 81ef7474 77b49626 408af43d ... 1060f840 96774348 },\n {length = 32, bytes = 0x08b3a633 5fce5ef4 8f8f0e54 3986c07f ... 864bbd5b dd1f1cc9 },\n {length = 32, bytes = 0x9847e565 3e5e9e84 7516e5cb 818606aa ... 6d506988 e8d84347 },\n {length = 32, bytes = 0x55f77de4 1c037924 28f8d518 c5510422 ... 28ad653e 1ccec7bf }\n)}"; kSKExcludeSubdomainFromParentPolicy = 0; }; }; TSKSwizzleNetworkDelegates = 0; }

Validation

Pasted Graphic 3

=== TrustKit: Checking includeSubdomains configuration for google.com === TrustKit: Applying includeSubdomains configuration from google.com to www.google.com (new best match of 7 chars) === TrustKit: Checking certificate with CN: GTS Root R1 === TrustKit: Subject Public Key Info hash was found in the cache === TrustKit: Testing SSL Pin {length = 32, bytes = 0x871a9194 f4eed5b3 12ff40c8 4c1d524a ... 8cf81f68 0a7adc67 } === TrustKit: SSL Pin found for www.google.com === TrustKit: Pin validation succeeded for www.google.com

but now if I initialize the trukit again with invalid keys and make a new request, the validation of the keys is not done and the request comes out as successful.

initialization invalid keys

Pasted Graphic 4

=== TrustKit: Successfully initialized with configuration { TSKPinnedDomains = { "google.com" = { TSKDisableDefaultReportUri = 1; TSKEnforcePinning = 1; TSKIncludeSubdomains = 1; TSKPublicKeyHashes = "{(\n {length = 32, bytes = 0x762195c2 25586ee6 c0237456 e2107dc5 ... bd515913 cce68332 },\n {length = 32, bytes = 0x0b9fa5a5 9eed715c 26c1020c 711b4f6e ... 7a39dad3 01c5afc3 }\n)}"; kSKExcludeSubdomainFromParentPolicy = 0; }; }; TSKSwizzleNetworkDelegates = 0; }

validation with invalid keys

Pasted Graphic 5

After performing several tests and researching various forums, I found the following:

When the request is made to any domain even without initializing trustkit, it seems that NSURLSession maintains its own TLS session cache. This means that if I initialize trustkit again, this configuration will not be taken and the new validation will not be done with my new keys.

The test done to determine this was:

  1. Request without initializing trustkit.

  2. Initialize trustkit.

  3. Request to the same domain. In this case, truskit does not validate that connection again.

Note: I updated this post because by testing the instructions given by this blog https://developer.apple.com/library/archive/qa/qa1727/_index.html, I found that indeed when waiting for 10 minutes and performing a new request, validation was carried out again with the trustkiy pins

@cristian1206

Thanks for the investigation into the issue. Indeed, iOS maintains a session cache, which re-uses connections if they've been made successfully before. This is detailed in the Known Issues section in the README:
https://github.com/frw/react-native-ssl-public-key-pinning?tab=readme-ov-file#known-issues

On iOS, SSL/TLS sessions are cached. If a connection to your site previously succeeded, setting a pinning configuration that should fail the following request would not actually fail it since the previous session is used. You will need to restart your app to clear out this cache.

The main workaround would be to ensure you initialize the pinning before any network requests are made.

@quaos are you still facing an issue with the pinning, taking this into consideration?

Hello @frw , thank you very much for your prompt response, what a shame I didn't see the known issues section. In my case I am reviewing the issue of updating keys since I was trying to implement a request at the beginning that would bring me the new keys but with this issue that is presented in iOS I think it is going to be an inconvenience, I am going to review the updates via OTA Let's see if I can find something on that side. Thank you very much again for your response.

@cristian1206
In terms of security, I would highly recommend the OTA update option since you can sign the OTA bundle (CodePush, Expo Updates), ensuring that they are from a trusted source.
If you're updating your keys via an unpinned network request during startup, I believe you will be exposing a potentially security hole, since a bad actor would be able to perform a MitM attack and thus would be able to serve whatever keys they wish.

@cristian1206

Thanks for the investigation into the issue. Indeed, iOS maintains a session cache, which re-uses connections if they've been made successfully before. This is detailed in the Known Issues section in the README: https://github.com/frw/react-native-ssl-public-key-pinning?tab=readme-ov-file#known-issues

On iOS, SSL/TLS sessions are cached. If a connection to your site previously succeeded, setting a pinning configuration that should fail the following request would not actually fail it since the previous session is used. You will need to restart your app to clear out this cache.

The main workaround would be to ensure you initialize the pinning before any network requests are made.

@quaos are you still facing an issue with the pinning, taking this into consideration?

@cristian1206 @frw Thanks! I'd try applying the pins at the first entrypoint of application and see if that works.

(I'm also suspecting some 3rd party SDKs that 'd get initialized in AppDelegate.mm might cause networking resources to be set up before pinning.)

@quaos For further debugging, you can also check this thread for breakpoints you can use to see if the library is functioning properly: #220 (comment)

@frw @cristian1206

UPDATE

Here is my latest attempt to fix this:

  • Call initializeSslPinning in the very entrypoint of the app (index.ts), before calling Expo registerRootComponent
  • Share the state to App component via hookstate store to tell the component that the app is ready to render.

Still the requests to pinned domains do not get blocked. Here are the related event logs:

JS Console logs

BEFORE TrustKit init

# Debug log from AppDelegate.mm
[myapp-staging] AppDelegate.didFinishLaunchingWithOptions: {
UIApplicationLaunchOptionsURLKey = "myapp://expo-development-client/?url=http%3A%2F%2F192.168.1.41%3A8081";
}

iOS Bundling complete 12349ms

# Braze SDK with mock endpoint URL  in staging
[myapp-staging] HTTP URL error:
- code: -1003
- description: "A server with the specified hostname could not be found."
- url: [POST] https://mock.end.point/api/v3/data
# ...

 INFO  Initializing react-native-branch v. 5.6.2

TrustKit init

 DEBUG  2024-01-11T16:50:18.712+0700 setupSslPinning: {"some-service.tech": {"includeSubdomains": true, "publicKeyHashes": ["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="]}}

 DEBUG  2024-01-11T16:50:18.949+0700 SslPinningState: setIsReady: true
 DEBUG  2024-01-11T16:50:18.950+0700 SslPinningState: setErrorSubscription: {"remove": [Function remove]}
 DEBUG  2024-01-11T16:50:18.981+0700 App.tsx: isSslPinningReady: true

AFTER TrustKit init

# Request to baseURL: https://some-service.tech/api
 DEBUG  2024-01-11T16:50:20.276+0700 postRequest: {"url": "/v1.0/event-log"}
# success ❌

iOS Simulator -> MacOS Console logs

BEFORE TrustKit init

default 16:49:59.396897+0700  myapp-staging  AppDelegate.didFinishLaunchingWithOptions: {
    UIApplicationLaunchOptionsURLKey = "myapp://expo-development-client/?url=...:8081";
}

default 16:49:59.577024+0700  myapp-staging  networkd_settings_read_from_file initialized networkd settings by reading plist directly

default 16:50:00.057622+0700  myapp-staging  10.17.0 - [FirebaseMessaging][I-FCM001000] FIRMessaging Remote Notifications proxy enabled, will swizzle remote notification receiver handlers. If you'd prefer to manually integrate Firebase Messaging, add "FirebaseAppDelegateProxyEnabled" to your Info.plist, and set it to NO. ... blah blah blah ...

default 16:50:00.084090+0700  myapp-staging  10.17.0 - [FirebaseAnalytics][I-ACS023007] Analytics v.10.17.0 started

default 16:50:00.134980+0700  myapp-staging  Initializing Braze SDK 5.9.1 with configuration:
- api
  - key: mock-api-key
  - endpoint: mock.end.point
  # ...

default 16:50:00.708172+0700  myapp-staging  🟡 {"code":"JSRuntimeError","message":"EXUpdates: Could not emit event: name = Expo.nativeUpdatesStateChangeEvent, type = restart. Event will be emitted when the bridge is available","timestamp":1704966600000,"level":"warn"}

default 16:50:01.094304+0700  myapp-staging  Connection 1: starting, TC(0x0)
default 16:50:01.137934+0700  myapp-staging  [C1 12592B3C-88FE-4F8B-AAD2-0ACFB1E7B780 api2.branch.io:443 tcp, url hash: 209309af, tls, definite, attribution: developer, context: com.apple.CFNetwork.NSURLSession.{0172C748-DDF2-4629-9790-04E46EED31FD}{(null)}{Y}{2} (private), proc: DD1561F6-1434-3284-9849-FE087737480A] start

default 16:50:01.185426+0700  myapp-staging  Connection 2: starting, TC(0x0)
default 16:50:01.185468+0700  myapp-staging  [C2 80EBA4FA-6096-4BD4-87CE-1671466F941D mock.end.point:443 tcp, url hash: 5686465a, tls, definite, attribution: developer, context: com.apple.CFNetwork.NSURLSession.{7984BC31-B678-439F-B199-CABC71ED82E7}{(null)}{Y}{2} (private), proc: DD1561F6-1434-3284-9849-FE087737480A] start

error 16:50:01.286103+0700  myapp-staging  Connection 2: failed to connect 12:8, reason -1
default 16:50:01.288589+0700  myapp-staging  [C2 mock.end.point:443 tcp, url hash: 5686465a, tls, definite, attribution: developer] cancelled

default 16:50:01.299774+0700  myapp-staging  nw_flow_connected [C1.1.1 45241955:443 in_progress socket-flow (satisfied (Path is satisfied), interface: en0[802.11])] Transport protocol connected (socket)
# SSL pinning for domain "*.branch.io" is not configured in this app

error 16:50:17.897809+0700  myapp-staging  HTTP URL error:
- code: -1003
- description: "A server with the specified hostname could not be found."
- url: [POST] https://mock.end.point/api/v3/data
  # ...

TrustKit init

default 16:50:18.737565+0700  myapp-staging  === TrustKit: Successfully initialized with configuration {
    TSKPinnedDomains =     {
        "some-service.tech" =         {
            TSKDisableDefaultReportUri = 1;
            TSKEnforcePinning = 1;
            TSKIncludeSubdomains = 1;
            TSKPublicKeyHashes = "{(\n    {length = 32, bytes = 0x00000000 00000000 00000000 00000000 ... 00000000 00000000 },\n    {length = 32, bytes = 0x04104104 10410410 41041041 04104104 ... 04104104 10410410 }\n)}";
            kSKExcludeSubdomainFromParentPolicy = 0;
        };
        # ... domains with invalid keys ...
    };
    TSKSwizzleNetworkDelegates = 0;
}

AFTER TrustKit init

NOTE: The log lines streamed to MacOS console keep streaming even after I pressed Pause button, and the logs from a few minutes ago was lost

Hello @quaos . I have the following questions:

  1. If you do not initialize truskit and make a request to the domain that does not work (https://some-service.tech/api), is the request made correctly?

  2. If you request with truskit enabled for a known domain (github.com, google.com...) with and without the keys for each domain embedded in truskit, do both requests work for you?

  3. Do you have the xcode console log when you make the request to your domain and it fails, that is, after initializing truskit?

@cristian1206

Hello @quaos . I have the following questions:

  1. If you do not initialize truskit and make a request to the domain that does not work (https://some-service.tech/api), is the request made correctly?

Yes. It can make requests to the domain and get responses.

  1. If you request with truskit enabled for a known domain (github.com, google.com...) with and without the keys for each domain embedded in truskit, do both requests work for you?

I also added pins for *.google.com, maps.googleapis.com, fcm.googleapis.com, etc. The request worked whether I used correct or incorrect pinning keys for them.

  1. Do you have the xcode console log when you make the request to your domain and it fails, that is, after initializing truskit?

Currently when I open ios/myapp.xcodeproj in Xcode and try to build/run/debug the project I got build errors:

No such module 'ExpoModulesCore'

I get the project to run using expo run:ios for now.

UPDATE

I've created a test repo in attempt to reproduce the issue, but still can't:
test-expo-ssl-pinning-1

However, I could successfully run the original project with debugging in XCode (it stops at breakpoints as expected), and this is my latest findings:

  • TrustKit.TSKPinningValidator instance has been initialized, but the method evaluateTrust did not get called.
  • TrustKit.ssl_pin_verifier.verifyPublicKeyPin did not get called neither.
  • react-native-ssl-public-key-pinning.SslPublicKeyPinning.completionHandler did not get called neither.
  • In MacOS console streamed from iOS Sim, there are only boringssl_context_* logs showing up relating to HTTPS/SSL handshake activity.

UPDATE

Here are the latest log and stacktraces, showing that in the original repo the app execution does not enter TrustKit validator methods at all:

Test Repo (entering TrustKit methods ✅)

JS log

 DEBUG  useApiClient.query: GET undefined {"baseUrl": "https://quaos-portfolio.netlify.app/data/portfolio.json"}

XCode Debug stack trace

Thread 47 Queue : com.facebook.react.NetworkingQueue (serial)
#0	0x0000000104953d00 in -[RCTHTTPRequestHandler sendRequest:withDelegate:] at /Users/chakrit/Projects/test-expo-ssl-pinning-1/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm:71
#1	0x0000000104960d7c in -[RCTNetworkTask start] at /Users/chakrit/Projects/test-expo-ssl-pinning-1/node_modules/react-native/Libraries/Network/RCTNetworkTask.mm:75
#2	0x000000010495c8d8 in -[RCTNetworking sendRequest:responseType:incrementalUpdates:responseSender:] at /Users/chakrit/Projects/test-expo-ssl-pinning-1/node_modules/react-native/Libraries/Network/RCTNetworking.mm:652
#3	0x000000010495ed98 in __38-[RCTNetworking sendRequest:callback:]_block_invoke at /Users/chakrit/Projects/test-expo-ssl-pinning-1/node_modules/react-native/Libraries/Network/RCTNetworking.mm:721
#4	0x0000000104959960 in __46-[RCTNetworking buildRequest:completionBlock:]_block_invoke_2 at /Users/chakrit/Projects/test-expo-ssl-pinning-1/node_modules/react-native/Libraries/Network/RCTNetworking.mm:351

Thread 48 Queue : com.facebook.react.NetworkingQueue (serial)
#0	0x0000000104c8d9e0 in verifyPublicKeyPin at /Users/chakrit/Projects/test-expo-ssl-pinning-1/ios/Pods/TrustKit/TrustKit/Pinning/ssl_pin_verifier.m:24
#1	0x0000000104c94290 in -[TSKPinningValidator evaluateTrust:forHostname:] at /Users/chakrit/Projects/test-expo-ssl-pinning-1/ios/Pods/TrustKit/TrustKit/TSKPinningValidator.m:126
#2	0x0000000104c94774 in -[TSKPinningValidator handleChallenge:completionHandler:] at /Users/chakrit/Projects/test-expo-ssl-pinning-1/ios/Pods/TrustKit/TrustKit/TSKPinningValidator.m:202
#3	0x0000000104d5dc38 in -[RCTHTTPRequestHandler(SslPublicKeyPinning) URLSession:task:didReceiveChallenge:completionHandler:] at /Users/chakrit/Projects/test-expo-ssl-pinning-1/node_modules/react-native-ssl-public-key-pinning/ios/SslPublicKeyPinning.mm:108
#4	0x0000000183e10bcc in ___lldb_unnamed_symbol2730 ()
#5	0x0000000180b9a8e8 in __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ ()
# ...
Enqueued from com.apple.NSURLSession-work (Thread 48) Queue : com.apple.NSURLSession-work (serial)

iOS -> MacOS console log

(hard to capture, it flows non-stop and won't pause)

default	15:02:39.277090+0700	testexposslpinning1	=== TrustKit: Loaded 6 SPKI cache entries from the filesystem
default	15:31:35.878371+0700	testexposslpinning1	=== TrustKit: Successfully initialized with configuration {
    TSKPinnedDomains =     {
        "firebaseremoteconfig.googleapis.com" =         {
          // ...
        };
        "google.com" =         {
          // ...
        };
        "quaos-portfolio.netlify.app" =         {
            TSKDisableDefaultReportUri = 1;
            TSKEnforcePinning = 1;
            TSKIncludeSubdomains = 1;
            TSKPublicKeyHashes = "{(\n    {length = 32, bytes = 0xaff98890 6dde1295 5d9bebbf 928fdcc3 ... 1c8941ca 26e20391 },\n    {length = 32, bytes = 0x59e738e6 74221702 af1edb87 c5200c1a ... 265124c6 1bd83c79 }\n)}";
            kSKExcludeSubdomainFromParentPolicy = 0;
        };
    };
    TSKSwizzleNetworkDelegates = 0;
}

default	15:44:34.968784+0700	testexposslpinning1	boringssl_context_evaluate_trust_async(1635) [C37.1.1.3:2][0x1503151b0] Performing external trust evaluation
default	15:44:34.969041+0700	testexposslpinning1	boringssl_context_evaluate_trust_async_external(1620) [C37.1.1.3:2][0x1503151b0] Asyncing for external verify block
default	15:45:08.189121+0700	testexposslpinning1	=== TrustKit: Testing SSL Pin {length = 32, bytes = 0x59e738e6 74221702 af1edb87 c5200c1a ... 265124c6 1bd83c79 }
default	15:45:08.189281+0700	testexposslpinning1	=== TrustKit: SSL Pin found for quaos-portfolio.netlify.app
default	15:45:08.189379+0700	testexposslpinning1	=== TrustKit: Pin validation succeeded for quaos-portfolio.netlify.app
default	15:45:08.204597+0700	testexposslpinning1	boringssl_context_evaluate_trust_async_external_block_invoke(1608) [0x0] Cancelled during verify block

Original Repo (not entering TrustKit methods ❌)

JS log

 DEBUG  2024-01-26T07:44:38.461Z getRequest: {"url": "https://some-service.tech/api/v1.0/product"}

XCode Debug stack trace

Thread 91 Queue : com.facebook.react.NetworkingQueue (serial)
#0	0x0000000103718d64 in -[RCTHTTPRequestHandler sendRequest:withDelegate:] at /Users/chakrit/Projects/myapp/node_modules/react-native/Libraries/Network/RCTHTTPRequestHandler.mm:71
#1	0x0000000103725de0 in -[RCTNetworkTask start] at /Users/chakrit/Projects/myapp/node_modules/react-native/Libraries/Network/RCTNetworkTask.mm:75
#2	0x000000010372193c in -[RCTNetworking sendRequest:responseType:incrementalUpdates:responseSender:] at /Users/chakrit/Projects/my-app/node_modules/react-native/Libraries/Network/RCTNetworking.mm:652
#3	0x0000000103723dfc in __38-[RCTNetworking sendRequest:callback:]_block_invoke at /Users/chakrit/Projects/my-app/node_modules/react-native/Libraries/Network/RCTNetworking.mm:721
#4	0x000000010371e9c4 in __46-[RCTNetworking buildRequest:completionBlock:]_block_invoke_2 at /Users/chakrit/Projects/my-app/node_modules/react-native/Libraries/Network/RCTNetworking.mm:351
#5	0x000000010e82c594 in _dispatch_call_block_and_release ()

iOS -> MacOS console log

(hard to capture, it flows non-stop and won't pause)

default	16:08:30.082662+0700	myapp	=== TrustKit: Loaded 0 SPKI cache entries from the filesystem
default	16:08:57.250258+0700	myapp	=== TrustKit: Successfully initialized with configuration {
    TSKPinnedDomains =     {
        "some-service.tech" =         {
            TSKDisableDefaultReportUri = 1;
            TSKEnforcePinning = 1;
            TSKIncludeSubdomains = 1;
            TSKPublicKeyHashes = "{(\n    {length = 32, bytes = 0x00000000 00000000 00000000 00000000 ... 00000000 00000000 },\n    {length = 32, bytes = 0x04104104 10410410 41041041 04104104 ... 04104104 10410410 }\n)}";
            kSKExcludeSubdomainFromParentPolicy = 0;
        };
    };
    TSKSwizzleNetworkDelegates = 0;
}


default	16:17:39.503190+0700	myapp	boringssl_context_evaluate_trust_async(1635) [C255.1.2.1:2][0x15b241600] Performing external trust evaluation
default	16:17:39.503268+0700	myapp	boringssl_context_evaluate_trust_async_external(1620) [C255.1.2.1:2][0x15b241600] Asyncing for external verify block
default	16:17:39.520363+0700	myapp	boringssl_context_evaluate_trust_async_external_block_invoke_3(1576) [C255.1.2.1:2][0x15b241600] Returning from external verify block with result: true
default	16:17:39.520441+0700	myapp	boringssl_context_certificate_verify_callback(1797) [C255.1.2.1:2][0x15b241600] Certificate verification result: OK

UPDATE

Upon deeper investigation, I found that NONE of these network delegate methods existing in the original repo is getting called:

  • react-native-ssl-public-key-pinning > SslPublicKeyPinning.m > RCTHTTPRequestHandler (SslPublicKeyPinning).didReceiveChallenge
  • GoogleUtilities/Network > GULNetworkURLSession.m > GULNetworkURLSession.didReceiveChallenge

Notes

Might be related to: expo/expo#24096

Will try to reproduce by adding expo-updates and expo-dev-client

Seems like it is caused by expo-dev-client. I'm taking a look around the expo-dev-client code and I'm noticing that Expo is swizzling the URLSessionConfiguration and intercepting network requests, which might be why the pinning configuration is getting ignored when expo-dev-client is used.

Let me see what options are available to hook into Expo's URLSession so we can enable pinning even when using expo-dev-client

Otherwise, it seems like expo-dev-client only intercepts network requests in development mode, so you should be fine if you're building your app for production. Could you try building your app for production with the wrong keys and see if pinning is working fine?

Seems like it is caused by expo-dev-client. I'm taking a look around the expo-dev-client code and I'm noticing that Expo is swizzling the URLSessionConfiguration and intercepting network requests, which might be why the pinning configuration is getting ignored when expo-dev-client is used.

Let me see what options are available to hook into Expo's URLSession so we can enable pinning even when using expo-dev-client

Otherwise, it seems like expo-dev-client only intercepts network requests in development mode, so you should be fine if you're building your app for production. Could you try building your app for production with the wrong keys and see if pinning is working fine?

Thanks! I've built the original app for iOS simulator using EAS, and when running the build on simulator, SSL pinning is enforced as designed. So this should work correctly on production.

do you working to fix this ? @frw

@oottoohh Yes I will try to fix this issue, though I will need to do a little bit of digging around to see how I can make it work nicely with expo-dev-client.

I've taken a look around and I think the easiest solution is to disable the expo-dev-client's network inspector while you're testing out your pinning configuration on development builds. I've added the instructions for doing so to the README, hope they're easy enough to follow along:
https://github.com/frw/react-native-ssl-public-key-pinning/blob/main/README.md#disable-expo-dev-client-network-inspector-on-ios-optional

Let me know if you come across any issues with it!

thankyou for quick response @frw, when i try it still not working with my own project, i already try set false for expo-dev-client. btw im using this for my template project https://github.com/infinitered/ignite

@oottoohh The instructions only apply for the Expo managed workflow. Looking at the template, it seems like it's a regular React Native library that uses some Expo modules.

In this case, could you try adding

ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = 'false'

to the top of your Podfile and then rerunning pod install?

@frw yeah its already there in my podfile. but let me try again after looking your example maybe i need change the way of my implement.

@oottoohh Could you post your app.json and Podfile actually?

It seems like the boilerplate you're using does use the Expo managed workflow, and already has expo-build-properties included:
https://github.com/infinitered/ignite/blob/master/boilerplate/app.json#L58-L69
So there's no need to edit your Podfile at all.

App.json :

{
"name": "myProjectName",
"displayName": "myProjectName",
"expo": {
"name": "myProjectName",
"slug": "myProjectName",
"owner": "myProjectName",
"scheme": "myProjectName",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/app-icon-all.png",
"splash": {
"image": "./assets/images/splash-logo-all.png",
"resizeMode": "contain",
"backgroundColor": "#191015"
},
"updates": {
"enabled": false,
"fallbackToCacheTimeout": 0
},
"jsEngine": "hermes",
"assetBundlePatterns": ["**/*"],
"android": {
"icon": "./assets/images/app-icon-android-legacy.png",
"package": "secret",
"adaptiveIcon": {
"foregroundImage": "./assets/images/app-icon-android-adaptive-foreground.png",
"backgroundImage": "./assets/images/app-icon-android-adaptive-background.png"
},
"splash": {
"image": "./assets/images/splash-logo-android-universal.png",
"resizeMode": "contain",
"backgroundColor": "#191015"
},
"googleServicesFile": "./app/services/googleServices/Development/google-services.json"
},
"ios": {
"icon": "./assets/images/myicon.png",
"supportsTablet": true,
"bundleIdentifier": "isecret",
"splash": {
// "image": "./assets/images/splash-logo-ios-mobile.png",
"image": "",
"tabletImage": "",
"resizeMode": "contain",
"backgroundColor": "#FFFFFF"
},
"googleServicesFile": "./app/services/googleServices/Development/GoogleService-Info.plist"
},
"web": {
"favicon": "./assets/images/app-icon-web-favicon.png",
"splash": {
"image": "./assets/images/splash-logo-web.png",
"resizeMode": "contain",
"backgroundColor": "#191015"
},
"bundler": "metro"
},
"plugins": [
"expo-localization",
"@react-native-firebase/app",
"@react-native-firebase/perf",
"@react-native-firebase/crashlytics",
"@react-native-google-signin/google-signin",
[
"react-native-fbsdk-next",
{
"appID": "secret",
"clientToken": "secret",
"displayName": "secret",
"scheme": "secret",
"advertiserIDCollectionEnabled": false,
"autoLogAppEventsEnabled": false,
"isAutoInitEnabled": true,
"iosUserTrackingPermission": "This identifier will be used to deliver personalized ads to you."
}
],
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "$(PRODUCT_NAME) would like to get your location to update price and products with your current location and find Outlets and Workshops nearby.",
"locationAlwaysPermission": "$(PRODUCT_NAME) would like to get your location to update price and products with your current location and find Outlets and Workshops nearby.",
"locationWhenInUsePermission": "$(PRODUCT_NAME) would like to get your location to update price and products with your current location and find Outlets and Workshops nearby."
}
],
[
"expo-build-properties",
{
"ios": {
"newArchEnabled": false,
"useFrameworks": "static",
"networkInspector": false
},
"android": {
"newArchEnabled": false
}
}
]
],
"experiments": {
"typedRoutes": true,
"tsconfigPaths": true
},
"extra": {
"eas": {
"projectId": "secret"
}
}
},
"ignite": {
"version": "9.4.0"
}
}

podfile :
require File.join(File.dirname(node --print "require.resolve('expo/package.json')"), "scripts/autolinking")
require File.join(File.dirname(node --print "require.resolve('react-native/package.json')"), "scripts/react_native_pods")

require 'json'
podfile_properties = JSON.parse(File.read(File.join(dir, 'Podfile.properties.json'))) rescue {}

ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']

platform :ios, podfile_properties['ios.deploymentTarget'] || '13.0'
install! 'cocoapods',
:deterministic_uuids => false

prepare_react_native_project!

flipper_config = FlipperConfiguration.disabled
if ENV['NO_FLIPPER'] == '1' then

flipper_config = FlipperConfiguration.disabled
elsif podfile_properties.key?('ios.flipper') then

if podfile_properties['ios.flipper'] == 'true' then
flipper_config = FlipperConfiguration.enabled(["Debug", "Release"])
elsif podfile_properties['ios.flipper'] != 'false' then
flipper_config = FlipperConfiguration.enabled(["Debug", "Release"], { 'Flipper' => podfile_properties['ios.flipper'] })
end
end

abstract_target 'common' do

use_expo_modules!
config = use_native_modules!

use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
$RNFirebaseAsStaticFramework = true
pod 'CocoaDebug', :configurations => ['Debug']
pod 'GoogleSignIn', '> 6.2.2'
pod 'Google-Maps-iOS-Utils', :git => 'https://github.com/Simon-TechForm/google-maps-ios-utils.git', :branch => 'feat/support-apple-silicon'
rn_maps_path = '../node_modules/react-native-maps'
pod 'react-native-google-maps', :path => rn_maps_path
pod 'GoogleUtilities', :modular_headers => true
pod 'MoEngageGeofence'
pod 'ReactNativeMoEngage', :path => '../node_modules/react-native-moengage'
pod 'TrustKit', '
> 2.0.1'
flags = get_default_flags()

use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
:fabric_enabled => flags[:fabric_enabled],
:app_path => "#{Pod::Config.instance.installation_root}/..",
:flipper_configuration => flipper_config
)

post_install do |installer|
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false
)
__apply_Xcode_12_5_M1_post_install_workaround(installer)

installer.target_installation_results.pod_target_installation_results
  .each do |pod_name, target_installation_result|
  target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
    resource_bundle_target.build_configurations.each do |config|
      config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
    end
  end
end

end

post_integrate do |installer|
begin
expo_patch_react_imports!(installer)
rescue => e
Pod::UI.warn e
end
end
target 'scheme' do
end
target 'scheme-Development' do
end
target 'scheme-Staging' do
end
end

i just using iOS, so maybe you dont need checking for android, then implement ssl pinning using trustkit with native code before so thats why i define trustkit 2.0.1, is it problem ?

i try with new implementation, based on your example approach its successfully,

image

That's great to hear that your setup works properly now!

Depending on TrustKit 2.0.1 shouldn't be a problem, but I would advise you to update to the latest version by removing pod 'TrustKit', '> 2.0.1' and then doing pod update since TrustKit is now at version 3.0.3 and there may have been bugfixes in between.