]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CloudKitKeychainSyncingFixupTests.m
Security-59754.41.1.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 [[[mockFixups reject] 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:@"50184A35-4480-E8BA-769B-567CF72F1EC0"
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:@"10E76B80-CE1C-A52A-B0CB-462A2EBA05AF"
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.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
122 [self.keychainView waitForFetchAndIncomingQueueProcessing];
123
124 // Tear down the CKKS object
125 [self.keychainView halt];
126
127 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
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:@"10E76B80-CE1C-A52A-B0CB-462A2EBA05AF"
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 CKKSDatabaseTransactionCommit;
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 dispatchSyncWithReadOnlySQLTransaction:^{
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 }];
191 }
192
193 - (void)setFixupNumber:(CKKSFixup)newFixup ckks:(CKKSKeychainView*)ckks {
194 [ckks dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
195 // Edit the zone state entry to have no fixups
196 NSError* error = nil;
197 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:ckks.zoneID.zoneName error:&error];
198
199 XCTAssertNil(error, "no error pulling ckse from database");
200 XCTAssertNotNil(ckse, "received a ckse");
201
202 ckse.lastFixup = newFixup;
203 [ckse saveToDatabase: &error];
204 XCTAssertNil(error, "no error saving to database");
205 return CKKSDatabaseTransactionCommit;
206 }];
207 }
208
209 - (void)testFixupFetchAllTLKShareRecords {
210 // In <rdar://problem/34901306> CKKSTLK: TLKShare CloudKit upload/download on TLK change, trust set addition,
211 // we added the TLKShare CKRecord type. Upgrading devices must fetch all such records when they come online for the first time.
212
213 // Note that this already does TLK sharing, and so technically doesn't need to do the fixup, but we'll fix that later.
214 [self startCKKSSubsystem];
215 [self performOctagonTLKUpload:self.ckksViews];
216
217 [self waitForCKModifications];
218 OCMVerifyAllWithDelay(self.mockDatabase, 20);
219
220 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
221
222 // Tear down the CKKS object
223 [self.keychainView halt];
224 [self setFixupNumber:CKKSFixupRefetchCurrentItemPointers ckks:self.keychainView];
225
226 // Also, create a TLK share record that CKKS should find
227 // Make another share, but from an untrusted peer to some other peer. local shouldn't necessarily care.
228 NSError* error = nil;
229
230 CKKSSOSSelfPeer* remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
231 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
232 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
233 viewList:self.managedViewList];
234
235 CKKSTLKShareRecord* share = [CKKSTLKShareRecord share:self.keychainZoneKeys.tlk
236 as:remotePeer1
237 to:self.mockSOSAdapter.selfPeer
238 epoch:-1
239 poisoned:0
240 error:&error];
241 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
242 XCTAssertNotNil(share, "Should be able to create a share");
243
244 CKRecord* shareCKRecord = [share CKRecordWithZoneID: self.keychainZoneID];
245 XCTAssertNotNil(shareCKRecord, "Should have been able to create a CKRecord");
246 [self.keychainZone addToZone:shareCKRecord];
247
248 // Now, restart CKKS
249 self.silentFetchesAllowed = false;
250 [self expectCKFetchByQuery];
251
252 // We want to ensure that the key hierarchy state machine doesn't progress past fixup until we let this go
253 [self holdCloudKitFetches];
254
255 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
256 [self beginSOSTrustedViewOperation:self.keychainView];
257
258 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForFixupOperation] wait:20*NSEC_PER_SEC], "Key state should become waitforfixup");
259
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 dispatchSyncWithReadOnlySQLTransaction:^{
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 }];
275 }
276
277 - (void)testFixupLocalReload {
278 // In <rdar://problem/35540228> Server Generated CloudKit "Manatee Identity Lost"
279 // items could be deleted from the local keychain after CKKS believed they were already synced, and therefore wouldn't resync
280
281 [self startCKKSSubsystem];
282 [self performOctagonTLKUpload:self.ckksViews];
283
284 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
285 OCMVerifyAllWithDelay(self.mockDatabase, 20);
286 [self waitForCKModifications];
287
288 [self addGenericPassword: @"data" account: @"first"];
289 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
290 OCMVerifyAllWithDelay(self.mockDatabase, 20);
291 [self waitForCKModifications];
292
293 // Add another record to mock up early CKKS record saving
294 __block CKRecordID* secondRecordID = nil;
295 CKKSCondition* secondRecordIDFilled = [[CKKSCondition alloc] init];
296 [self addGenericPassword: @"data" account: @"second"];
297 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
298 secondRecordID = record.recordID;
299 [secondRecordIDFilled fulfill];
300 return TRUE;
301 }];
302 OCMVerifyAllWithDelay(self.mockDatabase, 20);
303 [self waitForCKModifications];
304 XCTAssertNotNil(secondRecordID, "Should have filled in secondRecordID");
305 XCTAssertEqual(0, [secondRecordIDFilled wait:20*NSEC_PER_SEC], "Should have filled in secondRecordID within enough time");
306
307 // Tear down the CKKS object
308 [self.keychainView halt];
309 [self setFixupNumber:CKKSFixupFetchTLKShares ckks:self.keychainView];
310
311 // Delete items from keychain
312 [self deleteGenericPassword:@"first"];
313
314 // Corrupt the second item's CKMirror entry to only contain system fields in the CKRecord portion (to emulate early CKKS behavior)
315 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
316 NSError* error = nil;
317 CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase:secondRecordID.recordName zoneID:self.keychainZoneID error:&error];
318 XCTAssertNil(error, "Should have no error pulling second CKKSMirrorEntry from database");
319
320 NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES];
321 [ckme.item.storedCKRecord encodeSystemFieldsWithCoder:archiver];
322 ckme.item.encodedCKRecord = archiver.encodedData;
323
324 [ckme saveToDatabase:&error];
325 XCTAssertNil(error, "No error saving system-fielded CKME back to database");
326 return CKKSDatabaseTransactionCommit;
327 }];
328
329 // Now, restart CKKS, but place a hold on the fixup operation
330 self.silentFetchesAllowed = false;
331 self.accountStatus = CKAccountStatusCouldNotDetermine;
332 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
333
334 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
335 [self beginSOSTrustedViewOperation:self.keychainView];
336 self.keychainView.holdFixupOperation = [CKKSResultOperation named:@"hold-fixup" withBlock:^{}];
337
338 self.accountStatus = CKAccountStatusAvailable;
339 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
340
341 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForFixupOperation] wait:20*NSEC_PER_SEC], "Key state should become waitforfixup");
342 [self.operationQueue addOperation: self.keychainView.holdFixupOperation];
343 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
344
345 [self.keychainView.lastFixupOperation waitUntilFinished];
346 XCTAssertNil(self.keychainView.lastFixupOperation.error, "Shouldn't have been any error performing fixup");
347
348 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
349
350 // And the item should be back!
351 [self checkGenericPassword: @"data" account: @"first"];
352 }
353
354 - (void)testFixupResaveDeviceStateEntries {
355 // In <rdar://problem/50612776>, we introduced a new field to DeviceStateEntries. But, Peace couldn't change the DB schema to match newer Yukons
356 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
357 [self putFakeOctagonOnlyDeviceStatusInCloudKit:self.keychainZoneID];
358 [self saveTLKMaterialToKeychain:self.keychainZoneID];
359 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
360
361 [self startCKKSSubsystem];
362
363 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
364 OCMVerifyAllWithDelay(self.mockDatabase, 20);
365 [self waitForCKModifications];
366
367
368 // Modify the sqlite DB to simulate how earlier verions would save these records
369 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
370 NSError* error = nil;
371 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry fromDatabase:self.remoteSOSOnlyPeer.peerID zoneID:self.keychainZoneID error:&error];
372 XCTAssertNil(error, "Should have no error pulling CKKSDeviceStateEntry from database");
373
374 XCTAssertNotNil(cdse.octagonPeerID, "CDSE should have an octagon peer ID");
375 XCTAssertNotNil(cdse.octagonStatus, "CDSE should have an octagon status");
376 cdse.octagonPeerID = nil;
377 cdse.octagonStatus = nil;
378
379 [cdse saveToDatabase:&error];
380 XCTAssertNil(error, "No error saving modified CDSE back to database");
381 return CKKSDatabaseTransactionCommit;
382 }];
383
384 // Tear down the CKKS object
385 [self.keychainView halt];
386 [self setFixupNumber:CKKSFixupFetchTLKShares ckks:self.keychainView];
387
388 // Now, restart CKKS
389 self.silentFetchesAllowed = false;
390 self.accountStatus = CKAccountStatusCouldNotDetermine;
391 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
392
393 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
394 [self beginSOSTrustedViewOperation:self.keychainView];
395
396 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
397
398 [self.keychainView.lastFixupOperation waitUntilFinished];
399 XCTAssertNil(self.keychainView.lastFixupOperation.error, "Shouldn't have been any error performing fixup");
400
401 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
402
403 // And all CDSEs should have an octagon peer ID again!
404 [self.keychainView dispatchSyncWithReadOnlySQLTransaction:^{
405 NSError* error = nil;
406 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry fromDatabase:self.remoteSOSOnlyPeer.peerID zoneID:self.keychainZoneID error:&error];
407 XCTAssertNil(error, "Should have no error pulling CKKSDeviceStateEntry from database");
408
409 XCTAssertNotNil(cdse.octagonPeerID, "CDSE should have an octagon peer ID");
410 XCTAssertNotNil(cdse.octagonStatus, "CDSE should have an octagon status");
411 }];
412 }
413
414 - (void)testFixupDeletesTombstoneEntries {
415 [self startCKKSSubsystem];
416 [self performOctagonTLKUpload:self.ckksViews];
417
418 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
419 OCMVerifyAllWithDelay(self.mockDatabase, 20);
420 [self waitForCKModifications];
421
422 // The CKKS stack now rejects tombstone items. So, let's inject one out of band.
423
424 [self.keychainView halt];
425
426 [self setFixupNumber:CKKSFixupResaveDeviceStateEntries ckks:self.keychainView];
427
428 CKRecord* ckr = [self createFakeTombstoneRecord:self.keychainZoneID
429 recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"
430 account:@"account-delete-me"];
431 [self.keychainZone addToZone:ckr];
432
433 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
434 CKKSMirrorEntry* ckme = [[CKKSMirrorEntry alloc] initWithCKRecord:ckr];
435 NSError* error = nil;
436 [ckme saveToDatabase:&error];
437
438 XCTAssertNil(error, "Should be no error saving the CKME to the database");
439 return CKKSDatabaseTransactionCommit;
440 }];
441
442 // Now, restart CKKS. The bad record should be deleted.
443 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
444
445 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
446 [self beginSOSTrustedViewOperation:self.keychainView];
447
448 // Deletions should occur
449 OCMVerifyAllWithDelay(self.mockDatabase, 20);
450
451 [self.keychainView.lastFixupOperation waitUntilFinished];
452 XCTAssertNil(self.keychainView.lastFixupOperation.error, "Shouldn't have been any error performing fixup");
453
454 [self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
455 }
456
457 @end
458
459 #endif // OCTAGON