CKKSManifestTests.m   [plain text]


/*
 * Copyright (c) 2017 Apple Inc. All Rights Reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 *
 * @APPLE_LICENSE_HEADER_END@
 */

#if OCTAGON

#import "CKKSTests.h"
#import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
#import "keychain/ckks/CKKSManifest.h"
#import "keychain/ckks/CKKSMirrorEntry.h"
#import "keychain/ckks/CKKSViewManager.h"
#import "keychain/ckks/CKKSCurrentItemPointer.h"
#import <Security/SecEntitlements.h>
#include <ipc/server_security_helpers.h>
#import <SecurityFoundation/SFKey.h>
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>

@interface CKKSManifestTests : CloudKitKeychainSyncingTestsBase {
    BOOL _manifestIsSetup;
    CKKSEgoManifest* _egoManifest;
    id _manifestMock;
    id _fakeSigner1Mock;
}

@end

@implementation CKKSManifestTests

- (NSArray*)mirrorItemsForExistingItems
{
    NSArray<CKRecord*>* records = [self.keychainZone.currentDatabase.allValues filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"self.recordType like %@", SecCKRecordItemType]];
    NSMutableArray* items = [[NSMutableArray alloc] init];
    __weak __typeof(self) weakSelf = self;
    __block NSError* error = nil;
    [self.keychainView dispatchSync:^bool(void) {
        for (CKRecord* record in records) {
            CKKSMirrorEntry* mirrorEntry = [CKKSMirrorEntry tryFromDatabase:record.recordID.recordName zoneID:weakSelf.keychainZoneID error:&error];
            XCTAssertNil(error, @"error encountered trying to generate CKKSMirrorEntry: %@", error);
            XCTAssertNotNil(mirrorEntry, @"failed to generate mirror entry");
            if (mirrorEntry) {
                [items addObject:mirrorEntry.item];
            }
        }
        
        return YES;
    }];
    
    return items;
}

- (void)setUp
{
    // Lie to our superclass about how the world is
    (void)[CKKSManifest shouldSyncManifests]; // initialize.
    SecCKKSSetSyncManifests(false);
    SecCKKSSetEnforceManifests(false);

    [super setUp];

    // Should be removed when manifests is turned back on
    SFECKeyPair* keyPair = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
    [CKKSManifestInjectionPointHelper registerEgoPeerID:@"MeMyselfAndI" keyPair:keyPair];

    // Always sync manifests, and never enforce them
    SecCKKSSetSyncManifests(true);
    SecCKKSSetEnforceManifests(false);

    XCTAssertTrue([CKKSManifest shouldSyncManifests], "Manifests syncing is enabled");
    XCTAssertFalse([CKKSManifest shouldEnforceManifests], "Manifests enforcement is disabled");

    NSError* error = nil;
    
    [CKKSManifestInjectionPointHelper setIgnoreChanges:NO];
    
    // Test starts with keys in CloudKit (so we can create items later)
    [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
    [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
    [self saveTLKMaterialToKeychain:self.keychainZoneID];

    [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];

    [self addGenericPassword:@"data" account:@"first"];
    [self addGenericPassword:@"data" account:@"second"];
    [self addGenericPassword:@"data" account:@"third"];
    [self addGenericPassword:@"data" account:@"fourth"];
    NSUInteger passwordCount = 4u;
    
    [self checkGenericPassword:@"data" account:@"first"];
    [self checkGenericPassword:@"data" account:@"second"];
    [self checkGenericPassword:@"data" account:@"third"];
    [self checkGenericPassword:@"data" account:@"fourth"];
    
    [self expectCKModifyItemRecords:passwordCount currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
    
    [self startCKKSSubsystem];
    [self.keychainView waitForKeyHierarchyReadiness];
    
    // Wait for uploads to happen
    OCMVerifyAllWithDelay(self.mockDatabase, 20);
    [self waitForCKModifications];
    int tlkshares = 1;
    int extraDeviceStates = 1;
    XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT + passwordCount + tlkshares + extraDeviceStates, "Have 6+passwordCount objects in cloudkit");
    
    NSArray* items = [self mirrorItemsForExistingItems];
    _egoManifest = [CKKSEgoManifest newManifestForZone:self.keychainZoneID.zoneName withItems:items peerManifestIDs:@[] currentItems:@{} error:&error];
    XCTAssertNil(error, @"no error encountered generating ego manifest");
    XCTAssertNotNil(_egoManifest, @"generated ego manifest");
}

- (void)tearDown {
    _egoManifest = nil;
    [super tearDown];
}

- (void)testSaveEgoManifest
{
    if (![CKKSManifest shouldSyncManifests]) {
        return;
    }

    NSError* error;
    [_egoManifest saveToDatabase:&error];
    XCTAssertNil(error, @"error encountered attempting to save ego manifest: %@", error);
}

- (void)testManifestValidatesItems
{
    if (![CKKSManifest shouldSyncManifests]) {
        return;
    }

    NSError* error = nil;
    for (CKKSItem* mirrorItem in [self mirrorItemsForExistingItems]) {
        XCTAssertTrue([_egoManifest validateItem:mirrorItem withError:&error], @"failed to validate item against manifest");
        XCTAssertNil(error, @"error generated attempting to validate item against manifest: %@", error);
    }
}

- (void)disable32852700testManifestDoesNotValidateUnknownItems
{
    [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
    [self startCKKSSubsystem];

    NSArray* knownItems = [self mirrorItemsForExistingItems];
    [self addGenericPassword:@"data" account:@"unknown_account"];
    [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
    OCMVerifyAllWithDelay(self.mockDatabase, 20);
    [self waitForCKModifications];
    
    NSArray* newItems = [self mirrorItemsForExistingItems];
    CKKSItem* unknownItem = nil;
    for (CKKSItem* item in newItems) {
        BOOL found = NO;
        for (CKKSItem* knownItem in knownItems) {
            if ([knownItem.CKRecordName isEqualToString:item.CKRecordName]) {
                found = YES;
                break;
            }
        }
        
        if (!found) {
            unknownItem = item;
            break;
        }
    }
    
    NSError* error = nil;
    XCTAssertNotNil(_egoManifest, @"have an ego manifest");
    XCTAssertFalse([_egoManifest validateItem:unknownItem withError:&error], @"erroneously validated an unknown item against the manifest");
    XCTAssertNotNil(error, @"failed to generate error when trying to validate an unknown item against the manifest");
}

- (void)testManifestValidatesCurrentItems
{
    NSError* error = nil;
    CKKSItem* item = [[self mirrorItemsForExistingItems] firstObject];
    [_egoManifest setCurrentItemUUID:item.uuid forIdentifier:@"testCurrentItemIdentifier"];
    CKKSCurrentItemPointer* currentItemPointer = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"testCurrentItemIdentifier" currentItemUUID:item.uuid state:SecCKKSProcessedStateLocal zoneID:self.keychainZoneID encodedCKRecord:nil];
    XCTAssertTrue([_egoManifest validateCurrentItem:currentItemPointer withError:&error], @"failed to validate current item against manifest");
    XCTAssertNil(error, @"error encountered validating current item: %@", error);
}

- (void)testManifestDoesNotValidateUnknownCurrentItems
{
    NSError* error = nil;
    CKKSItem* item = [[self mirrorItemsForExistingItems] firstObject];
    CKKSCurrentItemPointer* currentItemPointer = [[CKKSCurrentItemPointer alloc] initForIdentifier:@"testCurrentItemIdentifier" currentItemUUID:item.uuid state:SecCKKSProcessedStateLocal zoneID:self.keychainZoneID encodedCKRecord:nil];
    XCTAssertFalse([_egoManifest validateCurrentItem:currentItemPointer withError:&error], @"erroneously validated an uunknown current item against the manifest");
    XCTAssertNotNil(error, @"failed to generate error when trying to validate an unknown item against the manifest %@", error);
}

- (void)testEgoManifestValidates
{
    if (![CKKSManifest shouldSyncManifests]) {
        return;
    }

    NSError* error = nil;
    XCTAssertTrue([_egoManifest validateWithError:&error], @"failed to validate our own ego manifest");
    XCTAssertNil(error, @"error generated attempting to validate ego manifest: %@", error);
}

- (void)testEgoManifestCreatedOnUpload
{
    if (![CKKSManifest shouldSyncManifests]) {
        return;
    }

    CKKSEgoManifest* manifest = [CKKSEgoManifest tryCurrentEgoManifestForZone:self.keychainZoneID.zoneName];
    XCTAssertNotNil(manifest, @"created some items, but we don't have a current manifest");
}

- (void)disable32852700testGenerationCountIncreaseOnChanges
{
    if (![CKKSManifest shouldSyncManifests]) {
        return;
    }

    NSInteger initialManifestGenerationCount = [[CKKSEgoManifest tryCurrentEgoManifestForZone:self.keychainZoneID.zoneName] generationCount];
    
    [self addGenericPassword:@"data" account:@"GenerationCountIncrease"];
    [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
    OCMVerifyAllWithDelay(self.mockDatabase, 20);
    [self waitForCKModifications];
    
    NSInteger postUpdateGenerationCount = [[CKKSEgoManifest tryCurrentEgoManifestForZone:self.keychainZoneID.zoneName] generationCount];
    XCTAssertGreaterThan(postUpdateGenerationCount, initialManifestGenerationCount, @"the manifest after the update does not have a larger generation count as expected");
}

- (NSDictionary*)receiveOneItemInManifest:(BOOL)inManifest
{
    if (!inManifest) {
        [CKKSManifestInjectionPointHelper setIgnoreChanges:YES];
    }
    
    NSError* error = nil;
    
    NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
                            (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
                            (id)kSecAttrAccount : @"account-delete-me",
                            (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
                            (id)kSecMatchLimit : (id)kSecMatchLimitOne,
                            };
    
    CFTypeRef item = NULL;
    XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not yet exist");
    
    CKRecord* ckr = [self createFakeRecord:self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
    
    SFECKeyPair* keyPair = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
    CKKSEgoManifest* manifest = [CKKSEgoManifest newFakeManifestForZone:self.keychainZoneID.zoneName withItemRecords:@[ckr] currentItems:@{} signerID:@"FakeSigner-1" keyPair:keyPair error:&error];
    [manifest saveToDatabase:&error];
    XCTAssertNil(error, @"error encountered generating fake manifest: %@", error);
    XCTAssertNotNil(manifest, @"failed to generate a fake manifest");
    
    [self.keychainZone addToZone:ckr];
    for (CKRecord* record in [manifest allCKRecordsWithZoneID:self.keychainZoneID]) {
        [self.keychainZone addToZone:record];
    }
    
    // Trigger a notification (with hilariously fake data)
    [self.keychainView notifyZoneChange:nil];
    
    [self.keychainView waitForFetchAndIncomingQueueProcessing];
    
    return query;
}

// <rdar://problem/35102286> Fix primary keys in ckmanifest sql table
// The ckmanifest table only has a single primary key column, so each new manifest written appears to overwrite older manifests
// This is causing this test to fail, since the local peer overwrites the manifest created by FakeSigner-1.
// Disable this test until manifests come back.
- (void)disable35102286testReceiveManifest
{
    if (![CKKSManifest shouldSyncManifests]) {
        return;
    }

    NSError* error = nil;
    
    NSDictionary* itemQuery = [self receiveOneItemInManifest:YES];
    CFTypeRef item = NULL;
    XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)itemQuery, &item), "item should exist now");
    
    CKKSManifest* fetchedManifest = [CKKSManifest manifestForZone:self.keychainZoneID.zoneName peerID:@"FakeSigner-1" error:&error];
    XCTAssertNil(error, @"error encountered fetching expected manifest from database: %@", error);
    XCTAssertNotNil(fetchedManifest, @"failed to fetch manifest expected in database");
    
    CKKSMirrorEntry* fetchedEntry = [CKKSMirrorEntry tryFromDatabase:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" zoneID:self.keychainZoneID error:&error];
    XCTAssertNil(error, @"error encountered fetching expected item from database: %@", error);
    XCTAssertNotNil(fetchedEntry, @"failed to fetch item expected in database");
    
    XCTAssertTrue([fetchedManifest validateWithError:&error], @"failed to validate fetched manifest with error: %@", error);
    XCTAssertTrue([fetchedManifest validateItem:fetchedEntry.item withError:&error], @"failed to validate item (%@) against manifest with error: %@", fetchedEntry, error);
}

- (void)testUnauthorizedItemRejected
{
    if (![CKKSManifest shouldSyncManifests] || ![CKKSManifest shouldEnforceManifests]) {
        return;
    }

    NSDictionary* itemQuery = [self receiveOneItemInManifest:NO];
    CFTypeRef item = NULL;
    XCTAssertNotEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef)itemQuery, &item), "item should have been rejected");
}

- (void)testSaveManifestWithNilValues
{
    SFECKeyPair* keyPair = [[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]];
    CKKSManifest* manifest = [CKKSEgoManifest newFakeManifestForZone:@"SomeZone" withItemRecords:@[] currentItems:@{} signerID:@"BadBoy" keyPair:keyPair error:nil];
    [manifest nilAllIvars];
    XCTAssertNil(manifest.zoneID);
    XCTAssertNil(manifest.signerID);
    XCTAssertNoThrow([manifest saveToDatabase:nil]);
}

@end

#endif // OCTAGON