/ secdxctests / KeychainAPITests.m
KeychainAPITests.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 "KeychainXCTest.h"
 25  #import "SecDbKeychainItem.h"
 26  #import "SecdTestKeychainUtilities.h"
 27  #import "CKKS.h"
 28  #import "SecDbKeychainItemV7.h"
 29  #import "SecItemPriv.h"
 30  #include "SecItemInternal.h"
 31  #import "SecItemServer.h"
 32  #import "spi.h"
 33  #import "SecDbKeychainSerializedItemV7.h"
 34  #import "SecDbKeychainSerializedMetadata.h"
 35  #import "SecDbKeychainSerializedSecretData.h"
 36  #import "SecDbKeychainSerializedAKSWrappedKey.h"
 37  #import <utilities/SecCFWrappers.h>
 38  #import <SecurityFoundation/SFEncryptionOperation.h>
 39  #import <XCTest/XCTest.h>
 40  #import <OCMock/OCMock.h>
 41  #include <dispatch/dispatch.h>
 42  #include <utilities/SecDb.h>
 43  #include <sys/stat.h>
 44  #include <utilities/SecFileLocations.h>
 45  #include "der_plist.h"
 46  #import "SecItemRateLimit_tests.h"
 47  #include "ipc/server_security_helpers.h"
 48  #include <Security/SecEntitlements.h>
 49  #include "keychain/securityd/SecItemDb.h"
 50  
 51  #if USE_KEYSTORE
 52  
 53  @interface KeychainAPITests : KeychainXCTest
 54  @end
 55  
 56  @implementation KeychainAPITests
 57  
 58  + (void)setUp
 59  {
 60      [super setUp];
 61  }
 62  
 63  - (NSString*)nameOfTest
 64  {
 65      return [self.name componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" ]"]][1];
 66  }
 67  
 68  - (void)setUp
 69  {
 70      [super setUp];
 71      // KeychainXCTest already sets up keychain with custom test-named directory
 72  }
 73  
 74  - (void)testReturnValuesInSecItemUpdate
 75  {
 76      NSDictionary* addQuery = @{ (id)kSecClass : (id)kSecClassGenericPassword,
 77                                  (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
 78                                  (id)kSecAttrAccount : @"TestAccount",
 79                                  (id)kSecAttrService : @"TestService",
 80                                  (id)kSecUseDataProtectionKeychain : @(YES),
 81                                  (id)kSecReturnAttributes : @(YES)
 82                                };
 83      
 84      NSDictionary* updateQueryWithNoReturn = @{ (id)kSecClass : (id)kSecClassGenericPassword,
 85                                   (id)kSecAttrAccount : @"TestAccount",
 86                                   (id)kSecAttrService : @"TestService",
 87                                   (id)kSecUseDataProtectionKeychain : @(YES)
 88                                   };
 89      
 90      CFTypeRef result = NULL;
 91      
 92      // Add the item
 93      XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), errSecSuccess, @"Should have succeeded in adding test item to keychain");
 94      XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
 95      CFReleaseNull(result);
 96      
 97      // And we can update the item
 98      XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithNoReturn, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with clean update query");
 99      
100      // great, a normal update works
101      // now let's do updates with various queries which include return parameters to ensure they succeed on macOS and throw errors on iOS.
102      // this is a status-quo compromise between changing iOS match macOS (which has lamé no-op characteristics) and changing macOS to match iOS, which risks breaking existing clients
103      
104  #if TARGET_OS_OSX
105      NSMutableDictionary* updateQueryWithReturnAttributes = updateQueryWithNoReturn.mutableCopy;
106      updateQueryWithReturnAttributes[(id)kSecReturnAttributes] = @(YES);
107      XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnAttributes, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-attributes" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return attributes query");
108      
109      NSMutableDictionary* updateQueryWithReturnData = updateQueryWithNoReturn.mutableCopy;
110      updateQueryWithReturnAttributes[(id)kSecReturnData] = @(YES);
111      XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnData, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-data" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return data query");
112      
113      NSMutableDictionary* updateQueryWithReturnRef = updateQueryWithNoReturn.mutableCopy;
114      updateQueryWithReturnAttributes[(id)kSecReturnRef] = @(YES);
115      XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return ref query");
116      
117      NSMutableDictionary* updateQueryWithReturnPersistentRef = updateQueryWithNoReturn.mutableCopy;
118      updateQueryWithReturnAttributes[(id)kSecReturnPersistentRef] = @(YES);
119      XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnPersistentRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-persistent-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecSuccess, "failed to update item with return persistent ref query");
120  #else
121      NSMutableDictionary* updateQueryWithReturnAttributes = updateQueryWithNoReturn.mutableCopy;
122      updateQueryWithReturnAttributes[(id)kSecReturnAttributes] = @(YES);
123      XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnAttributes, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-attributes" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return attributes query");
124      
125      NSMutableDictionary* updateQueryWithReturnData = updateQueryWithNoReturn.mutableCopy;
126      updateQueryWithReturnData[(id)kSecReturnData] = @(YES);
127      XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnData, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-data" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return data query");
128      
129      NSMutableDictionary* updateQueryWithReturnRef = updateQueryWithNoReturn.mutableCopy;
130      updateQueryWithReturnRef[(id)kSecReturnRef] = @(YES);
131      XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return ref query");
132      
133      NSMutableDictionary* updateQueryWithReturnPersistentRef = updateQueryWithNoReturn.mutableCopy;
134      updateQueryWithReturnPersistentRef[(id)kSecReturnPersistentRef] = @(YES);
135      XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQueryWithReturnPersistentRef, (__bridge CFDictionaryRef)@{(id)kSecValueData: [@"return-persistent-ref" dataUsingEncoding:NSUTF8StringEncoding]}), errSecParam, "failed to generate error updating item with return persistent ref query");
136  #endif
137  }
138  
139  - (void)testBadTypeInParams
140  {
141      NSMutableDictionary *attrs = @{
142          (id)kSecClass: (id)kSecClassGenericPassword,
143          (id)kSecUseDataProtectionKeychain: @YES,
144          (id)kSecAttrLabel: @"testentry",
145      }.mutableCopy;
146  
147      SecItemDelete((CFDictionaryRef)attrs);
148      XCTAssertEqual(errSecSuccess, SecItemAdd((CFDictionaryRef)attrs, NULL));
149      XCTAssertEqual(errSecSuccess, SecItemDelete((CFDictionaryRef)attrs));
150  
151      // We try to fool SecItem API with unexpected type of kSecAttrAccessControl attribute in query and it should not crash.
152      attrs[(id)kSecAttrAccessControl] = @"string, no SecAccessControlRef!";
153      XCTAssertEqual(errSecParam, SecItemAdd((CFDictionaryRef)attrs, NULL));
154      XCTAssertEqual(errSecParam, SecItemDelete((CFDictionaryRef)attrs));
155  }
156  
157  - (BOOL)passInternalAttributeToKeychainAPIsWithKey:(id)key value:(id)value {
158       NSDictionary* badquery = @{
159          (id)kSecClass : (id)kSecClassGenericPassword,
160          (id)kSecAttrService : @"AppClipTestService",
161          (id)kSecUseDataProtectionKeychain : @YES,
162          (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
163          key : value,
164      };
165      NSDictionary* badupdate = @{key : value};
166  
167      NSDictionary* okquery = @{
168          (id)kSecClass : (id)kSecClassGenericPassword,
169          (id)kSecAttrService : @"AppClipTestService",
170          (id)kSecUseDataProtectionKeychain : @YES,
171          (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
172      };
173      NSDictionary* okupdate = @{(id)kSecAttrService : @"DifferentService"};
174  
175      if (SecItemAdd((__bridge CFDictionaryRef)badquery, NULL) != errSecParam) {
176          XCTFail("SecItemAdd did not return errSecParam");
177          return NO;
178      }
179      if (SecItemCopyMatching((__bridge CFDictionaryRef)badquery, NULL) != errSecParam) {
180          XCTFail("SecItemCopyMatching did not return errSecParam");
181          return NO;
182      }
183      if (SecItemUpdate((__bridge CFDictionaryRef)badquery, (__bridge CFDictionaryRef)okupdate) != errSecParam) {
184          XCTFail("SecItemUpdate with bad query did not return errSecParam");
185          return NO;
186      }
187      if (SecItemUpdate((__bridge CFDictionaryRef)okquery, (__bridge CFDictionaryRef)badupdate) != errSecParam) {
188          XCTFail("SecItemUpdate with bad update did not return errSecParam");
189          return NO;
190      }
191      if (SecItemDelete((__bridge CFDictionaryRef)badquery) != errSecParam) {
192          XCTFail("SecItemDelete did not return errSecParam");
193          return NO;
194      }
195      return YES;
196  }
197  
198  // Expand this, rdar://problem/59297616
199  - (void)testNotAllowedToPassInternalAttributes {
200      XCTAssert([self passInternalAttributeToKeychainAPIsWithKey:(__bridge NSString*)kSecAttrAppClipItem value:@YES], @"Expect errSecParam for 'clip' attribute");
201  }
202  
203  #pragma mark - Corruption Tests
204  
205  const uint8_t keychain_data[] = {
206      0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xd2, 0x01, 0x02, 0x03,
207      0x04, 0x5f, 0x10, 0x1b, 0x4e, 0x53, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77,
208      0x20, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x20, 0x50, 0x72, 0x6f, 0x63, 0x65,
209      0x73, 0x73, 0x50, 0x61, 0x6e, 0x65, 0x6c, 0x5f, 0x10, 0x1d, 0x4e, 0x53,
210      0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x20, 0x46, 0x72, 0x61, 0x6d, 0x65,
211      0x20, 0x41, 0x62, 0x6f, 0x75, 0x74, 0x20, 0x54, 0x68, 0x69, 0x73, 0x20,
212      0x4d, 0x61, 0x63, 0x5f, 0x10, 0x1c, 0x32, 0x38, 0x20, 0x33, 0x37, 0x33,
213      0x20, 0x33, 0x34, 0x36, 0x20, 0x32, 0x39, 0x30, 0x20, 0x30, 0x20, 0x30,
214      0x20, 0x31, 0x34, 0x34, 0x30, 0x20, 0x38, 0x37, 0x38, 0x20, 0x5f, 0x10,
215      0x1d, 0x35, 0x36, 0x38, 0x20, 0x33, 0x39, 0x35, 0x20, 0x33, 0x30, 0x37,
216      0x20, 0x33, 0x37, 0x39, 0x20, 0x30, 0x20, 0x30, 0x20, 0x31, 0x34, 0x34,
217      0x30, 0x20, 0x38, 0x37, 0x38, 0x20, 0x08, 0x0d, 0x2b, 0x4b, 0x6a, 0x00,
218      0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
219      0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
220      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a
221  };
222  
223  dispatch_semaphore_t sema = NULL;
224  
225  // The real corruption exit handler should xpc_transaction_exit_clean,
226  // let's be certain it does not. Also make sure exit handler gets called at all
227  static void SecDbTestCorruptionHandler(void)
228  {
229      dispatch_semaphore_signal(sema);
230  }
231  
232  - (void)testCorruptionHandler {
233      __security_simulatecrash_enable(false);
234  
235      SecDbCorruptionExitHandler = SecDbTestCorruptionHandler;
236      sema = dispatch_semaphore_create(0);
237  
238      // Test teardown will want to delete this keychain. Make sure it knows where to look...
239      NSString* corruptedKeychainPath = [NSString stringWithFormat:@"%@-bad", [self nameOfTest]];
240  
241      if(self.keychainDirectoryPrefix) {
242          XCTAssertTrue(secd_test_teardown_delete_temp_keychain([self.keychainDirectoryPrefix UTF8String]), "Should be able to delete the temp keychain");
243      }
244  
245      self.keychainDirectoryPrefix = corruptedKeychainPath;
246  
247      secd_test_setup_temp_keychain([corruptedKeychainPath UTF8String], ^{
248          CFStringRef keychain_path_cf = __SecKeychainCopyPath();
249  
250          CFStringPerformWithCString(keychain_path_cf, ^(const char *keychain_path) {
251              int fd = open(keychain_path, O_RDWR | O_CREAT | O_TRUNC, 0644);
252              XCTAssert(fd > -1, "Could not open fd to write keychain: %{darwin.errno}d", errno);
253  
254              size_t written = write(fd, keychain_data, sizeof(keychain_data));
255              XCTAssertEqual(written, sizeof(keychain_data), "Write garbage to disk, got %lu instead of %lu: %{darwin.errno}d", written, sizeof(keychain_data), errno);
256              XCTAssertEqual(close(fd), 0, "Close keychain file failed: %{darwin.errno}d", errno);
257          });
258  
259          CFReleaseNull(keychain_path_cf);
260      });
261  
262      NSDictionary* query = @{(id)kSecClass : (id)kSecClassGenericPassword,
263                              (id)kSecAttrAccount : @"TestAccount",
264                              (id)kSecAttrService : @"TestService",
265                              (id)kSecUseDataProtectionKeychain : @(YES),
266                              (id)kSecReturnAttributes : @(YES)
267                              };
268  
269      CFTypeRef result = NULL;
270      // Real keychain should xpc_transaction_exit_clean() after this, but we nerfed it
271      XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), errSecNotAvailable, "Expected badness from corrupt keychain");
272      XCTAssertEqual(dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC)), 0, "Timed out waiting for corruption exit handler");
273  
274      sema = NULL;
275      SecDbResetCorruptionExitHandler();
276      CFReleaseNull(result);
277  
278      NSString* markerpath = [NSString stringWithFormat:@"%@-iscorrupt", CFBridgingRelease(__SecKeychainCopyPath())];
279      struct stat info = {};
280      XCTAssertEqual(stat([markerpath UTF8String], &info), 0, "Unable to stat corruption marker: %{darwin.errno}d", errno);
281  }
282  
283  - (void)testRecoverFromCorruption {
284      __security_simulatecrash_enable(false);
285  
286      // Setup does a reset, but that doesn't create the db yet so let's sneak in first
287      __block struct stat before = {};
288      WithPathInKeychainDirectory(CFSTR("keychain-2.db"), ^(const char *filename) {
289          FILE* file = fopen(filename, "w");
290          XCTAssert(file != NULL, "Didn't get a FILE pointer");
291          fclose(file);
292          XCTAssertEqual(stat(filename, &before), 0, "Unable to stat newly created file");
293      });
294  
295      WithPathInKeychainDirectory(CFSTR("keychain-2.db-iscorrupt"), ^(const char *filename) {
296          FILE* file = fopen(filename, "w");
297          XCTAssert(file != NULL, "Didn't get a FILE pointer");
298          fclose(file);
299      });
300  
301      NSMutableDictionary* query = [@{(id)kSecClass : (id)kSecClassGenericPassword,
302                                      (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
303                                      (id)kSecAttrAccount : @"TestAccount",
304                                      (id)kSecAttrService : @"TestService",
305                                      (id)kSecUseDataProtectionKeychain : @(YES),
306                                      (id)kSecReturnAttributes : @(YES)
307                                      } mutableCopy];
308      CFTypeRef result = NULL;
309      XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, &result), errSecSuccess, @"Should have added item to keychain");
310      XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
311      CFReleaseNull(result);
312  
313      query[(id)kSecValueData] = nil;
314      XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)query, &result), errSecSuccess, @"Should have found item in keychain");
315      XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemCopyMatching");
316      CFReleaseNull(result);
317  
318      XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)query), errSecSuccess, @"Should have deleted item from keychain");
319  
320      WithPathInKeychainDirectory(CFSTR("keychain-2.db-iscorrupt"), ^(const char *filename) {
321          struct stat markerinfo = {};
322          XCTAssertNotEqual(stat(filename, &markerinfo), 0, "Expected not to find corruption marker after killing keychain");
323      });
324  
325      __block struct stat after = {};
326      WithPathInKeychainDirectory(CFSTR("keychain-2.db"), ^(const char *filename) {
327          FILE* file = fopen(filename, "w");
328          XCTAssert(file != NULL, "Didn't get a FILE pointer");
329          fclose(file);
330          XCTAssertEqual(stat(filename, &after), 0, "Unable to stat newly created file");
331      });
332  
333      if (before.st_birthtimespec.tv_sec == after.st_birthtimespec.tv_sec) {
334          XCTAssertLessThan(before.st_birthtimespec.tv_nsec, after.st_birthtimespec.tv_nsec, "db was not deleted and recreated");
335      } else {
336          XCTAssertLessThan(before.st_birthtimespec.tv_sec, after.st_birthtimespec.tv_sec, "db was not deleted and recreated");
337      }
338  }
339  
340  - (void)testInetBinaryFields {
341      NSData* note = [@"OBVIOUS_NOTES_DATA" dataUsingEncoding:NSUTF8StringEncoding];
342      NSData* history = [@"OBVIOUS_HISTORY_DATA" dataUsingEncoding:NSUTF8StringEncoding];
343      NSData* client0 = [@"OBVIOUS_CLIENT0_DATA" dataUsingEncoding:NSUTF8StringEncoding];
344      NSData* client1 = [@"OBVIOUS_CLIENT1_DATA" dataUsingEncoding:NSUTF8StringEncoding];
345      NSData* client2 = [@"OBVIOUS_CLIENT2_DATA" dataUsingEncoding:NSUTF8StringEncoding];
346      NSData* client3 = [@"OBVIOUS_CLIENT3_DATA" dataUsingEncoding:NSUTF8StringEncoding];
347  
348      NSData* originalPassword = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
349      NSMutableDictionary* query = [@{
350          (id)kSecClass : (id)kSecClassInternetPassword,
351          (id)kSecAttrAccessible : (id)kSecAttrAccessibleWhenUnlocked,
352          (id)kSecUseDataProtectionKeychain : @YES,
353          (id)kSecAttrDescription : @"desc",
354          (id)kSecAttrServer : @"server",
355          (id)kSecAttrAccount : @"test-account",
356          (id)kSecValueData : originalPassword,
357          (id)kSecDataInetExtraNotes : note,
358          (id)kSecDataInetExtraHistory : history,
359          (id)kSecDataInetExtraClientDefined0 : client0,
360          (id)kSecDataInetExtraClientDefined1 : client1,
361          (id)kSecDataInetExtraClientDefined2 : client2,
362          (id)kSecDataInetExtraClientDefined3 : client3,
363  
364          (id)kSecReturnAttributes : @YES,
365      } mutableCopy];
366  
367      CFTypeRef cfresult = nil;
368      XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)query, &cfresult), errSecSuccess, "Should be able to add an item using new binary fields");
369      NSDictionary* result = (NSDictionary*)CFBridgingRelease(cfresult);
370      XCTAssertNotNil(result, "Should have some sort of result");
371  
372      XCTAssertNil(result[(id)kSecDataInetExtraNotes], "Notes field should not be returned as an attribute from add");
373      XCTAssertNil(result[(id)kSecDataInetExtraHistory], "Notes field should not be returned as an attribute from add");
374  
375      NSDictionary* queryFind = @{
376          (id)kSecClass : (id)kSecClassInternetPassword,
377          (id)kSecUseDataProtectionKeychain : @YES,
378          (id)kSecAttrAccount : @"test-account",
379      };
380  
381      NSMutableDictionary* queryFindOneWithJustAttributes = [[NSMutableDictionary alloc] initWithDictionary:queryFind];
382      queryFindOneWithJustAttributes[(id)kSecReturnAttributes] = @YES;
383  
384      NSMutableDictionary* queryFindAllWithJustAttributes = [[NSMutableDictionary alloc] initWithDictionary:queryFindOneWithJustAttributes];
385      queryFindAllWithJustAttributes[(id)kSecMatchLimit] = (id)kSecMatchLimitAll;
386  
387      NSDictionary* queryFindOneWithAttributesAndData = @{
388          (id)kSecClass : (id)kSecClassInternetPassword,
389          (id)kSecUseDataProtectionKeychain : @YES,
390          (id)kSecReturnAttributes : @YES,
391          (id)kSecReturnData: @YES,
392          (id)kSecAttrAccount : @"test-account",
393      };
394  
395      NSDictionary* queryFindAllWithAttributesAndData = @{
396          (id)kSecClass : (id)kSecClassInternetPassword,
397          (id)kSecUseDataProtectionKeychain : @YES,
398          (id)kSecReturnAttributes : @YES,
399          (id)kSecReturnData: @YES,
400          (id)kSecMatchLimit : (id)kSecMatchLimitAll,
401          (id)kSecAttrAccount : @"test-account",
402      };
403  
404      /* Copy with a single record limite, but with attributes only */
405      XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindOneWithJustAttributes, &cfresult), errSecSuccess, "Should be able to find an item");
406  
407      result = (NSDictionary*)CFBridgingRelease(cfresult);
408      XCTAssertNotNil(result, "Should have some sort of result");
409  
410      XCTAssertNil(result[(id)kSecDataInetExtraNotes], "Notes field should not be returned as an attribute from copymatching when finding a single item");
411      XCTAssertNil(result[(id)kSecDataInetExtraHistory], "Notes field should not be returned as an attribute from copymatching when finding a single item");
412      XCTAssertNil(result[(id)kSecDataInetExtraClientDefined0], "ClientDefined0 field should not be returned as an attribute from copymatching when finding a single item");
413      XCTAssertNil(result[(id)kSecDataInetExtraClientDefined1], "ClientDefined1 field should not be returned as an attribute from copymatching when finding a single item");
414      XCTAssertNil(result[(id)kSecDataInetExtraClientDefined2], "ClientDefined2 field should not be returned as an attribute from copymatching when finding a single item");
415      XCTAssertNil(result[(id)kSecDataInetExtraClientDefined3], "ClientDefined3 field should not be returned as an attribute from copymatching when finding a single item");
416  
417      /* Copy with no limit, but with attributes only */
418      XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindAllWithJustAttributes, &cfresult), errSecSuccess, "Should be able to find an item");
419      NSArray* arrayResult = (NSArray*)CFBridgingRelease(cfresult);
420      XCTAssertNotNil(arrayResult, "Should have some sort of result");
421      XCTAssertTrue([arrayResult isKindOfClass:[NSArray class]], "Should have received an array back from copymatching");
422      XCTAssertEqual(arrayResult.count, 1, "Array should have one element");
423  
424      result = arrayResult[0];
425      XCTAssertNil(result[(id)kSecDataInetExtraNotes], "Notes field should not be returned as an attribute from copymatching when finding all items");
426      XCTAssertNil(result[(id)kSecDataInetExtraHistory], "Notes field should not be returned as an attribute from copymatching when finding all items");
427      XCTAssertNil(result[(id)kSecDataInetExtraClientDefined0], "ClientDefined0 field should not be returned as an attribute from copymatching when finding all items");
428      XCTAssertNil(result[(id)kSecDataInetExtraClientDefined1], "ClientDefined1 field should not be returned as an attribute from copymatching when finding all items");
429      XCTAssertNil(result[(id)kSecDataInetExtraClientDefined2], "ClientDefined2 field should not be returned as an attribute from copymatching when finding all items");
430      XCTAssertNil(result[(id)kSecDataInetExtraClientDefined3], "ClientDefined3 field should not be returned as an attribute from copymatching when finding all items");
431  
432      /* Copy with single-record limit, but with attributes and data */
433      XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindOneWithAttributesAndData, &cfresult), errSecSuccess, "Should be able to find an item");
434      result = (NSDictionary*)CFBridgingRelease(cfresult);
435      XCTAssertNotNil(result, "Should have some sort of result");
436  
437      XCTAssertEqualObjects(note, result[(id)kSecDataInetExtraNotes], "Notes field should be returned as data");
438      XCTAssertEqualObjects(history, result[(id)kSecDataInetExtraHistory], "History field should be returned as data");
439      XCTAssertEqualObjects(client0, result[(id)kSecDataInetExtraClientDefined0], "Client Defined 0 field should be returned as data");
440      XCTAssertEqualObjects(client1, result[(id)kSecDataInetExtraClientDefined1], "Client Defined 1 field should be returned as data");
441      XCTAssertEqualObjects(client2, result[(id)kSecDataInetExtraClientDefined2], "Client Defined 2 field should be returned as data");
442      XCTAssertEqualObjects(client3, result[(id)kSecDataInetExtraClientDefined3], "Client Defined 3 field should be returned as data");
443  
444      /* Copy with no limit, but with attributes and data */
445      XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindAllWithAttributesAndData, &cfresult), errSecSuccess, "Should be able to find an item");
446      arrayResult = (NSArray*)CFBridgingRelease(cfresult);
447      XCTAssertNotNil(arrayResult, "Should have some sort of result");
448      XCTAssertTrue([arrayResult isKindOfClass:[NSArray class]], "Should have received an array back from copymatching");
449      XCTAssertEqual(arrayResult.count, 1, "Array should have one element");
450      result = arrayResult[0];
451      XCTAssertEqualObjects(note, result[(id)kSecDataInetExtraNotes], "Notes field should be returned as data");
452      XCTAssertEqualObjects(history, result[(id)kSecDataInetExtraHistory], "History field should be returned as data");
453      XCTAssertEqualObjects(client0, result[(id)kSecDataInetExtraClientDefined0], "Client Defined 0 field should be returned as data");
454      XCTAssertEqualObjects(client1, result[(id)kSecDataInetExtraClientDefined1], "Client Defined 1 field should be returned as data");
455      XCTAssertEqualObjects(client2, result[(id)kSecDataInetExtraClientDefined2], "Client Defined 2 field should be returned as data");
456      XCTAssertEqualObjects(client3, result[(id)kSecDataInetExtraClientDefined3], "Client Defined 3 field should be returned as data");
457  
458      /* Copy just looking for the password */
459      XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)@{
460          (id)kSecClass : (id)kSecClassInternetPassword,
461          (id)kSecUseDataProtectionKeychain : @YES,
462          (id)kSecReturnData: @YES,
463          (id)kSecAttrAccount : @"test-account",
464      }, &cfresult), errSecSuccess, "Should be able to find an item");
465  
466      NSData* password = (NSData*)CFBridgingRelease(cfresult);
467      XCTAssertNotNil(password, "Should have some sort of password");
468      XCTAssertTrue([password isKindOfClass:[NSData class]], "Password is a data");
469      XCTAssertEqualObjects(originalPassword, password, "Should still be able to fetch the original password");
470  
471      NSData* newHistoryContents = [@"gone" dataUsingEncoding:NSUTF8StringEncoding];
472  
473      NSDictionary* updateQuery = @{
474          (id)kSecDataInetExtraHistory : newHistoryContents,
475      };
476  
477      XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)queryFind, (__bridge CFDictionaryRef)updateQuery), errSecSuccess, "Should be able to update a history field");
478  
479      // And find it again
480      XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)queryFindOneWithAttributesAndData, &cfresult), errSecSuccess, "Should be able to find an item");
481      result = (NSDictionary*)CFBridgingRelease(cfresult);
482      XCTAssertNotNil(result, "Should have some sort of result");
483  
484      XCTAssertEqualObjects(note, result[(id)kSecDataInetExtraNotes], "Notes field should be returned as data");
485      XCTAssertEqualObjects(newHistoryContents, result[(id)kSecDataInetExtraHistory], "History field should be updated");
486      XCTAssertEqualObjects(client0, result[(id)kSecDataInetExtraClientDefined0], "Client Defined 0 field should be returned as data");
487      XCTAssertEqualObjects(client1, result[(id)kSecDataInetExtraClientDefined1], "Client Defined 1 field should be returned as data");
488      XCTAssertEqualObjects(client2, result[(id)kSecDataInetExtraClientDefined2], "Client Defined 2 field should be returned as data");
489      XCTAssertEqualObjects(client3, result[(id)kSecDataInetExtraClientDefined3], "Client Defined 3 field should be returned as data");
490  }
491  
492  // When this test starts failing, hopefully rdar://problem/60332379 got fixed
493  - (void)testBadDateCausesDERDecodeValidationError {
494      // Wonky time calculation hastily stolen from SecGregorianDateGetAbsoluteTime and tweaked
495      // As of right now this causes CFCalendarDecomposeAbsoluteTime with Zulu calendar to give a seemingly incorrect date which then causes DER date validation issues
496      CFAbsoluteTime absTime = (CFAbsoluteTime)(((-(1902 * 365) + -38) * 24 + 0) * 60 + -1) * 60 + 1;
497      absTime -= 0.0004;  // Just to make sure the nanoseconds keep getting encoded/decoded properly
498      CFDateRef date = CFDateCreate(NULL, absTime);
499  
500      CFErrorRef error = NULL;
501      size_t plistSize = der_sizeof_plist(date, &error);
502      XCTAssert(error == NULL);
503      XCTAssertGreaterThan(plistSize, 0);
504  
505      // Encode without repair does not validate dates because that changes behavior I do not want to fiddle with
506      uint8_t* der = calloc(1, plistSize);
507      uint8_t* der_end = der + plistSize;
508      uint8_t* result = der_encode_plist(date, &error, der, der_end);
509      XCTAssert(error == NULL);
510      XCTAssertEqual(der, result);
511  
512      // ...but decoding does and will complain
513      CFPropertyListRef decoded = NULL;
514      XCTAssert(der_decode_plist(NULL, &decoded, &error, der, der_end) == NULL);
515      XCTAssert(error != NULL);
516      XCTAssertEqual(CFErrorGetDomain(error), kCFErrorDomainOSStatus);
517      NSString* description = CFBridgingRelease(CFErrorCopyDescription(error));
518      XCTAssert([description containsString:@"Invalid date"]);
519  
520      CFReleaseNull(error);
521      free(der);
522  }
523  
524  // When this test starts failing, hopefully rdar://problem/60332379 got fixed
525  - (void)testBadDateWithDEREncodingRepairProducesDefaultValue {
526      // Wonky time calculation hastily stolen from SecGregorianDateGetAbsoluteTime and tweaked
527      // As of right now this causes CFCalendarDecomposeAbsoluteTime with Zulu calendar to give a seemingly incorrect date which then causes DER date validation issues
528      CFAbsoluteTime absTime = (CFAbsoluteTime)(((-(1902 * 365) + -38) * 24 + 0) * 60 + -1) * 60 + 1;
529      absTime -= 0.0004;  // Just to make sure the nanoseconds keep getting encoded/decoded properly
530      CFDateRef date = CFDateCreate(NULL, absTime);
531  
532      CFErrorRef error = NULL;
533      size_t plistSize = der_sizeof_plist(date, &error);
534      XCTAssert(error == NULL);
535      XCTAssertGreaterThan(plistSize, 0);
536  
537      uint8_t* der = calloc(1, plistSize);
538      uint8_t* der_end = der + plistSize;
539      uint8_t* encoderesult = der_encode_plist_repair(date, &error, true, der, der_end);
540      XCTAssert(error == NULL);
541      XCTAssertEqual(der, encoderesult);
542  
543      CFPropertyListRef decoded = NULL;
544      const uint8_t* decoderesult = der_decode_plist(NULL, &decoded, &error, der, der_end);
545      XCTAssertEqual(der_end, decoderesult);
546      XCTAssertEqual(CFGetTypeID(decoded), CFDateGetTypeID());
547      XCTAssertEqualWithAccuracy(CFDateGetAbsoluteTime(decoded), 0, 60 * 60 * 24);
548  }
549  
550  - (void)testContainersWithBadDateWithDEREncodingRepairProducesDefaultValue {
551      // Wonky time calculation hastily stolen from SecGregorianDateGetAbsoluteTime and tweaked
552      // As of right now this causes CFCalendarDecomposeAbsoluteTime with Zulu calendar to give a seemingly incorrect date which then causes DER date validation issues
553      CFAbsoluteTime absTime = (CFAbsoluteTime)(((-(1902 * 365) + -38) * 24 + 0) * 60 + -1) * 60 + 1;
554      absTime -= 0.0004;
555      CFDateRef date = CFDateCreate(NULL, absTime);
556  
557      NSDictionary* dict = @{
558          @"dateset": [NSSet setWithObject:(__bridge id)date],
559          @"datearray": @[(__bridge id)date],
560      };
561  
562      CFErrorRef error = NULL;
563      size_t plistSize = der_sizeof_plist((__bridge CFTypeRef)dict, &error);
564      XCTAssertNil((__bridge NSError*)error, "Should be no error checking the size of the plist");
565      XCTAssertGreaterThan(plistSize, 0);
566  
567      uint8_t* der = calloc(1, plistSize);
568      uint8_t* der_end = der + plistSize;
569      uint8_t* encoderesult = der_encode_plist_repair((__bridge CFTypeRef)dict, &error, true, der, der_end);
570      XCTAssertNil((__bridge NSError*)error, "Should be no error encoding the plist");
571      XCTAssertEqual(der, encoderesult);
572  
573      CFPropertyListRef decoded = NULL;
574      const uint8_t* decoderesult = der_decode_plist(NULL, &decoded, &error, der, der_end);
575      XCTAssertNil((__bridge NSError*)error, "Should be no error decoding the plist");
576      XCTAssertEqual(der_end, decoderesult);
577  
578      XCTAssertNotNil((__bridge NSDictionary*)decoded, "Should have decoded some dictionary");
579      if(decoded == nil) {
580          return;
581      }
582  
583      XCTAssertEqual(CFGetTypeID(decoded), CFDictionaryGetTypeID());
584      CFDictionaryRef decodedCFDictionary = decoded;
585  
586      {
587          CFSetRef decodedCFSet = CFDictionaryGetValue(decodedCFDictionary, CFSTR("dateset"));
588          XCTAssertNotNil((__bridge NSSet*)decodedCFSet, "Should have some CFSet");
589  
590          if(decodedCFSet != NULL) {
591              XCTAssertEqual(CFGetTypeID(decodedCFSet), CFSetGetTypeID());
592              XCTAssertEqual(CFSetGetCount(decodedCFSet), 1, "Should have one item in set");
593  
594              __block bool dateprocessed = false;
595              CFSetForEach(decodedCFSet, ^(const void *value) {
596                  XCTAssertEqual(CFGetTypeID(value), CFDateGetTypeID());
597                  XCTAssertEqualWithAccuracy(CFDateGetAbsoluteTime(value), 0, 60 * 60 * 24);
598                  dateprocessed = true;
599              });
600  
601              XCTAssertTrue(dateprocessed, "Should have processed at least one date in the set");
602          }
603      }
604  
605      {
606          CFArrayRef decodedCFArray =  CFDictionaryGetValue(decodedCFDictionary, CFSTR("datearray"));
607          XCTAssertNotNil((__bridge NSArray*)decodedCFArray, "Should have some CFArray");
608  
609          if(decodedCFArray != NULL) {
610              XCTAssertEqual(CFGetTypeID(decodedCFArray), CFArrayGetTypeID());
611              XCTAssertEqual(CFArrayGetCount(decodedCFArray), 1, "Should have one item in array");
612  
613              __block bool dateprocessed = false;
614              CFArrayForEach(decodedCFArray, ^(const void *value) {
615                  XCTAssertEqual(CFGetTypeID(value), CFDateGetTypeID());
616                  XCTAssertEqualWithAccuracy(CFDateGetAbsoluteTime(value), 0, 60 * 60 * 24);
617                  dateprocessed = true;
618              });
619  
620              XCTAssertTrue(dateprocessed, "Should have processed at least one date in the array");
621          }
622      }
623  
624      CFReleaseNull(decoded);
625  }
626  
627  - (void)testSecItemCopyMatchingWithBadDateInItem {
628      // Wonky time calculation hastily stolen from SecGregorianDateGetAbsoluteTime and tweaked
629      // As of right now this causes CFCalendarDecomposeAbsoluteTime with Zulu calendar to give a seemingly incorrect date which then causes DER date validation issues
630      CFAbsoluteTime absTime = (CFAbsoluteTime)(((-(1902 * 365) + -38) * 24 + 0) * 60 + -1) * 60 + 1;
631      absTime -= 0.0004;
632      CFDateRef date = CFDateCreate(NULL, absTime);
633  
634      NSDictionary* addQuery = @{
635          //(id)kSecClass : (id)kSecClassGenericPassword,
636          (id)kSecAttrCreationDate : (__bridge id)date,
637          (id)kSecAttrModificationDate : (__bridge id)date,
638  
639          (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
640          (id)kSecAttrAccount : @"TestAccount",
641          (id)kSecAttrService : @"TestService",
642          (id)kSecAttrAccessGroup : @"com.apple.security.securityd",
643  
644          (id)kSecAttrAccessible  : @"ak",
645          (id)kSecAttrTombstone : [NSNumber numberWithInt: 0],
646          (id)kSecAttrMultiUser : [[NSData alloc] init],
647      };
648  
649      __block CFErrorRef cferror = NULL;
650      kc_with_dbt(true, &cferror, ^bool(SecDbConnectionRef dbt) {
651          return kc_transaction_type(dbt, kSecDbExclusiveTransactionType, &cferror, ^bool {
652              SecDbItemRef item = SecDbItemCreateWithAttributes(NULL, kc_class_with_name(kSecClassGenericPassword), (__bridge CFDictionaryRef)addQuery, KEYBAG_DEVICE, &cferror);
653  
654              bool ret = SecDbItemInsert(item, dbt, false, &cferror);
655  
656              XCTAssertTrue(ret, "Should be able to add an item");
657              CFReleaseNull(item);
658  
659              return ret;
660          });
661      });
662  
663      NSDictionary* findQuery = @{
664          (id)kSecClass : (id)kSecClassGenericPassword,
665          (id)kSecUseDataProtectionKeychain : @YES,
666          (id)kSecAttrAccount : @"TestAccount",
667          (id)kSecAttrService : @"TestService",
668          (id)kSecAttrAccessGroup : @"com.apple.security.securityd",
669  
670          (id)kSecReturnAttributes : @YES,
671          (id)kSecReturnData: @YES,
672          (id)kSecMatchLimit: (id)kSecMatchLimitAll,
673      };
674  
675      CFTypeRef result = NULL;
676      XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecItemNotFound, @"Should not be able to find our misdated item");
677  
678      // This is a bit of a mystery: how do these items have a bad date that's not in the item?
679  
680      CFReleaseNull(result);
681      CFReleaseNull(date);
682  }
683  
684  - (void)testSecItemCopyMatchingWithBadDateInSQLColumn {
685      NSDictionary* addQuery = @{
686          (id)kSecClass : (id)kSecClassGenericPassword,
687  
688          (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
689          (id)kSecAttrAccount : @"TestAccount",
690          (id)kSecAttrService : @"TestService",
691          (id)kSecAttrAccessGroup : @"com.apple.security.securityd",
692  
693          (id)kSecAttrAccessible  : @"ak",
694          (id)kSecAttrService : @"",
695  
696          (id)kSecUseDataProtectionKeychain: @YES,
697      };
698  
699      OSStatus status = SecItemAdd((__bridge CFDictionaryRef)addQuery, NULL);
700      XCTAssertEqual(status, errSecSuccess, "Should be able to add an item to the keychain");
701  
702      // Modify the cdat/mdat columns in the keychain db
703      __block CFErrorRef cferror = NULL;
704      kc_with_dbt(true, &cferror, ^bool(SecDbConnectionRef dbt) {
705          return kc_transaction_type(dbt, kSecDbExclusiveTransactionType, &cferror, ^bool {
706              CFErrorRef updateError = NULL;
707  
708              // Magic number extracted from testSecItemCopyMatchingWithBadDateInItem
709              SecDbExec(dbt,
710                        (__bridge CFStringRef)@"UPDATE genp SET cdat = -59984755259.0004, mdat = -59984755259.0004;",
711                        &updateError);
712  
713              XCTAssertNil((__bridge NSError*)updateError, "Should be no error updating the table");
714              CFReleaseNull(updateError);
715  
716              return true;
717          });
718      });
719  
720      // Can we find the item?
721      NSDictionary* findQuery = @{
722          (id)kSecClass : (id)kSecClassGenericPassword,
723          (id)kSecUseDataProtectionKeychain : @YES,
724          (id)kSecAttrAccount : @"TestAccount",
725          (id)kSecAttrService : @"TestService",
726          (id)kSecAttrAccessGroup : @"com.apple.security.securityd",
727  
728          (id)kSecReturnAttributes : @YES,
729          (id)kSecReturnData: @YES,
730          (id)kSecMatchLimit: (id)kSecMatchLimitAll,
731      };
732  
733      CFTypeRef result = NULL;
734      XCTAssertEqual(SecItemCopyMatching((__bridge CFDictionaryRef)findQuery, &result), errSecSuccess, @"Should be able to find our misdated item");
735  
736      CFReleaseNull(result);
737  }
738  
739  - (void)testDurableWriteAPI
740  {
741      NSDictionary* addQuery = @{
742          (id)kSecClass : (id)kSecClassGenericPassword,
743          (id)kSecValueData : [@"password" dataUsingEncoding:NSUTF8StringEncoding],
744          (id)kSecAttrAccount : @"TestAccount",
745          (id)kSecAttrService : @"TestService",
746          (id)kSecUseDataProtectionKeychain : @(YES),
747          (id)kSecReturnAttributes : @(YES),
748      };
749  
750      NSDictionary* updateQuery = @{
751          (id)kSecClass : (id)kSecClassGenericPassword,
752          (id)kSecAttrAccount : @"TestAccount",
753          (id)kSecAttrService : @"TestService",
754          (id)kSecUseDataProtectionKeychain : @(YES),
755      };
756  
757      CFTypeRef result = NULL;
758  
759      // Add the item
760      XCTAssertEqual(SecItemAdd((__bridge CFDictionaryRef)addQuery, &result), errSecSuccess, @"Should have succeeded in adding test item to keychain");
761      XCTAssertNotNil((__bridge id)result, @"Should have received a dictionary back from SecItemAdd");
762      CFReleaseNull(result);
763  
764      // Using the API without the entitlement should fail
765      CFErrorRef cferror = NULL;
766      XCTAssertEqual(SecItemPersistKeychainWritesAtHighPerformanceCost(&cferror), errSecMissingEntitlement, @"Should not be able to persist keychain writes without the entitlement");
767      XCTAssertNotNil((__bridge NSError*)cferror, "Should be an error persisting keychain writes without the entitlement");
768      CFReleaseNull(cferror);
769  
770      // But with the entitlement, you're good
771      SecResetLocalSecuritydXPCFakeEntitlements();
772      SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivatePerformanceImpactingAPI, kCFBooleanTrue);
773  
774      XCTAssertEqual(SecItemPersistKeychainWritesAtHighPerformanceCost(&cferror), errSecSuccess, @"Should be able to persist keychain writes");
775      XCTAssertNil((__bridge NSError*)cferror, "Should be no error persisting keychain writes");
776  
777      // And we can update the item
778      XCTAssertEqual(SecItemUpdate((__bridge CFDictionaryRef)updateQuery,
779                                   (__bridge CFDictionaryRef)@{
780                                       (id)kSecValueData: [@"otherpassword" dataUsingEncoding:NSUTF8StringEncoding],
781                                                             }),
782                     errSecSuccess, "should be able to update item with clean update query");
783  
784      XCTAssertEqual(SecItemPersistKeychainWritesAtHighPerformanceCost(&cferror), errSecSuccess, @"Should be able to persist keychain writes after an update");
785      XCTAssertNil((__bridge NSError*)cferror, "Should be no error persisting keychain writes");
786  
787      XCTAssertEqual(SecItemDelete((__bridge CFDictionaryRef)updateQuery), errSecSuccess, "Should be able to delete item");
788      XCTAssertEqual(SecItemPersistKeychainWritesAtHighPerformanceCost(&cferror), errSecSuccess, @"Should be able to persist keychain writes after a delete");
789      XCTAssertNil((__bridge NSError*)cferror, "Should be no error persisting keychain writes");
790  }
791  
792  
793  #pragma mark - SecItemRateLimit
794  
795  // This is not super accurate in BATS, so put some margin around what you need
796  - (void)sleepAlternativeForXCTest:(double)interval
797  {
798      dispatch_semaphore_t localsema = dispatch_semaphore_create(0);
799      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * interval), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
800          dispatch_semaphore_signal(localsema);
801      });
802      dispatch_semaphore_wait(localsema, DISPATCH_TIME_FOREVER);
803  }
804  
805  - (void)testSecItemRateLimitTimePasses {
806      SecItemRateLimit* rl = [SecItemRateLimit getStaticRateLimit];
807      [rl forceEnabled: true];
808  
809      for (int idx = 0; idx < rl.roCapacity; ++idx) {
810          XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
811      }
812  
813      for (int idx = 0; idx < rl.rwCapacity; ++idx) {
814          XCTAssertTrue(isModifyingAPIRateWithinLimits());
815      }
816  
817      [self sleepAlternativeForXCTest: 2];
818      XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
819      XCTAssertTrue(isModifyingAPIRateWithinLimits());
820  
821      [SecItemRateLimit resetStaticRateLimit];
822  }
823  
824  - (void)testSecItemRateLimitResetAfterExceed {
825      SecItemRateLimit* rl = [SecItemRateLimit getStaticRateLimit];
826  	[rl forceEnabled: true];
827  
828      for (int idx = 0; idx < rl.roCapacity; ++idx) {
829          XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
830      }
831      XCTAssertFalse(isReadOnlyAPIRateWithinLimits());
832  	XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
833  
834      for (int idx = 0; idx < rl.rwCapacity; ++idx) {
835          XCTAssertTrue(isModifyingAPIRateWithinLimits());
836      }
837      XCTAssertFalse(isModifyingAPIRateWithinLimits());
838      XCTAssertTrue(isModifyingAPIRateWithinLimits());
839  
840      [SecItemRateLimit resetStaticRateLimit];
841  }
842  
843  - (void)testSecItemRateLimitMultiplier {
844      SecItemRateLimit* rl = [SecItemRateLimit getStaticRateLimit];
845      [rl forceEnabled: true];
846  
847      int ro_iterations_before = 0;
848      for (; ro_iterations_before < rl.roCapacity; ++ro_iterations_before) {
849          XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
850      }
851      XCTAssertFalse(isReadOnlyAPIRateWithinLimits());
852  
853      int rw_iterations_before = 0;
854      for (; rw_iterations_before < rl.rwCapacity; ++rw_iterations_before) {
855          XCTAssertTrue(isModifyingAPIRateWithinLimits());
856      }
857      XCTAssertFalse(isModifyingAPIRateWithinLimits());
858  
859  
860      int ro_iterations_after = 0;
861      for (; ro_iterations_after < rl.roCapacity; ++ro_iterations_after) {
862          XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
863      }
864      XCTAssertFalse(isReadOnlyAPIRateWithinLimits());
865  
866      int rw_iterations_after = 0;
867      for (; rw_iterations_after < rl.rwCapacity; ++rw_iterations_after) {
868          XCTAssertTrue(isModifyingAPIRateWithinLimits());
869      }
870      XCTAssertFalse(isModifyingAPIRateWithinLimits());
871  
872      XCTAssertEqualWithAccuracy(rl.limitMultiplier * ro_iterations_before, ro_iterations_after, 1);
873      XCTAssertEqualWithAccuracy(rl.limitMultiplier * rw_iterations_before, rw_iterations_after, 1);
874      [SecItemRateLimit resetStaticRateLimit];
875  }
876  
877  // We stipulate that this test is run on an internal release.
878  // If this were a platform binary limits would be enforced, but it should not be so they should not.
879  - (void)testSecItemRateLimitInternalPlatformBinariesOnly {
880      SecItemRateLimit* rl = [SecItemRateLimit getStaticRateLimit];
881  
882      for (int idx = 0; idx < 3 * MAX(rl.roCapacity, rl.rwCapacity); ++idx) {
883          XCTAssertTrue(isReadOnlyAPIRateWithinLimits());
884          XCTAssertTrue(isModifyingAPIRateWithinLimits());
885      }
886  
887      [SecItemRateLimit resetStaticRateLimit];
888  }
889  
890  @end
891  
892  #endif