2 * Copyright (c) 2016 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>
35 #import <MobileAsset/MAAsset.h>
36 #import <MobileAsset/MAAssetQuery.h>
40 #import <MobileAsset/MobileAsset.h>
43 #import <Security/SecInternalReleasePriv.h>
45 #import <securityd/OTATrustUtilities.h>
46 #import <securityd/SecPinningDb.h>
48 #include "utilities/debugging.h"
49 #include "utilities/sqlutils.h"
50 #include "utilities/iOSforOSX.h"
51 #include <utilities/SecCFError.h>
52 #include <utilities/SecCFRelease.h>
53 #include <utilities/SecCFWrappers.h>
54 #include <utilities/SecDb.h>
55 #include <utilities/SecFileLocations.h>
56 #include "utilities/sec_action.h"
58 #define kSecPinningBasePath "/Library/Keychains/"
59 #define kSecPinningDbFileName "pinningrules.sqlite3"
61 const uint64_t PinningDbSchemaVersion = 2;
62 const NSString *PinningDbPolicyNameKey = @"policyName"; /* key for a string value */
63 const NSString *PinningDbDomainsKey = @"domains"; /* key for an array of dictionaries */
64 const NSString *PinningDbPoliciesKey = @"rules"; /* key for an array of dictionaries */
65 const NSString *PinningDbDomainSuffixKey = @"suffix"; /* key for a string */
66 const NSString *PinningDbLabelRegexKey = @"labelRegex"; /* key for a regex string */
68 const CFStringRef kSecPinningDbKeyHostname = CFSTR("PinningHostname");
69 const CFStringRef kSecPinningDbKeyPolicyName = CFSTR("PinningPolicyName");
70 const CFStringRef kSecPinningDbKeyRules = CFSTR("PinningRules");
73 const NSString *PinningDbMobileAssetType = @"com.apple.MobileAsset.CertificatePinning";
74 #define kSecPinningDbMobileAssetNotification "com.apple.MobileAsset.CertificatePinning.cached-metadata-updated"
78 const NSUInteger PinningDbMobileAssetCompatibilityVersion = 1;
81 @interface SecPinningDb : NSObject
82 @property (assign) SecDbRef db;
83 @property dispatch_queue_t queue;
84 @property NSURL *dbPath;
85 - (instancetype) init;
86 - ( NSDictionary * _Nullable ) queryForDomain:(NSString *)domain;
87 - ( NSDictionary * _Nullable ) queryForPolicyName:(NSString *)policyName;
90 static bool isDbOwner() {
92 // Test app running as securityd
93 #elif TARGET_OS_IPHONE
94 if (getuid() == 64) // _securityd
104 static inline bool isNSNumber(id nsType) {
105 return nsType && [nsType isKindOfClass:[NSNumber class]];
108 static inline bool isNSArray(id nsType) {
109 return nsType && [nsType isKindOfClass:[NSArray class]];
112 static inline bool isNSDictionary(id nsType) {
113 return nsType && [nsType isKindOfClass:[NSDictionary class]];
116 @implementation SecPinningDb
117 #define getSchemaVersionSQL CFSTR("PRAGMA user_version")
118 #define selectVersionSQL CFSTR("SELECT ival FROM admin WHERE key='version'")
119 #define insertAdminSQL CFSTR("INSERT OR REPLACE INTO admin (key,ival,value) VALUES (?,?,?)")
120 #define selectDomainSQL CFSTR("SELECT DISTINCT labelRegex,policyName,policies FROM rules WHERE domainSuffix=?")
121 #define selectPolicyNameSQL CFSTR("SELECT DISTINCT policies FROM rules WHERE policyName=?")
122 #define insertRuleSQL CFSTR("INSERT OR REPLACE INTO rules (policyName,domainSuffix,labelRegex,policies) VALUES (?,?,?,?) ")
123 #define removeAllRulesSQL CFSTR("DELETE FROM rules;")
125 - (NSNumber *)getSchemaVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
126 __block bool ok = true;
127 __block NSNumber *version = nil;
128 ok &= SecDbWithSQL(dbconn, getSchemaVersionSQL, error, ^bool(sqlite3_stmt *selectVersion) {
129 ok &= SecDbStep(dbconn, selectVersion, error, ^(bool *stop) {
130 int ival = sqlite3_column_int(selectVersion, 0);
131 version = [NSNumber numberWithInt:ival];
138 - (BOOL)setSchemaVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
140 NSString *setVersion = [NSString stringWithFormat:@"PRAGMA user_version = %llu", PinningDbSchemaVersion];
141 ok &= SecDbExec(dbconn,
142 (__bridge CFStringRef)setVersion,
145 secerror("SecPinningDb: failed to create admin table: %@", error ? *error : nil);
150 - (NSNumber *)getContentVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
151 __block bool ok = true;
152 __block NSNumber *version = nil;
153 ok &= SecDbWithSQL(dbconn, selectVersionSQL, error, ^bool(sqlite3_stmt *selectVersion) {
154 ok &= SecDbStep(dbconn, selectVersion, error, ^(bool *stop) {
155 uint64_t ival = sqlite3_column_int64(selectVersion, 0);
156 version = [NSNumber numberWithUnsignedLongLong:ival];
163 - (BOOL)setContentVersion:(NSNumber *)version dbConnection:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
164 __block BOOL ok = true;
165 ok &= SecDbWithSQL(dbconn, insertAdminSQL, error, ^bool(sqlite3_stmt *insertAdmin) {
166 const char *versionKey = "version";
167 ok &= SecDbBindText(insertAdmin, 1, versionKey, strlen(versionKey), SQLITE_TRANSIENT, error);
168 ok &= SecDbBindInt64(insertAdmin, 2, [version unsignedLongLongValue], error);
169 ok &= SecDbStep(dbconn, insertAdmin, error, NULL);
173 secerror("SecPinningDb: failed to set version %@ from pinning list: %@", version, error ? *error : nil);
178 - (BOOL) shouldUpdateContent:(NSNumber *)new_version {
179 __block CFErrorRef error = NULL;
180 __block BOOL ok = YES;
181 __block BOOL newer = NO;
182 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
183 NSNumber *db_version = [self getContentVersion:dbconn error:&error];
184 if (!db_version || [new_version compare:db_version] == NSOrderedDescending) {
186 secnotice("pinningDb", "Pinning database should update from version %@ to version %@", db_version, new_version);
191 secerror("SecPinningDb: error reading content version from database %@", error);
193 CFReleaseNull(error);
197 - (BOOL) insertRuleWithName:(NSString *)policyName
198 domainSuffix:(NSString *)domainSuffix
199 labelRegex:(NSString *)labelRegex
200 policies:(NSArray *)policies
201 dbConnection:(SecDbConnectionRef)dbconn
202 error:(CFErrorRef *)error{
203 /* @@@ This insertion mechanism assumes that the input is trusted -- namely, that the new rules
204 * are allowed to replace existing rules. For third-party inputs, this assumption isn't true. */
206 secdebug("pinningDb", "inserting new rule: %@ for %@.%@", policyName, labelRegex, domainSuffix);
208 __block bool ok = true;
209 ok &= SecDbWithSQL(dbconn, insertRuleSQL, error, ^bool(sqlite3_stmt *insertRule) {
210 ok &= SecDbBindText(insertRule, 1, [policyName UTF8String], [policyName length], SQLITE_TRANSIENT, error);
211 ok &= SecDbBindText(insertRule, 2, [domainSuffix UTF8String], [domainSuffix length], SQLITE_TRANSIENT, error);
212 ok &= SecDbBindText(insertRule, 3, [labelRegex UTF8String], [labelRegex length], SQLITE_TRANSIENT, error);
213 NSData *xmlPolicies = [NSPropertyListSerialization dataWithPropertyList:policies
214 format:NSPropertyListXMLFormat_v1_0
218 secerror("SecPinningDb: failed to serialize policies");
221 ok &= SecDbBindBlob(insertRule, 4, [xmlPolicies bytes], [xmlPolicies length], SQLITE_TRANSIENT, error);
222 ok &= SecDbStep(dbconn, insertRule, error, NULL);
226 secerror("SecPinningDb: failed to insert rule %@ for %@.%@ with error %@", policyName, labelRegex, domainSuffix, error ? *error : nil);
231 - (BOOL) populateDbFromBundle:(NSArray *)pinningList dbConnection:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
232 __block BOOL ok = true;
233 [pinningList enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
234 if (idx ==0) { return; } // Skip the first value which is the version
235 if (!isNSDictionary(obj)) {
236 secerror("SecPinningDb: rule entry in pinning plist is wrong class");
240 NSDictionary *rule = obj;
241 __block NSString *policyName = [rule objectForKey:PinningDbPolicyNameKey];
242 NSArray *domains = [rule objectForKey:PinningDbDomainsKey];
243 __block NSArray *policies = [rule objectForKey:PinningDbPoliciesKey];
245 if (!policyName || !domains || !policies) {
246 secerror("SecPinningDb: failed to get required fields from rule entry %lu", (unsigned long)idx);
251 [domains enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
252 if (!isNSDictionary(obj)) {
253 secerror("SecPinningDb: domain entry %lu for %@ in pinning rule is wrong class", (unsigned long)idx, policyName);
257 NSDictionary *domain = obj;
258 NSString *suffix = [domain objectForKey:PinningDbDomainSuffixKey];
259 NSString *labelRegex = [domain objectForKey:PinningDbLabelRegexKey];
261 if (!suffix || !labelRegex) {
262 secerror("SecPinningDb: failed to get required fields for entry %lu for %@", (unsigned long)idx, policyName);
266 ok &= [self insertRuleWithName:policyName domainSuffix:suffix labelRegex:labelRegex policies:policies
267 dbConnection:dbconn error:error];
271 secerror("SecPinningDb: failed to populate DB from pinning list: %@", error ? *error : nil);
276 - (BOOL) removeAllRulesFromDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
277 __block BOOL ok = true;
278 ok &= SecDbWithSQL(dbconn, removeAllRulesSQL, error, ^bool(sqlite3_stmt *deleteRules) {
279 ok &= SecDbStep(dbconn, deleteRules, error, NULL);
283 secerror("SecPinningDb: failed to delete old values: %@", error ? *error :nil);
289 - (BOOL) createOrAlterAdminTable:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
291 ok &= SecDbExec(dbconn,
292 CFSTR("CREATE TABLE IF NOT EXISTS admin("
293 "key TEXT PRIMARY KEY NOT NULL,"
294 "ival INTEGER NOT NULL,"
299 secerror("SecPinningDb: failed to create admin table: %@", error ? *error : nil);
304 - (BOOL) createOrAlterRulesTable:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
306 ok &= SecDbExec(dbconn,
307 CFSTR("CREATE TABLE IF NOT EXISTS rules("
308 "policyName TEXT NOT NULL,"
309 "domainSuffix TEXT NOT NULL,"
310 "labelRegex TEXT NOT NULL,"
311 "policies BLOB NOT NULL,"
312 "UNIQUE(policyName, domainSuffix, labelRegex)"
315 ok &= SecDbExec(dbconn, CFSTR("CREATE INDEX IF NOT EXISTS idomain ON rules(domainSuffix);"), error);
316 ok &= SecDbExec(dbconn, CFSTR("CREATE INDEX IF NOT EXISTS ipolicy ON rules(policyName);"), error);
318 secerror("SecPinningDb: failed to create rules table: %@", error ? *error : nil);
323 #if !TARGET_OS_BRIDGE
324 - (BOOL) installDbFromURL:(NSURL *)localURL {
326 secerror("SecPinningDb: missing url for downloaded asset");
329 NSURL* basePath = nil, *fileLoc = nil;
330 if (![localURL scheme]) {
331 /* MobileAsset provides the URL without the scheme. Fix it up. */
332 NSString *pathWithScheme = [[NSString alloc] initWithFormat:@"%@",localURL];
333 basePath = [NSURL fileURLWithPath:pathWithScheme isDirectory:YES];
337 fileLoc = [NSURL URLWithString:@"CertificatePinning.plist"
338 relativeToURL:basePath];
339 __block NSArray *pinningList = [NSArray arrayWithContentsOfURL:fileLoc];
341 secerror("SecPinningDb: unable to create pinning list from asset file: %@", fileLoc);
345 NSNumber *plist_version = [pinningList objectAtIndex:0];
346 if (![self shouldUpdateContent:plist_version]) {
347 /* We got a new plist but we already have that version installed. */
352 __block CFErrorRef error = NULL;
353 __block BOOL ok = YES;
354 ok &= SecDbPerformWrite(_db, &error, ^(SecDbConnectionRef dbconn) {
355 ok &= [self updateDb:dbconn error:&error pinningList:pinningList updateSchema:NO updateContent:YES];
359 secerror("SecPinningDb: error installing updated pinning list version %@: %@", [pinningList objectAtIndex:0], error);
360 CFReleaseNull(error);
367 const CFStringRef kSecSUPrefDomain = CFSTR("com.apple.SoftwareUpdate");
368 const CFStringRef kSecSUScanPrefConfigDataInstallKey = CFSTR("ConfigDataInstall");
371 static BOOL PinningDbCanCheckMobileAsset(void) {
374 /* Check the user's SU preferences to determine if "Install system data files" is off */
375 if (!CFPreferencesSynchronize(kSecSUPrefDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost)) {
376 secerror("SecPinningDb: unable to synchronize SoftwareUpdate prefs");
381 if (CFPreferencesAppValueIsForced(kSecSUScanPrefConfigDataInstallKey, kSecSUPrefDomain)) {
382 value = CFBridgingRelease(CFPreferencesCopyAppValue(kSecSUScanPrefConfigDataInstallKey, kSecSUPrefDomain));
384 value = CFBridgingRelease(CFPreferencesCopyValue(kSecSUScanPrefConfigDataInstallKey, kSecSUPrefDomain,
385 kCFPreferencesAnyUser, kCFPreferencesCurrentHost));
387 if (isNSNumber(value)) {
388 result = [value boolValue];
391 if (!result) { secnotice("pinningDb", "User has disabled system data installation."); }
393 /* MobileAsset.framework isn't mastered into the BaseSystem. Check that the MA classes are linked. */
394 if (![ASAssetQuery class] || ![ASAsset class] || ![MAAssetQuery class] || ![MAAsset class]) {
395 secnotice("PinningDb", "Weak linked MobileAsset framework missing.");
403 - (void) downloadPinningAsset:(BOOL __unused)isLocalOnly {
404 if (!PinningDbCanCheckMobileAsset()) {
405 secnotice("pinningDb", "MobileAsset disabled, skipping check.");
409 secnotice("pinningDb", "begin MobileAsset query for catalog");
410 [MAAsset startCatalogDownload:(NSString *)PinningDbMobileAssetType then:^(MADownLoadResult result) {
411 if (result != MADownloadSucceesful) {
412 secerror("SecPinningDb: failed to download catalog: %ld", (long)result);
415 MAAssetQuery *query = [[MAAssetQuery alloc] initWithType:(NSString *)PinningDbMobileAssetType];
416 [query augmentResultsWithState:true];
418 secnotice("pinningDb", "begin MobileAsset metadata sync request");
419 MAQueryResult queryResult = [query queryMetaDataSync];
420 if (queryResult != MAQuerySucceesful) {
421 secerror("SecPinningDb: failed to query MobileAsset metadata: %ld", (long)queryResult);
425 if (!query.results) {
426 secerror("SecPinningDb: no results in MobileAsset query");
430 for (MAAsset *asset in query.results) {
431 NSNumber *asset_version = [asset assetProperty:@"_ContentVersion"];
432 if (![self shouldUpdateContent:asset_version]) {
433 secdebug("pinningDb", "skipping asset because we already have _ContentVersion %@", asset_version);
436 switch(asset.state) {
438 secerror("SecPinningDb: unknown asset state %ld", (long)asset.state);
441 /* The asset is already in the cache, get it from disk. */
442 secdebug("pinningDb", "CertificatePinning asset already installed");
443 if([self installDbFromURL:[asset getLocalUrl]]) {
444 secnotice("pinningDb", "finished db update from installed asset. purging asset.");
445 [asset purge:^(MAPurgeResult purge_result) {
446 if (purge_result != MAPurgeSucceeded) {
447 secerror("SecPinningDb: purge failed: %ld", (long)purge_result);
453 secerror("SecPinningDb: pinning asset is unknown");
456 secnotice("pinningDb", "pinning asset is downloading");
459 secnotice("pinningDb", "begin download of CertificatePinning asset");
460 [asset startDownload:^(MADownLoadResult downloadResult) {
461 if (downloadResult != MADownloadSucceesful) {
462 secerror("SecPinningDb: failed to download pinning asset: %ld", (long)downloadResult);
465 if([self installDbFromURL:[asset getLocalUrl]]) {
466 secnotice("pinningDb", "finished db update from installed asset. purging asset.");
467 [asset purge:^(MAPurgeResult purge_result) {
468 if (purge_result != MAPurgeSucceeded) {
469 secerror("SecPinningDb: purge failed: %ld", (long)purge_result);
479 #else /* !TARGET_OS_IPHONE */
480 /* <rdar://problem/30879827> MobileAssetV2 fails on macOS, so use V1 */
481 - (void) downloadPinningAsset:(BOOL)isLocalOnly {
482 if (!PinningDbCanCheckMobileAsset()) {
483 secnotice("pinningDb", "MobileAsset disabled, skipping check.");
487 ASAssetQuery *query = [[ASAssetQuery alloc] initWithAssetType:(NSString *)PinningDbMobileAssetType];
488 [query setQueriesLocalAssetInformationOnly:isLocalOnly]; // Omitting this leads to a notifcation loop.
489 NSError *error = nil;
490 NSArray<ASAsset *>*query_results = [query runQueryAndReturnError:&error];
491 if (!query_results) {
492 secerror("SecPinningDb: asset query failed: %@", error);
496 for (ASAsset *asset in query_results) {
497 NSDictionary *attributes = [asset attributes];
499 NSNumber *compatibilityVersion = [attributes objectForKey:ASAttributeCompatibilityVersion];
500 if (!isNSNumber(compatibilityVersion) ||
501 [compatibilityVersion unsignedIntegerValue] != PinningDbMobileAssetCompatibilityVersion) {
502 secnotice("pinningDb", "Skipping asset with compatibility version %@", compatibilityVersion);
506 NSNumber *contentVersion = [attributes objectForKey:ASAttributeContentVersion];
507 if (!isNSNumber(contentVersion) || ![self shouldUpdateContent:contentVersion]) {
508 secnotice("pinningDb", "Skipping asset with content version %@", contentVersion);
512 ASProgressHandler pinningHandler = ^(NSDictionary *state, NSError *progressError){
514 secerror("SecPinningDb: asset download error: %@", progressError);
519 secerror("SecPinningDb: no asset state in progress handler");
523 NSString *operationState = [state objectForKey:ASStateOperation];
524 secdebug("pinningDb", "Asset state is %@", operationState);
526 if (operationState && [operationState isEqualToString:ASOperationCompleted]) {
527 if ([self installDbFromURL:[asset localURL]]) {
528 secnotice("pinningDb", "finished db update from installed asset. purging asset.");
529 [asset purge:^(NSError *error) {
531 secerror("SecPinningDb: purge failed %@", error);
538 switch ([asset state]) {
539 case ASAssetStateNotPresent:
540 secdebug("pinningDb", "CertificatePinning asset needs to be downloaded");
541 asset.progressHandler= pinningHandler;
542 asset.userInitiatedDownload = YES;
543 [asset beginDownloadWithOptions:@{ASDownloadOptionPriority : ASDownloadPriorityNormal}];
545 case ASAssetStateInstalled:
546 /* The asset is already in the cache, get it from disk. */
547 secdebug("pinningDb", "CertificatePinning asset already installed");
548 if([self installDbFromURL:[asset localURL]]) {
549 secnotice("pinningDb", "finished db update from installed asset. purging asset.");
550 [asset purge:^(NSError *error) {
552 secerror("SecPinningDb: purge failed %@", error);
557 case ASAssetStatePaused:
558 secdebug("pinningDb", "CertificatePinning asset download paused");
559 asset.progressHandler = pinningHandler;
560 asset.userInitiatedDownload = YES;
561 if (![asset resumeDownloadAndReturnError:&error]) {
562 secerror("SecPinningDb: failed to resume download of asset: %@", error);
565 case ASAssetStateDownloading:
566 secdebug("pinningDb", "CertificatePinning asset downloading");
567 asset.progressHandler = pinningHandler;
568 asset.userInitiatedDownload = YES;
571 secerror("SecPinningDb: unhandled asset state %ld", (long)asset.state);
576 #endif /* !TARGET_OS_IPHONE */
578 - (void) downloadPinningAsset {
579 [self downloadPinningAsset:NO];
581 #endif /* !TARGET_OS_BRIDGE */
583 - (NSArray *) copyCurrentPinningList {
584 NSArray *pinningList = nil;
585 /* Get the pinning list shipped with the OS */
586 SecOTAPKIRef otapkiref = SecOTAPKICopyCurrentOTAPKIRef();
588 pinningList = CFBridgingRelease(SecOTAPKICopyPinningList(otapkiref));
589 CFReleaseNull(otapkiref);
591 secerror("SecPinningDb: failed to read pinning plist from bundle");
595 #if !TARGET_OS_BRIDGE
596 /* Asynchronously ask MobileAsset for most recent pinning list. */
597 dispatch_async(_queue, ^{
598 secnotice("pinningDb", "Initial check with MobileAsset for newer pinning asset");
599 [self downloadPinningAsset];
602 /* Register for changes in our asset */
603 if (PinningDbCanCheckMobileAsset()) {
605 notify_register_dispatch(kSecPinningDbMobileAssetNotification, &out_token, self->_queue, ^(int __unused token) {
606 secnotice("pinningDb", "Got a notification about a new pinning asset.");
607 [self downloadPinningAsset:YES];
615 - (BOOL) updateDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error pinningList:(NSArray *)pinningList
616 updateSchema:(BOOL)updateSchema updateContent:(BOOL)updateContent
618 if (!isDbOwner()) { return false; }
619 secdebug("pinningDb", "updating or creating database");
621 __block bool ok = true;
622 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, error, ^(bool *commit) {
624 /* update the tables */
625 ok &= [self createOrAlterAdminTable:dbconn error:error];
626 ok &= [self createOrAlterRulesTable:dbconn error:error];
627 ok &= [self setSchemaVersion:dbconn error:error];
631 /* remove the old data */
632 /* @@@ This behavior assumes that we have all the rules we want to populate
633 * elsewhere on disk and that the DB doesn't contain the sole copy of that data. */
634 ok &= [self removeAllRulesFromDb:dbconn error:error];
636 /* read the new data */
637 NSNumber *version = [pinningList objectAtIndex:0];
639 /* populate the tables */
640 ok &= [self populateDbFromBundle:pinningList dbConnection:dbconn error:error];
641 ok &= [self setContentVersion:version dbConnection:dbconn error:error];
650 - (SecDbRef) createAtPath {
651 bool readWrite = isDbOwner();
654 CFStringRef path = CFStringCreateWithCString(NULL, [_dbPath fileSystemRepresentation], kCFStringEncodingUTF8);
655 SecDbRef result = SecDbCreateWithOptions(path, mode, readWrite, false, false,
656 ^bool (SecDbRef db, SecDbConnectionRef dbconn, bool didCreate, bool *callMeAgainForNextConnection, CFErrorRef *error) {
658 /* Non-owner process can't update the db, but it should get a db connection.
659 * @@@ Revisit if new schema version is needed by reader processes. */
663 __block BOOL ok = true;
664 dispatch_sync(self->_queue, ^{
665 bool updateSchema = false;
666 bool updateContent = false;
668 /* Get the pinning plist */
669 NSArray *pinningList = [self copyCurrentPinningList];
671 secerror("SecPinningDb: failed to find pinning plist in bundle");
676 /* Check latest data and schema versions against existing table. */
677 if (!isNSNumber([pinningList objectAtIndex:0])) {
678 secerror("SecPinningDb: pinning plist in wrong format");
679 return; // Don't change status. We can continue to use old DB.
681 NSNumber *plist_version = [pinningList objectAtIndex:0];
682 NSNumber *db_version = [self getContentVersion:dbconn error:error];
683 if (!db_version || [plist_version compare:db_version] == NSOrderedDescending) {
684 secnotice("pinningDb", "Updating pinning database content from version %@ to version %@",
685 db_version ? db_version : 0, plist_version);
686 updateContent = true;
688 NSNumber *schema_version = [self getSchemaVersion:dbconn error:error];
689 NSNumber *current_version = [NSNumber numberWithUnsignedLongLong:PinningDbSchemaVersion];
690 if (!schema_version || ![schema_version isEqualToNumber:current_version]) {
691 secnotice("pinningDb", "Updating pinning database schema from version %@ to version %@",
692 schema_version, current_version);
696 if (updateContent || updateSchema) {
697 ok &= [self updateDb:dbconn error:error pinningList:pinningList updateSchema:updateSchema updateContent:updateContent];
700 secerror("SecPinningDb: %s failed: %@", didCreate ? "Create" : "Open", error ? *error : NULL);
710 static void verify_create_path(const char *path)
712 int ret = mkpath_np(path, 0755);
713 if (!(ret == 0 || ret == EEXIST)) {
714 secerror("could not create path: %s (%s)", path, strerror(ret));
718 - (NSURL *)pinningDbPath {
719 /* Make sure the /Library/Keychains directory is there */
721 NSURL *directory = CFBridgingRelease(SecCopyURLForFileInKeychainDirectory(nil));
723 NSURL *directory = [NSURL fileURLWithFileSystemRepresentation:"/Library/Keychains/" isDirectory:YES relativeToURL:nil];
725 verify_create_path([directory fileSystemRepresentation]);
727 /* Get the full path of the pinning DB */
728 return [directory URLByAppendingPathComponent:@"pinningrules.sqlite3"];
731 - (void) initializedDb {
732 dispatch_sync(_queue, ^{
734 self->_dbPath = [self pinningDbPath];
735 self->_db = [self createAtPath];
740 - (instancetype) init {
741 if (self = [super init]) {
742 _queue = dispatch_queue_create("Pinning DB Queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
743 [self initializedDb];
752 - (BOOL) isPinningDisabled:(NSString * _Nullable)policy {
753 static dispatch_once_t once;
754 static sec_action_t action;
756 BOOL pinningDisabled = NO;
757 if (SecIsInternalRelease()) {
758 NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.security"];
759 pinningDisabled = [defaults boolForKey:@"AppleServerAuthenticationNoPinning"];
760 if (!pinningDisabled && policy) {
761 NSMutableString *policySpecificKey = [NSMutableString stringWithString:@"AppleServerAuthenticationNoPinning"];
762 [policySpecificKey appendString:policy];
763 pinningDisabled = [defaults boolForKey:policySpecificKey];
764 secinfo("pinningQA", "%@ disable pinning = %d", policy, pinningDisabled);
768 dispatch_once(&once, ^{
769 /* Only log system-wide pinning status once a minute */
770 action = sec_action_create("pinning logging charles", 60.0);
771 sec_action_set_handler(action, ^{
772 if (!SecIsInternalRelease()) {
773 secnotice("pinningQA", "could not disable pinning: not an internal release");
775 NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.security"];
776 secnotice("pinningQA", "generic pinning disable = %d", [defaults boolForKey:@"AppleServerAuthenticationNoPinning"]);
780 sec_action_perform(action);
782 return pinningDisabled;
785 - ( NSDictionary * _Nullable ) queryForDomain:(NSString *)domain {
786 if (!_queue) { (void)[self init]; }
787 if (!_db) { [self initializedDb]; }
789 /* Check for general no-pinning setting */
790 if ([self isPinningDisabled:nil]) {
794 /* parse the domain into suffix and 1st label */
795 NSRange firstDot = [domain rangeOfString:@"."];
796 if (firstDot.location == NSNotFound) { return nil; } // Probably not a legitimate domain name
797 __block NSString *firstLabel = [domain substringToIndex:firstDot.location];
798 __block NSString *suffix = [domain substringFromIndex:(firstDot.location + 1)];
801 __block bool ok = true;
802 __block CFErrorRef error = NULL;
803 __block NSMutableArray *resultRules = [NSMutableArray array];
804 __block NSString *resultName = nil;
805 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
806 ok &= SecDbWithSQL(dbconn, selectDomainSQL, &error, ^bool(sqlite3_stmt *selectDomain) {
807 ok &= SecDbBindText(selectDomain, 1, [suffix UTF8String], [suffix length], SQLITE_TRANSIENT, &error);
808 ok &= SecDbStep(dbconn, selectDomain, &error, ^(bool *stop) {
809 /* Match the labelRegex */
810 const uint8_t *regex = sqlite3_column_text(selectDomain, 0);
811 if (!regex) { return; }
812 NSString *regexStr = [NSString stringWithUTF8String:(const char *)regex];
813 if (!regexStr) { return; }
814 NSRegularExpression *regularExpression = [NSRegularExpression regularExpressionWithPattern:regexStr
815 options:NSRegularExpressionCaseInsensitive
817 if (!regularExpression) { return; }
818 NSUInteger numMatches = [regularExpression numberOfMatchesInString:firstLabel
820 range:NSMakeRange(0, [firstLabel length])];
821 if (numMatches == 0) {
824 secdebug("SecPinningDb", "found matching rule for %@.%@", firstLabel, suffix);
826 /* Check the policyName for no-pinning settings */
827 const uint8_t *policyName = sqlite3_column_text(selectDomain, 1);
828 NSString *policyNameStr = [NSString stringWithUTF8String:(const char *)policyName];
829 if ([self isPinningDisabled:policyNameStr]) {
833 /* Deserialize the policies and return.
834 * @@@ Assumes there is only one rule with matching suffix/label pairs. */
835 NSData *xmlPolicies = [NSData dataWithBytes:sqlite3_column_blob(selectDomain, 2) length:sqlite3_column_bytes(selectDomain, 2)];
836 if (!xmlPolicies) { return; }
837 id policies = [NSPropertyListSerialization propertyListWithData:xmlPolicies options:0 format:nil error:nil];
838 if (!isNSArray(policies)) {
841 [resultRules addObjectsFromArray:(NSArray *)policies];
842 resultName = policyNameStr;
849 secerror("SecPinningDb: error querying DB for hostname: %@", error);
850 CFReleaseNull(error);
853 if ([resultRules count] > 0) {
854 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
855 (__bridge NSString*)kSecPinningDbKeyPolicyName:resultName};
861 - (NSDictionary * _Nullable) queryForPolicyName:(NSString *)policyName {
862 if (!_queue) { (void)[self init]; }
863 if (!_db) { [self initializedDb]; }
865 /* Skip the "sslServer" policyName, which is not a pinning policy */
866 if ([policyName isEqualToString:@"sslServer"]) {
870 /* Check for general no-pinning setting */
871 if ([self isPinningDisabled:nil] || [self isPinningDisabled:policyName]) {
876 __block bool ok = true;
877 __block CFErrorRef error = NULL;
878 __block NSMutableArray *resultRules = [NSMutableArray array];
879 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
880 ok &= SecDbWithSQL(dbconn, selectPolicyNameSQL, &error, ^bool(sqlite3_stmt *selectPolicyName) {
881 ok &= SecDbBindText(selectPolicyName, 1, [policyName UTF8String], [policyName length], SQLITE_TRANSIENT, &error);
882 ok &= SecDbStep(dbconn, selectPolicyName, &error, ^(bool *stop) {
883 secdebug("SecPinningDb", "found matching rule for %@ policy", policyName);
885 /* Deserialize the policies and return */
886 NSData *xmlPolicies = [NSData dataWithBytes:sqlite3_column_blob(selectPolicyName, 0) length:sqlite3_column_bytes(selectPolicyName, 0)];
887 if (!xmlPolicies) { return; }
888 id policies = [NSPropertyListSerialization propertyListWithData:xmlPolicies options:0 format:nil error:nil];
889 if (!isNSArray(policies)) {
892 [resultRules addObjectsFromArray:(NSArray *)policies];
899 secerror("SecPinningDb: error querying DB for policyName: %@", error);
900 CFReleaseNull(error);
903 if ([resultRules count] > 0) {
904 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
905 (__bridge NSString*)kSecPinningDbKeyPolicyName:policyName};
913 static SecPinningDb *pinningDb = nil;
914 void SecPinningDbInitialize(void) {
915 static dispatch_once_t onceToken;
916 dispatch_once(&onceToken, ^{
917 pinningDb = [[SecPinningDb alloc] init];
918 __block CFErrorRef error = NULL;
919 BOOL ok = SecDbPerformRead([pinningDb db], &error, ^(SecDbConnectionRef dbconn) {
920 NSNumber *contentVersion = [pinningDb getContentVersion:dbconn error:&error];
921 NSNumber *schemaVersion = [pinningDb getSchemaVersion:dbconn error:&error];
922 secinfo("pinningDb", "Database Schema: %@ Content: %@", schemaVersion, contentVersion);
925 secerror("SecPinningDb: unable to initialize db: %@", error);
927 CFReleaseNull(error);
931 CFDictionaryRef _Nullable SecPinningDbCopyMatching(CFDictionaryRef query) {
932 SecPinningDbInitialize();
934 NSDictionary *nsQuery = (__bridge NSDictionary*)query;
935 NSString *hostname = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyHostname];
937 NSDictionary *results = [pinningDb queryForDomain:hostname];
938 if (results) { return CFBridgingRetain(results); }
939 NSString *policyName = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyPolicyName];
940 results = [pinningDb queryForPolicyName:policyName];
941 if (!results) { return nil; }
942 return CFBridgingRetain(results);