2 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
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"
39 #import "keychain/ckks/tests/MockCloudKit.h"
40 #import "keychain/ckks/tests/CKKSTests.h"
41 #import "keychain/ckks/tests/CKKSTests+API.h"
44 @interface CloudKitKeychainSyncingFixupTests : CloudKitKeychainSyncingTestsBase
47 @implementation CloudKitKeychainSyncingFixupTests
49 - (void)testNoFixupOnInitialStart {
50 id mockFixups = OCMClassMock([CKKSFixups class]);
51 OCMReject([[[mockFixups stub] ignoringNonObjectArgs] fixup:0 for:[OCMArg any]]);
53 [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:1 zoneID:self.keychainZoneID];
54 [self startCKKSSubsystem];
56 [self.keychainView waitForKeyHierarchyReadiness];
57 [self waitForCKModifications];
58 OCMVerifyAllWithDelay(self.mockDatabase, 8);
61 [mockFixups stopMocking];
64 - (void)testImmediateRestartUsesLatestFixup {
65 id mockFixups = OCMClassMock([CKKSFixups class]);
66 OCMExpect([mockFixups fixup:CKKSCurrentFixupNumber for:[OCMArg any]]);
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];
72 [self.keychainView waitForKeyHierarchyReadiness];
73 [self waitForCKModifications];
74 OCMVerifyAllWithDelay(self.mockDatabase, 8);
76 // Tear down the CKKS object
77 [self.keychainView cancelAllOperations];
79 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
80 [self.keychainView waitForKeyHierarchyReadiness];
81 OCMVerifyAllWithDelay(self.mockDatabase, 8);
84 [mockFixups stopMocking];
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.
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];
95 [self.keychainView waitForKeyHierarchyReadiness];
96 [self waitForCKModifications];
97 OCMVerifyAllWithDelay(self.mockDatabase, 8);
99 // Add some current item pointers. They don't necessarily need to point to anything...
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");
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");
121 [self.keychainView notifyZoneChange:nil];
122 [self.keychainView waitForFetchAndIncomingQueueProcessing];
124 // Tear down the CKKS object
125 [self.keychainView cancelAllOperations];
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];
132 XCTAssertNil(error, "no error pulling ckse from database");
133 XCTAssertNotNil(ckse, "received a ckse");
135 ckse.lastFixup = CKKSFixupNever;
136 [ckse saveToDatabase: &error];
137 XCTAssertNil(error, "no error saving to database");
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");
152 self.silentFetchesAllowed = false;
153 [self expectCKFetchByRecordID];
154 if(SecCKKSShareTLKs()) {
155 [self expectCKFetchByQuery]; // and one for the TLKShare fixup
158 // Change one of the CIPs while CKKS is offline
159 cip2.currentItemUUID = @"changed-by-cloudkit";
160 [self.keychainZone addToZone: [cip2 CKRecordWithZoneID:self.keychainZoneID]];
162 // Bring CKKS back up
163 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
164 [self.keychainView waitForKeyHierarchyReadiness];
166 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
167 OCMVerifyAllWithDelay(self.mockDatabase, 8);
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");
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");
181 XCTestExpectation* foundCIP2 = [self expectationWithDescription: @"found CIP2"];
182 for(CKKSCurrentItemPointer* loaded in allCIPs) {
183 if([loaded.identifier isEqualToString: cip2.identifier]) {
185 XCTAssertEqualObjects(loaded.currentItemUUID, @"changed-by-cloudkit", "Fixup should have fixed UUID to new value, not pre-shutdown value");
187 XCTAssertNotEqualObjects(loaded.identifier, @"garbage", "Garbage CIP shouldn't exist anymore");
190 [self waitForExpectations:@[foundCIP2] timeout:0.1];
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];
201 XCTAssertNil(error, "no error pulling ckse from database");
202 XCTAssertNotNil(ckse, "received a ckse");
204 ckse.lastFixup = newFixup;
205 [ckse saveToDatabase: &error];
206 XCTAssertNil(error, "no error saving to database");
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.
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];
221 [self.keychainView waitForKeyHierarchyReadiness];
222 [self waitForCKModifications];
223 OCMVerifyAllWithDelay(self.mockDatabase, 8);
225 // Tear down the CKKS object
226 [self.keychainView cancelAllOperations];
227 [self setFixupNumber:CKKSFixupRefetchCurrentItemPointers ckks:self.keychainView];
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;
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]]];
237 CKKSTLKShare* share = [CKKSTLKShare share:self.keychainZoneKeys.tlk
239 to:self.currentSelfPeer
243 XCTAssertNil(error, "Should have been no error sharing a CKKSKey");
244 XCTAssertNotNil(share, "Should be able to create a share");
246 CKRecord* shareCKRecord = [share CKRecordWithZoneID: self.keychainZoneID];
247 XCTAssertNotNil(shareCKRecord, "Should have been able to create a CKRecord");
248 [self.keychainZone addToZone:shareCKRecord];
251 self.silentFetchesAllowed = false;
252 [self expectCKFetchByQuery];
254 // We want to ensure that the key hierarchy state machine doesn't progress past fixup until we let this go
255 [self holdCloudKitFetches];
257 self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
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");
263 OCMVerifyAllWithDelay(self.mockDatabase, 8);
265 [self.keychainView.lastFixupOperation waitUntilFinished];
266 XCTAssertNil(self.keychainView.lastFixupOperation.error, "Shouldn't have been any error performing fixup");
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");
277 SecCKKSSetShareTLKs(false);