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@
27 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
28 #import "keychain/ckks/CKKSManifest.h"
29 #import "keychain/ckks/CKKSMirrorEntry.h"
30 #import "keychain/ckks/CKKSViewManager.h"
31 #import "keychain/ckks/CKKSCurrentItemPointer.h"
32 #import <Security/SecEntitlements.h>
33 #include <ipc/server_security_helpers.h>
34 #import <SecurityFoundation/SFKey.h>
35 #import <Foundation/Foundation.h>
36 #import <XCTest/XCTest.h>
37 #import <OCMock/OCMock.h>
39 @interface CKKSManifestTests : CloudKitKeychainSyncingTestsBase {
40 BOOL _manifestIsSetup;
41 CKKSEgoManifest* _egoManifest;
48 @implementation CKKSManifestTests
50 - (NSArray*)mirrorItemsForExistingItems
52 NSArray<CKRecord*>* records = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
53 NSMutableArray* items = [[NSMutableArray alloc] init];
54 __weak __typeof(self) weakSelf = self;
55 __block NSError* error = nil;
56 [self.keychainView dispatchSyncWithSQLTransaction:^CKKSDatabaseTransactionResult{
57 for (CKRecord* record in records) {
58 CKKSMirrorEntry* mirrorEntry = [CKKSMirrorEntry tryFromDatabase:record.recordID.recordName zoneID:weakSelf.keychainZoneID error:&error];
59 XCTAssertNil(error, @"error encountered trying to generate CKKSMirrorEntry: %@", error);
60 XCTAssertNotNil(mirrorEntry, @"failed to generate mirror entry");
62 [items addObject:mirrorEntry.item];
66 return CKKSDatabaseTransactionCommit;
74 // Lie to our superclass about how the world is
75 (void)[CKKSManifest shouldSyncManifests]; // initialize.
76 SecCKKSSetSyncManifests(false);
77 SecCKKSSetEnforceManifests(false);
81 // Should be removed when manifests is turned back on
82 SFECKeyPair* keyPair = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
83 [CKKSManifestInjectionPointHelper registerEgoPeerID:@"MeMyselfAndI" keyPair:keyPair];
85 // We've now disabled manifests.
86 SecCKKSSetSyncManifests(false);
87 SecCKKSSetEnforceManifests(false);
89 XCTAssertFalse([CKKSManifest shouldSyncManifests], "Manifests syncing is disabled");
90 XCTAssertFalse([CKKSManifest shouldEnforceManifests], "Manifests enforcement is disabled");
94 [CKKSManifestInjectionPointHelper setIgnoreChanges:NO];
96 // Test starts with keys in CloudKit (so we can create items later)
97 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
98 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
99 [self saveTLKMaterialToKeychain:self.keychainZoneID];
101 [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
103 [self addGenericPassword:@"data" account:@"first"];
104 [self addGenericPassword:@"data" account:@"second"];
105 [self addGenericPassword:@"data" account:@"third"];
106 [self addGenericPassword:@"data" account:@"fourth"];
107 NSUInteger passwordCount = 4u;
109 [self checkGenericPassword:@"data" account:@"first"];
110 [self checkGenericPassword:@"data" account:@"second"];
111 [self checkGenericPassword:@"data" account:@"third"];
112 [self checkGenericPassword:@"data" account:@"fourth"];
114 [self expectCKModifyItemRecords:passwordCount currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
116 [self startCKKSSubsystem];
117 [self.keychainView waitForKeyHierarchyReadiness];
119 // Wait for uploads to happen
120 OCMVerifyAllWithDelay(self.mockDatabase, 20);
121 [self waitForCKModifications];
123 int extraDeviceStates = 1;
124 XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT + passwordCount + tlkshares + extraDeviceStates, "Have 6+passwordCount objects in cloudkit");
126 NSArray* items = [self mirrorItemsForExistingItems];
127 _egoManifest = [CKKSEgoManifest newManifestForZone:self.keychainZoneID.zoneName withItems:items peerManifestIDs:@[] currentItems:@{} error:&error];
128 XCTAssertNil(error, @"no error encountered generating ego manifest");
129 XCTAssertNotNil(_egoManifest, @"generated ego manifest");
137 - (void)testSaveEgoManifest
139 if (![CKKSManifest shouldSyncManifests]) {
144 [_egoManifest saveToDatabase:&error];
145 XCTAssertNil(error, @"error encountered attempting to save ego manifest: %@", error);
148 - (void)testManifestValidatesItems
150 if (![CKKSManifest shouldSyncManifests]) {
154 NSError* error = nil;
155 for (CKKSItem* mirrorItem in [self mirrorItemsForExistingItems]) {
156 XCTAssertTrue([_egoManifest validateItem:mirrorItem withError:&error], @"failed to validate item against manifest");
157 XCTAssertNil(error, @"error generated attempting to validate item against manifest: %@", error);
161 - (void)disable32852700testManifestDoesNotValidateUnknownItems
163 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
164 [self startCKKSSubsystem];
166 NSArray* knownItems = [self mirrorItemsForExistingItems];
167 [self addGenericPassword:@"data" account:@"unknown_account"];
168 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
169 OCMVerifyAllWithDelay(self.mockDatabase, 20);
170 [self waitForCKModifications];
172 NSArray* newItems = [self mirrorItemsForExistingItems];
173 CKKSItem* unknownItem = nil;
174 for (CKKSItem* item in newItems) {
176 for (CKKSItem* knownItem in knownItems) {
177 if ([knownItem.CKRecordName isEqualToString:item.CKRecordName]) {
189 NSError* error = nil;
190 XCTAssertNotNil(_egoManifest, @"have an ego manifest");
191 XCTAssertFalse([_egoManifest validateItem:unknownItem withError:&error], @"erroneously validated an unknown item against the manifest");
192 XCTAssertNotNil(error, @"failed to generate error when trying to validate an unknown item against the manifest");
195 - (void)testManifestValidatesCurrentItems
197 NSError* error = nil;
198 CKKSItem* item = [[self mirrorItemsForExistingItems] firstObject];
199 [_egoManifest setCurrentItemUUID:item.uuid forIdentifier:@"testCurrentItemIdentifier"];
200 CKKSCurrentItemPointer* currentItemPointer = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"testCurrentItemIdentifier" currentItemUUID:item.uuid state:SecCKKSProcessedStateLocal zoneID:self.keychainZoneID encodedCKRecord:nil];
201 XCTAssertTrue([_egoManifest validateCurrentItem:currentItemPointer withError:&error], @"failed to validate current item against manifest");
202 XCTAssertNil(error, @"error encountered validating current item: %@", error);
205 - (void)testManifestDoesNotValidateUnknownCurrentItems
207 NSError* error = nil;
208 CKKSItem* item = [[self mirrorItemsForExistingItems] firstObject];
209 CKKSCurrentItemPointer* currentItemPointer = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"testCurrentItemIdentifier" currentItemUUID:item.uuid state:SecCKKSProcessedStateLocal zoneID:self.keychainZoneID encodedCKRecord:nil];
210 XCTAssertFalse([_egoManifest validateCurrentItem:currentItemPointer withError:&error], @"erroneously validated an uunknown current item against the manifest");
211 XCTAssertNotNil(error, @"failed to generate error when trying to validate an unknown item against the manifest %@", error);
214 - (void)testEgoManifestValidates
216 if (![CKKSManifest shouldSyncManifests]) {
220 NSError* error = nil;
221 XCTAssertTrue([_egoManifest validateWithError:&error], @"failed to validate our own ego manifest");
222 XCTAssertNil(error, @"error generated attempting to validate ego manifest: %@", error);
225 - (void)testEgoManifestCreatedOnUpload
227 if (![CKKSManifest shouldSyncManifests]) {
231 CKKSEgoManifest* manifest = [CKKSEgoManifest tryCurrentEgoManifestForZone:self.keychainZoneID.zoneName];
232 XCTAssertNotNil(manifest, @"created some items, but we don't have a current manifest");
235 - (void)disable32852700testGenerationCountIncreaseOnChanges
237 if (![CKKSManifest shouldSyncManifests]) {
241 NSInteger initialManifestGenerationCount = [[CKKSEgoManifest tryCurrentEgoManifestForZone:self.keychainZoneID.zoneName] generationCount];
243 [self addGenericPassword:@"data" account:@"GenerationCountIncrease"];
244 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
245 OCMVerifyAllWithDelay(self.mockDatabase, 20);
246 [self waitForCKModifications];
248 NSInteger postUpdateGenerationCount = [[CKKSEgoManifest tryCurrentEgoManifestForZone:self.keychainZoneID.zoneName] generationCount];
249 XCTAssertGreaterThan(postUpdateGenerationCount, initialManifestGenerationCount, @"the manifest after the update does not have a larger generation count as expected");
252 - (NSDictionary*)receiveOneItemInManifest:(BOOL)inManifest
255 [CKKSManifestInjectionPointHelper setIgnoreChanges:YES];
258 NSError* error = nil;
260 NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
261 (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
262 (id)kSecAttrAccount : @"account-delete-me",
263 (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
264 (id)kSecMatchLimit : (id)kSecMatchLimitOne,
267 CFTypeRef item = NULL;
268 XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
270 CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
272 SFECKeyPair* keyPair = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
273 CKKSEgoManifest* manifest = [CKKSEgoManifest newFakeManifestForZone:self.keychainZoneID.zoneName withItemRecords:@[ckr] currentItems:@{} signerID:@"FakeSigner-1" keyPair:keyPair error:&error];
274 [manifest saveToDatabase:&error];
275 XCTAssertNil(error, @"error encountered generating fake manifest: %@", error);
276 XCTAssertNotNil(manifest, @"failed to generate a fake manifest");
278 [self.keychainZone addToZone:ckr];
279 for (CKRecord* record in [manifest allCKRecordsWithZoneID:self.keychainZoneID]) {
280 [self.keychainZone addToZone:record];
283 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
284 [self.keychainView waitForFetchAndIncomingQueueProcessing];
289 // <rdar://problem/35102286> Fix primary keys in ckmanifest sql table
290 // The ckmanifest table only has a single primary key column, so each new manifest written appears to overwrite older manifests
291 // This is causing this test to fail, since the local peer overwrites the manifest created by FakeSigner-1.
292 // Disable this test until manifests come back.
293 - (void)disable35102286testReceiveManifest
295 if (![CKKSManifest shouldSyncManifests]) {
299 NSError* error = nil;
301 NSDictionary* itemQuery = [self receiveOneItemInManifest:YES];
302 CFTypeRef item = NULL;
303 XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)itemQuery, &item), "item should exist now");
305 CKKSManifest* fetchedManifest = [CKKSManifest manifestForZone:self.keychainZoneID.zoneName peerID:@"FakeSigner-1" error:&error];
306 XCTAssertNil(error, @"error encountered fetching expected manifest from database: %@", error);
307 XCTAssertNotNil(fetchedManifest, @"failed to fetch manifest expected in database");
309 CKKSMirrorEntry* fetchedEntry = [CKKSMirrorEntry tryFromDatabase:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.keychainZoneID error:&error];
310 XCTAssertNil(error, @"error encountered fetching expected item from database: %@", error);
311 XCTAssertNotNil(fetchedEntry, @"failed to fetch item expected in database");
313 XCTAssertTrue([fetchedManifest validateWithError:&error], @"failed to validate fetched manifest with error: %@", error);
314 XCTAssertTrue([fetchedManifest validateItem:fetchedEntry.item withError:&error], @"failed to validate item (%@) against manifest with error: %@", fetchedEntry, error);
317 - (void)testUnauthorizedItemRejected
319 if (![CKKSManifest shouldSyncManifests] || ![CKKSManifest shouldEnforceManifests]) {
323 NSDictionary* itemQuery = [self receiveOneItemInManifest:NO];
324 CFTypeRef item = NULL;
325 XCTAssertNotEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)itemQuery, &item), "item should have been rejected");
328 - (void)testSaveManifestWithNilValues
330 [CKKSSQLDatabaseObject performCKKSTransaction:^CKKSDatabaseTransactionResult {
331 SFECKeyPair* keyPair = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
332 CKKSManifest* manifest = [CKKSEgoManifest newFakeManifestForZone:@"SomeZone" withItemRecords:@[] currentItems:@{} signerID:@"BadBoy" keyPair:keyPair error:nil];
333 [manifest nilAllIvars];
334 XCTAssertNil(manifest.zoneID);
335 XCTAssertNil(manifest.signerID);
336 XCTAssertNoThrow([manifest saveToDatabase:nil]);
337 return CKKSDatabaseTransactionRollback;