#if OCTAGON class OctagonSOSTests: OctagonTestsBase { func testSOSOctagonKeyConsistency() throws { self.putFakeKeyHierarchiesInCloudKit() self.putSelfTLKSharesInCloudKit() self.saveTLKMaterialToKeychain() self.startCKAccountStatusMock() self.mockSOSAdapter.circleStatus = SOSCCStatus(kSOSCCInCircle) XCTAssertTrue(OctagonPerformSOSUpgrade(), "SOS upgrade should be on") self.cuttlefishContext.startOctagonStateMachine() self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertConsidersSelfTrusted(context: self.cuttlefishContext) assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) self.verifyDatabaseMocks() self.waitForCKModifications() self.assertSelfTLKSharesInCloudKit(context: self.cuttlefishContext) let peerID = try self.cuttlefishContext.accountMetadataStore.getEgoPeerID() XCTAssertNotNil(peerID, "Should have a peer ID") // CKKS will upload new TLKShares self.assertAllCKKSViewsUpload(tlkShares: 2) let newSOSPeer = createSOSPeer(peerID: peerID) self.mockSOSAdapter.selfPeer = newSOSPeer self.mockSOSAdapter.trustedPeers.add(newSOSPeer) // Now restart the context self.manager.removeContext(forContainerName: OTCKContainerName, contextID: OTDefaultContext) self.restartCKKSViews() self.cuttlefishContext = self.manager.context(forContainerName: OTCKContainerName, contextID: OTDefaultContext) self.cuttlefishContext.startOctagonStateMachine() self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertConsidersSelfTrustedCachedAccountStatus(context: self.cuttlefishContext) let restartedPeerID = try self.cuttlefishContext.accountMetadataStore.getEgoPeerID() XCTAssertNotNil(restartedPeerID, "Should have a peer ID after restarting") XCTAssertEqual(peerID, restartedPeerID, "Should have the same peer ID after restarting") assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) self.verifyDatabaseMocks() } func testSOSOctagonKeyConsistencyLocked() throws { self.putFakeKeyHierarchiesInCloudKit() self.putSelfTLKSharesInCloudKit() self.saveTLKMaterialToKeychain() self.startCKAccountStatusMock() self.mockSOSAdapter.circleStatus = SOSCCStatus(kSOSCCInCircle) XCTAssertTrue(OctagonPerformSOSUpgrade(), "SOS upgrade should be on") self.cuttlefishContext.startOctagonStateMachine() self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertConsidersSelfTrusted(context: self.cuttlefishContext) assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) self.verifyDatabaseMocks() self.waitForCKModifications() self.assertSelfTLKSharesInCloudKit(context: self.cuttlefishContext) let peerID = try self.cuttlefishContext.accountMetadataStore.getEgoPeerID() XCTAssertNotNil(peerID, "Should have a peer ID") let newSOSPeer = createSOSPeer(peerID: peerID) self.mockSOSAdapter.selfPeer = newSOSPeer self.mockSOSAdapter.trustedPeers.add(newSOSPeer) self.aksLockState = true self.lockStateTracker.recheck() assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) // Now restart the context self.manager.removeContext(forContainerName: OTCKContainerName, contextID: OTDefaultContext) self.restartCKKSViews() self.cuttlefishContext = self.manager.context(forContainerName: OTCKContainerName, contextID: OTDefaultContext) self.cuttlefishContext.startOctagonStateMachine() self.assertEnters(context: self.cuttlefishContext, state: OctagonStateWaitForUnlock, within: 10 * NSEC_PER_SEC) assertAllCKKSViews(enter: SecCKKSZoneKeyStateWaitForTrust, within: 10 * NSEC_PER_SEC) self.assertAllCKKSViewsUpload(tlkShares: 2) self.aksLockState = false self.lockStateTracker.recheck() self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertConsidersSelfTrustedCachedAccountStatus(context: self.cuttlefishContext) let restartedPeerID = try self.cuttlefishContext.accountMetadataStore.getEgoPeerID() XCTAssertNotNil(restartedPeerID, "Should have a peer ID after restarting") XCTAssertEqual(peerID, restartedPeerID, "Should have the same peer ID after restarting") self.verifyDatabaseMocks() self.waitForCKModifications() assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) } func testSOSOctagonKeyConsistencySucceedsAfterUpdatingSOS() throws { self.putFakeKeyHierarchiesInCloudKit() self.putSelfTLKSharesInCloudKit() self.saveTLKMaterialToKeychain() self.startCKAccountStatusMock() self.mockSOSAdapter.circleStatus = SOSCCStatus(kSOSCCInCircle) XCTAssertTrue(OctagonPerformSOSUpgrade(), "SOS upgrade should be on") self.cuttlefishContext.startOctagonStateMachine() self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertConsidersSelfTrusted(context: self.cuttlefishContext) assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) self.verifyDatabaseMocks() self.waitForCKModifications() self.assertSelfTLKSharesInCloudKit(context: self.cuttlefishContext) let peerID = try self.cuttlefishContext.accountMetadataStore.getEgoPeerID() XCTAssertNotNil(peerID, "Should have a peer ID") let newSOSPeer = createSOSPeer(peerID: peerID) self.mockSOSAdapter.selfPeer = newSOSPeer self.mockSOSAdapter.trustedPeers.add(newSOSPeer) self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertSelfTLKSharesInCloudKit(context: self.cuttlefishContext) self.assertConsidersSelfTrustedCachedAccountStatus(context: self.cuttlefishContext) assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) // Now restart the context self.manager.removeContext(forContainerName: OTCKContainerName, contextID: OTDefaultContext) self.restartCKKSViews() self.cuttlefishContext = self.manager.context(forContainerName: OTCKContainerName, contextID: OTDefaultContext) self.cuttlefishContext.startOctagonStateMachine() self.aksLockState = true self.lockStateTracker.recheck() self.assertEnters(context: self.cuttlefishContext, state: OctagonStateWaitForUnlock, within: 10 * NSEC_PER_SEC) assertAllCKKSViews(enter: SecCKKSZoneKeyStateWaitForTrust, within: 10 * NSEC_PER_SEC) self.assertAllCKKSViewsUpload(tlkShares: 2) self.aksLockState = false self.lockStateTracker.recheck() self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertConsidersSelfTrustedCachedAccountStatus(context: self.cuttlefishContext) let restartedPeerID = try self.cuttlefishContext.accountMetadataStore.getEgoPeerID() XCTAssertNotNil(restartedPeerID, "Should have a peer ID after restarting") XCTAssertEqual(peerID, restartedPeerID, "Should have the same peer ID after restarting") self.verifyDatabaseMocks() self.waitForCKModifications() assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) } func testSOSPerformOctagonKeyConsistencyOnCircleChange() throws { self.startCKAccountStatusMock() // Octagon establishes its identity before SOS joins self.mockSOSAdapter.circleStatus = SOSCCStatus(kSOSCCNotInCircle) self.assertResetAndBecomeTrustedInDefaultContext() let peerID = try self.cuttlefishContext.accountMetadataStore.getEgoPeerID() XCTAssertNotNil(peerID, "Should have a peer ID") // Now, SOS arrives let updateExpectation = self.expectation(description: "Octagon should inform SOS of its keys") self.mockSOSAdapter.updateOctagonKeySetListener = { _ in // Don't currently check the key set at all here updateExpectation.fulfill() } // CKKS will upload itself new shares (for the newly trusted SOS self peer) self.assertAllCKKSViewsUpload(tlkShares: 1) self.mockSOSAdapter.circleStatus = SOSCCStatus(kSOSCCInCircle) // Note: this should probably be sendSelfPeerChangedUpdate, but we don't have great fidelity around which peer // actually changed. So, just use this channel for now self.mockSOSAdapter.sendTrustedPeerSetChangedUpdate() self.wait(for: [updateExpectation], timeout: 10) self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) } func testDisablingSOSFeatureFlag() throws { self.startCKAccountStatusMock() OctagonSetSOSFeatureEnabled(false) let recoverykeyotcliqueContext = OTConfigurationContext() recoverykeyotcliqueContext.context = "recoveryContext" recoverykeyotcliqueContext.dsid = "1234" recoverykeyotcliqueContext.altDSID = self.mockAuthKit.altDSID! recoverykeyotcliqueContext.otControl = self.otControl var clique: OTClique do { clique = try OTClique.newFriends(withContextData: recoverykeyotcliqueContext, resetReason: .testGenerated) XCTAssertNotNil(clique, "Clique should not be nil") } catch { XCTFail("Shouldn't have errored making new friends: \(error)") throw error } do { try clique.joinAfterRestore() } catch { XCTAssertNotNil(error, "error should not be nil") } do { try clique.isLastFriend() } catch { XCTAssertNotNil(error, "error should not be nil") } do { try clique.safariPasswordSyncingEnabled() } catch { XCTAssertNotNil(error, "error should not be nil") } do { try clique.waitForInitialSync() } catch { XCTAssertNotNil(error, "error should not be nil") } clique.viewSet(Set(), disabledViews: Set()) do { try clique.setUserCredentialsAndDSID("", password: Data()) } catch { XCTAssertNotNil(error, "error should not be nil") } do { try clique.tryUserCredentialsAndDSID("", password: Data()) } catch { XCTAssertNotNil(error, "error should not be nil") } do { try clique.peersHaveViewsEnabled([""]) } catch { XCTAssertNotNil(error, "error should not be nil") } do { try clique.requestToJoinCircle() } catch { XCTAssertNotNil(error, "error should not be nil") } clique.accountUserKeyAvailable() do { _ = try clique.copyViewUnawarePeerInfo() } catch { XCTAssertNotNil(error, "error should not be nil") } do { _ = try clique.copyPeerPeerInfo() } catch { XCTAssertNotNil(error, "error should not be nil") } } func testPreapproveSOSPeersWhenInCircle() throws { self.startCKAccountStatusMock() self.mockSOSAdapter.circleStatus = SOSCCStatus(kSOSCCInCircle) let peer1Preapproval = TPHashBuilder.hash(with: .SHA256, of: self.mockSOSAdapter.selfPeer.publicSigningKey.encodeSubjectPublicKeyInfo()) let peer2SOSMockPeer = self.createSOSPeer(peerID: "peer2ID") self.mockSOSAdapter.trustedPeers.add(peer2SOSMockPeer) let peer2Preapproval = TPHashBuilder.hash(with: .SHA256, of: peer2SOSMockPeer.publicSigningKey.encodeSubjectPublicKeyInfo()) let peer3SOSMockPeer = self.createSOSPeer(peerID: "peer3ID") self.mockSOSAdapter.trustedPeers.add(peer3SOSMockPeer) let peer3Preapproval = TPHashBuilder.hash(with: .SHA256, of: peer3SOSMockPeer.publicSigningKey.encodeSubjectPublicKeyInfo()) let establishTwiceExpectation = self.expectation(description: "establish should be called twice") establishTwiceExpectation.expectedFulfillmentCount = 2 self.fakeCuttlefishServer.establishListener = { request in XCTAssertTrue(request.hasPeer, "establish request should have a peer") let newDynamicInfo = TPPeerDynamicInfo(data: request.peer.dynamicInfoAndSig.peerDynamicInfo, sig: request.peer.dynamicInfoAndSig.sig) XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamicInfo from protobuf") XCTAssertTrue(newDynamicInfo?.preapprovals.contains(peer2Preapproval) ?? false, "Fake peer 2 should be preapproved") XCTAssertTrue(newDynamicInfo?.preapprovals.contains(peer3Preapproval) ?? false, "Fake peer 3 should be preapproved") establishTwiceExpectation.fulfill() return nil } self.assertAllCKKSViewsUpload(tlkShares: 3) // Just starting the state machine is sufficient; it should perform an SOS upgrade self.cuttlefishContext.startOctagonStateMachine() self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) self.verifyDatabaseMocks() // And a reset does the right thing with preapprovals as well do { let arguments = OTConfigurationContext() arguments.altDSID = try self.cuttlefishContext.authKitAdapter.primaryiCloudAccountAltDSID() arguments.context = self.cuttlefishContext.contextID arguments.otControl = self.otControl let clique = try OTClique.newFriends(withContextData: arguments, resetReason: .testGenerated) XCTAssertNotNil(clique, "Clique should not be nil") } catch { XCTFail("Shouldn't have errored making new friends: \(error)") } self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) self.verifyDatabaseMocks() self.wait(for: [establishTwiceExpectation], timeout: 1) // And do we do the right thing when joining via SOS preapproval? let peer2JoinExpectation = self.expectation(description: "join called") self.fakeCuttlefishServer.joinListener = { request in XCTAssertTrue(request.hasPeer, "establish request should have a peer") let newDynamicInfo = TPPeerDynamicInfo(data: request.peer.dynamicInfoAndSig.peerDynamicInfo, sig: request.peer.dynamicInfoAndSig.sig) XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamicInfo from protobuf") XCTAssertFalse(newDynamicInfo?.preapprovals.contains(peer1Preapproval) ?? false, "Fake peer 1 should NOT be preapproved by peer2 (as it's already in Octagon)") XCTAssertTrue(newDynamicInfo?.preapprovals.contains(peer3Preapproval) ?? false, "Fake peer 3 should be preapproved by peer2") peer2JoinExpectation.fulfill() return nil } let peer2mockSOS = CKKSMockSOSPresentAdapter(selfPeer: peer2SOSMockPeer, trustedPeers: self.mockSOSAdapter.allPeers(), essential: false) let peer2 = self.makeInitiatorContext(contextID: "peer2", authKitAdapter: self.mockAuthKit2, sosAdapter: peer2mockSOS) peer2.startOctagonStateMachine() self.assertEnters(context: peer2, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertConsidersSelfTrusted(context: peer2) self.wait(for: [peer2JoinExpectation], timeout: 1) } func testDoNotPreapproveSOSPeerWhenOutOfCircle() throws { self.startCKAccountStatusMock() // SOS returns 'trusted' peers without actually being in-circle // We don't want to preapprove those peers self.mockSOSAdapter.circleStatus = SOSCCStatus(kSOSCCNotInCircle) let peer1Preapproval = TPHashBuilder.hash(with: .SHA256, of: self.mockSOSAdapter.selfPeer.publicSigningKey.encodeSubjectPublicKeyInfo()) let peer2SOSMockPeer = self.createSOSPeer(peerID: "peer2ID") self.mockSOSAdapter.trustedPeers.add(peer2SOSMockPeer) let peer2Preapproval = TPHashBuilder.hash(with: .SHA256, of: peer2SOSMockPeer.publicSigningKey.encodeSubjectPublicKeyInfo()) let peer3SOSMockPeer = self.createSOSPeer(peerID: "peer3ID") self.mockSOSAdapter.trustedPeers.add(peer3SOSMockPeer) let peer3Preapproval = TPHashBuilder.hash(with: .SHA256, of: peer3SOSMockPeer.publicSigningKey.encodeSubjectPublicKeyInfo()) self.fakeCuttlefishServer.establishListener = { request in XCTAssertTrue(request.hasPeer, "establish request should have a peer") let newDynamicInfo = TPPeerDynamicInfo(data: request.peer.dynamicInfoAndSig.peerDynamicInfo, sig: request.peer.dynamicInfoAndSig.sig) XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamicInfo from protobuf") XCTAssertFalse(newDynamicInfo?.preapprovals.contains(peer2Preapproval) ?? false, "Fake peer 2 should not be preapproved") XCTAssertFalse(newDynamicInfo?.preapprovals.contains(peer3Preapproval) ?? false, "Fake peer 3 should not be preapproved") return nil } self.assertResetAndBecomeTrustedInDefaultContext() self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertConsidersSelfTrusted(context: self.cuttlefishContext) self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) self.verifyDatabaseMocks() // And do we do the right thing when joining via bottle? let peer2JoinExpectation = self.expectation(description: "join called") self.fakeCuttlefishServer.joinListener = { request in XCTAssertTrue(request.hasPeer, "establish request should have a peer") let newDynamicInfo = TPPeerDynamicInfo(data: request.peer.dynamicInfoAndSig.peerDynamicInfo, sig: request.peer.dynamicInfoAndSig.sig) XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamicInfo from protobuf") XCTAssertFalse(newDynamicInfo?.preapprovals.contains(peer1Preapproval) ?? false, "Fake peer 1 should NOT be preapproved by peer2 (as it's not in SOS)") XCTAssertFalse(newDynamicInfo?.preapprovals.contains(peer3Preapproval) ?? false, "Fake peer 3 should not be preapproved by peer2 (as it's not in SOS)") peer2JoinExpectation.fulfill() return nil } let peer2mockSOS = CKKSMockSOSPresentAdapter(selfPeer: peer2SOSMockPeer, trustedPeers: self.mockSOSAdapter.allPeers(), essential: false) peer2mockSOS.circleStatus = SOSCCStatus(kSOSCCNotInCircle) let peer2 = self.makeInitiatorContext(contextID: "peer2", authKitAdapter: self.mockAuthKit2, sosAdapter: peer2mockSOS) peer2.startOctagonStateMachine() _ = self.assertJoinViaEscrowRecovery(joiningContext: peer2, sponsor: self.cuttlefishContext) self.assertEnters(context: peer2, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertConsidersSelfTrusted(context: peer2) self.wait(for: [peer2JoinExpectation], timeout: 1) } func testRespondToNewOctagonPeerWhenUpdatingPreapprovedKeys() throws { self.startCKAccountStatusMock() self.mockSOSAdapter.circleStatus = SOSCCStatus(kSOSCCInCircle) let peer1Preapproval = TPHashBuilder.hash(with: .SHA256, of: self.mockSOSAdapter.selfPeer.publicSigningKey.encodeSubjectPublicKeyInfo()) let peer2SOSMockPeer = self.createSOSPeer(peerID: "peer2ID") self.mockSOSAdapter.trustedPeers.add(peer2SOSMockPeer) let peer2Preapproval = TPHashBuilder.hash(with: .SHA256, of: peer2SOSMockPeer.publicSigningKey.encodeSubjectPublicKeyInfo()) self.assertAllCKKSViewsUpload(tlkShares: 2) // Just starting the state machine is sufficient; it should perform an SOS upgrade self.cuttlefishContext.startOctagonStateMachine() self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) let peer1ID = try self.cuttlefishContext.accountMetadataStore.getEgoPeerID() self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) self.verifyDatabaseMocks() // Another peer arrives, but we miss the Octagon push let peer2mockSOS = CKKSMockSOSPresentAdapter(selfPeer: peer2SOSMockPeer, trustedPeers: self.mockSOSAdapter.allPeers(), essential: false) let joiningContext = self.makeInitiatorContext(contextID: "joiner", authKitAdapter: self.mockAuthKit2, sosAdapter: peer2mockSOS) let peer2ID = self.assertJoinViaEscrowRecovery(joiningContext: joiningContext, sponsor: self.cuttlefishContext) // Now, SOS updates its key list: we should update our preapproved keys (and then also trust the newly-joined peer) let peer3SOSMockPeer = self.createSOSPeer(peerID: "peer3ID") self.mockSOSAdapter.trustedPeers.add(peer3SOSMockPeer) let peer3Preapproval = TPHashBuilder.hash(with: .SHA256, of: peer3SOSMockPeer.publicSigningKey.encodeSubjectPublicKeyInfo()) let updateKeysExpectation = self.expectation(description: "UpdateTrust should fire (once)") self.fakeCuttlefishServer.updateListener = { [unowned self] request in XCTAssertEqual(request.peerID, peer1ID, "UpdateTrust should be for peer1") let newDynamicInfo = request.dynamicInfoAndSig.dynamicInfo() XCTAssertFalse(newDynamicInfo.preapprovals.contains(peer1Preapproval), "Fake peer 1 should NOT be preapproved by peer1 (as it's its own keys)") XCTAssertTrue(newDynamicInfo.preapprovals.contains(peer2Preapproval), "Fake peer 2 should be preapproved by original peer") XCTAssertTrue(newDynamicInfo.preapprovals.contains(peer3Preapproval), "Fake peer 3 should be preapproved by original peer") self.fakeCuttlefishServer.updateListener = nil updateKeysExpectation.fulfill() return nil } // And we'll send TLKShares to the new SOS peer and the new Octagon peer self.assertAllCKKSViewsUpload(tlkShares: 2) // to avoid CKKS race conditions (wherein it uploads each TLKShare in its own operation), send the SOS notification only to the Octagon context //self.mockSOSAdapter.sendTrustedPeerSetChangedUpdate() self.cuttlefishContext.trustedPeerSetChanged(self.mockSOSAdapter) self.wait(for: [updateKeysExpectation], timeout: 10) self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC) self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC) self.verifyDatabaseMocks() XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer1ID)), "peer 1 should trust peer 1") XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer2ID)), "peer 1 should trust peer 2") } } #endif