2 * Copyright (c) 2016-2018 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
29 #include <AssertMacros.h>
30 #import <Foundation/Foundation.h>
36 #import <MobileAsset/MAAsset.h>
37 #import <MobileAsset/MAAssetQuery.h>
41 #import <MobileAsset/MobileAsset.h>
45 #import <Security/SecInternalReleasePriv.h>
47 #import <securityd/OTATrustUtilities.h>
48 #import <securityd/SecPinningDb.h>
49 #import <securityd/SecTrustLoggingServer.h>
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"
61 #define kSecPinningBasePath "/Library/Keychains/"
62 #define kSecPinningDbFileName "pinningrules.sqlite3"
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 */
71 const CFStringRef kSecPinningDbKeyHostname = CFSTR("PinningHostname");
72 const CFStringRef kSecPinningDbKeyPolicyName = CFSTR("PinningPolicyName");
73 const CFStringRef kSecPinningDbKeyRules = CFSTR("PinningRules");
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;
86 static inline bool isNSNumber(id nsType) {
87 return nsType && [nsType isKindOfClass:[NSNumber class]];
90 static inline bool isNSArray(id nsType) {
91 return nsType && [nsType isKindOfClass:[NSArray class]];
94 static inline bool isNSDictionary(id nsType) {
95 return nsType && [nsType isKindOfClass:[NSDictionary class]];
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;")
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];
120 - (BOOL)setSchemaVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
122 NSString *setVersion = [NSString stringWithFormat:@"PRAGMA user_version = %llu", PinningDbSchemaVersion];
123 ok &= SecDbExec(dbconn,
124 (__bridge CFStringRef)setVersion,
127 secerror("SecPinningDb: failed to create admin table: %@", error ? *error : nil);
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];
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);
155 secerror("SecPinningDb: failed to set version %@ from pinning list: %@", version, error ? *error : nil);
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) {
168 secnotice("pinningDb", "Pinning database should update from version %@ to version %@", db_version, new_version);
173 secerror("SecPinningDb: error reading content version from database %@", error);
175 if (nserror && error) { *nserror = CFBridgingRelease(error); }
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. */
188 secdebug("pinningDb", "inserting new rule: %@ for %@.%@", policyName, labelRegex, domainSuffix);
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
200 secerror("SecPinningDb: failed to serialize policies");
203 ok &= SecDbBindBlob(insertRule, 4, [xmlPolicies bytes], [xmlPolicies length], SQLITE_TRANSIENT, error);
204 ok &= SecDbStep(dbconn, insertRule, error, NULL);
208 secerror("SecPinningDb: failed to insert rule %@ for %@.%@ with error %@", policyName, labelRegex, domainSuffix, error ? *error : nil);
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");
222 NSDictionary *rule = obj;
223 __block NSString *policyName = [rule objectForKey:PinningDbPolicyNameKey];
224 NSArray *domains = [rule objectForKey:PinningDbDomainsKey];
225 __block NSArray *policies = [rule objectForKey:PinningDbPoliciesKey];
227 if (!policyName || !domains || !policies) {
228 secerror("SecPinningDb: failed to get required fields from rule entry %lu", (unsigned long)idx);
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);
239 NSDictionary *domain = obj;
240 NSString *suffix = [domain objectForKey:PinningDbDomainSuffixKey];
241 NSString *labelRegex = [domain objectForKey:PinningDbLabelRegexKey];
243 if (!suffix || !labelRegex) {
244 secerror("SecPinningDb: failed to get required fields for entry %lu for %@", (unsigned long)idx, policyName);
248 ok &= [self insertRuleWithName:policyName domainSuffix:suffix labelRegex:labelRegex policies:policies
249 dbConnection:dbconn error:error];
253 secerror("SecPinningDb: failed to populate DB from pinning list: %@", error ? *error : nil);
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);
265 secerror("SecPinningDb: failed to delete old values: %@", error ? *error :nil);
271 - (BOOL) createOrAlterAdminTable:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
273 ok &= SecDbExec(dbconn,
274 CFSTR("CREATE TABLE IF NOT EXISTS admin("
275 "key TEXT PRIMARY KEY NOT NULL,"
276 "ival INTEGER NOT NULL,"
281 secerror("SecPinningDb: failed to create admin table: %@", error ? *error : nil);
286 - (BOOL) createOrAlterRulesTable:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
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)"
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);
300 secerror("SecPinningDb: failed to create rules table: %@", error ? *error : nil);
305 #if !TARGET_OS_BRIDGE
306 - (BOOL) installDbFromURL:(NSURL *)localURL error:(NSError **)nserror {
308 secerror("SecPinningDb: missing url for downloaded asset");
311 NSURL *fileLoc = [NSURL URLWithString:@"CertificatePinning.plist"
312 relativeToURL:localURL];
313 __block NSArray *pinningList = [NSArray arrayWithContentsOfURL:fileLoc error:nserror];
315 secerror("SecPinningDb: unable to create pinning list from asset file: %@", fileLoc);
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) {
325 /* We got a new plist but we already have that version installed. */
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];
336 /* We changed the database, so clear the database cache */
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); }
353 #endif /* !TARGET_OS_BRIDGE */
355 - (NSArray *) copySystemPinningList {
356 NSArray *pinningList = nil;
357 NSURL *pinningListURL = nil;
358 /* Get the pinning list shipped with the OS */
359 SecOTAPKIRef otapkiref = SecOTAPKICopyCurrentOTAPKIRef();
361 pinningListURL = CFBridgingRelease(SecOTAPKICopyPinningList(otapkiref));
362 CFReleaseNull(otapkiref);
363 if (!pinningListURL) {
364 secerror("SecPinningDb: failed to get pinning plist URL");
366 NSError *error = nil;
367 pinningList = [NSArray arrayWithContentsOfURL:pinningListURL error:&error];
369 secerror("SecPinningDb: failed to read pinning plist from bundle: %@", error);
376 - (BOOL) updateDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error pinningList:(NSArray *)pinningList
377 updateSchema:(BOOL)updateSchema updateContent:(BOOL)updateContent
379 if (!SecOTAPKIIsSystemTrustd()) { return false; }
380 secdebug("pinningDb", "updating or creating database");
382 __block bool ok = true;
383 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, error, ^(bool *commit) {
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];
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];
397 /* read the new data */
398 NSNumber *version = [pinningList objectAtIndex:0];
400 /* populate the tables */
401 ok &= [self populateDbFromBundle:pinningList dbConnection:dbconn error:error];
402 ok &= [self setContentVersion:version dbConnection:dbconn error:error];
411 - (SecDbRef) createAtPath {
412 bool readWrite = SecOTAPKIIsSystemTrustd();
414 mode_t mode = 0644; // Root trustd can rw. All other trustds need to read.
416 mode_t mode = 0600; // Only one trustd.
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. */
428 __block BOOL ok = true;
429 dispatch_sync(self->_queue, ^{
430 bool updateSchema = false;
431 bool updateContent = false;
433 /* Get the pinning plist */
434 NSArray *pinningList = [self copySystemPinningList];
436 secerror("SecPinningDb: failed to find pinning plist in bundle");
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.
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;
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);
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);
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
485 static void verify_create_path(const char *path)
487 int ret = mkpath_np(path, 0755);
488 if (!(ret == 0 || ret == EEXIST)) {
489 secerror("could not create path: %s (%s)", path, strerror(ret));
493 - (NSURL *)pinningDbPath {
494 /* Make sure the /Library/Keychains directory is there */
496 NSURL *directory = CFBridgingRelease(SecCopyURLForFileInKeychainDirectory(nil));
498 NSURL *directory = [NSURL fileURLWithFileSystemRepresentation:"/Library/Keychains/" isDirectory:YES relativeToURL:nil];
500 verify_create_path([directory fileSystemRepresentation]);
502 /* Get the full path of the pinning DB */
503 return [directory URLByAppendingPathComponent:@"pinningrules.sqlite3"];
506 - (void) initializedDb {
507 dispatch_sync(_queue, ^{
509 self->_dbPath = [self pinningDbPath];
510 self->_db = [self createAtPath];
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];
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);
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);
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])) {
552 for (NSRegularExpression *regex in cacheEntry) {
553 NSUInteger numMatches = [regex numberOfMatchesInString:firstLabel
555 range:NSMakeRange(0, [firstLabel length])];
556 if (numMatches == 0) {
559 secinfo("SecPinningDb", "found matching rule in cache for %@.%@", firstLabel, suffix);
560 NSDictionary *resultDictionary = [cacheEntry objectForKey:regex];
562 /* Check the policyName for no-pinning settings */
563 if ([self isPinningDisabled:resultDictionary[(__bridge NSString *)kSecPinningDbKeyPolicyName]]) {
567 /* Return the pinning rules */
569 *results = resultDictionary;
573 os_unfair_lock_unlock(&_regexCacheLock);
578 - (BOOL) isPinningDisabled:(NSString * _Nullable)policy {
579 static dispatch_once_t once;
580 static sec_action_t action;
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);
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");
602 NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.security"];
603 secnotice("pinningQA", "generic pinning disable = %d", [defaults boolForKey:@"AppleServerAuthenticationNoPinning"]);
607 sec_action_perform(action);
609 return pinningDisabled;
612 - (NSDictionary * _Nullable) queryForDomain:(NSString *)domain {
613 if (!_queue) { (void)[self init]; }
614 if (!_db) { [self initializedDb]; }
616 /* Check for general no-pinning setting */
617 if ([self isPinningDisabled:nil]) {
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)];
628 NSDictionary *cacheResult = nil;
629 if ([self queryCacheForSuffix:suffix firstLabel:firstLabel results:&cacheResult]) {
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) {
644 /* Get the data from the entry */
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
653 verify_action(regularExpression, return);
655 const uint8_t *policyName = sqlite3_column_text(selectDomain, 1);
656 NSString *policyNameStr = [NSString stringWithUTF8String:(const char *)policyName];
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);
663 /* Add to cache entry */
664 [newCacheEntry setObject:@{(__bridge NSString*)kSecPinningDbKeyPolicyName:policyNameStr,
665 (__bridge NSString*)kSecPinningDbKeyRules:policies}
666 forKey:regularExpression];
668 /* Match the labelRegex */
669 NSUInteger numMatches = [regularExpression numberOfMatchesInString:firstLabel
671 range:NSMakeRange(0, [firstLabel length])];
672 if (numMatches == 0) {
675 secinfo("SecPinningDb", "found matching rule in DB for %@.%@", firstLabel, suffix);
677 /* Check the policyName for no-pinning settings */
678 if ([self isPinningDisabled:policyNameStr]) {
683 * @@@ Assumes there is only one rule with matching suffix/label pairs. */
684 [resultRules addObjectsFromArray:(NSArray *)policies];
685 resultName = policyNameStr;
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);
703 /* Add new cache entry to cache. */
704 if ([newCacheEntry count] > 0) {
705 [self addSuffixToCache:suffix entry:newCacheEntry];
708 /* Return results if found */
709 if ([resultRules count] > 0) {
710 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
711 (__bridge NSString*)kSecPinningDbKeyPolicyName:resultName};
717 - (NSDictionary * _Nullable) queryForPolicyName:(NSString *)policyName {
718 if (!_queue) { (void)[self init]; }
719 if (!_db) { [self initializedDb]; }
721 /* Skip the "sslServer" policyName, which is not a pinning policy */
722 if ([policyName isEqualToString:@"sslServer"]) {
726 /* Check for general no-pinning setting */
727 if ([self isPinningDisabled:nil] || [self isPinningDisabled:policyName]) {
731 secinfo("SecPinningDb", "Fetching rules for policy named %@", policyName);
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) {
742 secinfo("SecPinningDb", "found matching rule for %@ policy", policyName);
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)) {
751 [resultRules addObjectsFromArray:(NSArray *)policies];
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);
769 if ([resultRules count] > 0) {
770 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
771 (__bridge NSString*)kSecPinningDbKeyPolicyName:policyName};
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, ^{
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);
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
802 CFReleaseNull(error);
807 CFDictionaryRef _Nullable SecPinningDbCopyMatching(CFDictionaryRef query) {
809 SecPinningDbInitialize();
811 NSDictionary *nsQuery = (__bridge NSDictionary*)query;
812 NSString *hostname = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyHostname];
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);
823 #if !TARGET_OS_BRIDGE
824 bool SecPinningDbUpdateFromURL(NSURL *url, NSError **error) {
825 SecPinningDbInitialize();
827 return [pinningDb installDbFromURL:url error:error];
831 CFNumberRef SecPinningDbCopyContentVersion(void) {
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];
839 secerror("SecPinningDb: unable to get content version: %@", error);
841 CFReleaseNull(error);
842 if (!contentVersion) {
843 contentVersion = [NSNumber numberWithInteger:0];
845 return CFBridgingRetain(contentVersion);