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"
41 #import "keychain/categories/NSError+UsefulConstructors.h"
43 #import "keychain/ckks/tests/MockCloudKit.h"
44 #import "keychain/ckks/tests/CKKSTests.h"
45 #import "keychain/ot/OTDefines.h"
47 @interface CloudKitKeychainSyncingTLKSharingTests : CloudKitKeychainSyncingTestsBase
48 @property CKKSSOSSelfPeer* remotePeer1;
49 @property CKKSSOSPeer* remotePeer2;
52 @property CKKSSOSSelfPeer* untrustedPeer;
54 @property (nullable) NSMutableSet<id<CKKSSelfPeer>>* pastSelfPeers;
56 // Used to test a single code path. If true, no past self peers will be valid
57 @property bool breakLoadSelfPeerEncryptionKey;
60 @implementation CloudKitKeychainSyncingTLKSharingTests
65 self.pastSelfPeers = [NSMutableSet set];
67 // Use the upsetting old-style mocks so we can ignore the enum
68 [[[[self.mockCKKSViewManager stub] andCall:@selector(fakeLoadRestoredBottledKeysOfType:error:)
69 onObject:self] ignoringNonObjectArgs]
70 loadRestoredBottledKeysOfType:0 error:[OCMArg anyObjectRef]];
72 self.remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
73 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
74 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
76 self.remotePeer2 = [[CKKSSOSPeer alloc] initWithSOSPeerID:@"remote-peer2"
77 encryptionPublicKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]].publicKey
78 signingPublicKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]].publicKey];
80 // Local SOS trusts these peers
81 [self.currentPeers addObject:self.remotePeer1];
82 [self.currentPeers addObject:self.remotePeer2];
84 self.untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
85 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
86 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
90 self.pastSelfPeers = nil;
91 self.remotePeer1 = nil;
92 self.remotePeer2 = nil;
93 self.untrustedPeer = nil;
99 - (NSArray<NSDictionary *>* _Nullable)fakeLoadRestoredBottledKeysOfType:(OctagonKeyType)keyType error:(NSError**)error {
100 if(self.aksLockState) {
102 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
106 if(self.breakLoadSelfPeerEncryptionKey && keyType == OctagonEncryptionKey) {
108 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecItemNotFound userInfo:nil];
113 // Convert self.pastSelfPeers into an array of dictionaries
114 NSMutableArray<NSDictionary*>* keys = [NSMutableArray array];
116 for(id<CKKSSelfPeer> peer in self.pastSelfPeers) {
117 SFECKeyPair* key = nil;
120 case OctagonSigningKey:
121 key = peer.signingKey;
123 case OctagonEncryptionKey:
124 key = peer.encryptionKey;
128 XCTAssertNotNil(key, "Should have a key at this point");
130 NSData* signingPublicKeyHashBytes = [SFSHA384DigestOperation digest:peer.signingKey.publicKey.keyData];
131 NSString* signingPublicKeyHash = [signingPublicKeyHashBytes base64EncodedStringWithOptions:0];
133 NSDictionary* dict = @{
134 (id)kSecAttrAccount : peer.peerID,
135 (id)kSecAttrLabel : signingPublicKeyHash,
136 (id)kSecValueData : key.keyData,
138 [keys addObject:dict];
145 - (void)testAcceptExistingTLKSharedKeyHierarchy {
146 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
147 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
149 // Test also starts with the TLK shared to all trusted peers from peer1
150 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
152 // The CKKS subsystem should accept the keys, and share the TLK back to itself
153 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
154 [self startCKKSSubsystem];
155 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
157 OCMVerifyAllWithDelay(self.mockDatabase, 20);
159 // Verify that there are three local keys, and three local current key records
160 __weak __typeof(self) weakSelf = self;
161 [self.keychainView dispatchSync: ^bool{
162 __strong __typeof(weakSelf) strongSelf = weakSelf;
163 XCTAssertNotNil(strongSelf, "self exists");
165 NSError* error = nil;
167 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
168 XCTAssertNil(error, "no error fetching keys");
169 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
171 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all:&error];
172 XCTAssertNil(error, "no error fetching current keys");
173 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
179 - (void)testAcceptExistingTLKSharedKeyHierarchyForPastSelf {
180 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
181 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
183 // Test also starts with the TLK shared to all trusted peers from peer1
184 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
186 // Self rolls its keys and ID...
187 [self.pastSelfPeers addObject:self.currentSelfPeer];
188 self.currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"new-local-peer"
189 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
190 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
192 // The CKKS subsystem should accept the keys, and share the TLK back to itself
193 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
194 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
195 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
196 XCTAssertEqualObjects(share.receiver.peerID, self.currentSelfPeer.peerID, "Receiver peerID on TLKShare should match current self");
197 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.currentSelfPeer.publicEncryptionKey, "Receiver encryption key on TLKShare should match current self");
198 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
201 [self startCKKSSubsystem];
202 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
204 OCMVerifyAllWithDelay(self.mockDatabase, 20);
206 // Verify that there are three local keys, and three local current key records
207 __weak __typeof(self) weakSelf = self;
208 [self.keychainView dispatchSync: ^bool{
209 __strong __typeof(weakSelf) strongSelf = weakSelf;
210 XCTAssertNotNil(strongSelf, "self exists");
212 NSError* error = nil;
214 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
215 XCTAssertNil(error, "no error fetching keys");
216 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
218 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all:&error];
219 XCTAssertNil(error, "no error fetching current keys");
220 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
226 - (void)testDontCrashOnHalfBottle {
227 self.breakLoadSelfPeerEncryptionKey = true;
229 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
230 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
232 // Test also starts with the TLK shared to all trusted peers from peer1
233 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
235 // Self rolls its keys and ID...
236 [self.pastSelfPeers addObject:self.currentSelfPeer];
237 self.currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"new-local-peer"
238 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
239 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
241 // CKKS should enter 'waitfortlk' without crashing
242 [self startCKKSSubsystem];
243 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
246 - (void)testAcceptExistingTLKSharedKeyHierarchyAndUse {
247 // Test starts with nothing in database, but one in our fake CloudKit.
248 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
250 // Test also starts with the TLK shared to all trusted peers from peer1
251 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
253 // The CKKS subsystem should accept the keys, and share the TLK back to itself
254 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
255 [self startCKKSSubsystem];
256 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
258 // We expect a single record to be uploaded for each key class
259 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
260 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
261 [self addGenericPassword: @"data" account: @"account-delete-me"];
262 OCMVerifyAllWithDelay(self.mockDatabase, 20);
264 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
265 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
266 [self addGenericPassword:@"asdf"
267 account:@"account-class-A"
269 access:(id)kSecAttrAccessibleWhenUnlocked
270 expecting:errSecSuccess
271 message:@"Adding class A item"];
272 OCMVerifyAllWithDelay(self.mockDatabase, 20);
275 - (void)testNewTLKSharesHaveChangeTags {
276 // Since there's currently no flow for CKKS to ever update a TLK share when things are working properly, do some hackery
278 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
279 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
281 // Test also starts with the TLK shared to all trusted peers from peer1
282 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
283 [self saveTLKSharesInLocalDatabase:self.keychainZoneID];
285 // The CKKS subsystem should accept the keys, and share the TLK back to itself
286 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
287 [self startCKKSSubsystem];
288 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
290 OCMVerifyAllWithDelay(self.mockDatabase, 20);
291 [self waitForCKModifications];
293 // Verify that making a new share will have the old share's change tag
294 __weak __typeof(self) weakSelf = self;
295 [self.keychainView dispatchSyncWithAccountKeys: ^bool{
296 __strong __typeof(weakSelf) strongSelf = weakSelf;
297 XCTAssertNotNil(strongSelf, "self exists");
299 NSError* error = nil;
300 CKKSTLKShare* share = [CKKSTLKShare share:strongSelf.keychainZoneKeys.tlk
301 as:strongSelf.currentSelfPeer
302 to:strongSelf.currentSelfPeer
306 XCTAssertNil(error, "Shouldn't be an error creating a share");
307 XCTAssertNotNil(share, "Should be able to create share");
309 CKRecord* newRecord = [share CKRecordWithZoneID:strongSelf.keychainZoneID];
310 XCTAssertNotNil(newRecord, "Should be able to create a CKRecord");
312 CKRecord* cloudKitRecord = strongSelf.keychainZone.currentDatabase[newRecord.recordID];
313 XCTAssertNotNil(cloudKitRecord, "Should have found existing CKRecord in cloudkit");
314 XCTAssertNotNil(cloudKitRecord.recordChangeTag, "Existing record should have a change tag");
316 XCTAssertEqualObjects(cloudKitRecord.recordChangeTag, newRecord.recordChangeTag, "Change tags on existing and new records should match");
322 - (void)testReceiveTLKShareRecordsAndDeletes {
323 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
324 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
326 // Test also starts with the TLK shared to all trusted peers from peer1
327 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
329 // The CKKS subsystem should accept the keys, and share the TLK back to itself
330 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
331 [self startCKKSSubsystem];
332 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
334 // The CKKS subsystem should not try to write anything to the CloudKit database while it's accepting the keys
335 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
336 OCMVerifyAllWithDelay(self.mockDatabase, 20);
339 // Make another share, but from an untrusted peer to some other peer. local shouldn't necessarily care.
340 NSError* error = nil;
341 CKKSTLKShare* share = [CKKSTLKShare share:self.keychainZoneKeys.tlk
342 as:self.untrustedPeer
347 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
348 XCTAssertNotNil(share, "Should be able to create a share");
350 CKRecord* shareCKRecord = [share CKRecordWithZoneID: self.keychainZoneID];
351 XCTAssertNotNil(shareCKRecord, "Should have been able to create a CKRecord");
352 [self.keychainZone addToZone:shareCKRecord];
353 [self.keychainView notifyZoneChange:nil];
354 [self.keychainView waitForFetchAndIncomingQueueProcessing];
356 [self.keychainView dispatchSync:^bool {
357 NSError* blockerror = nil;
358 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
359 XCTAssertNil(blockerror, "Shouldn't error finding TLKShare record in database");
360 XCTAssertNotNil(localshare, "Should be able to find a TLKShare record in database");
364 // Delete the record in CloudKit...
365 [self.keychainZone deleteCKRecordIDFromZone:shareCKRecord.recordID];
366 [self.keychainView notifyZoneChange:nil];
367 [self.keychainView waitForFetchAndIncomingQueueProcessing];
369 // Should be gone now.
370 [self.keychainView dispatchSync:^bool {
371 NSError* blockerror = nil;
372 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
374 XCTAssertNil(blockerror, "Shouldn't error trying to find non-existent TLKShare record in database");
375 XCTAssertNil(localshare, "Shouldn't be able to find a TLKShare record in database");
381 - (void)testReceiveSharedTLKWhileInWaitForTLK {
382 // Test starts with nothing in database, but one in our fake CloudKit.
383 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
384 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
386 // Spin up CKKS subsystem.
387 [self startCKKSSubsystem];
389 // The CKKS subsystem should not try to write anything to the CloudKit database, but it should enter waitfortlk
390 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
392 // peer1 arrives to save the day
393 // The CKKS subsystem should accept the keys, and share the TLK back to itself
394 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
396 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
397 [self.keychainView notifyZoneChange:nil];
398 [self.keychainView waitForFetchAndIncomingQueueProcessing];
400 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
402 // We expect a single record to be uploaded for each key class
403 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
404 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
405 [self addGenericPassword: @"data" account: @"account-delete-me"];
406 OCMVerifyAllWithDelay(self.mockDatabase, 20);
408 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
409 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
410 [self addGenericPassword:@"asdf"
411 account:@"account-class-A"
413 access:(id)kSecAttrAccessibleWhenUnlocked
414 expecting:errSecSuccess
415 message:@"Adding class A item"];
416 OCMVerifyAllWithDelay(self.mockDatabase, 20);
419 - (void)testReceiveTLKShareWhileLocked {
420 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
421 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
423 // Test also starts with the TLK shared to all trusted peers from peer1
424 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
426 self.aksLockState = true;
427 [self.lockStateTracker recheck];
429 // Spin up CKKS subsystem.
430 [self startCKKSSubsystem];
432 // The CKKS subsystem should not try to write anything to the CloudKit database, but it should enter waitforunlock
433 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], "Key state should become waitforunlock");
435 // Now unlock things. We expect a TLKShare upload.
436 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
438 self.aksLockState = false;
439 [self.lockStateTracker recheck];
441 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
442 OCMVerifyAllWithDelay(self.mockDatabase, 20);
445 - (void)testUploadTLKSharesForExistingHierarchy {
446 // Test starts with key material locally and in CloudKit.
447 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
448 [self saveTLKMaterialToKeychain:self.keychainZoneID];
450 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
451 [self startCKKSSubsystem];
453 OCMVerifyAllWithDelay(self.mockDatabase, 20);
456 - (void)testUploadTLKSharesForExistingHierarchyOnRestart {
457 // Bring up CKKS. It'll upload a few TLK Shares, but we'll delete them to get into state
458 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
459 [self saveTLKMaterialToKeychain:self.keychainZoneID];
461 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
462 [self startCKKSSubsystem];
464 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
465 OCMVerifyAllWithDelay(self.mockDatabase, 20);
466 [self waitForCKModifications];
468 // Now, delete all the TLK Shares, so CKKS will upload them again
469 [self.keychainView dispatchSync:^bool {
470 NSError* error = nil;
471 [CKKSTLKShare deleteAll:self.keychainZoneID error:&error];
472 XCTAssertNil(error, "Shouldn't be an error deleting all TLKShares");
474 NSArray<CKRecord*>* records = [self.zones[self.keychainZoneID].currentDatabase allValues];
475 for(CKRecord* record in records) {
476 if([record.recordType isEqualToString:SecCKRecordTLKShareType]) {
477 [self.zones[self.keychainZoneID] deleteFromHistory:record.recordID];
484 // Restart. We expect an upload of 3 TLK shares.
485 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
486 self.keychainView = [self.injectedManager restartZone: self.keychainZoneID.zoneName];
488 OCMVerifyAllWithDelay(self.mockDatabase, 20);
489 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
492 - (void)testHandleExternalSharedTLKRoll {
493 // Test starts with key material locally and in CloudKit.
494 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
495 [self saveTLKMaterialToKeychain:self.keychainZoneID];
497 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
498 [self startCKKSSubsystem];
500 OCMVerifyAllWithDelay(self.mockDatabase, 20);
501 [self waitForCKModifications];
503 // Now the external peer rolls the TLK and updates the shares
504 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
505 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
507 // CKKS will share the TLK back to itself
508 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
510 // Trigger a notification
511 [self.keychainView notifyZoneChange:nil];
512 [self.keychainView waitForFetchAndIncomingQueueProcessing];
513 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
514 OCMVerifyAllWithDelay(self.mockDatabase, 20);
515 [self waitForCKModifications];
517 // We expect a single record to be uploaded.
518 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
519 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
520 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
522 OCMVerifyAllWithDelay(self.mockDatabase, 20);
525 - (void)testUploadTLKSharesForExternalTLKRollWithoutShares {
526 // Test starts with key material locally and in CloudKit.
527 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
528 [self saveTLKMaterialToKeychain:self.keychainZoneID];
530 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
531 [self startCKKSSubsystem];
533 OCMVerifyAllWithDelay(self.mockDatabase, 20);
535 // Now, an old (Tigris) peer rolls the TLK, but doesn't share it
536 // CKKS should get excited and throw 3 new share records up
537 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
539 // Wait for that modification to finish before changing CK data
540 [self waitForCKModifications];
542 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
543 [self saveTLKMaterialToKeychain:self.keychainZoneID];
545 // Trigger a notification
546 [self.keychainView notifyZoneChange:nil];
548 OCMVerifyAllWithDelay(self.mockDatabase, 20);
549 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
551 // We expect a single record to be uploaded.
552 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
553 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
554 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
556 OCMVerifyAllWithDelay(self.mockDatabase, 20);
559 - (void)testRecoverFromTLKShareUploadFailure {
560 // Test starts with key material locally and in CloudKit.
561 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
562 [self saveTLKMaterialToKeychain:self.keychainZoneID];
564 __weak __typeof(self) weakSelf = self;
565 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:^{
566 __strong __typeof(self) strongSelf = weakSelf;
567 [strongSelf expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
569 [self startCKKSSubsystem];
571 OCMVerifyAllWithDelay(self.mockDatabase, 20);
572 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
575 - (void)testFillInMissingPeerShares {
576 // Test starts with nothing in database, but one in our fake CloudKit.
577 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
579 // Test also starts with the TLK shared to just the local peer from peer1
580 // We expect the local peer to send it to peer2
581 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
583 // The CKKS subsystem should accept the keys, and share the TLK back to itself
584 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
585 [self startCKKSSubsystem];
586 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
588 // We expect a single record to be uploaded for each key class
589 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
590 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
591 [self addGenericPassword: @"data" account: @"account-delete-me"];
592 OCMVerifyAllWithDelay(self.mockDatabase, 20);
594 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
595 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
596 [self addGenericPassword:@"asdf"
597 account:@"account-class-A"
599 access:(id)kSecAttrAccessibleWhenUnlocked
600 expecting:errSecSuccess
601 message:@"Adding class A item"];
602 OCMVerifyAllWithDelay(self.mockDatabase, 20);
605 - (void)testDontAcceptTLKFromUntrustedPeer {
606 // Test starts with nothing in database, but key hierarchy in our fake CloudKit.
607 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
608 // The remote peer should also have given the TLK to a non-TLKShare peer (which is also offline)
609 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
611 // Test also starts with the key hierarchy shared from a non-trusted peer
612 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer zoneID:self.keychainZoneID];
614 // The CKKS subsystem should go into waitfortlk, since it doesn't trust this peer, but the peer is active
615 [self startCKKSSubsystem];
616 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become ready");
619 - (void)testAcceptSharedTLKOnTrustSetAdditionOfSharer {
620 // Test starts with nothing in database, but key hierarchy in our fake CloudKit.
621 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
622 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
624 // Test also starts with the key hierarchy shared from a non-trusted peer
625 // note that it would share it itself too
626 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer zoneID:self.keychainZoneID];
627 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer to:self.untrustedPeer zoneID:self.keychainZoneID];
629 // The CKKS subsystem should go into waitfortlk, since it doesn't trust this peer, but the peer is active
630 [self startCKKSSubsystem];
631 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
633 // Wait to be sure we really get into that state
634 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
636 // Now, trust the previously-untrusted peer
637 [self.currentPeers addObject: self.untrustedPeer];
638 [self.injectedManager sendTrustedPeerSetChangedUpdate];
640 // The CKKS subsystem should now accept the key, and share the TLK back to itself
641 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
643 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
645 // And use it as well
646 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
647 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
648 [self addGenericPassword: @"data" account: @"account-delete-me"];
649 OCMVerifyAllWithDelay(self.mockDatabase, 20);
652 - (void)testSendNewTLKSharesOnTrustSetAddition {
653 // step 1: add a new peer; we should share the TLK with them
654 // start with no trusted peers
655 [self.currentPeers removeAllObjects];
657 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
658 [self startCKKSSubsystem];
660 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
661 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
662 [self addGenericPassword: @"data" account: @"account-delete-me"];
663 OCMVerifyAllWithDelay(self.mockDatabase, 20);
665 // Cool! New peer arrives!
666 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
667 [self.currentPeers addObject:self.remotePeer1];
668 [self.injectedManager sendTrustedPeerSetChangedUpdate];
670 OCMVerifyAllWithDelay(self.mockDatabase, 20);
671 [self waitForCKModifications];
673 // step 2: add a new peer who already has a share; no share should be created
674 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 to:self.remotePeer2 zoneID:self.keychainZoneID];
675 [self.keychainView notifyZoneChange:nil];
676 [self.keychainView waitForFetchAndIncomingQueueProcessing];
678 // CKKS should not upload a tlk share for this peer
679 [self.currentPeers addObject:self.remotePeer2];
680 [self.injectedManager sendTrustedPeerSetChangedUpdate];
682 [self.keychainView waitUntilAllOperationsAreFinished];
685 - (void)testFillInMissingPeerSharesAfterUnlock {
686 // step 1: add a new peer; we should share the TLK with them
687 // start with no trusted peers
688 [self.currentPeers removeAllObjects];
690 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
691 [self startCKKSSubsystem];
693 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
695 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
696 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
697 [self addGenericPassword: @"data" account: @"account-delete-me"];
698 OCMVerifyAllWithDelay(self.mockDatabase, 20);
701 self.aksLockState = true;
702 [self.lockStateTracker recheck];
704 // New peer arrives! This can't actually happen (since we have to be unlocked to accept a new peer), but this will exercise CKKS
705 [self.currentPeers addObject:self.remotePeer1];
706 [self.injectedManager sendTrustedPeerSetChangedUpdate];
708 // CKKS should notice that it has things to do...
709 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:20*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
712 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
713 self.aksLockState = false;
714 [self.lockStateTracker recheck];
716 OCMVerifyAllWithDelay(self.mockDatabase, 20);
718 // and return to ready
719 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
722 - (void)testAddItemDuringNewTLKSharesOnTrustSetAddition {
723 // step 1: add a new peer; we should share the TLK with them
724 // start with no trusted peers
725 [self.currentPeers removeAllObjects];
727 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
728 [self startCKKSSubsystem];
730 OCMVerifyAllWithDelay(self.mockDatabase, 20);
731 [self waitForCKModifications];
733 // Hold the TLK share modification
734 [self holdCloudKitModifications];
736 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
737 [self.currentPeers addObject:self.remotePeer1];
738 [self.injectedManager sendTrustedPeerSetChangedUpdate];
740 OCMVerifyAllWithDelay(self.mockDatabase, 20);
742 // While CloudKit is hanging the write, add an item
743 [self addGenericPassword: @"data" account: @"account-delete-me"];
745 // After that returns, release the write. CKKS should upload the new item
746 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
747 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
748 [self releaseCloudKitModificationHold];
750 OCMVerifyAllWithDelay(self.mockDatabase, 20);
753 - (void)testSendNewTLKSharesOnTrustSetRemoval {
754 // Not implemented. Trust set removal demands a key roll, but let's not get ahead of ourselves...
757 - (void)testWaitForTLKWithMissingKeys {
758 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
759 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
761 // Test also starts with the TLK shared to all trusted peers from peer1
762 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
764 // self no longer has that key pair, but it does have a new one with the same peer ID....
765 self.currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:self.currentSelfPeer.peerID
766 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
767 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
768 self.pastSelfPeers = [NSMutableSet set];
770 // CKKS should become very upset, and enter waitfortlk.
771 [self startCKKSSubsystem];
772 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
773 OCMVerifyAllWithDelay(self.mockDatabase, 20);
776 - (void)testSendNewTLKShareToPeerOnPeerEncryptionKeyChange {
777 // If a peer changes its keys, CKKS should send it a new TLK share with the right keys
778 // This recovers from the remote peer losing its Octagon keys and making new ones
780 // step 1: add a new peer; we should share the TLK with them
781 // start with no trusted peers
782 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:3 zoneID:self.keychainZoneID];
783 [self startCKKSSubsystem];
785 OCMVerifyAllWithDelay(self.mockDatabase, 20);
786 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
788 // Remote peer rolls its encryption key...
789 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
790 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
791 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
792 XCTAssertEqualObjects(share.receiver.peerID, self.remotePeer1.peerID, "Receiver peerID on TLKShare should match remote peer");
793 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer1.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer");
794 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
798 self.remotePeer1.encryptionKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
799 [self.injectedManager sendTrustedPeerSetChangedUpdate];
801 OCMVerifyAllWithDelay(self.mockDatabase, 20);
802 [self waitForCKModifications];
804 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
807 - (void)testRecoverFromBrokenSignatureOnTLKShareDuetoSignatureKeyChange {
808 // If a peer changes its signature key, CKKS shouldn't necessarily enter 'error': it should enter 'waitfortlk'.
809 // The peer should then send us another TLKShare
810 // This recovers from the remote peer losing its Octagon keys and making new ones
812 // For this test, only have one peer
813 self.currentPeers = [NSMutableSet setWithObject:self.remotePeer1];
815 // Test starts with nothing in database, but one in our fake CloudKit.
816 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
817 // Test also starts with the TLK shared to all trusted peers from remotePeer1
818 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
820 // BUT, remotePeer1 has rolled its signing key
821 self.remotePeer1.signingKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
823 [self startCKKSSubsystem];
825 OCMVerifyAllWithDelay(self.mockDatabase, 20);
826 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
828 // Remote peer discovers its error and sends a new TLKShare! CKKS should recover and share itself a TLKShare
829 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
830 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
831 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
832 XCTAssertEqualObjects(share.receiver.peerID, self.currentSelfPeer.peerID, "Receiver peerID on TLKShare should match self peer");
833 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.currentSelfPeer.publicEncryptionKey, "Receiver encryption key on TLKShare should match self peer");
834 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
838 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
839 [self.keychainView notifyZoneChange:nil];
841 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
843 OCMVerifyAllWithDelay(self.mockDatabase, 20);
844 [self waitForCKModifications];
847 - (void)testSendNewTLKShareToSelfOnPeerSigningKeyChange {
848 // 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
849 // This recovers from the local peer losing its Octagon keys and making new ones
851 // For this test, only have one peer
852 self.currentPeers = [NSMutableSet setWithObject:self.remotePeer1];
854 // Test starts with nothing in database, but one in our fake CloudKit.
855 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
856 // Test also starts with the TLK shared to all trusted peers from peer1
857 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
858 // The CKKS subsystem should accept the keys, and share the TLK back to itself
859 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
860 [self startCKKSSubsystem];
861 OCMVerifyAllWithDelay(self.mockDatabase, 20);
862 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
864 // Remote peer rolls its signing key, but hasn't updated its TLKShare. We should send it one.
865 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
866 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
867 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
868 XCTAssertEqualObjects(share.receiver.peerID, self.remotePeer1.peerID, "Receiver peerID on TLKShare should match remote peer");
869 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer1.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer");
870 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
874 self.remotePeer1.signingKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
875 [self.injectedManager sendTrustedPeerSetChangedUpdate];
877 OCMVerifyAllWithDelay(self.mockDatabase, 20);
878 [self waitForCKModifications];
880 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
883 - (void)testSendNewTLKShareToPeerOnDisappearanceOfPeerKeys {
884 // If a CKKS peer deletes its own octagon keys (BUT WHY), local CKKS should be able to respond
886 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
887 // Test also starts with the TLK shared to all trusted peers from peer1
888 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
889 // The CKKS subsystem should accept the keys, and share the TLK back to itself
890 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
891 [self startCKKSSubsystem];
892 OCMVerifyAllWithDelay(self.mockDatabase, 20);
893 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
895 // Now, peer 1 updates its keys (to be nil). Local peer should re-send TLKShares to peer2.
897 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
898 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
899 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
900 XCTAssertEqualObjects(share.receiver.peerID, self.remotePeer2.peerID, "Receiver peerID on TLKShare should match remote peer");
901 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer2.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer");
902 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
906 CKKSSOSPeer* brokenRemotePeer1 = [[CKKSSOSPeer alloc] initWithSOSPeerID:self.remotePeer1.peerID encryptionPublicKey:nil signingPublicKey:nil];
907 [self.currentPeers removeObject:self.remotePeer1];
908 [self.currentPeers addObject:brokenRemotePeer1];
909 [self.injectedManager sendTrustedPeerSetChangedUpdate];
911 OCMVerifyAllWithDelay(self.mockDatabase, 20);
912 [self waitForCKModifications];
914 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
917 - (void)testSendNewTLKShareToPeerOnDisappearanceOfPeerSigningKey {
918 // 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
919 // This recovers from the local peer losing its Octagon keys and making new ones
921 // Test starts with nothing in database, but one in our fake CloudKit.
922 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
923 // Test also starts with the TLK shared to all trusted peers from peer1
924 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
925 // The CKKS subsystem should accept the keys, and share the TLK back to itself
926 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
927 [self startCKKSSubsystem];
928 OCMVerifyAllWithDelay(self.mockDatabase, 20);
929 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
931 // Now, peer 1 updates its signing key (to be nil). Local peer should re-send TLKShares to peer1 and peer2.
932 // Both should be sent because both peers don't have a signed TLKShare that gives them the TLK
934 XCTestExpectation *peer1Share = [self expectationWithDescription:@"share uploaded for peer1"];
935 XCTestExpectation *peer2Share = [self expectationWithDescription:@"share uploaded for peer2"];
937 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:2 zoneID:self.keychainZoneID
938 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
939 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
940 if([share.receiver.peerID isEqualToString:self.remotePeer1.peerID]) {
941 [peer1Share fulfill];
942 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer1.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer1");
944 if([share.receiver.peerID isEqualToString:self.remotePeer2.peerID]) {
945 [peer2Share fulfill];
946 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer2.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer2");
949 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
953 CKKSSOSPeer* brokenRemotePeer1 = [[CKKSSOSPeer alloc] initWithSOSPeerID:self.remotePeer1.peerID
954 encryptionPublicKey:self.remotePeer1.publicEncryptionKey
955 signingPublicKey:nil];
956 [self.currentPeers removeObject:self.remotePeer1];
957 [self.currentPeers addObject:brokenRemotePeer1];
958 [self.injectedManager sendTrustedPeerSetChangedUpdate];
960 OCMVerifyAllWithDelay(self.mockDatabase, 20);
961 [self waitForCKModifications];
962 [self waitForExpectations:@[peer1Share, peer2Share] timeout:5];
964 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
967 - (void)testSendNewTLKShareToSelfOnSelfKeyChanges {
968 // 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
969 // This recovers from the local peer losing its Octagon keys and making new ones
971 // Test starts with nothing in database, but one in our fake CloudKit.
972 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
973 // Test also starts with the TLK shared to all trusted peers from peer1
974 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
975 // The CKKS subsystem should accept the keys, and share the TLK back to itself
976 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
977 [self startCKKSSubsystem];
978 OCMVerifyAllWithDelay(self.mockDatabase, 20);
979 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
981 // Local peer rolls its encryption key (and loses the old ones)
982 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
983 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
984 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
985 XCTAssertEqualObjects(share.receiver.peerID, self.currentSelfPeer.peerID, "Receiver peerID on TLKShare should match current self");
986 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.currentSelfPeer.publicEncryptionKey, "Receiver encryption key on TLKShare should match current self");
987 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
988 NSError* signatureVerifyError = nil;
989 XCTAssertTrue([share verifySignature:share.signature verifyingPeer:self.currentSelfPeer error:&signatureVerifyError], "New share's signature should verify");
990 XCTAssertNil(signatureVerifyError, "Should be no error verifying signature on new TLKShare");
994 self.currentSelfPeer.encryptionKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
995 self.pastSelfPeers = [NSMutableSet set];
996 [self.injectedManager sendSelfPeerChangedUpdate];
998 OCMVerifyAllWithDelay(self.mockDatabase, 20);
999 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
1001 // Now, local peer loses and rolls its signing key (and loses the old one)
1002 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
1003 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
1004 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
1005 XCTAssertEqualObjects(share.receiver.peerID, self.currentSelfPeer.peerID, "Receiver peerID on TLKShare should match current self");
1006 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.currentSelfPeer.publicEncryptionKey, "Receiver encryption key on TLKShare should match current self");
1007 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
1008 NSError* signatureVerifyError = nil;
1009 XCTAssertTrue([share verifySignature:share.signature verifyingPeer:self.currentSelfPeer error:&signatureVerifyError], "New share's signature should verify");
1010 XCTAssertNil(signatureVerifyError, "Should be no error verifying signature on new TLKShare");
1014 self.currentSelfPeer.signingKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
1015 self.pastSelfPeers = [NSMutableSet set];
1016 [self.injectedManager sendSelfPeerChangedUpdate];
1018 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1019 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
1022 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToRecentTLKShare {
1023 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1025 // CKKS shouldn't reset this zone, due to a recent TLK Share from a trusted peer (indicating the presence of TLKs)
1026 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 to:self.remotePeer1 zoneID:self.keychainZoneID];
1028 NSDateComponents* offset = [[NSDateComponents alloc] init];
1030 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1031 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1032 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1033 record.creationDate = updateTime;
1034 record.modificationDate = updateTime;
1038 self.keychainZone.flag = true;
1039 [self startCKKSSubsystem];
1041 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1043 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1046 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToVeryRecentUntrustedTLKShare {
1047 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1049 // 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.
1050 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1051 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1052 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
1053 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1055 NSDateComponents* offset = [[NSDateComponents alloc] init];
1057 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1058 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1059 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1060 record.creationDate = updateTime;
1061 record.modificationDate = updateTime;
1065 self.keychainZone.flag = true;
1066 [self startCKKSSubsystem];
1068 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1069 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1071 // And ensure it doesn't go on to 'reset'
1072 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:100*NSEC_PER_MSEC], @"Key state should not become 'resetzone'");
1075 - (void)testResetCloudKitZoneFromWaitForTLKDueToUntustedTLKShareNotRecentEnough {
1076 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1078 // CKKS shouldn't reset this zone, due to a recent TLK Share (indicating the presence of TLKs)
1079 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1080 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1081 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
1082 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1084 NSDateComponents* offset = [[NSDateComponents alloc] init];
1086 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1087 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1088 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1089 record.creationDate = updateTime;
1090 record.modificationDate = updateTime;
1094 self.silentZoneDeletesAllowed = true;
1095 self.keychainZone.flag = true;
1096 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:3 zoneID:self.keychainZoneID];
1097 [self startCKKSSubsystem];
1098 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:20*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1100 // Then we should reset.
1101 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1102 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
1104 // And the zone should have been cleared and re-made
1105 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1108 - (void)testNoSelfEncryptionKeys {
1109 // If you lose your local encryption keys, CKKS should do something reasonable
1111 // Test also starts with the TLK shared to all trusted peers from peer1
1112 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1113 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
1114 [self saveTLKSharesInLocalDatabase:self.keychainZoneID];
1116 // But, we lost our local keys :(
1117 id<CKKSSelfPeer> oldSelfPeer = self.currentSelfPeer;
1119 self.currentSelfPeer = nil;
1120 self.currentSelfPeerError = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecParam description:@"injected test failure"];
1122 // CKKS subsystem should realize that it can't read the shares it has, and enter waitfortlk
1123 [self startCKKSSubsystem];
1124 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become 'waitfortlk'");
1126 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1127 [self waitForCKModifications];
1129 // Fetching status should be quick
1130 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1131 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1132 XCTAssertNil(error, "should be no error fetching status for keychain");
1133 [callbackOccurs fulfill];
1135 [self waitForExpectations:@[callbackOccurs] timeout:20];
1137 // But, if by some miracle those keys come back, CKKS should be able to recover
1138 // It'll also upload itself a TLK share
1139 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
1141 self.currentSelfPeer = oldSelfPeer;
1142 self.currentSelfPeerError = nil;
1144 [self.injectedManager sendSelfPeerChangedUpdate];
1145 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become 'ready''");
1147 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1148 [self waitForCKModifications];