2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
29 #import <SecurityFoundation/SFKey.h>
30 #import <SecurityFoundation/SFKey_Private.h>
31 #import <SecurityFoundation/SFDigestOperation.h>
33 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
34 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
35 #import "keychain/ckks/CKKS.h"
36 #import "keychain/ckks/CKKSKey.h"
37 #import "keychain/ckks/CKKSPeer.h"
38 #import "keychain/ckks/CKKSTLKShare.h"
39 #import "keychain/ckks/CKKSViewManager.h"
40 #import "keychain/ckks/CloudKitCategories.h"
42 #import "keychain/ckks/tests/MockCloudKit.h"
43 #import "keychain/ckks/tests/CKKSTests.h"
44 #import "keychain/ot/OTDefines.h"
46 @interface CloudKitKeychainSyncingTLKSharingTests : CloudKitKeychainSyncingTestsBase
47 @property CKKSSOSSelfPeer* remotePeer1;
48 @property CKKSSOSPeer* remotePeer2;
51 @property CKKSSOSSelfPeer* untrustedPeer;
53 @property (nullable) NSMutableSet<id<CKKSSelfPeer>>* pastSelfPeers;
55 // Used to test a single code path. If true, no past self peers will be valid
56 @property bool breakLoadSelfPeerEncryptionKey;
59 @implementation CloudKitKeychainSyncingTLKSharingTests
64 self.pastSelfPeers = [NSMutableSet set];
66 // Use the upsetting old-style mocks so we can ignore the enum
67 [[[[self.mockCKKSViewManager stub] andCall:@selector(fakeLoadRestoredBottledKeysOfType:error:)
68 onObject:self] ignoringNonObjectArgs]
69 loadRestoredBottledKeysOfType:0 error:[OCMArg anyObjectRef]];
71 self.remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
72 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
73 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
75 self.remotePeer2 = [[CKKSSOSPeer alloc] initWithSOSPeerID:@"remote-peer2"
76 encryptionPublicKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]].publicKey
77 signingPublicKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]].publicKey];
79 // Local SOS trusts these peers
80 [self.currentPeers addObject:self.remotePeer1];
81 [self.currentPeers addObject:self.remotePeer2];
83 self.untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
84 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
85 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
89 self.pastSelfPeers = nil;
90 self.remotePeer1 = nil;
91 self.remotePeer2 = nil;
92 self.untrustedPeer = nil;
98 - (NSArray<NSDictionary *>* _Nullable)fakeLoadRestoredBottledKeysOfType:(OctagonKeyType)keyType error:(NSError**)error {
99 if(self.aksLockState) {
101 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
105 if(self.breakLoadSelfPeerEncryptionKey && keyType == OctagonEncryptionKey) {
107 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecItemNotFound userInfo:nil];
112 // Convert self.pastSelfPeers into an array of dictionaries
113 NSMutableArray<NSDictionary*>* keys = [NSMutableArray array];
115 for(id<CKKSSelfPeer> peer in self.pastSelfPeers) {
116 SFECKeyPair* key = nil;
119 case OctagonSigningKey:
120 key = peer.signingKey;
122 case OctagonEncryptionKey:
123 key = peer.encryptionKey;
127 XCTAssertNotNil(key, "Should have a key at this point");
129 NSData* signingPublicKeyHashBytes = [SFSHA384DigestOperation digest:peer.signingKey.publicKey.keyData];
130 NSString* signingPublicKeyHash = [signingPublicKeyHashBytes base64EncodedStringWithOptions:0];
132 NSDictionary* dict = @{
133 (id)kSecAttrAccount : peer.peerID,
134 (id)kSecAttrLabel : signingPublicKeyHash,
135 (id)kSecValueData : key.keyData,
137 [keys addObject:dict];
144 - (void)testAcceptExistingTLKSharedKeyHierarchy {
145 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
146 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
148 // Test also starts with the TLK shared to all trusted peers from peer1
149 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
151 // The CKKS subsystem should accept the keys, and share the TLK back to itself
152 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
153 [self startCKKSSubsystem];
154 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
156 OCMVerifyAllWithDelay(self.mockDatabase, 8);
158 // Verify that there are three local keys, and three local current key records
159 __weak __typeof(self) weakSelf = self;
160 [self.keychainView dispatchSync: ^bool{
161 __strong __typeof(weakSelf) strongSelf = weakSelf;
162 XCTAssertNotNil(strongSelf, "self exists");
164 NSError* error = nil;
166 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
167 XCTAssertNil(error, "no error fetching keys");
168 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
170 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all:&error];
171 XCTAssertNil(error, "no error fetching current keys");
172 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
178 - (void)testAcceptExistingTLKSharedKeyHierarchyForPastSelf {
179 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
180 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
182 // Test also starts with the TLK shared to all trusted peers from peer1
183 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
185 // Self rolls its keys and ID...
186 [self.pastSelfPeers addObject:self.currentSelfPeer];
187 self.currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"new-local-peer"
188 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
189 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
191 // The CKKS subsystem should accept the keys, and share the TLK back to itself
192 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
193 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
194 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
195 XCTAssertEqualObjects(share.receiver.peerID, self.currentSelfPeer.peerID, "Receiver peerID on TLKShare should match current self");
196 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.currentSelfPeer.publicEncryptionKey, "Receiver encryption key on TLKShare should match current self");
197 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
200 [self startCKKSSubsystem];
201 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
203 OCMVerifyAllWithDelay(self.mockDatabase, 8);
205 // Verify that there are three local keys, and three local current key records
206 __weak __typeof(self) weakSelf = self;
207 [self.keychainView dispatchSync: ^bool{
208 __strong __typeof(weakSelf) strongSelf = weakSelf;
209 XCTAssertNotNil(strongSelf, "self exists");
211 NSError* error = nil;
213 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
214 XCTAssertNil(error, "no error fetching keys");
215 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
217 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all:&error];
218 XCTAssertNil(error, "no error fetching current keys");
219 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
225 - (void)testDontCrashOnHalfBottle {
226 self.breakLoadSelfPeerEncryptionKey = true;
228 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
229 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
231 // Test also starts with the TLK shared to all trusted peers from peer1
232 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
234 // Self rolls its keys and ID...
235 [self.pastSelfPeers addObject:self.currentSelfPeer];
236 self.currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"new-local-peer"
237 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
238 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
240 // CKKS should enter 'waitfortlk' without crashing
241 [self startCKKSSubsystem];
242 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:10*NSEC_PER_SEC], "Key state should become waitfortlk");
245 - (void)testAcceptExistingTLKSharedKeyHierarchyAndUse {
246 // Test starts with nothing in database, but one in our fake CloudKit.
247 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
249 // Test also starts with the TLK shared to all trusted peers from peer1
250 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
252 // The CKKS subsystem should accept the keys, and share the TLK back to itself
253 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
254 [self startCKKSSubsystem];
255 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
257 // We expect a single record to be uploaded for each key class
258 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
259 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
260 [self addGenericPassword: @"data" account: @"account-delete-me"];
261 OCMVerifyAllWithDelay(self.mockDatabase, 8);
263 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
264 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
265 [self addGenericPassword:@"asdf"
266 account:@"account-class-A"
268 access:(id)kSecAttrAccessibleWhenUnlocked
269 expecting:errSecSuccess
270 message:@"Adding class A item"];
271 OCMVerifyAllWithDelay(self.mockDatabase, 8);
274 - (void)testNewTLKSharesHaveChangeTags {
275 // Since there's currently no flow for CKKS to ever update a TLK share when things are working properly, do some hackery
277 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
278 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
280 // Test also starts with the TLK shared to all trusted peers from peer1
281 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
282 [self saveTLKSharesInLocalDatabase:self.keychainZoneID];
284 // The CKKS subsystem should accept the keys, and share the TLK back to itself
285 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
286 [self startCKKSSubsystem];
287 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
289 OCMVerifyAllWithDelay(self.mockDatabase, 8);
290 [self waitForCKModifications];
292 // Verify that making a new share will have the old share's change tag
293 __weak __typeof(self) weakSelf = self;
294 [self.keychainView dispatchSyncWithAccountKeys: ^bool{
295 __strong __typeof(weakSelf) strongSelf = weakSelf;
296 XCTAssertNotNil(strongSelf, "self exists");
298 NSError* error = nil;
299 CKKSTLKShare* share = [CKKSTLKShare share:strongSelf.keychainZoneKeys.tlk
300 as:strongSelf.currentSelfPeer
301 to:strongSelf.currentSelfPeer
305 XCTAssertNil(error, "Shouldn't be an error creating a share");
306 XCTAssertNotNil(share, "Should be able to create share");
308 CKRecord* newRecord = [share CKRecordWithZoneID:strongSelf.keychainZoneID];
309 XCTAssertNotNil(newRecord, "Should be able to create a CKRecord");
311 CKRecord* cloudKitRecord = strongSelf.keychainZone.currentDatabase[newRecord.recordID];
312 XCTAssertNotNil(cloudKitRecord, "Should have found existing CKRecord in cloudkit");
313 XCTAssertNotNil(cloudKitRecord.recordChangeTag, "Existing record should have a change tag");
315 XCTAssertEqualObjects(cloudKitRecord.recordChangeTag, newRecord.recordChangeTag, "Change tags on existing and new records should match");
321 - (void)testReceiveTLKShareRecordsAndDeletes {
322 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
323 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
325 // Test also starts with the TLK shared to all trusted peers from peer1
326 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
328 // The CKKS subsystem should accept the keys, and share the TLK back to itself
329 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
330 [self startCKKSSubsystem];
331 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
333 // The CKKS subsystem should not try to write anything to the CloudKit database while it's accepting the keys
334 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
335 OCMVerifyAllWithDelay(self.mockDatabase, 8);
338 // Make another share, but from an untrusted peer to some other peer. local shouldn't necessarily care.
339 NSError* error = nil;
340 CKKSTLKShare* share = [CKKSTLKShare share:self.keychainZoneKeys.tlk
341 as:self.untrustedPeer
346 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
347 XCTAssertNotNil(share, "Should be able to create a share");
349 CKRecord* shareCKRecord = [share CKRecordWithZoneID: self.keychainZoneID];
350 XCTAssertNotNil(shareCKRecord, "Should have been able to create a CKRecord");
351 [self.keychainZone addToZone:shareCKRecord];
352 [self.keychainView notifyZoneChange:nil];
353 [self.keychainView waitForFetchAndIncomingQueueProcessing];
355 [self.keychainView dispatchSync:^bool {
356 NSError* blockerror = nil;
357 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
358 XCTAssertNil(blockerror, "Shouldn't error finding TLKShare record in database");
359 XCTAssertNotNil(localshare, "Should be able to find a TLKShare record in database");
363 // Delete the record in CloudKit...
364 [self.keychainZone deleteCKRecordIDFromZone:shareCKRecord.recordID];
365 [self.keychainView notifyZoneChange:nil];
366 [self.keychainView waitForFetchAndIncomingQueueProcessing];
368 // Should be gone now.
369 [self.keychainView dispatchSync:^bool {
370 NSError* blockerror = nil;
371 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
373 XCTAssertNil(blockerror, "Shouldn't error trying to find non-existent TLKShare record in database");
374 XCTAssertNil(localshare, "Shouldn't be able to find a TLKShare record in database");
380 - (void)testReceiveSharedTLKWhileInWaitForTLK {
381 // Test starts with nothing in database, but one in our fake CloudKit.
382 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
383 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
385 // Spin up CKKS subsystem.
386 [self startCKKSSubsystem];
388 // The CKKS subsystem should not try to write anything to the CloudKit database, but it should enter waitfortlk
389 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
391 // peer1 arrives to save the day
392 // The CKKS subsystem should accept the keys, and share the TLK back to itself
393 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
395 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
396 [self.keychainView notifyZoneChange:nil];
397 [self.keychainView waitForFetchAndIncomingQueueProcessing];
399 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
401 // We expect a single record to be uploaded for each key class
402 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
403 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
404 [self addGenericPassword: @"data" account: @"account-delete-me"];
405 OCMVerifyAllWithDelay(self.mockDatabase, 8);
407 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
408 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
409 [self addGenericPassword:@"asdf"
410 account:@"account-class-A"
412 access:(id)kSecAttrAccessibleWhenUnlocked
413 expecting:errSecSuccess
414 message:@"Adding class A item"];
415 OCMVerifyAllWithDelay(self.mockDatabase, 8);
418 - (void)testReceiveTLKShareWhileLocked {
419 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
420 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
422 // Test also starts with the TLK shared to all trusted peers from peer1
423 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
425 self.aksLockState = true;
426 [self.lockStateTracker recheck];
428 // Spin up CKKS subsystem.
429 [self startCKKSSubsystem];
431 // The CKKS subsystem should not try to write anything to the CloudKit database, but it should enter waitforunlock
432 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:10*NSEC_PER_SEC], "Key state should become waitforunlock");
434 // Now unlock things. We expect a TLKShare upload.
435 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
437 self.aksLockState = false;
438 [self.lockStateTracker recheck];
440 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
441 OCMVerifyAllWithDelay(self.mockDatabase, 8);
444 - (void)testUploadTLKSharesForExistingHierarchy {
445 // Test starts with key material locally and in CloudKit.
446 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
447 [self saveTLKMaterialToKeychain:self.keychainZoneID];
449 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
450 [self startCKKSSubsystem];
452 OCMVerifyAllWithDelay(self.mockDatabase, 8);
455 - (void)testUploadTLKSharesForExistingHierarchyOnRestart {
456 // Bring up CKKS. It'll upload a few TLK Shares, but we'll delete them to get into state
457 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
458 [self saveTLKMaterialToKeychain:self.keychainZoneID];
460 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
461 [self startCKKSSubsystem];
463 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
464 OCMVerifyAllWithDelay(self.mockDatabase, 8);
465 [self waitForCKModifications];
467 // Now, delete all the TLK Shares, so CKKS will upload them again
468 [self.keychainView dispatchSync:^bool {
469 NSError* error = nil;
470 [CKKSTLKShare deleteAll:self.keychainZoneID error:&error];
471 XCTAssertNil(error, "Shouldn't be an error deleting all TLKShares");
473 NSArray<CKRecord*>* records = [self.zones[self.keychainZoneID].currentDatabase allValues];
474 for(CKRecord* record in records) {
475 if([record.recordType isEqualToString:SecCKRecordTLKShareType]) {
476 [self.zones[self.keychainZoneID] deleteFromHistory:record.recordID];
483 // Restart. We expect an upload of 3 TLK shares.
484 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
485 self.keychainView = [self.injectedManager restartZone: self.keychainZoneID.zoneName];
487 OCMVerifyAllWithDelay(self.mockDatabase, 8);
488 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
491 - (void)testHandleExternalSharedTLKRoll {
492 // Test starts with key material locally and in CloudKit.
493 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
494 [self saveTLKMaterialToKeychain:self.keychainZoneID];
496 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
497 [self startCKKSSubsystem];
499 OCMVerifyAllWithDelay(self.mockDatabase, 8);
500 [self waitForCKModifications];
502 // Now the external peer rolls the TLK and updates the shares
503 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
504 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
506 // CKKS will share the TLK back to itself
507 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
509 // Trigger a notification
510 [self.keychainView notifyZoneChange:nil];
511 [self.keychainView waitForFetchAndIncomingQueueProcessing];
512 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
513 OCMVerifyAllWithDelay(self.mockDatabase, 8);
514 [self waitForCKModifications];
516 // We expect a single record to be uploaded.
517 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
518 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
519 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
521 OCMVerifyAllWithDelay(self.mockDatabase, 8);
524 - (void)testUploadTLKSharesForExternalTLKRollWithoutShares {
525 // Test starts with key material locally and in CloudKit.
526 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
527 [self saveTLKMaterialToKeychain:self.keychainZoneID];
529 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
530 [self startCKKSSubsystem];
532 OCMVerifyAllWithDelay(self.mockDatabase, 8);
534 // Now, an old (Tigris) peer rolls the TLK, but doesn't share it
535 // CKKS should get excited and throw 3 new share records up
536 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
538 // Wait for that modification to finish before changing CK data
539 [self waitForCKModifications];
541 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
542 [self saveTLKMaterialToKeychain:self.keychainZoneID];
544 // Trigger a notification
545 [self.keychainView notifyZoneChange:nil];
547 OCMVerifyAllWithDelay(self.mockDatabase, 8);
548 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
550 // We expect a single record to be uploaded.
551 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
552 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
553 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
555 OCMVerifyAllWithDelay(self.mockDatabase, 8);
558 - (void)testRecoverFromTLKShareUploadFailure {
559 // Test starts with key material locally and in CloudKit.
560 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
561 [self saveTLKMaterialToKeychain:self.keychainZoneID];
563 __weak __typeof(self) weakSelf = self;
564 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:^{
565 __strong __typeof(self) strongSelf = weakSelf;
566 [strongSelf expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
568 [self startCKKSSubsystem];
570 OCMVerifyAllWithDelay(self.mockDatabase, 8);
571 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
574 - (void)testFillInMissingPeerShares {
575 // Test starts with nothing in database, but one in our fake CloudKit.
576 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
578 // Test also starts with the TLK shared to just the local peer from peer1
579 // We expect the local peer to send it to peer2
580 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
582 // The CKKS subsystem should accept the keys, and share the TLK back to itself
583 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
584 [self startCKKSSubsystem];
585 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
587 // We expect a single record to be uploaded for each key class
588 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
589 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
590 [self addGenericPassword: @"data" account: @"account-delete-me"];
591 OCMVerifyAllWithDelay(self.mockDatabase, 8);
593 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
594 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
595 [self addGenericPassword:@"asdf"
596 account:@"account-class-A"
598 access:(id)kSecAttrAccessibleWhenUnlocked
599 expecting:errSecSuccess
600 message:@"Adding class A item"];
601 OCMVerifyAllWithDelay(self.mockDatabase, 8);
604 - (void)testDontAcceptTLKFromUntrustedPeer {
605 // Test starts with nothing in database, but key hierarchy in our fake CloudKit.
606 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
607 // The remote peer should also have given the TLK to a non-TLKShare peer (which is also offline)
608 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
610 // Test also starts with the key hierarchy shared from a non-trusted peer
611 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer zoneID:self.keychainZoneID];
613 // The CKKS subsystem should go into waitfortlk, since it doesn't trust this peer, but the peer is active
614 [self startCKKSSubsystem];
615 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become ready");
618 - (void)testAcceptSharedTLKOnTrustSetAdditionOfSharer {
619 // Test starts with nothing in database, but key hierarchy in our fake CloudKit.
620 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
621 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
623 // Test also starts with the key hierarchy shared from a non-trusted peer
624 // note that it would share it itself too
625 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer zoneID:self.keychainZoneID];
626 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer to:self.untrustedPeer zoneID:self.keychainZoneID];
628 // The CKKS subsystem should go into waitfortlk, since it doesn't trust this peer, but the peer is active
629 [self startCKKSSubsystem];
630 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
632 // Wait to be sure we really get into that state
633 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
635 // Now, trust the previously-untrusted peer
636 [self.currentPeers addObject: self.untrustedPeer];
637 [self.injectedManager sendTrustedPeerSetChangedUpdate];
639 // The CKKS subsystem should now accept the key, and share the TLK back to itself
640 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
642 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should become ready");
644 // And use it as well
645 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
646 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
647 [self addGenericPassword: @"data" account: @"account-delete-me"];
648 OCMVerifyAllWithDelay(self.mockDatabase, 8);
651 - (void)testSendNewTLKSharesOnTrustSetAddition {
652 // step 1: add a new peer; we should share the TLK with them
653 // start with no trusted peers
654 [self.currentPeers removeAllObjects];
656 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
657 [self startCKKSSubsystem];
659 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
660 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
661 [self addGenericPassword: @"data" account: @"account-delete-me"];
662 OCMVerifyAllWithDelay(self.mockDatabase, 8);
664 // Cool! New peer arrives!
665 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
666 [self.currentPeers addObject:self.remotePeer1];
667 [self.injectedManager sendTrustedPeerSetChangedUpdate];
669 OCMVerifyAllWithDelay(self.mockDatabase, 8);
670 [self waitForCKModifications];
672 // step 2: add a new peer who already has a share; no share should be created
673 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 to:self.remotePeer2 zoneID:self.keychainZoneID];
674 [self.keychainView notifyZoneChange:nil];
675 [self.keychainView waitForFetchAndIncomingQueueProcessing];
677 // CKKS should not upload a tlk share for this peer
678 [self.currentPeers addObject:self.remotePeer2];
679 [self.injectedManager sendTrustedPeerSetChangedUpdate];
681 [self.keychainView waitUntilAllOperationsAreFinished];
684 - (void)testFillInMissingPeerSharesAfterUnlock {
685 // step 1: add a new peer; we should share the TLK with them
686 // start with no trusted peers
687 [self.currentPeers removeAllObjects];
689 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
690 [self startCKKSSubsystem];
692 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
694 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
695 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
696 [self addGenericPassword: @"data" account: @"account-delete-me"];
697 OCMVerifyAllWithDelay(self.mockDatabase, 8);
700 self.aksLockState = true;
701 [self.lockStateTracker recheck];
703 // New peer arrives! This can't actually happen (since we have to be unlocked to accept a new peer), but this will exercise CKKS
704 [self.currentPeers addObject:self.remotePeer1];
705 [self.injectedManager sendTrustedPeerSetChangedUpdate];
707 // CKKS should notice that it has things to do...
708 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
711 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
712 self.aksLockState = false;
713 [self.lockStateTracker recheck];
715 OCMVerifyAllWithDelay(self.mockDatabase, 8);
717 // and return to ready
718 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
721 - (void)testAddItemDuringNewTLKSharesOnTrustSetAddition {
722 // step 1: add a new peer; we should share the TLK with them
723 // start with no trusted peers
724 [self.currentPeers removeAllObjects];
726 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
727 [self startCKKSSubsystem];
729 OCMVerifyAllWithDelay(self.mockDatabase, 8);
730 [self waitForCKModifications];
732 // Hold the TLK share modification
733 [self holdCloudKitModifications];
735 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
736 [self.currentPeers addObject:self.remotePeer1];
737 [self.injectedManager sendTrustedPeerSetChangedUpdate];
739 OCMVerifyAllWithDelay(self.mockDatabase, 8);
741 // While CloudKit is hanging the write, add an item
742 [self addGenericPassword: @"data" account: @"account-delete-me"];
744 // After that returns, release the write. CKKS should upload the new item
745 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
746 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
747 [self releaseCloudKitModificationHold];
749 OCMVerifyAllWithDelay(self.mockDatabase, 8);
752 - (void)testSendNewTLKSharesOnTrustSetRemoval {
753 // Not implemented. Trust set removal demands a key roll, but let's not get ahead of ourselves...
756 - (void)testWaitForTLKWithMissingKeys {
757 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
758 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
760 // Test also starts with the TLK shared to all trusted peers from peer1
761 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
763 // self no longer has that key pair, but it does have a new one with the same peer ID....
764 self.currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:self.currentSelfPeer.peerID
765 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
766 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
767 self.pastSelfPeers = [NSMutableSet set];
769 // CKKS should become very upset, and enter waitfortlk.
770 [self startCKKSSubsystem];
771 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:4000*NSEC_PER_SEC], "Key state should become waitfortlk");
772 OCMVerifyAllWithDelay(self.mockDatabase, 8);
775 - (void)testSendNewTLKShareToPeerOnPeerEncryptionKeyChange {
776 // If a peer changes its keys, CKKS should send it a new TLK share with the right keys
777 // This recovers from the remote peer losing its Octagon keys and making new ones
779 // step 1: add a new peer; we should share the TLK with them
780 // start with no trusted peers
781 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:3 zoneID:self.keychainZoneID];
782 [self startCKKSSubsystem];
784 OCMVerifyAllWithDelay(self.mockDatabase, 8);
785 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:1000*NSEC_PER_SEC], "Key state should become ready");
787 // Remote peer rolls its encryption key...
788 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
789 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
790 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
791 XCTAssertEqualObjects(share.receiver.peerID, self.remotePeer1.peerID, "Receiver peerID on TLKShare should match remote peer");
792 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer1.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer");
793 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
797 self.remotePeer1.encryptionKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
798 [self.injectedManager sendTrustedPeerSetChangedUpdate];
800 OCMVerifyAllWithDelay(self.mockDatabase, 8);
801 [self waitForCKModifications];
803 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
806 - (void)testRecoverFromBrokenSignatureOnTLKShareDuetoSignatureKeyChange {
807 // If a peer changes its signature key, CKKS shouldn't necessarily enter 'error': it should enter 'waitfortlk'.
808 // The peer should then send us another TLKShare
809 // This recovers from the remote peer losing its Octagon keys and making new ones
811 // For this test, only have one peer
812 self.currentPeers = [NSMutableSet setWithObject:self.remotePeer1];
814 // Test starts with nothing in database, but one in our fake CloudKit.
815 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
816 // Test also starts with the TLK shared to all trusted peers from remotePeer1
817 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
819 // BUT, remotePeer1 has rolled its signing key
820 self.remotePeer1.signingKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
822 [self startCKKSSubsystem];
824 OCMVerifyAllWithDelay(self.mockDatabase, 8);
825 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:10*NSEC_PER_SEC], "Key state should become waitfortlk");
827 // Remote peer discovers its error and sends a new TLKShare! CKKS should recover and share itself a TLKShare
828 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
829 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
830 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
831 XCTAssertEqualObjects(share.receiver.peerID, self.currentSelfPeer.peerID, "Receiver peerID on TLKShare should match self peer");
832 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.currentSelfPeer.publicEncryptionKey, "Receiver encryption key on TLKShare should match self peer");
833 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
837 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
838 [self.keychainView notifyZoneChange:nil];
840 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
842 OCMVerifyAllWithDelay(self.mockDatabase, 8);
843 [self waitForCKModifications];
846 - (void)testSendNewTLKShareToSelfOnPeerSigningKeyChange {
847 // If a CKKS peer rolls its own keys, but has the TLK, it should write a new TLK share to itself with its new Octagon keys
848 // This recovers from the local peer losing its Octagon keys and making new ones
850 // For this test, only have one peer
851 self.currentPeers = [NSMutableSet setWithObject:self.remotePeer1];
853 // Test starts with nothing in database, but one in our fake CloudKit.
854 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
855 // Test also starts with the TLK shared to all trusted peers from peer1
856 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
857 // The CKKS subsystem should accept the keys, and share the TLK back to itself
858 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
859 [self startCKKSSubsystem];
860 OCMVerifyAllWithDelay(self.mockDatabase, 8);
861 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:100*NSEC_PER_SEC], "Key state should become ready");
863 // Remote peer rolls its signing key, but hasn't updated its TLKShare. We should send it one.
864 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
865 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
866 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
867 XCTAssertEqualObjects(share.receiver.peerID, self.remotePeer1.peerID, "Receiver peerID on TLKShare should match remote peer");
868 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer1.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer");
869 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
873 self.remotePeer1.signingKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
874 [self.injectedManager sendTrustedPeerSetChangedUpdate];
876 OCMVerifyAllWithDelay(self.mockDatabase, 8);
877 [self waitForCKModifications];
879 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
882 - (void)testSendNewTLKShareToPeerOnDisappearanceOfPeerKeys {
883 // If a CKKS peer deletes its own octagon keys (BUT WHY), local CKKS should be able to respond
885 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
886 // Test also starts with the TLK shared to all trusted peers from peer1
887 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
888 // The CKKS subsystem should accept the keys, and share the TLK back to itself
889 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
890 [self startCKKSSubsystem];
891 OCMVerifyAllWithDelay(self.mockDatabase, 8);
892 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:100*NSEC_PER_SEC], "Key state should become ready");
894 // Now, peer 1 updates its keys (to be nil). Local peer should re-send TLKShares to peer2.
896 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
897 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
898 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
899 XCTAssertEqualObjects(share.receiver.peerID, self.remotePeer2.peerID, "Receiver peerID on TLKShare should match remote peer");
900 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer2.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer");
901 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
905 CKKSSOSPeer* brokenRemotePeer1 = [[CKKSSOSPeer alloc] initWithSOSPeerID:self.remotePeer1.peerID encryptionPublicKey:nil signingPublicKey:nil];
906 [self.currentPeers removeObject:self.remotePeer1];
907 [self.currentPeers addObject:brokenRemotePeer1];
908 [self.injectedManager sendTrustedPeerSetChangedUpdate];
910 OCMVerifyAllWithDelay(self.mockDatabase, 8);
911 [self waitForCKModifications];
913 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
916 - (void)testSendNewTLKShareToPeerOnDisappearanceOfPeerSigningKey {
917 // If a CKKS peer rolls its own keys, but has the TLK, it should write a new TLK share to itself with its new Octagon keys
918 // This recovers from the local peer losing its Octagon keys and making new ones
920 // Test starts with nothing in database, but one in our fake CloudKit.
921 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
922 // Test also starts with the TLK shared to all trusted peers from peer1
923 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
924 // The CKKS subsystem should accept the keys, and share the TLK back to itself
925 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
926 [self startCKKSSubsystem];
927 OCMVerifyAllWithDelay(self.mockDatabase, 8);
928 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:100*NSEC_PER_SEC], "Key state should become ready");
930 // Now, peer 1 updates its signing key (to be nil). Local peer should re-send TLKShares to peer1 and peer2.
931 // Both should be sent because both peers don't have a signed TLKShare that gives them the TLK
933 XCTestExpectation *peer1Share = [self expectationWithDescription:@"share uploaded for peer1"];
934 XCTestExpectation *peer2Share = [self expectationWithDescription:@"share uploaded for peer2"];
936 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:2 zoneID:self.keychainZoneID
937 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
938 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
939 if([share.receiver.peerID isEqualToString:self.remotePeer1.peerID]) {
940 [peer1Share fulfill];
941 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer1.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer1");
943 if([share.receiver.peerID isEqualToString:self.remotePeer2.peerID]) {
944 [peer2Share fulfill];
945 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer2.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer2");
948 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
952 CKKSSOSPeer* brokenRemotePeer1 = [[CKKSSOSPeer alloc] initWithSOSPeerID:self.remotePeer1.peerID
953 encryptionPublicKey:self.remotePeer1.publicEncryptionKey
954 signingPublicKey:nil];
955 [self.currentPeers removeObject:self.remotePeer1];
956 [self.currentPeers addObject:brokenRemotePeer1];
957 [self.injectedManager sendTrustedPeerSetChangedUpdate];
959 OCMVerifyAllWithDelay(self.mockDatabase, 8);
960 [self waitForCKModifications];
961 [self waitForExpectations:@[peer1Share, peer2Share] timeout:5];
963 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
966 - (void)testSendNewTLKShareToSelfOnSelfKeyChanges {
967 // If a CKKS peer rolls its own keys, but has the TLK, it should write a new TLK share to itself with its new Octagon keys
968 // This recovers from the local peer losing its Octagon keys and making new ones
970 // Test starts with nothing in database, but one in our fake CloudKit.
971 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
972 // Test also starts with the TLK shared to all trusted peers from peer1
973 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
974 // The CKKS subsystem should accept the keys, and share the TLK back to itself
975 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
976 [self startCKKSSubsystem];
977 OCMVerifyAllWithDelay(self.mockDatabase, 8);
978 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
980 // Local peer rolls its encryption key (and loses the old ones)
981 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
982 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
983 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
984 XCTAssertEqualObjects(share.receiver.peerID, self.currentSelfPeer.peerID, "Receiver peerID on TLKShare should match current self");
985 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.currentSelfPeer.publicEncryptionKey, "Receiver encryption key on TLKShare should match current self");
986 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
987 NSError* signatureVerifyError = nil;
988 XCTAssertTrue([share verifySignature:share.signature verifyingPeer:self.currentSelfPeer error:&signatureVerifyError], "New share's signature should verify");
989 XCTAssertNil(signatureVerifyError, "Should be no error verifying signature on new TLKShare");
993 self.currentSelfPeer.encryptionKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
994 self.pastSelfPeers = [NSMutableSet set];
995 [self.injectedManager sendSelfPeerChangedUpdate];
997 OCMVerifyAllWithDelay(self.mockDatabase, 8);
998 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
1000 // Now, local peer loses and rolls its signing key (and loses the old one)
1001 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
1002 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
1003 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
1004 XCTAssertEqualObjects(share.receiver.peerID, self.currentSelfPeer.peerID, "Receiver peerID on TLKShare should match current self");
1005 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.currentSelfPeer.publicEncryptionKey, "Receiver encryption key on TLKShare should match current self");
1006 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
1007 NSError* signatureVerifyError = nil;
1008 XCTAssertTrue([share verifySignature:share.signature verifyingPeer:self.currentSelfPeer error:&signatureVerifyError], "New share's signature should verify");
1009 XCTAssertNil(signatureVerifyError, "Should be no error verifying signature on new TLKShare");
1013 self.currentSelfPeer.signingKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
1014 self.pastSelfPeers = [NSMutableSet set];
1015 [self.injectedManager sendSelfPeerChangedUpdate];
1017 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1018 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
1021 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToRecentTLKShare {
1022 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1024 // CKKS shouldn't reset this zone, due to a recent TLK Share from a trusted peer (indicating the presence of TLKs)
1025 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 to:self.remotePeer1 zoneID:self.keychainZoneID];
1027 NSDateComponents* offset = [[NSDateComponents alloc] init];
1029 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1030 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1031 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1032 record.creationDate = updateTime;
1033 record.modificationDate = updateTime;
1037 self.keychainZone.flag = true;
1038 [self startCKKSSubsystem];
1040 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1042 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1045 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToVeryRecentUntrustedTLKShare {
1046 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1048 // CKKS shouldn't reset this zone, due to a very recent (but untrusted) TLK Share. You can hit this getting a circle reset; the device with the TLKs will have a CFU.
1049 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1050 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1051 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
1052 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1054 NSDateComponents* offset = [[NSDateComponents alloc] init];
1056 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1057 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1058 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1059 record.creationDate = updateTime;
1060 record.modificationDate = updateTime;
1064 self.keychainZone.flag = true;
1065 [self startCKKSSubsystem];
1067 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1068 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1070 // And ensure it doesn't go on to 'reset'
1071 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:100*NSEC_PER_MSEC], @"Key state should not become 'resetzone'");
1074 - (void)testResetCloudKitZoneFromWaitForTLKDueToUntustedTLKShareNotRecentEnough {
1075 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1077 // CKKS shouldn't reset this zone, due to a recent TLK Share (indicating the presence of TLKs)
1078 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1079 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1080 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
1081 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1083 NSDateComponents* offset = [[NSDateComponents alloc] init];
1085 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1086 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1087 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1088 record.creationDate = updateTime;
1089 record.modificationDate = updateTime;
1093 self.silentZoneDeletesAllowed = true;
1094 self.keychainZone.flag = true;
1095 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:3 zoneID:self.keychainZoneID];
1096 [self startCKKSSubsystem];
1097 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:8*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1099 // Then we should reset.
1100 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1101 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1103 // And the zone should have been cleared and re-made
1104 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1107 - (void)testNoSelfEncryptionKeys {
1108 // If you lose your local encryption keys, CKKS should do something reasonable
1110 // Test also starts with the TLK shared to all trusted peers from peer1
1111 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1112 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
1113 [self saveTLKSharesInLocalDatabase:self.keychainZoneID];
1115 // But, we lost our local keys :(
1116 id<CKKSSelfPeer> oldSelfPeer = self.currentSelfPeer;
1118 self.currentSelfPeer = nil;
1119 self.currentSelfPeerError = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecParam description:@"injected test failure"];
1121 // CKKS subsystem should realize that it can't read the shares it has, and enter waitfortlk
1122 [self startCKKSSubsystem];
1123 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:10*NSEC_PER_SEC], "Key state should become 'waitfortlk'");
1125 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1126 [self waitForCKModifications];
1128 // Fetching status should be quick
1129 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1130 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1131 XCTAssertNil(error, "should be no error fetching status for keychain");
1132 [callbackOccurs fulfill];
1134 [self waitForExpectations:@[callbackOccurs] timeout:1.0];
1136 // But, if by some miracle those keys come back, CKKS should be able to recover
1137 // It'll also upload itself a TLK share
1138 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
1140 self.currentSelfPeer = oldSelfPeer;
1141 self.currentSelfPeerError = nil;
1143 [self.injectedManager sendSelfPeerChangedUpdate];
1144 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:100*NSEC_PER_SEC], "Key state should become 'ready''");
1146 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1147 [self waitForCKModifications];