2 * Copyright (c) 2017 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 #define kSecRevocationBasePath "/Library/Keychains/crls"
53 static CFStringRef kSecPrefsDomain = CFSTR("com.apple.security");
54 static CFStringRef kUpdateWiFiOnlyKey = CFSTR("ValidUpdateWiFiOnly");
55 static CFStringRef kUpdateBackgroundKey = CFSTR("ValidUpdateBackground");
57 extern CFAbsoluteTime gUpdateStarted;
58 extern CFAbsoluteTime gNextUpdate;
60 static int checkBasePath(const char *basePath) {
61 return mkpath_np((char*)basePath, 0755);
64 static uint64_t systemUptimeInSeconds() {
65 struct timeval boottime;
66 size_t tv_size = sizeof(boottime);
67 time_t now, uptime = 0;
70 mib[1] = KERN_BOOTTIME;
72 if (sysctl(mib, 2, &boottime, &tv_size, NULL, 0) != -1 &&
73 boottime.tv_sec != 0) {
74 uptime = now - boottime.tv_sec;
76 return (uint64_t)uptime;
79 typedef void (^CompletionHandler)(void);
81 @interface ValidDelegate : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
82 @property CompletionHandler handler;
83 @property dispatch_queue_t revDbUpdateQueue;
84 @property os_transaction_t transaction;
85 @property NSString *currentUpdateServer;
86 @property NSFileHandle *currentUpdateFile;
87 @property NSURL *currentUpdateFileURL;
88 @property BOOL finishedDownloading;
91 @implementation ValidDelegate
94 /* POWER LOG EVENT: operation canceled */
95 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
96 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
97 @"event" : (self->_finishedDownloading) ? @"updateCanceled" : @"downloadCanceled"
99 secnotice("validupdate", "%s canceled at %f",
100 (self->_finishedDownloading) ? "update" : "download",
101 (double)CFAbsoluteTimeGetCurrent());
104 SecRevocationDbComputeAndSetNextUpdateTime();
105 if (self->_transaction) {
106 self->_transaction = nil;
110 - (void)updateDb:(NSUInteger)version {
111 __block NSURL *updateFileURL = self->_currentUpdateFileURL;
112 __block NSString *updateServer = self->_currentUpdateServer;
113 __block NSFileHandle *updateFile = self->_currentUpdateFile;
114 if (!updateFileURL || !updateFile) {
119 /* Hold a transaction until we finish the update */
120 __block os_transaction_t transaction = os_transaction_create("com.apple.trustd.valid.updateDb");
121 dispatch_async(_revDbUpdateQueue, ^{
122 /* POWER LOG EVENT: background update started */
123 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
124 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
125 @"event" : @"updateStarted"
127 secnotice("validupdate", "update started at %f", (double)CFAbsoluteTimeGetCurrent());
129 CFDataRef updateData = NULL;
130 const char *updateFilePath = [updateFileURL fileSystemRepresentation];
132 if ((rtn = readValidFile(updateFilePath, &updateData)) != 0) {
133 secerror("failed to read %@ with error %d", updateFileURL, rtn);
134 TrustdHealthAnalyticsLogErrorCode(TAEventValidUpdate, TAFatalError, rtn);
140 secdebug("validupdate", "verifying and ingesting data from %@", updateFileURL);
141 SecValidUpdateVerifyAndIngest(updateData, (__bridge CFStringRef)updateServer, (0 == version));
142 if ((rtn = munmap((void *)CFDataGetBytePtr(updateData), CFDataGetLength(updateData))) != 0) {
143 secerror("unable to unmap current update %ld bytes at %p (error %d)", CFDataGetLength(updateData), CFDataGetBytePtr(updateData), rtn);
145 CFReleaseNull(updateData);
147 /* We're done with this file */
148 [updateFile closeFile];
149 if (updateFilePath) {
150 (void)remove(updateFilePath);
152 self->_currentUpdateFile = nil;
153 self->_currentUpdateFileURL = nil;
154 self->_currentUpdateServer = nil;
156 /* POWER LOG EVENT: background update finished */
157 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
158 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
159 @"event" : @"updateFinished"
162 /* Update is complete */
163 secnotice("validupdate", "update finished at %f", (double)CFAbsoluteTimeGetCurrent());
167 transaction = nil; // we're all done now
171 - (NSInteger)versionFromTask:(NSURLSessionTask *)task {
172 return atol([task.taskDescription cStringUsingEncoding:NSUTF8StringEncoding]);
175 - (void)URLSession:(NSURLSession *)session
176 dataTask:(NSURLSessionDataTask *)dataTask
177 didReceiveResponse:(NSURLResponse *)response
178 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
179 /* nsurlsessiond started our download. Create a transaction since we're going to be working for a little bit */
180 self->_transaction = os_transaction_create("com.apple.trustd.valid.download");
181 secinfo("validupdate", "Session %@ data task %@ returned response %ld, expecting %lld bytes", session, dataTask,
182 (long)[(NSHTTPURLResponse *)response statusCode],[response expectedContentLength]);
184 (void)checkBasePath(kSecRevocationBasePath);
185 CFURLRef updateFileURL = SecCopyURLForFileInRevocationInfoDirectory(CFSTR("update-current"));
186 self->_currentUpdateFileURL = (updateFileURL) ? CFBridgingRelease(updateFileURL) : nil;
187 const char *updateFilePath = [self->_currentUpdateFileURL fileSystemRepresentation];
188 if (!updateFilePath) {
189 secnotice("validupdate", "failed to find revocation info directory. canceling task %@", dataTask);
190 completionHandler(NSURLSessionResponseCancel);
195 /* Clean up any old files from previous tasks. */
196 (void)remove(updateFilePath);
200 fd = open(updateFilePath, O_RDWR | O_CREAT | O_TRUNC, 0644);
201 if (fd < 0 || (off = lseek(fd, 0, SEEK_SET)) < 0) {
202 secnotice("validupdate","unable to open %@ (errno %d)", self->_currentUpdateFileURL, errno);
208 /* POWER LOG EVENT: background download actually started */
209 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
210 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
211 @"event" : @"downloadStarted"
213 secnotice("validupdate", "download started at %f", (double)CFAbsoluteTimeGetCurrent());
215 NSError *error = nil;
216 self->_currentUpdateFile = [NSFileHandle fileHandleForWritingToURL:self->_currentUpdateFileURL error:&error];
217 if (!self->_currentUpdateFile) {
218 secnotice("validupdate", "failed to open %@: %@. canceling task %@", self->_currentUpdateFileURL, error, dataTask);
219 #if ENABLE_TRUSTD_ANALYTICS
220 [[TrustdHealthAnalytics logger] logResultForEvent:TrustdHealthAnalyticsEventValidUpdate hardFailure:NO result:error];
221 #endif // ENABLE_TRUSTD_ANALYTICS
222 completionHandler(NSURLSessionResponseCancel);
227 completionHandler(NSURLSessionResponseAllow);
230 - (void)URLSession:(NSURLSession *)session
231 dataTask:(NSURLSessionDataTask *)dataTask
232 didReceiveData:(NSData *)data {
233 secdebug("validupdate", "Session %@ data task %@ returned %lu bytes (%lld bytes so far) out of expected %lld bytes",
234 session, dataTask, (unsigned long)[data length], [dataTask countOfBytesReceived], [dataTask countOfBytesExpectedToReceive]);
236 if (!self->_currentUpdateFile) {
237 secnotice("validupdate", "received data, but output file is not open");
244 /* Writing can fail and throw an exception, e.g. if we run out of disk space. */
245 [self->_currentUpdateFile writeData:data];
247 @catch(NSException *exception) {
248 secnotice("validupdate", "%s", exception.description.UTF8String);
249 TrustdHealthAnalyticsLogErrorCode(TAEventValidUpdate, TARecoverableError, errSecDiskFull);
255 - (void)URLSession:(NSURLSession *)session
256 task:(NSURLSessionTask *)task
257 didCompleteWithError:(NSError *)error {
259 secnotice("validupdate", "Session %@ task %@ failed with error %@", session, task, error);
260 #if ENABLE_TRUSTD_ANALYTICS
261 [[TrustdHealthAnalytics logger] logResultForEvent:TrustdHealthAnalyticsEventValidUpdate hardFailure:NO result:error];
262 #endif // ENABLE_TRUSTD_ANALYTICS
264 /* close file before we leave */
265 [self->_currentUpdateFile closeFile];
266 self->_currentUpdateFile = nil;
267 self->_currentUpdateServer = nil;
268 self->_currentUpdateFileURL = nil;
270 /* POWER LOG EVENT: background download finished */
271 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
272 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
273 @"event" : @"downloadFinished"
275 secnotice("validupdate", "download finished at %f", (double)CFAbsoluteTimeGetCurrent());
276 secdebug("validupdate", "Session %@ task %@ succeeded", session, task);
277 self->_finishedDownloading = YES;
278 [self updateDb:[self versionFromTask:task]];
280 if (self->_transaction) {
281 self->_transaction = nil;
287 @interface ValidUpdateRequest : NSObject
288 @property NSTimeInterval updateScheduled;
289 @property NSURLSession *backgroundSession;
292 static ValidUpdateRequest *request = nil;
294 @implementation ValidUpdateRequest
296 - (NSURLSessionConfiguration *)validUpdateConfiguration {
297 /* preferences to override defaults */
298 CFTypeRef value = NULL;
299 bool updateOnWiFiOnly = true;
300 value = CFPreferencesCopyValue(kUpdateWiFiOnlyKey, kSecPrefsDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost);
301 if (isBoolean(value)) {
302 updateOnWiFiOnly = CFBooleanGetValue((CFBooleanRef)value);
304 CFReleaseNull(value);
305 bool updateInBackground = true;
306 value = CFPreferencesCopyValue(kUpdateBackgroundKey, kSecPrefsDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost);
307 if (isBoolean(value)) {
308 updateInBackground = CFBooleanGetValue((CFBooleanRef)value);
310 CFReleaseNull(value);
312 NSURLSessionConfiguration *config = nil;
313 if (updateInBackground) {
314 config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier: @"com.apple.trustd.networking.background"];
315 config.networkServiceType = NSURLNetworkServiceTypeBackground;
316 config.discretionary = YES;
318 config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; // no cookies or data storage
319 config.networkServiceType = NSURLNetworkServiceTypeDefault;
320 config.discretionary = NO;
323 config.HTTPAdditionalHeaders = @{ @"User-Agent" : @"com.apple.trustd/2.0",
325 @"Accept-Encoding" : @"gzip,deflate,br"};
327 config.TLSMinimumSupportedProtocol = kTLSProtocol12;
328 config.TLSMaximumSupportedProtocol = kTLSProtocol13;
330 config._requiresPowerPluggedIn = YES;
332 config.allowsCellularAccess = (!updateOnWiFiOnly) ? YES : NO;
337 - (void) createSession:(dispatch_queue_t)updateQueue forServer:(NSString *)updateServer {
338 NSURLSessionConfiguration *config = [self validUpdateConfiguration];
339 ValidDelegate *delegate = [[ValidDelegate alloc] init];
340 delegate.handler = ^(void) {
341 request.updateScheduled = 0.0;
342 secdebug("validupdate", "resetting scheduled time");
344 delegate.transaction = NULL;
345 delegate.revDbUpdateQueue = updateQueue;
346 delegate.finishedDownloading = NO;
347 delegate.currentUpdateServer = [updateServer copy];
349 /* Callbacks should be on a separate NSOperationQueue.
350 We'll then dispatch the work on updateQueue and return from the callback. */
351 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
352 _backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:queue];
355 - (BOOL) scheduleUpdateFromServer:(NSString *)server forVersion:(NSUInteger)version withQueue:(dispatch_queue_t)updateQueue {
357 secnotice("validupdate", "invalid update request");
362 secnotice("validupdate", "missing update queue, skipping update");
366 /* nsurlsessiond waits for unlock to finish launching, so we can't block trust evaluations
367 * on scheduling this background task. Also, we want to wait a sufficient amount of time
368 * after system boot before trying to initiate network activity, to avoid the possibility
369 * of a performance regression in the boot path. */
370 dispatch_async(updateQueue, ^{
371 /* Take a transaction while we work */
372 os_transaction_t transaction = os_transaction_create("com.apple.trustd.valid.scheduleUpdate");
373 CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
374 if (self.updateScheduled != 0.0) {
375 secdebug("validupdate", "update in progress (scheduled %f)", (double)self.updateScheduled);
378 uint64_t uptime = systemUptimeInSeconds();
379 const uint64_t minUptime = 180;
380 if (uptime < minUptime) {
381 gNextUpdate = now + (minUptime - uptime);
383 secnotice("validupdate", "postponing update until %f", gNextUpdate);
385 self.updateScheduled = now;
386 secnotice("validupdate", "scheduling update at %f", (double)self.updateScheduled);
390 NSURL *validUrl = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@/g3/v%ld",
391 server, (unsigned long)version]];
393 secnotice("validupdate", "invalid update url");
397 /* clear all old sessions and cleanup disk (for previous download tasks) */
398 static dispatch_once_t onceToken;
399 dispatch_once(&onceToken, ^{
401 [NSURLSession _obliterateAllBackgroundSessionsWithCompletionHandler:^{
402 secnotice("validupdate", "removing all old sessions for trustd");
407 if (!self.backgroundSession) {
408 [self createSession:updateQueue forServer:server];
410 ValidDelegate *delegate = (ValidDelegate *)[self.backgroundSession delegate];
411 delegate.currentUpdateServer = [server copy];
414 /* POWER LOG EVENT: scheduling our background download session now */
415 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
416 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
417 @"event" : @"downloadScheduled",
418 @"version" : @(version)
421 NSURLSessionDataTask *dataTask = [self.backgroundSession dataTaskWithURL:validUrl];
422 dataTask.taskDescription = [NSString stringWithFormat:@"%lu",(unsigned long)version];
424 secnotice("validupdate", "scheduled background data task %@ at %f", dataTask, CFAbsoluteTimeGetCurrent());
425 (void) transaction; // dead store
433 bool SecValidUpdateRequest(dispatch_queue_t queue, CFStringRef server, CFIndex version) {
434 static dispatch_once_t onceToken;
435 dispatch_once(&onceToken, ^{
437 request = [[ValidUpdateRequest alloc] init];
441 return [request scheduleUpdateFromServer:(__bridge NSString*)server forVersion:version withQueue:queue];
446 /* MARK: OCSP Fetch Networking */
447 #define OCSP_REQUEST_THRESHOLD 10
449 @interface OCSPFetchDelegate : TrustURLSessionDelegate
452 @implementation OCSPFetchDelegate
453 - (BOOL)fetchNext:(NSURLSession *)session {
454 SecORVCRef orvc = (SecORVCRef)self.context;
455 TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(orvc->builder);
458 if ((result = [super fetchNext:session])) {
459 /* no fetch scheduled */
462 if (self.URIix > 0) {
463 orvc->responder = (__bridge CFURLRef)self.URIs[self.URIix - 1];
465 orvc->responder = (__bridge CFURLRef)self.URIs[0];
468 analytics->ocsp_fetches++;
474 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
475 /* call the superclass's method to set expiration */
476 [super URLSession:session task:task didCompleteWithError:error];
478 __block SecORVCRef orvc = (SecORVCRef)self.context;
479 if (!orvc || !orvc->builder) {
480 /* We already returned to the PathBuilder state machine. */
484 TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(orvc->builder);
487 secnotice("rvc", "Failed to download ocsp response %@, with error %@", task.originalRequest.URL, error);
489 analytics->ocsp_fetch_failed++;
492 SecOCSPResponseRef ocspResponse = SecOCSPResponseCreate((__bridge CFDataRef)self.response);
494 SecORVCConsumeOCSPResponse(orvc, ocspResponse, self.expiration, true, false);
495 if (analytics && !orvc->done) {
496 /* We got an OCSP response that didn't pass validation */
497 analytics-> ocsp_validation_failed = true;
499 } else if (analytics) {
500 /* We got something that wasn't an OCSP response (e.g. captive portal) --
501 * we consider that a fetch failure */
502 analytics->ocsp_fetch_failed++;
506 /* If we didn't get a valid OCSP response, try the next URI */
508 (void)[self fetchNext:session];
511 /* We got a valid OCSP response or couldn't schedule any more fetches.
512 * Close the session, update the PVCs, decrement the async count, and callback if we're all done. */
514 secdebug("rvc", "builder %p, done with OCSP fetches for cert: %ld", orvc->builder, orvc->certIX);
516 [session invalidateAndCancel];
517 SecORVCUpdatePVC(orvc);
518 if (0 == SecPathBuilderDecrementAsyncJobCount(orvc->builder)) {
519 /* We're the last async job to finish, jump back into the state machine */
520 secdebug("rvc", "builder %p, done with all async jobs", orvc->builder);
521 dispatch_async(SecPathBuilderGetQueue(orvc->builder), ^{
522 SecPathBuilderStep(orvc->builder);
528 - (NSURLRequest *)createNextRequest:(NSURL *)uri {
529 SecORVCRef orvc = (SecORVCRef)self.context;
530 CFDataRef ocspDER = CFRetainSafe(SecOCSPRequestGetDER(orvc->ocspRequest));
531 NSData *nsOcspDER = CFBridgingRelease(ocspDER);
532 NSString *ocspBase64 = [nsOcspDER base64EncodedStringWithOptions:0];
533 NSString *escapedRequest = [ocspBase64 stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLPathAllowedCharacterSet]];
534 NSURLRequest *request = nil;
536 /* Interesting tidbit from rfc5019
537 When sending requests that are less than or equal to 255 bytes in
538 total (after encoding) including the scheme and delimiters (http://),
539 server name and base64-encoded OCSPRequest structure, clients MUST
540 use the GET method (to enable OCSP response caching). OCSP requests
541 larger than 255 bytes SHOULD be submitted using the POST method.
543 if ([escapedRequest length] < 256) {
545 NSURL *requestURL = [uri URLByAppendingPathComponent:escapedRequest];
546 request = [NSURLRequest requestWithURL:requestURL];
549 NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:uri];
550 mutableRequest.HTTPMethod = @"POST";
551 mutableRequest.HTTPBody = nsOcspDER;
552 request = mutableRequest;
558 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)taskMetrics {
559 secdebug("rvc", "got metrics with task interval %f", taskMetrics.taskInterval.duration);
560 SecORVCRef orvc = (SecORVCRef)self.context;
561 if (orvc && orvc->builder) {
562 TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(orvc->builder);
564 analytics->ocsp_fetch_time += (uint64_t)(taskMetrics.taskInterval.duration * NSEC_PER_SEC);
570 bool SecORVCBeginFetches(SecORVCRef orvc, SecCertificateRef cert) {
572 CFArrayRef ocspResponders = CFRetainSafe(SecCertificateGetOCSPResponders(cert));
573 NSArray *nsResponders = CFBridgingRelease(ocspResponders);
575 NSInteger count = [nsResponders count];
576 if (count > OCSP_REQUEST_THRESHOLD) {
577 secnotice("rvc", "too may OCSP responder entries (%ld)", (long)count);
582 NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
583 config.timeoutIntervalForResource = TrustURLSessionGetResourceTimeout();
584 config.HTTPAdditionalHeaders = @{@"User-Agent" : @"com.apple.trustd/2.0"};
586 NSData *auditToken = CFBridgingRelease(SecPathBuilderCopyClientAuditToken(orvc->builder));
588 config._sourceApplicationAuditTokenData = auditToken;
591 OCSPFetchDelegate *delegate = [[OCSPFetchDelegate alloc] init];
592 delegate.context = orvc;
593 delegate.URIs = nsResponders;
596 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
598 NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:queue];
599 secdebug("rvc", "created URLSession for %@", cert);
602 if ((result = [delegate fetchNext:session])) {
603 /* no fetch scheduled, close the session */
604 [session invalidateAndCancel];