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