Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 131 additions & 70 deletions ios/SSLPinning.mm
Original file line number Diff line number Diff line change
Expand Up @@ -14,102 +14,163 @@
#import "SRWebSocket.h"
#import "EXSessionTaskDispatcher.h"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing #include <os/log.h> import

static os_log_t SSLLog(void) {
static os_log_t sLog;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sLog = os_log_create("chat.rocket.ssl", "authentication");
});
return sLog;
}

@implementation Challenge : NSObject
+(NSURLCredential *)getUrlCredential:(NSURLAuthenticationChallenge *)challenge path:(NSString *)path password:(NSString *)password

+(NSString *)stringToHex:(NSString *)string
{
NSString *authMethod = [[challenge protectionSpace] authenticationMethod];
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
char *utf8 = (char *)[string UTF8String];
NSMutableString *hex = [NSMutableString string];
while (*utf8) [hex appendFormat:@"%02X", *utf8++ & 0x00FF];

if ([authMethod isEqualToString:NSURLAuthenticationMethodServerTrust] || path == nil || password == nil) {
return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else if (path && password) {
NSMutableArray *policies = [NSMutableArray array];
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host)];
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
return [[NSString stringWithFormat:@"%@", hex] lowercaseString];
}
Comment on lines +28 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix nil-safety in stringToHex to avoid NULL deref.

If string is nil, while(*utf8) dereferences NULL. Use NSData bytes and guard.

Apply this diff:

-+(NSString *)stringToHex:(NSString *)string
-{
-  char *utf8 = (char *)[string UTF8String];
-  NSMutableString *hex = [NSMutableString string];
-  while (*utf8) [hex appendFormat:@"%02X", *utf8++ & 0x00FF];
-
-  return [[NSString stringWithFormat:@"%@", hex] lowercaseString];
-}
++(NSString *)stringToHex:(NSString *)string
+{
+  if (string.length == 0) { return @""; }
+  NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
+  const unsigned char *bytes = (const unsigned char *)data.bytes;
+  NSMutableString *hex = [NSMutableString stringWithCapacity:data.length * 2];
+  for (NSUInteger i = 0; i < data.length; i++) {
+    [hex appendFormat:@"%02x", bytes[i]];
+  }
+  return hex;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
+(NSString *)stringToHex:(NSString *)string
{
NSString *authMethod = [[challenge protectionSpace] authenticationMethod];
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
char *utf8 = (char *)[string UTF8String];
NSMutableString *hex = [NSMutableString string];
while (*utf8) [hex appendFormat:@"%02X", *utf8++ & 0x00FF];
if ([authMethod isEqualToString:NSURLAuthenticationMethodServerTrust] || path == nil || password == nil) {
return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else if (path && password) {
NSMutableArray *policies = [NSMutableArray array];
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host)];
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
return [[NSString stringWithFormat:@"%@", hex] lowercaseString];
}
(NSString *)stringToHex:(NSString *)string
{
if (string.length == 0) { return @""; }
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
const unsigned char *bytes = (const unsigned char *)data.bytes;
NSMutableString *hex = [NSMutableString stringWithCapacity:data.length * 2];
for (NSUInteger i = 0; i < data.length; i++) {
[hex appendFormat:@"%02x", bytes[i]];
}
return hex;
}
🤖 Prompt for AI Agents
In ios/SSLPinning.mm around lines 28-35, stringToHex currently dereferences a
NULL when string is nil by using UTF8String and while(*utf8); change to guard
against nil and use NSData bytes: if string is nil return @"" (or nil per
project convention), obtain NSData *data = [string
dataUsingEncoding:NSUTF8StringEncoding], get const unsigned char *bytes =
data.bytes and iterate for (NSUInteger i=0;i<data.length;i++) appending each
byte with %02X, then return the lowercased hex string; ensure you handle empty
data and avoid dereferencing NULL bytes.


SecTrustResultType result;
SecTrustEvaluate(serverTrust, &result);
+ (NSURLCredential *)getUrlCredential:(NSURLAuthenticationChallenge *)challenge path:(NSString *)path password:(NSString *)password {
os_log_t sslLog = SSLLog();

if (![[NSFileManager defaultManager] fileExistsAtPath:path])
{
return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
}
NSString *authMethod = challenge.protectionSpace.authenticationMethod;
if (![authMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) {
os_log_info(sslLog, "Not a client-certificate challenge");
return nil;
}

NSData *p12data = [NSData dataWithContentsOfFile:path];
NSDictionary* options = @{ (id)kSecImportExportPassphrase:password };
CFArrayRef rawItems = NULL;
OSStatus status = SecPKCS12Import((__bridge CFDataRef)p12data,
(__bridge CFDictionaryRef)options,
&rawItems);
if (path.length == 0 || password.length == 0) {
os_log_info(sslLog, "No path/password configured for client cert");
return nil;
}

if (status != noErr) {
return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
}
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
os_log_error(sslLog, "Client cert file not found at path: %{private}@", path);
return nil;
}

NSArray* items = (NSArray*)CFBridgingRelease(rawItems);
NSDictionary* firstItem = nil;
if ((status == errSecSuccess) && ([items count]>0)) {
firstItem = items[0];
}
NSData *p12data = [NSData dataWithContentsOfFile:path];
if (!p12data) {
os_log_error(sslLog, "Failed to read PKCS12 data");
return nil;
}

SecIdentityRef identity = (SecIdentityRef)CFBridgingRetain(firstItem[(id)kSecImportItemIdentity]);
SecCertificateRef certificate = NULL;
if (identity) {
SecIdentityCopyCertificate(identity, &certificate);
if (certificate) { CFRelease(certificate); }
}
NSDictionary *options = @{ (id)kSecImportExportPassphrase : password };
CFArrayRef rawItems = NULL;
OSStatus status = SecPKCS12Import((__bridge CFDataRef)p12data,
(__bridge CFDictionaryRef)options,
&rawItems);
if (status != errSecSuccess || rawItems == NULL) {
os_log_error(sslLog, "SecPKCS12Import failed: %d", (int)status);
if (rawItems) CFRelease(rawItems);
return nil;
}

NSMutableArray *certificates = [[NSMutableArray alloc] init];
[certificates addObject:CFBridgingRelease(certificate)];
NSArray *items = (__bridge_transfer NSArray *)rawItems;
if (items.count == 0) {
os_log_error(sslLog, "PKCS12 import returned zero items");
return nil;
}

[SDWebImageDownloader sharedDownloader].config.urlCredential = [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceNone];
NSDictionary *firstItem = items[0];
id identityObj = firstItem[(id)kSecImportItemIdentity];
if (!identityObj) {
os_log_error(sslLog, "No identity found in PKCS12");
return nil;
}

return [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceNone];
SecIdentityRef identity = (__bridge SecIdentityRef)identityObj;
// Copy certificate from identity
SecCertificateRef certificate = NULL;
OSStatus certStatus = SecIdentityCopyCertificate(identity, &certificate);
if (certStatus != errSecSuccess || certificate == NULL) {
os_log_error(sslLog, "SecIdentityCopyCertificate failed: %d", (int)certStatus);
if (certificate) CFRelease(certificate);
return nil;
}

return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
}
// Build NSArray of certificates (the array should contain certificates, not the identity).
// Some APIs accept an array starting with identity, but NSURLCredential expects
// an array of SecCertificateRef objects (chain). We'll pass the certificate we copied.
id certObj = (__bridge_transfer id)certificate; // certificate will be released by ARC
NSArray *certs = certObj ? @[certObj] : @[];

Comment on lines +73 to 101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use PKCS#12-provided cert chain (kSecImportItemCertChain); avoid manual SecIdentityCopyCertificate.

NSURLCredential’s certificates parameter should include the chain from the import; copying only the leaf can break client-auth on servers expecting intermediates.

Apply this refactor:

-  SecIdentityRef identity = (__bridge SecIdentityRef)identityObj;
-  // Copy certificate from identity
-  SecCertificateRef certificate = NULL;
-  OSStatus certStatus = SecIdentityCopyCertificate(identity, &certificate);
-  if (certStatus != errSecSuccess || certificate == NULL) {
-    os_log_error(sslLog, "SecIdentityCopyCertificate failed: %d", (int)certStatus);
-    if (certificate) CFRelease(certificate);
-    return nil;
-  }
-
-  // Build NSArray of certificates (the array should contain certificates, not the identity).
-  // Some APIs accept an array starting with identity, but NSURLCredential expects
-  // an array of SecCertificateRef objects (chain). We'll pass the certificate we copied.
-  id certObj = (__bridge_transfer id)certificate; // certificate will be released by ARC
-  NSArray *certs = certObj ? @[certObj] : @[];
+  SecIdentityRef identity = (__bridge SecIdentityRef)identityObj;
+  // Prefer the full chain provided by SecPKCS12Import (excluding the identity).
+  id chainObj = firstItem[(id)kSecImportItemCertChain];
+  NSArray *certs = ([chainObj isKindOfClass:[NSArray class]] && [chainObj count] > 0) ? (NSArray *)chainObj : nil;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
NSArray *items = (__bridge_transfer NSArray *)rawItems;
if (items.count == 0) {
os_log_error(sslLog, "PKCS12 import returned zero items");
return nil;
}
[SDWebImageDownloader sharedDownloader].config.urlCredential = [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceNone];
NSDictionary *firstItem = items[0];
id identityObj = firstItem[(id)kSecImportItemIdentity];
if (!identityObj) {
os_log_error(sslLog, "No identity found in PKCS12");
return nil;
}
return [NSURLCredential credentialWithIdentity:identity certificates:certificates persistence:NSURLCredentialPersistenceNone];
SecIdentityRef identity = (__bridge SecIdentityRef)identityObj;
// Copy certificate from identity
SecCertificateRef certificate = NULL;
OSStatus certStatus = SecIdentityCopyCertificate(identity, &certificate);
if (certStatus != errSecSuccess || certificate == NULL) {
os_log_error(sslLog, "SecIdentityCopyCertificate failed: %d", (int)certStatus);
if (certificate) CFRelease(certificate);
return nil;
}
return [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
}
// Build NSArray of certificates (the array should contain certificates, not the identity).
// Some APIs accept an array starting with identity, but NSURLCredential expects
// an array of SecCertificateRef objects (chain). We'll pass the certificate we copied.
id certObj = (__bridge_transfer id)certificate; // certificate will be released by ARC
NSArray *certs = certObj ? @[certObj] : @[];
NSArray *items = (__bridge_transfer NSArray *)rawItems;
if (items.count == 0) {
os_log_error(sslLog, "PKCS12 import returned zero items");
return nil;
}
NSDictionary *firstItem = items[0];
id identityObj = firstItem[(id)kSecImportItemIdentity];
if (!identityObj) {
os_log_error(sslLog, "No identity found in PKCS12");
return nil;
}
SecIdentityRef identity = (__bridge SecIdentityRef)identityObj;
// Prefer the full chain provided by SecPKCS12Import (excluding the identity).
id chainObj = firstItem[(id)kSecImportItemCertChain];
NSArray *certs = ([chainObj isKindOfClass:[NSArray class]] && [chainObj count] > 0) ? (NSArray *)chainObj : nil;
🤖 Prompt for AI Agents
In ios/SSLPinning.mm around lines 73 to 101, the code copies only the leaf
certificate via SecIdentityCopyCertificate and builds certs with a single cert,
which drops the PKCS#12-provided chain; replace that logic to read
kSecImportItemCertChain from firstItem, bridge it to an NSArray/CFArrayRef of
SecCertificateRef objects and use that chain for the NSURLCredential
certificates parameter (do not call SecIdentityCopyCertificate), preserving
correct CF/ARC memory management when bridging the cert chain.

+(NSString *)stringToHex:(NSString *)string
{
char *utf8 = (char *)[string UTF8String];
NSMutableString *hex = [NSMutableString string];
while (*utf8) [hex appendFormat:@"%02X", *utf8++ & 0x00FF];
// Create credential. Choose persistence according to desired policy.
// Use NSURLCredentialPersistenceNone for one-off, or ForSession if you want reuse within a session.
NSURLCredential *cred = [NSURLCredential credentialWithIdentity:identity
certificates:certs
persistence:NSURLCredentialPersistenceNone];

return [[NSString stringWithFormat:@"%@", hex] lowercaseString];
os_log_info(sslLog, "Created client credential (certs: %lu)", (unsigned long)certs.count);
return cred;
}

+(void)runChallenge:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
+ (void)runChallenge:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
NSString *host = challenge.protectionSpace.host;
os_log_t sslLog = SSLLog();
static NSInteger seq = 0; seq++;
NSString *host = challenge.protectionSpace.host ?: @"(unknown)";
NSString *authMethod = challenge.protectionSpace.authenticationMethod ?: @"(none)";

// Read the clientSSL info from MMKV
__block NSString *clientSSL;
SecureStorage *secureStorage = [[SecureStorage alloc] init];
os_log_info(sslLog, "Challenge #%ld host=%{public}@ authMethod=%{public}@", (long)seq, host, authMethod);

// https://github.com/ammarahm-ed/react-native-mmkv-storage/blob/master/src/loader.js#L31
NSString *key = [secureStorage getSecureKey:[self stringToHex:@"com.MMKV.default"]];
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if ([authMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) {
// Read stored client config (your existing MMKV code) — if not found, perform default handling.
SecureStorage *secureStorage = [[SecureStorage alloc] init];
NSString *key = [secureStorage getSecureKey:[self stringToHex:@"com.MMKV.default"]];
if (key == nil) {
os_log_info(sslLog, "No secure storage key -> default handling");
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
return;
}

if (key == NULL) {
return completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, credential);
}
NSData *cryptKey = [key dataUsingEncoding:NSUTF8StringEncoding];
MMKV *mmkv = [MMKV mmkvWithID:@"default" cryptKey:cryptKey mode:MMKVMultiProcess];
NSString *clientSSL = [mmkv getStringForKey:host];

NSData *cryptKey = [key dataUsingEncoding:NSUTF8StringEncoding];
MMKV *mmkv = [MMKV mmkvWithID:@"default" cryptKey:cryptKey mode:MMKVMultiProcess];
clientSSL = [mmkv getStringForKey:host];
if (!clientSSL) {
os_log_info(sslLog, "No client SSL configuration for host %{public}@", host);
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
return;
}

if (clientSSL) {
NSData *data = [clientSSL dataUsingEncoding:NSUTF8StringEncoding];
id dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSString *path = [dict objectForKey:@"path"];
NSString *password = [dict objectForKey:@"password"];
credential = [self getUrlCredential:challenge path:path password:password];
NSData *jsonData = [clientSSL dataUsingEncoding:NSUTF8StringEncoding];
NSError *jsonErr = nil;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&jsonErr];
if (jsonErr || ![dict isKindOfClass:[NSDictionary class]]) {
os_log_error(sslLog, "Malformed clientSSL JSON for host %{public}@: %{public}@", host, jsonErr.localizedDescription);
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
return;
}

NSString *path = dict[@"path"];
NSString *password = dict[@"password"];
if (!path || !password) {
os_log_error(sslLog, "clientSSL entry missing path/password");
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
return;
}

NSURLCredential *clientCred = [self getUrlCredential:challenge path:path password:password];
if (clientCred) {
os_log_info(sslLog, "Presenting client certificate for host %{public}@", host);
completionHandler(NSURLSessionAuthChallengeUseCredential, clientCred);
return;
} else {
os_log_error(sslLog, "Failed to build client credential - default handling");
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
return;
}
}

completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
// For all other auth methods, allow system default handling:
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
@end

Expand Down
Loading