[SOLVED] SecItemCopyMatching does not retrieve private key using same query as iOS12

Hello,


I'm running a regression tests agains iOS 13b4 and i have found an issue with the SecItemCopyMatching API.

In iOS 12 the following class can generate a key-pair, and later on retrieve and query for the existance of a key given an alias:

@interface PKIUtils : NSObject
+ (BOOL)generateKeyPair:(NSString* _Nonnull)alias error:(NSError* _Nullable* _Nullable)error;
+ (BOOL)hasKeyPair:(NSString* _Nonnull)alias;
+ (SecKeyRef) privateKeyForAlias:(NSString* _Nonnull) alias error:(NSError * _Nullable* _Nullable) error;
@end

@implementation PKIUtils

+ (BOOL)generateKeyPair:(NSString* _Nonnull)alias error:(NSError* _Nullable* _Nullable)error {
    if (!alias || alias.length <= 0) {
        NSLog(@"Alias cannot be empty");
        return NO;
    }

    if ([self hasKeyPair:alias]) {
        NSLog(@"key already exists");
        return NO;
    }

    NSDictionary* attributes = [self buildAttributes:alias error:error];
    if (!attributes) {
        return NO;
    }

    CFErrorRef errorRef = nil;
    SecKeyRef privateKey = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &errorRef);

    if (!privateKey) {
        if (error) *error = (__bridge NSError*)errorRef;
        return NO;
    }

    // release the private key reference as soon as it is not longer needed.
    CFRelease(privateKey);

    // everything went fine :)
    return YES;
}

+ (NSDictionary*)buildAttributes:(NSString*)alias error:(NSError**)error {
    CFErrorRef errorRef = nil;
    UInt32 accessControlFlags = kSecAccessControlPrivateKeyUsage;

    SecAccessControlRef access = SecAccessControlCreateWithFlags(NULL, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                                                                 accessControlFlags, &errorRef);

    if (!access) { // unable to create the access Control reference
        if (error)
            *error = (__bridge NSError*)errorRef;
        return nil;
    }

    NSDictionary* attributes = @{
        (__bridge NSString*)kSecAttrKeyType : (id)kSecAttrKeyTypeECSECPrimeRandom,
        (__bridge NSString*)kSecAttrKeySizeInBits : @256,
        (__bridge NSString*)kSecPrivateKeyAttrs : @{
            (__bridge NSString*)kSecAttrIsPermanent : @YES,
            (__bridge NSString*)kSecAttrApplicationTag : [alias dataUsingEncoding:NSUTF8StringEncoding],
            (__bridge NSString*)kSecAttrAccessControl : (__bridge id)access,
        }
    };

    // release the access control reference as soon as it is not longer needed.
    CFRelease(access);

    return attributes;
}

+ (BOOL)hasKeyPair:(NSString* _Nonnull)alias {
    SecKeyRef privateKeyRef = [self privateKeyForAlias:alias error:nil];
    return privateKeyRef != nil;
}

+ (SecKeyRef) privateKeyForAlias:(NSString* _Nonnull) alias
                           error:(NSError * _Nullable* _Nullable) error {
    NSDictionary* keyQuery = @{
        (__bridge NSString*)kSecClass : (id)kSecClassKey,
        (__bridge NSString*)kSecAttrApplicationTag : [alias dataUsingEncoding:NSUTF8StringEncoding],
        (__bridge NSString*)kSecReturnRef : @YES
    };

    SecKeyRef privateKeyRef = nil;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)keyQuery, (CFTypeRef*)&privateKeyRef);
    if (status != noErr) {
        NSLog(@"Error retrieving key-pair from the storage (%@)", @(status));
        return nil;
    }
    return privateKeyRef;
}


@end


Called from an app delegate as:

NSError* error;
    NSString* alias = @"some alias"
    BOOL result = [PKIUtils generateKeyPair:alias error:&error];
    if(result){
        if([PKIUtils hasKeyPair:alias]){
            NSLog(@"Hooray");
        } else {
            NSLog(@"Oh no!");
        }
    }


However, in iOS 13b4, it can generate the key-pair, i can perform operations over the private key reference (export public key, sign, verify) but whenever i attempt to query for the same private key, it cannot be found. The OSStatus code returned is -25300 (item not found).


Am i missing something?


NOTES:

- The code has only been tested on the simulator.

- Moving to CrytoKit is not a viable option

- I have attempted multiple combinations of parameters, all the same result.

Accepted Reply

The solution i found was to add precompiler if around certain parameters when using the simulator, in particular:

#if !(TARGET_IPHONE_SIMULATOR)
        (__bridge NSString*)kSecAttrTokenID : (id)kSecAttrTokenIDSecureEnclave,
#endif
        (__bridge NSString*)kSecPrivateKeyAttrs : @{
#if !(TARGET_IPHONE_SIMULATOR)
            (__bridge NSString*)kSecAttrAccessControl : (__bridge id)access,
#endif


This fully solved the issue for me.

Replies

I've just found this same issue with out tests against the simulator. Did you find a solution?

For anyone looking for further information regarding this issue, there's a thread on SO:

https://stackoverflow.com/questions/56700680/keychain-query-always-returns-errsecitemnotfound-after-upgrading-to-ios-13

I've read that thread but the solution is unclear. What was your solution in the end?

Did you find a solution and if yes, what was it?

andI ran into the exact same problem. my tests were fine on the device, but not on the simulator, and what i found is that by doing:

UInt32 accessControlFlags = kSecAccessControlPrivateKeyUsage;  
  
    SecAccessControlRef access = SecAccessControlCreateWithFlags(NULL, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,  
                                                                 accessControlFlags, &errorRef);  
 


when i saved the query, it was blackholeing it it seems. I did not get an error on the simulator, it just reported success for the save. but when i went to query out all keys from the keychain using:


  func getAllKeychainItems() throws {
    
    let classes = [kSecClassGenericPassword as String,  // Generic password items
                   kSecClassInternetPassword as String, // Internet password items
                   kSecClassCertificate as String,      // Certificate items
                   kSecClassKey as String,              // Cryptographic key items
                   kSecClassIdentity as String]         // Identity items
    
    
    classes.forEach { secClass in
      let items = getAllKeyChainItemsOfClass( secClass )
      NSLog(items.description)
    }
  }
  
  
  func getAllKeyChainItemsOfClass(_ secClass: String) -> [String: AnyObject] {
    
    let query: [String: Any] = [
      kSecClass as String : secClass,
      kSecReturnData as String  : true,
      kSecReturnAttributes as String : true,
      kSecReturnRef as String : true,
      kSecMatchLimit as String: kSecMatchLimitAll
    ]
    
    var result: AnyObject?
    
    let lastResultCode = withUnsafeMutablePointer(to: &result) {
      SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
    }
    
    var values = [String: AnyObject]()
    if lastResultCode == noErr {
      let array = result as? Array<Dictionary<String, Any>>
      
      for item in array! {
        if let key = item[kSecAttrAccount as String] as? String,
          let value = item[kSecValueData as String] as? Data {
          values[key] = String(data: value, encoding:.utf8) as AnyObject?
        }
        else if let key = item[kSecAttrLabel as String] as? String,
          let value = item[kSecValueRef as String] {
          values[key] = value as AnyObject
        }
      }
    }
    return values
  }


It was not priniting them from the simulator. But i had a test example that did work correctly without the access control, but every thing else the same.


Now the only reason i was using the access control was to set the private key usage for the public/private key generation when using the the SecureEnclave as part of a ECDH key pair generation. I was not requiring biometrics in my access control, similar to what you have above. I wanted my private key to be acessible in the background after first unlocked, in order to decrypt incoming push events that we have encrypted and generated with a public private key generated on the device. The simulator does not have the secure enclave, and so we must save both the public key and private key. On the device if the isPermanent is set in the keypair generation method, it is stored, but not on a device that does not have the secure enclave, like the simulator. The `isPermanent` used to do that for us, but on iOS 13, it seems it no longer does, at least in my poking at it, it did not.


So i detect in my code if am on a simulator, or better put, if secure enclave is not available, and save it if it was with a simple save query the same you have when its not permanent.


Whenever i saved with the access control specified on the simulator, it would not save, even though it reported a success. When i removed the access control it would save and i could query it. I still need to set the accessibility on it, in case my code runs on a device that does not have secure enclav, and do not want it to just be defaulted with is `unlocked`. Since the only reason i was using the access control was to set the accessibility flag and that private key usage for the secure enclave, when i detect that there is not secure enclave, i do not specify access control int the query, but rather in the query i now specify the accessible flag instead directly on the save query only when not secure enclave, and when secure enclave i specify the access control with the private usage flag set and the same accessible level, and it resolved it for me.


in looking over, this seems to be consistent with:

https://developer.apple.com/documentation/security/keychain_services/keychain_items/restricting_keychain_item_accessibility

https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_secure_enclave


Hope that helps or gives you a few clues in your question if you are still looking.

The solution i found was to add precompiler if around certain parameters when using the simulator, in particular:

#if !(TARGET_IPHONE_SIMULATOR)
        (__bridge NSString*)kSecAttrTokenID : (id)kSecAttrTokenIDSecureEnclave,
#endif
        (__bridge NSString*)kSecPrivateKeyAttrs : @{
#if !(TARGET_IPHONE_SIMULATOR)
            (__bridge NSString*)kSecAttrAccessControl : (__bridge id)access,
#endif


This fully solved the issue for me.