]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTLKSharingTests.m
Security-58286.260.20.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSTLKSharingTests.m
1 /*
2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
29 #import <SecurityFoundation/SFKey.h>
30 #import <SecurityFoundation/SFKey_Private.h>
31 #import <SecurityFoundation/SFDigestOperation.h>
32
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"
42
43 #import "keychain/ckks/tests/MockCloudKit.h"
44 #import "keychain/ckks/tests/CKKSTests.h"
45 #import "keychain/ot/OTDefines.h"
46
47 @interface CloudKitKeychainSyncingTLKSharingTests : CloudKitKeychainSyncingTestsBase
48 @property CKKSSOSSelfPeer* remotePeer1;
49 @property CKKSSOSPeer* remotePeer2;
50
51
52 @property CKKSSOSSelfPeer* untrustedPeer;
53
54 @property (nullable) NSMutableSet<id<CKKSSelfPeer>>* pastSelfPeers;
55
56 // Used to test a single code path. If true, no past self peers will be valid
57 @property bool breakLoadSelfPeerEncryptionKey;
58 @end
59
60 @implementation CloudKitKeychainSyncingTLKSharingTests
61
62 - (void)setUp {
63 [super setUp];
64
65 self.pastSelfPeers = [NSMutableSet set];
66
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]];
71
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]]];
75
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];
79
80 // Local SOS trusts these peers
81 [self.currentPeers addObject:self.remotePeer1];
82 [self.currentPeers addObject:self.remotePeer2];
83
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]]];
87 }
88
89 - (void)tearDown {
90 self.pastSelfPeers = nil;
91 self.remotePeer1 = nil;
92 self.remotePeer2 = nil;
93 self.untrustedPeer = nil;
94
95 [super tearDown];
96 }
97
98
99 - (NSArray<NSDictionary *>* _Nullable)fakeLoadRestoredBottledKeysOfType:(OctagonKeyType)keyType error:(NSError**)error {
100 if(self.aksLockState) {
101 if(error) {
102 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
103 }
104 return nil;
105 } else {
106 if(self.breakLoadSelfPeerEncryptionKey && keyType == OctagonEncryptionKey) {
107 if(error) {
108 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecItemNotFound userInfo:nil];
109 }
110 return nil;
111 }
112
113 // Convert self.pastSelfPeers into an array of dictionaries
114 NSMutableArray<NSDictionary*>* keys = [NSMutableArray array];
115
116 for(id<CKKSSelfPeer> peer in self.pastSelfPeers) {
117 SFECKeyPair* key = nil;
118
119 switch(keyType) {
120 case OctagonSigningKey:
121 key = peer.signingKey;
122 break;
123 case OctagonEncryptionKey:
124 key = peer.encryptionKey;
125 break;
126 }
127
128 XCTAssertNotNil(key, "Should have a key at this point");
129
130 NSData* signingPublicKeyHashBytes = [SFSHA384DigestOperation digest:peer.signingKey.publicKey.keyData];
131 NSString* signingPublicKeyHash = [signingPublicKeyHashBytes base64EncodedStringWithOptions:0];
132
133 NSDictionary* dict = @{
134 (id)kSecAttrAccount : peer.peerID,
135 (id)kSecAttrLabel : signingPublicKeyHash,
136 (id)kSecValueData : key.keyData,
137 };
138 [keys addObject:dict];
139 }
140
141 return keys;
142 }
143 }
144
145 - (void)testAcceptExistingTLKSharedKeyHierarchy {
146 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
147 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
148
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];
151
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");
156
157 OCMVerifyAllWithDelay(self.mockDatabase, 20);
158
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");
164
165 NSError* error = nil;
166
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");
170
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");
174
175 return false;
176 }];
177 }
178
179 - (void)testAcceptExistingTLKSharedKeyHierarchyForPastSelf {
180 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
181 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
182
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];
185
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]]];
191
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");
199 return TRUE;
200 }];
201 [self startCKKSSubsystem];
202 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
203
204 OCMVerifyAllWithDelay(self.mockDatabase, 20);
205
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");
211
212 NSError* error = nil;
213
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");
217
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");
221
222 return false;
223 }];
224 }
225
226 - (void)testDontCrashOnHalfBottle {
227 self.breakLoadSelfPeerEncryptionKey = true;
228
229 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
230 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
231
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];
234
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]]];
240
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");
244 }
245
246 - (void)testAcceptExistingTLKSharedKeyHierarchyAndUse {
247 // Test starts with nothing in database, but one in our fake CloudKit.
248 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
249
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];
252
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");
257
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);
263
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"
268 viewHint:nil
269 access:(id)kSecAttrAccessibleWhenUnlocked
270 expecting:errSecSuccess
271 message:@"Adding class A item"];
272 OCMVerifyAllWithDelay(self.mockDatabase, 20);
273 }
274
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
277
278 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
279 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
280
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];
284
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");
289
290 OCMVerifyAllWithDelay(self.mockDatabase, 20);
291 [self waitForCKModifications];
292
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");
298
299 NSError* error = nil;
300 CKKSTLKShare* share = [CKKSTLKShare share:strongSelf.keychainZoneKeys.tlk
301 as:strongSelf.currentSelfPeer
302 to:strongSelf.currentSelfPeer
303 epoch:-1
304 poisoned:0
305 error:&error];
306 XCTAssertNil(error, "Shouldn't be an error creating a share");
307 XCTAssertNotNil(share, "Should be able to create share");
308
309 CKRecord* newRecord = [share CKRecordWithZoneID:strongSelf.keychainZoneID];
310 XCTAssertNotNil(newRecord, "Should be able to create a CKRecord");
311
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");
315
316 XCTAssertEqualObjects(cloudKitRecord.recordChangeTag, newRecord.recordChangeTag, "Change tags on existing and new records should match");
317
318 return false;
319 }];
320 }
321
322 - (void)testReceiveTLKShareRecordsAndDeletes {
323 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
324 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
325
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];
328
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");
333
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);
337
338
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
343 to:self.remotePeer1
344 epoch:-1
345 poisoned:0
346 error:&error];
347 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
348 XCTAssertNotNil(share, "Should be able to create a share");
349
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];
355
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");
361 return true;
362 }];
363
364 // Delete the record in CloudKit...
365 [self.keychainZone deleteCKRecordIDFromZone:shareCKRecord.recordID];
366 [self.keychainView notifyZoneChange:nil];
367 [self.keychainView waitForFetchAndIncomingQueueProcessing];
368
369 // Should be gone now.
370 [self.keychainView dispatchSync:^bool {
371 NSError* blockerror = nil;
372 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
373
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");
376
377 return true;
378 }];
379 }
380
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];
385
386 // Spin up CKKS subsystem.
387 [self startCKKSSubsystem];
388
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");
391
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];
395
396 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
397 [self.keychainView notifyZoneChange:nil];
398 [self.keychainView waitForFetchAndIncomingQueueProcessing];
399
400 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
401
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);
407
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"
412 viewHint:nil
413 access:(id)kSecAttrAccessibleWhenUnlocked
414 expecting:errSecSuccess
415 message:@"Adding class A item"];
416 OCMVerifyAllWithDelay(self.mockDatabase, 20);
417 }
418
419 - (void)testReceiveTLKShareWhileLocked {
420 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
421 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
422
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];
425
426 self.aksLockState = true;
427 [self.lockStateTracker recheck];
428
429 // Spin up CKKS subsystem.
430 [self startCKKSSubsystem];
431
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");
434
435 // Now unlock things. We expect a TLKShare upload.
436 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
437
438 self.aksLockState = false;
439 [self.lockStateTracker recheck];
440
441 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
442 OCMVerifyAllWithDelay(self.mockDatabase, 20);
443 }
444
445 - (void)testUploadTLKSharesForExistingHierarchy {
446 // Test starts with key material locally and in CloudKit.
447 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
448 [self saveTLKMaterialToKeychain:self.keychainZoneID];
449
450 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
451 [self startCKKSSubsystem];
452
453 OCMVerifyAllWithDelay(self.mockDatabase, 20);
454 }
455
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];
460
461 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
462 [self startCKKSSubsystem];
463
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];
467
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");
473
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];
478 }
479 }
480
481 return true;
482 }];
483
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];
487
488 OCMVerifyAllWithDelay(self.mockDatabase, 20);
489 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
490 }
491
492 - (void)testHandleExternalSharedTLKRoll {
493 // Test starts with key material locally and in CloudKit.
494 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
495 [self saveTLKMaterialToKeychain:self.keychainZoneID];
496
497 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
498 [self startCKKSSubsystem];
499
500 OCMVerifyAllWithDelay(self.mockDatabase, 20);
501 [self waitForCKModifications];
502
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];
506
507 // CKKS will share the TLK back to itself
508 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
509
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];
516
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"];
521
522 OCMVerifyAllWithDelay(self.mockDatabase, 20);
523 }
524
525 - (void)testUploadTLKSharesForExternalTLKRollWithoutShares {
526 // Test starts with key material locally and in CloudKit.
527 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
528 [self saveTLKMaterialToKeychain:self.keychainZoneID];
529
530 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
531 [self startCKKSSubsystem];
532
533 OCMVerifyAllWithDelay(self.mockDatabase, 20);
534
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];
538
539 // Wait for that modification to finish before changing CK data
540 [self waitForCKModifications];
541
542 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
543 [self saveTLKMaterialToKeychain:self.keychainZoneID];
544
545 // Trigger a notification
546 [self.keychainView notifyZoneChange:nil];
547
548 OCMVerifyAllWithDelay(self.mockDatabase, 20);
549 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
550
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"];
555
556 OCMVerifyAllWithDelay(self.mockDatabase, 20);
557 }
558
559 - (void)testRecoverFromTLKShareUploadFailure {
560 // Test starts with key material locally and in CloudKit.
561 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
562 [self saveTLKMaterialToKeychain:self.keychainZoneID];
563
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];
568 }];
569 [self startCKKSSubsystem];
570
571 OCMVerifyAllWithDelay(self.mockDatabase, 20);
572 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
573 }
574
575 - (void)testFillInMissingPeerShares {
576 // Test starts with nothing in database, but one in our fake CloudKit.
577 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
578
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];
582
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");
587
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);
593
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"
598 viewHint:nil
599 access:(id)kSecAttrAccessibleWhenUnlocked
600 expecting:errSecSuccess
601 message:@"Adding class A item"];
602 OCMVerifyAllWithDelay(self.mockDatabase, 20);
603 }
604
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];
610
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];
613
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");
617 }
618
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];
623
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];
628
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");
632
633 // Wait to be sure we really get into that state
634 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
635
636 // Now, trust the previously-untrusted peer
637 [self.currentPeers addObject: self.untrustedPeer];
638 [self.injectedManager sendTrustedPeerSetChangedUpdate];
639
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];
642
643 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
644
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);
650 }
651
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];
656
657 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
658 [self startCKKSSubsystem];
659
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);
664
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];
669
670 OCMVerifyAllWithDelay(self.mockDatabase, 20);
671 [self waitForCKModifications];
672
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];
677
678 // CKKS should not upload a tlk share for this peer
679 [self.currentPeers addObject:self.remotePeer2];
680 [self.injectedManager sendTrustedPeerSetChangedUpdate];
681
682 [self.keychainView waitUntilAllOperationsAreFinished];
683 }
684
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];
689
690 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
691 [self startCKKSSubsystem];
692
693 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
694
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);
699
700 // Now, lock.
701 self.aksLockState = true;
702 [self.lockStateTracker recheck];
703
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];
707
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'");
710
711 // And do them.
712 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
713 self.aksLockState = false;
714 [self.lockStateTracker recheck];
715
716 OCMVerifyAllWithDelay(self.mockDatabase, 20);
717
718 // and return to ready
719 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"Key state should become 'ready'");
720 }
721
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];
726
727 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
728 [self startCKKSSubsystem];
729
730 OCMVerifyAllWithDelay(self.mockDatabase, 20);
731 [self waitForCKModifications];
732
733 // Hold the TLK share modification
734 [self holdCloudKitModifications];
735
736 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
737 [self.currentPeers addObject:self.remotePeer1];
738 [self.injectedManager sendTrustedPeerSetChangedUpdate];
739
740 OCMVerifyAllWithDelay(self.mockDatabase, 20);
741
742 // While CloudKit is hanging the write, add an item
743 [self addGenericPassword: @"data" account: @"account-delete-me"];
744
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];
749
750 OCMVerifyAllWithDelay(self.mockDatabase, 20);
751 }
752
753 - (void)testSendNewTLKSharesOnTrustSetRemoval {
754 // Not implemented. Trust set removal demands a key roll, but let's not get ahead of ourselves...
755 }
756
757 - (void)testWaitForTLKWithMissingKeys {
758 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
759 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
760
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];
763
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];
769
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);
774 }
775
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
779
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];
784
785 OCMVerifyAllWithDelay(self.mockDatabase, 20);
786 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
787
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");
795 return TRUE;
796 }];
797
798 self.remotePeer1.encryptionKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
799 [self.injectedManager sendTrustedPeerSetChangedUpdate];
800
801 OCMVerifyAllWithDelay(self.mockDatabase, 20);
802 [self waitForCKModifications];
803
804 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
805 }
806
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
811
812 // For this test, only have one peer
813 self.currentPeers = [NSMutableSet setWithObject:self.remotePeer1];
814
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];
819
820 // BUT, remotePeer1 has rolled its signing key
821 self.remotePeer1.signingKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
822
823 [self startCKKSSubsystem];
824
825 OCMVerifyAllWithDelay(self.mockDatabase, 20);
826 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
827
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");
835 return TRUE;
836 }];
837
838 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
839 [self.keychainView notifyZoneChange:nil];
840
841 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
842
843 OCMVerifyAllWithDelay(self.mockDatabase, 20);
844 [self waitForCKModifications];
845 }
846
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
850
851 // For this test, only have one peer
852 self.currentPeers = [NSMutableSet setWithObject:self.remotePeer1];
853
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");
863
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");
871 return TRUE;
872 }];
873
874 self.remotePeer1.signingKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
875 [self.injectedManager sendTrustedPeerSetChangedUpdate];
876
877 OCMVerifyAllWithDelay(self.mockDatabase, 20);
878 [self waitForCKModifications];
879
880 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
881 }
882
883 - (void)testSendNewTLKShareToPeerOnDisappearanceOfPeerKeys {
884 // If a CKKS peer deletes its own octagon keys (BUT WHY), local CKKS should be able to respond
885
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");
894
895 // Now, peer 1 updates its keys (to be nil). Local peer should re-send TLKShares to peer2.
896
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");
903 return TRUE;
904 }];
905
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];
910
911 OCMVerifyAllWithDelay(self.mockDatabase, 20);
912 [self waitForCKModifications];
913
914 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
915 }
916
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
920
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");
930
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
933
934 XCTestExpectation *peer1Share = [self expectationWithDescription:@"share uploaded for peer1"];
935 XCTestExpectation *peer2Share = [self expectationWithDescription:@"share uploaded for peer2"];
936
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");
943 }
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");
947 }
948
949 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
950 return TRUE;
951 }];
952
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];
959
960 OCMVerifyAllWithDelay(self.mockDatabase, 20);
961 [self waitForCKModifications];
962 [self waitForExpectations:@[peer1Share, peer2Share] timeout:5];
963
964 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
965 }
966
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
970
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");
980
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");
991 return TRUE;
992 }];
993
994 self.currentSelfPeer.encryptionKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
995 self.pastSelfPeers = [NSMutableSet set];
996 [self.injectedManager sendSelfPeerChangedUpdate];
997
998 OCMVerifyAllWithDelay(self.mockDatabase, 20);
999 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
1000
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");
1011 return TRUE;
1012 }];
1013
1014 self.currentSelfPeer.signingKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
1015 self.pastSelfPeers = [NSMutableSet set];
1016 [self.injectedManager sendSelfPeerChangedUpdate];
1017
1018 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1019 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
1020 }
1021
1022 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToRecentTLKShare {
1023 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1024
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];
1027
1028 NSDateComponents* offset = [[NSDateComponents alloc] init];
1029 [offset setDay:-5];
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;
1035 }
1036 }
1037
1038 self.keychainZone.flag = true;
1039 [self startCKKSSubsystem];
1040
1041 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1042
1043 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1044 }
1045
1046 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToVeryRecentUntrustedTLKShare {
1047 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1048
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];
1054
1055 NSDateComponents* offset = [[NSDateComponents alloc] init];
1056 [offset setDay:-2];
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;
1062 }
1063 }
1064
1065 self.keychainZone.flag = true;
1066 [self startCKKSSubsystem];
1067
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");
1070
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'");
1073 }
1074
1075 - (void)testResetCloudKitZoneFromWaitForTLKDueToUntustedTLKShareNotRecentEnough {
1076 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1077
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];
1083
1084 NSDateComponents* offset = [[NSDateComponents alloc] init];
1085 [offset setDay:-5];
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;
1091 }
1092 }
1093
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'");
1099
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'");
1103
1104 // And the zone should have been cleared and re-made
1105 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1106 }
1107
1108 - (void)testNoSelfEncryptionKeys {
1109 // If you lose your local encryption keys, CKKS should do something reasonable
1110
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];
1115
1116 // But, we lost our local keys :(
1117 id<CKKSSelfPeer> oldSelfPeer = self.currentSelfPeer;
1118
1119 self.currentSelfPeer = nil;
1120 self.currentSelfPeerError = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecParam description:@"injected test failure"];
1121
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'");
1125
1126 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1127 [self waitForCKModifications];
1128
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];
1134 }];
1135 [self waitForExpectations:@[callbackOccurs] timeout:20];
1136
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];
1140
1141 self.currentSelfPeer = oldSelfPeer;
1142 self.currentSelfPeerError = nil;
1143
1144 [self.injectedManager sendSelfPeerChangedUpdate];
1145 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become 'ready''");
1146
1147 OCMVerifyAllWithDelay(self.mockDatabase, 20);
1148 [self waitForCKModifications];
1149 }
1150
1151 @end
1152
1153 #endif // OCTAGON