]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTLKSharingTests.m
Security-58286.41.2.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
30 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
31 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
32 #import "keychain/ckks/CKKS.h"
33 #import "keychain/ckks/CKKSKey.h"
34 #import "keychain/ckks/CKKSPeer.h"
35 #import "keychain/ckks/CKKSTLKShare.h"
36 #import "keychain/ckks/CKKSViewManager.h"
37
38 #import "keychain/ckks/tests/MockCloudKit.h"
39 #import "keychain/ckks/tests/CKKSTests.h"
40
41 @interface CloudKitKeychainSyncingTLKSharingTests : CloudKitKeychainSyncingTestsBase
42 @property CKKSSOSSelfPeer* remotePeer1;
43 @property CKKSSOSPeer* remotePeer2;
44
45
46 @property CKKSSOSSelfPeer* untrustedPeer;
47 @end
48
49 @implementation CloudKitKeychainSyncingTLKSharingTests
50
51 - (void)setUp {
52 [super setUp];
53
54 self.remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
55 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
56 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
57
58 self.remotePeer2 = [[CKKSSOSPeer alloc] initWithSOSPeerID:@"remote-peer2"
59 encryptionPublicKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]].publicKey
60 signingPublicKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]].publicKey];
61
62 // Local SOS trusts these peers
63 [self.currentPeers addObject:self.remotePeer1];
64 [self.currentPeers addObject:self.remotePeer2];
65
66 self.untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
67 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
68 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
69 }
70
71 - (void)tearDown {
72 self.remotePeer1 = nil;
73 self.remotePeer2 = nil;
74 self.untrustedPeer = nil;
75
76 [super tearDown];
77 }
78
79 - (void)testAcceptExistingTLKSharedKeyHierarchy {
80 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
81 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
82
83 // Test also starts with the TLK shared to all trusted peers from peer1
84 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
85
86 // The CKKS subsystem should accept the keys, and share the TLK back to itself
87 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
88 [self startCKKSSubsystem];
89 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
90
91 OCMVerifyAllWithDelay(self.mockDatabase, 8);
92
93 // Verify that there are three local keys, and three local current key records
94 __weak __typeof(self) weakSelf = self;
95 [self.keychainView dispatchSync: ^bool{
96 __strong __typeof(weakSelf) strongSelf = weakSelf;
97 XCTAssertNotNil(strongSelf, "self exists");
98
99 NSError* error = nil;
100
101 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
102 XCTAssertNil(error, "no error fetching keys");
103 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
104
105 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all:&error];
106 XCTAssertNil(error, "no error fetching current keys");
107 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
108
109 return false;
110 }];
111 }
112
113 - (void)testAcceptExistingTLKSharedKeyHierarchyAndUse {
114 // Test starts with nothing in database, but one in our fake CloudKit.
115 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
116
117 // Test also starts with the TLK shared to all trusted peers from peer1
118 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
119
120 // The CKKS subsystem should accept the keys, and share the TLK back to itself
121 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
122 [self startCKKSSubsystem];
123 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
124
125 // We expect a single record to be uploaded for each key class
126 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
127 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
128 [self addGenericPassword: @"data" account: @"account-delete-me"];
129 OCMVerifyAllWithDelay(self.mockDatabase, 8);
130
131 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
132 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
133 [self addGenericPassword:@"asdf"
134 account:@"account-class-A"
135 viewHint:nil
136 access:(id)kSecAttrAccessibleWhenUnlocked
137 expecting:errSecSuccess
138 message:@"Adding class A item"];
139 OCMVerifyAllWithDelay(self.mockDatabase, 8);
140 }
141
142 - (void)testNewTLKSharesHaveChangeTags {
143 // Since there's currently no flow for CKKS to ever update a TLK share when things are working properly, do some hackery
144
145 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
146 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
147
148 // Test also starts with the TLK shared to all trusted peers from peer1
149 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
150 [self saveTLKSharesInLocalDatabase: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:10*NSEC_PER_SEC], "Key state should become ready");
156
157 OCMVerifyAllWithDelay(self.mockDatabase, 8);
158 [self waitForCKModifications];
159
160 // Verify that making a new share will have the old share's change tag
161 __weak __typeof(self) weakSelf = self;
162 [self.keychainView dispatchSyncWithAccountKeys: ^bool{
163 __strong __typeof(weakSelf) strongSelf = weakSelf;
164 XCTAssertNotNil(strongSelf, "self exists");
165
166 NSError* error = nil;
167 CKKSTLKShare* share = [CKKSTLKShare share:strongSelf.keychainZoneKeys.tlk
168 as:strongSelf.currentSelfPeer
169 to:strongSelf.currentSelfPeer
170 epoch:-1
171 poisoned:0
172 error:&error];
173 XCTAssertNil(error, "Shouldn't be an error creating a share");
174 XCTAssertNotNil(share, "Should be able to create share");
175
176 CKRecord* newRecord = [share CKRecordWithZoneID:strongSelf.keychainZoneID];
177 XCTAssertNotNil(newRecord, "Should be able to create a CKRecord");
178
179 CKRecord* cloudKitRecord = strongSelf.keychainZone.currentDatabase[newRecord.recordID];
180 XCTAssertNotNil(cloudKitRecord, "Should have found existing CKRecord in cloudkit");
181 XCTAssertNotNil(cloudKitRecord.recordChangeTag, "Existing record should have a change tag");
182
183 XCTAssertEqualObjects(cloudKitRecord.recordChangeTag, newRecord.recordChangeTag, "Change tags on existing and new records should match");
184
185 return false;
186 }];
187 }
188
189 - (void)testReceiveTLKShareRecordsAndDeletes {
190 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
191 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
192
193 // Test also starts with the TLK shared to all trusted peers from peer1
194 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
195
196 // The CKKS subsystem should accept the keys, and share the TLK back to itself
197 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
198 [self startCKKSSubsystem];
199 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
200
201 // The CKKS subsystem should not try to write anything to the CloudKit database while it's accepting the keys
202 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
203 OCMVerifyAllWithDelay(self.mockDatabase, 8);
204
205
206 // Make another share, but from an untrusted peer to some other peer. local shouldn't necessarily care.
207 NSError* error = nil;
208 CKKSTLKShare* share = [CKKSTLKShare share:self.keychainZoneKeys.tlk
209 as:self.untrustedPeer
210 to:self.remotePeer1
211 epoch:-1
212 poisoned:0
213 error:&error];
214 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
215 XCTAssertNotNil(share, "Should be able to create a share");
216
217 CKRecord* shareCKRecord = [share CKRecordWithZoneID: self.keychainZoneID];
218 XCTAssertNotNil(shareCKRecord, "Should have been able to create a CKRecord");
219 [self.keychainZone addToZone:shareCKRecord];
220 [self.keychainView notifyZoneChange:nil];
221 [self.keychainView waitForFetchAndIncomingQueueProcessing];
222
223 [self.keychainView dispatchSync:^bool {
224 NSError* blockerror = nil;
225 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
226 XCTAssertNil(blockerror, "Shouldn't error finding TLKShare record in database");
227 XCTAssertNotNil(localshare, "Should be able to find a TLKShare record in database");
228 return true;
229 }];
230
231 // Delete the record in CloudKit...
232 [self.keychainZone deleteCKRecordIDFromZone:shareCKRecord.recordID];
233 [self.keychainView notifyZoneChange:nil];
234 [self.keychainView waitForFetchAndIncomingQueueProcessing];
235
236 // Should be gone now.
237 [self.keychainView dispatchSync:^bool {
238 NSError* blockerror = nil;
239 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
240
241 XCTAssertNil(blockerror, "Shouldn't error trying to find non-existent TLKShare record in database");
242 XCTAssertNil(localshare, "Shouldn't be able to find a TLKShare record in database");
243
244 return true;
245 }];
246 }
247
248 - (void)testReceiveSharedTLKWhileInWaitForTLK {
249 // Test starts with nothing in database, but one in our fake CloudKit.
250 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
251
252 // Spin up CKKS subsystem.
253 [self startCKKSSubsystem];
254
255 // The CKKS subsystem should not try to write anything to the CloudKit database, but it should enter waitfortlk
256 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
257
258 // peer1 arrives to save the day
259 // The CKKS subsystem should accept the keys, and share the TLK back to itself
260 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
261
262 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
263 [self.keychainView notifyZoneChange:nil];
264 [self.keychainView waitForFetchAndIncomingQueueProcessing];
265
266 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
267
268 // We expect a single record to be uploaded for each key class
269 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
270 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
271 [self addGenericPassword: @"data" account: @"account-delete-me"];
272 OCMVerifyAllWithDelay(self.mockDatabase, 8);
273
274 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
275 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
276 [self addGenericPassword:@"asdf"
277 account:@"account-class-A"
278 viewHint:nil
279 access:(id)kSecAttrAccessibleWhenUnlocked
280 expecting:errSecSuccess
281 message:@"Adding class A item"];
282 OCMVerifyAllWithDelay(self.mockDatabase, 8);
283 }
284
285 - (void)testReceiveTLKShareWhileLocked {
286 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
287 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
288
289 // Test also starts with the TLK shared to all trusted peers from peer1
290 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
291
292 self.aksLockState = true;
293 [self.lockStateTracker recheck];
294
295 // Spin up CKKS subsystem.
296 [self startCKKSSubsystem];
297
298 // The CKKS subsystem should not try to write anything to the CloudKit database, but it should enter waitforunlock
299 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:10*NSEC_PER_SEC], "Key state should become waitforunlock");
300
301 // Now unlock things. We expect a TLKShare upload.
302 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
303
304 self.aksLockState = false;
305 [self.lockStateTracker recheck];
306
307 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
308 OCMVerifyAllWithDelay(self.mockDatabase, 8);
309 }
310
311 - (void)testUploadTLKSharesForExistingHierarchy {
312 // Test starts with key material locally and in CloudKit.
313 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
314 [self saveTLKMaterialToKeychain:self.keychainZoneID];
315
316 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
317 [self startCKKSSubsystem];
318
319 OCMVerifyAllWithDelay(self.mockDatabase, 8);
320 }
321
322 - (void)testUploadTLKSharesForExistingHierarchyOnRestart {
323 // Bring up CKKS. It'll upload a few TLK Shares, but we'll delete them to get into state
324 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
325 [self saveTLKMaterialToKeychain:self.keychainZoneID];
326
327 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
328 [self startCKKSSubsystem];
329
330 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
331 OCMVerifyAllWithDelay(self.mockDatabase, 8);
332 [self waitForCKModifications];
333
334 // Now, delete all the TLK Shares, so CKKS will upload them again
335 [self.keychainView dispatchSync:^bool {
336 NSError* error = nil;
337 [CKKSTLKShare deleteAll:self.keychainZoneID error:&error];
338 XCTAssertNil(error, "Shouldn't be an error deleting all TLKShares");
339
340 NSArray<CKRecord*>* records = [self.zones[self.keychainZoneID].currentDatabase allValues];
341 for(CKRecord* record in records) {
342 if([record.recordType isEqualToString:SecCKRecordTLKShareType]) {
343 [self.zones[self.keychainZoneID] deleteFromHistory:record.recordID];
344 }
345 }
346
347 return true;
348 }];
349
350 // Restart. We expect an upload of 3 TLK shares.
351 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
352 self.keychainView = [self.injectedManager restartZone: self.keychainZoneID.zoneName];
353
354 OCMVerifyAllWithDelay(self.mockDatabase, 8);
355 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
356 }
357
358 - (void)testHandleExternalSharedTLKRoll {
359 // Test starts with key material locally and in CloudKit.
360 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
361 [self saveTLKMaterialToKeychain:self.keychainZoneID];
362
363 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
364 [self startCKKSSubsystem];
365
366 OCMVerifyAllWithDelay(self.mockDatabase, 8);
367
368 // Now the external peer rolls the TLK and updates the shares
369 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
370 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
371
372 // CKKS will share the TLK back to itself
373 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
374
375 // Trigger a notification
376 [self.keychainView notifyZoneChange:nil];
377 [self.keychainView waitForFetchAndIncomingQueueProcessing];
378 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
379 OCMVerifyAllWithDelay(self.mockDatabase, 8);
380 [self waitForCKModifications];
381
382 // We expect a single record to be uploaded.
383 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
384 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
385 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
386
387 OCMVerifyAllWithDelay(self.mockDatabase, 8);
388 }
389
390 - (void)testUploadTLKSharesForExternalTLKRollWithoutShares {
391 // Test starts with key material locally and in CloudKit.
392 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
393 [self saveTLKMaterialToKeychain:self.keychainZoneID];
394
395 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
396 [self startCKKSSubsystem];
397
398 OCMVerifyAllWithDelay(self.mockDatabase, 8);
399
400 // Now, an old (Tigris) peer rolls the TLK, but doesn't share it
401 // CKKS should get excited and throw 3 new share records up
402 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
403
404 // Wait for that modification to finish before changing CK data
405 [self waitForCKModifications];
406
407 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
408 [self saveTLKMaterialToKeychain:self.keychainZoneID];
409
410 // Trigger a notification
411 [self.keychainView notifyZoneChange:nil];
412
413 OCMVerifyAllWithDelay(self.mockDatabase, 8);
414 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
415
416 // We expect a single record to be uploaded.
417 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
418 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
419 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
420
421 OCMVerifyAllWithDelay(self.mockDatabase, 8);
422 }
423
424 - (void)testRecoverFromTLKShareUploadFailure {
425 // Test starts with key material locally and in CloudKit.
426 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
427 [self saveTLKMaterialToKeychain:self.keychainZoneID];
428
429 __weak __typeof(self) weakSelf = self;
430 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:^{
431 __strong __typeof(self) strongSelf = weakSelf;
432 [strongSelf expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
433 }];
434 [self startCKKSSubsystem];
435
436 OCMVerifyAllWithDelay(self.mockDatabase, 8);
437 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
438 }
439
440 - (void)testFillInMissingPeerShares {
441 // Test starts with nothing in database, but one in our fake CloudKit.
442 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
443
444 // Test also starts with the TLK shared to just the local peer from peer1
445 // We expect the local peer to send it to peer2
446 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
447
448 // The CKKS subsystem should accept the keys, and share the TLK back to itself
449 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
450 [self startCKKSSubsystem];
451 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
452
453 // We expect a single record to be uploaded for each key class
454 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
455 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
456 [self addGenericPassword: @"data" account: @"account-delete-me"];
457 OCMVerifyAllWithDelay(self.mockDatabase, 8);
458
459 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
460 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
461 [self addGenericPassword:@"asdf"
462 account:@"account-class-A"
463 viewHint:nil
464 access:(id)kSecAttrAccessibleWhenUnlocked
465 expecting:errSecSuccess
466 message:@"Adding class A item"];
467 OCMVerifyAllWithDelay(self.mockDatabase, 8);
468 }
469
470 - (void)testDontAcceptTLKFromUntrustedPeer {
471 // Test starts with nothing in database, but key hierarchy in our fake CloudKit.
472 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
473
474 // Test also starts with the key hierarchy shared from a non-trusted peer
475 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer zoneID:self.keychainZoneID];
476
477 // The CKKS subsystem should go into waitfortlk, since it doesn't trust this peer
478 [self startCKKSSubsystem];
479 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become ready");
480 }
481
482 - (void)testAcceptSharedTLKOnTrustSetAdditionOfSharer {
483 // Test starts with nothing in database, but key hierarchy in our fake CloudKit.
484 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
485
486 // Test also starts with the key hierarchy shared from a non-trusted peer
487 // note that it would share it itself too
488 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer zoneID:self.keychainZoneID];
489 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer to:self.untrustedPeer zoneID:self.keychainZoneID];
490
491 // The CKKS subsystem should go into waitfortlk, since it doesn't trust this peer
492 [self startCKKSSubsystem];
493 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
494
495 // Wait to be sure we really get into that state
496 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
497
498 // Now, trust the previously-untrusted peer
499 [self.currentPeers addObject: self.untrustedPeer];
500 [self.injectedManager sendTrustedPeerSetChangedUpdate];
501
502 // The CKKS subsystem should now accept the key, and share the TLK back to itself
503 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
504
505 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should become ready");
506
507 // And use it as well
508 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
509 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
510 [self addGenericPassword: @"data" account: @"account-delete-me"];
511 OCMVerifyAllWithDelay(self.mockDatabase, 8);
512 }
513
514 - (void)testSendNewTLKSharesOnTrustSetAddition {
515 // step 1: add a new peer; we should share the TLK with them
516 // start with no trusted peers
517 [self.currentPeers removeAllObjects];
518
519 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
520 [self startCKKSSubsystem];
521
522 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
523 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
524 [self addGenericPassword: @"data" account: @"account-delete-me"];
525 OCMVerifyAllWithDelay(self.mockDatabase, 8);
526
527 // Cool! New peer arrives!
528 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
529 [self.currentPeers addObject:self.remotePeer1];
530 [self.injectedManager sendTrustedPeerSetChangedUpdate];
531
532 OCMVerifyAllWithDelay(self.mockDatabase, 8);
533 [self waitForCKModifications];
534
535 // step 2: add a new peer who already has a share; no share should be created
536 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 to:self.remotePeer2 zoneID:self.keychainZoneID];
537 [self.keychainView notifyZoneChange:nil];
538 [self.keychainView waitForFetchAndIncomingQueueProcessing];
539
540 // CKKS should not upload a tlk share for this peer
541 [self.currentPeers addObject:self.remotePeer2];
542 [self.injectedManager sendTrustedPeerSetChangedUpdate];
543
544 [self.keychainView waitUntilAllOperationsAreFinished];
545 }
546
547 - (void)testFillInMissingPeerSharesAfterUnlock {
548 // step 1: add a new peer; we should share the TLK with them
549 // start with no trusted peers
550 [self.currentPeers removeAllObjects];
551
552 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
553 [self startCKKSSubsystem];
554
555 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
556
557 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
558 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
559 [self addGenericPassword: @"data" account: @"account-delete-me"];
560 OCMVerifyAllWithDelay(self.mockDatabase, 8);
561
562 // Now, lock.
563 self.aksLockState = true;
564 [self.lockStateTracker recheck];
565
566 // New peer arrives! This can't actually happen (since we have to be unlocked to accept a new peer), but this will exercise CKKS
567 [self.currentPeers addObject:self.remotePeer1];
568 [self.injectedManager sendTrustedPeerSetChangedUpdate];
569
570 // CKKS should notice that it has things to do...
571 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
572
573 // And do them.
574 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
575 self.aksLockState = false;
576 [self.lockStateTracker recheck];
577
578 OCMVerifyAllWithDelay(self.mockDatabase, 8);
579
580 // and return to ready
581 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
582 }
583
584 - (void)testAddItemDuringNewTLKSharesOnTrustSetAddition {
585 // step 1: add a new peer; we should share the TLK with them
586 // start with no trusted peers
587 [self.currentPeers removeAllObjects];
588
589 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
590 [self startCKKSSubsystem];
591
592 OCMVerifyAllWithDelay(self.mockDatabase, 8);
593 [self waitForCKModifications];
594
595 // Hold the TLK share modification
596 [self holdCloudKitModifications];
597
598 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
599 [self.currentPeers addObject:self.remotePeer1];
600 [self.injectedManager sendTrustedPeerSetChangedUpdate];
601
602 OCMVerifyAllWithDelay(self.mockDatabase, 8);
603
604 // While CloudKit is hanging the write, add an item
605 [self addGenericPassword: @"data" account: @"account-delete-me"];
606
607 // After that returns, release the write. CKKS should upload the new item
608 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
609 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
610 [self releaseCloudKitModificationHold];
611
612 OCMVerifyAllWithDelay(self.mockDatabase, 8);
613 }
614
615 - (void)testSendNewTLKSharesOnTrustSetRemoval {
616 // Not implemented. Trust set removal demands a key roll, but let's not get ahead of ourselves...
617 }
618
619
620 @end
621
622 #endif // OCTAGON