Create an SecIdentityRef from a certificate and private key

Hi, I am working on a react native module used for tis connection and I am trying to implement the possibility to use a custom certificate/Private key. I have already implemented on android but on iOS I am getting hard times, we cannot find lots of resources, api is different on macOS and iOS with subtle differences so after having tested SO, chatgpt, ... I am trying here: I even tried to use an internal api since it seems ffmpeg uses it but with no success.

I have attached my current code because it does not fit here.

to sump up after having inserted cert and private key I try to get a SecIdentityRef but it fails. I assume that it's not enough to simply add certain and private key...

// Query for the identity with correct attributes
    NSDictionary *identityQuery = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassIdentity,
        (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne,
        (__bridge id)kSecReturnRef: @YES,
        (__bridge id)kSecReturnData: @YES,
        (__bridge id)kSecAttrLabel: @"My Certificate",
        //(__bridge id)kSecUseDataProtectionKeychain: @YES
    };
    
    SecIdentityRef identity = NULL;
    status = SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, (CFTypeRef *)&identity);

SecItemCopyMatching with kSecClassIdentity fails, SecIdentityCreate return NULL... So please help and indicates what I am doing wrong and how I am supposed getting a SecIdentityRef. Thanks

Answered by DTS Engineer in 823700022

Honestly, I’m not sure why this is failing for you. I’m struggling to think of ways I can debug this remotely, so instead I decided to write up an end-to-end example that shows how to do this. See Importing a PEM-based RSA Private Key and its Certificate.

I suggest you do the following:

  1. Put that code into a simple test app and confirm that it works. If it does, that rules out any environmental issues.

  2. Then modify that test app to use your private key and certificate PEMs. If it continues to work, that rules out any issues with your credentials.

  3. Finally, try adapting this code for your project. If you’re gonna convert this to Objective-C, do that one routine at a time. That way you can test your new code within your test app that you know works in general.

Good luck!

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

First up, read the following:

That’ll help you use the SecItem API effectively.

Looking at your code, it seems to be adding data directly to the keychain, rather than importing the data first and then adding the resulting credential. Your approach can work but it’s hard to get right. I just updated SecItem: Pitfalls and Best Practices with a new section, Import, Then Add, that explains this in more detail.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks, personally I would prefer a GitHub with some examples instead of reading all the documentation. But I have followed what is described ie:

// Creates a certificate object from a DER representation of a certificate
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData);

// Import certificate in keychain
NSDictionary *certAttributes = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassCertificate,
        (__bridge id)kSecValueRef: (__bridge id)cert,
        (__bridge id)kSecAttrLabel: kKeychainCertificateLabel
    };
    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)certAttributes, NULL);

NSDictionary *privateKeyAttributes = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassKey,
        (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA,
        (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate,
        (__bridge id)kSecAttrKeySizeInBits: @(keySize),
        (__bridge id)kSecAttrLabel: @"My PrivateKey",
        //(__bridge id)kSecAttrApplicationTag: [kKeychainApplicationTag dataUsingEncoding:NSUTF8StringEncoding],
    };
    
// Creating private key
    CFErrorRef error = NULL;
    SecKeyRef privateKey = SecKeyCreateWithData((__bridge CFDataRef)keyData,
                                                (__bridge CFDictionaryRef)privateKeyAttributes,
                                                &error);

but SecKeyCreateWithData return NULL, am I supposed to use this API or should I directly use SecItemAdd for for the private key ?

Thanks

I would prefer a GitHub with some examples instead of reading all the documentation

With SecItem, it’s really important that you understand the fundamentals. Without that, it’s easy to take a working example, tweak it slightly, and end up with code that works most of the time and then fails in specific circumstances. To avoid problems like that you need to have a mental model of the API as a whole [1].

but SecKeyCreateWithData return NULL

Right, so you have an import problems, and it was being masked by the fact that you were trying to import and add at the same time.

I have another post that covers import issues: Importing Cryptographic Keys. That should be sufficient for you to resolve this issue. If not, please post a hex dump of an example key.

IMPORTANT Make sure that this is a test key. You don’t want to be sharing real credentials on a public forum.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Now, you could argue that the keychain API shouldn’t require that, and I’d probably agree with you (-: However, right now, we have to work with the API we have )-:

Ok so I really don"t know what I am doing wrong...

// Function declaration from private API
SecIdentityRef SecIdentityCreate(CFAllocatorRef allocator, SecCertificateRef certificate, SecKeyRef privateKey);

static NSString * const kKeychainPrivateKeyLabel = @"My PrivateKey";
static NSString * const kKeychainCertificateLabel = @"My Certificate";

- (SecIdentityRef)createIdentityWithCert:(NSString *)pemCert privateKey:(NSString *)pemKey {
    RCTLogWarn(@"createIdentity: Starting identity creation");
    
    // Strip PEM headers and convert to data
    NSString *cleanedCert = [self stripPEMHeader:pemCert prefix:@"CERTIFICATE"];
    NSString *cleanedKey = [self stripPEMHeader:pemKey prefix:@"PRIVATE KEY"];
    
    NSData *certData = [[NSData alloc] initWithBase64EncodedString:cleanedCert
                                                         options:NSDataBase64DecodingIgnoreUnknownCharacters];
    NSData *keyData = [[NSData alloc] initWithBase64EncodedString:cleanedKey
                                                        options:NSDataBase64DecodingIgnoreUnknownCharacters];
    
    if (!certData || !keyData) {
        RCTLogWarn(@"createIdentity: Failed to create data from base64");
        return NULL;
    }

    // Creates a certificate object from its DER representation
    SecCertificateRef cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData);
    if (!cert) {
        RCTLogWarn(@"createIdentity: Failed to create certificate from data");
        return NULL;
    }

    // Creates a private key object from its DER representation
    NSDictionary *privateKeyAttributes = @{
        //(__bridge id)kSecClass: (__bridge id)kSecClassKey,
        (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA,
        (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate,
        //(__bridge id)kSecAttrLabel: kKeychainPrivateKeyLabel,
        //(__bridge id)kSecAttrApplicationTag: [kKeychainApplicationTag dataUsingEncoding:NSUTF8StringEncoding],
    };
    
    
    CFErrorRef error = NULL;
    SecKeyRef privateKey = SecKeyCreateWithData((__bridge CFDataRef)keyData,
                                                (__bridge CFDictionaryRef)privateKeyAttributes,
                                                &error);
    if (!privateKey) {
        RCTLogWarn(@"createIdentity: Failed to create private key: %@", error);
        CFRelease(cert);
        if (error) CFRelease(error);
        return NULL;
    }

I can see inside the debugger that cleanedKey contains the same string without the header as the attached file client-selfsigned.key.txt but privateKey is NULL

You wrote:

// Function declaration from private API
SecIdentityRef SecIdentityCreate(CFAllocatorRef allocator, SecCertificateRef certificate, SecKeyRef privateKey);

I’m not sure what you’re planning to do with that, but it shouldn’t be necessary and will likely cause problems. I recommend that you remove it.

Looking at client-selfsigned.key.txt I see this:

% dumpasn1 private-key.asn1 
   0 2371: SEQUENCE {
   4    1:   INTEGER 0
   7   13:   SEQUENCE {
   9    9:     OBJECT IDENTIFIER rsaEncryption (1 2 840 113549 1 1 1)
  20    0:     NULL
         :     }
  22 2349:   OCTET STRING, encapsulates {
  26 2345:     SEQUENCE {
  30    1:       INTEGER 0
  33  513:       INTEGER
         :         00 C8 21 C2 CA 34 90 7C FC EC B7 88 8D 02 0F F4
         :         9C 2D 99 32 B2 61 AF D7 16 AB 8D 6F A8 AA 02 26
         :         1F 29 E3 E9 C5 6E BE 52 21 19 2E C2 1C 48 E3 D2
         :         B0 A1 BC 5E 76 62 74 BB FB 40 C4 61 71 62 5D C7
         :         21 9E 84 DF FF 08 5D DC 5E 19 C3 A0 3A 5A A5 BB
         :         B5 50 6C 53 AD 15 F5 10 5F 43 B4 05 D5 92 38 3A
         :         23 82 02 6A 69 9B 3F B2 D9 72 A8 FE C9 F7 8E 2D
         :         3E C1 F6 51 41 7F 4A FE 87 6F FE 1C 87 DC A8 06
         :                 [ Another 385 bytes skipped ]
 550    3:       INTEGER 65537
 …

The stuff inside the encapsulation is an RSAPrivateKey structure, per On Cryptographic Key Formats. That’s what SecKeyCreateWithData is looking for. To import such a key, use the code from the Import RSA Keys section of Importing Cryptographic Keys.

The issue here is that this is encapsulated within a PKCS#8 structure. Security framework can’t import that directly. The best path to a solution here is to switch to elliptic curve. That’s because:

  • Elliptic curve has numerous technical advantages over RSA.

  • Apple CryptoKit has support for importing SECG directly from PEM.

However, I’m gonna presume that you have to stick to RSA. In that case you’ll need to find a way to remove that prefix yourself. For a quick hack you can just drop a fixed number of bytes:

let keyBytes: [UInt8] = [
    0x30, 0x82, 0x09, 0x43, 0x02, 0x01, 0x00, 0x30,
    0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7,
    0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x04, 0x82,
    0x09, 0x2d, 0x30, 0x82, 0x09, 0x29, 0x02, 0x01,
    … elided …
]

func test() -> SecKey? {
    let keyData = Data(keyBytes).dropFirst(26)
    guard let privateKey = SecKeyCreateWithData(keyData as NSData, [
        kSecAttrKeyType: kSecAttrKeyTypeRSA,
        kSecAttrKeyClass: kSecAttrKeyClassPrivate,
    ] as NSDictionary, nil) else {
        return nil
    }
    return privateKey
}

A real solution involves parsing the ASN.1 header, checking for the right key type OID, and then extracting the encapsulated data. iOS does not have an ASN.1 API, so you’ll need to either write or acquire one. I generally reach for SwiftASN1. IIRC, that package actually uses PKCS#8 as an example in its tests.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks a lot for your help! Just to be sure in you code you wrote kSecAttrKeyClassPublic, you wanted to write Private right ?

And finallly SecIdentityCreate seems to work while I don't even know how to get it without this, I tried with status = SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, (CFTypeRef *)&identity); with no success

it's so obvious that even on SO they still are looking for how to do it:

https://stackoverflow.com/questions/45997841/how-to-get-a-secidentityref-from-a-seccertificateref-and-a-seckeyref

https://stackoverflow.com/questions/53897612/how-to-get-secidentity-after-seccertificatecreatewithdata-and-secitemadd

Answer is either to use SecIdentityCreate or use some SecItemExport/SecItemImport ...

So now my code is:

                              privateKey:(NSString *)pemKey
                                settings:(NSDictionary *)settings {
    RCTLogWarn(@"createIdentity: Starting identity creation");
    
    SecIdentityRef identity = NULL;
    
    // Strip PEM headers and convert to data
    NSString *cleanedCert = [self stripPEMHeader:pemCert prefix:@"CERTIFICATE"];
    NSString *cleanedPrivateKey = [self stripPEMHeader:pemKey prefix:@"PRIVATE KEY"];
    NSString *certAlias = settings[@"certAlias"];
    NSString *keyAlias= settings[@"keyAlias"];
    
    NSData *certData = [[NSData alloc] initWithBase64EncodedString:cleanedCert
                                                           options:NSDataBase64DecodingIgnoreUnknownCharacters];
    NSData *pkcs8KeyData = [[NSData alloc] initWithBase64EncodedString:cleanedPrivateKey
                                                               options:NSDataBase64DecodingIgnoreUnknownCharacters];
    
    if (!certData || !pkcs8KeyData) {
        RCTLogWarn(@"createIdentity: Failed to create data from base64");
        return NULL;
    }
    
    // Creates a certificate object from its DER representation
    SecCertificateRef cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData);
    if (!cert) {
        RCTLogWarn(@"createIdentity: Failed to create certificate from data");
        return NULL;
    }
    
    // Extract RSA key from PKCS#8 - TODO use a ASN1 decoder to detect the key format ...
    // For my own use I know it's a pem but I prefer not to trust a file extension and
    // it's better to check from asn1 data
    NSData *rsaKeyData = [self extractRSAKeyFromPKCS8:pkcs8KeyData];
    if (!rsaKeyData) {
        RCTLogWarn(@"Failed to extract RSA key from PKCS#8");
        CFRelease(cert);
        return NULL;
    }
    
    NSDictionary *privateKeyAttributes = @{
        //(__bridge id)kSecClass: (__bridge id)kSecClassKey,
        (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA,
        (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate,
        //(__bridge id)kSecAttrLabel: kKeychainPrivateKeyLabel,
        //(__bridge id)kSecAttrApplicationTag: [kKeychainApplicationTag dataUsingEncoding:NSUTF8StringEncoding],
    };
    
    CFErrorRef error = NULL;
    SecKeyRef privateKey = SecKeyCreateWithData((__bridge CFDataRef)rsaKeyData,
                                                (__bridge CFDictionaryRef)privateKeyAttributes,
                                                &error);
    if (!privateKey) {
        RCTLogWarn(@"createIdentity: Failed to create private key: %@", error);
        CFRelease(cert);
        if (error) CFRelease(error);
        return NULL;
    }
    
    NSDictionary *deleteKeyQuery = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassKey,
        (__bridge id)kSecAttrLabel: keyAlias
    };
    SecItemDelete((__bridge CFDictionaryRef)deleteKeyQuery);
    
    // Import certificate in keychain
    NSDictionary *deleteCertQuery = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassCertificate,
        (__bridge id)kSecAttrLabel: certAlias
    };
    SecItemDelete((__bridge CFDictionaryRef)deleteCertQuery);
    
    NSDictionary *certAttributes = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassCertificate,
        (__bridge id)kSecValueRef: (__bridge id)cert,
        (__bridge id)kSecAttrLabel: certAlias
    };
    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)certAttributes, NULL);
    if (status != errSecSuccess && status != errSecDuplicateItem) {
        RCTLogWarn(@"createIdentity: Failed to store certificate, status: %d", (int)status);
        CFRelease(cert);
        CFRelease(privateKey);
        return NULL;
    }
    
    // Add the private key to keychain
    NSDictionary *keyAttributes = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassKey,
        (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeRSA,
        (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate,
        //(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlock,
        (__bridge id)kSecAttrLabel: keyAlias,
        //(__bridge id)kSecAttrApplicationTag: [kKeychainApplicationTag dataUsingEncoding:NSUTF8StringEncoding],
    };
    status = SecItemAdd((__bridge CFDictionaryRef)keyAttributes, NULL);
    
    if (status != errSecSuccess && status != errSecDuplicateItem) {
        RCTLogWarn(@"createIdentity: Failed to store private key, status: %d", (int)status);
        CFRelease(cert);
        CFRelease(privateKey);
        return NULL;
    }
    
    //------ PRIVATE API: need to find the proper way of doing it -------
    if (YES) {
        identity = SecIdentityCreate(NULL, cert, privateKey);
    } else {
        NSDictionary *identityQuery = @{
            (__bridge id)kSecClass: (__bridge id)kSecClassIdentity,
            (__bridge id)kSecReturnRef: @YES,
            (__bridge id)kSecMatchItemList:@[(__bridge id)cert],
            (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne,
            //(__bridge id)kSecAttrLabel: @"My Certificate",
            //(__bridge id)kSecUseDataProtectionKeychain: @YES
        };
        status = SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, (CFTypeRef *)&identity);
        
        if (status != errSecSuccess || !identity) {
            RCTLogWarn(@"createIdentity: Failed to find identity, status: %d", (int)status);
        } else {
            RCTLogWarn(@"createIdentity: Successfully found identity");
        }
    }
    
    // Clean up
    CFRelease(cert);
    CFRelease(privateKey);
    
    return identity;
}

One last effort and it should be ok ...

Just to be sure in you code you wrote kSecAttrKeyClassPublic

D’oh! Yes. I’ve gone back and fixed the post [1]. Thanks for the sharp eyes!

I don't even know how to get it without this

You’re on the right path with SecItemCopyMatching. For that to work you need to add both the certificate and the private key to the keychain. Moreover, those items need to have the matching values in the identity formation attributes, as described in the Digital Identities Aren’t Real section of SecItem: Pitfalls and Best Practices. Normally the system adds those attributes correctly; indeed, that’s a key reason why it’s better to add import and then add.

If digital identity formation isn’t working correctly, write some small test code that calls SecItemCopyMatching with kSecReturnAttributes. Run that against both the key and the certificate and then look at the returned attributes. Check that the items are present as you expect and that the identity formation attributes both have the same value.

Oh, wait. You’re using kSecMatchItemList. Don’t do that. When debugging this, request all digital identities and then look through the results. That’ll let you distinguish between two important cases:

  • The items failing to form a digital identity

  • You matching the correct digital identity

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] I try to avoid doing that, but in this case I think it’s justified because folks are likely to copy’n’paste the code.

Accepted Answer
write some small test code that calls SecItemCopyMatching with kSecReturnAttributes.

Oh, hey. I made a note to myself to update SecItem: Pitfalls and Best Practices to discuss these debugging ideas in more detail, but it turns out that Past Quinn™ has got you covered. Check out the Lost Keychain Items, Redux section of that post for a general explanation of this idea.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

thanks for your help. it should be ok now.

Finally nothing work anymore since now every time I try to insert the private key I get a errSecDuplicateItem but when I try to delete it before it says it does not exist. I also tried to uninstall the app and restart the phone but same results. So I am having a break for a few weeks and if I have motivated I will try again to make it work...

Hum actually if we pass a Label to SecItemDelete it fails ... no error is allowed. So finally I am dumping attributes:

Key Attributes: { accc = "<SecAccessControlRef: ak>"; agrp = "D27Q6535KW.com.lotalogic.testandroidtvremoteapp"; atag = ""; bsiz = 0; cdat = "2025-02-01 11:09:49 +0000"; crtr = 0; edat = "2001-01-01 00:00:00 +0000"; esiz = 0; kcls = 1; klbl = {length = 0, bytes = 0x}; labl = cert; mdat = "2025-02-01 11:09:49 +0000"; musr = {length = 0, bytes = 0x}; pdmn = ak; sdat = "2001-01-01 00:00:00 +0000"; sha1 = {length = 20, bytes = 0xdc6b0c25a45f014b2d6da9f668e0d4cd325f3b35}; sync = 0; tomb = 0; type = 42; }

Cert Attributes: { accc = "<SecAccessControlRef: dk>"; agrp = "D27Q6535KW.com.lotalogic.testandroidtvremoteapp"; cdat = "2025-02-01 11:10:04 +0000"; cenc = 3; ctyp = 3; issr = {length = 89, bytes = 0x310b3009 06035504 06130246 52310c30 ... 69636520 4e616d65 }; labl = cert; mdat = "2025-02-01 11:10:04 +0000"; musr = {length = 0, bytes = 0x}; pdmn = dk; pkhh = {length = 20, bytes = 0x312ede000df0366b3b5fb559a48177ca23215516}; sha1 = {length = 20, bytes = 0xaf3c77416df73b8d8ba4449c797f16ef8f5ac59c}; skid = {length = 20, bytes = 0x312ede000df0366b3b5fb559a48177ca23215516}; slnr = {length = 20, bytes = 0x5c2576d44745619b3cab62ebcc402d19741150d4}; subj = {length = 89, bytes = 0x310b3009 06035504 06130246 52310c30 ... 69636520 4e616d65 }; sync = 0; tomb = 0;

but still no identity:

NSDictionary *identityQuery = @{
            (__bridge id)kSecClass: (__bridge id)kSecClassIdentity,
            (__bridge id)kSecReturnRef: @YES,
        };
        status = SecItemCopyMatching((__bridge CFDictionaryRef)identityQuery, (CFTypeRef *)&identity);

I think I will reverse SecIdentityCreate it will be easier.

hum klbl seems empty, it should be the reason because per documentation:

The system forms a digital identity by matching the kSecAttrApplicationLabel (klbl) attribute of the private key with the kSecAttrPublicKeyHash (pkhh) attribute of the certificate

Create an SecIdentityRef from a certificate and private key
 
 
Q