]> git.saurik.com Git - apple/security.git/blob - OSX/sec/securityd/SecPinningDb.m
Security-58286.1.32.tar.gz
[apple/security.git] / OSX / sec / securityd / SecPinningDb.m
1 /*
2 * Copyright (c) 2016 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 /*
26 * SecPinningDb.m
27 */
28
29 #include <AssertMacros.h>
30 #import <Foundation/Foundation.h>
31 #import <sys/stat.h>
32 #import <notify.h>
33
34 #if !TARGET_OS_BRIDGE
35 #import <MobileAsset/MAAsset.h>
36 #import <MobileAsset/MAAssetQuery.h>
37 #endif
38
39 #if TARGET_OS_OSX
40 #import <MobileAsset/MobileAsset.h>
41 #endif
42
43 #import <Security/SecInternalReleasePriv.h>
44
45 #import <securityd/OTATrustUtilities.h>
46 #import <securityd/SecPinningDb.h>
47
48 #include "utilities/debugging.h"
49 #include "utilities/sqlutils.h"
50 #include "utilities/iOSforOSX.h"
51 #include <utilities/SecCFError.h>
52 #include <utilities/SecCFRelease.h>
53 #include <utilities/SecCFWrappers.h>
54 #include <utilities/SecDb.h>
55 #include <utilities/SecFileLocations.h>
56 #include "utilities/sec_action.h"
57
58 #define kSecPinningBasePath "/Library/Keychains/"
59 #define kSecPinningDbFileName "pinningrules.sqlite3"
60
61 const uint64_t PinningDbSchemaVersion = 2;
62 const NSString *PinningDbPolicyNameKey = @"policyName"; /* key for a string value */
63 const NSString *PinningDbDomainsKey = @"domains"; /* key for an array of dictionaries */
64 const NSString *PinningDbPoliciesKey = @"rules"; /* key for an array of dictionaries */
65 const NSString *PinningDbDomainSuffixKey = @"suffix"; /* key for a string */
66 const NSString *PinningDbLabelRegexKey = @"labelRegex"; /* key for a regex string */
67
68 const CFStringRef kSecPinningDbKeyHostname = CFSTR("PinningHostname");
69 const CFStringRef kSecPinningDbKeyPolicyName = CFSTR("PinningPolicyName");
70 const CFStringRef kSecPinningDbKeyRules = CFSTR("PinningRules");
71
72 #if !TARGET_OS_BRIDGE
73 const NSString *PinningDbMobileAssetType = @"com.apple.MobileAsset.CertificatePinning";
74 #define kSecPinningDbMobileAssetNotification "com.apple.MobileAsset.CertificatePinning.cached-metadata-updated"
75 #endif
76
77 #if TARGET_OS_OSX
78 const NSUInteger PinningDbMobileAssetCompatibilityVersion = 1;
79 #endif
80
81 @interface SecPinningDb : NSObject
82 @property (assign) SecDbRef db;
83 @property dispatch_queue_t queue;
84 @property NSURL *dbPath;
85 - (instancetype) init;
86 - ( NSDictionary * _Nullable ) queryForDomain:(NSString *)domain;
87 - ( NSDictionary * _Nullable ) queryForPolicyName:(NSString *)policyName;
88 @end
89
90 static bool isDbOwner() {
91 #ifdef NO_SERVER
92 // Test app running as securityd
93 #elif TARGET_OS_IPHONE
94 if (getuid() == 64) // _securityd
95 #else
96 if (getuid() == 0)
97 #endif
98 {
99 return true;
100 }
101 return false;
102 }
103
104 static inline bool isNSNumber(id nsType) {
105 return nsType && [nsType isKindOfClass:[NSNumber class]];
106 }
107
108 static inline bool isNSArray(id nsType) {
109 return nsType && [nsType isKindOfClass:[NSArray class]];
110 }
111
112 static inline bool isNSDictionary(id nsType) {
113 return nsType && [nsType isKindOfClass:[NSDictionary class]];
114 }
115
116 @implementation SecPinningDb
117 #define getSchemaVersionSQL CFSTR("PRAGMA user_version")
118 #define selectVersionSQL CFSTR("SELECT ival FROM admin WHERE key='version'")
119 #define insertAdminSQL CFSTR("INSERT OR REPLACE INTO admin (key,ival,value) VALUES (?,?,?)")
120 #define selectDomainSQL CFSTR("SELECT DISTINCT labelRegex,policyName,policies FROM rules WHERE domainSuffix=?")
121 #define selectPolicyNameSQL CFSTR("SELECT DISTINCT policies FROM rules WHERE policyName=?")
122 #define insertRuleSQL CFSTR("INSERT OR REPLACE INTO rules (policyName,domainSuffix,labelRegex,policies) VALUES (?,?,?,?) ")
123 #define removeAllRulesSQL CFSTR("DELETE FROM rules;")
124
125 - (NSNumber *)getSchemaVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
126 __block bool ok = true;
127 __block NSNumber *version = nil;
128 ok &= SecDbWithSQL(dbconn, getSchemaVersionSQL, error, ^bool(sqlite3_stmt *selectVersion) {
129 ok &= SecDbStep(dbconn, selectVersion, error, ^(bool *stop) {
130 int ival = sqlite3_column_int(selectVersion, 0);
131 version = [NSNumber numberWithInt:ival];
132 });
133 return ok;
134 });
135 return version;
136 }
137
138 - (BOOL)setSchemaVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
139 bool ok = true;
140 NSString *setVersion = [NSString stringWithFormat:@"PRAGMA user_version = %llu", PinningDbSchemaVersion];
141 ok &= SecDbExec(dbconn,
142 (__bridge CFStringRef)setVersion,
143 error);
144 if (!ok) {
145 secerror("SecPinningDb: failed to create admin table: %@", error ? *error : nil);
146 }
147 return ok;
148 }
149
150 - (NSNumber *)getContentVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
151 __block bool ok = true;
152 __block NSNumber *version = nil;
153 ok &= SecDbWithSQL(dbconn, selectVersionSQL, error, ^bool(sqlite3_stmt *selectVersion) {
154 ok &= SecDbStep(dbconn, selectVersion, error, ^(bool *stop) {
155 uint64_t ival = sqlite3_column_int64(selectVersion, 0);
156 version = [NSNumber numberWithUnsignedLongLong:ival];
157 });
158 return ok;
159 });
160 return version;
161 }
162
163 - (BOOL)setContentVersion:(NSNumber *)version dbConnection:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
164 __block BOOL ok = true;
165 ok &= SecDbWithSQL(dbconn, insertAdminSQL, error, ^bool(sqlite3_stmt *insertAdmin) {
166 const char *versionKey = "version";
167 ok &= SecDbBindText(insertAdmin, 1, versionKey, strlen(versionKey), SQLITE_TRANSIENT, error);
168 ok &= SecDbBindInt64(insertAdmin, 2, [version unsignedLongLongValue], error);
169 ok &= SecDbStep(dbconn, insertAdmin, error, NULL);
170 return ok;
171 });
172 if (!ok) {
173 secerror("SecPinningDb: failed to set version %@ from pinning list: %@", version, error ? *error : nil);
174 }
175 return ok;
176 }
177
178 - (BOOL) shouldUpdateContent:(NSNumber *)new_version {
179 __block CFErrorRef error = NULL;
180 __block BOOL ok = YES;
181 __block BOOL newer = NO;
182 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
183 NSNumber *db_version = [self getContentVersion:dbconn error:&error];
184 if (!db_version || [new_version compare:db_version] == NSOrderedDescending) {
185 newer = YES;
186 secnotice("pinningDb", "Pinning database should update from version %@ to version %@", db_version, new_version);
187 }
188 });
189
190 if (!ok || error) {
191 secerror("SecPinningDb: error reading content version from database %@", error);
192 }
193 CFReleaseNull(error);
194 return newer;
195 }
196
197 - (BOOL) insertRuleWithName:(NSString *)policyName
198 domainSuffix:(NSString *)domainSuffix
199 labelRegex:(NSString *)labelRegex
200 policies:(NSArray *)policies
201 dbConnection:(SecDbConnectionRef)dbconn
202 error:(CFErrorRef *)error{
203 /* @@@ This insertion mechanism assumes that the input is trusted -- namely, that the new rules
204 * are allowed to replace existing rules. For third-party inputs, this assumption isn't true. */
205
206 secdebug("pinningDb", "inserting new rule: %@ for %@.%@", policyName, labelRegex, domainSuffix);
207
208 __block bool ok = true;
209 ok &= SecDbWithSQL(dbconn, insertRuleSQL, error, ^bool(sqlite3_stmt *insertRule) {
210 ok &= SecDbBindText(insertRule, 1, [policyName UTF8String], [policyName length], SQLITE_TRANSIENT, error);
211 ok &= SecDbBindText(insertRule, 2, [domainSuffix UTF8String], [domainSuffix length], SQLITE_TRANSIENT, error);
212 ok &= SecDbBindText(insertRule, 3, [labelRegex UTF8String], [labelRegex length], SQLITE_TRANSIENT, error);
213 NSData *xmlPolicies = [NSPropertyListSerialization dataWithPropertyList:policies
214 format:NSPropertyListXMLFormat_v1_0
215 options:0
216 error:nil];
217 if (!xmlPolicies) {
218 secerror("SecPinningDb: failed to serialize policies");
219 ok = false;
220 }
221 ok &= SecDbBindBlob(insertRule, 4, [xmlPolicies bytes], [xmlPolicies length], SQLITE_TRANSIENT, error);
222 ok &= SecDbStep(dbconn, insertRule, error, NULL);
223 return ok;
224 });
225 if (!ok) {
226 secerror("SecPinningDb: failed to insert rule %@ for %@.%@ with error %@", policyName, labelRegex, domainSuffix, error ? *error : nil);
227 }
228 return ok;
229 }
230
231 - (BOOL) populateDbFromBundle:(NSArray *)pinningList dbConnection:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
232 __block BOOL ok = true;
233 [pinningList enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
234 if (idx ==0) { return; } // Skip the first value which is the version
235 if (!isNSDictionary(obj)) {
236 secerror("SecPinningDb: rule entry in pinning plist is wrong class");
237 ok = false;
238 return;
239 }
240 NSDictionary *rule = obj;
241 __block NSString *policyName = [rule objectForKey:PinningDbPolicyNameKey];
242 NSArray *domains = [rule objectForKey:PinningDbDomainsKey];
243 __block NSArray *policies = [rule objectForKey:PinningDbPoliciesKey];
244
245 if (!policyName || !domains || !policies) {
246 secerror("SecPinningDb: failed to get required fields from rule entry %lu", (unsigned long)idx);
247 ok = false;
248 return;
249 }
250
251 [domains enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
252 if (!isNSDictionary(obj)) {
253 secerror("SecPinningDb: domain entry %lu for %@ in pinning rule is wrong class", (unsigned long)idx, policyName);
254 ok = false;
255 return;
256 }
257 NSDictionary *domain = obj;
258 NSString *suffix = [domain objectForKey:PinningDbDomainSuffixKey];
259 NSString *labelRegex = [domain objectForKey:PinningDbLabelRegexKey];
260
261 if (!suffix || !labelRegex) {
262 secerror("SecPinningDb: failed to get required fields for entry %lu for %@", (unsigned long)idx, policyName);
263 ok = false;
264 return;
265 }
266 ok &= [self insertRuleWithName:policyName domainSuffix:suffix labelRegex:labelRegex policies:policies
267 dbConnection:dbconn error:error];
268 }];
269 }];
270 if (!ok) {
271 secerror("SecPinningDb: failed to populate DB from pinning list: %@", error ? *error : nil);
272 }
273 return ok;
274 }
275
276 - (BOOL) removeAllRulesFromDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
277 __block BOOL ok = true;
278 ok &= SecDbWithSQL(dbconn, removeAllRulesSQL, error, ^bool(sqlite3_stmt *deleteRules) {
279 ok &= SecDbStep(dbconn, deleteRules, error, NULL);
280 return ok;
281 });
282 if (!ok) {
283 secerror("SecPinningDb: failed to delete old values: %@", error ? *error :nil);
284 }
285 return ok;
286 }
287
288
289 - (BOOL) createOrAlterAdminTable:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
290 bool ok = true;
291 ok &= SecDbExec(dbconn,
292 CFSTR("CREATE TABLE IF NOT EXISTS admin("
293 "key TEXT PRIMARY KEY NOT NULL,"
294 "ival INTEGER NOT NULL,"
295 "value BLOB"
296 ");"),
297 error);
298 if (!ok) {
299 secerror("SecPinningDb: failed to create admin table: %@", error ? *error : nil);
300 }
301 return ok;
302 }
303
304 - (BOOL) createOrAlterRulesTable:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
305 bool ok = true;
306 ok &= SecDbExec(dbconn,
307 CFSTR("CREATE TABLE IF NOT EXISTS rules("
308 "policyName TEXT NOT NULL,"
309 "domainSuffix TEXT NOT NULL,"
310 "labelRegex TEXT NOT NULL,"
311 "policies BLOB NOT NULL,"
312 "UNIQUE(policyName, domainSuffix, labelRegex)"
313 ");"),
314 error);
315 ok &= SecDbExec(dbconn, CFSTR("CREATE INDEX IF NOT EXISTS idomain ON rules(domainSuffix);"), error);
316 ok &= SecDbExec(dbconn, CFSTR("CREATE INDEX IF NOT EXISTS ipolicy ON rules(policyName);"), error);
317 if (!ok) {
318 secerror("SecPinningDb: failed to create rules table: %@", error ? *error : nil);
319 }
320 return ok;
321 }
322
323 #if !TARGET_OS_BRIDGE
324 - (BOOL) installDbFromURL:(NSURL *)localURL {
325 if (!localURL) {
326 secerror("SecPinningDb: missing url for downloaded asset");
327 return NO;
328 }
329 NSURL* basePath = nil, *fileLoc = nil;
330 if (![localURL scheme]) {
331 /* MobileAsset provides the URL without the scheme. Fix it up. */
332 NSString *pathWithScheme = [[NSString alloc] initWithFormat:@"%@",localURL];
333 basePath = [NSURL fileURLWithPath:pathWithScheme isDirectory:YES];
334 } else {
335 basePath = localURL;
336 }
337 fileLoc = [NSURL URLWithString:@"CertificatePinning.plist"
338 relativeToURL:basePath];
339 __block NSArray *pinningList = [NSArray arrayWithContentsOfURL:fileLoc];
340 if (!pinningList) {
341 secerror("SecPinningDb: unable to create pinning list from asset file: %@", fileLoc);
342 return NO;
343 }
344
345 NSNumber *plist_version = [pinningList objectAtIndex:0];
346 if (![self shouldUpdateContent:plist_version]) {
347 /* We got a new plist but we already have that version installed. */
348 return YES;
349 }
350
351 /* Update Content */
352 __block CFErrorRef error = NULL;
353 __block BOOL ok = YES;
354 ok &= SecDbPerformWrite(_db, &error, ^(SecDbConnectionRef dbconn) {
355 ok &= [self updateDb:dbconn error:&error pinningList:pinningList updateSchema:NO updateContent:YES];
356 });
357
358 if (error) {
359 secerror("SecPinningDb: error installing updated pinning list version %@: %@", [pinningList objectAtIndex:0], error);
360 CFReleaseNull(error);
361 }
362
363 return ok;
364 }
365
366 #if TARGET_OS_OSX
367 const CFStringRef kSecSUPrefDomain = CFSTR("com.apple.SoftwareUpdate");
368 const CFStringRef kSecSUScanPrefConfigDataInstallKey = CFSTR("ConfigDataInstall");
369 #endif
370
371 static BOOL PinningDbCanCheckMobileAsset(void) {
372 BOOL result = YES;
373 #if TARGET_OS_OSX
374 /* Check the user's SU preferences to determine if "Install system data files" is off */
375 if (!CFPreferencesSynchronize(kSecSUPrefDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost)) {
376 secerror("SecPinningDb: unable to synchronize SoftwareUpdate prefs");
377 return NO;
378 }
379
380 id value = nil;
381 if (CFPreferencesAppValueIsForced(kSecSUScanPrefConfigDataInstallKey, kSecSUPrefDomain)) {
382 value = CFBridgingRelease(CFPreferencesCopyAppValue(kSecSUScanPrefConfigDataInstallKey, kSecSUPrefDomain));
383 } else {
384 value = CFBridgingRelease(CFPreferencesCopyValue(kSecSUScanPrefConfigDataInstallKey, kSecSUPrefDomain,
385 kCFPreferencesAnyUser, kCFPreferencesCurrentHost));
386 }
387 if (isNSNumber(value)) {
388 result = [value boolValue];
389 }
390
391 if (!result) { secnotice("pinningDb", "User has disabled system data installation."); }
392
393 /* MobileAsset.framework isn't mastered into the BaseSystem. Check that the MA classes are linked. */
394 if (![ASAssetQuery class] || ![ASAsset class] || ![MAAssetQuery class] || ![MAAsset class]) {
395 secnotice("PinningDb", "Weak linked MobileAsset framework missing.");
396 result = NO;
397 }
398 #endif
399 return result;
400 }
401
402 #if TARGET_OS_IPHONE
403 - (void) downloadPinningAsset:(BOOL __unused)isLocalOnly {
404 if (!PinningDbCanCheckMobileAsset()) {
405 secnotice("pinningDb", "MobileAsset disabled, skipping check.");
406 return;
407 }
408
409 secnotice("pinningDb", "begin MobileAsset query for catalog");
410 [MAAsset startCatalogDownload:(NSString *)PinningDbMobileAssetType then:^(MADownLoadResult result) {
411 if (result != MADownloadSucceesful) {
412 secerror("SecPinningDb: failed to download catalog: %ld", (long)result);
413 return;
414 }
415 MAAssetQuery *query = [[MAAssetQuery alloc] initWithType:(NSString *)PinningDbMobileAssetType];
416 [query augmentResultsWithState:true];
417
418 secnotice("pinningDb", "begin MobileAsset metadata sync request");
419 MAQueryResult queryResult = [query queryMetaDataSync];
420 if (queryResult != MAQuerySucceesful) {
421 secerror("SecPinningDb: failed to query MobileAsset metadata: %ld", (long)queryResult);
422 return;
423 }
424
425 if (!query.results) {
426 secerror("SecPinningDb: no results in MobileAsset query");
427 return;
428 }
429
430 for (MAAsset *asset in query.results) {
431 NSNumber *asset_version = [asset assetProperty:@"_ContentVersion"];
432 if (![self shouldUpdateContent:asset_version]) {
433 secdebug("pinningDb", "skipping asset because we already have _ContentVersion %@", asset_version);
434 continue;
435 }
436 switch(asset.state) {
437 default:
438 secerror("SecPinningDb: unknown asset state %ld", (long)asset.state);
439 continue;
440 case MAInstalled:
441 /* The asset is already in the cache, get it from disk. */
442 secdebug("pinningDb", "CertificatePinning asset already installed");
443 if([self installDbFromURL:[asset getLocalUrl]]) {
444 secnotice("pinningDb", "finished db update from installed asset. purging asset.");
445 [asset purge:^(MAPurgeResult purge_result) {
446 if (purge_result != MAPurgeSucceeded) {
447 secerror("SecPinningDb: purge failed: %ld", (long)purge_result);
448 }
449 }];
450 }
451 break;
452 case MAUnknown:
453 secerror("SecPinningDb: pinning asset is unknown");
454 continue;
455 case MADownloading:
456 secnotice("pinningDb", "pinning asset is downloading");
457 /* fall through */
458 case MANotPresent:
459 secnotice("pinningDb", "begin download of CertificatePinning asset");
460 [asset startDownload:^(MADownLoadResult downloadResult) {
461 if (downloadResult != MADownloadSucceesful) {
462 secerror("SecPinningDb: failed to download pinning asset: %ld", (long)downloadResult);
463 return;
464 }
465 if([self installDbFromURL:[asset getLocalUrl]]) {
466 secnotice("pinningDb", "finished db update from installed asset. purging asset.");
467 [asset purge:^(MAPurgeResult purge_result) {
468 if (purge_result != MAPurgeSucceeded) {
469 secerror("SecPinningDb: purge failed: %ld", (long)purge_result);
470 }
471 }];
472 }
473 }];
474 break;
475 }
476 }
477 }];
478 }
479 #else /* !TARGET_OS_IPHONE */
480 /* <rdar://problem/30879827> MobileAssetV2 fails on macOS, so use V1 */
481 - (void) downloadPinningAsset:(BOOL)isLocalOnly {
482 if (!PinningDbCanCheckMobileAsset()) {
483 secnotice("pinningDb", "MobileAsset disabled, skipping check.");
484 return;
485 }
486
487 ASAssetQuery *query = [[ASAssetQuery alloc] initWithAssetType:(NSString *)PinningDbMobileAssetType];
488 [query setQueriesLocalAssetInformationOnly:isLocalOnly]; // Omitting this leads to a notifcation loop.
489 NSError *error = nil;
490 NSArray<ASAsset *>*query_results = [query runQueryAndReturnError:&error];
491 if (!query_results) {
492 secerror("SecPinningDb: asset query failed: %@", error);
493 return;
494 }
495
496 for (ASAsset *asset in query_results) {
497 NSDictionary *attributes = [asset attributes];
498
499 NSNumber *compatibilityVersion = [attributes objectForKey:ASAttributeCompatibilityVersion];
500 if (!isNSNumber(compatibilityVersion) ||
501 [compatibilityVersion unsignedIntegerValue] != PinningDbMobileAssetCompatibilityVersion) {
502 secnotice("pinningDb", "Skipping asset with compatibility version %@", compatibilityVersion);
503 continue;
504 }
505
506 NSNumber *contentVersion = [attributes objectForKey:ASAttributeContentVersion];
507 if (!isNSNumber(contentVersion) || ![self shouldUpdateContent:contentVersion]) {
508 secnotice("pinningDb", "Skipping asset with content version %@", contentVersion);
509 continue;
510 }
511
512 ASProgressHandler pinningHandler = ^(NSDictionary *state, NSError *progressError){
513 if (progressError) {
514 secerror("SecPinningDb: asset download error: %@", progressError);
515 return;
516 }
517
518 if (!state) {
519 secerror("SecPinningDb: no asset state in progress handler");
520 return;
521 }
522
523 NSString *operationState = [state objectForKey:ASStateOperation];
524 secdebug("pinningDb", "Asset state is %@", operationState);
525
526 if (operationState && [operationState isEqualToString:ASOperationCompleted]) {
527 if ([self installDbFromURL:[asset localURL]]) {
528 secnotice("pinningDb", "finished db update from installed asset. purging asset.");
529 [asset purge:^(NSError *error) {
530 if (error) {
531 secerror("SecPinningDb: purge failed %@", error);
532 }
533 }];
534 }
535 }
536 };
537
538 switch ([asset state]) {
539 case ASAssetStateNotPresent:
540 secdebug("pinningDb", "CertificatePinning asset needs to be downloaded");
541 asset.progressHandler= pinningHandler;
542 asset.userInitiatedDownload = YES;
543 [asset beginDownloadWithOptions:@{ASDownloadOptionPriority : ASDownloadPriorityNormal}];
544 break;
545 case ASAssetStateInstalled:
546 /* The asset is already in the cache, get it from disk. */
547 secdebug("pinningDb", "CertificatePinning asset already installed");
548 if([self installDbFromURL:[asset localURL]]) {
549 secnotice("pinningDb", "finished db update from installed asset. purging asset.");
550 [asset purge:^(NSError *error) {
551 if (error) {
552 secerror("SecPinningDb: purge failed %@", error);
553 }
554 }];
555 }
556 break;
557 case ASAssetStatePaused:
558 secdebug("pinningDb", "CertificatePinning asset download paused");
559 asset.progressHandler = pinningHandler;
560 asset.userInitiatedDownload = YES;
561 if (![asset resumeDownloadAndReturnError:&error]) {
562 secerror("SecPinningDb: failed to resume download of asset: %@", error);
563 }
564 break;
565 case ASAssetStateDownloading:
566 secdebug("pinningDb", "CertificatePinning asset downloading");
567 asset.progressHandler = pinningHandler;
568 asset.userInitiatedDownload = YES;
569 break;
570 default:
571 secerror("SecPinningDb: unhandled asset state %ld", (long)asset.state);
572 continue;
573 }
574 }
575 }
576 #endif /* !TARGET_OS_IPHONE */
577
578 - (void) downloadPinningAsset {
579 [self downloadPinningAsset:NO];
580 }
581 #endif /* !TARGET_OS_BRIDGE */
582
583 - (NSArray *) copyCurrentPinningList {
584 NSArray *pinningList = nil;
585 /* Get the pinning list shipped with the OS */
586 SecOTAPKIRef otapkiref = SecOTAPKICopyCurrentOTAPKIRef();
587 if (otapkiref) {
588 pinningList = CFBridgingRelease(SecOTAPKICopyPinningList(otapkiref));
589 CFReleaseNull(otapkiref);
590 if (!pinningList) {
591 secerror("SecPinningDb: failed to read pinning plist from bundle");
592 }
593 }
594
595 #if !TARGET_OS_BRIDGE
596 /* Asynchronously ask MobileAsset for most recent pinning list. */
597 dispatch_async(_queue, ^{
598 secnotice("pinningDb", "Initial check with MobileAsset for newer pinning asset");
599 [self downloadPinningAsset];
600 });
601
602 /* Register for changes in our asset */
603 if (PinningDbCanCheckMobileAsset()) {
604 int out_token = 0;
605 notify_register_dispatch(kSecPinningDbMobileAssetNotification, &out_token, self->_queue, ^(int __unused token) {
606 secnotice("pinningDb", "Got a notification about a new pinning asset.");
607 [self downloadPinningAsset:YES];
608 });
609 }
610 #endif
611
612 return pinningList;
613 }
614
615 - (BOOL) updateDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error pinningList:(NSArray *)pinningList
616 updateSchema:(BOOL)updateSchema updateContent:(BOOL)updateContent
617 {
618 if (!isDbOwner()) { return false; }
619 secdebug("pinningDb", "updating or creating database");
620
621 __block bool ok = true;
622 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, error, ^(bool *commit) {
623 if (updateSchema) {
624 /* update the tables */
625 ok &= [self createOrAlterAdminTable:dbconn error:error];
626 ok &= [self createOrAlterRulesTable:dbconn error:error];
627 ok &= [self setSchemaVersion:dbconn error:error];
628 }
629
630 if (updateContent) {
631 /* remove the old data */
632 /* @@@ This behavior assumes that we have all the rules we want to populate
633 * elsewhere on disk and that the DB doesn't contain the sole copy of that data. */
634 ok &= [self removeAllRulesFromDb:dbconn error:error];
635
636 /* read the new data */
637 NSNumber *version = [pinningList objectAtIndex:0];
638
639 /* populate the tables */
640 ok &= [self populateDbFromBundle:pinningList dbConnection:dbconn error:error];
641 ok &= [self setContentVersion:version dbConnection:dbconn error:error];
642 }
643
644 *commit = ok;
645 });
646
647 return ok;
648 }
649
650 - (SecDbRef) createAtPath {
651 bool readWrite = isDbOwner();
652 mode_t mode = 0644;
653
654 CFStringRef path = CFStringCreateWithCString(NULL, [_dbPath fileSystemRepresentation], kCFStringEncodingUTF8);
655 SecDbRef result = SecDbCreateWithOptions(path, mode, readWrite, false, false,
656 ^bool (SecDbRef db, SecDbConnectionRef dbconn, bool didCreate, bool *callMeAgainForNextConnection, CFErrorRef *error) {
657 if (!isDbOwner()) {
658 /* Non-owner process can't update the db, but it should get a db connection.
659 * @@@ Revisit if new schema version is needed by reader processes. */
660 return true;
661 }
662
663 __block BOOL ok = true;
664 dispatch_sync(self->_queue, ^{
665 bool updateSchema = false;
666 bool updateContent = false;
667
668 /* Get the pinning plist */
669 NSArray *pinningList = [self copyCurrentPinningList];
670 if (!pinningList) {
671 secerror("SecPinningDb: failed to find pinning plist in bundle");
672 ok = false;
673 return;
674 }
675
676 /* Check latest data and schema versions against existing table. */
677 if (!isNSNumber([pinningList objectAtIndex:0])) {
678 secerror("SecPinningDb: pinning plist in wrong format");
679 return; // Don't change status. We can continue to use old DB.
680 }
681 NSNumber *plist_version = [pinningList objectAtIndex:0];
682 NSNumber *db_version = [self getContentVersion:dbconn error:error];
683 if (!db_version || [plist_version compare:db_version] == NSOrderedDescending) {
684 secnotice("pinningDb", "Updating pinning database content from version %@ to version %@",
685 db_version ? db_version : 0, plist_version);
686 updateContent = true;
687 }
688 NSNumber *schema_version = [self getSchemaVersion:dbconn error:error];
689 NSNumber *current_version = [NSNumber numberWithUnsignedLongLong:PinningDbSchemaVersion];
690 if (!schema_version || ![schema_version isEqualToNumber:current_version]) {
691 secnotice("pinningDb", "Updating pinning database schema from version %@ to version %@",
692 schema_version, current_version);
693 updateSchema = true;
694 }
695
696 if (updateContent || updateSchema) {
697 ok &= [self updateDb:dbconn error:error pinningList:pinningList updateSchema:updateSchema updateContent:updateContent];
698 }
699 if (!ok) {
700 secerror("SecPinningDb: %s failed: %@", didCreate ? "Create" : "Open", error ? *error : NULL);
701 }
702 });
703 return ok;
704 });
705
706 CFReleaseNull(path);
707 return result;
708 }
709
710 static void verify_create_path(const char *path)
711 {
712 int ret = mkpath_np(path, 0755);
713 if (!(ret == 0 || ret == EEXIST)) {
714 secerror("could not create path: %s (%s)", path, strerror(ret));
715 }
716 }
717
718 - (NSURL *)pinningDbPath {
719 /* Make sure the /Library/Keychains directory is there */
720 #if TARGET_OS_IPHONE
721 NSURL *directory = CFBridgingRelease(SecCopyURLForFileInKeychainDirectory(nil));
722 #else
723 NSURL *directory = [NSURL fileURLWithFileSystemRepresentation:"/Library/Keychains/" isDirectory:YES relativeToURL:nil];
724 #endif
725 verify_create_path([directory fileSystemRepresentation]);
726
727 /* Get the full path of the pinning DB */
728 return [directory URLByAppendingPathComponent:@"pinningrules.sqlite3"];
729 }
730
731 - (void) initializedDb {
732 dispatch_sync(_queue, ^{
733 if (!self->_db) {
734 self->_dbPath = [self pinningDbPath];
735 self->_db = [self createAtPath];
736 }
737 });
738 }
739
740 - (instancetype) init {
741 if (self = [super init]) {
742 _queue = dispatch_queue_create("Pinning DB Queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
743 [self initializedDb];
744 }
745 return self;
746 }
747
748 - (void) dealloc {
749 CFReleaseNull(_db);
750 }
751
752 - (BOOL) isPinningDisabled:(NSString * _Nullable)policy {
753 static dispatch_once_t once;
754 static sec_action_t action;
755
756 BOOL pinningDisabled = NO;
757 if (SecIsInternalRelease()) {
758 NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.security"];
759 pinningDisabled = [defaults boolForKey:@"AppleServerAuthenticationNoPinning"];
760 if (!pinningDisabled && policy) {
761 NSMutableString *policySpecificKey = [NSMutableString stringWithString:@"AppleServerAuthenticationNoPinning"];
762 [policySpecificKey appendString:policy];
763 pinningDisabled = [defaults boolForKey:policySpecificKey];
764 secinfo("pinningQA", "%@ disable pinning = %d", policy, pinningDisabled);
765 }
766 }
767
768 dispatch_once(&once, ^{
769 /* Only log system-wide pinning status once a minute */
770 action = sec_action_create("pinning logging charles", 60.0);
771 sec_action_set_handler(action, ^{
772 if (!SecIsInternalRelease()) {
773 secnotice("pinningQA", "could not disable pinning: not an internal release");
774 } else {
775 NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.security"];
776 secnotice("pinningQA", "generic pinning disable = %d", [defaults boolForKey:@"AppleServerAuthenticationNoPinning"]);
777 }
778 });
779 });
780 sec_action_perform(action);
781
782 return pinningDisabled;
783 }
784
785 - ( NSDictionary * _Nullable ) queryForDomain:(NSString *)domain {
786 if (!_queue) { (void)[self init]; }
787 if (!_db) { [self initializedDb]; }
788
789 /* Check for general no-pinning setting */
790 if ([self isPinningDisabled:nil]) {
791 return nil;
792 }
793
794 /* parse the domain into suffix and 1st label */
795 NSRange firstDot = [domain rangeOfString:@"."];
796 if (firstDot.location == NSNotFound) { return nil; } // Probably not a legitimate domain name
797 __block NSString *firstLabel = [domain substringToIndex:firstDot.location];
798 __block NSString *suffix = [domain substringFromIndex:(firstDot.location + 1)];
799
800 /* Perform SELECT */
801 __block bool ok = true;
802 __block CFErrorRef error = NULL;
803 __block NSMutableArray *resultRules = [NSMutableArray array];
804 __block NSString *resultName = nil;
805 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
806 ok &= SecDbWithSQL(dbconn, selectDomainSQL, &error, ^bool(sqlite3_stmt *selectDomain) {
807 ok &= SecDbBindText(selectDomain, 1, [suffix UTF8String], [suffix length], SQLITE_TRANSIENT, &error);
808 ok &= SecDbStep(dbconn, selectDomain, &error, ^(bool *stop) {
809 /* Match the labelRegex */
810 const uint8_t *regex = sqlite3_column_text(selectDomain, 0);
811 if (!regex) { return; }
812 NSString *regexStr = [NSString stringWithUTF8String:(const char *)regex];
813 if (!regexStr) { return; }
814 NSRegularExpression *regularExpression = [NSRegularExpression regularExpressionWithPattern:regexStr
815 options:NSRegularExpressionCaseInsensitive
816 error:nil];
817 if (!regularExpression) { return; }
818 NSUInteger numMatches = [regularExpression numberOfMatchesInString:firstLabel
819 options:0
820 range:NSMakeRange(0, [firstLabel length])];
821 if (numMatches == 0) {
822 return;
823 }
824 secdebug("SecPinningDb", "found matching rule for %@.%@", firstLabel, suffix);
825
826 /* Check the policyName for no-pinning settings */
827 const uint8_t *policyName = sqlite3_column_text(selectDomain, 1);
828 NSString *policyNameStr = [NSString stringWithUTF8String:(const char *)policyName];
829 if ([self isPinningDisabled:policyNameStr]) {
830 return;
831 }
832
833 /* Deserialize the policies and return.
834 * @@@ Assumes there is only one rule with matching suffix/label pairs. */
835 NSData *xmlPolicies = [NSData dataWithBytes:sqlite3_column_blob(selectDomain, 2) length:sqlite3_column_bytes(selectDomain, 2)];
836 if (!xmlPolicies) { return; }
837 id policies = [NSPropertyListSerialization propertyListWithData:xmlPolicies options:0 format:nil error:nil];
838 if (!isNSArray(policies)) {
839 return;
840 }
841 [resultRules addObjectsFromArray:(NSArray *)policies];
842 resultName = policyNameStr;
843 });
844 return ok;
845 });
846 });
847
848 if (error) {
849 secerror("SecPinningDb: error querying DB for hostname: %@", error);
850 CFReleaseNull(error);
851 }
852
853 if ([resultRules count] > 0) {
854 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
855 (__bridge NSString*)kSecPinningDbKeyPolicyName:resultName};
856 return results;
857 }
858 return nil;
859 }
860
861 - (NSDictionary * _Nullable) queryForPolicyName:(NSString *)policyName {
862 if (!_queue) { (void)[self init]; }
863 if (!_db) { [self initializedDb]; }
864
865 /* Skip the "sslServer" policyName, which is not a pinning policy */
866 if ([policyName isEqualToString:@"sslServer"]) {
867 return nil;
868 }
869
870 /* Check for general no-pinning setting */
871 if ([self isPinningDisabled:nil] || [self isPinningDisabled:policyName]) {
872 return nil;
873 }
874
875 /* Perform SELECT */
876 __block bool ok = true;
877 __block CFErrorRef error = NULL;
878 __block NSMutableArray *resultRules = [NSMutableArray array];
879 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
880 ok &= SecDbWithSQL(dbconn, selectPolicyNameSQL, &error, ^bool(sqlite3_stmt *selectPolicyName) {
881 ok &= SecDbBindText(selectPolicyName, 1, [policyName UTF8String], [policyName length], SQLITE_TRANSIENT, &error);
882 ok &= SecDbStep(dbconn, selectPolicyName, &error, ^(bool *stop) {
883 secdebug("SecPinningDb", "found matching rule for %@ policy", policyName);
884
885 /* Deserialize the policies and return */
886 NSData *xmlPolicies = [NSData dataWithBytes:sqlite3_column_blob(selectPolicyName, 0) length:sqlite3_column_bytes(selectPolicyName, 0)];
887 if (!xmlPolicies) { return; }
888 id policies = [NSPropertyListSerialization propertyListWithData:xmlPolicies options:0 format:nil error:nil];
889 if (!isNSArray(policies)) {
890 return;
891 }
892 [resultRules addObjectsFromArray:(NSArray *)policies];
893 });
894 return ok;
895 });
896 });
897
898 if (error) {
899 secerror("SecPinningDb: error querying DB for policyName: %@", error);
900 CFReleaseNull(error);
901 }
902
903 if ([resultRules count] > 0) {
904 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
905 (__bridge NSString*)kSecPinningDbKeyPolicyName:policyName};
906 return results;
907 }
908 return nil;
909 }
910
911 @end
912
913 static SecPinningDb *pinningDb = nil;
914 void SecPinningDbInitialize(void) {
915 static dispatch_once_t onceToken;
916 dispatch_once(&onceToken, ^{
917 pinningDb = [[SecPinningDb alloc] init];
918 __block CFErrorRef error = NULL;
919 BOOL ok = SecDbPerformRead([pinningDb db], &error, ^(SecDbConnectionRef dbconn) {
920 NSNumber *contentVersion = [pinningDb getContentVersion:dbconn error:&error];
921 NSNumber *schemaVersion = [pinningDb getSchemaVersion:dbconn error:&error];
922 secinfo("pinningDb", "Database Schema: %@ Content: %@", schemaVersion, contentVersion);
923 });
924 if (!ok || error) {
925 secerror("SecPinningDb: unable to initialize db: %@", error);
926 }
927 CFReleaseNull(error);
928 });
929 }
930
931 CFDictionaryRef _Nullable SecPinningDbCopyMatching(CFDictionaryRef query) {
932 SecPinningDbInitialize();
933
934 NSDictionary *nsQuery = (__bridge NSDictionary*)query;
935 NSString *hostname = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyHostname];
936
937 NSDictionary *results = [pinningDb queryForDomain:hostname];
938 if (results) { return CFBridgingRetain(results); }
939 NSString *policyName = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyPolicyName];
940 results = [pinningDb queryForPolicyName:policyName];
941 if (!results) { return nil; }
942 return CFBridgingRetain(results);
943 }