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];
337 /* We changed the database, so clear the database cache */
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); }
355 #endif /* !TARGET_OS_BRIDGE */
357 - (NSArray *) copySystemPinningList {
358 NSArray *pinningList = nil;
359 NSURL *pinningListURL = nil;
360 /* Get the pinning list shipped with the OS */
361 SecOTAPKIRef otapkiref = SecOTAPKICopyCurrentOTAPKIRef();
363 pinningListURL = CFBridgingRelease(SecOTAPKICopyPinningList(otapkiref));
364 CFReleaseNull(otapkiref);
365 if (!pinningListURL) {
366 secerror("SecPinningDb: failed to get pinning plist URL");
368 NSError *error = nil;
369 pinningList = [NSArray arrayWithContentsOfURL:pinningListURL error:&error];
371 secerror("SecPinningDb: failed to read pinning plist from bundle: %@", error);
378 - (BOOL) updateDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error pinningList:(NSArray *)pinningList
379 updateSchema:(BOOL)updateSchema updateContent:(BOOL)updateContent
381 if (!SecOTAPKIIsSystemTrustd()) { return false; }
382 secdebug("pinningDb", "updating or creating database");
384 __block bool ok = true;
385 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, error, ^(bool *commit) {
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];
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];
399 /* read the new data */
400 NSNumber *version = [pinningList objectAtIndex:0];
402 /* populate the tables */
403 ok &= [self populateDbFromBundle:pinningList dbConnection:dbconn error:error];
404 ok &= [self setContentVersion:version dbConnection:dbconn error:error];
413 - (SecDbRef) createAtPath {
414 bool readWrite = SecOTAPKIIsSystemTrustd();
416 mode_t mode = 0644; // Root trustd can rw. All other trustds need to read.
418 mode_t mode = 0600; // Only one trustd.
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. */
430 __block BOOL ok = true;
431 dispatch_sync(self->_queue, ^{
432 bool updateSchema = false;
433 bool updateContent = false;
435 /* Get the pinning plist */
436 NSArray *pinningList = [self copySystemPinningList];
438 secerror("SecPinningDb: failed to find pinning plist in bundle");
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.
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;
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);
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);
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
487 static void verify_create_path(const char *path)
489 int ret = mkpath_np(path, 0755);
490 if (!(ret == 0 || ret == EEXIST)) {
491 secerror("could not create path: %s (%s)", path, strerror(ret));
495 - (NSURL *)pinningDbPath {
496 /* Make sure the /Library/Keychains directory is there */
498 NSURL *directory = CFBridgingRelease(SecCopyURLForFileInKeychainDirectory(nil));
500 NSURL *directory = [NSURL fileURLWithFileSystemRepresentation:"/Library/Keychains/" isDirectory:YES relativeToURL:nil];
502 verify_create_path([directory fileSystemRepresentation]);
504 /* Get the full path of the pinning DB */
505 return [directory URLByAppendingPathComponent:@"pinningrules.sqlite3"];
508 - (void) initializedDb {
509 dispatch_sync(_queue, ^{
511 self->_dbPath = [self pinningDbPath];
512 self->_db = [self createAtPath];
517 - (instancetype) init {
518 if (self = [super init]) {
519 _queue = dispatch_queue_create("Pinning DB Queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
521 _regexCache = [NSMutableDictionary dictionary];
522 _regexCacheLock = OS_UNFAIR_LOCK_INIT;
524 [self initializedDb];
534 * The cache is represented a dictionary defined as { suffix : { regex : resultsDictionary } }
535 * The cache is not used on watchOS to reduce memory overhead. */
537 - (void) clearCache {
538 os_unfair_lock_lock(&_regexCacheLock);
539 self.regexCache = [NSMutableDictionary dictionary];
540 os_unfair_lock_unlock(&_regexCacheLock);
542 #endif // !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);
551 #endif // !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])) {
562 for (NSRegularExpression *regex in cacheEntry) {
563 NSUInteger numMatches = [regex numberOfMatchesInString:firstLabel
565 range:NSMakeRange(0, [firstLabel length])];
566 if (numMatches == 0) {
569 secinfo("SecPinningDb", "found matching rule in cache for %@.%@", firstLabel, suffix);
570 NSDictionary *resultDictionary = [cacheEntry objectForKey:regex];
572 /* Check the policyName for no-pinning settings */
573 if ([self isPinningDisabled:resultDictionary[(__bridge NSString *)kSecPinningDbKeyPolicyName]]) {
577 /* Return the pinning rules */
579 *results = resultDictionary;
583 os_unfair_lock_unlock(&_regexCacheLock);
587 #endif // !TARGET_OS_WATCH
589 - (BOOL) isPinningDisabled:(NSString * _Nullable)policy {
590 static dispatch_once_t once;
591 static sec_action_t action;
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);
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");
613 NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.security"];
614 secnotice("pinningQA", "generic pinning disable = %d", [defaults boolForKey:@"AppleServerAuthenticationNoPinning"]);
618 sec_action_perform(action);
620 return pinningDisabled;
623 - (NSDictionary * _Nullable) queryForDomain:(NSString *)domain {
624 if (!_queue) { (void)[self init]; }
625 if (!_db) { [self initializedDb]; }
627 /* Check for general no-pinning setting */
628 if ([self isPinningDisabled:nil]) {
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)];
640 NSDictionary *cacheResult = nil;
641 if ([self queryCacheForSuffix:suffix firstLabel:firstLabel results:&cacheResult]) {
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;
652 __block NSMutableDictionary <NSRegularExpression *, NSDictionary *> *newCacheEntry = [NSMutableDictionary dictionary];
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) {
659 /* Get the data from the entry */
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
668 verify_action(regularExpression, return);
670 const uint8_t *policyName = sqlite3_column_text(selectDomain, 1);
671 NSString *policyNameStr = [NSString stringWithUTF8String:(const char *)policyName];
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);
679 /* Add to cache entry */
680 [newCacheEntry setObject:@{(__bridge NSString*)kSecPinningDbKeyPolicyName:policyNameStr,
681 (__bridge NSString*)kSecPinningDbKeyRules:policies}
682 forKey:regularExpression];
685 /* Match the labelRegex */
686 NSUInteger numMatches = [regularExpression numberOfMatchesInString:firstLabel
688 range:NSMakeRange(0, [firstLabel length])];
689 if (numMatches == 0) {
692 secinfo("SecPinningDb", "found matching rule in DB for %@.%@", firstLabel, suffix);
694 /* Check the policyName for no-pinning settings */
695 if ([self isPinningDisabled:policyNameStr]) {
700 * @@@ Assumes there is only one rule with matching suffix/label pairs. */
701 [resultRules addObjectsFromArray:(NSArray *)policies];
702 resultName = policyNameStr;
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);
721 /* Add new cache entry to cache. */
722 if ([newCacheEntry count] > 0) {
723 [self addSuffixToCache:suffix entry:newCacheEntry];
727 /* Return results if found */
728 if ([resultRules count] > 0) {
729 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
730 (__bridge NSString*)kSecPinningDbKeyPolicyName:resultName};
736 - (NSDictionary * _Nullable) queryForPolicyName:(NSString *)policyName {
737 if (!_queue) { (void)[self init]; }
738 if (!_db) { [self initializedDb]; }
740 /* Skip the "sslServer" policyName, which is not a pinning policy */
741 if ([policyName isEqualToString:@"sslServer"]) {
745 /* Check for general no-pinning setting */
746 if ([self isPinningDisabled:nil] || [self isPinningDisabled:policyName]) {
750 secinfo("SecPinningDb", "Fetching rules for policy named %@", policyName);
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) {
761 secinfo("SecPinningDb", "found matching rule for %@ policy", policyName);
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)) {
770 [resultRules addObjectsFromArray:(NSArray *)policies];
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);
788 if ([resultRules count] > 0) {
789 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
790 (__bridge NSString*)kSecPinningDbKeyPolicyName:policyName};
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, ^{
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);
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
821 CFReleaseNull(error);
826 CFDictionaryRef _Nullable SecPinningDbCopyMatching(CFDictionaryRef query) {
828 SecPinningDbInitialize();
830 NSDictionary *nsQuery = (__bridge NSDictionary*)query;
831 NSString *hostname = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyHostname];
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);
842 #if !TARGET_OS_BRIDGE
843 bool SecPinningDbUpdateFromURL(NSURL *url, NSError **error) {
844 SecPinningDbInitialize();
846 return [pinningDb installDbFromURL:url error:error];
850 CFNumberRef SecPinningDbCopyContentVersion(void) {
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];
858 secerror("SecPinningDb: unable to get content version: %@", error);
860 CFReleaseNull(error);
861 if (!contentVersion) {
862 contentVersion = [NSNumber numberWithInteger:0];
864 return CFBridgingRetain(contentVersion);