]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CloudKitKeychainSyncingFixupTests.m
Security-59306.11.20.tar.gz
[apple/security.git] / keychain / ckks / tests / CloudKitKeychainSyncingFixupTests.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 #import <Foundation/NSKeyedArchiver_Private.h>
30
31 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
32 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
33 #import "keychain/ckks/CKKS.h"
34 #import "keychain/ckks/CKKSFixups.h"
35 #import "keychain/ckks/CKKSZoneStateEntry.h"
36 #import "keychain/ckks/CKKSViewManager.h"
37 #import "keychain/ckks/CKKSCurrentItemPointer.h"
38 #import "keychain/ckks/CKKSIncomingQueueOperation.h"
39
40 #import "keychain/ckks/tests/MockCloudKit.h"
41 #import "keychain/ckks/tests/CKKSTests.h"
42 #import "keychain/ckks/tests/CKKSTests+API.h"
43
44
45 @interface CloudKitKeychainSyncingFixupTests : CloudKitKeychainSyncingTestsBase
46 @end
47
48 @implementation CloudKitKeychainSyncingFixupTests
49
50 - (void)testNoFixupOnInitialStart {
51 id mockFixups = OCMClassMock([CKKSFixups class]);
52 OCMReject([[[mockFixups stub] ignoringNonObjectArgs] fixup:0 for:[OCMArg any]]);
53
54 [self startCKKSSubsystem];
55 [self performOctagonTLKUpload:self.ckksViews];
56
57 [self.keychainView waitForKeyHierarchyReadiness];
58 [self waitForCKModifications];
59 OCMVerifyAllWithDelay(self.mockDatabase, 20);
60
61 [mockFixups verify];
62 [mockFixups stopMocking];
63 }
64
65 - (void)testImmediateRestartUsesLatestFixup {
66 id mockFixups = OCMClassMock([CKKSFixups class]);
67 OCMExpect([mockFixups fixup:CKKSCurrentFixupNumber for:[OCMArg any]]);
68
69 [self startCKKSSubsystem];
70 [self performOctagonTLKUpload:self.ckksViews];
71
72 [self.keychainView waitForKeyHierarchyReadiness];
73 [self waitForCKModifications];
74 OCMVerifyAllWithDelay(self.mockDatabase, 20);
75
76 // Tear down the CKKS object
77 [self.keychainView halt];
78
79 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
80 [self beginSOSTrustedViewOperation:self.keychainView];
81 [self.keychainView waitForKeyHierarchyReadiness];
82 OCMVerifyAllWithDelay(self.mockDatabase, 20);
83
84 [mockFixups verify];
85 [mockFixups stopMocking];
86 }
87
88 - (void)testFixupRefetchAllCurrentItemPointers {
89 // Due to <rdar://problem/34916549> CKKS: current item pointer CKRecord resurrection,
90 // CKKS needs to refetch all current item pointers if it restarts and hasn't yet.
91
92 [self startCKKSSubsystem];
93 [self performOctagonTLKUpload:self.ckksViews];
94
95 [self.keychainView waitForKeyHierarchyReadiness];
96 [self waitForCKModifications];
97 OCMVerifyAllWithDelay(self.mockDatabase, 20);
98
99 // Add some current item pointers. They don't necessarily need to point to anything...
100
101 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
102 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
103 state:SecCKKSProcessedStateRemote
104 zoneID:self.keychainZoneID
105 encodedCKRecord:nil];
106 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
107 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
108 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
109 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
110
111 CKKSCurrentItemPointer* cip2 = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice2"
112 currentItemUUID:@"3AB8E78D-75AF-CFEF-F833-FA3E3E90978A"
113 state:SecCKKSProcessedStateRemote
114 zoneID:self.keychainZoneID
115 encodedCKRecord:nil];
116 [self.keychainZone addToZone: [cip2 CKRecordWithZoneID:self.keychainZoneID]];
117 CKRecordID* currentPointerRecordID2 = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice2" zoneID:self.keychainZoneID];
118 CKRecord* currentPointerRecord2 = self.keychainZone.currentDatabase[currentPointerRecordID2];
119 XCTAssertNotNil(currentPointerRecord2, "Found record in CloudKit at expected UUID");
120
121 [self.keychainView notifyZoneChange:nil];
122 [self.keychainView waitForFetchAndIncomingQueueProcessing];
123
124 // Tear down the CKKS object
125 [self.keychainView halt];
126
127 [self.keychainView dispatchSync: ^bool {
128 // Edit the zone state entry to have no fixups
129 NSError* error = nil;
130 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
131
132 XCTAssertNil(error, "no error pulling ckse from database");
133 XCTAssertNotNil(ckse, "received a ckse");
134
135 ckse.lastFixup = CKKSFixupNever;
136 [ckse saveToDatabase: &error];
137 XCTAssertNil(error, "no error saving to database");
138
139 // And add a garbage CIP
140 CKKSCurrentItemPointer* cip3 = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"garbage"
141 currentItemUUID:@"3AB8E78D-75AF-CFEF-F833-FA3E3E90978A"
142 state:SecCKKSProcessedStateLocal
143 zoneID:self.keychainZoneID
144 encodedCKRecord:nil];
145 cip3.storedCKRecord = [cip3 CKRecordWithZoneID:self.keychainZoneID];
146 XCTAssertEqual(cip3.identifier, @"garbage", "Identifier is what we thought it was");
147 [cip3 saveToDatabase:&error];
148 XCTAssertNil(error, "no error saving to database");
149 return true;
150 }];
151
152 self.silentFetchesAllowed = false;
153 [self expectCKFetchByRecordID];
154 [self expectCKFetchByQuery]; // and one for the TLKShare fixup
155
156 // Change one of the CIPs while CKKS is offline
157 cip2.currentItemUUID = @"changed-by-cloudkit";
158 [self.keychainZone addToZone: [cip2 CKRecordWithZoneID:self.keychainZoneID]];
159
160 // Bring CKKS back up
161 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
162 [self beginSOSTrustedViewOperation:self.keychainView];
163 [self.keychainView waitForKeyHierarchyReadiness];
164
165 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
166 OCMVerifyAllWithDelay(self.mockDatabase, 20);
167
168 [self.keychainView dispatchSync: ^bool {
169 // The zone state entry should be up the most recent fixup level
170 NSError* error = nil;
171 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
172 XCTAssertNil(error, "no error pulling ckse from database");
173 XCTAssertNotNil(ckse, "received a ckse");
174 XCTAssertEqual(ckse.lastFixup, CKKSCurrentFixupNumber, "CKSE should have the current fixup number stored");
175
176 // The garbage CIP should be gone, and CKKS should have caught up to the CIP change
177 NSArray<CKKSCurrentItemPointer*>* allCIPs = [CKKSCurrentItemPointer allInZone:self.keychainZoneID error:&error];
178 XCTAssertNil(error, "no error loading all CIPs from database");
179
180 XCTestExpectation* foundCIP2 = [self expectationWithDescription: @"found CIP2"];
181 for(CKKSCurrentItemPointer* loaded in allCIPs) {
182 if([loaded.identifier isEqualToString: cip2.identifier]) {
183 [foundCIP2 fulfill];
184 XCTAssertEqualObjects(loaded.currentItemUUID, @"changed-by-cloudkit", "Fixup should have fixed UUID to new value, not pre-shutdown value");
185 }
186 XCTAssertNotEqualObjects(loaded.identifier, @"garbage", "Garbage CIP shouldn't exist anymore");
187 }
188
189 [self waitForExpectations:@[foundCIP2] timeout:0.1];
190 return true;
191 }];
192 }
193
194 - (void)setFixupNumber:(CKKSFixup)newFixup ckks:(CKKSKeychainView*)ckks {
195 [ckks dispatchSync: ^bool {
196 // Edit the zone state entry to have no fixups
197 NSError* error = nil;
198 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:ckks.zoneID.zoneName error:&error];
199
200 XCTAssertNil(error, "no error pulling ckse from database");
201 XCTAssertNotNil(ckse, "received a ckse");
202
203 ckse.lastFixup = newFixup;
204 [ckse saveToDatabase: &error];
205 XCTAssertNil(error, "no error saving to database");
206 return true;
207 }];
208 }
209
210 - (void)testFixupFetchAllTLKShareRecords {
211 // In <rdar://problem/34901306> CKKSTLK: TLKShare CloudKit upload/download on TLK change, trust set addition,
212 // we added the TLKShare CKRecord type. Upgrading devices must fetch all such records when they come online for the first time.
213
214 // Note that this already does TLK sharing, and so technically doesn't need to do the fixup, but we'll fix that later.
215 [self startCKKSSubsystem];
216 [self performOctagonTLKUpload:self.ckksViews];
217
218 [self waitForCKModifications];
219 OCMVerifyAllWithDelay(self.mockDatabase, 20);
220
221 // Tear down the CKKS object
222 [self.keychainView halt];
223 [self setFixupNumber:CKKSFixupRefetchCurrentItemPointers ckks:self.keychainView];
224
225 // Also, create a TLK share record that CKKS should find
226 // Make another share, but from an untrusted peer to some other peer. local shouldn't necessarily care.
227 NSError* error = nil;
228
229 CKKSSOSSelfPeer* remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
230 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
231 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
232 viewList:self.managedViewList];
233
234 CKKSTLKShareRecord* share = [CKKSTLKShareRecord share:self.keychainZoneKeys.tlk
235 as:remotePeer1
236 to:self.mockSOSAdapter.selfPeer
237 epoch:-1
238 poisoned:0
239 error:&error];
240 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
241 XCTAssertNotNil(share, "Should be able to create a share");
242
243 CKRecord* shareCKRecord = [share CKRecordWithZoneID: self.keychainZoneID];
244 XCTAssertNotNil(shareCKRecord, "Should have been able to create a CKRecord");
245 [self.keychainZone addToZone:shareCKRecord];
246
247 // Now, restart CKKS
248 self.silentFetchesAllowed = false;
249 [self expectCKFetchByQuery];
250
251 // We want to ensure that the key hierarchy state machine doesn't progress past fixup until we let this go
252 [self holdCloudKitFetches];
253
254 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
255 [self beginSOSTrustedViewOperation:self.keychainView];
256
257 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForFixupOperation] wait:20*NSEC_PER_SEC], "Key state should become waitforfixup");
258
259 self.silentFetchesAllowed = true;
260 [self releaseCloudKitFetchHold];
261 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
262
263 OCMVerifyAllWithDelay(self.mockDatabase, 20);
264
265 [self.keychainView.lastFixupOperation waitUntilFinished];
266 XCTAssertNil(self.keychainView.lastFixupOperation.error, "Shouldn't have been any error performing fixup");
267
268 // and check that the share made it
269 [self.keychainView dispatchSync:^bool {
270 NSError* blockerror = nil;
271 CKKSTLKShareRecord* localshare = [CKKSTLKShareRecord tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
272 XCTAssertNil(blockerror, "Shouldn't error finding new TLKShare record in database");
273 XCTAssertNotNil(localshare, "Should be able to find a new TLKShare record in database");
274 return true;
275 }];
276 }
277
278 - (void)testFixupLocalReload {
279 // In <rdar://problem/35540228> Server Generated CloudKit "Manatee Identity Lost"
280 // items could be deleted from the local keychain after CKKS believed they were already synced, and therefore wouldn't resync
281
282 [self startCKKSSubsystem];
283 [self performOctagonTLKUpload:self.ckksViews];
284
285 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
286 OCMVerifyAllWithDelay(self.mockDatabase, 20);
287 [self waitForCKModifications];
288
289 [self addGenericPassword: @"data" account: @"first"];
290 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
291 OCMVerifyAllWithDelay(self.mockDatabase, 20);
292 [self waitForCKModifications];
293
294 // Add another record to mock up early CKKS record saving
295 __block CKRecordID* secondRecordID = nil;
296 CKKSCondition* secondRecordIDFilled = [[CKKSCondition alloc] init];
297 [self addGenericPassword: @"data" account: @"second"];
298 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
299 secondRecordID = record.recordID;
300 [secondRecordIDFilled fulfill];
301 return TRUE;
302 }];
303 OCMVerifyAllWithDelay(self.mockDatabase, 20);
304 [self waitForCKModifications];
305 XCTAssertNotNil(secondRecordID, "Should have filled in secondRecordID");
306 XCTAssertEqual(0, [secondRecordIDFilled wait:20*NSEC_PER_SEC], "Should have filled in secondRecordID within enough time");
307
308 // Tear down the CKKS object
309 [self.keychainView halt];
310 [self setFixupNumber:CKKSFixupFetchTLKShares ckks:self.keychainView];
311
312 // Delete items from keychain
313 [self deleteGenericPassword:@"first"];
314
315 // Corrupt the second item's CKMirror entry to only contain system fields in the CKRecord portion (to emulate early CKKS behavior)
316 [self.keychainView dispatchSync:^bool {
317 NSError* error = nil;
318 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase:secondRecordID.recordName zoneID:self.keychainZoneID error:&error];
319 XCTAssertNil(error, "Should have no error pulling second CKKSMirrorEntry from database");
320
321 NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES];
322 [ckme.item.storedCKRecord encodeSystemFieldsWithCoder:archiver];
323 ckme.item.encodedCKRecord = archiver.encodedData;
324
325 [ckme saveToDatabase:&error];
326 XCTAssertNil(error, "No error saving system-fielded CKME back to database");
327 return true;
328 }];
329
330 // Now, restart CKKS, but place a hold on the fixup operation
331 self.silentFetchesAllowed = false;
332 self.accountStatus = CKAccountStatusCouldNotDetermine;
333 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
334
335 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
336 [self beginSOSTrustedViewOperation:self.keychainView];
337 self.keychainView.holdFixupOperation = [CKKSResultOperation named:@"hold-fixup" withBlock:^{}];
338
339 self.accountStatus = CKAccountStatusAvailable;
340 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
341
342 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForFixupOperation] wait:20*NSEC_PER_SEC], "Key state should become waitforfixup");
343 [self.operationQueue addOperation: self.keychainView.holdFixupOperation];
344 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
345
346 [self.keychainView.lastFixupOperation waitUntilFinished];
347 XCTAssertNil(self.keychainView.lastFixupOperation.error, "Shouldn't have been any error performing fixup");
348
349 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
350
351 // And the item should be back!
352 [self checkGenericPassword: @"data" account: @"first"];
353 }
354
355 - (void)testFixupResaveDeviceStateEntries {
356 // In <rdar://problem/50612776>, we introduced a new field to DeviceStateEntries. But, Peace couldn't change the DB schema to match newer Yukons
357 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
358 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
359 [self saveTLKMaterialToKeychain:self.keychainZoneID];
360 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
361
362 [self startCKKSSubsystem];
363
364 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
365 OCMVerifyAllWithDelay(self.mockDatabase, 20);
366 [self waitForCKModifications];
367
368
369 // Modify the sqlite DB to simulate how earlier verions would save these records
370 [self.keychainView dispatchSync:^bool {
371 NSError* error = nil;
372 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry fromDatabase:self.remoteSOSOnlyPeer.peerID zoneID:self.keychainZoneID error:&error];
373 XCTAssertNil(error, "Should have no error pulling CKKSDeviceStateEntry from database");
374
375 XCTAssertNotNil(cdse.octagonPeerID, "CDSE should have an octagon peer ID");
376 XCTAssertNotNil(cdse.octagonStatus, "CDSE should have an octagon status");
377 cdse.octagonPeerID = nil;
378 cdse.octagonStatus = nil;
379
380 [cdse saveToDatabase:&error];
381 XCTAssertNil(error, "No error saving modified CDSE back to database");
382 return true;
383 }];
384
385 // Tear down the CKKS object
386 [self.keychainView halt];
387 [self setFixupNumber:CKKSFixupFetchTLKShares ckks:self.keychainView];
388
389 // Now, restart CKKS
390 self.silentFetchesAllowed = false;
391 self.accountStatus = CKAccountStatusCouldNotDetermine;
392 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
393
394 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
395 [self beginSOSTrustedViewOperation:self.keychainView];
396
397 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
398
399 [self.keychainView.lastFixupOperation waitUntilFinished];
400 XCTAssertNil(self.keychainView.lastFixupOperation.error, "Shouldn't have been any error performing fixup");
401
402 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
403
404 // And all CDSEs should have an octagon peer ID again!
405 [self.keychainView dispatchSync:^bool {
406 NSError* error = nil;
407 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry fromDatabase:self.remoteSOSOnlyPeer.peerID zoneID:self.keychainZoneID error:&error];
408 XCTAssertNil(error, "Should have no error pulling CKKSDeviceStateEntry from database");
409
410 XCTAssertNotNil(cdse.octagonPeerID, "CDSE should have an octagon peer ID");
411 XCTAssertNotNil(cdse.octagonStatus, "CDSE should have an octagon status");
412 return false;
413 }];
414 }
415
416 @end
417
418 #endif // OCTAGON