]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+CurrentPointerAPI.m
Security-58286.20.16.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSTests+CurrentPointerAPI.m
1 /*
2 * Copyright (c) 2017 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 #if OCTAGON
25
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
29
30 #include <Security/SecItemPriv.h>
31 #include <Security/SecEntitlements.h>
32 #include <ipc/server_security_helpers.h>
33
34 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
35 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
36 #import "keychain/ckks/CKKS.h"
37 #import "keychain/ckks/CKKSCurrentItemPointer.h"
38 #import "keychain/ckks/CKKSMirrorEntry.h"
39
40 #import "keychain/ckks/tests/MockCloudKit.h"
41
42 #import "keychain/ckks/tests/CKKSTests.h"
43 #import "keychain/ckks/tests/CKKSTests+API.h"
44
45 @implementation CloudKitKeychainSyncingTests (CurrentPointerAPITests)
46
47 -(void)fetchCurrentPointer:(bool)cached persistentRef:(NSData*)persistentRef
48 {
49 XCTestExpectation* currentExpectation = [self expectationWithDescription: @"callback occurs"];
50 SecItemFetchCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
51 (__bridge CFStringRef)@"pcsservice",
52 (__bridge CFStringRef)@"keychain",
53 cached,
54 ^(CFDataRef currentPersistentRef, CFErrorRef cferror) {
55 XCTAssertNotNil((__bridge id)currentPersistentRef, "current item exists");
56 XCTAssertNil((__bridge id)cferror, "no error exists when there's a current item");
57 XCTAssertEqualObjects(persistentRef, (__bridge id)currentPersistentRef, "persistent ref matches expected persistent ref");
58 [currentExpectation fulfill];
59 });
60 [self waitForExpectationsWithTimeout:8.0 handler:nil];
61 }
62 -(void)fetchCurrentPointerExpectingError:(bool)cached
63 {
64 XCTestExpectation* currentExpectation = [self expectationWithDescription: @"callback occurs"];
65 SecItemFetchCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
66 (__bridge CFStringRef)@"pcsservice",
67 (__bridge CFStringRef)@"keychain",
68 cached,
69 ^(CFDataRef currentPersistentRef, CFErrorRef cferror) {
70 XCTAssertNil((__bridge id)currentPersistentRef, "no current item exists");
71 XCTAssertNotNil((__bridge id)cferror, "Error exists when there's a current item");
72 [currentExpectation fulfill];
73 });
74 [self waitForExpectationsWithTimeout:8.0 handler:nil];
75 }
76
77 - (void)testPCSFetchCurrentPointerCachedAndUncached {
78 SecResetLocalSecuritydXPCFakeEntitlements();
79 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
80 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
81 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
82
83 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
84 [self startCKKSSubsystem];
85
86 [self.keychainView waitForKeyHierarchyReadiness];
87
88 // Ensure that local queries don't hit the server.
89 self.silentFetchesAllowed = false;
90 [self fetchCurrentPointerExpectingError:false];
91
92 // And ensure that global queries do.
93 [self expectCKFetch];
94 [self fetchCurrentPointerExpectingError:true];
95 OCMVerifyAllWithDelay(self.mockDatabase, 8);
96 SecResetLocalSecuritydXPCFakeEntitlements();
97 }
98
99 - (void)testPCSCurrentPointerAddAndUpdate {
100 SecResetLocalSecuritydXPCFakeEntitlements();
101 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
102 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
103 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
104
105 NSNumber* servIdentifier = @3;
106 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
107 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
108
109 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
110 [self startCKKSSubsystem];
111
112 // Let things shake themselves out.
113 [self.keychainView waitForKeyHierarchyReadiness];
114
115 // Ensure there's no current pointer
116 [self fetchCurrentPointerExpectingError:false];
117
118 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
119
120 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
121 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
122 PCSServiceIdentifier:(NSNumber *)servIdentifier
123 PCSPublicKey:publicKey
124 PCSPublicIdentity:publicIdentity]];
125
126 NSDictionary* result = [self pcsAddItem:@"testaccount"
127 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
128 serviceIdentifier:(NSNumber*)servIdentifier
129 publicKey:(NSData*)publicKey
130 publicIdentity:(NSData*)publicIdentity
131 expectingSync:true];
132 XCTAssertNotNil(result, "Received result from adding item");
133 [self waitForExpectations:@[keychainChanged] timeout:1];
134
135 // Check that the record is where we expect it in CloudKit
136 [self waitForCKModifications];
137 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
138 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
139 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
140
141 NSData* persistentRef = result[(id)kSecValuePersistentRef];
142 NSData* sha1 = result[(id)kSecAttrSHA1];
143
144 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
145 deletedRecordTypeCounts:nil
146 zoneID:self.keychainZoneID
147 checkModifiedRecord:nil
148 runAfterModification:nil];
149
150 // Set the 'current' pointer.
151 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
152
153 // Ensure that setting the current pointer sends a notification
154 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
155
156 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
157 (__bridge CFStringRef)@"pcsservice",
158 (__bridge CFStringRef)@"keychain",
159 (__bridge CFDataRef)persistentRef,
160 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
161 NSError* error = (__bridge NSError*)cferror;
162 XCTAssertNil(error, "No error setting current item");
163 [setCurrentExpectation fulfill];
164 });
165 OCMVerifyAllWithDelay(self.mockDatabase, 8);
166 [self waitForExpectations:@[keychainChanged] timeout:1];
167 [self waitForCKModifications];
168
169 [self waitForExpectationsWithTimeout:8.0 handler:nil];
170
171 CKRecord* currentItemPointer = self.keychainZone.currentDatabase[[[CKRecordID alloc] initWithRecordName:@"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID]];
172 XCTAssertNotNil(currentItemPointer, "Found a CKRecord at the expected location in CloudKit");
173 XCTAssertEqualObjects(currentItemPointer.recordType, SecCKRecordCurrentItemType, "Saved CKRecord is correct type");
174 XCTAssertEqualObjects(((CKReference*)currentItemPointer[SecCKRecordItemRefKey]).recordID, pcsItemRecordID, "Current Item record points to correct record");
175
176 // Check that the status APIs return the right value
177 [self fetchCurrentPointer:false persistentRef:persistentRef];
178
179 // Rad. If we got here, adding a new current item pointer works. Let's see if we can modify one.
180 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
181
182 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
183 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
184 PCSServiceIdentifier:(NSNumber *)servIdentifier
185 PCSPublicKey:publicKey
186 PCSPublicIdentity:publicIdentity]];
187
188 result = [self pcsAddItem:@"tOTHER-ITEM"
189 data:[@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding]
190 serviceIdentifier:(NSNumber*)servIdentifier
191 publicKey:(NSData*)publicKey
192 publicIdentity:(NSData*)publicIdentity
193 expectingSync:true];
194 XCTAssertNotNil(result, "Received result from adding item");
195 [self waitForExpectations:@[keychainChanged] timeout:1];
196
197 // Check that the record is where we expect it
198 [self waitForCKModifications];
199 CKRecordID* pcsOtherItemRecordID = [[CKRecordID alloc] initWithRecordName: @"878BEAA6-1EE9-1079-1025-E6832AC8F2F3" zoneID:self.keychainZoneID];
200 CKRecord* recordOther = self.keychainZone.currentDatabase[pcsOtherItemRecordID];
201 XCTAssertNotNil(recordOther, "Found other record in CloudKit at expected UUID");
202
203 NSData* otherPersistentRef = result[(id)kSecValuePersistentRef];
204 NSData* otherSha1 = result[(id)kSecAttrSHA1];
205
206 // change the 'current' pointer.
207
208 // Refetch the old item's hash, just in case it's changed (it does, about 50% of the time. I'm not sure why).
209 CFTypeRef cfresult = NULL;
210 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) @{
211 (id)kSecValuePersistentRef : persistentRef,
212 (id)kSecReturnAttributes : @YES,
213 }, &cfresult), "Found original item by persistent reference");
214
215 XCTAssertNotNil((__bridge id)cfresult, "Received an item by finding persistent reference");
216 NSData* actualSHA1 = CFBridgingRelease(CFRetainSafe(CFDictionaryGetValue(cfresult, kSecAttrSHA1)));
217 XCTAssertNotNil(actualSHA1, "Have a SHA1 for the original item");
218 CFReleaseNull(cfresult);
219
220 if(![actualSHA1 isEqual:sha1]) {
221 secnotice("ckks", "SHA1s don't match, but why?");
222 }
223
224 XCTestExpectation* otherSetCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
225
226 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
227 deletedRecordTypeCounts:nil
228 zoneID:self.keychainZoneID
229 checkModifiedRecord:nil
230 runAfterModification:nil];
231
232 // Ensure that setting the current pointer sends a notification
233 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
234
235 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
236 (__bridge CFStringRef)@"pcsservice",
237 (__bridge CFStringRef)@"keychain",
238 (__bridge CFDataRef)otherPersistentRef,
239 (__bridge CFDataRef)otherSha1,
240 (__bridge CFDataRef)persistentRef,
241 (__bridge CFDataRef)actualSHA1, ^ (CFErrorRef cferror) {
242 NSError* error = (__bridge NSError*)cferror;
243 XCTAssertNil(error, "No error setting current item");
244 [otherSetCurrentExpectation fulfill];
245 });
246 OCMVerifyAllWithDelay(self.mockDatabase, 8);
247 [self waitForExpectations:@[keychainChanged] timeout:1];
248 [self waitForCKModifications];
249
250 [self waitForExpectationsWithTimeout:8.0 handler:nil];
251
252 currentItemPointer = self.keychainZone.currentDatabase[[[CKRecordID alloc] initWithRecordName:@"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID]];
253 XCTAssertNotNil(currentItemPointer, "Found a CKRecord at the expected location in CloudKit");
254 XCTAssertEqualObjects(currentItemPointer.recordType, SecCKRecordCurrentItemType, "Saved CKRecord is correct type");
255 XCTAssertEqualObjects(((CKReference*)currentItemPointer[SecCKRecordItemRefKey]).recordID, pcsOtherItemRecordID, "Current Item record points to updated record");
256
257 // And: again
258 [self fetchCurrentPointer:false persistentRef:otherPersistentRef];
259 [self fetchCurrentPointer:true persistentRef:otherPersistentRef];
260
261 SecResetLocalSecuritydXPCFakeEntitlements();
262 }
263
264 - (void)testPCSCurrentPointerAddNoCloudKitAccount {
265 SecResetLocalSecuritydXPCFakeEntitlements();
266 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
267 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
268 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
269
270 NSNumber* servIdentifier = @3;
271 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
272 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
273
274 // Entirely signed out of iCloud. all current record writes should fail.
275 self.accountStatus = CKAccountStatusNoAccount;
276 self.circleStatus = kSOSCCNotInCircle;
277 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
278
279 self.silentFetchesAllowed = false;
280 [self startCKKSSubsystem];
281
282 // Ensure there's no current pointer
283 [self fetchCurrentPointerExpectingError:false];
284
285 // Should NOT add an item to CloudKit
286 NSDictionary* result = [self pcsAddItem:@"testaccount"
287 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
288 serviceIdentifier:(NSNumber*)servIdentifier
289 publicKey:(NSData*)publicKey
290 publicIdentity:(NSData*)publicIdentity
291 expectingSync:false];
292 XCTAssertNotNil(result, "Received result from adding item");
293
294 NSData* persistentRef = result[(id)kSecValuePersistentRef];
295 NSData* sha1 = result[(id)kSecAttrSHA1];
296
297 // Set the 'current' pointer. This should fail.
298 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
299
300 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
301 (__bridge CFStringRef)@"pcsservice",
302 (__bridge CFStringRef)@"keychain",
303 (__bridge CFDataRef)persistentRef,
304 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
305 NSError* error = (__bridge NSError*)cferror;
306 XCTAssertNotNil(error, "Error setting current item with no CloudKit account");
307 [setCurrentExpectation fulfill];
308 });
309
310 [self waitForExpectationsWithTimeout:8.0 handler:nil];
311 SecResetLocalSecuritydXPCFakeEntitlements();
312 }
313
314 - (void)testPCSCurrentPointerAddNonSyncItem {
315 SecResetLocalSecuritydXPCFakeEntitlements();
316 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
317 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
318 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
319
320 NSNumber* servIdentifier = @3;
321 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
322 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
323
324 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
325 [self startCKKSSubsystem];
326 [self.keychainView waitForKeyHierarchyReadiness];
327
328 NSMutableDictionary* query = [self pcsAddItemQuery:@"account"
329 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
330 serviceIdentifier:servIdentifier
331 publicKey:publicKey
332 publicIdentity:publicIdentity];
333 query[(id)kSecAttrSynchronizable] = @NO;
334
335 CFTypeRef cfresult = NULL;
336 XCTestExpectation* syncExpectation = [self expectationWithDescription: @"_SecItemAddAndNotifyOnSync callback occured"];
337
338 // Note that you will NOT receive a notification here, since you're adding a nonsync item
339 syncExpectation.inverted = YES;
340
341 XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, &cfresult, ^(bool didSync, CFErrorRef error) {
342 XCTAssertFalse(didSync, "Item did not sync");
343 XCTAssertNotNil((__bridge NSError*)error, "Error syncing item");
344
345 [syncExpectation fulfill];
346 }), @"_SecItemAddAndNotifyOnSync succeeded");
347
348 // We don't expect this callback to fire, so give it a second or so
349 [self waitForExpectations:@[syncExpectation] timeout:2.0];
350
351 NSDictionary* result = CFBridgingRelease(cfresult);
352
353 XCTAssertNotNil(result, "Received result from adding item");
354
355 NSData* persistentRef = result[(id)kSecValuePersistentRef];
356 NSData* sha1 = result[(id)kSecAttrSHA1];
357
358 // Set the 'current' pointer. This should fail.
359 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
360
361 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
362 (__bridge CFStringRef)@"pcsservice",
363 (__bridge CFStringRef)@"keychain",
364 (__bridge CFDataRef)persistentRef,
365 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
366 NSError* error = (__bridge NSError*)cferror;
367 XCTAssertNotNil(error, "Error setting current item to nonsyncable item");
368 [setCurrentExpectation fulfill];
369 });
370
371 [self waitForExpectations:@[setCurrentExpectation] timeout:8.0];
372 SecResetLocalSecuritydXPCFakeEntitlements();
373 }
374
375 - (void)testPCSCurrentPointerReceive {
376 SecResetLocalSecuritydXPCFakeEntitlements();
377 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
378 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
379 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
380
381 NSNumber* servIdentifier = @3;
382 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
383 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
384
385 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
386 [self startCKKSSubsystem];
387
388 // Let things shake themselves out.
389 [self.keychainView waitForKeyHierarchyReadiness];
390
391 // Ensure there's no current pointer
392 [self fetchCurrentPointerExpectingError:false];
393
394 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
395 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
396 PCSServiceIdentifier:(NSNumber *)servIdentifier
397 PCSPublicKey:publicKey
398 PCSPublicIdentity:publicIdentity]];
399
400 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
401
402 NSDictionary* result = [self pcsAddItem:@"testaccount"
403 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
404 serviceIdentifier:(NSNumber*)servIdentifier
405 publicKey:(NSData*)publicKey
406 publicIdentity:(NSData*)publicIdentity
407 expectingSync:true];
408 XCTAssertNotNil(result, "Received result from adding item");
409 NSData* persistentRef = result[(id)kSecValuePersistentRef];
410
411 [self waitForExpectations:@[keychainChanged] timeout:1];
412
413 // Check that the record is where we expect it in CloudKit
414 [self waitForCKModifications];
415 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
416 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
417 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
418
419 // Still no current pointer.
420 [self fetchCurrentPointerExpectingError:false];
421
422 // Another machine comes along and updates the pointer!
423 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
424 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
425 state:SecCKKSProcessedStateRemote
426 zoneID:self.keychainZoneID
427 encodedCKRecord:nil];
428 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
429 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
430 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
431 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
432
433 // Ensure that receiving the current item pointer generates a notification
434 keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
435
436 [self.keychainView notifyZoneChange:nil];
437 [self.keychainView waitForFetchAndIncomingQueueProcessing];
438
439 [self waitForExpectations:@[keychainChanged] timeout:1];
440
441 [self fetchCurrentPointer:false persistentRef:persistentRef];
442
443 SecResetLocalSecuritydXPCFakeEntitlements();
444 }
445
446
447 - (void)testPCSCurrentPointerReceiveDelete {
448 SecResetLocalSecuritydXPCFakeEntitlements();
449 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
450 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
451 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
452
453 NSNumber* servIdentifier = @3;
454 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
455 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
456
457 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
458 [self startCKKSSubsystem];
459
460 // Let things shake themselves out.
461 [self.keychainView waitForKeyHierarchyReadiness];
462
463 // Ensure there's no current pointer
464 [self fetchCurrentPointerExpectingError:false];
465
466 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
467 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
468 PCSServiceIdentifier:(NSNumber *)servIdentifier
469 PCSPublicKey:publicKey
470 PCSPublicIdentity:publicIdentity]];
471
472 NSDictionary* result = [self pcsAddItem:@"testaccount"
473 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
474 serviceIdentifier:(NSNumber*)servIdentifier
475 publicKey:(NSData*)publicKey
476 publicIdentity:(NSData*)publicIdentity
477 expectingSync:true];
478 XCTAssertNotNil(result, "Received result from adding item");
479 NSData* persistentRef = result[(id)kSecValuePersistentRef];
480
481 // Check that the record is where we expect it in CloudKit
482 [self waitForCKModifications];
483 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
484 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
485 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
486
487 // Still no current pointer.
488 [self fetchCurrentPointerExpectingError:false];
489
490 // Another machine comes along and updates the pointer!
491 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
492 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
493 state:SecCKKSProcessedStateRemote
494 zoneID:self.keychainZoneID
495 encodedCKRecord:nil];
496 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
497 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
498 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
499 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
500
501 [self.keychainView notifyZoneChange:nil];
502 [self.keychainView waitForFetchAndIncomingQueueProcessing];
503
504 [self fetchCurrentPointer:false persistentRef:persistentRef];
505
506 // Ensure that receiving the current item pointer generates a notification
507 XCTestExpectation* keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
508
509 // Another machine comes along and deletes the pointer!
510 [self.keychainZone deleteCKRecordIDFromZone: currentPointerRecordID];
511 [self.keychainView notifyZoneChange:nil];
512 [self.keychainView waitForFetchAndIncomingQueueProcessing];
513 [self waitForExpectations:@[keychainChanged] timeout:1];
514
515 [self fetchCurrentPointerExpectingError:false];
516
517 SecResetLocalSecuritydXPCFakeEntitlements();
518 }
519
520
521 - (void)testPCSCurrentPointerRecoverFromRecordExistsError {
522 SecResetLocalSecuritydXPCFakeEntitlements();
523 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
524 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
525 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
526
527 NSNumber* servIdentifier = @3;
528 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
529 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
530
531 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
532 [self startCKKSSubsystem];
533
534 // Let things shake themselves out.
535 [self.keychainView waitForKeyHierarchyReadiness];
536
537 // Ensure there's no current pointer
538 [self fetchCurrentPointerExpectingError:false];
539
540 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
541 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
542 PCSServiceIdentifier:(NSNumber *)servIdentifier
543 PCSPublicKey:publicKey
544 PCSPublicIdentity:publicIdentity]];
545
546 NSDictionary* result = [self pcsAddItem:@"testaccount"
547 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
548 serviceIdentifier:(NSNumber*)servIdentifier
549 publicKey:(NSData*)publicKey
550 publicIdentity:(NSData*)publicIdentity
551 expectingSync:true];
552 XCTAssertNotNil(result, "Received result from adding item");
553
554 // Check that the record is where we expect it in CloudKit
555 [self waitForCKModifications];
556 NSString* recordUUID = @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3";
557 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName:recordUUID zoneID:self.keychainZoneID];
558 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
559 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
560
561 // Someone else sets the current record pointer
562 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
563 currentItemUUID:recordUUID
564 state:SecCKKSProcessedStateRemote
565 zoneID:self.keychainZoneID
566 encodedCKRecord:nil];
567 XCTAssertNotNil(cip, "Should have created a CIP");
568 CKRecord* cipRecord = [cip CKRecordWithZoneID:self.keychainZoneID];
569 XCTAssertNotNil(cipRecord, "Should have created a CKRecord for this CIP");
570 [self.keychainZone addToZone: cipRecord];
571
572 NSData* persistentRef = result[(id)kSecValuePersistentRef];
573 NSData* sha1 = result[(id)kSecAttrSHA1];
574
575 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
576
577 // Set the 'current' pointer.
578 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
579
580 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
581 (__bridge CFStringRef)@"pcsservice",
582 (__bridge CFStringRef)@"keychain",
583 (__bridge CFDataRef)persistentRef,
584 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
585 NSError* error = (__bridge NSError*)cferror;
586 XCTAssertNotNil(error, "Should have received an error setting current item (because of conflict)");
587 [setCurrentExpectation fulfill];
588 });
589 OCMVerifyAllWithDelay(self.mockDatabase, 8);
590 [self waitForCKModifications];
591 [self waitForExpectationsWithTimeout:8.0 handler:nil];
592
593 [self.keychainView waitUntilAllOperationsAreFinished];
594
595 [self fetchCurrentPointer:false persistentRef:persistentRef];
596
597 SecResetLocalSecuritydXPCFakeEntitlements();
598 }
599
600 - (void)testPCSCurrentPointerWasCurrent {
601 SecResetLocalSecuritydXPCFakeEntitlements();
602 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
603 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
604 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
605
606 NSNumber* servIdentifier = @3;
607 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
608 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
609
610 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
611 [self startCKKSSubsystem];
612
613 // Let things shake themselves out.
614 [self.keychainView waitForKeyHierarchyReadiness];
615
616 // Ensure there's no current pointer
617 [self fetchCurrentPointerExpectingError:false];
618
619 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
620 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
621 PCSServiceIdentifier:(NSNumber *)servIdentifier
622 PCSPublicKey:publicKey
623 PCSPublicIdentity:publicIdentity]];
624
625 NSDictionary* result = [self pcsAddItem:@"testaccount"
626 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
627 serviceIdentifier:(NSNumber*)servIdentifier
628 publicKey:(NSData*)publicKey
629 publicIdentity:(NSData*)publicIdentity
630 expectingSync:true];
631 XCTAssertNotNil(result, "Received result from adding item");
632
633 // Check that the record is where we expect it in CloudKit
634 [self waitForCKModifications];
635 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
636 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
637 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
638
639 NSData* persistentRef = result[(id)kSecValuePersistentRef];
640 NSData* sha1 = result[(id)kSecAttrSHA1];
641
642 [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
643 deletedRecordTypeCounts:nil
644 zoneID:self.keychainZoneID
645 checkModifiedRecord:nil
646 runAfterModification:nil];
647
648 // Set the 'current' pointer.
649 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
650
651 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
652 (__bridge CFStringRef)@"pcsservice",
653 (__bridge CFStringRef)@"keychain",
654 (__bridge CFDataRef)persistentRef,
655 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
656 NSError* error = (__bridge NSError*)cferror;
657 XCTAssertNil(error, "No error setting current item");
658 [setCurrentExpectation fulfill];
659 });
660 OCMVerifyAllWithDelay(self.mockDatabase, 8);
661 [self waitForExpectationsWithTimeout:8.0 handler:nil];
662 [self waitForCKModifications];
663
664 // Set the 'was current' flag on the record
665 CKRecord* modifiedRecord = [record copy];
666 modifiedRecord[SecCKRecordServerWasCurrent] = [NSNumber numberWithInteger:10];
667 [self.keychainZone addToZone:modifiedRecord];
668
669 [self.keychainView notifyZoneChange:nil];
670 [self.keychainView waitForFetchAndIncomingQueueProcessing];
671
672 // Check that the number is on the CKKSMirrorEntry
673 [self.keychainView dispatchSync: ^bool {
674 NSError* error = nil;
675 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID error:&error];
676
677 XCTAssertNil(error, "no error fetching ckme");
678 XCTAssertNotNil(ckme, "Received a ckme");
679
680 XCTAssertEqual(ckme.wasCurrent, 10u, "Properly received wasCurrent");
681
682 return true;
683 }];
684 }
685
686 -(void)testPCSCurrentPointerWriteFailure {
687 SecResetLocalSecuritydXPCFakeEntitlements();
688 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
689 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
690 SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
691
692 NSNumber* servIdentifier = @3;
693 NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
694 NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
695
696 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
697 [self startCKKSSubsystem];
698
699 // Let things shake themselves out.
700 [self.keychainView waitForKeyHierarchyReadiness];
701
702 // Ensure there's no current pointer
703 [self fetchCurrentPointerExpectingError:false];
704
705 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
706 checkItem: [self checkPCSFieldsBlock:self.keychainZoneID
707 PCSServiceIdentifier:(NSNumber *)servIdentifier
708 PCSPublicKey:publicKey
709 PCSPublicIdentity:publicIdentity]];
710
711 NSDictionary* result = [self pcsAddItem:@"testaccount"
712 data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
713 serviceIdentifier:(NSNumber*)servIdentifier
714 publicKey:(NSData*)publicKey
715 publicIdentity:(NSData*)publicIdentity
716 expectingSync:true];
717 XCTAssertNotNil(result, "Received result from adding item");
718
719 // Check that the record is where we expect it in CloudKit
720 [self waitForCKModifications];
721 CKRecordID* pcsItemRecordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
722 CKRecord* record = self.keychainZone.currentDatabase[pcsItemRecordID];
723 XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
724
725 NSData* persistentRef = result[(id)kSecValuePersistentRef];
726 NSData* sha1 = result[(id)kSecAttrSHA1];
727
728 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
729
730 // Set the 'current' pointer.
731 XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
732
733 SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
734 (__bridge CFStringRef)@"pcsservice",
735 (__bridge CFStringRef)@"keychain",
736 (__bridge CFDataRef)persistentRef,
737 (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
738 NSError* error = (__bridge NSError*)cferror;
739 XCTAssertNotNil(error, "Error setting current item when the write fails");
740 [setCurrentExpectation fulfill];
741 });
742 OCMVerifyAllWithDelay(self.mockDatabase, 8);
743
744 [self waitForExpectationsWithTimeout:8.0 handler:nil];
745
746 SecResetLocalSecuritydXPCFakeEntitlements();
747 }
748
749 @end
750
751 #endif // OCTAGON