TSKBackgroundReporter.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. /*
  2. TSKBackgroundReporter.m
  3. TrustKit
  4. Copyright 2015 The TrustKit Project Authors
  5. Licensed under the MIT license, see associated LICENSE file for terms.
  6. See AUTHORS file for the list of project authors.
  7. */
  8. #import "TSKBackgroundReporter.h"
  9. #import "../TSKTrustKitConfig.h"
  10. #import "../TSKLog.h"
  11. #import "TSKPinFailureReport.h"
  12. #import "reporting_utils.h"
  13. #import "TSKReportsRateLimiter.h"
  14. #import "vendor_identifier.h"
  15. // Session identifier for background uploads: <bundle_id>.TSKBackgroundReporter
  16. static NSString * const kTSKBackgroundSessionIdentifierFormat = @"%@.TSKBackgroundReporter.%@";
  17. @interface TSKBackgroundReporter()
  18. @property (nonatomic, nonnull) NSURLSession *backgroundSession;
  19. @property (nonatomic, nonnull) NSString *appBundleId;
  20. @property (nonatomic, nonnull) NSString *appVersion;
  21. @property (nonatomic, nonnull) NSString *appVendorId;
  22. @property (nonatomic, nonnull) NSString *appPlatform;
  23. @property (nonatomic, nonnull) NSString *appPlatformVersion;
  24. @property (nonatomic, nonnull) TSKReportsRateLimiter *rateLimiter;
  25. @property (nonatomic) BOOL shouldRateLimitReports;
  26. @end
  27. @implementation TSKBackgroundReporter
  28. #pragma mark Public methods
  29. - (nonnull instancetype)initAndRateLimitReports:(BOOL)shouldRateLimitReports
  30. sharedContainerIdentifier:(NSString *)sharedContainerIdentifier
  31. {
  32. self = [super init];
  33. if (self)
  34. {
  35. _shouldRateLimitReports = shouldRateLimitReports;
  36. _rateLimiter = [TSKReportsRateLimiter new];
  37. // Retrieve the App and device's information
  38. #if TARGET_OS_IPHONE
  39. #if TARGET_OS_TV
  40. _appPlatform = @"TVOS";
  41. #elif TARGET_OS_WATCH
  42. _appPlatform = @"WATCHOS";
  43. #else
  44. _appPlatform = @"IOS";
  45. #endif
  46. #else
  47. _appPlatform = @"MACOS";
  48. #endif
  49. // If we don't have the OS version yet, we are on a device that provides the operatingSystemVersion method
  50. if (_appPlatformVersion == nil)
  51. {
  52. NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion];
  53. _appPlatformVersion = [NSString stringWithFormat:@"%ld.%ld.%ld",
  54. (long)version.majorVersion,
  55. (long)version.minorVersion,
  56. (long)version.patchVersion];
  57. }
  58. CFBundleRef appBundle = CFBundleGetMainBundle();
  59. _appVersion = (__bridge NSString *)CFBundleGetValueForInfoDictionaryKey(appBundle, (CFStringRef) @"CFBundleShortVersionString");
  60. if (_appVersion == nil)
  61. {
  62. _appVersion = @"";
  63. }
  64. _appBundleId = (__bridge NSString *)CFBundleGetIdentifier(appBundle);
  65. if (_appBundleId == nil)
  66. {
  67. // The bundle ID we get is nil if we're running tests on Travis. If the bundle ID is nil, background sessions can't be used
  68. // backgroundSessionConfigurationWithIdentifier: will throw an exception
  69. TSKLog(@"Null bundle ID: we are running the test suite; falling back to a normal session.");
  70. _appBundleId = @"N/A";
  71. _appVendorId = @"unit-tests";
  72. _backgroundSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]
  73. delegate:self
  74. delegateQueue:nil];
  75. }
  76. else
  77. {
  78. // Get the vendor identifier
  79. _appVendorId = identifier_for_vendor();
  80. // We're not running unit tests - use a background session
  81. // AppleDoc (currently 10.3) state that multiple background sessions with the same
  82. // identifier should never be created, so ensure that cannot happen by creating
  83. // a unique ID per instance.
  84. NSString *backgroundSessionId = [NSString stringWithFormat:kTSKBackgroundSessionIdentifierFormat,
  85. _appBundleId, [[NSUUID UUID] UUIDString]];
  86. NSURLSessionConfiguration *backgroundConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:backgroundSessionId];
  87. backgroundConfiguration.discretionary = YES;
  88. backgroundConfiguration.sharedContainerIdentifier = sharedContainerIdentifier;
  89. #if TARGET_OS_IPHONE
  90. // iOS-only settings
  91. // Do not wake up the App after completing the upload
  92. backgroundConfiguration.sessionSendsLaunchEvents = NO;
  93. #endif
  94. // We have to use a delegate as background sessions can't use completion handlers
  95. _backgroundSession = [NSURLSession sessionWithConfiguration:backgroundConfiguration
  96. delegate:self
  97. delegateQueue:nil];
  98. }
  99. }
  100. return self;
  101. }
  102. - (void) pinValidationFailedForHostname:(nonnull NSString *)serverHostname
  103. port:(nullable NSNumber *)serverPort
  104. certificateChain:(nonnull NSArray *)certificateChain
  105. notedHostname:(nonnull NSString *)notedHostname
  106. reportURIs:(nonnull NSArray<NSURL *> *)reportURIs
  107. includeSubdomains:(BOOL)includeSubdomains
  108. enforcePinning:(BOOL)enforcePinning
  109. knownPins:(nonnull NSSet<NSData *> *)knownPins
  110. validationResult:(TSKTrustEvaluationResult)validationResult
  111. expirationDate:(nullable NSDate *)knownPinsExpirationDate
  112. {
  113. // Default port to 0 if not specified
  114. if (serverPort == nil)
  115. {
  116. serverPort = @(0);
  117. }
  118. if (reportURIs == nil)
  119. {
  120. [NSException raise:@"TSKBackgroundReporter configuration invalid"
  121. format:@"Reporter was given an invalid value for reportURIs: %@ for domain %@",
  122. reportURIs, notedHostname];
  123. }
  124. // Create the pin validation failure report
  125. NSArray *formattedPins = convertPinsToHpkpPins(knownPins);
  126. TSKPinFailureReport *report = [[TSKPinFailureReport alloc]initWithAppBundleId:_appBundleId
  127. appVersion:_appVersion
  128. appPlatform:_appPlatform
  129. appPlatformVersion:_appPlatformVersion
  130. appVendorId:_appVendorId
  131. trustkitVersion:TrustKitVersion
  132. hostname:serverHostname
  133. port:serverPort
  134. dateTime:[NSDate date] // current date & time
  135. notedHostname:notedHostname
  136. includeSubdomains:includeSubdomains
  137. enforcePinning:enforcePinning
  138. validatedCertificateChain:certificateChain
  139. knownPins:formattedPins
  140. validationResult:validationResult
  141. expirationDate:knownPinsExpirationDate];
  142. // Should we rate-limit this report?
  143. if (_shouldRateLimitReports && [self.rateLimiter shouldRateLimitReport:report])
  144. {
  145. // We recently sent the exact same report; do not send this report
  146. TSKLog(@"Pin failure report for %@ was not sent due to rate-limiting", serverHostname);
  147. return;
  148. }
  149. // Create a temporary file for storing the JSON data in ~/tmp
  150. NSURL *tmpDirURL = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
  151. NSURL *tmpFileURL = [[tmpDirURL URLByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]
  152. URLByAppendingPathExtension:@"tsk-report"];
  153. // Write the JSON report data to the temporary file
  154. NSError *error;
  155. NSUInteger writeOptions = NSDataWritingAtomic;
  156. #if TARGET_OS_IPHONE
  157. // Ensure the report is accessible when locked on iOS, in case the App has the NSFileProtectionComplete entitlement
  158. writeOptions = writeOptions | NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication;
  159. #endif
  160. if (!([[report json] writeToURL:tmpFileURL options:writeOptions error:&error]))
  161. {
  162. #if DEBUG
  163. // Only raise this exception for debug as not being able to save the report would crash a prod App
  164. // https://github.com/datatheorem/TrustKit/issues/32
  165. // This might happen when the device's storage is full?
  166. [NSException raise:@"TSKBackgroundReporter runtime error"
  167. format:@"Report cannot be saved to file: %@", [error description]];
  168. #endif
  169. }
  170. TSKLog(@"Report for %@ created at: %@", serverHostname, [tmpFileURL path]);
  171. // Create the HTTP request for all the configured report URIs and send it
  172. for (NSURL *reportUri in reportURIs)
  173. {
  174. NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:reportUri];
  175. [request setHTTPMethod:@"POST"];
  176. [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
  177. // Pass the URL and the temporary file to the background upload task and start uploading
  178. NSURLSessionUploadTask *uploadTask = [_backgroundSession uploadTaskWithRequest:request
  179. fromFile:tmpFileURL];
  180. [uploadTask resume];
  181. }
  182. }
  183. - (void)URLSession:(nonnull NSURLSession *)session task:(nonnull NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error
  184. {
  185. if (error == nil)
  186. {
  187. TSKLog(@"Background upload - task completed successfully: pinning failure report sent");
  188. }
  189. else
  190. {
  191. TSKLog(@"Background upload - task completed with error: %@ (code %ld)", [error localizedDescription], (long)error.code);
  192. }
  193. }
  194. @end