react-native-ssl-public-key-pinning: [Bug][iOS] Not working with expo-dev-client in debug build

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):

  • Also opened an issue in TrustKit repo.
  • Created a test repo in attempt to reproduce the issue, but still can’t: test-expo-ssl-pinning-1
  • Tried with Android and it shows Certificate pinning failure! error message as expected.

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!

About this issue

  • Original URL
  • State: open
  • Created 6 months ago
  • Comments: 26 (9 by maintainers)

Commits related to this issue

Most upvoted comments

@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.

@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.

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.

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.

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?

Notes

Might be related to: https://github.com/expo/expo/issues/24096

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

@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.)

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

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 @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