/ secdxctests / KeychainEntitlementsTest.m
KeychainEntitlementsTest.m
1 /* 2 * Copyright (c) 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 #import <Security/Security.h> 25 #import <Security/SecItemPriv.h> 26 #import <os/feature_private.h> 27 28 #import "KeychainXCTest.h" 29 30 #if USE_KEYSTORE 31 @interface KeychainEntitlementsTest : KeychainXCTest 32 @end 33 34 @implementation KeychainEntitlementsTest 35 36 - (void)testNoEntitlements { 37 NSDictionary *params = @{ (id)kSecUseDataProtectionKeychain: @YES, 38 (id)kSecClass: (id)kSecClassGenericPassword, (id)kSecAttrLabel: @"label", }; 39 40 // Application with no keychain-related entitlements at all, but CopyMatching must work in order to support 41 // backward compatibility with smart-card-enabled macos applications (com.apple.token AG is added automatically in this case). 42 [self setEntitlements:@{} validated:false]; 43 if (os_feature_enabled(CryptoTokenKit, UseTokens)) { 44 #if TARGET_OS_OSX 45 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound); 46 #else 47 // On non-macOS targets, token items must be explicitly enabled, and that requires entitlements. 48 // But since this test has no entitlements, it will always fail with errSecMissingEntitlement. 49 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecMissingEntitlement); 50 #endif 51 } else { 52 // If tokens are not enabled, this situation really means that there is an entitlement problem. 53 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecMissingEntitlement); 54 } 55 56 // However, write access is declined for such application. 57 XCTAssertEqual(SecItemAdd((CFDictionaryRef)params, NULL), errSecMissingEntitlement); 58 XCTAssertEqual(SecItemDelete((CFDictionaryRef)params), errSecMissingEntitlement); 59 } 60 61 #if TARGET_OS_OSX 62 - (void)testInvalidEntitlementsAppID { 63 NSDictionary *params; 64 params = @{ (id)kSecUseDataProtectionKeychain: @YES, 65 (id)kSecClass: (id)kSecClassGenericPassword, 66 (id)kSecAttrLabel: @"label", }; 67 68 // Un-validated app-identifier entitlements must disallow any access to the keychain. 69 [self setEntitlements:@{ @"com.apple.application-identifier": @"com.apple.test-app-identifier" } validated:NO]; 70 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecMissingEntitlement); 71 XCTAssertEqual(SecItemAdd((CFDictionaryRef)params, NULL), errSecMissingEntitlement); 72 XCTAssertEqual(SecItemDelete((CFDictionaryRef)params), errSecMissingEntitlement); 73 74 // However, keychain-access-groups entitlements should work even if not validated, AMFI will take care 75 // about cases when keychain-access-groups is not correctly used and we have to support cases when 76 // process contains application-groups entitlement but that entitlement is not present in provisioned profile, thus 77 // failing entitlement validation test. 78 [self setEntitlements:@{ @"keychain-access-groups": @[@"com.apple.test-app-identifier"] } validated:NO]; 79 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound); 80 XCTAssertEqual(SecItemAdd((CFDictionaryRef)params, NULL), errSecSuccess); 81 XCTAssertEqual(SecItemDelete((CFDictionaryRef)params), errSecSuccess); 82 } 83 #endif // TARGET_OS_OSX 84 85 - (void)testValidEntitlementsAppID { 86 NSDictionary *params; 87 params = @{ (id)kSecUseDataProtectionKeychain: @YES, 88 (id)kSecClass: (id)kSecClassGenericPassword, 89 (id)kSecAttrLabel: @"label", 90 (id)kSecAttrAccessGroup: @"com.apple.test-app-identifier", }; 91 #if TARGET_OS_OSX 92 [self setEntitlements:@{ @"com.apple.application-identifier": @"com.apple.test-app-identifier" } validated:YES]; 93 #else 94 [self setEntitlements:@{ @"application-identifier": @"com.apple.test-app-identifier" } validated:YES]; 95 #endif 96 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound); 97 XCTAssertEqual(SecItemAdd((CFDictionaryRef)params, NULL), errSecSuccess); 98 XCTAssertEqual(SecItemDelete((CFDictionaryRef)params), errSecSuccess); 99 } 100 101 - (void)testEntitlementsAssociatedAppID { 102 NSMutableDictionary *params = [@{(id)kSecUseDataProtectionKeychain: @YES, 103 (id)kSecClass: (id)kSecClassGenericPassword, 104 (id)kSecAttrLabel: @"label" } mutableCopy]; 105 106 [self setEntitlements:@{ 107 #if TARGET_OS_OSX 108 @"com.apple.application-identifier": @"com.apple.test-app-identifier", 109 #else 110 @"application-identifier": @"com.apple.test-app-identifier", 111 #endif 112 @"com.apple.developer.associated-application-identifier": @[ @"com.apple.test-associated-app-identifier" ] 113 } validated:YES]; 114 115 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound); 116 117 // The associated app identifier is preferred over the 'regular' app identifier (in practice this is only relevant on macOS) 118 XCTAssertEqual(SecItemAdd((CFDictionaryRef)params, NULL), errSecSuccess); 119 params[(id)kSecReturnAttributes] = @(YES); 120 CFTypeRef result = NULL; 121 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)params, &result), errSecSuccess); 122 XCTAssertTrue(CFEqual(CFDictionaryGetValue(result, kSecAttrAccessGroup), CFSTR("com.apple.test-associated-app-identifier"))); 123 CFReleaseNull(result); 124 XCTAssertEqual(SecItemDelete((CFDictionaryRef)params), errSecSuccess); 125 } 126 127 - (void)testDisallowTokenGroupWrite { 128 NSDictionary *params; 129 130 // Explicit com.apple.token agrp is not acceptable for writing operations, but acceptable for reading. 131 params = @{ (id)kSecUseDataProtectionKeychain: @YES, 132 (id)kSecClass: (id)kSecClassGenericPassword, 133 (id)kSecAttrLabel: @"label", 134 (id)kSecAttrAccessGroup: (id)kSecAttrAccessGroupToken, }; 135 [self setEntitlements:@{ @"keychain-access-groups": @[ (id)kSecAttrAccessGroupToken ] } validated:YES]; 136 if (os_feature_enabled(CryptoTokenKit, UseTokens)) { 137 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound); 138 } else { 139 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecMissingEntitlement); 140 } 141 XCTAssertEqual(SecItemAdd((CFDictionaryRef)params, NULL), errSecMissingEntitlement); 142 XCTAssertEqual(SecItemDelete((CFDictionaryRef)params), errSecMissingEntitlement); 143 } 144 145 #if TARGET_OS_OSX 146 - (void)testInvalidAppGroups { 147 NSDictionary *params; 148 params = @{ (id)kSecUseDataProtectionKeychain: @YES, 149 (id)kSecClass: (id)kSecClassGenericPassword, 150 (id)kSecAttrLabel: @"label", }; 151 [self setEntitlements:@{ @"com.apple.security.application-groups": @[@"com.apple.test-app-groups"] } validated:NO]; 152 153 // Invalid access group entitlement should still allow querying com.apple.token, if tokens are enabled 154 if (os_feature_enabled(CryptoTokenKit, UseTokens)) { 155 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound); 156 } else { 157 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecMissingEntitlement); 158 } 159 160 // But write-access is forbidden, 161 XCTAssertEqual(SecItemAdd((CFDictionaryRef)params, NULL), errSecMissingEntitlement); 162 XCTAssertEqual(SecItemDelete((CFDictionaryRef)params), errSecMissingEntitlement); 163 164 // Similarly as explicitly referring to AG specified in unverified entitlements. 165 params = @{ (id)kSecUseDataProtectionKeychain: @YES, 166 (id)kSecClass: (id)kSecClassGenericPassword, 167 (id)kSecAttrLabel: @"label", 168 (id)kSecAttrAccessGroup: @"com.apple.test-app-groups", }; 169 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecMissingEntitlement); 170 XCTAssertEqual(SecItemAdd((CFDictionaryRef)params, NULL), errSecMissingEntitlement); 171 XCTAssertEqual(SecItemDelete((CFDictionaryRef)params), errSecMissingEntitlement); 172 173 params = @{ (id)kSecUseDataProtectionKeychain: @YES, 174 (id)kSecClass: (id)kSecClassGenericPassword, 175 (id)kSecAttrLabel: @"label", 176 (id)kSecAttrAccessGroup: (id)kSecAttrAccessGroupToken, }; 177 if (os_feature_enabled(CryptoTokenKit, UseTokens)) { 178 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound); 179 } else { 180 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecMissingEntitlement); 181 } 182 } 183 #endif // TARGET_OS_OSX 184 185 - (void)testTokenItemsGroup { 186 NSDictionary *params; 187 188 [self setEntitlements:@{ 189 #if TARGET_OS_OSX 190 @"com.apple.application-identifier": @"com.apple.test-app-identifier", 191 #else 192 @"application-identifier": @"com.apple.test-app-identifier", 193 #endif 194 @"keychain-access-groups": @[ @"com.apple.token" ], 195 } validated:YES]; 196 197 // Add token items for testing into the keychain. 198 NSArray *tokenItems = @[ @{ 199 (id)kSecClass: (id)kSecClassGenericPassword, 200 (id)kSecAttrAccessGroup: (id)kSecAttrAccessGroupToken, 201 (id)kSecAttrLabel: @"label", 202 } ]; 203 XCTAssertEqual(SecItemUpdateTokenItemsForAccessGroups(@"com.apple.testtoken", (__bridge CFArrayRef)@[(id)kSecAttrAccessGroupToken], (__bridge CFArrayRef)tokenItems), errSecSuccess); 204 205 // Query should find items, because we have token access group in entitlements. 206 params = @{ (id)kSecUseDataProtectionKeychain: @YES, 207 (id)kSecClass: (id)kSecClassGenericPassword, 208 (id)kSecAttrLabel: @"label", }; 209 if (os_feature_enabled(CryptoTokenKit, UseTokens)) { 210 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecSuccess); 211 } else { 212 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound); 213 } 214 215 #if TARGET_OS_IPHONE 216 // Not having access group in entitlements will not find items. 217 [self setEntitlements:@{ 218 @"application-identifier": @"com.apple.test-app-identifier", 219 } validated:YES]; 220 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound); 221 #endif 222 223 // Delete all test token items. 224 [self setEntitlements:@{ 225 #if TARGET_OS_OSX 226 @"com.apple.application-identifier": @"com.apple.test-app-identifier", 227 #else 228 @"application-identifier": @"com.apple.test-app-identifier", 229 #endif 230 @"keychain-access-groups": @[ @"com.apple.token" ], 231 } validated:YES]; 232 SecItemUpdateTokenItemsForAccessGroups(@"com.apple.testtoken", (__bridge CFArrayRef)@[(id)kSecAttrAccessGroupToken], (__bridge CFArrayRef)@[]); 233 } 234 235 - (void)testEntitlementForExplicitAccessGroupLacking { 236 [self setEntitlements:@{@"com.apple.application-identifier": @"com.apple.test-app-identifier", @"application-identifier": @"com.apple.test-app-identifier"} validated:YES]; 237 238 NSMutableDictionary* query = [@{(id)kSecClass : (id)kSecClassGenericPassword, 239 (id)kSecUseDataProtectionKeychain : @YES, 240 (id)kSecAttrAccount : @"TestAccount", 241 (id)kSecAttrLabel : @"TestLabel", 242 (id)kSecAttrAccessGroup : @"com.apple.test.myaccessgroup", 243 (id)kSecValueData : [@"passwd" dataUsingEncoding:NSUTF8StringEncoding], 244 } mutableCopy]; 245 NSMutableDictionary* update = [@{(id)kSecAttrLabel : @"NewLabel"} mutableCopy]; 246 247 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, NULL), errSecMissingEntitlement); 248 query[(id)kSecValueData] = nil; 249 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL), errSecMissingEntitlement); 250 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update), errSecMissingEntitlement); 251 XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)query), errSecMissingEntitlement); 252 253 [self setEntitlements:@{@"com.apple.application-identifier": @"com.apple.test-app-identifier", @"application-identifier": @"com.apple.test-app-identifier", @"keychain-access-groups": @[@"com.apple.test.myaccessgroup"]} validated:YES]; 254 query[(id)kSecValueData] = [@"secret" dataUsingEncoding:NSUTF8StringEncoding]; 255 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, NULL), errSecSuccess); 256 query[(id)kSecValueData] = nil; 257 258 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL), errSecSuccess); 259 [self setEntitlements:@{@"com.apple.application-identifier": @"com.apple.test-app-identifier"} validated:YES]; 260 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL), errSecMissingEntitlement); 261 262 query[(id)kSecAttrAccessGroup] = nil; 263 update[(id)kSecAttrAccessGroup] = @"com.apple.test.myotheraccessgroup"; 264 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update), errSecMissingEntitlement); 265 266 query[(id)kSecAttrAccessGroup] = @"com.apple.test.myaccessgroup"; 267 [self setEntitlements:@{@"com.apple.application-identifier": @"com.apple.test-app-identifier", @"application-identifier": @"com.apple.test-app-identifier", @"keychain-access-groups": @[@"com.apple.test.myaccessgroup"]} validated:YES]; 268 XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)query), errSecSuccess, @"keychain item not deleted after querying without entitlements"); 269 } 270 271 - (void)testEntitlementForImplicitAccessGroupLacking { 272 NSMutableDictionary* query = [@{(id)kSecClass : (id)kSecClassGenericPassword, 273 (id)kSecUseDataProtectionKeychain : @YES, 274 (id)kSecAttrAccount : @"TestAccount", 275 (id)kSecAttrLabel : @"TestLabel", 276 (id)kSecAttrAccessGroup : @"com.apple.test.myaccessgroup", 277 (id)kSecValueData : [@"passwd" dataUsingEncoding:NSUTF8StringEncoding], 278 } mutableCopy]; 279 NSDictionary* update = @{(id)kSecAttrLabel : @"NewLabel"}; 280 281 // Have to use explicit access group here or we just get the app identifier 282 [self setEntitlements:@{@"com.apple.application-identifier": @"com.apple.test-app-identifier", @"application-identifier": @"com.apple.test-app-identifier", @"keychain-access-groups": @[@"com.apple.test.myaccessgroup"]} validated:YES]; 283 XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, NULL), errSecSuccess); 284 285 [self setEntitlements:@{@"com.apple.application-identifier": @"com.apple.test-app-identifier", @"application-identifier": @"com.apple.test-app-identifier"} validated:YES]; 286 query[(id)kSecValueData] = nil; 287 query[(id)kSecAttrAccessGroup] = nil; 288 289 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL), errSecItemNotFound); 290 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update), errSecItemNotFound); 291 XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)query), errSecItemNotFound); 292 293 [self setEntitlements:@{@"com.apple.application-identifier": @"com.apple.test-app-identifier", @"application-identifier": @"com.apple.test-app-identifier", @"keychain-access-groups": @[@"com.apple.test.myaccessgroup"]} validated:YES]; 294 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update), errSecSuccess); 295 query[(id)kSecAttrLabel] = @"NewLabel"; 296 297 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL), errSecSuccess); 298 XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)query), errSecSuccess, @"keychain item not deleted after querying without entitlements"); 299 } 300 301 @end 302 303 #endif // USE_KEYSTORE