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