2 * Copyright (c) 2017-2019 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
25 #include <AssertMacros.h>
26 #import <Foundation/Foundation.h>
27 #import <CFNetwork/CFNSURLConnection.h>
29 #include <sys/sysctl.h>
31 #include <mach/mach_time.h>
32 #include <os/transaction_private.h>
33 #include <Security/SecCertificateInternal.h>
34 #include "utilities/debugging.h"
35 #include "utilities/SecCFWrappers.h"
36 #include "utilities/SecPLWrappers.h"
37 #include "utilities/SecFileLocations.h"
39 #include "SecRevocationDb.h"
40 #include "SecRevocationServer.h"
41 #include "SecTrustServer.h"
42 #include "SecOCSPRequest.h"
43 #include "SecOCSPResponse.h"
45 #import "SecTrustLoggingServer.h"
46 #import "TrustURLSessionDelegate.h"
48 #import "SecRevocationNetworking.h"
50 /* MARK: Valid Update Networking */
51 static CFStringRef kSecPrefsDomain = CFSTR("com.apple.security");
52 static CFStringRef kUpdateWiFiOnlyKey = CFSTR("ValidUpdateWiFiOnly");
53 static CFStringRef kUpdateBackgroundKey = CFSTR("ValidUpdateBackground");
55 extern CFAbsoluteTime gUpdateStarted;
56 extern CFAbsoluteTime gNextUpdate;
58 static int checkBasePath(const char *basePath) {
59 return mkpath_np((char*)basePath, 0755);
62 static uint64_t systemUptimeInSeconds() {
63 struct timeval boottime;
64 size_t tv_size = sizeof(boottime);
65 time_t now, uptime = 0;
68 mib[1] = KERN_BOOTTIME;
70 if (sysctl(mib, 2, &boottime, &tv_size, NULL, 0) != -1 &&
71 boottime.tv_sec != 0) {
72 uptime = now - boottime.tv_sec;
74 return (uint64_t)uptime;
77 typedef void (^CompletionHandler)(void);
79 @interface ValidDelegate : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
80 @property CompletionHandler handler;
81 @property dispatch_queue_t revDbUpdateQueue;
82 @property os_transaction_t transaction;
83 @property NSString *currentUpdateServer;
84 @property NSFileHandle *currentUpdateFile;
85 @property NSURL *currentUpdateFileURL;
86 @property BOOL finishedDownloading;
89 @implementation ValidDelegate
92 /* POWER LOG EVENT: operation canceled */
93 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
94 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
95 @"event" : (self->_finishedDownloading) ? @"updateCanceled" : @"downloadCanceled"
97 secnotice("validupdate", "%s canceled at %f",
98 (self->_finishedDownloading) ? "update" : "download",
99 (double)CFAbsoluteTimeGetCurrent());
102 SecRevocationDbComputeAndSetNextUpdateTime();
103 if (self->_transaction) {
104 self->_transaction = nil;
108 - (void)updateDb:(NSUInteger)version {
109 __block NSURL *updateFileURL = self->_currentUpdateFileURL;
110 __block NSString *updateServer = self->_currentUpdateServer;
111 __block NSFileHandle *updateFile = self->_currentUpdateFile;
112 if (!updateFileURL || !updateFile) {
117 /* Hold a transaction until we finish the update */
118 __block os_transaction_t transaction = os_transaction_create("com.apple.trustd.valid.updateDb");
119 dispatch_async(_revDbUpdateQueue, ^{
120 /* POWER LOG EVENT: background update started */
121 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
122 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
123 @"event" : @"updateStarted"
125 secnotice("validupdate", "update started at %f", (double)CFAbsoluteTimeGetCurrent());
127 CFDataRef updateData = NULL;
128 const char *updateFilePath = [updateFileURL fileSystemRepresentation];
130 if ((rtn = readValidFile(updateFilePath, &updateData)) != 0) {
131 secerror("failed to read %@ with error %d", updateFileURL, rtn);
132 TrustdHealthAnalyticsLogErrorCode(TAEventValidUpdate, TAFatalError, rtn);
138 secdebug("validupdate", "verifying and ingesting data from %@", updateFileURL);
139 SecValidUpdateVerifyAndIngest(updateData, (__bridge CFStringRef)updateServer, (0 == version));
140 if ((rtn = munmap((void *)CFDataGetBytePtr(updateData), CFDataGetLength(updateData))) != 0) {
141 secerror("unable to unmap current update %ld bytes at %p (error %d)", CFDataGetLength(updateData), CFDataGetBytePtr(updateData), rtn);
143 CFReleaseNull(updateData);
145 /* We're done with this file */
146 [updateFile closeFile];
147 if (updateFilePath) {
148 (void)remove(updateFilePath);
150 self->_currentUpdateFile = nil;
151 self->_currentUpdateFileURL = nil;
152 self->_currentUpdateServer = nil;
154 /* POWER LOG EVENT: background update finished */
155 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
156 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
157 @"event" : @"updateFinished"
160 /* Update is complete */
161 secnotice("validupdate", "update finished at %f", (double)CFAbsoluteTimeGetCurrent());
165 transaction = nil; // we're all done now
169 - (NSInteger)versionFromTask:(NSURLSessionTask *)task {
170 return atol([task.taskDescription cStringUsingEncoding:NSUTF8StringEncoding]);
173 - (void)URLSession:(NSURLSession *)session
174 dataTask:(NSURLSessionDataTask *)dataTask
175 didReceiveResponse:(NSURLResponse *)response
176 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
177 /* nsurlsessiond started our download. Create a transaction since we're going to be working for a little bit */
178 self->_transaction = os_transaction_create("com.apple.trustd.valid.download");
179 secinfo("validupdate", "Session %@ data task %@ returned response %ld (%@), expecting %lld bytes",
180 session, dataTask, (long)[(NSHTTPURLResponse *)response statusCode],
181 [response MIMEType], [response expectedContentLength]);
183 WithPathInRevocationInfoDirectory(NULL, ^(const char *utf8String) {
184 (void)checkBasePath(utf8String);
186 CFURLRef updateFileURL = SecCopyURLForFileInRevocationInfoDirectory(CFSTR("update-current"));
187 self->_currentUpdateFileURL = (updateFileURL) ? CFBridgingRelease(updateFileURL) : nil;
188 const char *updateFilePath = [self->_currentUpdateFileURL fileSystemRepresentation];
189 if (!updateFilePath) {
190 secnotice("validupdate", "failed to find revocation info directory. canceling task %@", dataTask);
191 completionHandler(NSURLSessionResponseCancel);
196 /* Clean up any old files from previous tasks. */
197 (void)remove(updateFilePath);
201 fd = open(updateFilePath, O_RDWR | O_CREAT | O_TRUNC, 0644);
202 if (fd < 0 || (off = lseek(fd, 0, SEEK_SET)) < 0) {
203 secnotice("validupdate","unable to open %@ (errno %d)", self->_currentUpdateFileURL, errno);
209 /* POWER LOG EVENT: background download actually started */
210 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
211 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
212 @"event" : @"downloadStarted"
214 secnotice("validupdate", "download started at %f", (double)CFAbsoluteTimeGetCurrent());
216 NSError *error = nil;
217 self->_currentUpdateFile = [NSFileHandle fileHandleForWritingToURL:self->_currentUpdateFileURL error:&error];
218 if (!self->_currentUpdateFile) {
219 secnotice("validupdate", "failed to open %@: %@. canceling task %@", self->_currentUpdateFileURL, error, dataTask);
220 #if ENABLE_TRUSTD_ANALYTICS
221 [[TrustdHealthAnalytics logger] logResultForEvent:TrustdHealthAnalyticsEventValidUpdate hardFailure:NO result:error];
222 #endif // ENABLE_TRUSTD_ANALYTICS
223 completionHandler(NSURLSessionResponseCancel);
228 completionHandler(NSURLSessionResponseAllow);
231 - (void)URLSession:(NSURLSession *)session
232 dataTask:(NSURLSessionDataTask *)dataTask
233 didReceiveData:(NSData *)data {
234 secdebug("validupdate", "Session %@ data task %@ returned %lu bytes (%lld bytes so far) out of expected %lld bytes",
235 session, dataTask, (unsigned long)[data length], [dataTask countOfBytesReceived], [dataTask countOfBytesExpectedToReceive]);
237 if (!self->_currentUpdateFile) {
238 secnotice("validupdate", "received data, but output file is not open");
245 /* Writing can fail and throw an exception, e.g. if we run out of disk space. */
246 [self->_currentUpdateFile writeData:data];
248 @catch(NSException *exception) {
249 secnotice("validupdate", "%s", exception.description.UTF8String);
250 TrustdHealthAnalyticsLogErrorCode(TAEventValidUpdate, TARecoverableError, errSecDiskFull);
256 - (void)URLSession:(NSURLSession *)session
257 task:(NSURLSessionTask *)task
258 didCompleteWithError:(NSError *)error {
260 secnotice("validupdate", "Session %@ task %@ failed with error %@", session, task, error);
261 #if ENABLE_TRUSTD_ANALYTICS
262 [[TrustdHealthAnalytics logger] logResultForEvent:TrustdHealthAnalyticsEventValidUpdate hardFailure:NO result:error];
263 #endif // ENABLE_TRUSTD_ANALYTICS
265 /* close file before we leave */
266 [self->_currentUpdateFile closeFile];
267 self->_currentUpdateFile = nil;
268 self->_currentUpdateServer = nil;
269 self->_currentUpdateFileURL = nil;
271 /* POWER LOG EVENT: background download finished */
272 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
273 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
274 @"event" : @"downloadFinished"
276 secnotice("validupdate", "download finished at %f", (double)CFAbsoluteTimeGetCurrent());
277 secdebug("validupdate", "Session %@ task %@ succeeded", session, task);
278 self->_finishedDownloading = YES;
279 [self updateDb:[self versionFromTask:task]];
281 if (self->_transaction) {
282 self->_transaction = nil;
288 @interface ValidUpdateRequest : NSObject
289 @property NSTimeInterval updateScheduled;
290 @property NSURLSession *backgroundSession;
293 static ValidUpdateRequest *request = nil;
295 @implementation ValidUpdateRequest
297 - (NSURLSessionConfiguration *)validUpdateConfiguration {
298 /* preferences to override defaults */
299 CFTypeRef value = NULL;
300 bool updateOnWiFiOnly = true;
301 value = CFPreferencesCopyValue(kUpdateWiFiOnlyKey, kSecPrefsDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost);
302 if (isBoolean(value)) {
303 updateOnWiFiOnly = CFBooleanGetValue((CFBooleanRef)value);
305 CFReleaseNull(value);
306 bool updateInBackground = true;
307 value = CFPreferencesCopyValue(kUpdateBackgroundKey, kSecPrefsDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost);
308 if (isBoolean(value)) {
309 updateInBackground = CFBooleanGetValue((CFBooleanRef)value);
311 CFReleaseNull(value);
313 NSURLSessionConfiguration *config = nil;
314 if (updateInBackground) {
315 config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier: @"com.apple.trustd.networking.background"];
316 config.networkServiceType = NSURLNetworkServiceTypeBackground;
317 config.discretionary = YES;
318 config._requiresPowerPluggedIn = YES;
319 config.allowsCellularAccess = (!updateOnWiFiOnly) ? YES : NO;
321 config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; // no cookies or data storage
322 config.networkServiceType = NSURLNetworkServiceTypeDefault;
323 config.discretionary = NO;
326 config.HTTPAdditionalHeaders = @{ @"User-Agent" : @"com.apple.trustd/2.0",
328 @"Accept-Encoding" : @"gzip,deflate,br"};
330 config.TLSMinimumSupportedProtocol = kTLSProtocol12;
335 - (void) createSession:(dispatch_queue_t)updateQueue forServer:(NSString *)updateServer {
336 NSURLSessionConfiguration *config = [self validUpdateConfiguration];
337 ValidDelegate *delegate = [[ValidDelegate alloc] init];
338 delegate.handler = ^(void) {
339 request.updateScheduled = 0.0;
340 secdebug("validupdate", "resetting scheduled time");
342 delegate.transaction = NULL;
343 delegate.revDbUpdateQueue = updateQueue;
344 delegate.finishedDownloading = NO;
345 delegate.currentUpdateServer = [updateServer copy];
347 /* Callbacks should be on a separate NSOperationQueue.
348 We'll then dispatch the work on updateQueue and return from the callback. */
349 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
350 queue.maxConcurrentOperationCount = 1;
351 _backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:queue];
354 - (BOOL) scheduleUpdateFromServer:(NSString *)server forVersion:(NSUInteger)version withQueue:(dispatch_queue_t)updateQueue {
356 secnotice("validupdate", "invalid update request");
361 secnotice("validupdate", "missing update queue, skipping update");
365 /* nsurlsessiond waits for unlock to finish launching, so we can't block trust evaluations
366 * on scheduling this background task. Also, we want to wait a sufficient amount of time
367 * after system boot before trying to initiate network activity, to avoid the possibility
368 * of a performance regression in the boot path. */
369 dispatch_async(updateQueue, ^{
370 CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
371 if (self.updateScheduled != 0.0) {
372 secdebug("validupdate", "update in progress (scheduled %f)", (double)self.updateScheduled);
375 uint64_t uptime = systemUptimeInSeconds();
376 const uint64_t minUptime = 180;
377 if (uptime < minUptime) {
378 gNextUpdate = now + (minUptime - uptime);
380 secnotice("validupdate", "postponing update until %f", gNextUpdate);
383 self.updateScheduled = now;
384 secnotice("validupdate", "scheduling update at %f", (double)self.updateScheduled);
388 /* we have an update to schedule, so take a transaction while we work */
389 os_transaction_t transaction = os_transaction_create("com.apple.trustd.valid.scheduleUpdate");
391 /* clear all old sessions and cleanup disk (for previous download tasks) */
392 static dispatch_once_t onceToken;
393 dispatch_once(&onceToken, ^{
395 [NSURLSession _obliterateAllBackgroundSessionsWithCompletionHandler:^{
396 secnotice("validupdate", "removing all old sessions for trustd");
401 if (!self.backgroundSession) {
402 [self createSession:updateQueue forServer:server];
404 ValidDelegate *delegate = (ValidDelegate *)[self.backgroundSession delegate];
405 delegate.currentUpdateServer = [server copy];
408 /* POWER LOG EVENT: scheduling our background download session now */
409 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
410 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
411 @"event" : @"downloadScheduled",
412 @"version" : @(version)
415 NSURL *validUrl = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@/g3/v%ld",
416 server, (unsigned long)version]];
417 NSURLSessionDataTask *dataTask = [self.backgroundSession dataTaskWithURL:validUrl];
418 dataTask.taskDescription = [NSString stringWithFormat:@"%lu",(unsigned long)version];
420 secnotice("validupdate", "scheduled background data task %@ at %f", dataTask, CFAbsoluteTimeGetCurrent());
421 (void) transaction; // dead store
422 transaction = nil; // ARC releases the transaction
429 bool SecValidUpdateRequest(dispatch_queue_t queue, CFStringRef server, CFIndex version) {
430 static dispatch_once_t onceToken;
431 dispatch_once(&onceToken, ^{
433 request = [[ValidUpdateRequest alloc] init];
437 return [request scheduleUpdateFromServer:(__bridge NSString*)server forVersion:version withQueue:queue];
442 /* MARK: OCSP Fetch Networking */
443 #define OCSP_REQUEST_THRESHOLD 10
445 @interface OCSPFetchDelegate : TrustURLSessionDelegate
448 @implementation OCSPFetchDelegate
449 - (BOOL)fetchNext:(NSURLSession *)session {
450 SecORVCRef orvc = (SecORVCRef)self.context;
451 TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(orvc->builder);
454 if ((result = [super fetchNext:session])) {
455 /* no fetch scheduled */
458 if (self.URIix > 0) {
459 orvc->responder = (__bridge CFURLRef)self.URIs[self.URIix - 1];
461 orvc->responder = (__bridge CFURLRef)self.URIs[0];
464 analytics->ocsp_fetches++;
470 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
471 /* call the superclass's method to set expiration */
472 [super URLSession:session task:task didCompleteWithError:error];
474 __block SecORVCRef orvc = (SecORVCRef)self.context;
475 if (!orvc || !orvc->builder) {
476 /* We already returned to the PathBuilder state machine. */
480 TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(orvc->builder);
483 secnotice("rvc", "Failed to download ocsp response %@, with error %@", task.originalRequest.URL, error);
485 analytics->ocsp_fetch_failed++;
488 SecOCSPResponseRef ocspResponse = SecOCSPResponseCreate((__bridge CFDataRef)self.response);
490 SecORVCConsumeOCSPResponse(orvc, ocspResponse, self.expiration, true, false);
491 if (analytics && !orvc->done) {
492 /* We got an OCSP response that didn't pass validation */
493 analytics-> ocsp_validation_failed = true;
495 } else if (analytics) {
496 /* We got something that wasn't an OCSP response (e.g. captive portal) --
497 * we consider that a fetch failure */
498 analytics->ocsp_fetch_failed++;
502 /* If we didn't get a valid OCSP response, try the next URI */
504 (void)[self fetchNext:session];
507 /* We got a valid OCSP response or couldn't schedule any more fetches.
508 * Close the session, update the PVCs, decrement the async count, and callback if we're all done. */
510 secdebug("rvc", "builder %p, done with OCSP fetches for cert: %ld", orvc->builder, orvc->certIX);
512 [session invalidateAndCancel];
513 SecORVCUpdatePVC(orvc);
514 if (0 == SecPathBuilderDecrementAsyncJobCount(orvc->builder)) {
515 /* We're the last async job to finish, jump back into the state machine */
516 secdebug("rvc", "builder %p, done with all async jobs", orvc->builder);
517 dispatch_async(SecPathBuilderGetQueue(orvc->builder), ^{
518 SecPathBuilderStep(orvc->builder);
524 - (NSURLRequest *)createNextRequest:(NSURL *)uri {
525 SecORVCRef orvc = (SecORVCRef)self.context;
526 CFDataRef ocspDER = CFRetainSafe(SecOCSPRequestGetDER(orvc->ocspRequest));
527 NSData *nsOcspDER = CFBridgingRelease(ocspDER);
528 NSString *ocspBase64 = [nsOcspDER base64EncodedStringWithOptions:0];
530 /* Ensure that we percent-encode specific characters in the base64 path
531 which are defined as delimiters in RFC 3986 [2.2].
533 static NSMutableCharacterSet *allowedSet = nil;
534 static dispatch_once_t onceToken;
535 dispatch_once(&onceToken, ^{
536 allowedSet = [[NSCharacterSet URLPathAllowedCharacterSet] mutableCopy];
537 [allowedSet removeCharactersInString:@":/?#[]@!$&'()*+,;="];
539 NSString *escapedRequest = [ocspBase64 stringByAddingPercentEncodingWithAllowedCharacters:allowedSet];
540 NSURLRequest *request = nil;
542 /* Interesting tidbit from rfc5019
543 When sending requests that are less than or equal to 255 bytes in
544 total (after encoding) including the scheme and delimiters (http://),
545 server name and base64-encoded OCSPRequest structure, clients MUST
546 use the GET method (to enable OCSP response caching). OCSP requests
547 larger than 255 bytes SHOULD be submitted using the POST method.
549 if (([[uri absoluteString] length] + 1 + [escapedRequest length]) < 256) {
551 NSString *requestString = [NSString stringWithFormat:@"%@/%@", [uri absoluteString], escapedRequest];
552 NSURL *requestURL = [NSURL URLWithString:requestString];
553 request = [NSURLRequest requestWithURL:requestURL];
556 NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:uri];
557 mutableRequest.HTTPMethod = @"POST";
558 mutableRequest.HTTPBody = nsOcspDER;
559 request = mutableRequest;
565 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)taskMetrics {
566 secdebug("rvc", "got metrics with task interval %f", taskMetrics.taskInterval.duration);
567 SecORVCRef orvc = (SecORVCRef)self.context;
568 if (orvc && orvc->builder) {
569 TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(orvc->builder);
571 analytics->ocsp_fetch_time += (uint64_t)(taskMetrics.taskInterval.duration * NSEC_PER_SEC);
577 bool SecORVCBeginFetches(SecORVCRef orvc, SecCertificateRef cert) {
579 CFArrayRef ocspResponders = CFRetainSafe(SecCertificateGetOCSPResponders(cert));
580 NSArray *nsResponders = CFBridgingRelease(ocspResponders);
582 NSInteger count = [nsResponders count];
583 if (count > OCSP_REQUEST_THRESHOLD) {
584 secnotice("rvc", "too may OCSP responder entries (%ld)", (long)count);
589 NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
590 config.timeoutIntervalForResource = TrustURLSessionGetResourceTimeout();
591 config.HTTPAdditionalHeaders = @{@"User-Agent" : @"com.apple.trustd/2.0"};
593 NSData *auditToken = CFBridgingRelease(SecPathBuilderCopyClientAuditToken(orvc->builder));
595 config._sourceApplicationAuditTokenData = auditToken;
598 OCSPFetchDelegate *delegate = [[OCSPFetchDelegate alloc] init];
599 delegate.context = orvc;
600 delegate.URIs = nsResponders;
603 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
605 NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:queue];
606 secdebug("rvc", "created URLSession for %@", cert);
609 if ((result = [delegate fetchNext:session])) {
610 /* no fetch scheduled, close the session */
611 [session invalidateAndCancel];