]> git.saurik.com Git - apple/security.git/blob - OSX/sec/securityd/SecPinningDb.m
Security-58286.60.28.tar.gz
[apple/security.git] / OSX / sec / securityd / SecPinningDb.m
1 /*
2 * Copyright (c) 2016-2018 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 *
23 */
24
25 /*
26 * SecPinningDb.m
27 */
28
29 #include <AssertMacros.h>
30 #import <Foundation/Foundation.h>
31 #import <sys/stat.h>
32 #import <notify.h>
33
34 #if !TARGET_OS_BRIDGE
35 #import <MobileAsset/MAAsset.h>
36 #import <MobileAsset/MAAssetQuery.h>
37 #endif
38
39 #if TARGET_OS_OSX
40 #import <MobileAsset/MobileAsset.h>
41 #include <sys/csr.h>
42 #endif
43
44 #import <Security/SecInternalReleasePriv.h>
45
46 #import <securityd/OTATrustUtilities.h>
47 #import <securityd/SecPinningDb.h>
48 #import <securityd/SecTrustLoggingServer.h>
49
50 #include "utilities/debugging.h"
51 #include "utilities/sqlutils.h"
52 #include "utilities/iOSforOSX.h"
53 #include <utilities/SecCFError.h>
54 #include <utilities/SecCFRelease.h>
55 #include <utilities/SecCFWrappers.h>
56 #include <utilities/SecDb.h>
57 #include <utilities/SecFileLocations.h>
58 #include "utilities/sec_action.h"
59
60 #define kSecPinningBasePath "/Library/Keychains/"
61 #define kSecPinningDbFileName "pinningrules.sqlite3"
62
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 */
69
70 const CFStringRef kSecPinningDbKeyHostname = CFSTR("PinningHostname");
71 const CFStringRef kSecPinningDbKeyPolicyName = CFSTR("PinningPolicyName");
72 const CFStringRef kSecPinningDbKeyRules = CFSTR("PinningRules");
73
74 @interface SecPinningDb : NSObject
75 @property (assign) SecDbRef db;
76 @property dispatch_queue_t queue;
77 @property NSURL *dbPath;
78 - (instancetype) init;
79 - ( NSDictionary * _Nullable ) queryForDomain:(NSString *)domain;
80 - ( NSDictionary * _Nullable ) queryForPolicyName:(NSString *)policyName;
81 @end
82
83 static inline bool isNSNumber(id nsType) {
84 return nsType && [nsType isKindOfClass:[NSNumber class]];
85 }
86
87 static inline bool isNSArray(id nsType) {
88 return nsType && [nsType isKindOfClass:[NSArray class]];
89 }
90
91 static inline bool isNSDictionary(id nsType) {
92 return nsType && [nsType isKindOfClass:[NSDictionary class]];
93 }
94
95 @implementation SecPinningDb
96 #define getSchemaVersionSQL CFSTR("PRAGMA user_version")
97 #define selectVersionSQL CFSTR("SELECT ival FROM admin WHERE key='version'")
98 #define insertAdminSQL CFSTR("INSERT OR REPLACE INTO admin (key,ival,value) VALUES (?,?,?)")
99 #define selectDomainSQL CFSTR("SELECT DISTINCT labelRegex,policyName,policies FROM rules WHERE domainSuffix=?")
100 #define selectPolicyNameSQL CFSTR("SELECT DISTINCT policies FROM rules WHERE policyName=?")
101 #define insertRuleSQL CFSTR("INSERT OR REPLACE INTO rules (policyName,domainSuffix,labelRegex,policies) VALUES (?,?,?,?) ")
102 #define removeAllRulesSQL CFSTR("DELETE FROM rules;")
103
104 - (NSNumber *)getSchemaVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
105 __block bool ok = true;
106 __block NSNumber *version = nil;
107 ok &= SecDbWithSQL(dbconn, getSchemaVersionSQL, error, ^bool(sqlite3_stmt *selectVersion) {
108 ok &= SecDbStep(dbconn, selectVersion, error, ^(bool *stop) {
109 int ival = sqlite3_column_int(selectVersion, 0);
110 version = [NSNumber numberWithInt:ival];
111 });
112 return ok;
113 });
114 return version;
115 }
116
117 - (BOOL)setSchemaVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
118 bool ok = true;
119 NSString *setVersion = [NSString stringWithFormat:@"PRAGMA user_version = %llu", PinningDbSchemaVersion];
120 ok &= SecDbExec(dbconn,
121 (__bridge CFStringRef)setVersion,
122 error);
123 if (!ok) {
124 secerror("SecPinningDb: failed to create admin table: %@", error ? *error : nil);
125 }
126 return ok;
127 }
128
129 - (NSNumber *)getContentVersion:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
130 __block bool ok = true;
131 __block NSNumber *version = nil;
132 ok &= SecDbWithSQL(dbconn, selectVersionSQL, error, ^bool(sqlite3_stmt *selectVersion) {
133 ok &= SecDbStep(dbconn, selectVersion, error, ^(bool *stop) {
134 uint64_t ival = sqlite3_column_int64(selectVersion, 0);
135 version = [NSNumber numberWithUnsignedLongLong:ival];
136 });
137 return ok;
138 });
139 return version;
140 }
141
142 - (BOOL)setContentVersion:(NSNumber *)version dbConnection:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
143 __block BOOL ok = true;
144 ok &= SecDbWithSQL(dbconn, insertAdminSQL, error, ^bool(sqlite3_stmt *insertAdmin) {
145 const char *versionKey = "version";
146 ok &= SecDbBindText(insertAdmin, 1, versionKey, strlen(versionKey), SQLITE_TRANSIENT, error);
147 ok &= SecDbBindInt64(insertAdmin, 2, [version unsignedLongLongValue], error);
148 ok &= SecDbStep(dbconn, insertAdmin, error, NULL);
149 return ok;
150 });
151 if (!ok) {
152 secerror("SecPinningDb: failed to set version %@ from pinning list: %@", version, error ? *error : nil);
153 }
154 return ok;
155 }
156
157 - (BOOL) shouldUpdateContent:(NSNumber *)new_version {
158 __block CFErrorRef error = NULL;
159 __block BOOL ok = YES;
160 __block BOOL newer = NO;
161 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
162 NSNumber *db_version = [self getContentVersion:dbconn error:&error];
163 if (!db_version || [new_version compare:db_version] == NSOrderedDescending) {
164 newer = YES;
165 secnotice("pinningDb", "Pinning database should update from version %@ to version %@", db_version, new_version);
166 }
167 });
168
169 if (!ok || error) {
170 secerror("SecPinningDb: error reading content version from database %@", error);
171 }
172 CFReleaseNull(error);
173 return newer;
174 }
175
176 - (BOOL) insertRuleWithName:(NSString *)policyName
177 domainSuffix:(NSString *)domainSuffix
178 labelRegex:(NSString *)labelRegex
179 policies:(NSArray *)policies
180 dbConnection:(SecDbConnectionRef)dbconn
181 error:(CFErrorRef *)error{
182 /* @@@ This insertion mechanism assumes that the input is trusted -- namely, that the new rules
183 * are allowed to replace existing rules. For third-party inputs, this assumption isn't true. */
184
185 secdebug("pinningDb", "inserting new rule: %@ for %@.%@", policyName, labelRegex, domainSuffix);
186
187 __block bool ok = true;
188 ok &= SecDbWithSQL(dbconn, insertRuleSQL, error, ^bool(sqlite3_stmt *insertRule) {
189 ok &= SecDbBindText(insertRule, 1, [policyName UTF8String], [policyName length], SQLITE_TRANSIENT, error);
190 ok &= SecDbBindText(insertRule, 2, [domainSuffix UTF8String], [domainSuffix length], SQLITE_TRANSIENT, error);
191 ok &= SecDbBindText(insertRule, 3, [labelRegex UTF8String], [labelRegex length], SQLITE_TRANSIENT, error);
192 NSData *xmlPolicies = [NSPropertyListSerialization dataWithPropertyList:policies
193 format:NSPropertyListXMLFormat_v1_0
194 options:0
195 error:nil];
196 if (!xmlPolicies) {
197 secerror("SecPinningDb: failed to serialize policies");
198 ok = false;
199 }
200 ok &= SecDbBindBlob(insertRule, 4, [xmlPolicies bytes], [xmlPolicies length], SQLITE_TRANSIENT, error);
201 ok &= SecDbStep(dbconn, insertRule, error, NULL);
202 return ok;
203 });
204 if (!ok) {
205 secerror("SecPinningDb: failed to insert rule %@ for %@.%@ with error %@", policyName, labelRegex, domainSuffix, error ? *error : nil);
206 }
207 return ok;
208 }
209
210 - (BOOL) populateDbFromBundle:(NSArray *)pinningList dbConnection:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
211 __block BOOL ok = true;
212 [pinningList enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
213 if (idx ==0) { return; } // Skip the first value which is the version
214 if (!isNSDictionary(obj)) {
215 secerror("SecPinningDb: rule entry in pinning plist is wrong class");
216 ok = false;
217 return;
218 }
219 NSDictionary *rule = obj;
220 __block NSString *policyName = [rule objectForKey:PinningDbPolicyNameKey];
221 NSArray *domains = [rule objectForKey:PinningDbDomainsKey];
222 __block NSArray *policies = [rule objectForKey:PinningDbPoliciesKey];
223
224 if (!policyName || !domains || !policies) {
225 secerror("SecPinningDb: failed to get required fields from rule entry %lu", (unsigned long)idx);
226 ok = false;
227 return;
228 }
229
230 [domains enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
231 if (!isNSDictionary(obj)) {
232 secerror("SecPinningDb: domain entry %lu for %@ in pinning rule is wrong class", (unsigned long)idx, policyName);
233 ok = false;
234 return;
235 }
236 NSDictionary *domain = obj;
237 NSString *suffix = [domain objectForKey:PinningDbDomainSuffixKey];
238 NSString *labelRegex = [domain objectForKey:PinningDbLabelRegexKey];
239
240 if (!suffix || !labelRegex) {
241 secerror("SecPinningDb: failed to get required fields for entry %lu for %@", (unsigned long)idx, policyName);
242 ok = false;
243 return;
244 }
245 ok &= [self insertRuleWithName:policyName domainSuffix:suffix labelRegex:labelRegex policies:policies
246 dbConnection:dbconn error:error];
247 }];
248 }];
249 if (!ok) {
250 secerror("SecPinningDb: failed to populate DB from pinning list: %@", error ? *error : nil);
251 }
252 return ok;
253 }
254
255 - (BOOL) removeAllRulesFromDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
256 __block BOOL ok = true;
257 ok &= SecDbWithSQL(dbconn, removeAllRulesSQL, error, ^bool(sqlite3_stmt *deleteRules) {
258 ok &= SecDbStep(dbconn, deleteRules, error, NULL);
259 return ok;
260 });
261 if (!ok) {
262 secerror("SecPinningDb: failed to delete old values: %@", error ? *error :nil);
263 }
264 return ok;
265 }
266
267
268 - (BOOL) createOrAlterAdminTable:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
269 bool ok = true;
270 ok &= SecDbExec(dbconn,
271 CFSTR("CREATE TABLE IF NOT EXISTS admin("
272 "key TEXT PRIMARY KEY NOT NULL,"
273 "ival INTEGER NOT NULL,"
274 "value BLOB"
275 ");"),
276 error);
277 if (!ok) {
278 secerror("SecPinningDb: failed to create admin table: %@", error ? *error : nil);
279 }
280 return ok;
281 }
282
283 - (BOOL) createOrAlterRulesTable:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error {
284 bool ok = true;
285 ok &= SecDbExec(dbconn,
286 CFSTR("CREATE TABLE IF NOT EXISTS rules("
287 "policyName TEXT NOT NULL,"
288 "domainSuffix TEXT NOT NULL,"
289 "labelRegex TEXT NOT NULL,"
290 "policies BLOB NOT NULL,"
291 "UNIQUE(policyName, domainSuffix, labelRegex)"
292 ");"),
293 error);
294 ok &= SecDbExec(dbconn, CFSTR("CREATE INDEX IF NOT EXISTS idomain ON rules(domainSuffix);"), error);
295 ok &= SecDbExec(dbconn, CFSTR("CREATE INDEX IF NOT EXISTS ipolicy ON rules(policyName);"), error);
296 if (!ok) {
297 secerror("SecPinningDb: failed to create rules table: %@", error ? *error : nil);
298 }
299 return ok;
300 }
301
302 #if !TARGET_OS_BRIDGE
303 - (BOOL) installDbFromURL:(NSURL *)localURL {
304 if (!localURL) {
305 secerror("SecPinningDb: missing url for downloaded asset");
306 return NO;
307 }
308 NSURL *fileLoc = [NSURL URLWithString:@"CertificatePinning.plist"
309 relativeToURL:localURL];
310 __block NSArray *pinningList = [NSArray arrayWithContentsOfURL:fileLoc];
311 if (!pinningList) {
312 secerror("SecPinningDb: unable to create pinning list from asset file: %@", fileLoc);
313 return NO;
314 }
315
316 NSNumber *plist_version = [pinningList objectAtIndex:0];
317 if (![self shouldUpdateContent:plist_version]) {
318 /* We got a new plist but we already have that version installed. */
319 return YES;
320 }
321
322 /* Update Content */
323 __block CFErrorRef error = NULL;
324 __block BOOL ok = YES;
325 dispatch_sync(self->_queue, ^{
326 ok &= SecDbPerformWrite(self->_db, &error, ^(SecDbConnectionRef dbconn) {
327 ok &= [self updateDb:dbconn error:&error pinningList:pinningList updateSchema:NO updateContent:YES];
328 });
329 });
330
331 if (!ok || error) {
332 secerror("SecPinningDb: error installing updated pinning list version %@: %@", [pinningList objectAtIndex:0], error);
333 #if ENABLE_TRUSTD_ANALYTICS
334 [[TrustdHealthAnalytics logger] logHardError:(__bridge NSError *)error
335 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
336 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
337 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationWrite) }];
338 #endif // ENABLE_TRUSTD_ANALYTICS
339 CFReleaseNull(error);
340 }
341
342 return ok;
343 }
344 #endif /* !TARGET_OS_BRIDGE */
345
346 - (NSArray *) copySystemPinningList {
347 NSArray *pinningList = nil;
348 NSURL *pinningListURL = nil;
349 /* Get the pinning list shipped with the OS */
350 SecOTAPKIRef otapkiref = SecOTAPKICopyCurrentOTAPKIRef();
351 if (otapkiref) {
352 pinningListURL = CFBridgingRelease(SecOTAPKICopyPinningList(otapkiref));
353 CFReleaseNull(otapkiref);
354 if (!pinningListURL) {
355 secerror("SecPinningDb: failed to get pinning plist URL");
356 }
357 NSError *error = nil;
358 pinningList = [NSArray arrayWithContentsOfURL:pinningListURL error:&error];
359 if (!pinningList) {
360 secerror("SecPinningDb: failed to read pinning plist from bundle: %@", error);
361 }
362 }
363
364 return pinningList;
365 }
366
367 - (BOOL) updateDb:(SecDbConnectionRef)dbconn error:(CFErrorRef *)error pinningList:(NSArray *)pinningList
368 updateSchema:(BOOL)updateSchema updateContent:(BOOL)updateContent
369 {
370 if (!SecOTAPKIIsSystemTrustd()) { return false; }
371 secdebug("pinningDb", "updating or creating database");
372
373 __block bool ok = true;
374 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, error, ^(bool *commit) {
375 if (updateSchema) {
376 /* update the tables */
377 ok &= [self createOrAlterAdminTable:dbconn error:error];
378 ok &= [self createOrAlterRulesTable:dbconn error:error];
379 ok &= [self setSchemaVersion:dbconn error:error];
380 }
381
382 if (updateContent) {
383 /* remove the old data */
384 /* @@@ This behavior assumes that we have all the rules we want to populate
385 * elsewhere on disk and that the DB doesn't contain the sole copy of that data. */
386 ok &= [self removeAllRulesFromDb:dbconn error:error];
387
388 /* read the new data */
389 NSNumber *version = [pinningList objectAtIndex:0];
390
391 /* populate the tables */
392 ok &= [self populateDbFromBundle:pinningList dbConnection:dbconn error:error];
393 ok &= [self setContentVersion:version dbConnection:dbconn error:error];
394 }
395
396 *commit = ok;
397 });
398
399 return ok;
400 }
401
402 - (SecDbRef) createAtPath {
403 bool readWrite = SecOTAPKIIsSystemTrustd();
404 #if TARGET_OS_OSX
405 mode_t mode = 0644; // Root trustd can rw. All other trustds need to read.
406 #else
407 mode_t mode = 0600; // Only one trustd.
408 #endif
409
410 CFStringRef path = CFStringCreateWithCString(NULL, [_dbPath fileSystemRepresentation], kCFStringEncodingUTF8);
411 SecDbRef result = SecDbCreateWithOptions(path, mode, readWrite, readWrite, false,
412 ^bool (SecDbRef db, SecDbConnectionRef dbconn, bool didCreate, bool *callMeAgainForNextConnection, CFErrorRef *error) {
413 if (!SecOTAPKIIsSystemTrustd()) {
414 /* Non-owner process can't update the db, but it should get a db connection.
415 * @@@ Revisit if new schema version is needed by reader processes. */
416 return true;
417 }
418
419 __block BOOL ok = true;
420 dispatch_sync(self->_queue, ^{
421 bool updateSchema = false;
422 bool updateContent = false;
423
424 /* Get the pinning plist */
425 NSArray *pinningList = [self copySystemPinningList];
426 if (!pinningList) {
427 secerror("SecPinningDb: failed to find pinning plist in bundle");
428 ok = false;
429 return;
430 }
431
432 /* Check latest data and schema versions against existing table. */
433 if (!isNSNumber([pinningList objectAtIndex:0])) {
434 secerror("SecPinningDb: pinning plist in wrong format");
435 return; // Don't change status. We can continue to use old DB.
436 }
437 NSNumber *plist_version = [pinningList objectAtIndex:0];
438 NSNumber *db_version = [self getContentVersion:dbconn error:error];
439 secnotice("pinningDb", "Opening db with version %@", db_version);
440 if (!db_version || [plist_version compare:db_version] == NSOrderedDescending) {
441 secnotice("pinningDb", "Updating pinning database content from version %@ to version %@",
442 db_version ? db_version : 0, plist_version);
443 updateContent = true;
444 }
445 NSNumber *schema_version = [self getSchemaVersion:dbconn error:error];
446 NSNumber *current_version = [NSNumber numberWithUnsignedLongLong:PinningDbSchemaVersion];
447 if (!schema_version || ![schema_version isEqualToNumber:current_version]) {
448 secnotice("pinningDb", "Updating pinning database schema from version %@ to version %@",
449 schema_version, current_version);
450 updateSchema = true;
451 }
452
453 if (updateContent || updateSchema) {
454 ok &= [self updateDb:dbconn error:error pinningList:pinningList updateSchema:updateSchema updateContent:updateContent];
455 /* Since we updated the DB to match the list that shipped with the system,
456 * reset the OTAPKI Asset version to the system asset version */
457 (void)SecOTAPKIResetCurrentAssetVersion(NULL);
458 }
459 if (!ok) {
460 secerror("SecPinningDb: %s failed: %@", didCreate ? "Create" : "Open", error ? *error : NULL);
461 #if ENABLE_TRUSTD_ANALYTICS
462 [[TrustdHealthAnalytics logger] logHardError:(error ? (__bridge NSError *)*error : nil)
463 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
464 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
465 TrustdHealthAnalyticsAttributeDatabaseOperation : didCreate ? @(TAOperationCreate) : @(TAOperationOpen)}];
466 #endif // ENABLE_TRUSTD_ANALYTICS
467 }
468 });
469 return ok;
470 });
471
472 CFReleaseNull(path);
473 return result;
474 }
475
476 static void verify_create_path(const char *path)
477 {
478 int ret = mkpath_np(path, 0755);
479 if (!(ret == 0 || ret == EEXIST)) {
480 secerror("could not create path: %s (%s)", path, strerror(ret));
481 }
482 }
483
484 - (NSURL *)pinningDbPath {
485 /* Make sure the /Library/Keychains directory is there */
486 #if TARGET_OS_IPHONE
487 NSURL *directory = CFBridgingRelease(SecCopyURLForFileInKeychainDirectory(nil));
488 #else
489 NSURL *directory = [NSURL fileURLWithFileSystemRepresentation:"/Library/Keychains/" isDirectory:YES relativeToURL:nil];
490 #endif
491 verify_create_path([directory fileSystemRepresentation]);
492
493 /* Get the full path of the pinning DB */
494 return [directory URLByAppendingPathComponent:@"pinningrules.sqlite3"];
495 }
496
497 - (void) initializedDb {
498 dispatch_sync(_queue, ^{
499 if (!self->_db) {
500 self->_dbPath = [self pinningDbPath];
501 self->_db = [self createAtPath];
502 }
503 });
504 }
505
506 - (instancetype) init {
507 if (self = [super init]) {
508 _queue = dispatch_queue_create("Pinning DB Queue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
509 [self initializedDb];
510 }
511 return self;
512 }
513
514 - (void) dealloc {
515 CFReleaseNull(_db);
516 }
517
518 - (BOOL) isPinningDisabled:(NSString * _Nullable)policy {
519 static dispatch_once_t once;
520 static sec_action_t action;
521
522 BOOL pinningDisabled = NO;
523 if (SecIsInternalRelease()) {
524 NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.security"];
525 pinningDisabled = [defaults boolForKey:@"AppleServerAuthenticationNoPinning"];
526 if (!pinningDisabled && policy) {
527 NSMutableString *policySpecificKey = [NSMutableString stringWithString:@"AppleServerAuthenticationNoPinning"];
528 [policySpecificKey appendString:policy];
529 pinningDisabled = [defaults boolForKey:policySpecificKey];
530 secinfo("pinningQA", "%@ disable pinning = %d", policy, pinningDisabled);
531 }
532 }
533
534
535 dispatch_once(&once, ^{
536 /* Only log system-wide pinning status once every five minutes */
537 action = sec_action_create("pinning logging charles", 5*60.0);
538 sec_action_set_handler(action, ^{
539 if (!SecIsInternalRelease()) {
540 secnotice("pinningQA", "could not disable pinning: not an internal release");
541 } else {
542 NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.apple.security"];
543 secnotice("pinningQA", "generic pinning disable = %d", [defaults boolForKey:@"AppleServerAuthenticationNoPinning"]);
544 }
545 });
546 });
547 sec_action_perform(action);
548
549 return pinningDisabled;
550 }
551
552 - ( NSDictionary * _Nullable ) queryForDomain:(NSString *)domain {
553 if (!_queue) { (void)[self init]; }
554 if (!_db) { [self initializedDb]; }
555
556 /* Check for general no-pinning setting */
557 if ([self isPinningDisabled:nil]) {
558 return nil;
559 }
560
561 /* parse the domain into suffix and 1st label */
562 NSRange firstDot = [domain rangeOfString:@"."];
563 if (firstDot.location == NSNotFound) { return nil; } // Probably not a legitimate domain name
564 __block NSString *firstLabel = [domain substringToIndex:firstDot.location];
565 __block NSString *suffix = [domain substringFromIndex:(firstDot.location + 1)];
566
567 /* Perform SELECT */
568 __block bool ok = true;
569 __block CFErrorRef error = NULL;
570 __block NSMutableArray *resultRules = [NSMutableArray array];
571 __block NSString *resultName = nil;
572 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
573 ok &= SecDbWithSQL(dbconn, selectDomainSQL, &error, ^bool(sqlite3_stmt *selectDomain) {
574 ok &= SecDbBindText(selectDomain, 1, [suffix UTF8String], [suffix length], SQLITE_TRANSIENT, &error);
575 ok &= SecDbStep(dbconn, selectDomain, &error, ^(bool *stop) {
576 /* Match the labelRegex */
577 const uint8_t *regex = sqlite3_column_text(selectDomain, 0);
578 if (!regex) { return; }
579 NSString *regexStr = [NSString stringWithUTF8String:(const char *)regex];
580 if (!regexStr) { return; }
581 NSRegularExpression *regularExpression = [NSRegularExpression regularExpressionWithPattern:regexStr
582 options:NSRegularExpressionCaseInsensitive
583 error:nil];
584 if (!regularExpression) { return; }
585 NSUInteger numMatches = [regularExpression numberOfMatchesInString:firstLabel
586 options:0
587 range:NSMakeRange(0, [firstLabel length])];
588 if (numMatches == 0) {
589 return;
590 }
591 secdebug("SecPinningDb", "found matching rule for %@.%@", firstLabel, suffix);
592
593 /* Check the policyName for no-pinning settings */
594 const uint8_t *policyName = sqlite3_column_text(selectDomain, 1);
595 NSString *policyNameStr = [NSString stringWithUTF8String:(const char *)policyName];
596 if ([self isPinningDisabled:policyNameStr]) {
597 return;
598 }
599
600 /* Deserialize the policies and return.
601 * @@@ Assumes there is only one rule with matching suffix/label pairs. */
602 NSData *xmlPolicies = [NSData dataWithBytes:sqlite3_column_blob(selectDomain, 2) length:sqlite3_column_bytes(selectDomain, 2)];
603 if (!xmlPolicies) { return; }
604 id policies = [NSPropertyListSerialization propertyListWithData:xmlPolicies options:0 format:nil error:nil];
605 if (!isNSArray(policies)) {
606 return;
607 }
608 [resultRules addObjectsFromArray:(NSArray *)policies];
609 resultName = policyNameStr;
610 });
611 return ok;
612 });
613 });
614
615 if (!ok || error) {
616 secerror("SecPinningDb: error querying DB for hostname: %@", error);
617 #if ENABLE_TRUSTD_ANALYTICS
618 [[TrustdHealthAnalytics logger] logHardError:(__bridge NSError *)error
619 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
620 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
621 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationRead)}];
622 #endif // ENABLE_TRUSTD_ANALYTICS
623 CFReleaseNull(error);
624 }
625
626 if ([resultRules count] > 0) {
627 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
628 (__bridge NSString*)kSecPinningDbKeyPolicyName:resultName};
629 return results;
630 }
631 return nil;
632 }
633
634 - (NSDictionary * _Nullable) queryForPolicyName:(NSString *)policyName {
635 if (!_queue) { (void)[self init]; }
636 if (!_db) { [self initializedDb]; }
637
638 /* Skip the "sslServer" policyName, which is not a pinning policy */
639 if ([policyName isEqualToString:@"sslServer"]) {
640 return nil;
641 }
642
643 /* Check for general no-pinning setting */
644 if ([self isPinningDisabled:nil] || [self isPinningDisabled:policyName]) {
645 return nil;
646 }
647
648 secinfo("SecPinningDb", "Fetching rules for policy named %@", policyName);
649
650 /* Perform SELECT */
651 __block bool ok = true;
652 __block CFErrorRef error = NULL;
653 __block NSMutableArray *resultRules = [NSMutableArray array];
654 ok &= SecDbPerformRead(_db, &error, ^(SecDbConnectionRef dbconn) {
655 ok &= SecDbWithSQL(dbconn, selectPolicyNameSQL, &error, ^bool(sqlite3_stmt *selectPolicyName) {
656 ok &= SecDbBindText(selectPolicyName, 1, [policyName UTF8String], [policyName length], SQLITE_TRANSIENT, &error);
657 ok &= SecDbStep(dbconn, selectPolicyName, &error, ^(bool *stop) {
658 secdebug("SecPinningDb", "found matching rule for %@ policy", policyName);
659
660 /* Deserialize the policies and return */
661 NSData *xmlPolicies = [NSData dataWithBytes:sqlite3_column_blob(selectPolicyName, 0) length:sqlite3_column_bytes(selectPolicyName, 0)];
662 if (!xmlPolicies) { return; }
663 id policies = [NSPropertyListSerialization propertyListWithData:xmlPolicies options:0 format:nil error:nil];
664 if (!isNSArray(policies)) {
665 return;
666 }
667 [resultRules addObjectsFromArray:(NSArray *)policies];
668 });
669 return ok;
670 });
671 });
672
673 if (!ok || error) {
674 secerror("SecPinningDb: error querying DB for policyName: %@", error);
675 #if ENABLE_TRUSTD_ANALYTICS
676 [[TrustdHealthAnalytics logger] logHardError:(__bridge NSError *)error
677 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
678 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
679 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationRead)}];
680 #endif // ENABLE_TRUSTD_ANALYTICS
681 CFReleaseNull(error);
682 }
683
684 if ([resultRules count] > 0) {
685 NSDictionary *results = @{(__bridge NSString*)kSecPinningDbKeyRules:resultRules,
686 (__bridge NSString*)kSecPinningDbKeyPolicyName:policyName};
687 return results;
688 }
689 return nil;
690 }
691
692 @end
693
694 /* C interfaces */
695 static SecPinningDb *pinningDb = nil;
696 void SecPinningDbInitialize(void) {
697 /* Create the pinning object once per launch */
698 static dispatch_once_t onceToken;
699 dispatch_once(&onceToken, ^{
700 @autoreleasepool {
701 pinningDb = [[SecPinningDb alloc] init];
702 __block CFErrorRef error = NULL;
703 BOOL ok = SecDbPerformRead([pinningDb db], &error, ^(SecDbConnectionRef dbconn) {
704 NSNumber *contentVersion = [pinningDb getContentVersion:dbconn error:&error];
705 NSNumber *schemaVersion = [pinningDb getSchemaVersion:dbconn error:&error];
706 secinfo("pinningDb", "Database Schema: %@ Content: %@", schemaVersion, contentVersion);
707 });
708 if (!ok || error) {
709 secerror("SecPinningDb: unable to initialize db: %@", error);
710 #if ENABLE_TRUSTD_ANALYTICS
711 [[TrustdHealthAnalytics logger] logHardError:(__bridge NSError *)error
712 withEventName:TrustdHealthAnalyticsEventDatabaseEvent
713 withAttributes:@{TrustdHealthAnalyticsAttributeAffectedDatabase : @(TAPinningDb),
714 TrustdHealthAnalyticsAttributeDatabaseOperation : @(TAOperationRead)}];
715 #endif // ENABLE_TRUSTD_ANALYTICS
716 }
717 CFReleaseNull(error);
718 }
719 });
720 }
721
722 CFDictionaryRef _Nullable SecPinningDbCopyMatching(CFDictionaryRef query) {
723 @autoreleasepool {
724 SecPinningDbInitialize();
725
726 NSDictionary *nsQuery = (__bridge NSDictionary*)query;
727 NSString *hostname = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyHostname];
728
729 NSDictionary *results = [pinningDb queryForDomain:hostname];
730 if (results) { return CFBridgingRetain(results); }
731 NSString *policyName = [nsQuery objectForKey:(__bridge NSString*)kSecPinningDbKeyPolicyName];
732 results = [pinningDb queryForPolicyName:policyName];
733 if (!results) { return nil; }
734 return CFBridgingRetain(results);
735 }
736 }
737
738 #if !TARGET_OS_BRIDGE
739 bool SecPinningDbUpdateFromURL(CFURLRef url) {
740 SecPinningDbInitialize();
741
742 return [pinningDb installDbFromURL:(__bridge NSURL*)url];
743 }
744 #endif
745
746 CFNumberRef SecPinningDbCopyContentVersion(void) {
747 @autoreleasepool {
748 __block CFErrorRef error = NULL;
749 __block NSNumber *contentVersion = nil;
750 BOOL ok = SecDbPerformRead([pinningDb db], &error, ^(SecDbConnectionRef dbconn) {
751 contentVersion = [pinningDb getContentVersion:dbconn error:&error];
752 });
753 if (!ok || error) {
754 secerror("SecPinningDb: unable to get content version: %@", error);
755 }
756 CFReleaseNull(error);
757 if (!contentVersion) {
758 contentVersion = [NSNumber numberWithInteger:0];
759 }
760 return CFBridgingRetain(contentVersion);
761 }
762 }