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 #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",
182 session, dataTask, (long)[(NSHTTPURLResponse *)response statusCode],
183 [response MIMEType], [response expectedContentLength]);
185 WithPathInRevocationInfoDirectory(NULL, ^(const char *utf8String) {
186 (void)checkBasePath(utf8String);
188 CFURLRef updateFileURL = SecCopyURLForFileInRevocationInfoDirectory(CFSTR("update-current"));
189 self->_currentUpdateFileURL = (updateFileURL) ? CFBridgingRelease(updateFileURL) : nil;
190 const char *updateFilePath = [self->_currentUpdateFileURL fileSystemRepresentation];
191 if (!updateFilePath) {
192 secnotice("validupdate", "failed to find revocation info directory. canceling task %@", dataTask);
193 completionHandler(NSURLSessionResponseCancel);
198 /* Clean up any old files from previous tasks. */
199 (void)remove(updateFilePath);
203 fd = open(updateFilePath, O_RDWR | O_CREAT | O_TRUNC, 0644);
204 if (fd < 0 || (off = lseek(fd, 0, SEEK_SET)) < 0) {
205 secnotice("validupdate","unable to open %@ (errno %d)", self->_currentUpdateFileURL, errno);
211 /* POWER LOG EVENT: background download actually started */
212 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
213 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
214 @"event" : @"downloadStarted"
216 secnotice("validupdate", "download started at %f", (double)CFAbsoluteTimeGetCurrent());
218 NSError *error = nil;
219 self->_currentUpdateFile = [NSFileHandle fileHandleForWritingToURL:self->_currentUpdateFileURL error:&error];
220 if (!self->_currentUpdateFile) {
221 secnotice("validupdate", "failed to open %@: %@. canceling task %@", self->_currentUpdateFileURL, error, dataTask);
222 #if ENABLE_TRUSTD_ANALYTICS
223 [[TrustdHealthAnalytics logger] logResultForEvent:TrustdHealthAnalyticsEventValidUpdate hardFailure:NO result:error];
224 #endif // ENABLE_TRUSTD_ANALYTICS
225 completionHandler(NSURLSessionResponseCancel);
230 completionHandler(NSURLSessionResponseAllow);
233 - (void)URLSession:(NSURLSession *)session
234 dataTask:(NSURLSessionDataTask *)dataTask
235 didReceiveData:(NSData *)data {
236 secdebug("validupdate", "Session %@ data task %@ returned %lu bytes (%lld bytes so far) out of expected %lld bytes",
237 session, dataTask, (unsigned long)[data length], [dataTask countOfBytesReceived], [dataTask countOfBytesExpectedToReceive]);
239 if (!self->_currentUpdateFile) {
240 secnotice("validupdate", "received data, but output file is not open");
247 /* Writing can fail and throw an exception, e.g. if we run out of disk space. */
248 [self->_currentUpdateFile writeData:data];
250 @catch(NSException *exception) {
251 secnotice("validupdate", "%s", exception.description.UTF8String);
252 TrustdHealthAnalyticsLogErrorCode(TAEventValidUpdate, TARecoverableError, errSecDiskFull);
258 - (void)URLSession:(NSURLSession *)session
259 task:(NSURLSessionTask *)task
260 didCompleteWithError:(NSError *)error {
262 secnotice("validupdate", "Session %@ task %@ failed with error %@", session, task, error);
263 #if ENABLE_TRUSTD_ANALYTICS
264 [[TrustdHealthAnalytics logger] logResultForEvent:TrustdHealthAnalyticsEventValidUpdate hardFailure:NO result:error];
265 #endif // ENABLE_TRUSTD_ANALYTICS
267 /* close file before we leave */
268 [self->_currentUpdateFile closeFile];
269 self->_currentUpdateFile = nil;
270 self->_currentUpdateServer = nil;
271 self->_currentUpdateFileURL = nil;
273 /* POWER LOG EVENT: background download finished */
274 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
275 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
276 @"event" : @"downloadFinished"
278 secnotice("validupdate", "download finished at %f", (double)CFAbsoluteTimeGetCurrent());
279 secdebug("validupdate", "Session %@ task %@ succeeded", session, task);
280 self->_finishedDownloading = YES;
281 [self updateDb:[self versionFromTask:task]];
283 if (self->_transaction) {
284 self->_transaction = nil;
290 @interface ValidUpdateRequest : NSObject
291 @property NSTimeInterval updateScheduled;
292 @property NSURLSession *backgroundSession;
295 static ValidUpdateRequest *request = nil;
297 @implementation ValidUpdateRequest
299 - (NSURLSessionConfiguration *)validUpdateConfiguration {
300 /* preferences to override defaults */
301 CFTypeRef value = NULL;
302 bool updateOnWiFiOnly = true;
303 value = CFPreferencesCopyValue(kUpdateWiFiOnlyKey, kSecPrefsDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost);
304 if (isBoolean(value)) {
305 updateOnWiFiOnly = CFBooleanGetValue((CFBooleanRef)value);
307 CFReleaseNull(value);
308 bool updateInBackground = true;
309 value = CFPreferencesCopyValue(kUpdateBackgroundKey, kSecPrefsDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost);
310 if (isBoolean(value)) {
311 updateInBackground = CFBooleanGetValue((CFBooleanRef)value);
313 CFReleaseNull(value);
315 NSURLSessionConfiguration *config = nil;
316 if (updateInBackground) {
317 config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier: @"com.apple.trustd.networking.background"];
318 config.networkServiceType = NSURLNetworkServiceTypeBackground;
319 config.discretionary = YES;
320 config._requiresPowerPluggedIn = YES;
321 config.allowsCellularAccess = (!updateOnWiFiOnly) ? YES : NO;
323 config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; // no cookies or data storage
324 config.networkServiceType = NSURLNetworkServiceTypeDefault;
325 config.discretionary = NO;
328 config.HTTPAdditionalHeaders = @{ @"User-Agent" : @"com.apple.trustd/2.0",
330 @"Accept-Encoding" : @"gzip,deflate,br"};
332 config.TLSMinimumSupportedProtocol = kTLSProtocol12;
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 queue.maxConcurrentOperationCount = 1;
353 _backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:queue];
356 - (BOOL) scheduleUpdateFromServer:(NSString *)server forVersion:(NSUInteger)version withQueue:(dispatch_queue_t)updateQueue {
358 secnotice("validupdate", "invalid update request");
363 secnotice("validupdate", "missing update queue, skipping update");
367 /* nsurlsessiond waits for unlock to finish launching, so we can't block trust evaluations
368 * on scheduling this background task. Also, we want to wait a sufficient amount of time
369 * after system boot before trying to initiate network activity, to avoid the possibility
370 * of a performance regression in the boot path. */
371 dispatch_async(updateQueue, ^{
372 CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
373 if (self.updateScheduled != 0.0) {
374 secdebug("validupdate", "update in progress (scheduled %f)", (double)self.updateScheduled);
377 uint64_t uptime = systemUptimeInSeconds();
378 const uint64_t minUptime = 180;
379 if (uptime < minUptime) {
380 gNextUpdate = now + (minUptime - uptime);
382 secnotice("validupdate", "postponing update until %f", gNextUpdate);
385 self.updateScheduled = now;
386 secnotice("validupdate", "scheduling update at %f", (double)self.updateScheduled);
390 /* we have an update to schedule, so take a transaction while we work */
391 os_transaction_t transaction = os_transaction_create("com.apple.trustd.valid.scheduleUpdate");
393 /* clear all old sessions and cleanup disk (for previous download tasks) */
394 static dispatch_once_t onceToken;
395 dispatch_once(&onceToken, ^{
397 [NSURLSession _obliterateAllBackgroundSessionsWithCompletionHandler:^{
398 secnotice("validupdate", "removing all old sessions for trustd");
403 if (!self.backgroundSession) {
404 [self createSession:updateQueue forServer:server];
406 ValidDelegate *delegate = (ValidDelegate *)[self.backgroundSession delegate];
407 delegate.currentUpdateServer = [server copy];
410 /* POWER LOG EVENT: scheduling our background download session now */
411 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
412 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
413 @"event" : @"downloadScheduled",
414 @"version" : @(version)
417 NSURL *validUrl = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@/g3/v%ld",
418 server, (unsigned long)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
424 transaction = nil; // ARC releases the transaction
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];