I'm developing an SDK that will allow iOS devices (iOS 13+) to connect to AWS IoT Core using Native C. The endpoint requires a mutual TLS handshake to connect. I have been able to successfully import a Certificate and Private Key into the keychain but am unable to generate a SecIdentityRef from them for use in setting up a nw_protocol_options_t. I've looked through other forum posts and have been unable to figure out what's going on (Some are from 5+ years ago and maybe things have changed since then).
After prepping the raw data for the cert and key into expected formats I import the certificate:
const void *add_keys[] = {
kSecClass,
kSecAttrLabel,
kSecAttrSerialNumber,
kSecValueData,
kSecReturnRef };
const void *add_values[] = {
kSecClassCertificate,
label,
serial_data,
cert_data,
kCFBooleanTrue };
attributes = CFDictionaryCreate(
cf_alloc,
add_keys,
add_values,
5,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
status = SecItemAdd(attributes, (CFTypeRef *)out_certificate);
Next I import the private key:
const void *add_keys[] = {
kSecClass,
kSecAttrKeyClass,
kSecAttrKeyType,
kSecAttrApplicationLabel,
kSecAttrLabel,
kSecValueData,
kSecReturnRef };
const void *add_values[] = {
kSecClassKey,
kSecAttrKeyClassPrivate,
key_type,
application_label,
label,
key_data,
kCFBooleanTrue };
attributes = CFDictionaryCreate(
cf_alloc,
add_keys,
add_values,
7,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
status = SecItemAdd(attributes, (CFTypeRef *)out_private_key);
The full code handles duplicate items in which case attributes are updated. Following the successful import of the cert and key to the keychain, I attempt to retrieve the identity with the following:
SecIdentityRef identity = NULL;
CFDictionaryRef query = NULL;
const void *query_keys[] = {
kSecClass,
kSecReturnRef,
// kSecAttrSerialNumber,
// kSecAttrLabel
kSecMatchLimit
};
const void *query_values[] = {
kSecClassIdentity,
kCFBooleanTrue,
// cert_serial_data,
// cert_label_ref
kSecMatchLimitAll
};
query = CFDictionaryCreate(
cf_alloc,
query_keys,
query_values,
3,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
OSStatus identity_status = SecItemCopyMatching(query, (CFTypeRef *)&identity);
I have attempted using various search parameters related to the label and the serial of the certificate. Based on other forum post suggestions I have also tried expanding the search to kSecMatchLimitAll to get back ANY stored kSecClassIdentity and all variations returned OSStatus of -25300 (errSecItemNotFound). Once I am able to retrieve the SecIdentityRef, my understanding is that I can add it to the following during creation of the socket:
nw_protocol_options_t tls_options = nw_tls_create_options();
sec_protocol_options_t sec_options = nw_tls_copy_sec_protocol_options(tls_options);
sec_protocol_options_set_min_tls_protocol_version(sec_options, tls_protocol_version_TLSv12);
sec_protocol_options_set_max_tls_protocol_version(sec_options, tls_protocol_version_TLSv13);
sec_protocol_options_set_local_identity(sec_options, SecIdentityRef);
Am I missing some step that is required to create an identity from the certificate and private key? I have tested the cert/key pair and they connect properly when using the old deprecated SecItemImport and SecIdentityCreateWithCertificate (on our old macOS only implementation).
I will continue to dig through Apple documentation as well as more forum posts but I feel like I'm hitting a wall and missing something very obvious as this seems like a very common networking task. Thanks!
The provided links below are to the full code related to the work in progress iOS import functions:
Link to import function https://github.com/awslabs/aws-c-io/blob/cad8639ef0ea08ba3cc74b72cfc1c9866adbb7e5/source/darwin/darwin_pki_utils.c#L735
Link to private key import: https://github.com/awslabs/aws-c-io/blob/cad8639ef0ea08ba3cc74b72cfc1c9866adbb7e5/source/darwin/darwin_pki_utils.c#L561
Link to certificate import: https://github.com/awslabs/aws-c-io/blob/cad8639ef0ea08ba3cc74b72cfc1c9866adbb7e5/source/darwin/darwin_pki_utils.c#L398
Don't quite understand WHY it's the hard path and not set up to be easier to use
Don’t get me wrong, there should be better APIs for this. The Security framework APIs are both very limited and tricky to use correctly. There’s no good reason for that )-: [1]
However, given that situation it’s better to not make your life harder. If, for example, you started with a PKCS#12, you’d have a lot fewer problems. And you’d be writing a lot less code if you use Objective-C or Swift.
Unfortunately, I cannot switch to Objective C or CPP. The entire library is written in native C and then bound out for use in CPP, Java, JavaScript, and Python.
I don’t understand your logic here. Consider:
-
These are Apple specific APIs, which means you have to build this code with Apple tools and the Apple SDK.
-
Given that, you have ready access to Objective-C:
-
Move this code into a separate
.m
file. -
Create a
.h
file that exports a C interface that’s tailored to your specific requirements. -
In the
.m
file, implement those C functions using Objective-C. You don’t even need any classes or objects; the fact that it’s an Objective-C file means that you can use Objective-C shortcuts. -
Adjust your build system to build the
.m
file with Objective-C enabled. You might not even need to do this; most build systems understand.m
.
-
As an example of just how much this buys you, consider these two functions based on one of your earlier code snippets:
extern int clearKeysC(const void * serialData, size_t serialDataCount) {
CFDataRef serial_data = CFDataCreate(NULL, serialData, (CFIndex) serialDataCount);
const void *delete_keys[] = {
kSecClass,
kSecAttrSerialNumber
};
const void *delete_values[] = {
kSecClassCertificate,
serial_data
};
CFDictionaryRef delete_query = CFDictionaryCreate(
NULL,
delete_keys,
delete_values,
2,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
BOOL success = SecItemDelete(delete_query) == errSecSuccess;
CFRelease(serialData);
return success;
}
extern int clearKeysObjC(const void * serialData, size_t serialDataCount) {
NSData * serialDataObj = [NSData dataWithBytes:serialData length:serialDataCount];
return SecItemDelete( (__bridge CFDictionaryRef) @{
(__bridge NSString *) kSecClass: (__bridge NSString *) kSecClassCertificate,
(__bridge NSString *) kSecAttrSerialNumber: serialDataObj,
} ) == errSecSuccess;
}
Life it too short to be wrangling CF dictionaries by hand (-:
Swift is the next language we are adding support for
Cool.
That actually speaks to this point:
I really wish iOS had an implementation of the
SecIdentityCreateWithCertificate
macOS function
Normally I’d suggest that you file an enhancement request for that but, honestly, I don’t think that’s worth it in this case:
-
I think you’re currently stuck on identity formation, and
SecIdentityCreateWithCertificate
follows the same identity formation rules. -
If we did add a new API for this, it’s most likely be in Swift, so asking for
SecIdentityCreateWithCertificate
unlikely to get much traction.
Maybe I can add the public key into the keychain in a way that will cause the private key and the certificate to match up to form an identity.
No, that won’t help [2].
The next debugging step is to check that the attributes responsible for identity formation are correct. So, something like:
-
Add the private key and certificate to the keychain.
-
Call
SecItemCopyMatching
to fetch their attributes (kSecReturnAttributes
). -
Make sure that:
-
The certificate has
kSecAttrPublicKeyHash
(pkhh
) set to its public key hash. -
The private key has
kSecAttrApplicationLabel
(klbl
) set to the same value.
-
I talk about this more in SecItem attributes for keys, which is linked to from SecItem: Pitfalls and Best Practices but that’s easy to miss. I just updated the latter to make this more obvious.
Oh, wait! After writing the above I took another look at your code and it seems that you’re overriding kSecAttrApplicationLabel
on your key. That’s almost certainly the cause of this failure to form an identity.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] It’s partly for historical reasons, but there are Apple internal factors as well, and that’s not something I can discuss publicly.
[2] And I’ve encountered cases where it makes things worse!