]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSServerValidationRecoveryTests.m
Security-58286.70.7.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSServerValidationRecoveryTests.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/CKKSCurrentKeyPointer.h"
34 #import "keychain/ckks/CKKSKey.h"
35 #import "keychain/ckks/CKKSOutgoingQueueEntry.h"
36 #import "keychain/ckks/CKKSIncomingQueueEntry.h"
37
38 #import "keychain/ckks/tests/MockCloudKit.h"
39 #import "keychain/ckks/tests/CKKSTests.h"
40
41 @interface CloudKitKeychainSyncingServerValidationRecoveryTests : CloudKitKeychainSyncingTestsBase
42 @end
43
44 @implementation CloudKitKeychainSyncingServerValidationRecoveryTests
45
46 /* Tests for CKKSServerUnexpectedSyncKeyInChain */
47
48 - (void)testRecoverFromWrongClassACurrentKeyPointersOnStartup {
49 // The current key pointers in cloudkit should always point directly under the top TLK.
50
51 // Test starts with a broken key hierarchy in our fake CloudKit, and the TLK already arrived.
52 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
53 [self saveTLKMaterialToKeychain:self.keychainZoneID];
54
55 CKReference* oldClassAKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
56 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
57 [self saveTLKMaterialToKeychain:self.keychainZoneID];
58 CKReference* newClassAKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
59
60 // Break the reference
61 self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = oldClassAKey;
62 self.keychainZoneKeys.currentClassAPointer.currentKeyUUID = oldClassAKey.recordID.recordName;
63
64 // CKKS should then fix the pointers and give itself a TLK share, but not update any keys
65 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 1 tlkShareRecords: 1 zoneID:self.keychainZoneID];
66
67 // And then upload the record as normal
68 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
69 checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
70 [self addGenericPassword:@"asdf"
71 account:@"account-class-A"
72 viewHint:nil
73 access:(id)kSecAttrAccessibleWhenUnlocked
74 expecting:errSecSuccess
75 message:@"Adding class A item"];
76
77 // Spin up CKKS subsystem.
78 [self startCKKSSubsystem];
79
80 OCMVerifyAllWithDelay(self.mockDatabase, 8);
81
82 [self waitForCKModifications];
83 XCTAssertEqualObjects(newClassAKey, self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class A key reference");
84 }
85
86 - (void)testRecoverFromWrongClassCCurrentKeyPointersOnStartup {
87 // The current key pointers in cloudkit should always point directly under the top TLK.
88
89 // Test starts with a broken key hierarchy in our fake CloudKit, and the TLK already arrived.
90 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
91 [self saveTLKMaterialToKeychain:self.keychainZoneID];
92
93 CKReference* oldClassCKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
94 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
95 [self saveTLKMaterialToKeychain:self.keychainZoneID];
96 CKReference* newClassCKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
97
98 // Break the reference
99 self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = oldClassCKey;
100 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = oldClassCKey.recordID.recordName;
101
102 // CKKS should then fix the pointers and its TLK shares, but not update any keys
103 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:1 tlkShareRecords:1 zoneID:self.keychainZoneID];
104
105 // And then upload the record as normal
106 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
107 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
108 [self addGenericPassword: @"data" account: @"account-delete-me"];
109
110 // Spin up CKKS subsystem.
111 [self startCKKSSubsystem];
112
113 OCMVerifyAllWithDelay(self.mockDatabase, 8);
114
115 [self waitForCKModifications];
116 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");
117 }
118
119 - (void)testRecoverFromWrongClassCCurrentKeyPointersOnNotification {
120 // The current key pointers in cloudkit should always point directly under the top TLK.
121
122 // Test starts with a good key hierarchy in our fake CloudKit, and the TLK already arrived.
123 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
124 [self saveTLKMaterialToKeychain:self.keychainZoneID];
125 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
126
127 CKReference* oldClassCKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
128
129 // Spin up CKKS subsystem.
130 [self startCKKSSubsystem];
131
132 // Uploading works
133 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
134 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
135 [self addGenericPassword: @"data" account: @"account-delete-me"];
136 OCMVerifyAllWithDelay(self.mockDatabase, 8);
137
138 // Break the key hierarchy
139 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
140 [self saveTLKMaterialToKeychain:self.keychainZoneID];
141
142 CKReference* newClassCKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
143 self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = oldClassCKey;
144 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = oldClassCKey.recordID.recordName;
145
146 [self.keychainView notifyZoneChange:nil];
147
148 // CKKS should then fix the pointers and give itself a new TLK share record, but not update any keys
149 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:1 tlkShareRecords:1 zoneID:self.keychainZoneID];
150 [self.keychainView notifyZoneChange:nil];
151 OCMVerifyAllWithDelay(self.mockDatabase, 8);
152
153 // And then upload the item as usual
154 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
155 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
156 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
157 OCMVerifyAllWithDelay(self.mockDatabase, 8);
158
159 [self waitForCKModifications];
160 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");
161 }
162
163 - (void)testRecoverFromWrongClassCCurrentKeyPointersOnNotificationFixRejected {
164 // The current key pointers in cloudkit should always point directly under the top TLK.
165
166 // Test starts with a good key hierarchy in our fake CloudKit, and the TLK already arrived.
167 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
168 [self saveTLKMaterialToKeychain:self.keychainZoneID];
169 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
170
171 CKRecordID* classCPointerRecordID = self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID;
172
173 CKReference* oldClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
174
175 // Spin up CKKS subsystem.
176 [self startCKKSSubsystem];
177
178 // Uploading works
179 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
180 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
181 [self addGenericPassword: @"data" account: @"account-delete-me"];
182 OCMVerifyAllWithDelay(self.mockDatabase, 8);
183
184 // Break the key hierarchy
185 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
186 [self saveTLKMaterialToKeychain:self.keychainZoneID];
187
188 CKReference* newClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
189 CKRecord* classCPointer = self.keychainZone.currentDatabase[classCPointerRecordID];
190
191 CKRecord* brokenClassCPointer = [classCPointer copy];
192 brokenClassCPointer[SecCKRecordParentKeyRefKey] = oldClassCKey;
193 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = oldClassCKey.recordID.recordName;
194 [self.keychainZone addCKRecordToZone:brokenClassCPointer];
195
196 self.silentFetchesAllowed = false;
197 [self expectCKFetchAndRunBeforeFinished: ^{
198 // Directly after CKKS fetches, we should fix up the pointers to be right again
199 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = newClassCKey.recordID.recordName;
200 [self.keychainZone addToZone: classCPointer];
201 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");
202 self.silentFetchesAllowed = true;
203 }];
204
205 // CKKS should try to fix the pointers, but be rejected (since someone else has already fixed them)
206 // It should not try to modify the pointers again, but it should give itself the new TLK
207 [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
208 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
209 [self.keychainView notifyZoneChange:nil];
210 OCMVerifyAllWithDelay(self.mockDatabase, 8);
211
212 // And then use the 'new' key as it should
213 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
214 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
215 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
216 OCMVerifyAllWithDelay(self.mockDatabase, 8);
217
218 [self waitForCKModifications];
219 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");
220 }
221
222 - (void)testRecoverFromWrongClassCCurrentKeyPointersOnRecordWrite {
223 // The current key pointers in cloudkit should always point directly under the top TLK.
224
225 // Test starts with a good but rolled key hierarchy in our fake CloudKit, and the TLK already arrived.
226 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
227 [self saveTLKMaterialToKeychain:self.keychainZoneID];
228
229 CKRecordID* classCPointerRecordID = self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID;
230 CKReference* oldClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
231
232 [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
233 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
234 [self saveTLKMaterialToKeychain:self.keychainZoneID];
235
236 // Spin up CKKS subsystem.
237 [self startCKKSSubsystem];
238
239 // Uploading works
240 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
241 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
242 [self addGenericPassword: @"data" account: @"account-delete-me"];
243 OCMVerifyAllWithDelay(self.mockDatabase, 8);
244 [self waitForCKModifications];
245
246 // Now, break the class C pointer, but don't tell CKKS
247 CKReference* newClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
248 CKRecord* classCPointer = self.keychainZone.currentDatabase[classCPointerRecordID];
249
250 CKRecord* brokenClassCPointer = [classCPointer copy];
251 brokenClassCPointer[SecCKRecordParentKeyRefKey] = oldClassCKey;
252 self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = oldClassCKey.recordID.recordName;
253 [self.keychainZone addCKRecordToZone:brokenClassCPointer];
254
255 // CKKS should receive a key hierarchy error, since it's wrong in CloudKit
256 // It should then fix the pointers and retry the upload
257 [self expectCKReceiveSyncKeyHierarchyError:self.keychainZoneID];
258 [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 1 tlkShareRecords: 0 zoneID:self.keychainZoneID];
259
260 // And then use the 'new' key as it should
261 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
262 checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
263 [self addGenericPassword: @"data" account: @"account-delete-me-2"];
264 OCMVerifyAllWithDelay(self.mockDatabase, 8);
265
266 [self waitForCKModifications];
267 XCTAssertEqualObjects(newClassCKey, self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey], "CKKS should have fixed up the broken class C key reference");
268 }
269
270 @end
271
272 #endif // OCTAGON