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