Skip to content

Commit 2a9fee0

Browse files
committed
feat: add session scoped auth challenge validation for anchor certificates
- remove task scoped auth challenge - refactor pinning certificate validation to operate jointly with anchor certificate validation
1 parent e447699 commit 2a9fee0

File tree

1 file changed

+132
-22
lines changed

1 file changed

+132
-22
lines changed

ios/RCTHTTPRequestHandler+AuthenticationChallenge.m

+132-22
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
#import <objc/runtime.h>
22
#import <Foundation/Foundation.h>
33
#import <React/RCTHTTPRequestHandler.h>
4+
#import <Security/Security.h>
45

6+
/**
7+
* There will be only one instance of this class on runtime accordinly the React Native code.
8+
* This provides a good opportunity to cache data on the class itself.
9+
*/
510
@implementation RCTHTTPRequestHandler (AuthenticationChallengeExtension)
611

12+
// Cache allocated certificates on memory to hold them through the class lifecycle.
13+
static NSMutableDictionary<NSString *, id> *_certificateCache = nil;
14+
// Cache a set of allowed domains
715
static NSSet *_cachedAllowedDomains = nil;
816

9-
/* The load method is called by the Objective-C runtime when the class is loaded into memory,
17+
/**
18+
* The load method is called by the Objective-C runtime when the class is loaded into memory,
1019
* it happens early in the application's lifecycle.
1120
*/
1221
+ (void)load {
1322
// dispatch_once avoids the load method to run twice
1423
static dispatch_once_t onceToken;
1524
dispatch_once(&onceToken, ^{
16-
NSLog(@"[RCTHTTPRequestHandler extension] It will swizzle authentication challenge on this data delegate.");
1725
Class class = [RCTHTTPRequestHandler class];
1826

19-
SEL originalSelector = @selector(URLSession:task:didReceiveChallenge:completionHandler:);
20-
SEL swizzledSelector = @selector(authenticationChallenge_URLSession:task:didReceiveChallenge:completionHandler:);
27+
NSLog(@"[RCTHTTPRequestHandler (AuthenticationChallengeExtension)] Initializing _certificateCache.");
28+
_certificateCache = [NSMutableDictionary new];
29+
30+
NSLog(@"[RCTHTTPRequestHandler (AuthenticationChallengeExtension)] Swizzling authentication challenge handler on session scope.");
31+
SEL originalSelector = @selector(URLSession:didReceiveChallenge:completionHandler:);
32+
SEL swizzledSelector = @selector(authenticationChallenge_URLSession:didReceiveChallenge:completionHandler:);
2133

2234
Method originalMethod = class_getInstanceMethod(class, originalSelector);
2335
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
@@ -29,10 +41,24 @@ + (void)load {
2941
} else {
3042
method_exchangeImplementations(originalMethod, swizzledMethod);
3143
}
44+
45+
NSLog(@"[RCTHTTPRequestHandler (AuthenticationChallengeExtension)] Swizzling the dealloc method.");
46+
SEL deallocOriginalSelector = NSSelectorFromString(@"dealloc");
47+
SEL deallocSwizzledSelector = @selector(swizzled_dealloc);
48+
49+
Method deallocOriginalMethod = class_getInstanceMethod(class, deallocOriginalSelector);
50+
Method deallocSwizzledMethod = class_getInstanceMethod(class, deallocSwizzledSelector);
51+
52+
if (!class_addMethod(class, deallocOriginalSelector, method_getImplementation(deallocSwizzledMethod), method_getTypeEncoding(deallocSwizzledMethod))) {
53+
method_exchangeImplementations(deallocOriginalMethod, deallocSwizzledMethod);
54+
} else {
55+
class_replaceMethod(class, deallocSwizzledSelector, method_getImplementation(deallocOriginalMethod), method_getTypeEncoding(deallocOriginalMethod));
56+
}
3257
});
3358
}
3459

35-
/* This method extends the RCTHTTPRequestHandler by intercepting the authentication challenge,
60+
/**
61+
* This method extends the RCTHTTPRequestHandler by intercepting the authentication challenge,
3662
* and gives to it a custom behavior regarding allowing or denying connections.
3763
* The current implementation of RCTHTTPRequestHandler doesn't provide an implementation for
3864
* the authentication challenge event, therefore we don't risk to interfere with React Native behavior.
@@ -45,38 +71,99 @@ + (void)load {
4571
* In React Native, when a session is created, this delegate is injected in.
4672
*/
4773
- (void)authenticationChallenge_URLSession:(NSURLSession *)session
48-
task:(NSURLSessionTask *)task
4974
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
5075
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
51-
// Set of allowed domains
52-
NSSet *allowedDomains = [self getCachedAllowedDomains];
53-
// Check if the challenge is of type ServerTrust
76+
static int _counter = 0;
77+
NSLog(@"Session handler call %i, host %@.", _counter, challenge.protectionSpace.host);
78+
_counter++;
79+
5480
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
55-
// Check if the host is an allowed domain
56-
if ([self checkValidityOfHost:challenge.protectionSpace.host allowedInDomains:allowedDomains]) {
57-
// Check if proceed with the original authentication handling
58-
if ([self checkValidityOfTrust:challenge.protectionSpace.serverTrust]) {
59-
// The challenge is of type ServerTrust, the host is allowed and the certificate is trust worthy
60-
// Create the credential and completes the delegate
61-
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
62-
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
81+
NSArray *anchorCertificates = [self getAnchorCertificates];
82+
// Set resource certificates to the trust entity to be evaluated
83+
OSStatus status = SecTrustSetAnchorCertificates(challenge.protectionSpace.serverTrust, (__bridge CFArrayRef)anchorCertificates);
84+
if (status == errSecSuccess) {
85+
/* The following method signs to the evalution if the system bundle certificates must be taken in consideration
86+
* when evaluating the trust entity. The second param determines this behavior, set YES to constrain the evaluation
87+
* to the resource certificates only, and NO to allow system bundle certificates.
88+
*/
89+
SecTrustSetAnchorCertificatesOnly(challenge.protectionSpace.serverTrust, YES);
90+
// A set of allowed domains
91+
NSSet *allowedDomains = [self getCachedAllowedDomains];
92+
// Check if the host is an allowed domain
93+
if ([self checkValidityOfHost:challenge.protectionSpace.host allowedInDomains:allowedDomains]) {
94+
// Check if proceed with the original authentication handling
95+
if ([self checkValidityOfTrust:challenge.protectionSpace.serverTrust]) {
96+
// The challenge is of type ServerTrust, the host is allowed and the certificate is trust worthy
97+
// Create the credential and completes the delegate
98+
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
99+
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
100+
} else {
101+
// Certificate is invalid, cancelling authentication
102+
NSLog(@"Invalid certificate for Domain %@. Cancelling authentication.", challenge.protectionSpace.host);
103+
// Cancel the authentication challenge
104+
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
105+
}
63106
} else {
64-
// Certificate is invalid, cancelling authentication
65-
NSLog(@"Invalid certificate for Domain %@. Cancelling authentication.", challenge.protectionSpace.host);
107+
// Host is not allowed, cancelling authentication
108+
NSLog(@"Host domain %@ is not allowed. Cancelling authentication.", challenge.protectionSpace.host);
66109
// Cancel the authentication challenge
67110
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
68111
}
69112
} else {
70-
// Host is not allowed, cancelling authentication
71-
NSLog(@"Host domain %@ is not allowed. Cancelling authentication.", challenge.protectionSpace.host);
113+
NSLog(@"Certificate not set.");
72114
// Cancel the authentication challenge
73115
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
74116
}
75117
} else {
76118
// Trust is not of type ServerTrust, proceeding with default challenge, if any
77119
NSLog(@"Not ServerTrust challenge. Calling default authentication challenge.");
78120
// For other authentication methods, call the original implementation
79-
[self authenticationChallenge_URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
121+
[self authenticationChallenge_URLSession:session didReceiveChallenge:challenge completionHandler:completionHandler];
122+
}
123+
}
124+
125+
/**
126+
* Fetch anchor certificates either from the resources or the cache.
127+
*/
128+
- (NSArray *)getAnchorCertificates {
129+
// Load root certificates from resources in the first run and from the cache afterwards
130+
SecCertificateRef hathor_root_ca_1 = [self getCert:@"hathor_network_root_ca_1"];
131+
SecCertificateRef hathor_root_ca_2 = [self getCert:@"hathor_network_root_ca_2"];
132+
133+
// Create an NSArray with the certificates
134+
NSArray *certArray = @[(__bridge id)hathor_root_ca_1, (__bridge id)hathor_root_ca_2];
135+
136+
return certArray;
137+
}
138+
139+
/**
140+
* The certificate must be of type .der and should be in DER encoding.
141+
*/
142+
- (SecCertificateRef)getCert:(NSString *)certFilename {
143+
@synchronized (_certificateCache) {
144+
id cachedCert = [_certificateCache objectForKey:certFilename];;
145+
if (cachedCert) {
146+
return (__bridge SecCertificateRef)cachedCert;
147+
}
148+
149+
NSString *certPath = [[NSBundle mainBundle] pathForResource:certFilename ofType:@"der"];
150+
NSData *certData = [NSData dataWithContentsOfFile:certPath];
151+
152+
if (!certData) {
153+
@throw [NSException exceptionWithName:NSInternalInconsistencyException
154+
reason:[NSString stringWithFormat:@"%@ not found", certFilename]
155+
userInfo:nil];
156+
}
157+
158+
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData);
159+
if (!cert) {
160+
@throw [NSException exceptionWithName:NSInternalInconsistencyException
161+
reason:[NSString stringWithFormat:@"Error on parsing the %@ certificate", certFilename]
162+
userInfo:nil];
163+
}
164+
165+
[_certificateCache setObject:(__bridge id)cert forKey:certFilename];
166+
return cert;
80167
}
81168
}
82169

@@ -206,4 +293,27 @@ - (BOOL)checkValidityOfHost:(NSString *)host allowedInDomains:(NSSet *)allowedDo
206293
return NO;
207294
}
208295

296+
/**
297+
* Release each allocated certificate in memory and clean the cache dictionary in a thread safe manner.
298+
*/
299+
- (void)clearCertificateCache {
300+
NSLog(@"[RCTHTTPRequestHandler (AuthenticationChallengeExtension)] Clearing the certificate cache.");
301+
@synchronized(_certificateCache) {
302+
for (id cert in _certificateCache.allValues) {
303+
CFRelease((__bridge SecCertificateRef)cert);
304+
}
305+
[_certificateCache removeAllObjects];
306+
}
307+
}
308+
309+
/**
310+
* As this class has only one instance and it will probably live until the user quits the app,
311+
* it will probably never be called. However, we shouldn't worry much about it because
312+
* the app memory will be released anyway after quit.
313+
*/
314+
- (void)swizzled_dealloc {
315+
[self clearCertificateCache];
316+
[self swizzled_dealloc]; // This will call the original dealloc method
317+
}
318+
209319
@end

0 commit comments

Comments
 (0)