]> git.saurik.com Git - apple/security.git/blob - OSX/sec/securityd/SecPinningDb.m
78df478018c1bae061854788ad3465f0504be947
[apple/security.git] / OSX / sec / securityd / SecPinningDb.m
1 /*
2 * Copyright (c) 2016-2018 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 #import <os/lock.h>
34
35 #if !TARGET_OS_BRIDGE
36 #import <MobileAsset/MAAsset.h>
37 #import <MobileAsset/MAAssetQuery.h>
38 #endif
39
40 #if TARGET_OS_OSX
41 #import <MobileAsset/MobileAsset.h>
42 #include <sys/csr.h>
43 #endif
44
45 #import <Security/SecInternalReleasePriv.h>
46
47 #import <securityd/OTATrustUtilities.h>
48 #import <securityd/SecPinningDb.h>
49 #import <securityd/SecTrustLoggingServer.h>
50
51 #include "utilities/debugging.h"
52 #include "utilities/sqlutils.h"
53 #include "utilities/iOSforOSX.h"
54 #include <utilities/SecCFError.h>
55 #include <utilities/SecCFRelease.h>
56 #include <utilities/SecCFWrappers.h>
57 #include <utilities/SecDb.h>
58 #include <utilities/SecFileLocations.h>
59 #include "utilities/sec_action.h"
60
61 #define kSecPinningBasePath "/Library/Keychains/"
62 #define kSecPinningDbFileName "pinningrules.sqlite3"
63
64 const uint64_t PinningDbSchemaVersion = 2;
65 const NSString *PinningDbPolicyNameKey = @"policyName"; /* key for a string value */
66 const NSString *PinningDbDomainsKey = @"domains"; /* key for an array of dictionaries */
67 const NSString *PinningDbPoliciesKey = @"rules"; /* key for an array of dictionaries */
68 const NSString *PinningDbDomainSuffixKey = @"suffix"; /* key for a string */
69 const NSString *PinningDbLabelRegexKey = @"labelRegex"; /* key for a regex string */
70
71 const CFStringRef kSecPinningDbKeyHostname = CFSTR("PinningHostname");
72 const CFStringRef kSecPinningDbKeyPolicyName = CFSTR("PinningPolicyName");
73 const CFStringRef kSecPinningDbKeyRules = CFSTR("PinningRules");
74
75 @interface SecPinningDb : NSObject
76 @property (assign) SecDbRef db;
77 @property dispatch_queue_t queue;
78 @property NSURL *dbPath;
79 @property (assign) os_unfair_lock regexCacheLock;
80 @property NSMutableDictionary *regexCache;
81 - (instancetype) init;
82 - ( NSDictionary * _Nullable ) queryForDomain:(NSString *)domain;
83 - ( NSDictionary * _Nullable ) queryForPolicyName:(NSString *)policyName;
84 @end
85
86 static inline bool isNSNumber(id nsType) {
87 return nsType && [nsType isKindOfClass:[NSNumber class]];
88 }
89
90 static inline bool isNSArray(id nsType) {
91 return nsType && [nsType isKindOfClass:[NSArray class]];
92 }
93
94 static inline bool isNSDictionary(id nsType) {
95 return nsType && [nsType isKindOfClass:[NSDictionary class]];
96 }
97
98 @implementation SecPinningDb
99 #define getSchemaVersionSQL CFSTR("PRAGMA user_version")
100 #define selectVersionSQL CFSTR("SELECT ival FROM admin WHERE key='version'")
101 #define insertAdminSQL CFSTR("INSERT OR REPLACE INTO admin (key,ival,value) VALUES (?,?,?)")
102 #define selectDomainSQL CFSTR("SELECT DISTINCT labelRegex,policyName,policies FROM rules WHERE domainSuffix=?")
103 #define selectPolicyNameSQL CFSTR("SELECT DISTINCT policies FROM rules WHERE policyName=?")
104 #define insertRuleSQL CFSTR("INSERT OR REPLACE INTO rules (policyName,domainSuffix,labelRegex,policies) VALUES (?,?,?,?) ")
105 #define removeAllRulesSQL CFSTR("DELETE FROM rules;")
106
107 - (NSNumber *)getSchemaVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
108 __block bool ok = true;
109 __block NSNumber *version = nil;
110 ok &= SecDbWithSQL(dbconn, getSchemaVersionSQL, error, ^bool(sqlite3_stmt *selectVersion) {
111 ok &= SecDbStep(dbconn, selectVersion, error, ^(bool *stop) {
112 int ival = sqlite3_column_int(selectVersion, 0);
113 version = [NSNumber numberWithInt:ival];
114 });
115 return ok;
116 });
117 return version;
118 }
119
120 - (BOOL)setSchemaVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
121 bool ok = true;
122 NSString *setVersion = [NSString stringWithFormat:@"PRAGMA user_version = %llu", PinningDbSchemaVersion];
123 ok &= SecDbExec(dbconn,
124 (__bridge CFStringRef)setVersion,
125 error);
126 if (!ok) {
127 secerror("SecPinningDb: failed to create admin table: %@", error ? *error : nil);
128 }
129 return ok;
130 }
131
132 - (NSNumber *)getContentVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
133 __block bool ok = true;
134 __block NSNumber *version = nil;
135 ok &= SecDbWithSQL(dbconn, selectVersionSQL, error, ^bool(sqlite3_stmt *selectVersion) {
136 ok &= SecDbStep(dbconn, selectVersion, error, ^(bool *stop) {
137 uint64_t ival = sqlite3_column_int64(selectVersion, 0);
138 version = [NSNumber numberWithUnsignedLongLong:ival];
139 });
140 return ok;
141 });
142 return version;
143 }
144
145 - (BOOL)setContentVersion:(NSNumber *)version dbConnection:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
146 __block BOOL ok = true;
147 ok &= SecDbWithSQL(dbconn, insertAdminSQL, error, ^bool(sqlite3_stmt *insertAdmin) {
148 const char *versionKey = "version";
149 ok &= SecDbBindText(insertAdmin, 1, versionKey, strlen(versionKey), SQLITE_TRANSIENT, error);
150 ok &= SecDbBindInt64(insertAdmin, 2, [version unsignedLongLongValue], error);
151 ok &= SecDbStep(dbconn, insertAdmin, error, NULL);
152 return ok;
153 });
154 if (!ok) {
155 secerror("SecPinningDb: failed to set version %@ from pinning list: %@", version, error ? *error : nil);
156 }
157 return ok;
158 }
159
160 - (BOOL) shouldUpdateContent:(NSNumber *)new_version {
161 __block CFErrorRef error = NULL;
162 __block BOOL ok = YES;
163 __block BOOL newer = NO;
164 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
165 NSNumber *db_version = [self getContentVersion:dbconn error:&error];
166 if (!db_version || [new_version compare:db_version] == NSOrderedDescending) {
167 newer = YES;
168 secnotice("pinningDb", "Pinning database should update from version %@ to version %@", db_version, new_version);
169 }
170 });
171
172 if (!ok || error) {
173 secerror("SecPinningDb: error reading content version from database %@", error);
174 }
175 CFReleaseNull(error);
176 return newer;
177 }
178
179 - (BOOL) insertRuleWithName:(NSString *)policyName
180 domainSuffix:(NSString *)domainSuffix
181 labelRegex:(NSString *)labelRegex
182 policies:(NSArray *)policies
183 dbConnection:(SecDbConnectionRef)dbconn
184 error:(CFErrorRef *)error{
185 /* @@@ This insertion mechanism assumes that the input is trusted -- namely, that the new rules
186 * are allowed to replace existing rules. For third-party inputs, this assumption isn't true. */
187
188 secdebug("pinningDb", "inserting new rule: %@ for %@.%@", policyName, labelRegex, domainSuffix);
189
190 __block bool ok = true;
191 ok &= SecDbWithSQL(dbconn, insertRuleSQL, error, ^bool(sqlite3_stmt *insertRule) {
192 ok &= SecDbBindText(insertRule, 1, [policyName UTF8String], [policyName length], SQLITE_TRANSIENT, error);
193 ok &= SecDbBindText(insertRule, 2, [domainSuffix UTF8String], [domainSuffix length], SQLITE_TRANSIENT, error);
194 ok &= SecDbBindText(insertRule, 3, [labelRegex UTF8String], [labelRegex length], SQLITE_TRANSIENT, error);
195 NSData *xmlPolicies = [NSPropertyListSerialization dataWithPropertyList:policies
196 format:NSPropertyListXMLFormat_v1_0
197 options:0
198 error:nil];
199 if (!xmlPolicies) {
200 secerror("SecPinningDb: failed to serialize policies");
201 ok = false;
202 }
203 ok &= SecDbBindBlob(insertRule, 4, [xmlPolicies bytes], [xmlPolicies length], SQLITE_TRANSIENT, error);
204 ok &= SecDbStep(dbconn, insertRule, error, NULL);
205 return ok;
206 });
207 if (!ok) {
208 secerror("SecPinningDb: failed to insert rule %@ for %@.%@ with error %@", policyName, labelRegex, domainSuffix, error ? *error : nil);
209 }
210 return ok;
211 }
212
213 - (BOOL) populateDbFromBundle:(NSArray *)pinningList dbConnection:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
214 __block BOOL ok = true;
215 [pinningList enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
216 if (idx ==0) { return; } // Skip the first value which is the version
217 if (!isNSDictionary(obj)) {
218 secerror("SecPinningDb: rule entry in pinning plist is wrong class");
219 ok = false;
220 return;
221 }
222 NSDictionary *rule = obj;
223 __block NSString *policyName = [rule objectForKey:PinningDbPolicyNameKey];
224 NSArray *domains = [rule objectForKey:PinningDbDomainsKey];
225 __block NSArray *policies = [rule objectForKey:PinningDbPoliciesKey];
226
227 if (!policyName || !domains || !policies) {
228 secerror("SecPinningDb: failed to get required fields from rule entry %lu", (unsigned long)idx);
229 ok = false;
230 return;
231 }
232
233 [domains enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
234 if (!isNSDictionary(obj)) {
235 secerror("SecPinningDb: domain entry %lu for %@ in pinning rule is wrong class", (unsigned long)idx, policyName);
236 ok = false;
237 return;
238 }
239 NSDictionary *domain = obj;
240 NSString *suffix = [domain objectForKey:PinningDbDomainSuffixKey];
241 NSString *labelRegex = [domain objectForKey:PinningDbLabelRegexKey];
242
243 if (!suffix || !labelRegex) {
244 secerror("SecPinningDb: failed to get required fields for entry %lu for %@", (unsigned long)idx, policyName);
245 ok = false;
246 return;
247 }
248 ok &= [self insertRuleWithName:policyName domainSuffix:suffix labelRegex:labelRegex policies:policies
249 dbConnection:dbconn error:error];
250 }];
251 }];
252 if (!ok) {
253 secerror("SecPinningDb: failed to populate DB from pinning list: %@", error ? *error : nil);
254 }
255 return ok;
256 }
257
258 - (BOOL) removeAllRulesFromDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
259 __block BOOL ok = true;
260 ok &= SecDbWithSQL(dbconn, removeAllRulesSQL, error, ^bool(sqlite3_stmt *deleteRules) {
261 ok &= SecDbStep(dbconn, deleteRules, error, NULL);
262 return ok;
263 });
264 if (!ok) {
265 secerror("SecPinningDb: failed to delete old values: %@", error ? *error :nil);
266 }
267 return ok;
268 }
269
270
271 - (BOOL) createOrAlterAdminTable:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
272 bool ok = true;
273 ok &= SecDbExec(dbconn,
274 CFSTR("CREATE TABLE IF NOT EXISTS admin("
275 "key TEXT PRIMARY KEY NOT NULL,"
276 "ival INTEGER NOT NULL,"
277 "value BLOB"
278 ");"),
279 error);
280 if (!ok) {
281 secerror("SecPinningDb: failed to create admin table: %@", error ? *error : nil);
282 }
283 return ok;
284 }
285
286 - (BOOL) createOrAlterRulesTable:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
287 bool ok = true;
288 ok &= SecDbExec(dbconn,
289 CFSTR("CREATE TABLE IF NOT EXISTS rules("
290 "policyName TEXT NOT NULL,"
291 "domainSuffix TEXT NOT NULL,"
292 "labelRegex TEXT NOT NULL,"
293 "policies BLOB NOT NULL,"
294 "UNIQUE(policyName, domainSuffix, labelRegex)"
295 ");"),
296 error);
297 ok &= SecDbExec(dbconn, CFSTR("CREATE INDEX IF NOT EXISTS idomain ON rules(domainSuffix);"), error);
298 ok &= SecDbExec(dbconn, CFSTR("CREATE INDEX IF NOT EXISTS ipolicy ON rules(policyName);"), error);
299 if (!ok) {
300 secerror("SecPinningDb: failed to create rules table: %@", error ? *error : nil);
301 }
302 return ok;
303 }
304
305 #if !TARGET_OS_BRIDGE
306 - (BOOL) installDbFromURL:(NSURL *)localURL {
307 if (!localURL) {
308 secerror("SecPinningDb: missing url for downloaded asset");
309 return NO;
310 }
311 NSURL *fileLoc = [NSURL URLWithString:@"CertificatePinning.plist"
312 relativeToURL:localURL];
313 __block NSArray *pinningList = [NSArray arrayWithContentsOfURL:fileLoc];
314 if (!pinningList) {
315 secerror("SecPinningDb: unable to create pinning list from asset file: %@", fileLoc);
316 return NO;
317 }
318
319 NSNumber *plist_version = [pinningList objectAtIndex:0];
320 if (![self shouldUpdateContent:plist_version]) {
321 /* We got a new plist but we already have that version installed. */
322 return YES;
323 }
324
325 /* Update Content */
326 __block CFErrorRef error = NULL;
327 __block BOOL ok = YES;
328 dispatch_sync(self->_queue, ^{
329 ok &= SecDbPerformWrite(self->_db, &error, ^(SecDbConnectionRef dbconn) {
330 ok &= [self updateDb:dbconn error:&error pinningList:pinningList updateSchema:NO updateContent:YES];
331 });
332 /* We changed the database, so clear the database cache */
333 [self clearCache];
334 });
335
336 if (!ok || error) {
337 secerror("SecPinningDb: error installing updated pinning list version %@: %@", [pinningList objectAtIndex:0], error);
338 #if ENABLE_TRUSTD_ANALYTICS
339 [[TrustdHealthAnalytics logger] logHardError:(__bridge NSError *)error
340 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
341 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
342 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationWrite) }];
343 #endif // ENABLE_TRUSTD_ANALYTICS
344 CFReleaseNull(error);
345 }
346
347 return ok;
348 }
349 #endif /* !TARGET_OS_BRIDGE */
350
351 - (NSArray *) copySystemPinningList {
352 NSArray *pinningList = nil;
353 NSURL *pinningListURL = nil;
354 /* Get the pinning list shipped with the OS */
355 SecOTAPKIRef otapkiref = SecOTAPKICopyCurrentOTAPKIRef();
356 if (otapkiref) {
357 pinningListURL = CFBridgingRelease(SecOTAPKICopyPinningList(otapkiref));
358 CFReleaseNull(otapkiref);
359 if (!pinningListURL) {
360 secerror("SecPinningDb: failed to get pinning plist URL");
361 }
362 NSError *error = nil;
363 pinningList = [NSArray arrayWithContentsOfURL:pinningListURL error:&error];
364 if (!pinningList) {
365 secerror("SecPinningDb: failed to read pinning plist from bundle: %@", error);
366 }
367 }
368
369 return pinningList;
370 }
371
372 - (BOOL) updateDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error pinningList:(NSArray *)pinningList
373 updateSchema:(BOOL)updateSchema updateContent:(BOOL)updateContent
374 {
375 if (!SecOTAPKIIsSystemTrustd()) { return false; }
376 secdebug("pinningDb", "updating or creating database");
377
378 __block bool ok = true;
379 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, error, ^(bool *commit) {
380 if (updateSchema) {
381 /* update the tables */
382 ok &= [self createOrAlterAdminTable:dbconn error:error];
383 ok &= [self createOrAlterRulesTable:dbconn error:error];
384 ok &= [self setSchemaVersion:dbconn error:error];
385 }
386
387 if (updateContent) {
388 /* remove the old data */
389 /* @@@ This behavior assumes that we have all the rules we want to populate
390 * elsewhere on disk and that the DB doesn't contain the sole copy of that data. */
391 ok &= [self removeAllRulesFromDb:dbconn error:error];
392
393 /* read the new data */
394 NSNumber *version = [pinningList objectAtIndex:0];
395
396 /* populate the tables */
397 ok &= [self populateDbFromBundle:pinningList dbConnection:dbconn error:error];
398 ok &= [self setContentVersion:version dbConnection:dbconn error:error];
399 }
400
401 *commit = ok;
402 });
403
404 return ok;
405 }
406
407 - (SecDbRef) createAtPath {
408 bool readWrite = SecOTAPKIIsSystemTrustd();
409 #if TARGET_OS_OSX
410 mode_t mode = 0644; // Root trustd can rw. All other trustds need to read.
411 #else
412 mode_t mode = 0600; // Only one trustd.
413 #endif
414
415 CFStringRef path = CFStringCreateWithCString(NULL, [_dbPath fileSystemRepresentation], kCFStringEncodingUTF8);
416 SecDbRef result = SecDbCreate(path, mode, readWrite, readWrite, false, false, 1,
417 ^bool (SecDbRef db, SecDbConnectionRef dbconn, bool didCreate, bool *callMeAgainForNextConnection, CFErrorRef *error) {
418 if (!SecOTAPKIIsSystemTrustd()) {
419 /* Non-owner process can't update the db, but it should get a db connection.
420 * @@@ Revisit if new schema version is needed by reader processes. */
421 return true;
422 }
423
424 __block BOOL ok = true;
425 dispatch_sync(self->_queue, ^{
426 bool updateSchema = false;
427 bool updateContent = false;
428
429 /* Get the pinning plist */
430 NSArray *pinningList = [self copySystemPinningList];
431 if (!pinningList) {
432 secerror("SecPinningDb: failed to find pinning plist in bundle");
433 ok = false;
434 return;
435 }
436
437 /* Check latest data and schema versions against existing table. */
438 if (!isNSNumber([pinningList objectAtIndex:0])) {
439 secerror("SecPinningDb: pinning plist in wrong format");
440 return; // Don't change status. We can continue to use old DB.
441 }
442 NSNumber *plist_version = [pinningList objectAtIndex:0];
443 NSNumber *db_version = [self getContentVersion:dbconn error:error];
444 secnotice("pinningDb", "Opening db with version %@", db_version);
445 if (!db_version || [plist_version compare:db_version] == NSOrderedDescending) {
446 secnotice("pinningDb", "Updating pinning database content from version %@ to version %@",
447 db_version ? db_version : 0, plist_version);
448 updateContent = true;
449 }
450 NSNumber *schema_version = [self getSchemaVersion:dbconn error:error];
451 NSNumber *current_version = [NSNumber numberWithUnsignedLongLong:PinningDbSchemaVersion];
452 if (!schema_version || ![schema_version isEqualToNumber:current_version]) {
453 secnotice("pinningDb", "Updating pinning database schema from version %@ to version %@",
454 schema_version, current_version);
455 updateSchema = true;
456 }
457
458 if (updateContent || updateSchema) {
459 ok &= [self updateDb:dbconn error:error pinningList:pinningList updateSchema:updateSchema updateContent:updateContent];
460 /* Since we updated the DB to match the list that shipped with the system,
461 * reset the OTAPKI Asset version to the system asset version */
462 (void)SecOTAPKIResetCurrentAssetVersion(NULL);
463 }
464 if (!ok) {
465 secerror("SecPinningDb: %s failed: %@", didCreate ? "Create" : "Open", error ? *error : NULL);
466 #if ENABLE_TRUSTD_ANALYTICS
467 [[TrustdHealthAnalytics logger] logHardError:(error ? (__bridge NSError *)*error : nil)
468 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
469 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
470 TrustdHealthAnalyticsAttributeDatabaseOperation : didCreate ? @(TAOperationCreate) : @(TAOperationOpen)}];
471 #endif // ENABLE_TRUSTD_ANALYTICS
472 }
473 });
474 return ok;
475 });
476
477 CFReleaseNull(path);
478 return result;
479 }
480
481 static void verify_create_path(const char *path)
482 {
483 int ret = mkpath_np(path, 0755);
484 if (!(ret == 0 || ret == EEXIST)) {
485 secerror("could not create path: %s (%s)", path, strerror(ret));
486 }
487 }
488
489 - (NSURL *)pinningDbPath {
490 /* Make sure the /Library/Keychains directory is there */
491 #if TARGET_OS_IPHONE
492 NSURL *directory = CFBridgingRelease(SecCopyURLForFileInKeychainDirectory(nil));
493 #else
494 NSURL *directory = [NSURL fileURLWithFileSystemRepresentation:"/Library/Keychains/" isDirectory:YES relativeToURL:nil];
495 #endif
496 verify_create_path([directory fileSystemRepresentation]);
497
498 /* Get the full path of the pinning DB */
499 return [directory URLByAppendingPathComponent:@"pinningrules.sqlite3"];
500 }
501
502 - (void) initializedDb {
503 dispatch_sync(_queue, ^{
504 if (!self->_db) {
505 self->_dbPath = [self pinningDbPath];
506 self->_db = [self createAtPath];
507 }
508 });
509 }
510
511 - (instancetype) init {
512 if (self = [super init]) {
513 _queue = dispatch_queue_create("Pinning DB Queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
514 _regexCache = [NSMutableDictionary dictionary];
515 _regexCacheLock = OS_UNFAIR_LOCK_INIT;
516 [self initializedDb];
517 }
518 return self;
519 }
520
521 - (void) dealloc {
522 CFReleaseNull(_db);
523 }
524
525 /* MARK: DB Cache
526 * The cache is represented a dictionary defined as { suffix : { regex : resultsDictionary } } */
527 - (void) clearCache {
528 os_unfair_lock_lock(&_regexCacheLock);
529 self.regexCache = [NSMutableDictionary dictionary];
530 os_unfair_lock_unlock(&_regexCacheLock);
531 }
532
533 - (void) addSuffixToCache:(NSString *)suffix entry:(NSDictionary <NSRegularExpression *, NSDictionary *> *)entry {
534 os_unfair_lock_lock(&_regexCacheLock);
535 secinfo("SecPinningDb", "adding %llu entries for %@ to cache", (unsigned long long)[entry count], suffix);
536 self.regexCache[suffix] = entry;
537 os_unfair_lock_unlock(&_regexCacheLock);
538 }
539
540 /* Because we iterate over all DB entries for a suffix, even if we find a match, we guarantee
541 * that the cache, if the cache has an entry for a suffix, it has all the entries for that suffix */
542 - (BOOL) queryCacheForSuffix:(NSString *)suffix firstLabel:(NSString *)firstLabel results:(NSDictionary * __autoreleasing *)results{
543 __block BOOL foundSuffix = NO;
544 os_unfair_lock_lock(&_regexCacheLock);
545 NSDictionary <NSRegularExpression *, NSDictionary *> *cacheEntry;
546 if (NULL != (cacheEntry = self.regexCache[suffix])) {
547 foundSuffix = YES;
548 for (NSRegularExpression *regex in cacheEntry) {
549 NSUInteger numMatches = [regex numberOfMatchesInString:firstLabel
550 options:0
551 range:NSMakeRange(0, [firstLabel length])];
552 if (numMatches == 0) {
553 continue;
554 }
555 secinfo("SecPinningDb", "found matching rule in cache for %@.%@", firstLabel, suffix);
556 NSDictionary *resultDictionary = [cacheEntry objectForKey:regex];
557
558 /* Check the policyName for no-pinning settings */
559 if ([self isPinningDisabled:resultDictionary[(__bridge NSString *)kSecPinningDbKeyPolicyName]]) {
560 continue;
561 }
562
563 /* Return the pinning rules */
564 if (results) {
565 *results = resultDictionary;
566 }
567 }
568 }
569 os_unfair_lock_unlock(&_regexCacheLock);
570
571 return foundSuffix;
572 }
573
574 - (BOOL) isPinningDisabled:(NSString * _Nullable)policy {
575 static dispatch_once_t once;
576 static sec_action_t action;
577
578 BOOL pinningDisabled = NO;
579 if (SecIsInternalRelease()) {
580 NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.security"];
581 pinningDisabled = [defaults boolForKey:@"AppleServerAuthenticationNoPinning"];
582 if (!pinningDisabled && policy) {
583 NSMutableString *policySpecificKey = [NSMutableString stringWithString:@"AppleServerAuthenticationNoPinning"];
584 [policySpecificKey appendString:policy];
585 pinningDisabled = [defaults boolForKey:policySpecificKey];
586 secinfo("pinningQA", "%@ disable pinning = %d", policy, pinningDisabled);
587 }
588 }
589
590
591 dispatch_once(&once, ^{
592 /* Only log system-wide pinning status once every five minutes */
593 action = sec_action_create("pinning logging charles", 5*60.0);
594 sec_action_set_handler(action, ^{
595 if (!SecIsInternalRelease()) {
596 secnotice("pinningQA", "could not disable pinning: not an internal release");
597 } else {
598 NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.security"];
599 secnotice("pinningQA", "generic pinning disable = %d", [defaults boolForKey:@"AppleServerAuthenticationNoPinning"]);
600 }
601 });
602 });
603 sec_action_perform(action);
604
605 return pinningDisabled;
606 }
607
608 - (NSDictionary * _Nullable) queryForDomain:(NSString *)domain {
609 if (!_queue) { (void)[self init]; }
610 if (!_db) { [self initializedDb]; }
611
612 /* Check for general no-pinning setting */
613 if ([self isPinningDisabled:nil]) {
614 return nil;
615 }
616
617 /* parse the domain into suffix and 1st label */
618 NSRange firstDot = [domain rangeOfString:@"."];
619 if (firstDot.location == NSNotFound) { return nil; } // Probably not a legitimate domain name
620 __block NSString *firstLabel = [domain substringToIndex:firstDot.location];
621 __block NSString *suffix = [domain substringFromIndex:(firstDot.location + 1)];
622
623 /* Search cache */
624 NSDictionary *cacheResult = nil;
625 if ([self queryCacheForSuffix:suffix firstLabel:firstLabel results:&cacheResult]) {
626 return cacheResult;
627 }
628
629 /* Cache miss. Perform SELECT */
630 __block bool ok = true;
631 __block CFErrorRef error = NULL;
632 __block NSMutableArray *resultRules = [NSMutableArray array];
633 __block NSString *resultName = nil;
634 __block NSMutableDictionary <NSRegularExpression *, NSDictionary *> *newCacheEntry = [NSMutableDictionary dictionary];
635 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
636 ok &= SecDbWithSQL(dbconn, selectDomainSQL, &error, ^bool(sqlite3_stmt *selectDomain) {
637 ok &= SecDbBindText(selectDomain, 1, [suffix UTF8String], [suffix length], SQLITE_TRANSIENT, &error);
638 ok &= SecDbStep(dbconn, selectDomain, &error, ^(bool *stop) {
639 @autoreleasepool {
640 /* Get the data from the entry */
641 // First Label Regex
642 const uint8_t *regex = sqlite3_column_text(selectDomain, 0);
643 verify_action(regex, return);
644 NSString *regexStr = [NSString stringWithUTF8String:(const char *)regex];
645 verify_action(regexStr, return);
646 NSRegularExpression *regularExpression = [NSRegularExpression regularExpressionWithPattern:regexStr
647 options:NSRegularExpressionCaseInsensitive
648 error:nil];
649 verify_action(regularExpression, return);
650 // Policy name
651 const uint8_t *policyName = sqlite3_column_text(selectDomain, 1);
652 NSString *policyNameStr = [NSString stringWithUTF8String:(const char *)policyName];
653 // Policies
654 NSData *xmlPolicies = [NSData dataWithBytes:sqlite3_column_blob(selectDomain, 2) length:sqlite3_column_bytes(selectDomain, 2)];
655 verify_action(xmlPolicies, return);
656 id policies = [NSPropertyListSerialization propertyListWithData:xmlPolicies options:0 format:nil error:nil];
657 verify_action(isNSArray(policies), return);
658
659 /* Add to cache entry */
660 [newCacheEntry setObject:@{(__bridge NSString*)kSecPinningDbKeyPolicyName:policyNameStr,
661 (__bridge NSString*)kSecPinningDbKeyRules:policies}
662 forKey:regularExpression];
663
664 /* Match the labelRegex */
665 NSUInteger numMatches = [regularExpression numberOfMatchesInString:firstLabel
666 options:0
667 range:NSMakeRange(0, [firstLabel length])];
668 if (numMatches == 0) {
669 return;
670 }
671 secinfo("SecPinningDb", "found matching rule in DB for %@.%@", firstLabel, suffix);
672
673 /* Check the policyName for no-pinning settings */
674 if ([self isPinningDisabled:policyNameStr]) {
675 return;
676 }
677
678 /* Add return data
679 * @@@ Assumes there is only one rule with matching suffix/label pairs. */
680 [resultRules addObjectsFromArray:(NSArray *)policies];
681 resultName = policyNameStr;
682 }
683 });
684 return ok;
685 });
686 });
687
688 if (!ok || error) {
689 secerror("SecPinningDb: error querying DB for hostname: %@", error);
690 #if ENABLE_TRUSTD_ANALYTICS
691 [[TrustdHealthAnalytics logger] logHardError:(__bridge NSError *)error
692 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
693 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
694 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationRead)}];
695 #endif // ENABLE_TRUSTD_ANALYTICS
696 CFReleaseNull(error);
697 }
698
699 /* Add new cache entry to cache. */
700 if ([newCacheEntry count] > 0) {
701 [self addSuffixToCache:suffix entry:newCacheEntry];
702 }
703
704 /* Return results if found */
705 if ([resultRules count] > 0) {
706 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
707 (__bridge NSString*)kSecPinningDbKeyPolicyName:resultName};
708 return results;
709 }
710 return nil;
711 }
712
713 - (NSDictionary * _Nullable) queryForPolicyName:(NSString *)policyName {
714 if (!_queue) { (void)[self init]; }
715 if (!_db) { [self initializedDb]; }
716
717 /* Skip the "sslServer" policyName, which is not a pinning policy */
718 if ([policyName isEqualToString:@"sslServer"]) {
719 return nil;
720 }
721
722 /* Check for general no-pinning setting */
723 if ([self isPinningDisabled:nil] || [self isPinningDisabled:policyName]) {
724 return nil;
725 }
726
727 secinfo("SecPinningDb", "Fetching rules for policy named %@", policyName);
728
729 /* Perform SELECT */
730 __block bool ok = true;
731 __block CFErrorRef error = NULL;
732 __block NSMutableArray *resultRules = [NSMutableArray array];
733 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
734 ok &= SecDbWithSQL(dbconn, selectPolicyNameSQL, &error, ^bool(sqlite3_stmt *selectPolicyName) {
735 ok &= SecDbBindText(selectPolicyName, 1, [policyName UTF8String], [policyName length], SQLITE_TRANSIENT, &error);
736 ok &= SecDbStep(dbconn, selectPolicyName, &error, ^(bool *stop) {
737 @autoreleasepool {
738 secinfo("SecPinningDb", "found matching rule for %@ policy", policyName);
739
740 /* Deserialize the policies and return */
741 NSData *xmlPolicies = [NSData dataWithBytes:sqlite3_column_blob(selectPolicyName, 0) length:sqlite3_column_bytes(selectPolicyName, 0)];
742 if (!xmlPolicies) { return; }
743 id policies = [NSPropertyListSerialization propertyListWithData:xmlPolicies options:0 format:nil error:nil];
744 if (!isNSArray(policies)) {
745 return;
746 }
747 [resultRules addObjectsFromArray:(NSArray *)policies];
748 }
749 });
750 return ok;
751 });
752 });
753
754 if (!ok || error) {
755 secerror("SecPinningDb: error querying DB for policyName: %@", error);
756 #if ENABLE_TRUSTD_ANALYTICS
757 [[TrustdHealthAnalytics logger] logHardError:(__bridge NSError *)error
758 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
759 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
760 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationRead)}];
761 #endif // ENABLE_TRUSTD_ANALYTICS
762 CFReleaseNull(error);
763 }
764
765 if ([resultRules count] > 0) {
766 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
767 (__bridge NSString*)kSecPinningDbKeyPolicyName:policyName};
768 return results;
769 }
770 return nil;
771 }
772
773 @end
774
775 /* C interfaces */
776 static SecPinningDb *pinningDb = nil;
777 void SecPinningDbInitialize(void) {
778 /* Create the pinning object once per launch */
779 static dispatch_once_t onceToken;
780 dispatch_once(&onceToken, ^{
781 @autoreleasepool {
782 pinningDb = [[SecPinningDb alloc] init];
783 __block CFErrorRef error = NULL;
784 BOOL ok = SecDbPerformRead([pinningDb db], &error, ^(SecDbConnectionRef dbconn) {
785 NSNumber *contentVersion = [pinningDb getContentVersion:dbconn error:&error];
786 NSNumber *schemaVersion = [pinningDb getSchemaVersion:dbconn error:&error];
787 secinfo("pinningDb", "Database Schema: %@ Content: %@", schemaVersion, contentVersion);
788 });
789 if (!ok || error) {
790 secerror("SecPinningDb: unable to initialize db: %@", error);
791 #if ENABLE_TRUSTD_ANALYTICS
792 [[TrustdHealthAnalytics logger] logHardError:(__bridge NSError *)error
793 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
794 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
795 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationRead)}];
796 #endif // ENABLE_TRUSTD_ANALYTICS
797 }
798 CFReleaseNull(error);
799 }
800 });
801 }
802
803 CFDictionaryRef _Nullable SecPinningDbCopyMatching(CFDictionaryRef query) {
804 @autoreleasepool {
805 SecPinningDbInitialize();
806
807 NSDictionary *nsQuery = (__bridge NSDictionary*)query;
808 NSString *hostname = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyHostname];
809
810 NSDictionary *results = [pinningDb queryForDomain:hostname];
811 if (results) { return CFBridgingRetain(results); }
812 NSString *policyName = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyPolicyName];
813 results = [pinningDb queryForPolicyName:policyName];
814 if (!results) { return nil; }
815 return CFBridgingRetain(results);
816 }
817 }
818
819 #if !TARGET_OS_BRIDGE
820 bool SecPinningDbUpdateFromURL(CFURLRef url) {
821 SecPinningDbInitialize();
822
823 return [pinningDb installDbFromURL:(__bridge NSURL*)url];
824 }
825 #endif
826
827 CFNumberRef SecPinningDbCopyContentVersion(void) {
828 @autoreleasepool {
829 __block CFErrorRef error = NULL;
830 __block NSNumber *contentVersion = nil;
831 BOOL ok = SecDbPerformRead([pinningDb db], &error, ^(SecDbConnectionRef dbconn) {
832 contentVersion = [pinningDb getContentVersion:dbconn error:&error];
833 });
834 if (!ok || error) {
835 secerror("SecPinningDb: unable to get content version: %@", error);
836 }
837 CFReleaseNull(error);
838 if (!contentVersion) {
839 contentVersion = [NSNumber numberWithInteger:0];
840 }
841 return CFBridgingRetain(contentVersion);
842 }
843 }