]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTLKSharingTests.m
Security-58286.70.7.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
42 #import "keychain/ckks/tests/MockCloudKit.h"
43 #import "keychain/ckks/tests/CKKSTests.h"
44 #import "keychain/ot/OTDefines.h"
45
46 @interface CloudKitKeychainSyncingTLKSharingTests : CloudKitKeychainSyncingTestsBase
47 @property CKKSSOSSelfPeer* remotePeer1;
48 @property CKKSSOSPeer* remotePeer2;
49
50
51 @property CKKSSOSSelfPeer* untrustedPeer;
52
53 @property (nullable) NSMutableSet<id<CKKSSelfPeer>>* pastSelfPeers;
54
55 // Used to test a single code path. If true, no past self peers will be valid
56 @property bool breakLoadSelfPeerEncryptionKey;
57 @end
58
59 @implementation CloudKitKeychainSyncingTLKSharingTests
60
61 - (void)setUp {
62 [super setUp];
63
64 self.pastSelfPeers = [NSMutableSet set];
65
66 // Use the upsetting old-style mocks so we can ignore the enum
67 [[[[self.mockCKKSViewManager stub] andCall:@selector(fakeLoadRestoredBottledKeysOfType:error:)
68 onObject:self] ignoringNonObjectArgs]
69 loadRestoredBottledKeysOfType:0 error:[OCMArg anyObjectRef]];
70
71 self.remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
72 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
73 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
74
75 self.remotePeer2 = [[CKKSSOSPeer alloc] initWithSOSPeerID:@"remote-peer2"
76 encryptionPublicKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]].publicKey
77 signingPublicKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]].publicKey];
78
79 // Local SOS trusts these peers
80 [self.currentPeers addObject:self.remotePeer1];
81 [self.currentPeers addObject:self.remotePeer2];
82
83 self.untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
84 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
85 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
86 }
87
88 - (void)tearDown {
89 self.pastSelfPeers = nil;
90 self.remotePeer1 = nil;
91 self.remotePeer2 = nil;
92 self.untrustedPeer = nil;
93
94 [super tearDown];
95 }
96
97
98 - (NSArray<NSDictionary *>* _Nullable)fakeLoadRestoredBottledKeysOfType:(OctagonKeyType)keyType error:(NSError**)error {
99 if(self.aksLockState) {
100 if(error) {
101 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecInteractionNotAllowed userInfo:nil];
102 }
103 return nil;
104 } else {
105 if(self.breakLoadSelfPeerEncryptionKey && keyType == OctagonEncryptionKey) {
106 if(error) {
107 *error = [NSError errorWithDomain:(__bridge NSString*)kSecErrorDomain code:errSecItemNotFound userInfo:nil];
108 }
109 return nil;
110 }
111
112 // Convert self.pastSelfPeers into an array of dictionaries
113 NSMutableArray<NSDictionary*>* keys = [NSMutableArray array];
114
115 for(id<CKKSSelfPeer> peer in self.pastSelfPeers) {
116 SFECKeyPair* key = nil;
117
118 switch(keyType) {
119 case OctagonSigningKey:
120 key = peer.signingKey;
121 break;
122 case OctagonEncryptionKey:
123 key = peer.encryptionKey;
124 break;
125 }
126
127 XCTAssertNotNil(key, "Should have a key at this point");
128
129 NSData* signingPublicKeyHashBytes = [SFSHA384DigestOperation digest:peer.signingKey.publicKey.keyData];
130 NSString* signingPublicKeyHash = [signingPublicKeyHashBytes base64EncodedStringWithOptions:0];
131
132 NSDictionary* dict = @{
133 (id)kSecAttrAccount : peer.peerID,
134 (id)kSecAttrLabel : signingPublicKeyHash,
135 (id)kSecValueData : key.keyData,
136 };
137 [keys addObject:dict];
138 }
139
140 return keys;
141 }
142 }
143
144 - (void)testAcceptExistingTLKSharedKeyHierarchy {
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
151 // The CKKS subsystem should accept the keys, and share the TLK back to itself
152 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
153 [self startCKKSSubsystem];
154 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
155
156 OCMVerifyAllWithDelay(self.mockDatabase, 8);
157
158 // Verify that there are three local keys, and three local current key records
159 __weak __typeof(self) weakSelf = self;
160 [self.keychainView dispatchSync: ^bool{
161 __strong __typeof(weakSelf) strongSelf = weakSelf;
162 XCTAssertNotNil(strongSelf, "self exists");
163
164 NSError* error = nil;
165
166 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
167 XCTAssertNil(error, "no error fetching keys");
168 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
169
170 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all:&error];
171 XCTAssertNil(error, "no error fetching current keys");
172 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
173
174 return false;
175 }];
176 }
177
178 - (void)testAcceptExistingTLKSharedKeyHierarchyForPastSelf {
179 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
180 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
181
182 // Test also starts with the TLK shared to all trusted peers from peer1
183 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
184
185 // Self rolls its keys and ID...
186 [self.pastSelfPeers addObject:self.currentSelfPeer];
187 self.currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"new-local-peer"
188 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
189 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
190
191 // The CKKS subsystem should accept the keys, and share the TLK back to itself
192 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
193 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
194 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
195 XCTAssertEqualObjects(share.receiver.peerID, self.currentSelfPeer.peerID, "Receiver peerID on TLKShare should match current self");
196 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.currentSelfPeer.publicEncryptionKey, "Receiver encryption key on TLKShare should match current self");
197 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
198 return TRUE;
199 }];
200 [self startCKKSSubsystem];
201 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
202
203 OCMVerifyAllWithDelay(self.mockDatabase, 8);
204
205 // Verify that there are three local keys, and three local current key records
206 __weak __typeof(self) weakSelf = self;
207 [self.keychainView dispatchSync: ^bool{
208 __strong __typeof(weakSelf) strongSelf = weakSelf;
209 XCTAssertNotNil(strongSelf, "self exists");
210
211 NSError* error = nil;
212
213 NSArray<CKKSKey*>* keys = [CKKSKey localKeys:strongSelf.keychainZoneID error:&error];
214 XCTAssertNil(error, "no error fetching keys");
215 XCTAssertEqual(keys.count, 3u, "Three keys in local database");
216
217 NSArray<CKKSCurrentKeyPointer*>* currentkeys = [CKKSCurrentKeyPointer all:&error];
218 XCTAssertNil(error, "no error fetching current keys");
219 XCTAssertEqual(currentkeys.count, 3u, "Three current key pointers in local database");
220
221 return false;
222 }];
223 }
224
225 - (void)testDontCrashOnHalfBottle {
226 self.breakLoadSelfPeerEncryptionKey = true;
227
228 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
229 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
230
231 // Test also starts with the TLK shared to all trusted peers from peer1
232 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
233
234 // Self rolls its keys and ID...
235 [self.pastSelfPeers addObject:self.currentSelfPeer];
236 self.currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"new-local-peer"
237 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
238 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
239
240 // CKKS should enter 'waitfortlk' without crashing
241 [self startCKKSSubsystem];
242 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:10*NSEC_PER_SEC], "Key state should become waitfortlk");
243 }
244
245 - (void)testAcceptExistingTLKSharedKeyHierarchyAndUse {
246 // Test starts with nothing in database, but one in our fake CloudKit.
247 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
248
249 // Test also starts with the TLK shared to all trusted peers from peer1
250 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
251
252 // The CKKS subsystem should accept the keys, and share the TLK back to itself
253 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
254 [self startCKKSSubsystem];
255 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
256
257 // We expect a single record to be uploaded for each key class
258 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
259 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
260 [self addGenericPassword: @"data" account: @"account-delete-me"];
261 OCMVerifyAllWithDelay(self.mockDatabase, 8);
262
263 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
264 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
265 [self addGenericPassword:@"asdf"
266 account:@"account-class-A"
267 viewHint:nil
268 access:(id)kSecAttrAccessibleWhenUnlocked
269 expecting:errSecSuccess
270 message:@"Adding class A item"];
271 OCMVerifyAllWithDelay(self.mockDatabase, 8);
272 }
273
274 - (void)testNewTLKSharesHaveChangeTags {
275 // Since there's currently no flow for CKKS to ever update a TLK share when things are working properly, do some hackery
276
277 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
278 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
279
280 // Test also starts with the TLK shared to all trusted peers from peer1
281 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
282 [self saveTLKSharesInLocalDatabase:self.keychainZoneID];
283
284 // The CKKS subsystem should accept the keys, and share the TLK back to itself
285 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
286 [self startCKKSSubsystem];
287 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
288
289 OCMVerifyAllWithDelay(self.mockDatabase, 8);
290 [self waitForCKModifications];
291
292 // Verify that making a new share will have the old share's change tag
293 __weak __typeof(self) weakSelf = self;
294 [self.keychainView dispatchSyncWithAccountKeys: ^bool{
295 __strong __typeof(weakSelf) strongSelf = weakSelf;
296 XCTAssertNotNil(strongSelf, "self exists");
297
298 NSError* error = nil;
299 CKKSTLKShare* share = [CKKSTLKShare share:strongSelf.keychainZoneKeys.tlk
300 as:strongSelf.currentSelfPeer
301 to:strongSelf.currentSelfPeer
302 epoch:-1
303 poisoned:0
304 error:&error];
305 XCTAssertNil(error, "Shouldn't be an error creating a share");
306 XCTAssertNotNil(share, "Should be able to create share");
307
308 CKRecord* newRecord = [share CKRecordWithZoneID:strongSelf.keychainZoneID];
309 XCTAssertNotNil(newRecord, "Should be able to create a CKRecord");
310
311 CKRecord* cloudKitRecord = strongSelf.keychainZone.currentDatabase[newRecord.recordID];
312 XCTAssertNotNil(cloudKitRecord, "Should have found existing CKRecord in cloudkit");
313 XCTAssertNotNil(cloudKitRecord.recordChangeTag, "Existing record should have a change tag");
314
315 XCTAssertEqualObjects(cloudKitRecord.recordChangeTag, newRecord.recordChangeTag, "Change tags on existing and new records should match");
316
317 return false;
318 }];
319 }
320
321 - (void)testReceiveTLKShareRecordsAndDeletes {
322 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
323 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
324
325 // Test also starts with the TLK shared to all trusted peers from peer1
326 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
327
328 // The CKKS subsystem should accept the keys, and share the TLK back to itself
329 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
330 [self startCKKSSubsystem];
331 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
332
333 // The CKKS subsystem should not try to write anything to the CloudKit database while it's accepting the keys
334 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
335 OCMVerifyAllWithDelay(self.mockDatabase, 8);
336
337
338 // Make another share, but from an untrusted peer to some other peer. local shouldn't necessarily care.
339 NSError* error = nil;
340 CKKSTLKShare* share = [CKKSTLKShare share:self.keychainZoneKeys.tlk
341 as:self.untrustedPeer
342 to:self.remotePeer1
343 epoch:-1
344 poisoned:0
345 error:&error];
346 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
347 XCTAssertNotNil(share, "Should be able to create a share");
348
349 CKRecord* shareCKRecord = [share CKRecordWithZoneID: self.keychainZoneID];
350 XCTAssertNotNil(shareCKRecord, "Should have been able to create a CKRecord");
351 [self.keychainZone addToZone:shareCKRecord];
352 [self.keychainView notifyZoneChange:nil];
353 [self.keychainView waitForFetchAndIncomingQueueProcessing];
354
355 [self.keychainView dispatchSync:^bool {
356 NSError* blockerror = nil;
357 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
358 XCTAssertNil(blockerror, "Shouldn't error finding TLKShare record in database");
359 XCTAssertNotNil(localshare, "Should be able to find a TLKShare record in database");
360 return true;
361 }];
362
363 // Delete the record in CloudKit...
364 [self.keychainZone deleteCKRecordIDFromZone:shareCKRecord.recordID];
365 [self.keychainView notifyZoneChange:nil];
366 [self.keychainView waitForFetchAndIncomingQueueProcessing];
367
368 // Should be gone now.
369 [self.keychainView dispatchSync:^bool {
370 NSError* blockerror = nil;
371 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
372
373 XCTAssertNil(blockerror, "Shouldn't error trying to find non-existent TLKShare record in database");
374 XCTAssertNil(localshare, "Shouldn't be able to find a TLKShare record in database");
375
376 return true;
377 }];
378 }
379
380 - (void)testReceiveSharedTLKWhileInWaitForTLK {
381 // Test starts with nothing in database, but one in our fake CloudKit.
382 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
383 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
384
385 // Spin up CKKS subsystem.
386 [self startCKKSSubsystem];
387
388 // The CKKS subsystem should not try to write anything to the CloudKit database, but it should enter waitfortlk
389 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
390
391 // peer1 arrives to save the day
392 // The CKKS subsystem should accept the keys, and share the TLK back to itself
393 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
394
395 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
396 [self.keychainView notifyZoneChange:nil];
397 [self.keychainView waitForFetchAndIncomingQueueProcessing];
398
399 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
400
401 // We expect a single record to be uploaded for each key class
402 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
403 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
404 [self addGenericPassword: @"data" account: @"account-delete-me"];
405 OCMVerifyAllWithDelay(self.mockDatabase, 8);
406
407 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
408 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
409 [self addGenericPassword:@"asdf"
410 account:@"account-class-A"
411 viewHint:nil
412 access:(id)kSecAttrAccessibleWhenUnlocked
413 expecting:errSecSuccess
414 message:@"Adding class A item"];
415 OCMVerifyAllWithDelay(self.mockDatabase, 8);
416 }
417
418 - (void)testReceiveTLKShareWhileLocked {
419 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
420 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
421
422 // Test also starts with the TLK shared to all trusted peers from peer1
423 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
424
425 self.aksLockState = true;
426 [self.lockStateTracker recheck];
427
428 // Spin up CKKS subsystem.
429 [self startCKKSSubsystem];
430
431 // The CKKS subsystem should not try to write anything to the CloudKit database, but it should enter waitforunlock
432 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:10*NSEC_PER_SEC], "Key state should become waitforunlock");
433
434 // Now unlock things. We expect a TLKShare upload.
435 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
436
437 self.aksLockState = false;
438 [self.lockStateTracker recheck];
439
440 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
441 OCMVerifyAllWithDelay(self.mockDatabase, 8);
442 }
443
444 - (void)testUploadTLKSharesForExistingHierarchy {
445 // Test starts with key material locally and in CloudKit.
446 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
447 [self saveTLKMaterialToKeychain:self.keychainZoneID];
448
449 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
450 [self startCKKSSubsystem];
451
452 OCMVerifyAllWithDelay(self.mockDatabase, 8);
453 }
454
455 - (void)testUploadTLKSharesForExistingHierarchyOnRestart {
456 // Bring up CKKS. It'll upload a few TLK Shares, but we'll delete them to get into state
457 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
458 [self saveTLKMaterialToKeychain:self.keychainZoneID];
459
460 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
461 [self startCKKSSubsystem];
462
463 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
464 OCMVerifyAllWithDelay(self.mockDatabase, 8);
465 [self waitForCKModifications];
466
467 // Now, delete all the TLK Shares, so CKKS will upload them again
468 [self.keychainView dispatchSync:^bool {
469 NSError* error = nil;
470 [CKKSTLKShare deleteAll:self.keychainZoneID error:&error];
471 XCTAssertNil(error, "Shouldn't be an error deleting all TLKShares");
472
473 NSArray<CKRecord*>* records = [self.zones[self.keychainZoneID].currentDatabase allValues];
474 for(CKRecord* record in records) {
475 if([record.recordType isEqualToString:SecCKRecordTLKShareType]) {
476 [self.zones[self.keychainZoneID] deleteFromHistory:record.recordID];
477 }
478 }
479
480 return true;
481 }];
482
483 // Restart. We expect an upload of 3 TLK shares.
484 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
485 self.keychainView = [self.injectedManager restartZone: self.keychainZoneID.zoneName];
486
487 OCMVerifyAllWithDelay(self.mockDatabase, 8);
488 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
489 }
490
491 - (void)testHandleExternalSharedTLKRoll {
492 // Test starts with key material locally and in CloudKit.
493 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
494 [self saveTLKMaterialToKeychain:self.keychainZoneID];
495
496 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
497 [self startCKKSSubsystem];
498
499 OCMVerifyAllWithDelay(self.mockDatabase, 8);
500 [self waitForCKModifications];
501
502 // Now the external peer rolls the TLK and updates the shares
503 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
504 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
505
506 // CKKS will share the TLK back to itself
507 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
508
509 // Trigger a notification
510 [self.keychainView notifyZoneChange:nil];
511 [self.keychainView waitForFetchAndIncomingQueueProcessing];
512 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
513 OCMVerifyAllWithDelay(self.mockDatabase, 8);
514 [self waitForCKModifications];
515
516 // We expect a single record to be uploaded.
517 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
518 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
519 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
520
521 OCMVerifyAllWithDelay(self.mockDatabase, 8);
522 }
523
524 - (void)testUploadTLKSharesForExternalTLKRollWithoutShares {
525 // Test starts with key material locally and in CloudKit.
526 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
527 [self saveTLKMaterialToKeychain:self.keychainZoneID];
528
529 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
530 [self startCKKSSubsystem];
531
532 OCMVerifyAllWithDelay(self.mockDatabase, 8);
533
534 // Now, an old (Tigris) peer rolls the TLK, but doesn't share it
535 // CKKS should get excited and throw 3 new share records up
536 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
537
538 // Wait for that modification to finish before changing CK data
539 [self waitForCKModifications];
540
541 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
542 [self saveTLKMaterialToKeychain:self.keychainZoneID];
543
544 // Trigger a notification
545 [self.keychainView notifyZoneChange:nil];
546
547 OCMVerifyAllWithDelay(self.mockDatabase, 8);
548 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
549
550 // We expect a single record to be uploaded.
551 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
552 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
553 [self addGenericPassword: @"data" account: @"account-delete-me-rolled-key"];
554
555 OCMVerifyAllWithDelay(self.mockDatabase, 8);
556 }
557
558 - (void)testRecoverFromTLKShareUploadFailure {
559 // Test starts with key material locally and in CloudKit.
560 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
561 [self saveTLKMaterialToKeychain:self.keychainZoneID];
562
563 __weak __typeof(self) weakSelf = self;
564 [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:^{
565 __strong __typeof(self) strongSelf = weakSelf;
566 [strongSelf expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
567 }];
568 [self startCKKSSubsystem];
569
570 OCMVerifyAllWithDelay(self.mockDatabase, 8);
571 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
572 }
573
574 - (void)testFillInMissingPeerShares {
575 // Test starts with nothing in database, but one in our fake CloudKit.
576 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
577
578 // Test also starts with the TLK shared to just the local peer from peer1
579 // We expect the local peer to send it to peer2
580 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
581
582 // The CKKS subsystem should accept the keys, and share the TLK back to itself
583 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
584 [self startCKKSSubsystem];
585 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should become ready");
586
587 // We expect a single record to be uploaded for each key class
588 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
589 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
590 [self addGenericPassword: @"data" account: @"account-delete-me"];
591 OCMVerifyAllWithDelay(self.mockDatabase, 8);
592
593 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
594 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
595 [self addGenericPassword:@"asdf"
596 account:@"account-class-A"
597 viewHint:nil
598 access:(id)kSecAttrAccessibleWhenUnlocked
599 expecting:errSecSuccess
600 message:@"Adding class A item"];
601 OCMVerifyAllWithDelay(self.mockDatabase, 8);
602 }
603
604 - (void)testDontAcceptTLKFromUntrustedPeer {
605 // Test starts with nothing in database, but key hierarchy in our fake CloudKit.
606 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
607 // The remote peer should also have given the TLK to a non-TLKShare peer (which is also offline)
608 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
609
610 // Test also starts with the key hierarchy shared from a non-trusted peer
611 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer zoneID:self.keychainZoneID];
612
613 // The CKKS subsystem should go into waitfortlk, since it doesn't trust this peer, but the peer is active
614 [self startCKKSSubsystem];
615 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become ready");
616 }
617
618 - (void)testAcceptSharedTLKOnTrustSetAdditionOfSharer {
619 // Test starts with nothing in database, but key hierarchy in our fake CloudKit.
620 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
621 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
622
623 // Test also starts with the key hierarchy shared from a non-trusted peer
624 // note that it would share it itself too
625 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer zoneID:self.keychainZoneID];
626 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.untrustedPeer to:self.untrustedPeer zoneID:self.keychainZoneID];
627
628 // The CKKS subsystem should go into waitfortlk, since it doesn't trust this peer, but the peer is active
629 [self startCKKSSubsystem];
630 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "Key state should become waitfortlk");
631
632 // Wait to be sure we really get into that state
633 [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
634
635 // Now, trust the previously-untrusted peer
636 [self.currentPeers addObject: self.untrustedPeer];
637 [self.injectedManager sendTrustedPeerSetChangedUpdate];
638
639 // The CKKS subsystem should now accept the key, and share the TLK back to itself
640 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
641
642 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should become ready");
643
644 // And use it as well
645 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
646 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
647 [self addGenericPassword: @"data" account: @"account-delete-me"];
648 OCMVerifyAllWithDelay(self.mockDatabase, 8);
649 }
650
651 - (void)testSendNewTLKSharesOnTrustSetAddition {
652 // step 1: add a new peer; we should share the TLK with them
653 // start with no trusted peers
654 [self.currentPeers removeAllObjects];
655
656 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
657 [self startCKKSSubsystem];
658
659 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
660 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
661 [self addGenericPassword: @"data" account: @"account-delete-me"];
662 OCMVerifyAllWithDelay(self.mockDatabase, 8);
663
664 // Cool! New peer arrives!
665 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
666 [self.currentPeers addObject:self.remotePeer1];
667 [self.injectedManager sendTrustedPeerSetChangedUpdate];
668
669 OCMVerifyAllWithDelay(self.mockDatabase, 8);
670 [self waitForCKModifications];
671
672 // step 2: add a new peer who already has a share; no share should be created
673 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 to:self.remotePeer2 zoneID:self.keychainZoneID];
674 [self.keychainView notifyZoneChange:nil];
675 [self.keychainView waitForFetchAndIncomingQueueProcessing];
676
677 // CKKS should not upload a tlk share for this peer
678 [self.currentPeers addObject:self.remotePeer2];
679 [self.injectedManager sendTrustedPeerSetChangedUpdate];
680
681 [self.keychainView waitUntilAllOperationsAreFinished];
682 }
683
684 - (void)testFillInMissingPeerSharesAfterUnlock {
685 // step 1: add a new peer; we should share the TLK with them
686 // start with no trusted peers
687 [self.currentPeers removeAllObjects];
688
689 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
690 [self startCKKSSubsystem];
691
692 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
693
694 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
695 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
696 [self addGenericPassword: @"data" account: @"account-delete-me"];
697 OCMVerifyAllWithDelay(self.mockDatabase, 8);
698
699 // Now, lock.
700 self.aksLockState = true;
701 [self.lockStateTracker recheck];
702
703 // New peer arrives! This can't actually happen (since we have to be unlocked to accept a new peer), but this will exercise CKKS
704 [self.currentPeers addObject:self.remotePeer1];
705 [self.injectedManager sendTrustedPeerSetChangedUpdate];
706
707 // CKKS should notice that it has things to do...
708 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
709
710 // And do them.
711 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
712 self.aksLockState = false;
713 [self.lockStateTracker recheck];
714
715 OCMVerifyAllWithDelay(self.mockDatabase, 8);
716
717 // and return to ready
718 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
719 }
720
721 - (void)testAddItemDuringNewTLKSharesOnTrustSetAddition {
722 // step 1: add a new peer; we should share the TLK with them
723 // start with no trusted peers
724 [self.currentPeers removeAllObjects];
725
726 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
727 [self startCKKSSubsystem];
728
729 OCMVerifyAllWithDelay(self.mockDatabase, 8);
730 [self waitForCKModifications];
731
732 // Hold the TLK share modification
733 [self holdCloudKitModifications];
734
735 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
736 [self.currentPeers addObject:self.remotePeer1];
737 [self.injectedManager sendTrustedPeerSetChangedUpdate];
738
739 OCMVerifyAllWithDelay(self.mockDatabase, 8);
740
741 // While CloudKit is hanging the write, add an item
742 [self addGenericPassword: @"data" account: @"account-delete-me"];
743
744 // After that returns, release the write. CKKS should upload the new item
745 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
746 checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
747 [self releaseCloudKitModificationHold];
748
749 OCMVerifyAllWithDelay(self.mockDatabase, 8);
750 }
751
752 - (void)testSendNewTLKSharesOnTrustSetRemoval {
753 // Not implemented. Trust set removal demands a key roll, but let's not get ahead of ourselves...
754 }
755
756 - (void)testWaitForTLKWithMissingKeys {
757 // Test starts with no keys in CKKS database, but one in our fake CloudKit.
758 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
759
760 // Test also starts with the TLK shared to all trusted peers from peer1
761 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
762
763 // self no longer has that key pair, but it does have a new one with the same peer ID....
764 self.currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:self.currentSelfPeer.peerID
765 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
766 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
767 self.pastSelfPeers = [NSMutableSet set];
768
769 // CKKS should become very upset, and enter waitfortlk.
770 [self startCKKSSubsystem];
771 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:4000*NSEC_PER_SEC], "Key state should become waitfortlk");
772 OCMVerifyAllWithDelay(self.mockDatabase, 8);
773 }
774
775 - (void)testSendNewTLKShareToPeerOnPeerEncryptionKeyChange {
776 // If a peer changes its keys, CKKS should send it a new TLK share with the right keys
777 // This recovers from the remote peer losing its Octagon keys and making new ones
778
779 // step 1: add a new peer; we should share the TLK with them
780 // start with no trusted peers
781 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:3 zoneID:self.keychainZoneID];
782 [self startCKKSSubsystem];
783
784 OCMVerifyAllWithDelay(self.mockDatabase, 8);
785 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:1000*NSEC_PER_SEC], "Key state should become ready");
786
787 // Remote peer rolls its encryption key...
788 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
789 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
790 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
791 XCTAssertEqualObjects(share.receiver.peerID, self.remotePeer1.peerID, "Receiver peerID on TLKShare should match remote peer");
792 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer1.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer");
793 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
794 return TRUE;
795 }];
796
797 self.remotePeer1.encryptionKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
798 [self.injectedManager sendTrustedPeerSetChangedUpdate];
799
800 OCMVerifyAllWithDelay(self.mockDatabase, 8);
801 [self waitForCKModifications];
802
803 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
804 }
805
806 - (void)testRecoverFromBrokenSignatureOnTLKShareDuetoSignatureKeyChange {
807 // If a peer changes its signature key, CKKS shouldn't necessarily enter 'error': it should enter 'waitfortlk'.
808 // The peer should then send us another TLKShare
809 // This recovers from the remote peer losing its Octagon keys and making new ones
810
811 // For this test, only have one peer
812 self.currentPeers = [NSMutableSet setWithObject:self.remotePeer1];
813
814 // Test starts with nothing in database, but one in our fake CloudKit.
815 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
816 // Test also starts with the TLK shared to all trusted peers from remotePeer1
817 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
818
819 // BUT, remotePeer1 has rolled its signing key
820 self.remotePeer1.signingKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
821
822 [self startCKKSSubsystem];
823
824 OCMVerifyAllWithDelay(self.mockDatabase, 8);
825 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:10*NSEC_PER_SEC], "Key state should become waitfortlk");
826
827 // Remote peer discovers its error and sends a new TLKShare! CKKS should recover and share itself a TLKShare
828 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
829 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
830 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
831 XCTAssertEqualObjects(share.receiver.peerID, self.currentSelfPeer.peerID, "Receiver peerID on TLKShare should match self peer");
832 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.currentSelfPeer.publicEncryptionKey, "Receiver encryption key on TLKShare should match self peer");
833 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
834 return TRUE;
835 }];
836
837 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
838 [self.keychainView notifyZoneChange:nil];
839
840 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
841
842 OCMVerifyAllWithDelay(self.mockDatabase, 8);
843 [self waitForCKModifications];
844 }
845
846 - (void)testSendNewTLKShareToSelfOnPeerSigningKeyChange {
847 // If a CKKS peer rolls its own keys, but has the TLK, it should write a new TLK share to itself with its new Octagon keys
848 // This recovers from the local peer losing its Octagon keys and making new ones
849
850 // For this test, only have one peer
851 self.currentPeers = [NSMutableSet setWithObject:self.remotePeer1];
852
853 // Test starts with nothing in database, but one in our fake CloudKit.
854 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
855 // Test also starts with the TLK shared to all trusted peers from peer1
856 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
857 // The CKKS subsystem should accept the keys, and share the TLK back to itself
858 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
859 [self startCKKSSubsystem];
860 OCMVerifyAllWithDelay(self.mockDatabase, 8);
861 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:100*NSEC_PER_SEC], "Key state should become ready");
862
863 // Remote peer rolls its signing key, but hasn't updated its TLKShare. We should send it one.
864 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
865 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
866 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
867 XCTAssertEqualObjects(share.receiver.peerID, self.remotePeer1.peerID, "Receiver peerID on TLKShare should match remote peer");
868 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer1.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer");
869 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
870 return TRUE;
871 }];
872
873 self.remotePeer1.signingKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
874 [self.injectedManager sendTrustedPeerSetChangedUpdate];
875
876 OCMVerifyAllWithDelay(self.mockDatabase, 8);
877 [self waitForCKModifications];
878
879 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
880 }
881
882 - (void)testSendNewTLKShareToPeerOnDisappearanceOfPeerKeys {
883 // If a CKKS peer deletes its own octagon keys (BUT WHY), local CKKS should be able to respond
884
885 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
886 // Test also starts with the TLK shared to all trusted peers from peer1
887 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
888 // The CKKS subsystem should accept the keys, and share the TLK back to itself
889 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
890 [self startCKKSSubsystem];
891 OCMVerifyAllWithDelay(self.mockDatabase, 8);
892 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:100*NSEC_PER_SEC], "Key state should become ready");
893
894 // Now, peer 1 updates its keys (to be nil). Local peer should re-send TLKShares to peer2.
895
896 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
897 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
898 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
899 XCTAssertEqualObjects(share.receiver.peerID, self.remotePeer2.peerID, "Receiver peerID on TLKShare should match remote peer");
900 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer2.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer");
901 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
902 return TRUE;
903 }];
904
905 CKKSSOSPeer* brokenRemotePeer1 = [[CKKSSOSPeer alloc] initWithSOSPeerID:self.remotePeer1.peerID encryptionPublicKey:nil signingPublicKey:nil];
906 [self.currentPeers removeObject:self.remotePeer1];
907 [self.currentPeers addObject:brokenRemotePeer1];
908 [self.injectedManager sendTrustedPeerSetChangedUpdate];
909
910 OCMVerifyAllWithDelay(self.mockDatabase, 8);
911 [self waitForCKModifications];
912
913 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
914 }
915
916 - (void)testSendNewTLKShareToPeerOnDisappearanceOfPeerSigningKey {
917 // If a CKKS peer rolls its own keys, but has the TLK, it should write a new TLK share to itself with its new Octagon keys
918 // This recovers from the local peer losing its Octagon keys and making new ones
919
920 // Test starts with nothing in database, but one in our fake CloudKit.
921 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
922 // Test also starts with the TLK shared to all trusted peers from peer1
923 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
924 // The CKKS subsystem should accept the keys, and share the TLK back to itself
925 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
926 [self startCKKSSubsystem];
927 OCMVerifyAllWithDelay(self.mockDatabase, 8);
928 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:100*NSEC_PER_SEC], "Key state should become ready");
929
930 // Now, peer 1 updates its signing key (to be nil). Local peer should re-send TLKShares to peer1 and peer2.
931 // Both should be sent because both peers don't have a signed TLKShare that gives them the TLK
932
933 XCTestExpectation *peer1Share = [self expectationWithDescription:@"share uploaded for peer1"];
934 XCTestExpectation *peer2Share = [self expectationWithDescription:@"share uploaded for peer2"];
935
936 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:2 zoneID:self.keychainZoneID
937 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
938 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
939 if([share.receiver.peerID isEqualToString:self.remotePeer1.peerID]) {
940 [peer1Share fulfill];
941 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer1.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer1");
942 }
943 if([share.receiver.peerID isEqualToString:self.remotePeer2.peerID]) {
944 [peer2Share fulfill];
945 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.remotePeer2.publicEncryptionKey, "Receiver encryption key on TLKShare should match remote peer2");
946 }
947
948 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
949 return TRUE;
950 }];
951
952 CKKSSOSPeer* brokenRemotePeer1 = [[CKKSSOSPeer alloc] initWithSOSPeerID:self.remotePeer1.peerID
953 encryptionPublicKey:self.remotePeer1.publicEncryptionKey
954 signingPublicKey:nil];
955 [self.currentPeers removeObject:self.remotePeer1];
956 [self.currentPeers addObject:brokenRemotePeer1];
957 [self.injectedManager sendTrustedPeerSetChangedUpdate];
958
959 OCMVerifyAllWithDelay(self.mockDatabase, 8);
960 [self waitForCKModifications];
961 [self waitForExpectations:@[peer1Share, peer2Share] timeout:5];
962
963 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
964 }
965
966 - (void)testSendNewTLKShareToSelfOnSelfKeyChanges {
967 // If a CKKS peer rolls its own keys, but has the TLK, it should write a new TLK share to itself with its new Octagon keys
968 // This recovers from the local peer losing its Octagon keys and making new ones
969
970 // Test starts with nothing in database, but one in our fake CloudKit.
971 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
972 // Test also starts with the TLK shared to all trusted peers from peer1
973 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
974 // The CKKS subsystem should accept the keys, and share the TLK back to itself
975 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
976 [self startCKKSSubsystem];
977 OCMVerifyAllWithDelay(self.mockDatabase, 8);
978 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
979
980 // Local peer rolls its encryption key (and loses the old ones)
981 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
982 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
983 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
984 XCTAssertEqualObjects(share.receiver.peerID, self.currentSelfPeer.peerID, "Receiver peerID on TLKShare should match current self");
985 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.currentSelfPeer.publicEncryptionKey, "Receiver encryption key on TLKShare should match current self");
986 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
987 NSError* signatureVerifyError = nil;
988 XCTAssertTrue([share verifySignature:share.signature verifyingPeer:self.currentSelfPeer error:&signatureVerifyError], "New share's signature should verify");
989 XCTAssertNil(signatureVerifyError, "Should be no error verifying signature on new TLKShare");
990 return TRUE;
991 }];
992
993 self.currentSelfPeer.encryptionKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
994 self.pastSelfPeers = [NSMutableSet set];
995 [self.injectedManager sendSelfPeerChangedUpdate];
996
997 OCMVerifyAllWithDelay(self.mockDatabase, 8);
998 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
999
1000 // Now, local peer loses and rolls its signing key (and loses the old one)
1001 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID
1002 checkModifiedRecord:^BOOL(CKRecord* _Nonnull record) {
1003 CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:record];
1004 XCTAssertEqualObjects(share.receiver.peerID, self.currentSelfPeer.peerID, "Receiver peerID on TLKShare should match current self");
1005 XCTAssertEqualObjects(share.receiver.publicEncryptionKey, self.currentSelfPeer.publicEncryptionKey, "Receiver encryption key on TLKShare should match current self");
1006 XCTAssertEqualObjects(share.senderPeerID, self.currentSelfPeer.peerID, "Sender of TLKShare should match current self");
1007 NSError* signatureVerifyError = nil;
1008 XCTAssertTrue([share verifySignature:share.signature verifyingPeer:self.currentSelfPeer error:&signatureVerifyError], "New share's signature should verify");
1009 XCTAssertNil(signatureVerifyError, "Should be no error verifying signature on new TLKShare");
1010 return TRUE;
1011 }];
1012
1013 self.currentSelfPeer.signingKey = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
1014 self.pastSelfPeers = [NSMutableSet set];
1015 [self.injectedManager sendSelfPeerChangedUpdate];
1016
1017 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1018 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
1019 }
1020
1021 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToRecentTLKShare {
1022 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1023
1024 // CKKS shouldn't reset this zone, due to a recent TLK Share from a trusted peer (indicating the presence of TLKs)
1025 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 to:self.remotePeer1 zoneID:self.keychainZoneID];
1026
1027 NSDateComponents* offset = [[NSDateComponents alloc] init];
1028 [offset setDay:-5];
1029 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1030 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1031 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1032 record.creationDate = updateTime;
1033 record.modificationDate = updateTime;
1034 }
1035 }
1036
1037 self.keychainZone.flag = true;
1038 [self startCKKSSubsystem];
1039
1040 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1041
1042 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1043 }
1044
1045 - (void)testDoNotResetCloudKitZoneFromWaitForTLKDueToVeryRecentUntrustedTLKShare {
1046 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1047
1048 // CKKS shouldn't reset this zone, due to a very recent (but untrusted) TLK Share. You can hit this getting a circle reset; the device with the TLKs will have a CFU.
1049 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1050 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1051 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
1052 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1053
1054 NSDateComponents* offset = [[NSDateComponents alloc] init];
1055 [offset setDay:-2];
1056 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1057 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1058 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1059 record.creationDate = updateTime;
1060 record.modificationDate = updateTime;
1061 }
1062 }
1063
1064 self.keychainZone.flag = true;
1065 [self startCKKSSubsystem];
1066
1067 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], @"Key state should become 'waitfortlk'");
1068 XCTAssertTrue(self.keychainZone.flag, "Zone flag should not have been reset to false");
1069
1070 // And ensure it doesn't go on to 'reset'
1071 XCTAssertNotEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:100*NSEC_PER_MSEC], @"Key state should not become 'resetzone'");
1072 }
1073
1074 - (void)testResetCloudKitZoneFromWaitForTLKDueToUntustedTLKShareNotRecentEnough {
1075 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1076
1077 // CKKS shouldn't reset this zone, due to a recent TLK Share (indicating the presence of TLKs)
1078 CKKSSOSSelfPeer* untrustedPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"untrusted-peer"
1079 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
1080 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
1081 [self putTLKShareInCloudKit:self.keychainZoneKeys.tlk from:untrustedPeer to:untrustedPeer zoneID:self.keychainZoneID];
1082
1083 NSDateComponents* offset = [[NSDateComponents alloc] init];
1084 [offset setDay:-5];
1085 NSDate* updateTime = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:[NSDate date] options:0];
1086 for(CKRecord* record in self.keychainZone.currentDatabase.allValues) {
1087 if([record.recordType isEqualToString:SecCKRecordDeviceStateType] || [record.recordType isEqualToString:SecCKRecordTLKShareType]) {
1088 record.creationDate = updateTime;
1089 record.modificationDate = updateTime;
1090 }
1091 }
1092
1093 self.silentZoneDeletesAllowed = true;
1094 self.keychainZone.flag = true;
1095 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:3 zoneID:self.keychainZoneID];
1096 [self startCKKSSubsystem];
1097 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateResettingZone] wait:8*NSEC_PER_SEC], @"Key state should become 'resetzone'");
1098
1099 // Then we should reset.
1100 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1101 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
1102
1103 // And the zone should have been cleared and re-made
1104 XCTAssertFalse(self.keychainZone.flag, "Zone flag should have been reset to false");
1105 }
1106
1107 - (void)testNoSelfEncryptionKeys {
1108 // If you lose your local encryption keys, CKKS should do something reasonable
1109
1110 // Test also starts with the TLK shared to all trusted peers from peer1
1111 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
1112 [self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
1113 [self saveTLKSharesInLocalDatabase:self.keychainZoneID];
1114
1115 // But, we lost our local keys :(
1116 id<CKKSSelfPeer> oldSelfPeer = self.currentSelfPeer;
1117
1118 self.currentSelfPeer = nil;
1119 self.currentSelfPeerError = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecParam description:@"injected test failure"];
1120
1121 // CKKS subsystem should realize that it can't read the shares it has, and enter waitfortlk
1122 [self startCKKSSubsystem];
1123 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:10*NSEC_PER_SEC], "Key state should become 'waitfortlk'");
1124
1125 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1126 [self waitForCKModifications];
1127
1128 // Fetching status should be quick
1129 XCTestExpectation* callbackOccurs = [self expectationWithDescription:@"callback-occurs"];
1130 [self.ckksControl rpcStatus:@"keychain" reply:^(NSArray<NSDictionary*>* result, NSError* error) {
1131 XCTAssertNil(error, "should be no error fetching status for keychain");
1132 [callbackOccurs fulfill];
1133 }];
1134 [self waitForExpectations:@[callbackOccurs] timeout:1.0];
1135
1136 // But, if by some miracle those keys come back, CKKS should be able to recover
1137 // It'll also upload itself a TLK share
1138 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
1139
1140 self.currentSelfPeer = oldSelfPeer;
1141 self.currentSelfPeerError = nil;
1142
1143 [self.injectedManager sendSelfPeerChangedUpdate];
1144 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:100*NSEC_PER_SEC], "Key state should become 'ready''");
1145
1146 OCMVerifyAllWithDelay(self.mockDatabase, 8);
1147 [self waitForCKModifications];
1148 }
1149
1150 @end
1151
1152 #endif // OCTAGON