]> git.saurik.com Git - apple/security.git/blob - OSX/sec/securityd/SecPinningDb.m
Security-58286.220.15.tar.gz
[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 error:(NSError **)nserror {
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 if (nserror && error) { *nserror = CFBridgingRelease(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 error:(NSError **)nserror {
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 error:nserror];
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 error:nserror]) {
321 /* Something went wrong reading the DB in order to determine whether this version is new. */
322 if (nserror && *nserror) {
323 return NO;
324 }
325 /* We got a new plist but we already have that version installed. */
326 return YES;
327 }
328
329 /* Update Content */
330 __block CFErrorRef error = NULL;
331 __block BOOL ok = YES;
332 dispatch_sync(self->_queue, ^{
333 ok &= SecDbPerformWrite(self->_db, &error, ^(SecDbConnectionRef dbconn) {
334 ok &= [self updateDb:dbconn error:&error pinningList:pinningList updateSchema:NO updateContent:YES];
335 });
336 /* We changed the database, so clear the database cache */
337 [self clearCache];
338 });
339
340 if (!ok || error) {
341 secerror("SecPinningDb: error installing updated pinning list version %@: %@", [pinningList objectAtIndex:0], error);
342 #if ENABLE_TRUSTD_ANALYTICS
343 [[TrustdHealthAnalytics logger] logHardError:(__bridge NSError *)error
344 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
345 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
346 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationWrite) }];
347 #endif // ENABLE_TRUSTD_ANALYTICS
348 if (nserror && error) { *nserror = CFBridgingRelease(error); }
349 }
350
351 return ok;
352 }
353 #endif /* !TARGET_OS_BRIDGE */
354
355 - (NSArray *) copySystemPinningList {
356 NSArray *pinningList = nil;
357 NSURL *pinningListURL = nil;
358 /* Get the pinning list shipped with the OS */
359 SecOTAPKIRef otapkiref = SecOTAPKICopyCurrentOTAPKIRef();
360 if (otapkiref) {
361 pinningListURL = CFBridgingRelease(SecOTAPKICopyPinningList(otapkiref));
362 CFReleaseNull(otapkiref);
363 if (!pinningListURL) {
364 secerror("SecPinningDb: failed to get pinning plist URL");
365 }
366 NSError *error = nil;
367 pinningList = [NSArray arrayWithContentsOfURL:pinningListURL error:&error];
368 if (!pinningList) {
369 secerror("SecPinningDb: failed to read pinning plist from bundle: %@", error);
370 }
371 }
372
373 return pinningList;
374 }
375
376 - (BOOL) updateDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error pinningList:(NSArray *)pinningList
377 updateSchema:(BOOL)updateSchema updateContent:(BOOL)updateContent
378 {
379 if (!SecOTAPKIIsSystemTrustd()) { return false; }
380 secdebug("pinningDb", "updating or creating database");
381
382 __block bool ok = true;
383 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, error, ^(bool *commit) {
384 if (updateSchema) {
385 /* update the tables */
386 ok &= [self createOrAlterAdminTable:dbconn error:error];
387 ok &= [self createOrAlterRulesTable:dbconn error:error];
388 ok &= [self setSchemaVersion:dbconn error:error];
389 }
390
391 if (updateContent) {
392 /* remove the old data */
393 /* @@@ This behavior assumes that we have all the rules we want to populate
394 * elsewhere on disk and that the DB doesn't contain the sole copy of that data. */
395 ok &= [self removeAllRulesFromDb:dbconn error:error];
396
397 /* read the new data */
398 NSNumber *version = [pinningList objectAtIndex:0];
399
400 /* populate the tables */
401 ok &= [self populateDbFromBundle:pinningList dbConnection:dbconn error:error];
402 ok &= [self setContentVersion:version dbConnection:dbconn error:error];
403 }
404
405 *commit = ok;
406 });
407
408 return ok;
409 }
410
411 - (SecDbRef) createAtPath {
412 bool readWrite = SecOTAPKIIsSystemTrustd();
413 #if TARGET_OS_OSX
414 mode_t mode = 0644; // Root trustd can rw. All other trustds need to read.
415 #else
416 mode_t mode = 0600; // Only one trustd.
417 #endif
418
419 CFStringRef path = CFStringCreateWithCString(NULL, [_dbPath fileSystemRepresentation], kCFStringEncodingUTF8);
420 SecDbRef result = SecDbCreate(path, mode, readWrite, readWrite, false, false, 1,
421 ^bool (SecDbRef db, SecDbConnectionRef dbconn, bool didCreate, bool *callMeAgainForNextConnection, CFErrorRef *error) {
422 if (!SecOTAPKIIsSystemTrustd()) {
423 /* Non-owner process can't update the db, but it should get a db connection.
424 * @@@ Revisit if new schema version is needed by reader processes. */
425 return true;
426 }
427
428 __block BOOL ok = true;
429 dispatch_sync(self->_queue, ^{
430 bool updateSchema = false;
431 bool updateContent = false;
432
433 /* Get the pinning plist */
434 NSArray *pinningList = [self copySystemPinningList];
435 if (!pinningList) {
436 secerror("SecPinningDb: failed to find pinning plist in bundle");
437 ok = false;
438 return;
439 }
440
441 /* Check latest data and schema versions against existing table. */
442 if (!isNSNumber([pinningList objectAtIndex:0])) {
443 secerror("SecPinningDb: pinning plist in wrong format");
444 return; // Don't change status. We can continue to use old DB.
445 }
446 NSNumber *plist_version = [pinningList objectAtIndex:0];
447 NSNumber *db_version = [self getContentVersion:dbconn error:error];
448 secnotice("pinningDb", "Opening db with version %@", db_version);
449 if (!db_version || [plist_version compare:db_version] == NSOrderedDescending) {
450 secnotice("pinningDb", "Updating pinning database content from version %@ to version %@",
451 db_version ? db_version : 0, plist_version);
452 updateContent = true;
453 }
454 NSNumber *schema_version = [self getSchemaVersion:dbconn error:error];
455 NSNumber *current_version = [NSNumber numberWithUnsignedLongLong:PinningDbSchemaVersion];
456 if (!schema_version || ![schema_version isEqualToNumber:current_version]) {
457 secnotice("pinningDb", "Updating pinning database schema from version %@ to version %@",
458 schema_version, current_version);
459 updateSchema = true;
460 }
461
462 if (updateContent || updateSchema) {
463 ok &= [self updateDb:dbconn error:error pinningList:pinningList updateSchema:updateSchema updateContent:updateContent];
464 /* Since we updated the DB to match the list that shipped with the system,
465 * reset the OTAPKI Asset version to the system asset version */
466 (void)SecOTAPKIResetCurrentAssetVersion(NULL);
467 }
468 if (!ok) {
469 secerror("SecPinningDb: %s failed: %@", didCreate ? "Create" : "Open", error ? *error : NULL);
470 #if ENABLE_TRUSTD_ANALYTICS
471 [[TrustdHealthAnalytics logger] logHardError:(error ? (__bridge NSError *)*error : nil)
472 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
473 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
474 TrustdHealthAnalyticsAttributeDatabaseOperation : didCreate ? @(TAOperationCreate) : @(TAOperationOpen)}];
475 #endif // ENABLE_TRUSTD_ANALYTICS
476 }
477 });
478 return ok;
479 });
480
481 CFReleaseNull(path);
482 return result;
483 }
484
485 static void verify_create_path(const char *path)
486 {
487 int ret = mkpath_np(path, 0755);
488 if (!(ret == 0 || ret == EEXIST)) {
489 secerror("could not create path: %s (%s)", path, strerror(ret));
490 }
491 }
492
493 - (NSURL *)pinningDbPath {
494 /* Make sure the /Library/Keychains directory is there */
495 #if TARGET_OS_IPHONE
496 NSURL *directory = CFBridgingRelease(SecCopyURLForFileInKeychainDirectory(nil));
497 #else
498 NSURL *directory = [NSURL fileURLWithFileSystemRepresentation:"/Library/Keychains/" isDirectory:YES relativeToURL:nil];
499 #endif
500 verify_create_path([directory fileSystemRepresentation]);
501
502 /* Get the full path of the pinning DB */
503 return [directory URLByAppendingPathComponent:@"pinningrules.sqlite3"];
504 }
505
506 - (void) initializedDb {
507 dispatch_sync(_queue, ^{
508 if (!self->_db) {
509 self->_dbPath = [self pinningDbPath];
510 self->_db = [self createAtPath];
511 }
512 });
513 }
514
515 - (instancetype) init {
516 if (self = [super init]) {
517 _queue = dispatch_queue_create("Pinning DB Queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
518 _regexCache = [NSMutableDictionary dictionary];
519 _regexCacheLock = OS_UNFAIR_LOCK_INIT;
520 [self initializedDb];
521 }
522 return self;
523 }
524
525 - (void) dealloc {
526 CFReleaseNull(_db);
527 }
528
529 /* MARK: DB Cache
530 * The cache is represented a dictionary defined as { suffix : { regex : resultsDictionary } } */
531 - (void) clearCache {
532 os_unfair_lock_lock(&_regexCacheLock);
533 self.regexCache = [NSMutableDictionary dictionary];
534 os_unfair_lock_unlock(&_regexCacheLock);
535 }
536
537 - (void) addSuffixToCache:(NSString *)suffix entry:(NSDictionary <NSRegularExpression *, NSDictionary *> *)entry {
538 os_unfair_lock_lock(&_regexCacheLock);
539 secinfo("SecPinningDb", "adding %llu entries for %@ to cache", (unsigned long long)[entry count], suffix);
540 self.regexCache[suffix] = entry;
541 os_unfair_lock_unlock(&_regexCacheLock);
542 }
543
544 /* Because we iterate over all DB entries for a suffix, even if we find a match, we guarantee
545 * that the cache, if the cache has an entry for a suffix, it has all the entries for that suffix */
546 - (BOOL) queryCacheForSuffix:(NSString *)suffix firstLabel:(NSString *)firstLabel results:(NSDictionary * __autoreleasing *)results{
547 __block BOOL foundSuffix = NO;
548 os_unfair_lock_lock(&_regexCacheLock);
549 NSDictionary <NSRegularExpression *, NSDictionary *> *cacheEntry;
550 if (NULL != (cacheEntry = self.regexCache[suffix])) {
551 foundSuffix = YES;
552 for (NSRegularExpression *regex in cacheEntry) {
553 NSUInteger numMatches = [regex numberOfMatchesInString:firstLabel
554 options:0
555 range:NSMakeRange(0, [firstLabel length])];
556 if (numMatches == 0) {
557 continue;
558 }
559 secinfo("SecPinningDb", "found matching rule in cache for %@.%@", firstLabel, suffix);
560 NSDictionary *resultDictionary = [cacheEntry objectForKey:regex];
561
562 /* Check the policyName for no-pinning settings */
563 if ([self isPinningDisabled:resultDictionary[(__bridge NSString *)kSecPinningDbKeyPolicyName]]) {
564 continue;
565 }
566
567 /* Return the pinning rules */
568 if (results) {
569 *results = resultDictionary;
570 }
571 }
572 }
573 os_unfair_lock_unlock(&_regexCacheLock);
574
575 return foundSuffix;
576 }
577
578 - (BOOL) isPinningDisabled:(NSString * _Nullable)policy {
579 static dispatch_once_t once;
580 static sec_action_t action;
581
582 BOOL pinningDisabled = NO;
583 if (SecIsInternalRelease()) {
584 NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.security"];
585 pinningDisabled = [defaults boolForKey:@"AppleServerAuthenticationNoPinning"];
586 if (!pinningDisabled && policy) {
587 NSMutableString *policySpecificKey = [NSMutableString stringWithString:@"AppleServerAuthenticationNoPinning"];
588 [policySpecificKey appendString:policy];
589 pinningDisabled = [defaults boolForKey:policySpecificKey];
590 secinfo("pinningQA", "%@ disable pinning = %d", policy, pinningDisabled);
591 }
592 }
593
594
595 dispatch_once(&once, ^{
596 /* Only log system-wide pinning status once every five minutes */
597 action = sec_action_create("pinning logging charles", 5*60.0);
598 sec_action_set_handler(action, ^{
599 if (!SecIsInternalRelease()) {
600 secnotice("pinningQA", "could not disable pinning: not an internal release");
601 } else {
602 NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.security"];
603 secnotice("pinningQA", "generic pinning disable = %d", [defaults boolForKey:@"AppleServerAuthenticationNoPinning"]);
604 }
605 });
606 });
607 sec_action_perform(action);
608
609 return pinningDisabled;
610 }
611
612 - (NSDictionary * _Nullable) queryForDomain:(NSString *)domain {
613 if (!_queue) { (void)[self init]; }
614 if (!_db) { [self initializedDb]; }
615
616 /* Check for general no-pinning setting */
617 if ([self isPinningDisabled:nil]) {
618 return nil;
619 }
620
621 /* parse the domain into suffix and 1st label */
622 NSRange firstDot = [domain rangeOfString:@"."];
623 if (firstDot.location == NSNotFound) { return nil; } // Probably not a legitimate domain name
624 __block NSString *firstLabel = [domain substringToIndex:firstDot.location];
625 __block NSString *suffix = [domain substringFromIndex:(firstDot.location + 1)];
626
627 /* Search cache */
628 NSDictionary *cacheResult = nil;
629 if ([self queryCacheForSuffix:suffix firstLabel:firstLabel results:&cacheResult]) {
630 return cacheResult;
631 }
632
633 /* Cache miss. Perform SELECT */
634 __block bool ok = true;
635 __block CFErrorRef error = NULL;
636 __block NSMutableArray *resultRules = [NSMutableArray array];
637 __block NSString *resultName = nil;
638 __block NSMutableDictionary <NSRegularExpression *, NSDictionary *> *newCacheEntry = [NSMutableDictionary dictionary];
639 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
640 ok &= SecDbWithSQL(dbconn, selectDomainSQL, &error, ^bool(sqlite3_stmt *selectDomain) {
641 ok &= SecDbBindText(selectDomain, 1, [suffix UTF8String], [suffix length], SQLITE_TRANSIENT, &error);
642 ok &= SecDbStep(dbconn, selectDomain, &error, ^(bool *stop) {
643 @autoreleasepool {
644 /* Get the data from the entry */
645 // First Label Regex
646 const uint8_t *regex = sqlite3_column_text(selectDomain, 0);
647 verify_action(regex, return);
648 NSString *regexStr = [NSString stringWithUTF8String:(const char *)regex];
649 verify_action(regexStr, return);
650 NSRegularExpression *regularExpression = [NSRegularExpression regularExpressionWithPattern:regexStr
651 options:NSRegularExpressionCaseInsensitive
652 error:nil];
653 verify_action(regularExpression, return);
654 // Policy name
655 const uint8_t *policyName = sqlite3_column_text(selectDomain, 1);
656 NSString *policyNameStr = [NSString stringWithUTF8String:(const char *)policyName];
657 // Policies
658 NSData *xmlPolicies = [NSData dataWithBytes:sqlite3_column_blob(selectDomain, 2) length:sqlite3_column_bytes(selectDomain, 2)];
659 verify_action(xmlPolicies, return);
660 id policies = [NSPropertyListSerialization propertyListWithData:xmlPolicies options:0 format:nil error:nil];
661 verify_action(isNSArray(policies), return);
662
663 /* Add to cache entry */
664 [newCacheEntry setObject:@{(__bridge NSString*)kSecPinningDbKeyPolicyName:policyNameStr,
665 (__bridge NSString*)kSecPinningDbKeyRules:policies}
666 forKey:regularExpression];
667
668 /* Match the labelRegex */
669 NSUInteger numMatches = [regularExpression numberOfMatchesInString:firstLabel
670 options:0
671 range:NSMakeRange(0, [firstLabel length])];
672 if (numMatches == 0) {
673 return;
674 }
675 secinfo("SecPinningDb", "found matching rule in DB for %@.%@", firstLabel, suffix);
676
677 /* Check the policyName for no-pinning settings */
678 if ([self isPinningDisabled:policyNameStr]) {
679 return;
680 }
681
682 /* Add return data
683 * @@@ Assumes there is only one rule with matching suffix/label pairs. */
684 [resultRules addObjectsFromArray:(NSArray *)policies];
685 resultName = policyNameStr;
686 }
687 });
688 return ok;
689 });
690 });
691
692 if (!ok || error) {
693 secerror("SecPinningDb: error querying DB for hostname: %@", error);
694 #if ENABLE_TRUSTD_ANALYTICS
695 [[TrustdHealthAnalytics logger] logHardError:(__bridge NSError *)error
696 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
697 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
698 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationRead)}];
699 #endif // ENABLE_TRUSTD_ANALYTICS
700 CFReleaseNull(error);
701 }
702
703 /* Add new cache entry to cache. */
704 if ([newCacheEntry count] > 0) {
705 [self addSuffixToCache:suffix entry:newCacheEntry];
706 }
707
708 /* Return results if found */
709 if ([resultRules count] > 0) {
710 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
711 (__bridge NSString*)kSecPinningDbKeyPolicyName:resultName};
712 return results;
713 }
714 return nil;
715 }
716
717 - (NSDictionary * _Nullable) queryForPolicyName:(NSString *)policyName {
718 if (!_queue) { (void)[self init]; }
719 if (!_db) { [self initializedDb]; }
720
721 /* Skip the "sslServer" policyName, which is not a pinning policy */
722 if ([policyName isEqualToString:@"sslServer"]) {
723 return nil;
724 }
725
726 /* Check for general no-pinning setting */
727 if ([self isPinningDisabled:nil] || [self isPinningDisabled:policyName]) {
728 return nil;
729 }
730
731 secinfo("SecPinningDb", "Fetching rules for policy named %@", policyName);
732
733 /* Perform SELECT */
734 __block bool ok = true;
735 __block CFErrorRef error = NULL;
736 __block NSMutableArray *resultRules = [NSMutableArray array];
737 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
738 ok &= SecDbWithSQL(dbconn, selectPolicyNameSQL, &error, ^bool(sqlite3_stmt *selectPolicyName) {
739 ok &= SecDbBindText(selectPolicyName, 1, [policyName UTF8String], [policyName length], SQLITE_TRANSIENT, &error);
740 ok &= SecDbStep(dbconn, selectPolicyName, &error, ^(bool *stop) {
741 @autoreleasepool {
742 secinfo("SecPinningDb", "found matching rule for %@ policy", policyName);
743
744 /* Deserialize the policies and return */
745 NSData *xmlPolicies = [NSData dataWithBytes:sqlite3_column_blob(selectPolicyName, 0) length:sqlite3_column_bytes(selectPolicyName, 0)];
746 if (!xmlPolicies) { return; }
747 id policies = [NSPropertyListSerialization propertyListWithData:xmlPolicies options:0 format:nil error:nil];
748 if (!isNSArray(policies)) {
749 return;
750 }
751 [resultRules addObjectsFromArray:(NSArray *)policies];
752 }
753 });
754 return ok;
755 });
756 });
757
758 if (!ok || error) {
759 secerror("SecPinningDb: error querying DB for policyName: %@", error);
760 #if ENABLE_TRUSTD_ANALYTICS
761 [[TrustdHealthAnalytics logger] logHardError:(__bridge NSError *)error
762 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
763 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
764 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationRead)}];
765 #endif // ENABLE_TRUSTD_ANALYTICS
766 CFReleaseNull(error);
767 }
768
769 if ([resultRules count] > 0) {
770 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
771 (__bridge NSString*)kSecPinningDbKeyPolicyName:policyName};
772 return results;
773 }
774 return nil;
775 }
776
777 @end
778
779 /* C interfaces */
780 static SecPinningDb *pinningDb = nil;
781 void SecPinningDbInitialize(void) {
782 /* Create the pinning object once per launch */
783 static dispatch_once_t onceToken;
784 dispatch_once(&onceToken, ^{
785 @autoreleasepool {
786 pinningDb = [[SecPinningDb alloc] init];
787 __block CFErrorRef error = NULL;
788 BOOL ok = SecDbPerformRead([pinningDb db], &error, ^(SecDbConnectionRef dbconn) {
789 NSNumber *contentVersion = [pinningDb getContentVersion:dbconn error:&error];
790 NSNumber *schemaVersion = [pinningDb getSchemaVersion:dbconn error:&error];
791 secinfo("pinningDb", "Database Schema: %@ Content: %@", schemaVersion, contentVersion);
792 });
793 if (!ok || error) {
794 secerror("SecPinningDb: unable to initialize db: %@", error);
795 #if ENABLE_TRUSTD_ANALYTICS
796 [[TrustdHealthAnalytics logger] logHardError:(__bridge NSError *)error
797 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
798 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
799 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationRead)}];
800 #endif // ENABLE_TRUSTD_ANALYTICS
801 }
802 CFReleaseNull(error);
803 }
804 });
805 }
806
807 CFDictionaryRef _Nullable SecPinningDbCopyMatching(CFDictionaryRef query) {
808 @autoreleasepool {
809 SecPinningDbInitialize();
810
811 NSDictionary *nsQuery = (__bridge NSDictionary*)query;
812 NSString *hostname = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyHostname];
813
814 NSDictionary *results = [pinningDb queryForDomain:hostname];
815 if (results) { return CFBridgingRetain(results); }
816 NSString *policyName = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyPolicyName];
817 results = [pinningDb queryForPolicyName:policyName];
818 if (!results) { return nil; }
819 return CFBridgingRetain(results);
820 }
821 }
822
823 #if !TARGET_OS_BRIDGE
824 bool SecPinningDbUpdateFromURL(NSURL *url, NSError **error) {
825 SecPinningDbInitialize();
826
827 return [pinningDb installDbFromURL:url error:error];
828 }
829 #endif
830
831 CFNumberRef SecPinningDbCopyContentVersion(void) {
832 @autoreleasepool {
833 __block CFErrorRef error = NULL;
834 __block NSNumber *contentVersion = nil;
835 BOOL ok = SecDbPerformRead([pinningDb db], &error, ^(SecDbConnectionRef dbconn) {
836 contentVersion = [pinningDb getContentVersion:dbconn error:&error];
837 });
838 if (!ok || error) {
839 secerror("SecPinningDb: unable to get content version: %@", error);
840 }
841 CFReleaseNull(error);
842 if (!contentVersion) {
843 contentVersion = [NSNumber numberWithInteger:0];
844 }
845 return CFBridgingRetain(contentVersion);
846 }
847 }