]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CloudKitKeychainSyncingFixupTests.m
Security-58286.31.2.tar.gz
[apple/security.git] / keychain / ckks / tests / CloudKitKeychainSyncingFixupTests.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/CKKSFixups.h"
34 #import "keychain/ckks/CKKSZoneStateEntry.h"
35 #import "keychain/ckks/CKKSViewManager.h"
36 #import "keychain/ckks/CKKSCurrentItemPointer.h"
37 #import "keychain/ckks/CKKSIncomingQueueOperation.h"
38
39 #import "keychain/ckks/tests/MockCloudKit.h"
40 #import "keychain/ckks/tests/CKKSTests.h"
41 #import "keychain/ckks/tests/CKKSTests+API.h"
42
43
44 @interface CloudKitKeychainSyncingFixupTests : CloudKitKeychainSyncingTestsBase
45 @end
46
47 @implementation CloudKitKeychainSyncingFixupTests
48
49 - (void)testNoFixupOnInitialStart {
50 id mockFixups = OCMClassMock([CKKSFixups class]);
51 OCMReject([[[mockFixups stub] ignoringNonObjectArgs] fixup:0 for:[OCMArg any]]);
52
53 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:1 zoneID:self.keychainZoneID];
54 [self startCKKSSubsystem];
55
56 [self.keychainView waitForKeyHierarchyReadiness];
57 [self waitForCKModifications];
58 OCMVerifyAllWithDelay(self.mockDatabase, 8);
59
60 [mockFixups verify];
61 [mockFixups stopMocking];
62 }
63
64 - (void)testImmediateRestartUsesLatestFixup {
65 id mockFixups = OCMClassMock([CKKSFixups class]);
66 OCMExpect([mockFixups fixup:CKKSCurrentFixupNumber for:[OCMArg any]]);
67
68 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
69 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:1 zoneID:self.keychainZoneID];
70 [self startCKKSSubsystem];
71
72 [self.keychainView waitForKeyHierarchyReadiness];
73 [self waitForCKModifications];
74 OCMVerifyAllWithDelay(self.mockDatabase, 8);
75
76 // Tear down the CKKS object
77 [self.keychainView cancelAllOperations];
78
79 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
80 [self.keychainView waitForKeyHierarchyReadiness];
81 OCMVerifyAllWithDelay(self.mockDatabase, 8);
82
83 [mockFixups verify];
84 [mockFixups stopMocking];
85 }
86
87 - (void)testFixupRefetchAllCurrentItemPointers {
88 // Due to <rdar://problem/34916549> CKKS: current item pointer CKRecord resurrection,
89 // CKKS needs to refetch all current item pointers if it restarts and hasn't yet.
90
91 // Test starts with no keys in database. We expect some sort of TLK/key hierarchy upload.
92 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
93 [self startCKKSSubsystem];
94
95 [self.keychainView waitForKeyHierarchyReadiness];
96 [self waitForCKModifications];
97 OCMVerifyAllWithDelay(self.mockDatabase, 8);
98
99 // Add some current item pointers. They don't necessarily need to point to anything...
100
101 CKKSCurrentItemPointer* cip = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice"
102 currentItemUUID:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"
103 state:SecCKKSProcessedStateRemote
104 zoneID:self.keychainZoneID
105 encodedCKRecord:nil];
106 [self.keychainZone addToZone: [cip CKRecordWithZoneID:self.keychainZoneID]];
107 CKRecordID* currentPointerRecordID = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice" zoneID:self.keychainZoneID];
108 CKRecord* currentPointerRecord = self.keychainZone.currentDatabase[currentPointerRecordID];
109 XCTAssertNotNil(currentPointerRecord, "Found record in CloudKit at expected UUID");
110
111 CKKSCurrentItemPointer* cip2 = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"com.apple.security.ckks-pcsservice2"
112 currentItemUUID:@"3AB8E78D-75AF-CFEF-F833-FA3E3E90978A"
113 state:SecCKKSProcessedStateRemote
114 zoneID:self.keychainZoneID
115 encodedCKRecord:nil];
116 [self.keychainZone addToZone: [cip2 CKRecordWithZoneID:self.keychainZoneID]];
117 CKRecordID* currentPointerRecordID2 = [[CKRecordID alloc] initWithRecordName: @"com.apple.security.ckks-pcsservice2" zoneID:self.keychainZoneID];
118 CKRecord* currentPointerRecord2 = self.keychainZone.currentDatabase[currentPointerRecordID2];
119 XCTAssertNotNil(currentPointerRecord2, "Found record in CloudKit at expected UUID");
120
121 [self.keychainView notifyZoneChange:nil];
122 [self.keychainView waitForFetchAndIncomingQueueProcessing];
123
124 // Tear down the CKKS object
125 [self.keychainView cancelAllOperations];
126
127 [self.keychainView dispatchSync: ^bool {
128 // Edit the zone state entry to have no fixups
129 NSError* error = nil;
130 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
131
132 XCTAssertNil(error, "no error pulling ckse from database");
133 XCTAssertNotNil(ckse, "received a ckse");
134
135 ckse.lastFixup = CKKSFixupNever;
136 [ckse saveToDatabase: &error];
137 XCTAssertNil(error, "no error saving to database");
138
139 // And add a garbage CIP
140 CKKSCurrentItemPointer* cip3 = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"garbage"
141 currentItemUUID:@"3AB8E78D-75AF-CFEF-F833-FA3E3E90978A"
142 state:SecCKKSProcessedStateLocal
143 zoneID:self.keychainZoneID
144 encodedCKRecord:nil];
145 cip3.storedCKRecord = [cip3 CKRecordWithZoneID:self.keychainZoneID];
146 XCTAssertEqual(cip3.identifier, @"garbage", "Identifier is what we thought it was");
147 [cip3 saveToDatabase:&error];
148 XCTAssertNil(error, "no error saving to database");
149 return true;
150 }];
151
152 self.silentFetchesAllowed = false;
153 [self expectCKFetchByRecordID];
154 if(SecCKKSShareTLKs()) {
155 [self expectCKFetchByQuery]; // and one for the TLKShare fixup
156 }
157
158 // Change one of the CIPs while CKKS is offline
159 cip2.currentItemUUID = @"changed-by-cloudkit";
160 [self.keychainZone addToZone: [cip2 CKRecordWithZoneID:self.keychainZoneID]];
161
162 // Bring CKKS back up
163 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
164 [self.keychainView waitForKeyHierarchyReadiness];
165
166 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
167 OCMVerifyAllWithDelay(self.mockDatabase, 8);
168
169 [self.keychainView dispatchSync: ^bool {
170 // The zone state entry should be up the most recent fixup level
171 NSError* error = nil;
172 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:self.keychainZoneID.zoneName error:&error];
173 XCTAssertNil(error, "no error pulling ckse from database");
174 XCTAssertNotNil(ckse, "received a ckse");
175 XCTAssertEqual(ckse.lastFixup, CKKSCurrentFixupNumber, "CKSE should have the current fixup number stored");
176
177 // The garbage CIP should be gone, and CKKS should have caught up to the CIP change
178 NSArray<CKKSCurrentItemPointer*>* allCIPs = [CKKSCurrentItemPointer allInZone:self.keychainZoneID error:&error];
179 XCTAssertNil(error, "no error loading all CIPs from database");
180
181 XCTestExpectation* foundCIP2 = [self expectationWithDescription: @"found CIP2"];
182 for(CKKSCurrentItemPointer* loaded in allCIPs) {
183 if([loaded.identifier isEqualToString: cip2.identifier]) {
184 [foundCIP2 fulfill];
185 XCTAssertEqualObjects(loaded.currentItemUUID, @"changed-by-cloudkit", "Fixup should have fixed UUID to new value, not pre-shutdown value");
186 }
187 XCTAssertNotEqualObjects(loaded.identifier, @"garbage", "Garbage CIP shouldn't exist anymore");
188 }
189
190 [self waitForExpectations:@[foundCIP2] timeout:0.1];
191 return true;
192 }];
193 }
194
195 - (void)setFixupNumber:(CKKSFixup)newFixup ckks:(CKKSKeychainView*)ckks {
196 [ckks dispatchSync: ^bool {
197 // Edit the zone state entry to have no fixups
198 NSError* error = nil;
199 CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:ckks.zoneID.zoneName error:&error];
200
201 XCTAssertNil(error, "no error pulling ckse from database");
202 XCTAssertNotNil(ckse, "received a ckse");
203
204 ckse.lastFixup = newFixup;
205 [ckse saveToDatabase: &error];
206 XCTAssertNil(error, "no error saving to database");
207 return true;
208 }];
209 }
210
211 - (void)testFixupFetchAllTLKShareRecords {
212 SecCKKSSetShareTLKs(true);
213 // In <rdar://problem/34901306> CKKSTLK: TLKShare CloudKit upload/download on TLK change, trust set addition,
214 // we added the TLKShare CKRecord type. Upgrading devices must fetch all such records when they come online for the first time.
215
216 // Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
217 // Note that this already does TLK sharing, and so technically doesn't need to do the fixup, but we'll fix that later.
218 [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
219 [self startCKKSSubsystem];
220
221 [self.keychainView waitForKeyHierarchyReadiness];
222 [self waitForCKModifications];
223 OCMVerifyAllWithDelay(self.mockDatabase, 8);
224
225 // Tear down the CKKS object
226 [self.keychainView cancelAllOperations];
227 [self setFixupNumber:CKKSFixupRefetchCurrentItemPointers ckks:self.keychainView];
228
229 // Also, create a TLK share record that CKKS should find
230 // Make another share, but from an untrusted peer to some other peer. local shouldn't necessarily care.
231 NSError* error = nil;
232
233 CKKSSOSSelfPeer* remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
234 encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
235 signingKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]];
236
237 CKKSTLKShare* share = [CKKSTLKShare share:self.keychainZoneKeys.tlk
238 as:remotePeer1
239 to:self.currentSelfPeer
240 epoch:-1
241 poisoned:0
242 error:&error];
243 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
244 XCTAssertNotNil(share, "Should be able to create a share");
245
246 CKRecord* shareCKRecord = [share CKRecordWithZoneID: self.keychainZoneID];
247 XCTAssertNotNil(shareCKRecord, "Should have been able to create a CKRecord");
248 [self.keychainZone addToZone:shareCKRecord];
249
250 // Now, restart CKKS
251 self.silentFetchesAllowed = false;
252 [self expectCKFetchByQuery];
253
254 // We want to ensure that the key hierarchy state machine doesn't progress past fixup until we let this go
255 [self holdCloudKitFetches];
256
257 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
258
259 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForFixupOperation] wait:500*NSEC_PER_SEC], "Key state should become waitforfixup");
260 [self releaseCloudKitFetchHold];
261 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:500*NSEC_PER_SEC], "Key state should become ready");
262
263 OCMVerifyAllWithDelay(self.mockDatabase, 8);
264
265 [self.keychainView.lastFixupOperation waitUntilFinished];
266 XCTAssertNil(self.keychainView.lastFixupOperation.error, "Shouldn't have been any error performing fixup");
267
268 // and check that the share made it
269 [self.keychainView dispatchSync:^bool {
270 NSError* blockerror = nil;
271 CKKSTLKShare* localshare = [CKKSTLKShare tryFromDatabaseFromCKRecordID:shareCKRecord.recordID error:&blockerror];
272 XCTAssertNil(blockerror, "Shouldn't error finding new TLKShare record in database");
273 XCTAssertNotNil(localshare, "Should be able to find a new TLKShare record in database");
274 return true;
275 }];
276
277 SecCKKSSetShareTLKs(false);
278 }
279
280 @end
281
282 #endif // OCTAGON