2 * Copyright (c) 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@
24 #import <Security/Security.h>
25 #import <Security/SecItemPriv.h>
26 #import <os/feature_private.h>
28 #import "KeychainXCTest.h"
31 @interface KeychainEntitlementsTest : KeychainXCTest
34 @implementation KeychainEntitlementsTest
36 - (void)testNoEntitlements {
37 NSDictionary *params = @{ (id)kSecUseDataProtectionKeychain: @YES,
38 (id)kSecClass: (id)kSecClassGenericPassword, (id)kSecAttrLabel: @"label", };
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)) {
45 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound);
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);
52 // If tokens are not enabled, this situation really means that there is an entitlement problem.
53 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecMissingEntitlement);
56 // However, write access is declined for such application.
57 XCTAssertEqual(SecItemAdd((CFDictionaryRef)params, NULL), errSecMissingEntitlement);
58 XCTAssertEqual(SecItemDelete((CFDictionaryRef)params), errSecMissingEntitlement);
62 - (void)testInvalidEntitlementsAppID {
64 params = @{ (id)kSecUseDataProtectionKeychain: @YES,
65 (id)kSecClass: (id)kSecClassGenericPassword,
66 (id)kSecAttrLabel: @"label", };
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);
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);
83 #endif // TARGET_OS_OSX
85 - (void)testValidEntitlementsAppID {
87 params = @{ (id)kSecUseDataProtectionKeychain: @YES,
88 (id)kSecClass: (id)kSecClassGenericPassword,
89 (id)kSecAttrLabel: @"label",
90 (id)kSecAttrAccessGroup: @"com.apple.test-app-identifier", };
92 [self setEntitlements:@{ @"com.apple.application-identifier": @"com.apple.test-app-identifier" } validated:YES];
94 [self setEntitlements:@{ @"application-identifier": @"com.apple.test-app-identifier" } validated:YES];
96 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound);
97 XCTAssertEqual(SecItemAdd((CFDictionaryRef)params, NULL), errSecSuccess);
98 XCTAssertEqual(SecItemDelete((CFDictionaryRef)params), errSecSuccess);
101 - (void)testEntitlementsAssociatedAppID {
102 NSMutableDictionary *params = [@{(id)kSecUseDataProtectionKeychain: @YES,
103 (id)kSecClass: (id)kSecClassGenericPassword,
104 (id)kSecAttrLabel: @"label" } mutableCopy];
106 [self setEntitlements:@{
108 @"com.apple.application-identifier": @"com.apple.test-app-identifier",
110 @"application-identifier": @"com.apple.test-app-identifier",
112 @"com.apple.developer.associated-application-identifier": @[ @"com.apple.test-associated-app-identifier" ]
115 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound);
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);
127 - (void)testDisallowTokenGroupWrite {
128 NSDictionary *params;
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);
139 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecMissingEntitlement);
141 XCTAssertEqual(SecItemAdd((CFDictionaryRef)params, NULL), errSecMissingEntitlement);
142 XCTAssertEqual(SecItemDelete((CFDictionaryRef)params), errSecMissingEntitlement);
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];
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);
157 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecMissingEntitlement);
160 // But write-access is forbidden,
161 XCTAssertEqual(SecItemAdd((CFDictionaryRef)params, NULL), errSecMissingEntitlement);
162 XCTAssertEqual(SecItemDelete((CFDictionaryRef)params), errSecMissingEntitlement);
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);
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);
180 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecMissingEntitlement);
183 #endif // TARGET_OS_OSX
185 - (void)testTokenItemsGroup {
186 NSDictionary *params;
188 [self setEntitlements:@{
190 @"com.apple.application-identifier": @"com.apple.test-app-identifier",
192 @"application-identifier": @"com.apple.test-app-identifier",
194 @"keychain-access-groups": @[ @"com.apple.token" ],
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",
203 XCTAssertEqual(SecItemUpdateTokenItemsForAccessGroups(@"com.apple.testtoken", (__bridge CFArrayRef)@[(id)kSecAttrAccessGroupToken], (__bridge CFArrayRef)tokenItems), errSecSuccess);
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);
212 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound);
216 // Not having access group in entitlements will not find items.
217 [self setEntitlements:@{
218 @"application-identifier": @"com.apple.test-app-identifier",
220 XCTAssertEqual(SecItemCopyMatching((CFDictionaryRef)params, NULL), errSecItemNotFound);
223 // Delete all test token items.
224 [self setEntitlements:@{
226 @"com.apple.application-identifier": @"com.apple.test-app-identifier",
228 @"application-identifier": @"com.apple.test-app-identifier",
230 @"keychain-access-groups": @[ @"com.apple.token" ],
232 SecItemUpdateTokenItemsForAccessGroups(@"com.apple.testtoken", (__bridge CFArrayRef)@[(id)kSecAttrAccessGroupToken], (__bridge CFArrayRef)@[]);
235 - (void)testEntitlementForExplicitAccessGroupLacking {
236 [self setEntitlements:@{@"com.apple.application-identifier": @"com.apple.test-app-identifier", @"application-identifier": @"com.apple.test-app-identifier"} validated:YES];
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],
245 NSMutableDictionary* update = [@{(id)kSecAttrLabel : @"NewLabel"} mutableCopy];
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);
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;
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);
262 query[(id)kSecAttrAccessGroup] = nil;
263 update[(id)kSecAttrAccessGroup] = @"com.apple.test.myotheraccessgroup";
264 XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update), errSecMissingEntitlement);
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");
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],
279 NSDictionary* update = @{(id)kSecAttrLabel : @"NewLabel"};
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);
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;
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);
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";
297 XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL), errSecSuccess);
298 XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)query), errSecSuccess, @"keychain item not deleted after querying without entitlements");
303 #endif // USE_KEYSTORE