/ 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