]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSAPSHandlingTests.m
Security-59306.101.1.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSAPSHandlingTests.m
1 /*
2 * Copyright (c) 2018 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/OctagonAPSReceiver.h"
37 #import "keychain/ckks/CKKSKey.h"
38 #import "keychain/ckks/CKKSPeer.h"
39 #import "keychain/ckks/CKKSTLKShareRecord.h"
40 #import "keychain/ckks/CKKSViewManager.h"
41 #import "keychain/ckks/CloudKitCategories.h"
42
43 #import "keychain/ckks/tests/MockCloudKit.h"
44 #import "keychain/ckks/tests/CKKSTests.h"
45 #import "keychain/ot/OTDefines.h"
46
47 #import "keychain/ckks/tests/OctagonAPSReceiverTests.h"
48
49 @interface CKKSAPSHandlingTests : CloudKitKeychainSyncingTestsBase
50 @property CKRecordZoneID* manateeZoneID;
51 @property CKKSKeychainView* manateeView;
52 @property FakeCKZone* manateeZone;
53 @property (readonly) ZoneKeys* manateeZoneKeys;
54 @end
55
56
57 @implementation CKKSAPSHandlingTests
58 // We really just want two views here
59 - (NSSet*)managedViewList {
60 return [NSSet setWithObjects:@"keychain", @"Manatee", nil];
61 }
62
63 - (void)setUp {
64 [super setUp];
65
66 // Wait for the ViewManager to be brought up
67 XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:20*NSEC_PER_SEC], "No timeout waiting for SecCKKSInitialize");
68
69 self.manateeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Manatee" ownerName:CKCurrentUserDefaultName];
70 [self.ckksZones addObject:self.manateeZoneID];
71 self.manateeZone = [[FakeCKZone alloc] initZone: self.manateeZoneID];
72 self.zones[self.manateeZoneID] = self.manateeZone;
73 self.manateeView = [[CKKSViewManager manager] findOrCreateView:@"Manatee"];
74 XCTAssertNotNil(self.manateeView, "CKKSViewManager created the Manatee view");
75 [self.ckksViews addObject:self.manateeView];
76 }
77
78 - (void)tearDown {
79 // If the test didn't already do this, allow each zone to spin up
80 self.accountStatus = CKAccountStatusNoAccount;
81 [self startCKKSSubsystem];
82
83 [self.manateeView halt];
84 [self.manateeView waitUntilAllOperationsAreFinished];
85 self.manateeView = nil;
86
87 [super tearDown];
88 }
89
90 + (APSIncomingMessage*)messageWithTracingEnabledForZoneID:(CKRecordZoneID*)zoneID {
91 APSIncomingMessage* apsMessage = [OctagonAPSReceiverTests messageForZoneID:zoneID];
92 NSUUID* nsuuid = [NSUUID UUID];
93 uuid_t uuid = {0};
94 [nsuuid getUUIDBytes:(unsigned char*)&uuid];
95 NSData* uuidData = [NSData dataWithBytes:&uuid length:sizeof(uuid)];
96
97 apsMessage.tracingUUID = uuidData;
98 apsMessage.tracingEnabled = YES;
99
100 return apsMessage;
101 }
102
103 - (void)testSendPushMetricUponRequest {
104 for(CKRecordZoneID* zoneID in self.ckksZones) {
105 [self putFakeKeyHierarchyInCloudKit:zoneID];
106 [self saveTLKMaterialToKeychain:zoneID];
107 [self expectCKKSTLKSelfShareUpload:zoneID];
108 }
109
110 [self startCKKSSubsystem];
111
112 for(CKKSKeychainView* view in self.ckksViews) {
113 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
114 }
115
116 OCMVerifyAllWithDelay(self.mockDatabase, 20);
117
118 CKRecord* keychainRecord = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"keychain-view"];
119 [self.keychainZone addToZone: keychainRecord];
120
121 // Manatee gets one too!
122 CKRecord* manateeRecord = [self createFakeRecord:self.manateeZoneID recordName:@"7B598D31-F9C5-481E-98AC-000000000000" withAccount:@"manatee-view"];
123 [self.manateeZone addToZone:manateeRecord];
124
125 // Trigger a notification just for keychain zone. Both keychain and Manatee should process their incoming queues, and receive their items.
126 CKKSResultOperation* keychainProcessOp = [self.keychainView resultsOfNextProcessIncomingQueueOperation];
127 XCTAssertNotNil(keychainProcessOp, "Should have gotten a promise operation from Keychain");
128 CKKSResultOperation* manateeProcessOp = [self.manateeView resultsOfNextProcessIncomingQueueOperation];
129 XCTAssertNotNil(manateeProcessOp, "Should have gotten a promise operation from Manatee");
130
131 // But if something goes wrong, don't block the whole test. Only way to do that is to make more operations, since there's no guarantee that the process ops above will ever be added
132 // to a queue (and thus become 'finished')
133 CKKSResultOperation* keychainProcessTimeoutOp = [CKKSResultOperation named:@"keychain-timeout" withBlock:^{}];
134 [keychainProcessTimeoutOp timeout:20*NSEC_PER_SEC];
135 [keychainProcessTimeoutOp addSuccessDependency:keychainProcessOp];
136 [self.operationQueue addOperation:keychainProcessTimeoutOp];
137
138 CKKSResultOperation* manateeProcessTimeoutOp = [CKKSResultOperation named:@"manatee-timeout" withBlock:^{}];
139 [manateeProcessTimeoutOp timeout:20*NSEC_PER_SEC];
140 [manateeProcessTimeoutOp addSuccessDependency:manateeProcessOp];
141 [self.operationQueue addOperation:manateeProcessTimeoutOp];
142
143 APSIncomingMessage* apsMessage = [CKKSAPSHandlingTests messageWithTracingEnabledForZoneID:self.keychainZoneID];
144
145 // Inject a message at the APS layer
146 // Because we can only make APS receivers once iCloud tells us the push environment after sign-in, we can't use our normal injection strategy, and fell back on global state.
147 OctagonAPSReceiver* apsReceiver = [OctagonAPSReceiver receiverForEnvironment:self.apsEnvironment
148 namedDelegatePort:SecCKKSAPSNamedPort
149 apsConnectionClass:[FakeAPSConnection class]];
150 XCTAssertNotNil(apsReceiver, "Should have gotten an APS receiver");
151
152 // Also, CKKS should handle this in one single fetch
153 self.silentFetchesAllowed = false;
154 [self expectCKFetch];
155
156 // Expect two metric pushes, one from receiving the push and one from after we finish the fetch
157 // AFAICT there's no way to introspect a metric object to ensure we did it right
158 OCMExpect([self.mockContainerExpectations submitEventMetric:[OCMArg any]]);
159 OCMExpect([self.mockContainerExpectations submitEventMetric:[OCMArg any]]);
160
161 // Launch!
162 [apsReceiver connection:nil didReceiveIncomingMessage:apsMessage];
163
164 OCMVerifyAllWithDelay(self.mockContainerExpectations, 16);
165
166 // Now, wait for both views to run their processing
167 [keychainProcessTimeoutOp waitUntilFinished];
168 XCTAssertNil(keychainProcessTimeoutOp.error, "Shouldn't have been any error processing incoming queue (keychain)");
169
170 [manateeProcessTimeoutOp waitUntilFinished];
171 XCTAssertNil(manateeProcessTimeoutOp.error, "Shouldn't have been any error processing incoming queue (manatee)");
172
173 [self findGenericPassword:@"keychain-view" expecting:errSecSuccess];
174 [self findGenericPassword:@"manatee-view" expecting:errSecSuccess];
175 }
176
177 - (void)testDoNotSendPushMetricWithoutRequest {
178 for(CKRecordZoneID* zoneID in self.ckksZones) {
179 [self putFakeKeyHierarchyInCloudKit:zoneID];
180 [self saveTLKMaterialToKeychain:zoneID];
181 [self expectCKKSTLKSelfShareUpload:zoneID];
182 }
183
184 [self startCKKSSubsystem];
185
186 for(CKKSKeychainView* view in self.ckksViews) {
187 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
188 }
189
190 OCMVerifyAllWithDelay(self.mockDatabase, 20);
191
192 CKRecord* keychainRecord = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"keychain-view"];
193 [self.keychainZone addToZone: keychainRecord];
194
195 // Trigger a notification just for keychain zone. Both keychain and Manatee should process their incoming queues, and receive their items.
196 CKKSResultOperation* keychainProcessOp = [self.keychainView resultsOfNextProcessIncomingQueueOperation];
197 XCTAssertNotNil(keychainProcessOp, "Should have gotten a promise operation from Keychain");
198
199 // But if something goes wrong, don't block the whole test. Only way to do that is to make more operations, since there's no guarantee that the process ops above will ever be added
200 // to a queue (and thus become 'finished')
201 CKKSResultOperation* keychainProcessTimeoutOp = [CKKSResultOperation named:@"keychain-timeout" withBlock:^{}];
202 [keychainProcessTimeoutOp timeout:20*NSEC_PER_SEC];
203 [keychainProcessTimeoutOp addSuccessDependency:keychainProcessOp];
204 [self.operationQueue addOperation:keychainProcessTimeoutOp];
205
206 // Create a push that matchs all push tracing patterns except for the enabled flag
207 APSIncomingMessage* apsMessage = [CKKSAPSHandlingTests messageWithTracingEnabledForZoneID:self.keychainZoneID];
208 apsMessage.tracingEnabled = NO;
209
210 // Inject a message at the APS layer
211 // Because we can only make APS receivers once iCloud tells us the push environment after sign-in, we can't use our normal injection strategy, and fell back on global state.
212 OctagonAPSReceiver* apsReceiver = [OctagonAPSReceiver receiverForEnvironment:self.apsEnvironment
213 namedDelegatePort:SecCKKSAPSNamedPort
214 apsConnectionClass:[FakeAPSConnection class]];
215 XCTAssertNotNil(apsReceiver, "Should have gotten an APS receiver");
216
217 // Also, CKKS should handle this in one single fetch
218 self.silentFetchesAllowed = false;
219 [self expectCKFetch];
220
221 // Any metric push is verboten
222 OCMReject([self.mockContainerExpectations submitEventMetric:[OCMArg any]]);
223
224 // Launch!
225 [apsReceiver connection:nil didReceiveIncomingMessage:apsMessage];
226 OCMVerifyAllWithDelay(self.mockContainerExpectations, 16);
227
228 // Now, wait for both views to run their processing
229 [keychainProcessTimeoutOp waitUntilFinished];
230 XCTAssertNil(keychainProcessTimeoutOp.error, "Shouldn't have been any error processing incoming queue (keychain)");
231
232 [self findGenericPassword:@"keychain-view" expecting:errSecSuccess];
233 }
234
235 - (void)testSendPushMetricEvenIfPushArrivesEarly {
236 CKRecordZoneID* pushTestZone = [[CKRecordZoneID alloc] initWithZoneName:@"PushTestZone" ownerName:CKCurrentUserDefaultName];
237 [self.ckksZones addObject:pushTestZone];
238 self.zones[pushTestZone] = [[FakeCKZone alloc] initZone: pushTestZone];
239
240 NSMutableSet* viewList = [self.mockSOSAdapter.selfPeer.viewList mutableCopy];
241 [viewList addObject:@"PushTestZone"];
242 CKKSSOSSelfPeer* newSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:self.mockSOSAdapter.selfPeer.peerID
243 encryptionKey:self.mockSOSAdapter.selfPeer.encryptionKey
244 signingKey:self.mockSOSAdapter.selfPeer.signingKey
245 viewList:viewList];
246 self.mockSOSAdapter.selfPeer = newSelfPeer;
247
248 for(CKRecordZoneID* zoneID in self.ckksZones) {
249 [self putFakeKeyHierarchyInCloudKit:zoneID];
250 [self saveTLKMaterialToKeychain:zoneID];
251 [self expectCKKSTLKSelfShareUpload:zoneID];
252 }
253
254 // The push wakes securityd, so it happens before pushTestZone is created locally
255 // Send 2, just to test our infrastructure
256 APSIncomingMessage* apsMessage = [CKKSAPSHandlingTests messageWithTracingEnabledForZoneID:pushTestZone];
257 APSIncomingMessage* apsMessage2 = [CKKSAPSHandlingTests messageWithTracingEnabledForZoneID:pushTestZone];
258
259 // Inject a message at the APS layer
260 // Because we can only make APS receivers once iCloud tells us the push environment after sign-in, we can't use our normal injection strategy, and fell back on global state.
261 OctagonAPSReceiver* apsReceiver = [OctagonAPSReceiver receiverForEnvironment:self.apsEnvironment
262 namedDelegatePort:SecCKKSAPSNamedPort
263 apsConnectionClass:[FakeAPSConnection class]];
264 XCTAssertNotNil(apsReceiver, "Should have gotten an APS receiver");
265
266 [apsReceiver connection:nil didReceiveIncomingMessage:apsMessage];
267 [apsReceiver connection:nil didReceiveIncomingMessage:apsMessage2];
268
269 // Expect four metric pushes, two per push: one from receiving the push and one from after we finish the fetch
270 // AFAICT there's no way to introspect a metric object to ensure we did it right
271 OCMExpect([self.mockContainerExpectations submitEventMetric:[OCMArg any]]);
272 OCMExpect([self.mockContainerExpectations submitEventMetric:[OCMArg any]]);
273 OCMExpect([self.mockContainerExpectations submitEventMetric:[OCMArg any]]);
274 OCMExpect([self.mockContainerExpectations submitEventMetric:[OCMArg any]]);
275
276 // Launch!
277 CKKSKeychainView* pushTestView = [self.injectedManager findOrCreateView:pushTestZone.zoneName];
278 [self.ckksViews addObject:pushTestView];
279
280 [self startCKKSSubsystem];
281
282 for(CKKSKeychainView* view in self.ckksViews) {
283 XCTAssertEqual(0, [view.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], "Key state should enter 'ready' for view %@", view);
284 }
285 OCMVerifyAllWithDelay(self.mockDatabase, 20);
286 }
287
288 - (void)testDropStalePushes {
289 CKRecordZoneID* pushTestZone = [[CKRecordZoneID alloc] initWithZoneName:@"PushTestZone" ownerName:CKCurrentUserDefaultName];
290
291 APSIncomingMessage* apsMessage = [CKKSAPSHandlingTests messageWithTracingEnabledForZoneID:pushTestZone];
292
293 // Don't use the global map here, because we need to ensure we create a new object (to use the stalePushTimeout we injected above)
294 OctagonAPSReceiver* apsReceiver = OCMPartialMock([[OctagonAPSReceiver alloc] initWithEnvironmentName:self.apsEnvironment
295 namedDelegatePort:SecCKKSAPSNamedPort
296 apsConnectionClass:[FakeAPSConnection class]
297 stalePushTimeout:4 * NSEC_PER_SEC]);
298 XCTAssertNotNil(apsReceiver, "Should have gotten an APS receiver");
299
300 OCMExpect([apsReceiver reportDroppedPushes:[OCMArg any]]);
301 [apsReceiver connection:nil didReceiveIncomingMessage:apsMessage];
302
303 XCTAssertEqual(apsReceiver.haveStalePushes, YES, "should have stale pushes");
304 OCMVerifyAllWithDelay((id)apsReceiver, 20);
305 XCTAssertEqual(apsReceiver.haveStalePushes, NO, "should no longer have stale pushes");
306 }
307
308
309 @end
310
311 #endif