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 "trust/trustd/OTATrustUtilities.h"
48 #import "trust/trustd/SecPinningDb.h"
49 #import "trust/trustd/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 kSecPinningDbFileName "pinningrules.sqlite3"
63 const uint64_t PinningDbSchemaVersion = 2;
64 const NSString *PinningDbPolicyNameKey = @"policyName"; /* key for a string value */
65 const NSString *PinningDbDomainsKey = @"domains"; /* key for an array of dictionaries */
66 const NSString *PinningDbPoliciesKey = @"rules"; /* key for an array of dictionaries */
67 const NSString *PinningDbDomainSuffixKey = @"suffix"; /* key for a string */
68 const NSString *PinningDbLabelRegexKey = @"labelRegex"; /* key for a regex string */
70 const CFStringRef kSecPinningDbKeyHostname = CFSTR("PinningHostname");
71 const CFStringRef kSecPinningDbKeyPolicyName = CFSTR("PinningPolicyName");
72 const CFStringRef kSecPinningDbKeyRules = CFSTR("PinningRules");
74 @interface SecPinningDb : NSObject
75 @property (assign) SecDbRef db;
76 @property dispatch_queue_t queue;
77 @property NSURL *dbPath;
78 @property (assign) os_unfair_lock regexCacheLock;
79 @property NSMutableDictionary *regexCache;
80 - (instancetype) init;
81 - ( NSDictionary * _Nullable ) queryForDomain:(NSString *)domain;
82 - ( NSDictionary * _Nullable ) queryForPolicyName:(NSString *)policyName;
85 static inline bool isNSNumber(id nsType) {
86 return nsType && [nsType isKindOfClass:[NSNumber class]];
89 static inline bool isNSArray(id nsType) {
90 return nsType && [nsType isKindOfClass:[NSArray class]];
93 static inline bool isNSDictionary(id nsType) {
94 return nsType && [nsType isKindOfClass:[NSDictionary class]];
97 @implementation SecPinningDb
98 #define getSchemaVersionSQL CFSTR("PRAGMA user_version")
99 #define selectVersionSQL CFSTR("SELECT ival FROM admin WHERE key='version'")
100 #define insertAdminSQL CFSTR("INSERT OR REPLACE INTO admin (key,ival,value) VALUES (?,?,?)")
101 #define selectDomainSQL CFSTR("SELECT DISTINCT labelRegex,policyName,policies FROM rules WHERE domainSuffix=?")
102 #define selectPolicyNameSQL CFSTR("SELECT DISTINCT policies FROM rules WHERE policyName=?")
103 #define insertRuleSQL CFSTR("INSERT OR REPLACE INTO rules (policyName,domainSuffix,labelRegex,policies) VALUES (?,?,?,?) ")
104 #define removeAllRulesSQL CFSTR("DELETE FROM rules;")
106 - (NSNumber *)getSchemaVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
107 __block bool ok = true;
108 __block NSNumber *version = nil;
109 ok &= SecDbWithSQL(dbconn, getSchemaVersionSQL, error, ^bool(sqlite3_stmt *selectVersion) {
110 ok &= SecDbStep(dbconn, selectVersion, error, ^(bool *stop) {
111 int ival = sqlite3_column_int(selectVersion, 0);
112 version = [NSNumber numberWithInt:ival];
119 - (BOOL)setSchemaVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
121 NSString *setVersion = [NSString stringWithFormat:@"PRAGMA user_version = %llu", PinningDbSchemaVersion];
122 ok &= SecDbExec(dbconn,
123 (__bridge CFStringRef)setVersion,
126 secerror("SecPinningDb: failed to create admin table: %@", error ? *error : nil);
131 - (NSNumber *)getContentVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
132 __block bool ok = true;
133 __block NSNumber *version = nil;
134 ok &= SecDbWithSQL(dbconn, selectVersionSQL, error, ^bool(sqlite3_stmt *selectVersion) {
135 ok &= SecDbStep(dbconn, selectVersion, error, ^(bool *stop) {
136 uint64_t ival = sqlite3_column_int64(selectVersion, 0);
137 version = [NSNumber numberWithUnsignedLongLong:ival];
144 - (BOOL)setContentVersion:(NSNumber *)version dbConnection:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
145 __block BOOL ok = true;
146 ok &= SecDbWithSQL(dbconn, insertAdminSQL, error, ^bool(sqlite3_stmt *insertAdmin) {
147 const char *versionKey = "version";
148 ok &= SecDbBindText(insertAdmin, 1, versionKey, strlen(versionKey), SQLITE_TRANSIENT, error);
149 ok &= SecDbBindInt64(insertAdmin, 2, [version unsignedLongLongValue], error);
150 ok &= SecDbStep(dbconn, insertAdmin, error, NULL);
154 secerror("SecPinningDb: failed to set version %@ from pinning list: %@", version, error ? *error : nil);
159 - (BOOL) shouldUpdateContent:(NSNumber *)new_version error:(NSError **)nserror {
160 __block CFErrorRef error = NULL;
161 __block BOOL ok = YES;
162 __block BOOL newer = NO;
163 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
164 NSNumber *db_version = [self getContentVersion:dbconn error:&error];
165 if (!db_version || [new_version compare:db_version] == NSOrderedDescending) {
167 secnotice("pinningDb", "Pinning database should update from version %@ to version %@", db_version, new_version);
172 secerror("SecPinningDb: error reading content version from database %@", error);
174 if (nserror && error) { *nserror = CFBridgingRelease(error); }
178 - (BOOL) insertRuleWithName:(NSString *)policyName
179 domainSuffix:(NSString *)domainSuffix
180 labelRegex:(NSString *)labelRegex
181 policies:(NSArray *)policies
182 dbConnection:(SecDbConnectionRef)dbconn
183 error:(CFErrorRef *)error{
184 /* @@@ This insertion mechanism assumes that the input is trusted -- namely, that the new rules
185 * are allowed to replace existing rules. For third-party inputs, this assumption isn't true. */
187 secdebug("pinningDb", "inserting new rule: %@ for %@.%@", policyName, labelRegex, domainSuffix);
189 __block bool ok = true;
190 ok &= SecDbWithSQL(dbconn, insertRuleSQL, error, ^bool(sqlite3_stmt *insertRule) {
191 ok &= SecDbBindText(insertRule, 1, [policyName UTF8String], [policyName length], SQLITE_TRANSIENT, error);
192 ok &= SecDbBindText(insertRule, 2, [domainSuffix UTF8String], [domainSuffix length], SQLITE_TRANSIENT, error);
193 ok &= SecDbBindText(insertRule, 3, [labelRegex UTF8String], [labelRegex length], SQLITE_TRANSIENT, error);
194 NSData *xmlPolicies = [NSPropertyListSerialization dataWithPropertyList:policies
195 format:NSPropertyListXMLFormat_v1_0
199 secerror("SecPinningDb: failed to serialize policies");
202 ok &= SecDbBindBlob(insertRule, 4, [xmlPolicies bytes], [xmlPolicies length], SQLITE_TRANSIENT, error);
203 ok &= SecDbStep(dbconn, insertRule, error, NULL);
207 secerror("SecPinningDb: failed to insert rule %@ for %@.%@ with error %@", policyName, labelRegex, domainSuffix, error ? *error : nil);
212 - (BOOL) populateDbFromBundle:(NSArray *)pinningList dbConnection:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
213 __block BOOL ok = true;
214 [pinningList enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
215 if (idx ==0) { return; } // Skip the first value which is the version
216 if (!isNSDictionary(obj)) {
217 secerror("SecPinningDb: rule entry in pinning plist is wrong class");
221 NSDictionary *rule = obj;
222 __block NSString *policyName = [rule objectForKey:PinningDbPolicyNameKey];
223 NSArray *domains = [rule objectForKey:PinningDbDomainsKey];
224 __block NSArray *policies = [rule objectForKey:PinningDbPoliciesKey];
226 if (!policyName || !domains || !policies) {
227 secerror("SecPinningDb: failed to get required fields from rule entry %lu", (unsigned long)idx);
232 [domains enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
233 if (!isNSDictionary(obj)) {
234 secerror("SecPinningDb: domain entry %lu for %@ in pinning rule is wrong class", (unsigned long)idx, policyName);
238 NSDictionary *domain = obj;
239 NSString *suffix = [domain objectForKey:PinningDbDomainSuffixKey];
240 NSString *labelRegex = [domain objectForKey:PinningDbLabelRegexKey];
242 if (!suffix || !labelRegex) {
243 secerror("SecPinningDb: failed to get required fields for entry %lu for %@", (unsigned long)idx, policyName);
247 ok &= [self insertRuleWithName:policyName domainSuffix:suffix labelRegex:labelRegex policies:policies
248 dbConnection:dbconn error:error];
252 secerror("SecPinningDb: failed to populate DB from pinning list: %@", error ? *error : nil);
257 - (BOOL) removeAllRulesFromDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
258 __block BOOL ok = true;
259 ok &= SecDbWithSQL(dbconn, removeAllRulesSQL, error, ^bool(sqlite3_stmt *deleteRules) {
260 ok &= SecDbStep(dbconn, deleteRules, error, NULL);
264 secerror("SecPinningDb: failed to delete old values: %@", error ? *error :nil);
270 - (BOOL) createOrAlterAdminTable:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
272 ok &= SecDbExec(dbconn,
273 CFSTR("CREATE TABLE IF NOT EXISTS admin("
274 "key TEXT PRIMARY KEY NOT NULL,"
275 "ival INTEGER NOT NULL,"
280 secerror("SecPinningDb: failed to create admin table: %@", error ? *error : nil);
285 - (BOOL) createOrAlterRulesTable:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
287 ok &= SecDbExec(dbconn,
288 CFSTR("CREATE TABLE IF NOT EXISTS rules("
289 "policyName TEXT NOT NULL,"
290 "domainSuffix TEXT NOT NULL,"
291 "labelRegex TEXT NOT NULL,"
292 "policies BLOB NOT NULL,"
293 "UNIQUE(policyName, domainSuffix, labelRegex)"
296 ok &= SecDbExec(dbconn, CFSTR("CREATE INDEX IF NOT EXISTS idomain ON rules(domainSuffix);"), error);
297 ok &= SecDbExec(dbconn, CFSTR("CREATE INDEX IF NOT EXISTS ipolicy ON rules(policyName);"), error);
299 secerror("SecPinningDb: failed to create rules table: %@", error ? *error : nil);
304 #if !TARGET_OS_BRIDGE
305 - (BOOL) installDbFromURL:(NSURL *)localURL error:(NSError **)nserror {
307 secerror("SecPinningDb: missing url for downloaded asset");
310 NSURL *fileLoc = [NSURL URLWithString:@"CertificatePinning.plist"
311 relativeToURL:localURL];
312 __block NSArray *pinningList = [NSArray arrayWithContentsOfURL:fileLoc error:nserror];
314 secerror("SecPinningDb: unable to create pinning list from asset file: %@", fileLoc);
318 NSNumber *plist_version = [pinningList objectAtIndex:0];
319 if (![self shouldUpdateContent:plist_version error:nserror]) {
320 /* Something went wrong reading the DB in order to determine whether this version is new. */
321 if (nserror && *nserror) {
324 /* We got a new plist but we already have that version installed. */
329 __block CFErrorRef error = NULL;
330 __block BOOL ok = YES;
331 dispatch_sync(self->_queue, ^{
332 ok &= SecDbPerformWrite(self->_db, &error, ^(SecDbConnectionRef dbconn) {
333 ok &= [self updateDb:dbconn error:&error pinningList:pinningList updateSchema:NO updateContent:YES];
336 /* We changed the database, so clear the database cache */
342 secerror("SecPinningDb: error installing updated pinning list version %@: %@", [pinningList objectAtIndex:0], error);
343 #if ENABLE_TRUSTD_ANALYTICS
344 [[TrustAnalytics logger] logHardError:(__bridge NSError *)error
345 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
346 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
347 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationWrite) }];
348 #endif // ENABLE_TRUSTD_ANALYTICS
349 if (nserror && error) { *nserror = CFBridgingRelease(error); }
354 #endif /* !TARGET_OS_BRIDGE */
356 - (NSArray *) copySystemPinningList {
357 NSArray *pinningList = nil;
358 NSURL *pinningListURL = nil;
359 /* Get the pinning list shipped with the OS */
360 SecOTAPKIRef otapkiref = SecOTAPKICopyCurrentOTAPKIRef();
362 pinningListURL = CFBridgingRelease(SecOTAPKICopyPinningList(otapkiref));
363 CFReleaseNull(otapkiref);
364 if (!pinningListURL) {
365 secerror("SecPinningDb: failed to get pinning plist URL");
367 NSError *error = nil;
368 pinningList = [NSArray arrayWithContentsOfURL:pinningListURL error:&error];
370 secerror("SecPinningDb: failed to read pinning plist from bundle: %@", error);
377 - (BOOL) updateDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error pinningList:(NSArray *)pinningList
378 updateSchema:(BOOL)updateSchema updateContent:(BOOL)updateContent
380 if (!SecOTAPKIIsSystemTrustd()) { return false; }
381 secdebug("pinningDb", "updating or creating database");
383 __block bool ok = true;
384 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, error, ^(bool *commit) {
386 /* update the tables */
387 ok &= [self createOrAlterAdminTable:dbconn error:error];
388 ok &= [self createOrAlterRulesTable:dbconn error:error];
389 ok &= [self setSchemaVersion:dbconn error:error];
393 /* remove the old data */
394 /* @@@ This behavior assumes that we have all the rules we want to populate
395 * elsewhere on disk and that the DB doesn't contain the sole copy of that data. */
396 ok &= [self removeAllRulesFromDb:dbconn error:error];
398 /* read the new data */
399 NSNumber *version = [pinningList objectAtIndex:0];
401 /* populate the tables */
402 ok &= [self populateDbFromBundle:pinningList dbConnection:dbconn error:error];
403 ok &= [self setContentVersion:version dbConnection:dbconn error:error];
412 - (SecDbRef) createAtPath {
413 bool readWrite = SecOTAPKIIsSystemTrustd();
415 mode_t mode = 0644; // Root trustd can rw. All other trustds need to read.
417 mode_t mode = 0600; // Only one trustd.
420 CFStringRef path = CFStringCreateWithCString(NULL, [_dbPath fileSystemRepresentation], kCFStringEncodingUTF8);
421 SecDbRef result = SecDbCreate(path, mode, readWrite, readWrite, false, false, 1,
422 ^bool (SecDbRef db, SecDbConnectionRef dbconn, bool didCreate, bool *callMeAgainForNextConnection, CFErrorRef *error) {
423 if (!SecOTAPKIIsSystemTrustd()) {
424 /* Non-owner process can't update the db, but it should get a db connection.
425 * @@@ Revisit if new schema version is needed by reader processes. */
429 dispatch_assert_queue_not(self->_queue);
431 __block BOOL ok = true;
432 dispatch_sync(self->_queue, ^{
433 bool updateSchema = false;
434 bool updateContent = false;
436 /* Get the pinning plist */
437 NSArray *pinningList = [self copySystemPinningList];
439 secerror("SecPinningDb: failed to find pinning plist in bundle");
444 /* Check latest data and schema versions against existing table. */
445 if (!isNSNumber([pinningList objectAtIndex:0])) {
446 secerror("SecPinningDb: pinning plist in wrong format");
447 return; // Don't change status. We can continue to use old DB.
449 NSNumber *plist_version = [pinningList objectAtIndex:0];
450 NSNumber *db_version = [self getContentVersion:dbconn error:error];
451 secnotice("pinningDb", "Opening db with version %@", db_version);
452 if (!db_version || [plist_version compare:db_version] == NSOrderedDescending) {
453 secnotice("pinningDb", "Updating pinning database content from version %@ to version %@",
454 db_version ? db_version : 0, plist_version);
455 updateContent = true;
457 NSNumber *schema_version = [self getSchemaVersion:dbconn error:error];
458 NSNumber *current_version = [NSNumber numberWithUnsignedLongLong:PinningDbSchemaVersion];
459 if (!schema_version || ![schema_version isEqualToNumber:current_version]) {
460 secnotice("pinningDb", "Updating pinning database schema from version %@ to version %@",
461 schema_version, current_version);
465 if (updateContent || updateSchema) {
466 ok &= [self updateDb:dbconn error:error pinningList:pinningList updateSchema:updateSchema updateContent:updateContent];
467 /* Since we updated the DB to match the list that shipped with the system,
468 * reset the OTAPKI Asset version to the system asset version */
469 (void)SecOTAPKIResetCurrentAssetVersion(NULL);
472 secerror("SecPinningDb: %s failed: %@", didCreate ? "Create" : "Open", error ? *error : NULL);
473 #if ENABLE_TRUSTD_ANALYTICS
474 [[TrustAnalytics logger] logHardError:(error ? (__bridge NSError *)*error : nil)
475 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
476 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
477 TrustdHealthAnalyticsAttributeDatabaseOperation : didCreate ? @(TAOperationCreate) : @(TAOperationOpen)}];
478 #endif // ENABLE_TRUSTD_ANALYTICS
488 static void verify_create_path(const char *path)
490 int ret = mkpath_np(path, 0755);
491 if (!(ret == 0 || ret == EEXIST)) {
492 secerror("could not create path: %s (%s)", path, strerror(ret));
496 - (NSURL *)pinningDbPath {
497 /* Make sure the /Library/Keychains directory is there */
498 NSURL *directory = CFBridgingRelease(SecCopyURLForFileInSystemKeychainDirectory(nil));
499 verify_create_path([directory fileSystemRepresentation]);
501 /* Get the full path of the pinning DB */
502 return [directory URLByAppendingPathComponent:@kSecPinningDbFileName];
505 - (void) initializedDb {
506 dispatch_sync(_queue, ^{
508 self->_dbPath = [self pinningDbPath];
509 self->_db = [self createAtPath];
514 - (instancetype) init {
515 if (self = [super init]) {
516 _queue = dispatch_queue_create("Pinning DB Queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
518 _regexCache = [NSMutableDictionary dictionary];
519 _regexCacheLock = OS_UNFAIR_LOCK_INIT;
521 [self initializedDb];
531 * The cache is represented a dictionary defined as { suffix : { regex : resultsDictionary } }
532 * The cache is not used on watchOS to reduce memory overhead. */
534 - (void) clearCache {
535 os_unfair_lock_lock(&_regexCacheLock);
536 self.regexCache = [NSMutableDictionary dictionary];
537 os_unfair_lock_unlock(&_regexCacheLock);
539 #endif // !TARGET_OS_WATCH
542 - (void) addSuffixToCache:(NSString *)suffix entry:(NSDictionary <NSRegularExpression *, NSDictionary *> *)entry {
543 os_unfair_lock_lock(&_regexCacheLock);
544 secinfo("SecPinningDb", "adding %llu entries for %@ to cache", (unsigned long long)[entry count], suffix);
545 self.regexCache[suffix] = entry;
546 os_unfair_lock_unlock(&_regexCacheLock);
548 #endif // !TARGET_OS_WATCH
551 /* Because we iterate over all DB entries for a suffix, even if we find a match, we guarantee
552 * that the cache, if the cache has an entry for a suffix, it has all the entries for that suffix */
553 - (BOOL) queryCacheForSuffix:(NSString *)suffix firstLabel:(NSString *)firstLabel results:(NSDictionary * __autoreleasing *)results{
554 __block BOOL foundSuffix = NO;
555 os_unfair_lock_lock(&_regexCacheLock);
556 NSDictionary <NSRegularExpression *, NSDictionary *> *cacheEntry;
557 if (NULL != (cacheEntry = self.regexCache[suffix])) {
559 for (NSRegularExpression *regex in cacheEntry) {
560 NSUInteger numMatches = [regex numberOfMatchesInString:firstLabel
562 range:NSMakeRange(0, [firstLabel length])];
563 if (numMatches == 0) {
566 secinfo("SecPinningDb", "found matching rule in cache for %@.%@", firstLabel, suffix);
567 NSDictionary *resultDictionary = [cacheEntry objectForKey:regex];
569 /* Check the policyName for no-pinning settings */
570 /* Return the pinning rules */
572 /* Check the no-pinning settings to determine whether to use the rules */
573 NSString *policyName = resultDictionary[(__bridge NSString *)kSecPinningDbKeyPolicyName];
574 if ([self isPinningDisabled:policyName]) {
575 *results = @{ (__bridge NSString*)kSecPinningDbKeyRules:@[@{}],
576 (__bridge NSString*)kSecPinningDbKeyPolicyName:policyName};
578 *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 /* parse the domain into suffix and 1st label */
628 NSRange firstDot = [domain rangeOfString:@"."];
629 if (firstDot.location == NSNotFound) { return nil; } // Probably not a legitimate domain name
630 __block NSString *firstLabel = [domain substringToIndex:firstDot.location];
631 __block NSString *suffix = [domain substringFromIndex:(firstDot.location + 1)];
635 NSDictionary *cacheResult = nil;
636 if ([self queryCacheForSuffix:suffix firstLabel:firstLabel results:&cacheResult]) {
641 /* Cache miss. Perform SELECT */
642 __block bool ok = true;
643 __block CFErrorRef error = NULL;
644 __block NSMutableArray *resultRules = [NSMutableArray array];
645 __block NSString *resultName = nil;
647 __block NSMutableDictionary <NSRegularExpression *, NSDictionary *> *newCacheEntry = [NSMutableDictionary dictionary];
649 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
650 ok &= SecDbWithSQL(dbconn, selectDomainSQL, &error, ^bool(sqlite3_stmt *selectDomain) {
651 ok &= SecDbBindText(selectDomain, 1, [suffix UTF8String], [suffix length], SQLITE_TRANSIENT, &error);
652 ok &= SecDbStep(dbconn, selectDomain, &error, ^(bool *stop) {
654 /* Get the data from the entry */
656 const uint8_t *regex = sqlite3_column_text(selectDomain, 0);
657 verify_action(regex, return);
658 NSString *regexStr = [NSString stringWithUTF8String:(const char *)regex];
659 verify_action(regexStr, return);
660 NSRegularExpression *regularExpression = [NSRegularExpression regularExpressionWithPattern:regexStr
661 options:NSRegularExpressionCaseInsensitive
663 verify_action(regularExpression, return);
665 const uint8_t *policyName = sqlite3_column_text(selectDomain, 1);
666 NSString *policyNameStr = [NSString stringWithUTF8String:(const char *)policyName];
668 NSData *xmlPolicies = [NSData dataWithBytes:sqlite3_column_blob(selectDomain, 2) length:sqlite3_column_bytes(selectDomain, 2)];
669 verify_action(xmlPolicies, return);
670 id policies = [NSPropertyListSerialization propertyListWithData:xmlPolicies options:0 format:nil error:nil];
671 verify_action(isNSArray(policies), return);
674 /* Add to cache entry */
675 [newCacheEntry setObject:@{(__bridge NSString*)kSecPinningDbKeyPolicyName:policyNameStr,
676 (__bridge NSString*)kSecPinningDbKeyRules:policies}
677 forKey:regularExpression];
680 /* Match the labelRegex */
681 NSUInteger numMatches = [regularExpression numberOfMatchesInString:firstLabel
683 range:NSMakeRange(0, [firstLabel length])];
684 if (numMatches == 0) {
687 secinfo("SecPinningDb", "found matching rule in DB for %@.%@", firstLabel, suffix);
690 * @@@ Assumes there is only one rule with matching suffix/label pairs. */
691 [resultRules addObjectsFromArray:(NSArray *)policies];
692 resultName = policyNameStr;
700 secerror("SecPinningDb: error querying DB for hostname: %@", error);
701 #if ENABLE_TRUSTD_ANALYTICS
702 [[TrustAnalytics logger] logHardError:(__bridge NSError *)error
703 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
704 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
705 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationRead)}];
706 #endif // ENABLE_TRUSTD_ANALYTICS
707 CFReleaseNull(error);
711 /* Add new cache entry to cache. */
712 if ([newCacheEntry count] > 0) {
713 [self addSuffixToCache:suffix entry:newCacheEntry];
717 /* Return results if found */
718 if ([resultRules count] > 0) {
719 /* Check for general no-pinning setting and return empty rules. We want to still return a
720 * a policy name so that requirements that don't apply to pinned domains continue to not
722 if ([self isPinningDisabled:resultName]) {
723 return @{ (__bridge NSString*)kSecPinningDbKeyRules:@[@{}],
724 (__bridge NSString*)kSecPinningDbKeyPolicyName:resultName};
727 return @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
728 (__bridge NSString*)kSecPinningDbKeyPolicyName:resultName};
733 - (NSDictionary * _Nullable) queryForPolicyName:(NSString *)policyName {
734 if (!_queue) { (void)[self init]; }
735 if (!_db) { [self initializedDb]; }
737 /* Skip the "sslServer" policyName, which is not a pinning policy */
738 if ([policyName isEqualToString:@"sslServer"]) {
742 /* Check for general no-pinning setting */
743 if ([self isPinningDisabled:nil] || [self isPinningDisabled:policyName]) {
747 secinfo("SecPinningDb", "Fetching rules for policy named %@", policyName);
750 __block bool ok = true;
751 __block CFErrorRef error = NULL;
752 __block NSMutableArray *resultRules = [NSMutableArray array];
753 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
754 ok &= SecDbWithSQL(dbconn, selectPolicyNameSQL, &error, ^bool(sqlite3_stmt *selectPolicyName) {
755 ok &= SecDbBindText(selectPolicyName, 1, [policyName UTF8String], [policyName length], SQLITE_TRANSIENT, &error);
756 ok &= SecDbStep(dbconn, selectPolicyName, &error, ^(bool *stop) {
758 secinfo("SecPinningDb", "found matching rule for %@ policy", policyName);
760 /* Deserialize the policies and return */
761 NSData *xmlPolicies = [NSData dataWithBytes:sqlite3_column_blob(selectPolicyName, 0) length:sqlite3_column_bytes(selectPolicyName, 0)];
762 if (!xmlPolicies) { return; }
763 id policies = [NSPropertyListSerialization propertyListWithData:xmlPolicies options:0 format:nil error:nil];
764 if (!isNSArray(policies)) {
767 [resultRules addObjectsFromArray:(NSArray *)policies];
775 secerror("SecPinningDb: error querying DB for policyName: %@", error);
776 #if ENABLE_TRUSTD_ANALYTICS
777 [[TrustAnalytics logger] logHardError:(__bridge NSError *)error
778 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
779 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
780 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationRead)}];
781 #endif // ENABLE_TRUSTD_ANALYTICS
782 CFReleaseNull(error);
785 if ([resultRules count] > 0) {
786 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
787 (__bridge NSString*)kSecPinningDbKeyPolicyName:policyName};
796 static SecPinningDb *pinningDb = nil;
797 void SecPinningDbInitialize(void) {
798 /* Create the pinning object once per launch */
799 static dispatch_once_t onceToken;
800 dispatch_once(&onceToken, ^{
802 pinningDb = [[SecPinningDb alloc] init];
803 __block CFErrorRef error = NULL;
804 BOOL ok = SecDbPerformRead([pinningDb db], &error, ^(SecDbConnectionRef dbconn) {
805 NSNumber *contentVersion = [pinningDb getContentVersion:dbconn error:&error];
806 NSNumber *schemaVersion = [pinningDb getSchemaVersion:dbconn error:&error];
807 secinfo("pinningDb", "Database Schema: %@ Content: %@", schemaVersion, contentVersion);
810 secerror("SecPinningDb: unable to initialize db: %@", error);
811 #if ENABLE_TRUSTD_ANALYTICS
812 [[TrustAnalytics logger] logHardError:(__bridge NSError *)error
813 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
814 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
815 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationRead)}];
816 #endif // ENABLE_TRUSTD_ANALYTICS
818 CFReleaseNull(error);
823 CFDictionaryRef _Nullable SecPinningDbCopyMatching(CFDictionaryRef query) {
825 SecPinningDbInitialize();
826 NSDictionary *nsQuery = (__bridge NSDictionary*)query;
828 /* prefer rules queried by policy name */
829 NSString *policyName = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyPolicyName];
830 NSDictionary *results = [pinningDb queryForPolicyName:policyName];
832 return CFBridgingRetain(results);
835 /* then rules queried by hostname */
836 NSString *hostname = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyHostname];
837 results = [pinningDb queryForDomain:hostname];
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);