]> git.saurik.com Git - apple/security.git/blob - keychain/ot/tests/octagon/OctagonTests+ErrorHandling.swift
e838a1afd2f28fbc74e70bd5ee01ccd2c6c4144f
[apple/security.git] / keychain / ot / tests / octagon / OctagonTests+ErrorHandling.swift
1 #if OCTAGON
2
3 class OctagonErrorHandlingTests: OctagonTestsBase {
4
5 func testRecoverFromImmediateTimeoutDuringEstablish() throws {
6 self.startCKAccountStatusMock()
7
8 let establishExpectation = self.expectation(description: "establishExpectation")
9
10 self.fakeCuttlefishServer.establishListener = { [unowned self] request in
11 self.fakeCuttlefishServer.establishListener = nil
12 establishExpectation.fulfill()
13
14 return CKPrettyError(domain: CKErrorDomain,
15 code: CKError.networkFailure.rawValue,
16 userInfo: [:])
17 }
18
19 _ = self.assertResetAndBecomeTrustedInDefaultContext()
20 self.wait(for: [establishExpectation], timeout: 10)
21 }
22
23 func testRecoverFromRetryableErrorDuringEstablish() throws {
24 self.startCKAccountStatusMock()
25
26 let establishExpectation = self.expectation(description: "establishExpectation")
27
28 var t0 = Date.distantPast
29
30 self.fakeCuttlefishServer.establishListener = { [unowned self] request in
31 self.fakeCuttlefishServer.establishListener = nil
32 establishExpectation.fulfill()
33
34 t0 = Date()
35 return FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .retryableServerFailure)
36 }
37
38 _ = self.assertResetAndBecomeTrustedInDefaultContext()
39 self.wait(for: [establishExpectation], timeout: 10)
40 let t1 = Date()
41 let d = t0.distance(to: t1)
42 XCTAssertGreaterThanOrEqual(d, 4)
43 // Let slower devices have a few extra seconds: we expect this after 5s, but sometimes they need a bit.
44 XCTAssertLessThanOrEqual(d, 8)
45 }
46
47 func testRecoverFromTransactionalErrorDuringJoinWithVoucher() throws {
48 self.startCKAccountStatusMock()
49
50 self.assertResetAndBecomeTrustedInDefaultContext()
51
52 var t0 = Date.distantPast
53
54 let joinExpectation = self.expectation(description: "joinExpectation")
55 self.fakeCuttlefishServer.joinListener = { [unowned self] _ in
56 self.fakeCuttlefishServer.joinListener = nil
57 joinExpectation.fulfill()
58
59 t0 = Date()
60 return FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .transactionalFailure)
61 }
62
63 let joiningContext = self.makeInitiatorContext(contextID: "joiner", authKitAdapter: self.mockAuthKit2)
64 _ = self.assertJoinViaEscrowRecovery(joiningContext: joiningContext, sponsor: self.cuttlefishContext)
65
66 self.wait(for: [joinExpectation], timeout: 10)
67 let t1 = Date()
68 let d = t0.distance(to: t1)
69 XCTAssertGreaterThanOrEqual(d, 4)
70 // Let slower devices have a few extra seconds: we expect this after 5s, but sometimes they need a bit.
71 XCTAssertLessThanOrEqual(d, 8)
72 }
73
74 func testReceiveUpdateWhileUntrustedAndLocked() {
75 self.startCKAccountStatusMock()
76
77 self.cuttlefishContext.startOctagonStateMachine()
78 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
79
80 self.aksLockState = true
81 self.lockStateTracker.recheck()
82
83 self.sendContainerChange(context: self.cuttlefishContext)
84
85 XCTAssertEqual(0, self.cuttlefishContext.stateMachine.paused.wait(10 * NSEC_PER_SEC), "state machine should pause")
86 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
87 }
88
89 func testReceiveUpdateWhileReadyAndLocked() {
90 self.startCKAccountStatusMock()
91
92 self.cuttlefishContext.startOctagonStateMachine()
93 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
94
95 do {
96 let clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
97 XCTAssertNotNil(clique, "Clique should not be nil")
98 } catch {
99 XCTFail("Shouldn't have errored making new friends: \(error)")
100 }
101
102 // Now, we should be in 'ready'
103 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
104 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
105 self.assertConsidersSelfTrustedCachedAccountStatus(context: self.cuttlefishContext)
106
107 self.aksLockState = true
108 self.lockStateTracker.recheck()
109
110 self.sendContainerChange(context: self.cuttlefishContext)
111
112 XCTAssertEqual(0, self.cuttlefishContext.stateMachine.paused.wait(10 * NSEC_PER_SEC), "state machine should pause")
113 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
114
115 self.aksLockState = false
116 self.lockStateTracker.recheck()
117 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
118
119 // and again!
120 self.aksLockState = true
121 self.lockStateTracker.recheck()
122 self.sendContainerChange(context: self.cuttlefishContext)
123
124 sleep(1)
125 XCTAssertTrue(self.cuttlefishContext.stateMachine.possiblePendingFlags().contains("recd_push"), "Should have recd_push pending flag")
126
127 let waitForUnlockStateCondition = self.cuttlefishContext.stateMachine.stateConditions[OctagonStateWaitForUnlock] as! CKKSCondition
128 XCTAssertEqual(0, self.cuttlefishContext.stateMachine.paused.wait(10 * NSEC_PER_SEC), "state machine should pause")
129 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
130
131 sleep(1)
132 // Check that we haven't been spinning
133 let sameWaitForUnlockStateCondition = self.cuttlefishContext.stateMachine.stateConditions[OctagonStateWaitForUnlock] as! CKKSCondition
134 XCTAssert(waitForUnlockStateCondition == sameWaitForUnlockStateCondition, "Conditions should be the same (as the state machine should be halted)")
135
136 self.aksLockState = false
137 self.lockStateTracker.recheck()
138
139 sleep(1)
140
141 XCTAssertEqual(self.cuttlefishContext.stateMachine.possiblePendingFlags(), [], "Should have 0 pending flags")
142
143 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
144 }
145
146 func testReceiveUpdateWhileReadyAndAuthkitRetry() {
147 self.startCKAccountStatusMock()
148
149 self.cuttlefishContext.startOctagonStateMachine()
150 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
151
152 do {
153 let clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
154 XCTAssertNotNil(clique, "Clique should not be nil")
155 } catch {
156 XCTFail("Shouldn't have errored making new friends: \(error)")
157 }
158
159 // Now, we should be in 'ready'
160 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
161 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
162 self.assertConsidersSelfTrustedCachedAccountStatus(context: self.cuttlefishContext)
163
164 self.mockAuthKit.machineIDFetchErrors.append(CKPrettyError(domain: CKErrorDomain,
165 code: CKError.networkUnavailable.rawValue,
166 userInfo: [CKErrorRetryAfterKey: 2]))
167
168 self.sendContainerChange(context: self.cuttlefishContext)
169
170 XCTAssertEqual(0, self.cuttlefishContext.stateMachine.paused.wait(10 * NSEC_PER_SEC), "state machine should pause")
171 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
172
173 self.sendContainerChange(context: self.cuttlefishContext)
174 XCTAssertEqual(0, self.cuttlefishContext.stateMachine.paused.wait(10 * NSEC_PER_SEC), "state machine should pause")
175 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
176 }
177
178 func testReceiveUpdateWhileReadyAndLockedAndAuthkitRetry() {
179 self.startCKAccountStatusMock()
180
181 self.cuttlefishContext.startOctagonStateMachine()
182 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
183
184 do {
185 let clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
186 XCTAssertNotNil(clique, "Clique should not be nil")
187 } catch {
188 XCTFail("Shouldn't have errored making new friends: \(error)")
189 }
190
191 // Now, we should be in 'ready'
192 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
193 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
194 self.assertConsidersSelfTrustedCachedAccountStatus(context: self.cuttlefishContext)
195
196 self.aksLockState = true
197 self.lockStateTracker.recheck()
198
199 self.mockAuthKit.machineIDFetchErrors.append(CKPrettyError(domain: CKErrorDomain,
200 code: CKError.networkUnavailable.rawValue,
201 userInfo: [CKErrorRetryAfterKey: 2]))
202
203 self.sendContainerChange(context: self.cuttlefishContext)
204
205 XCTAssertEqual(0, self.cuttlefishContext.stateMachine.paused.wait(10 * NSEC_PER_SEC), "state machine should pause")
206 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
207
208 self.sendContainerChange(context: self.cuttlefishContext)
209 XCTAssertEqual(0, self.cuttlefishContext.stateMachine.paused.wait(10 * NSEC_PER_SEC), "state machine should pause")
210 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
211
212 sleep(1)
213
214 XCTAssertTrue(self.cuttlefishContext.stateMachine.possiblePendingFlags().contains("recd_push"), "Should have recd_push pending flag")
215
216 self.aksLockState = false
217 self.lockStateTracker.recheck()
218
219 sleep(1)
220
221 XCTAssertEqual(self.cuttlefishContext.stateMachine.possiblePendingFlags(), [], "Should have 0 pending flags")
222 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
223 }
224
225 func testReceiveTransactionErrorDuringUpdate() {
226 self.startCKAccountStatusMock()
227
228 self.cuttlefishContext.startOctagonStateMachine()
229 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
230
231 do {
232 let clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
233 XCTAssertNotNil(clique, "Clique should not be nil")
234 } catch {
235 XCTFail("Shouldn't have errored making new friends: \(error)")
236 }
237
238 // Now, we should be in 'ready'
239 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
240 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
241 self.assertConsidersSelfTrustedCachedAccountStatus(context: self.cuttlefishContext)
242
243 let pre = self.fakeCuttlefishServer.fetchChangesCalledCount
244
245 let ckError = FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .transactionalFailure)
246 self.fakeCuttlefishServer.nextFetchErrors.append(ckError)
247
248 self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
249
250 sleep(5)
251
252 XCTAssertEqual(0, self.cuttlefishContext.stateMachine.paused.wait(10 * NSEC_PER_SEC), "state machine should pause")
253 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
254
255 XCTAssertEqual(self.cuttlefishContext.stateMachine.possiblePendingFlags(), [], "Should have zero pending flags after retry")
256
257 let post = self.fakeCuttlefishServer.fetchChangesCalledCount
258 XCTAssertEqual(post, pre + 2, "should have fetched two times, the first response would have been a transaction error")
259 }
260
261 func testPreapprovedPushWhileLocked() throws {
262 // Peer 1 becomes SOS+Octagon
263 self.putFakeKeyHierarchy(inCloudKit: self.manateeZoneID)
264 self.putSelfTLKShares(inCloudKit: self.manateeZoneID)
265 self.saveTLKMaterial(toKeychain: self.manateeZoneID)
266
267 XCTAssertTrue(OctagonPerformSOSUpgrade(), "SOS upgrade should be on")
268
269 self.mockSOSAdapter.circleStatus = SOSCCStatus(kSOSCCInCircle)
270 self.startCKAccountStatusMock()
271
272 self.cuttlefishContext.startOctagonStateMachine()
273
274 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
275 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
276
277 let peerID = try self.cuttlefishContext.accountMetadataStore.getEgoPeerID()
278 XCTAssertNotNil(peerID, "Should have a peer ID after making new friends")
279
280 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
281
282 // Peer 2 attempts to join via preapprovalh
283 let peer2SOSMockPeer = self.createSOSPeer(peerID: "peer2ID")
284 let peer2contextID = "peer2"
285 let peer2mockSOS = CKKSMockSOSPresentAdapter(selfPeer: peer2SOSMockPeer, trustedPeers: self.mockSOSAdapter.allPeers(), essential: false)
286 let peer2 = self.manager.context(forContainerName: OTCKContainerName,
287 contextID: peer2contextID,
288 sosAdapter: peer2mockSOS,
289 authKitAdapter: self.mockAuthKit2,
290 lockStateTracker: self.lockStateTracker,
291 accountStateTracker: self.accountStateTracker,
292 deviceInformationAdapter: self.makeInitiatorDeviceInfoAdapter())
293
294 peer2.startOctagonStateMachine()
295
296 self.assertEnters(context: peer2, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
297
298 // Now, Peer1 should preapprove Peer2
299 let peer2Preapproval = TPHashBuilder.hash(with: .SHA256, of: peer2SOSMockPeer.publicSigningKey.encodeSubjectPublicKeyInfo())
300
301 self.mockSOSAdapter.trustedPeers.add(peer2SOSMockPeer)
302 self.mockSOSAdapter.sendTrustedPeerSetChangedUpdate()
303
304 // Peer1 should upload TLKs for Peer2
305 self.assertAllCKKSViewsUpload(tlkShares: 1)
306
307 let updateTrustExpectation = self.expectation(description: "updateTrust")
308 self.fakeCuttlefishServer.updateListener = { request in
309 XCTAssertEqual(peerID, request.peerID, "updateTrust request should be for ego peer ID")
310 XCTAssertTrue(request.hasDynamicInfoAndSig, "updateTrust request should have a dynamic info")
311 let newDynamicInfo = TPPeerDynamicInfo(data: request.dynamicInfoAndSig.peerDynamicInfo, sig: request.dynamicInfoAndSig.sig)
312 XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamic info from protobuf")
313
314 XCTAssertEqual(newDynamicInfo!.preapprovals.count, 1, "Should have a single preapproval")
315 XCTAssertTrue(newDynamicInfo!.preapprovals.contains(peer2Preapproval), "Octagon peer should preapprove new SOS peer")
316
317 // But, since this is an SOS peer and TLK uploads for those peers are currently handled through CK CRUD operations, this update
318 // shouldn't have any TLKShares
319 XCTAssertEqual(0, request.tlkShares.count, "Trust update should not have any new TLKShares")
320
321 updateTrustExpectation.fulfill()
322 return nil
323 }
324
325 self.verifyDatabaseMocks()
326 self.wait(for: [updateTrustExpectation], timeout: 100)
327 self.fakeCuttlefishServer.updateListener = nil
328
329 // Now, peer 2 should lock and receive an Octagon push
330 self.aksLockState = true
331 self.lockStateTracker.recheck()
332
333 // Now, peer2 should receive an Octagon push, try to realize it is preapproved, and get stuck
334 self.sendContainerChange(context: peer2)
335 self.assertEnters(context: peer2, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
336 sleep(1)
337
338 XCTAssertTrue(peer2.stateMachine.possiblePendingFlags().contains("recd_push"), "Should have recd_push pending flag")
339
340 self.aksLockState = false
341 self.lockStateTracker.recheck()
342
343 sleep(1)
344
345 XCTAssertEqual(self.cuttlefishContext.stateMachine.possiblePendingFlags(), [], "Should have 0 pending flags")
346 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
347
348 self.assertEnters(context: peer2, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
349 }
350
351 func testReceiveMachineListUpdateWhileReadyAndLocked() throws {
352 // Peer 1 becomes SOS+Octagon
353 self.putFakeKeyHierarchy(inCloudKit: self.manateeZoneID)
354 self.putSelfTLKShares(inCloudKit: self.manateeZoneID)
355 self.saveTLKMaterial(toKeychain: self.manateeZoneID)
356
357 XCTAssertTrue(OctagonPerformSOSUpgrade(), "SOS upgrade should be on")
358
359 self.startCKAccountStatusMock()
360 self.cuttlefishContext.startOctagonStateMachine()
361
362 let clique: OTClique
363 do {
364 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
365 XCTAssertNotNil(clique, "Clique should not be nil")
366 } catch {
367 XCTFail("Shouldn't have errored making new friends: \(error)")
368 throw error
369 }
370
371 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
372 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
373
374 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
375
376 // Peer 2 arrives (with a voucher), but is not on the trusted device list
377 let firstPeerID = clique.cliqueMemberIdentifier
378 XCTAssertNotNil(firstPeerID, "Clique should have a member identifier")
379 let bottle = self.fakeCuttlefishServer.state.bottles[0]
380 let entropy = try self.loadSecret(label: firstPeerID!)
381 XCTAssertNotNil(entropy, "entropy should not be nil")
382
383 let bNewOTCliqueContext = OTConfigurationContext()
384 bNewOTCliqueContext.context = "restoreB"
385 bNewOTCliqueContext.dsid = self.otcliqueContext.dsid
386 bNewOTCliqueContext.altDSID = self.otcliqueContext.altDSID
387 bNewOTCliqueContext.otControl = self.otcliqueContext.otControl
388 bNewOTCliqueContext.sbd = OTMockSecureBackup(bottleID: bottle.bottleID, entropy: entropy!)
389
390 let deviceBmockAuthKit = OTMockAuthKitAdapter(altDSID: self.otcliqueContext.altDSID,
391 machineID: "b-machine-id",
392 otherDevices: [self.mockAuthKit.currentMachineID])
393
394 let bRestoreContext = self.manager.context(forContainerName: OTCKContainerName,
395 contextID: bNewOTCliqueContext.context!,
396 sosAdapter: OTSOSMissingAdapter(),
397 authKitAdapter: deviceBmockAuthKit,
398 lockStateTracker: self.lockStateTracker,
399 accountStateTracker: self.accountStateTracker,
400 deviceInformationAdapter: self.makeInitiatorDeviceInfoAdapter())
401
402 bRestoreContext.startOctagonStateMachine()
403
404 self.sendContainerChange(context: bRestoreContext)
405 let bNewClique: OTClique
406 do {
407 bNewClique = try OTClique.performEscrowRecovery(withContextData: bNewOTCliqueContext, escrowArguments: [:])
408 XCTAssertNotNil(bNewClique, "bNewClique should not be nil")
409 } catch {
410 XCTFail("Shouldn't have errored recovering: \(error)")
411 throw error
412 }
413 self.assertEnters(context: bRestoreContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
414
415 // Device A notices, but doesn't update (because B isn't on the device list)
416 self.fakeCuttlefishServer.updateListener = { _ in
417 XCTFail("Should not have updated trust")
418 return nil
419 }
420
421 // Device A locks and gets the device list notification
422 self.aksLockState = true
423 self.lockStateTracker.recheck()
424
425 self.mockAuthKit.otherDevices.insert(deviceBmockAuthKit.currentMachineID)
426
427 self.cuttlefishContext.incompleteNotificationOfMachineIDListChange()
428 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateWaitForUnlock, within: 10 * NSEC_PER_SEC)
429
430 let updateTrustExpectation = self.expectation(description: "updateTrust")
431 self.fakeCuttlefishServer.updateListener = { request in
432 XCTAssertEqual(firstPeerID, request.peerID, "updateTrust request should be for ego peer ID")
433 XCTAssertTrue(request.hasDynamicInfoAndSig, "updateTrust request should have a dynamic info")
434 let newDynamicInfo = TPPeerDynamicInfo(data: request.dynamicInfoAndSig.peerDynamicInfo, sig: request.dynamicInfoAndSig.sig)
435 XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamic info from protobuf")
436
437 XCTAssertEqual(newDynamicInfo?.includedPeerIDs.count, 2, "Should trust both peers")
438 updateTrustExpectation.fulfill()
439 return nil
440 }
441
442 self.sendContainerChange(context: self.cuttlefishContext)
443 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateWaitForUnlock, within: 10 * NSEC_PER_SEC)
444
445 // And on unlock, it should handle the update
446 self.aksLockState = false
447 self.lockStateTracker.recheck()
448
449 self.wait(for: [updateTrustExpectation], timeout: 30)
450 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
451 }
452
453 func testCKKSResetRecoverFromCKKSConflict() throws {
454 self.putFakeKeyHierarchy(inCloudKit: self.manateeZoneID)
455 self.putFakeDeviceStatus(inCloudKit: self.manateeZoneID)
456 // But do NOT add them to the keychain
457
458 // CKKS should get stuck in waitfortlk
459 self.startCKAccountStatusMock()
460 self.cuttlefishContext.startOctagonStateMachine()
461 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
462
463 do {
464 let clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
465 XCTAssertNotNil(clique, "Clique should not be nil")
466 } catch {
467 XCTFail("Shouldn't have errored making new friends: \(error)")
468 }
469
470 // Now, we should be in 'ready', and CKKS should be stuck
471 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
472 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
473 self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateWaitForTLK, within: 10 * NSEC_PER_SEC)
474
475 // Now, CKKS decides to reset the world, but a conflict occurs on hierarchy upload
476 self.silentZoneDeletesAllowed = true
477 var tlkUUIDs : [CKRecordZone.ID:String] = [:]
478
479 self.silentFetchesAllowed = false
480 self.expectCKFetchAndRun(beforeFinished: {
481 self.putFakeKeyHierarchy(inCloudKit: self.manateeZoneID)
482 self.putFakeDeviceStatus(inCloudKit: self.manateeZoneID)
483 self.silentFetchesAllowed = true
484
485 // Use the commented version below when multi-zone support is readded to the tets
486 tlkUUIDs[self.manateeZoneID!] = (self.keys![self.manateeZoneID!] as? ZoneKeys)?.tlk?.uuid
487 /*
488 for zoneID in self.ckksZones {
489 tlkUUIDs[zoneID as! CKRecordZone.ID] = (self.keys![zoneID] as? ZoneKeys)?.tlk?.uuid
490 }
491 */
492 })
493
494 let resetExepctation = self.expectation(description: "reset callback is called")
495 self.cuttlefishContext.viewManager!.rpcResetCloudKit(nil, reason: "unit-test") {
496 error in
497 XCTAssertNil(error, "should be no error resetting cloudkit")
498 resetExepctation.fulfill()
499 }
500
501 // Deletions should occur, then the fetches, then get stuck (as we don't have the TLK)
502 self.wait(for: [resetExepctation], timeout: 10)
503 self.verifyDatabaseMocks()
504
505 // all subCKKSes should get stuck in waitfortlk
506 self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateWaitForTLK, within: 10 * NSEC_PER_SEC)
507 self.verifyDatabaseMocks()
508
509 XCTAssertEqual(tlkUUIDs.count, self.ckksZones.count, "Should have the right number of conflicted TLKs")
510 for (zoneID,tlkUUID) in tlkUUIDs {
511 XCTAssertEqual(tlkUUID, (self.keys![zoneID] as? ZoneKeys)?.tlk?.uuid, "TLK should match conflicted version")
512 }
513 }
514 }
515
516 #endif