Skip to content

How To: Provide a certificate at runtime (or at least through an updated js bundle) #177

@Catdaemon

Description

@Catdaemon

Hello,

Thanks for this package - it's great. We have some specific requirements for a project I'm working on, and I'd like to feed back a patch-package patch and some scripts I've made.

It implements this PR: #147 without the allowInvalidCertificates part (thanks!). I think with the allowInvalidCertificates part made optional this should be merged, but there has been no activity on this project for some time...

The use-case here is updating the pins via an OTA js bundle rather than relying on the app stores.

Here is the patch-package diff:

diff --git a/node_modules/react-native-ssl-pinning/index.d.ts b/node_modules/react-native-ssl-pinning/index.d.ts
index 7d0aebf..ce70fdd 100644
--- a/node_modules/react-native-ssl-pinning/index.d.ts
+++ b/node_modules/react-native-ssl-pinning/index.d.ts
@@ -17,6 +17,7 @@ export namespace ReactNativeSSLPinning {
         sslPinning: {
             certs: string[]
         },
+        certBase64?: string,
         timeoutInterval?: number,
         disableAllSecurity?: boolean,
         caseSensitiveHeaders?: boolean = false
diff --git a/node_modules/react-native-ssl-pinning/ios/RNSslPinning/RNSslPinning.m b/node_modules/react-native-ssl-pinning/ios/RNSslPinning/RNSslPinning.m
index 3179a72..806a399 100644
--- a/node_modules/react-native-ssl-pinning/ios/RNSslPinning/RNSslPinning.m
+++ b/node_modules/react-native-ssl-pinning/ios/RNSslPinning/RNSslPinning.m
@@ -215,6 +215,7 @@ RCT_EXPORT_METHOD(fetch:(NSString *)url obj:(NSDictionary *)obj callback:(RCTRes
     AFSecurityPolicy *policy;
     BOOL pkPinning = [[obj objectForKey:@"pkPinning"] boolValue];
     BOOL disableAllSecurity = [[obj objectForKey:@"disableAllSecurity"] boolValue];
+    NSString *certBase64=[obj objectForKey:@"certBase64"];
     
     NSSet *certificates = [AFSecurityPolicy certificatesInBundle:[NSBundle mainBundle]];
     
@@ -224,6 +225,12 @@ RCT_EXPORT_METHOD(fetch:(NSString *)url obj:(NSDictionary *)obj callback:(RCTRes
         policy.validatesDomainName = false;
         policy.allowInvalidCertificates = true;
     }
+    else if (certBase64) {
+        NSURL *certData = [NSURL URLWithString: [NSString stringWithFormat:@"data:application/octet-stream;base64,%@", certBase64]];
+        NSData *certRaw = [NSData dataWithContentsOfURL:certData];
+        policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate
+            withPinnedCertificates:[NSSet setWithObject:certRaw]];
+    }
     else if (pkPinning){
         policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey withPinnedCertificates:certificates];
     }

This issue body was partially generated by patch-package.

The following bash script can be used to generate a json file containing the necessary values:

# setup
CERTS_DIR="./.certs"
CERTS_OUTPUT="$CERTS_DIR/certs.json"
DOMAINS=(
    "google.com"
    "bing.com"
)

(mkdir $CERTS_DIR 2> /dev/null || true)
(rm $CERTS_DIR/* 2> /dev/null || true)
(rm $CERTS_OUTPUT 2> /dev/null || true)

echo "------------------------------------------------------"
echo "Generating certificate pins for the following domains:"
for domain in ${DOMAINS[@]}; do
    echo $domain
done

# get certificates from urls
for domain in ${DOMAINS[@]}; do
    openssl s_client -showcerts -servername $domain -connect $domain:443 < /dev/null 2> /dev/null | openssl x509 -outform der > $CERTS_DIR/$domain.cer
    openssl s_client -servername $domain -connect $domain:443 < /dev/null 2> /dev/null | openssl x509 -outform PEM > $CERTS_DIR/$domain.crt
    openssl x509 -in $CERTS_DIR/$domain.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 > $CERTS_DIR/$domain.fingerprint
done

printf "{ certs: [" > $CERTS_OUTPUT
for domain in ${DOMAINS[@]}; do
    filepath=$CERTS_DIR/$domain
    certbase64=$(cat $filepath.cer | base64 | tr -d '\n')
    certsha256=$(cat $filepath.fingerprint | tr -d '\n')
    printf "{ domain: '$domain', sha256: '$certsha256', certBase64: '$certbase64' }," >> $CERTS_OUTPUT
done
printf "] }" >> $CERTS_OUTPUT

rm $CERTS_DIR/*.cer
rm $CERTS_DIR/*.crt
rm $CERTS_DIR/*.fingerprint

echo "Certificate pinning complete"
echo "------------------------------------------------------"

Then it can be consumed by js like so:

import { fetch as pinnedFetch, ReactNativeSSLPinning } from 'react-native-ssl-pinning'
import Constants from 'expo-constants'
import pins from '../../.certs/certs.json'

type PinningFunctionOptions = Omit<ReactNativeSSLPinning.Options, 'pkPinning' | 'sslPinning'>

export default async function fetchWithCertPinning(url: string, options: PinningFunctionOptions | RequestInit) {
  // Use standard fetch for mock
  if (Constants.manifest?.extra?.API_USE_MOCK === 'true') {
    // eslint-disable-next-line no-console
    console.warn('NO SSL PINNING USED')
    return fetch(url, options as RequestInit)
  }

  // Extract domain from url
  const domain = url.match(/^https?:\/\/([^/?#]+)(?:[/?#]|$)/i)?.[1]

  const pin = pins.certs.find((x) => x.domain === domain)

  if (!pin) {
    throw new Error(`No pinned certificate found for domain ${domain}`)
  }

  const fetchOptions: ReactNativeSSLPinning.Options = {
    ...(options as PinningFunctionOptions),
    pkPinning: true, // On android, uses the certs array below, which contains sha256 hashes of the certificate private keys
    sslPinning: {
      certs: [`sha256/${pin.sha256}`]
    },
    certBase64: pin.certBase64 // On ios, feeds in a base64 copy of the ssl certificate
  }

  try {
    const result = await pinnedFetch(url, fetchOptions)
    return result
  } catch (e) {
    // eslint-disable-next-line no-console
    console.error(e)
    if (typeof e === 'string') {
      return {
        status: 'CertificatePinningError',
        json: () => {}
      }
    }
    return e as ReactNativeSSLPinning.Response
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions