2 * Copyright (c) 2017-2018 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;
317 config._requiresPowerPluggedIn = YES;
318 config.allowsCellularAccess = (!updateOnWiFiOnly) ? YES : NO;
320 config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; // no cookies or data storage
321 config.networkServiceType = NSURLNetworkServiceTypeDefault;
322 config.discretionary = NO;
325 config.HTTPAdditionalHeaders = @{ @"User-Agent" : @"com.apple.trustd/2.0",
327 @"Accept-Encoding" : @"gzip,deflate,br"};
329 config.TLSMinimumSupportedProtocol = kTLSProtocol12;
330 config.TLSMaximumSupportedProtocol = kTLSProtocol13;
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 _backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:queue];
353 - (BOOL) scheduleUpdateFromServer:(NSString *)server forVersion:(NSUInteger)version withQueue:(dispatch_queue_t)updateQueue {
355 secnotice("validupdate", "invalid update request");
360 secnotice("validupdate", "missing update queue, skipping update");
364 /* nsurlsessiond waits for unlock to finish launching, so we can't block trust evaluations
365 * on scheduling this background task. Also, we want to wait a sufficient amount of time
366 * after system boot before trying to initiate network activity, to avoid the possibility
367 * of a performance regression in the boot path. */
368 dispatch_async(updateQueue, ^{
369 /* Take a transaction while we work */
370 os_transaction_t transaction = os_transaction_create("com.apple.trustd.valid.scheduleUpdate");
371 CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
372 if (self.updateScheduled != 0.0) {
373 secdebug("validupdate", "update in progress (scheduled %f)", (double)self.updateScheduled);
376 uint64_t uptime = systemUptimeInSeconds();
377 const uint64_t minUptime = 180;
378 if (uptime < minUptime) {
379 gNextUpdate = now + (minUptime - uptime);
381 secnotice("validupdate", "postponing update until %f", gNextUpdate);
383 self.updateScheduled = now;
384 secnotice("validupdate", "scheduling update at %f", (double)self.updateScheduled);
388 NSURL *validUrl = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@/g3/v%ld",
389 server, (unsigned long)version]];
391 secnotice("validupdate", "invalid update url");
395 /* clear all old sessions and cleanup disk (for previous download tasks) */
396 static dispatch_once_t onceToken;
397 dispatch_once(&onceToken, ^{
399 [NSURLSession _obliterateAllBackgroundSessionsWithCompletionHandler:^{
400 secnotice("validupdate", "removing all old sessions for trustd");
405 if (!self.backgroundSession) {
406 [self createSession:updateQueue forServer:server];
408 ValidDelegate *delegate = (ValidDelegate *)[self.backgroundSession delegate];
409 delegate.currentUpdateServer = [server copy];
412 /* POWER LOG EVENT: scheduling our background download session now */
413 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
414 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
415 @"event" : @"downloadScheduled",
416 @"version" : @(version)
419 NSURLSessionDataTask *dataTask = [self.backgroundSession dataTaskWithURL:validUrl];
420 dataTask.taskDescription = [NSString stringWithFormat:@"%lu",(unsigned long)version];
422 secnotice("validupdate", "scheduled background data task %@ at %f", dataTask, CFAbsoluteTimeGetCurrent());
423 (void) transaction; // dead store
431 bool SecValidUpdateRequest(dispatch_queue_t queue, CFStringRef server, CFIndex version) {
432 static dispatch_once_t onceToken;
433 dispatch_once(&onceToken, ^{
435 request = [[ValidUpdateRequest alloc] init];
439 return [request scheduleUpdateFromServer:(__bridge NSString*)server forVersion:version withQueue:queue];
444 /* MARK: OCSP Fetch Networking */
445 #define OCSP_REQUEST_THRESHOLD 10
447 @interface OCSPFetchDelegate : TrustURLSessionDelegate
450 @implementation OCSPFetchDelegate
451 - (BOOL)fetchNext:(NSURLSession *)session {
452 SecORVCRef orvc = (SecORVCRef)self.context;
453 TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(orvc->builder);
456 if ((result = [super fetchNext:session])) {
457 /* no fetch scheduled */
460 if (self.URIix > 0) {
461 orvc->responder = (__bridge CFURLRef)self.URIs[self.URIix - 1];
463 orvc->responder = (__bridge CFURLRef)self.URIs[0];
466 analytics->ocsp_fetches++;
472 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
473 /* call the superclass's method to set expiration */
474 [super URLSession:session task:task didCompleteWithError:error];
476 __block SecORVCRef orvc = (SecORVCRef)self.context;
477 if (!orvc || !orvc->builder) {
478 /* We already returned to the PathBuilder state machine. */
482 TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(orvc->builder);
485 secnotice("rvc", "Failed to download ocsp response %@, with error %@", task.originalRequest.URL, error);
487 analytics->ocsp_fetch_failed++;
490 SecOCSPResponseRef ocspResponse = SecOCSPResponseCreate((__bridge CFDataRef)self.response);
492 SecORVCConsumeOCSPResponse(orvc, ocspResponse, self.expiration, true, false);
493 if (analytics && !orvc->done) {
494 /* We got an OCSP response that didn't pass validation */
495 analytics-> ocsp_validation_failed = true;
497 } else if (analytics) {
498 /* We got something that wasn't an OCSP response (e.g. captive portal) --
499 * we consider that a fetch failure */
500 analytics->ocsp_fetch_failed++;
504 /* If we didn't get a valid OCSP response, try the next URI */
506 (void)[self fetchNext:session];
509 /* We got a valid OCSP response or couldn't schedule any more fetches.
510 * Close the session, update the PVCs, decrement the async count, and callback if we're all done. */
512 secdebug("rvc", "builder %p, done with OCSP fetches for cert: %ld", orvc->builder, orvc->certIX);
514 [session invalidateAndCancel];
515 SecORVCUpdatePVC(orvc);
516 if (0 == SecPathBuilderDecrementAsyncJobCount(orvc->builder)) {
517 /* We're the last async job to finish, jump back into the state machine */
518 secdebug("rvc", "builder %p, done with all async jobs", orvc->builder);
519 dispatch_async(SecPathBuilderGetQueue(orvc->builder), ^{
520 SecPathBuilderStep(orvc->builder);
526 - (NSURLRequest *)createNextRequest:(NSURL *)uri {
527 SecORVCRef orvc = (SecORVCRef)self.context;
528 CFDataRef ocspDER = CFRetainSafe(SecOCSPRequestGetDER(orvc->ocspRequest));
529 NSData *nsOcspDER = CFBridgingRelease(ocspDER);
530 NSString *ocspBase64 = [nsOcspDER base64EncodedStringWithOptions:0];
532 /* Ensure that we percent-encode specific characters in the base64 path
533 which are defined as delimiters in RFC 3986 [2.2].
535 static NSMutableCharacterSet *allowedSet = nil;
536 static dispatch_once_t onceToken;
537 dispatch_once(&onceToken, ^{
538 allowedSet = [[NSCharacterSet URLPathAllowedCharacterSet] mutableCopy];
539 [allowedSet removeCharactersInString:@":/?#[]@!$&'()*+,;="];
541 NSString *escapedRequest = [ocspBase64 stringByAddingPercentEncodingWithAllowedCharacters:allowedSet];
542 NSURLRequest *request = nil;
544 /* Interesting tidbit from rfc5019
545 When sending requests that are less than or equal to 255 bytes in
546 total (after encoding) including the scheme and delimiters (http://),
547 server name and base64-encoded OCSPRequest structure, clients MUST
548 use the GET method (to enable OCSP response caching). OCSP requests
549 larger than 255 bytes SHOULD be submitted using the POST method.
551 if (([[uri absoluteString] length] + 1 + [escapedRequest length]) < 256) {
553 NSString *requestString = [NSString stringWithFormat:@"%@/%@", [uri absoluteString], escapedRequest];
554 NSURL *requestURL = [NSURL URLWithString:requestString];
555 request = [NSURLRequest requestWithURL:requestURL];
558 NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:uri];
559 mutableRequest.HTTPMethod = @"POST";
560 mutableRequest.HTTPBody = nsOcspDER;
561 request = mutableRequest;
567 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)taskMetrics {
568 secdebug("rvc", "got metrics with task interval %f", taskMetrics.taskInterval.duration);
569 SecORVCRef orvc = (SecORVCRef)self.context;
570 if (orvc && orvc->builder) {
571 TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(orvc->builder);
573 analytics->ocsp_fetch_time += (uint64_t)(taskMetrics.taskInterval.duration * NSEC_PER_SEC);
579 bool SecORVCBeginFetches(SecORVCRef orvc, SecCertificateRef cert) {
581 CFArrayRef ocspResponders = CFRetainSafe(SecCertificateGetOCSPResponders(cert));
582 NSArray *nsResponders = CFBridgingRelease(ocspResponders);
584 NSInteger count = [nsResponders count];
585 if (count > OCSP_REQUEST_THRESHOLD) {
586 secnotice("rvc", "too may OCSP responder entries (%ld)", (long)count);
591 NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
592 config.timeoutIntervalForResource = TrustURLSessionGetResourceTimeout();
593 config.HTTPAdditionalHeaders = @{@"User-Agent" : @"com.apple.trustd/2.0"};
595 NSData *auditToken = CFBridgingRelease(SecPathBuilderCopyClientAuditToken(orvc->builder));
597 config._sourceApplicationAuditTokenData = auditToken;
600 OCSPFetchDelegate *delegate = [[OCSPFetchDelegate alloc] init];
601 delegate.context = orvc;
602 delegate.URIs = nsResponders;
605 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
607 NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:queue];
608 secdebug("rvc", "created URLSession for %@", cert);
611 if ((result = [delegate fetchNext:session])) {
612 /* no fetch scheduled, close the session */
613 [session invalidateAndCancel];