]> git.saurik.com Git - apple/security.git/blob - trust/trustd/SecRevocationNetworking.m
Security-59754.80.3.tar.gz
[apple/security.git] / trust / trustd / SecRevocationNetworking.m
1 /*
2 * Copyright (c) 2017-2019 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 *
23 */
24
25 #include <AssertMacros.h>
26 #import <Foundation/Foundation.h>
27 #import <CFNetwork/CFNSURLConnection.h>
28
29 #include <sys/sysctl.h>
30 #include <sys/time.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"
38
39 #include "SecRevocationDb.h"
40 #include "SecRevocationServer.h"
41 #include "SecTrustServer.h"
42 #include "SecOCSPRequest.h"
43 #include "SecOCSPResponse.h"
44
45 #import "SecTrustLoggingServer.h"
46 #import "TrustURLSessionDelegate.h"
47
48 #import "SecRevocationNetworking.h"
49
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");
54
55 extern CFAbsoluteTime gUpdateStarted;
56 extern CFAbsoluteTime gNextUpdate;
57
58 static int checkBasePath(const char *basePath) {
59 return mkpath_np((char*)basePath, 0755);
60 }
61
62 static uint64_t systemUptimeInSeconds() {
63 struct timeval boottime;
64 size_t tv_size = sizeof(boottime);
65 time_t now, uptime = 0;
66 int mib[2];
67 mib[0] = CTL_KERN;
68 mib[1] = KERN_BOOTTIME;
69 (void) time(&now);
70 if (sysctl(mib, 2, &boottime, &tv_size, NULL, 0) != -1 &&
71 boottime.tv_sec != 0) {
72 uptime = now - boottime.tv_sec;
73 }
74 return (uint64_t)uptime;
75 }
76
77 typedef void (^CompletionHandler)(void);
78
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;
87 @end
88
89 @implementation ValidDelegate
90
91 - (void)reschedule {
92 /* POWER LOG EVENT: operation canceled */
93 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
94 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
95 @"event" : (self->_finishedDownloading) ? @"updateCanceled" : @"downloadCanceled"
96 });
97 secnotice("validupdate", "%s canceled at %f",
98 (self->_finishedDownloading) ? "update" : "download",
99 (double)CFAbsoluteTimeGetCurrent());
100
101 self->_handler();
102 SecRevocationDbComputeAndSetNextUpdateTime();
103 if (self->_transaction) {
104 self->_transaction = nil;
105 }
106 }
107
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) {
113 [self reschedule];
114 return;
115 }
116
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"
124 });
125 secnotice("validupdate", "update started at %f", (double)CFAbsoluteTimeGetCurrent());
126
127 CFDataRef updateData = NULL;
128 const char *updateFilePath = [updateFileURL fileSystemRepresentation];
129 int rtn;
130 if ((rtn = readValidFile(updateFilePath, &updateData)) != 0) {
131 secerror("failed to read %@ with error %d", updateFileURL, rtn);
132 TrustdHealthAnalyticsLogErrorCode(TAEventValidUpdate, TAFatalError, rtn);
133 [self reschedule];
134 transaction = nil;
135 return;
136 }
137
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);
142 }
143 CFReleaseNull(updateData);
144
145 /* We're done with this file */
146 [updateFile closeFile];
147 if (updateFilePath) {
148 (void)remove(updateFilePath);
149 }
150 self->_currentUpdateFile = nil;
151 self->_currentUpdateFileURL = nil;
152 self->_currentUpdateServer = nil;
153
154 /* POWER LOG EVENT: background update finished */
155 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
156 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
157 @"event" : @"updateFinished"
158 });
159
160 /* Update is complete */
161 secnotice("validupdate", "update finished at %f", (double)CFAbsoluteTimeGetCurrent());
162 gUpdateStarted = 0;
163
164 self->_handler();
165 transaction = nil; // we're all done now
166 });
167 }
168
169 - (NSInteger)versionFromTask:(NSURLSessionTask *)task {
170 return atol([task.taskDescription cStringUsingEncoding:NSUTF8StringEncoding]);
171 }
172
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]);
182
183 WithPathInRevocationInfoDirectory(NULL, ^(const char *utf8String) {
184 (void)checkBasePath(utf8String);
185 });
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);
192 [self reschedule];
193 return;
194 }
195
196 /* Clean up any old files from previous tasks. */
197 (void)remove(updateFilePath);
198
199 int fd;
200 fd = open(updateFilePath, O_RDWR | O_CREAT | O_TRUNC, 0644);
201 if (fd < 0) {
202 secnotice("validupdate","unable to open %@ (errno %d)", self->_currentUpdateFileURL, errno);
203 }
204 if (fd >= 0) {
205 close(fd);
206 }
207
208 /* POWER LOG EVENT: background download actually started */
209 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
210 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
211 @"event" : @"downloadStarted"
212 });
213 secnotice("validupdate", "download started at %f", (double)CFAbsoluteTimeGetCurrent());
214
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 [[TrustAnalytics logger] logResultForEvent:TrustdHealthAnalyticsEventValidUpdate hardFailure:NO result:error];
221 #endif // ENABLE_TRUSTD_ANALYTICS
222 completionHandler(NSURLSessionResponseCancel);
223 [self reschedule];
224 return;
225 }
226
227 completionHandler(NSURLSessionResponseAllow);
228 }
229
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]);
235
236 if (!self->_currentUpdateFile) {
237 secnotice("validupdate", "received data, but output file is not open");
238 [dataTask cancel];
239 [self reschedule];
240 return;
241 }
242
243 @try {
244 /* Writing can fail and throw an exception, e.g. if we run out of disk space. */
245 [self->_currentUpdateFile writeData:data];
246 }
247 @catch(NSException *exception) {
248 secnotice("validupdate", "%s", exception.description.UTF8String);
249 TrustdHealthAnalyticsLogErrorCode(TAEventValidUpdate, TARecoverableError, errSecDiskFull);
250 [dataTask cancel];
251 [self reschedule];
252 }
253 }
254
255 - (void)URLSession:(NSURLSession *)session
256 task:(NSURLSessionTask *)task
257 didCompleteWithError:(NSError *)error {
258 if (error) {
259 secnotice("validupdate", "Session %@ task %@ failed with error %@", session, task, error);
260 #if ENABLE_TRUSTD_ANALYTICS
261 [[TrustAnalytics logger] logResultForEvent:TrustdHealthAnalyticsEventValidUpdate hardFailure:NO result:error];
262 #endif // ENABLE_TRUSTD_ANALYTICS
263 [self reschedule];
264 /* close file before we leave */
265 [self->_currentUpdateFile closeFile];
266 self->_currentUpdateFile = nil;
267 self->_currentUpdateServer = nil;
268 self->_currentUpdateFileURL = nil;
269 } else {
270 /* POWER LOG EVENT: background download finished */
271 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
272 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
273 @"event" : @"downloadFinished"
274 });
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]];
279 }
280 if (self->_transaction) {
281 self->_transaction = nil;
282 }
283 }
284
285 @end
286
287 @interface ValidUpdateRequest : NSObject
288 @property NSTimeInterval updateScheduled;
289 @property NSURLSession *backgroundSession;
290 @property NSURLSession *ephemeralSession;
291 @end
292
293 static ValidUpdateRequest *request = nil;
294
295 @implementation ValidUpdateRequest
296
297 - (NSURLSessionConfiguration *)validUpdateConfiguration:(BOOL)background {
298 NSURLSessionConfiguration *config = nil;
299 if (background) {
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);
306 }
307 CFReleaseNull(value);
308
309 config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier: @"com.apple.trustd.networking.background"];
310 config.networkServiceType = NSURLNetworkServiceTypeBackground;
311 config.discretionary = YES;
312 config._requiresPowerPluggedIn = YES;
313 config.allowsCellularAccess = (!updateOnWiFiOnly) ? YES : NO;
314 } else {
315 config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; // no cookies or data storage
316 config.networkServiceType = NSURLNetworkServiceTypeDefault;
317 config.discretionary = NO;
318 }
319
320 config.HTTPAdditionalHeaders = @{ @"User-Agent" : @"com.apple.trustd/2.0",
321 @"Accept" : @"*/*",
322 @"Accept-Encoding" : @"gzip,deflate,br"};
323
324 config.TLSMinimumSupportedProtocol = kTLSProtocol12;
325
326 return config;
327 }
328
329 - (NSURLSession *)createSession:(BOOL)background queue:(dispatch_queue_t)updateQueue forServer:(NSString *)updateServer
330 {
331 NSURLSessionConfiguration *config = [self validUpdateConfiguration:background];
332 ValidDelegate *delegate = [[ValidDelegate alloc] init];
333 delegate.handler = ^(void) {
334 request.updateScheduled = 0.0;
335 secdebug("validupdate", "resetting scheduled time");
336 };
337 delegate.transaction = NULL;
338 delegate.revDbUpdateQueue = updateQueue;
339 delegate.finishedDownloading = NO;
340 delegate.currentUpdateServer = [updateServer copy];
341
342 /* Callbacks should be on a separate NSOperationQueue.
343 We'll then dispatch the work on updateQueue and return from the callback. */
344 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
345 queue.maxConcurrentOperationCount = 1;
346 return [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:queue];
347 }
348
349 - (void) createSessions:(dispatch_queue_t)updateQueue forServer:(NSString *)updateServer {
350 self.ephemeralSession = [self createSession:NO queue:updateQueue forServer:updateServer];
351
352 bool updateInBackground = true;
353 CFTypeRef value = CFPreferencesCopyValue(kUpdateBackgroundKey, kSecPrefsDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost);
354 if (isBoolean(value)) {
355 updateInBackground = CFBooleanGetValue((CFBooleanRef)value);
356 }
357 CFReleaseNull(value);
358 if (updateInBackground) {
359 self.backgroundSession = [self createSession:YES queue:updateQueue forServer:updateServer];
360 } else {
361 self.backgroundSession = self.ephemeralSession;
362 }
363 }
364
365 - (BOOL) scheduleUpdateFromServer:(NSString *)server forVersion:(NSUInteger)version withQueue:(dispatch_queue_t)updateQueue {
366 if (!server) {
367 secnotice("validupdate", "invalid update request");
368 return NO;
369 }
370
371 if (!updateQueue) {
372 secnotice("validupdate", "missing update queue, skipping update");
373 return NO;
374 }
375
376 /* nsurlsessiond waits for unlock to finish launching, so we can't block trust evaluations
377 * on scheduling this background task. Also, we want to wait a sufficient amount of time
378 * after system boot before trying to initiate network activity, to avoid the possibility
379 * of a performance regression in the boot path. */
380 dispatch_async(updateQueue, ^{
381 CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
382 if (self.updateScheduled != 0.0) {
383 secdebug("validupdate", "update in progress (scheduled %f)", (double)self.updateScheduled);
384 return;
385 } else {
386 uint64_t uptime = systemUptimeInSeconds();
387 const uint64_t minUptime = 180;
388 if (uptime < minUptime) {
389 gNextUpdate = now + (minUptime - uptime);
390 gUpdateStarted = 0;
391 secnotice("validupdate", "postponing update until %f", gNextUpdate);
392 return;
393 } else {
394 self.updateScheduled = now;
395 secnotice("validupdate", "scheduling update at %f", (double)self.updateScheduled);
396 }
397 }
398
399 /* we have an update to schedule, so take a transaction while we work */
400 os_transaction_t transaction = os_transaction_create("com.apple.trustd.valid.scheduleUpdate");
401
402 /* clear all old sessions and cleanup disk (for previous download tasks) */
403 static dispatch_once_t onceToken;
404 dispatch_once(&onceToken, ^{
405 @autoreleasepool {
406 [NSURLSession _obliterateAllBackgroundSessionsWithCompletionHandler:^{
407 secnotice("validupdate", "removing all old sessions for trustd");
408 }];
409 }
410 });
411
412 if (!self.backgroundSession) {
413 [self createSessions:updateQueue forServer:server];
414 } else {
415 ValidDelegate *delegate = (ValidDelegate *)[self.backgroundSession delegate];
416 delegate.currentUpdateServer = [server copy];
417 }
418
419 /* POWER LOG EVENT: scheduling our background download session now */
420 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
421 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
422 @"event" : @"downloadScheduled",
423 @"version" : @(version)
424 });
425
426 NSURL *validUrl = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@/g3/v%ld",
427 server, (unsigned long)version]];
428 NSURLSessionDataTask *dataTask = [self.backgroundSession dataTaskWithURL:validUrl];
429 dataTask.taskDescription = [NSString stringWithFormat:@"%lu",(unsigned long)version];
430 [dataTask resume];
431 secnotice("validupdate", "scheduled background data task %@ at %f", dataTask, CFAbsoluteTimeGetCurrent());
432 (void) transaction; // dead store
433 transaction = nil; // ARC releases the transaction
434 });
435
436 return YES;
437 }
438
439 - (BOOL)updateNowFromServer:(NSString *)server version:(NSUInteger)version queue:(dispatch_queue_t)updateQueue
440 {
441 if (!server) {
442 secnotice("validupdate", "invalid update request");
443 return NO;
444 }
445
446 if (!updateQueue) {
447 secnotice("validupdate", "missing update queue, skipping update");
448 return NO;
449 }
450
451 if (!self.ephemeralSession) {
452 [self createSessions:updateQueue forServer:server];
453 } else {
454 ValidDelegate *delegate = (ValidDelegate *)[self.ephemeralSession delegate];
455 delegate.currentUpdateServer = [server copy];
456 }
457
458 /* POWER LOG EVENT: scheduling our background download session now */
459 SecPLLogRegisteredEvent(@"ValidUpdateEvent", @{
460 @"timestamp" : @([[NSDate date] timeIntervalSince1970]),
461 @"event" : @"downloadScheduled",
462 @"version" : @(version)
463 });
464
465 NSURL *validUrl = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@/g3/v%ld",
466 server, (unsigned long)version]];
467 NSURLSessionDataTask *dataTask = [self.ephemeralSession dataTaskWithURL:validUrl];
468 dataTask.taskDescription = [NSString stringWithFormat:@"%lu",(unsigned long)version];
469 [dataTask resume];
470 secnotice("validupdate", "running foreground data task %@ at %f", dataTask, CFAbsoluteTimeGetCurrent());
471 return YES;
472 }
473
474 @end
475
476 static void SecValidUpdateCreateValidUpdateRequest()
477 {
478 static dispatch_once_t onceToken;
479 dispatch_once(&onceToken, ^{
480 @autoreleasepool {
481 request = [[ValidUpdateRequest alloc] init];
482 }
483 });
484 }
485
486 bool SecValidUpdateRequest(dispatch_queue_t queue, CFStringRef server, CFIndex version) {
487 SecValidUpdateCreateValidUpdateRequest();
488 @autoreleasepool {
489 return [request scheduleUpdateFromServer:(__bridge NSString*)server forVersion:version withQueue:queue];
490 }
491 }
492
493 bool SecValidUpdateUpdateNow(dispatch_queue_t queue, CFStringRef server, CFIndex version) {
494 SecValidUpdateCreateValidUpdateRequest();
495 @autoreleasepool {
496 return [request updateNowFromServer:(__bridge NSString*)server version:version queue:queue];
497 }
498 }
499
500 /* MARK: - */
501 /* MARK: OCSP Fetch Networking */
502 #define OCSP_REQUEST_THRESHOLD 10
503
504 @interface OCSPFetchDelegate : TrustURLSessionDelegate
505 @end
506
507 @implementation OCSPFetchDelegate
508 - (BOOL)fetchNext:(NSURLSession *)session {
509 SecORVCRef orvc = (SecORVCRef)self.context;
510 TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(orvc->builder);
511
512 BOOL result = true;
513 if ((result = [super fetchNext:session])) {
514 /* no fetch scheduled */
515 orvc->done = true;
516 } else {
517 if (self.URIix > 0) {
518 orvc->responder = (__bridge CFURLRef)self.URIs[self.URIix - 1];
519 } else {
520 orvc->responder = (__bridge CFURLRef)self.URIs[0];
521 }
522 if (analytics) {
523 analytics->ocsp_fetches++;
524 }
525 }
526 return result;
527 }
528
529 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
530 /* call the superclass's method to set expiration */
531 [super URLSession:session task:task didCompleteWithError:error];
532
533 __block SecORVCRef orvc = (SecORVCRef)self.context;
534 if (!orvc || !orvc->builder) {
535 /* We already returned to the PathBuilder state machine. */
536 return;
537 }
538
539 TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(orvc->builder);
540 if (error) {
541 /* Log the error */
542 secnotice("rvc", "Failed to download ocsp response %@, with error %@", task.originalRequest.URL, error);
543 if (analytics) {
544 analytics->ocsp_fetch_failed++;
545 }
546 } else {
547 SecOCSPResponseRef ocspResponse = SecOCSPResponseCreate((__bridge CFDataRef)self.response);
548 if (ocspResponse) {
549 SecORVCConsumeOCSPResponse(orvc, ocspResponse, self.expiration, true, false);
550 if (analytics && !orvc->done) {
551 /* We got an OCSP response that didn't pass validation */
552 analytics->ocsp_validation_failed = true;
553 }
554 } else if (analytics) {
555 /* We got something that wasn't an OCSP response (e.g. captive portal) --
556 * we consider that a fetch failure */
557 analytics->ocsp_fetch_failed++;
558 }
559 }
560
561 /* If we didn't get a valid OCSP response, try the next URI */
562 if (!orvc->done) {
563 (void)[self fetchNext:session];
564 }
565
566 /* We got a valid OCSP response or couldn't schedule any more fetches.
567 * Close the session, update the PVCs, decrement the async count, and callback if we're all done. */
568 if (orvc->done) {
569 secdebug("rvc", "builder %p, done with OCSP fetches for cert: %ld", orvc->builder, orvc->certIX);
570 self.context = nil;
571 [session invalidateAndCancel];
572 SecORVCUpdatePVC(orvc);
573 if (0 == SecPathBuilderDecrementAsyncJobCount(orvc->builder)) {
574 /* We're the last async job to finish, jump back into the state machine */
575 secdebug("rvc", "builder %p, done with all async jobs", orvc->builder);
576 dispatch_async(SecPathBuilderGetQueue(orvc->builder), ^{
577 SecPathBuilderStep(orvc->builder);
578 });
579 }
580 }
581 }
582
583 - (NSURLRequest *)createNextRequest:(NSURL *)uri {
584 SecORVCRef orvc = (SecORVCRef)self.context;
585 CFDataRef ocspDER = CFRetainSafe(SecOCSPRequestGetDER(orvc->ocspRequest));
586 NSData *nsOcspDER = CFBridgingRelease(ocspDER);
587 NSString *ocspBase64 = [nsOcspDER base64EncodedStringWithOptions:0];
588
589 /* Ensure that we percent-encode specific characters in the base64 path
590 which are defined as delimiters in RFC 3986 [2.2].
591 */
592 static NSMutableCharacterSet *allowedSet = nil;
593 static dispatch_once_t onceToken;
594 dispatch_once(&onceToken, ^{
595 allowedSet = [[NSCharacterSet URLPathAllowedCharacterSet] mutableCopy];
596 [allowedSet removeCharactersInString:@":/?#[]@!$&'()*+,;="];
597 });
598 NSString *escapedRequest = [ocspBase64 stringByAddingPercentEncodingWithAllowedCharacters:allowedSet];
599 NSURLRequest *request = nil;
600
601 /* Interesting tidbit from rfc5019
602 When sending requests that are less than or equal to 255 bytes in
603 total (after encoding) including the scheme and delimiters (http://),
604 server name and base64-encoded OCSPRequest structure, clients MUST
605 use the GET method (to enable OCSP response caching). OCSP requests
606 larger than 255 bytes SHOULD be submitted using the POST method.
607 */
608 if (([[uri absoluteString] length] + 1 + [escapedRequest length]) < 256) {
609 /* Use a GET */
610 NSString *requestString = [NSString stringWithFormat:@"%@/%@", [uri absoluteString], escapedRequest];
611 NSURL *requestURL = [NSURL URLWithString:requestString];
612 request = [NSURLRequest requestWithURL:requestURL];
613 } else {
614 /* Use a POST */
615 NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:uri];
616 mutableRequest.HTTPMethod = @"POST";
617 mutableRequest.HTTPBody = nsOcspDER;
618 request = mutableRequest;
619 }
620
621 return request;
622 }
623
624 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)taskMetrics {
625 secdebug("rvc", "got metrics with task interval %f", taskMetrics.taskInterval.duration);
626 SecORVCRef orvc = (SecORVCRef)self.context;
627 if (orvc && orvc->builder) {
628 TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(orvc->builder);
629 if (analytics) {
630 analytics->ocsp_fetch_time += (uint64_t)(taskMetrics.taskInterval.duration * NSEC_PER_SEC);
631 }
632 }
633 }
634 @end
635
636 bool SecORVCBeginFetches(SecORVCRef orvc, SecCertificateRef cert) {
637 @autoreleasepool {
638 CFArrayRef ocspResponders = CFRetainSafe(SecCertificateGetOCSPResponders(cert));
639 NSArray *nsResponders = CFBridgingRelease(ocspResponders);
640
641 NSInteger count = [nsResponders count];
642 if (count > OCSP_REQUEST_THRESHOLD) {
643 secnotice("rvc", "too may OCSP responder entries (%ld)", (long)count);
644 orvc->done = true;
645 return true;
646 }
647
648 NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
649 config.timeoutIntervalForResource = TrustURLSessionGetResourceTimeout();
650 config.HTTPAdditionalHeaders = @{@"User-Agent" : @"com.apple.trustd/2.0"};
651
652 NSData *auditToken = CFBridgingRelease(SecPathBuilderCopyClientAuditToken(orvc->builder));
653 if (auditToken) {
654 config._sourceApplicationAuditTokenData = auditToken;
655 }
656
657 OCSPFetchDelegate *delegate = [[OCSPFetchDelegate alloc] init];
658 delegate.context = orvc;
659 delegate.URIs = nsResponders;
660 delegate.URIix = 0;
661
662 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
663
664 NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:queue];
665 secdebug("rvc", "created URLSession for %@", cert);
666
667 bool result = false;
668 if ((result = [delegate fetchNext:session])) {
669 /* no fetch scheduled, close the session */
670 [session invalidateAndCancel];
671 }
672 return result;
673 }
674 }