Using SecIdentityRef to create a TLS connection

Hello, 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 and generate a SecIdentityRef that combines the cert/key pair which I believe is necessary to establish a TCP TLS nw_connection.

I've searched around and while I can find the individual pieces related to creating a TLS connection, I can't seem to find any that show how things go together.

The goal would be to use

nw_connection_create(endpoint, parameters);

to establish a TLS connection.

This is currently how I am creating the parameters for this connection. transport_ctx->secitem_identity is where the SecIdentityRef is kept.

nw_parameters_create_secure_tcp(
// nw_parameters_configure_protocol_block_t for configure_tls
                ^(nw_protocol_options_t tls_options) {

                    sec_protocol_options_t sec_options = nw_tls_copy_sec_protocol_options(tls_options);

                    // Set the minimum TLS version to TLS 1.2
                    sec_protocol_options_set_min_tls_protocol_version(sec_options, tls_protocol_version_TLSv12);
                    // Set the maximum TLS version to TLS 1.3
                    sec_protocol_options_set_max_tls_protocol_version(sec_options, tls_protocol_version_TLSv13);

                    sec_protocol_options_set_local_identity(sec_options, transport_ctx->secitem_identity);
                    },
// nw_parameters_configure_protocol_block_t for configure_tcp
// This is also manually set with a code block but not relevant to this q.
NW_PARAMETERS_DEFAULT_CONFIGURATION);

My question is whether or not I'm even on the right track with attempting to use these functions to setup the TLS options associated with the parameters? The sec_protocol_options_set_local_identity appears to be listed under "Security legacy reference" in the apple dev docs: https://developer.apple.com/documentation/security/sec_protocol_options_set_local_identity(::)?language=objc And the surrounding documentation related to using TLS with a network connection feels sparse at best.

Follow up question is whether there is any documentation or reading material available for setting up TLS with a TCP socket connection. I'd love to not have to take up time asking these questions if there's somewhere I can just learn it.

Thanks!

Answered by DTS Engineer in 803043022

Thanks for the C conversion!

Note that you didn’t include the receive function, but that’s OK because I could reproduce the crash without it (-:

The issue is with this line:

sec_protocol_options_set_local_identity(sec_options, ident);

It needs to be this:

sec_identity_t secIdent = sec_identity_create(ident);
sec_protocol_options_set_local_identity(sec_options, secIdent);

SecIdentityRef and sec_identity_t are different things.

Oh, and just FYI, the static analyser complains about you leaking copyResult and the temporary CFString that you create from name. Both of these are easy to fix, but you may not need to fix them because they’re in clientIdentityNamed, which is just scaffolding.

Share and Enjoy

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

You are on the right track with sec_protocol_options_set_local_identity. I have more concrete details in this post. It’s in Swift, but all the concepts map directly over to the C API.

documentation related to using TLS with a network connection feels sparse at best

Welcome to my world )-:

Availability of Low-Level APIs talks about this issue extensively. In the case of Network framework and the associated TLS gubbins from the Security framework, the best place to look is the header doc comments. And if you get stuck, try searching here DevForums. It’s likely that someone else has hit your roadblock before.

Share and Enjoy

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

Thanks for the link! I came across it during my search on the forums and it's where I got the sec_protocol_options_set_local_identity from to supply a client identity as you mentioned.

The post also includes some information related to customizing the server trust evaluation as well as responding to client challenges from the server. Are these only required if I want to take a custom action and is setting the local identity enough to handle a mutual TLS connection handshake or do I need to set up one or both of them and provide blocks that complete the requests?

Good call on looking through the header files themselves. And yeah, I'm constantly running into "This feels like a common enough issue that people would have run into it before" which led me to searches here. It's also great that you've composed posts that address common issues and questions. Thanks again for the help, it's very much appreciated!

This is how I've currently set up the parameter creation as straight forward as I can based on the linked post.

nw_parameters_create_secure_tcp(
                ^(nw_protocol_options_t tls_options) {
                    sec_protocol_options_t sec_options = nw_tls_copy_sec_protocol_options(tls_options);

                    sec_protocol_options_set_local_identity(sec_options, transport_ctx->secitem_identity);

                    // Set the minimum TLS version to TLS 1.2
                    sec_protocol_options_set_min_tls_protocol_version(sec_options, tls_protocol_version_TLSv12);
                    // Set the maximum TLS version to TLS 1.3
                    sec_protocol_options_set_max_tls_protocol_version(sec_options, tls_protocol_version_TLSv13);

                    // Set up the verify block for custom server trust evaluation
                    sec_protocol_options_set_verify_block(sec_options,
                        ^(sec_protocol_metadata_t metadata, sec_trust_t trust, sec_protocol_verify_complete_t complete) {
                            // Perform custom trust evaluation
                            SecTrustResultType trustResult;
                            OSStatus status = SecTrustEvaluate(trust, &trustResult);

                            // EAccept the server trust if it is valid
                            if (status == errSecSuccess && (trustResult == kSecTrustResultProceed || trustResult == kSecTrustResultUnspecified)) {
                                complete(true);  // Trust is accepted
                            } else {
                                complete(false); // Trust is rejected
                            }
                        }, dispatch_get_main_queue());

                    // Set up the challenge block for handling authentication challenges
                    sec_protocol_options_set_challenge_block(sec_options,
                        ^(sec_protocol_metadata_t metadata, sec_protocol_challenge_complete_t complete) {
                            // Handle a challenge here
                            // simply complete with true for now
                            complete(true);
                        }, dispatch_get_main_queue());
                    },

I am encountering an issue where after creating the nw_connection using nw_connection_create with an endpoint and the parameters, setting the state changed handler, and attempting to connect with nw_connection_start, the state appropriately changes to nw_connection_state_preparing but then immediately hits a EXC_BAD_ACCESS on a movq in CoreFoundation CFRetain.

tracking packets, a TCP connection is initiated with a SYN, responded to from the server with a SYN ACK and the local connection is sending a ACK. After TCP is established is the point it appears as though something is trying to access memory that's bad.

Will continue trying to track it down but I'm not CFReleasing anything so I'm wondering if there's something in the tls_options or the sec_protocol_options I need to explicitly retain a reference on in the code block. If you've got any clue or guess as to why this is happening only when I provide a TLS block, it'd help track things down >,<

is setting the local identity enough to handle a mutual TLS connection handshake

That’s right.

I am encountering an issue where [it] immediately hits a EXC_BAD_ACCESS on a movq in CoreFoundation CFRetain.

Well, that’s not fun.

Have you tried selectively apply your verify and challenge blocks to see if just one of them causes the crash?

Looking at your code, I see at least one problem:

OSStatus status = SecTrustEvaluate(trust, &trustResult);

trust is of type sec_trust_t, but that routine takes a parameter of SecTrustRef. You need to apply sec_trust_copy_ref.

Share and Enjoy

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

Thanks for the response! Knowing that setting the identity is all I need for the Mutual TLS handshake will make it easier to limit the pieces in play.

Have you tried selectively apply your verify and challenge blocks to see if just one of them causes the crash?

I originally tried just setting the identity and encountered the crash. I individually added the verify and challenge blocks one at a time and then both to check that not having set them wasn't causing the crash. I've also input print statements into the blocks and neither are being called so whatever is crashing is happening prior to them being hit.

I'm just getting back to this now after the long weekend and will try tracking memory addresses to see if I can't isolate what exactly is being accessed.

Update: I am still running into the EXC_BAD_ACCESS when using sec_protocol_options_set_local_identity

I've checked at the point of setting up the nw_protocol_options_t tls_options block that the SecIdentityRef is valid. It is not NULL and running SecIdentityCopyCertificate and SecIdentityCopyPrivateKey against it returns a valid cert and key that depict the correct contents when CFShow is applied to the results.

Applying an additional CFRetain on the identity does not prevent the issue.

Other manipulation of the sec_protocol_options_t retrieved using nw_tls_copy_sec_protocol_options(tls_options) such as sec_protocol_options_set_min_tls_protocol_version does not result in the crash. Only sec_protocol_options_set_local_identity.

Not using sec_protocol_options_set_local_identity results in the TCP socket being opened to the remote host followed by a Client Hello being sent via TLS 1.2, a Server Hello being sent back, and then a failure during certificate request phase of the TLS handshake. If the identity is set, the EXC_BAD_ACCESS occurs after the TCP socket is opened but before the Client Hello is sent out.

My guess it that something is hitting the EXC_BAD_ACCESS after the TCP handshake is completed and during the construction of the Client Hello that is to go out next. The log shows the following:

CoreFoundation`CFRetain:
    0x7ff8003f9494 <+0>:  pushq  %rbp
    0x7ff8003f9495 <+1>:  movq   %rsp, %rbp
    0x7ff8003f9498 <+4>:  pushq  %rbx
    0x7ff8003f9499 <+5>:  pushq  %rax
    0x7ff8003f949a <+6>:  testq  %rdi, %rdi
    0x7ff8003f949d <+9>:  je     0x7ff8003f94e2            ; <+78>
    0x7ff8003f949f <+11>: movq   %rdi, %rbx
    0x7ff8003f94a2 <+14>: jns    0x7ff8003f94ae            ; <+26>
    0x7ff8003f94a4 <+16>: movq   %rbx, %rax
    0x7ff8003f94a7 <+19>: addq   $0x8, %rsp
    0x7ff8003f94ab <+23>: popq   %rbx
    0x7ff8003f94ac <+24>: popq   %rbp
    0x7ff8003f94ad <+25>: retq   
->  0x7ff8003f94ae <+26>: movq   0x8(%rbx), %rdi
    0x7ff8003f94b2 <+30>: shrl   $0x8, %edi
    0x7ff8003f94b5 <+33>: andl   $0x3ff, %edi              ; imm = 0x3FF 
    0x7ff8003f94bb <+39>: movq   %rbx, %rsi  <- EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
    0x7ff8003f94be <+42>: callq  0x7ff8003f8e99            ; CF_IS_OBJC
    0x7ff8003f94c3 <+47>: movq   %rbx, %rdi
    0x7ff8003f94c6 <+50>: testb  %al, %al
    0x7ff8003f94c8 <+52>: je     0x7ff8003f94d5            ; <+65>
    0x7ff8003f94ca <+54>: addq   $0x8, %rsp
    0x7ff8003f94ce <+58>: popq   %rbx
    0x7ff8003f94cf <+59>: popq   %rbp
    0x7ff8003f94d0 <+60>: jmp    0x7ff800528fae            ; symbol stub for: objc_retain
    0x7ff8003f94d5 <+65>: xorl   %esi, %esi
    0x7ff8003f94d7 <+67>: addq   $0x8, %rsp
    0x7ff8003f94db <+71>: popq   %rbx
    0x7ff8003f94dc <+72>: popq   %rbp
    0x7ff8003f94dd <+73>: jmp    0x7ff8003f9aca            ; _CFRetain
    0x7ff8003f94e2 <+78>: callq  0x7ff800521daa            ; CFRetain.cold.1
    0x7ff8003f94e7 <+83>: xorl   %eax, %eax
    0x7ff8003f94e9 <+85>: jmp    0x7ff8003f94a7            ; <+19>

Will continue to try to figure out what's going on.

Further update:

In all cases below, setting the identity using sec_protocol_options_set_local_identity results in the aforementioned EXC_BAD_ACCESS during what looks like a CFRetain occurring within code outside of my control. This crash occurs AFTER a successful TCP [SYN], [SYN, ACK], [ACK] handshake between the client and the remote host but before sending a Client Hello to the remote host.

I've tried removing all nw_release and CFRelease calls within the entire package to allow any and all memory leaks to prevent decrementing references under my control and the EXC_BAD_ACCESS is still being hit.

I've tried retrieving the identity from just outside the TLS options block as well as within the TLS options block and used the identity and run into the same issue in those situations.

I have in all variations extracted the certificate and private key from the identity being used just prior to calling sec_protocol_options_set_local_identity and they are both valid and the expected private key and certificate pair.

I have tried using the sec_options that is being used with sec_protocol_options_set_local_identity without setting the identity and see the changes to sec_options reflected appropriately in the incoming/outgoing packets. When identity is not used, the TCP handshake successfully completes followed by an outbound Client Hello, an inbound Server Hello along with a Certificate, Server Key Exchange, and Certificate Request. The Client sends an ack for the Server Hello and then sends the requested information in the next packet. This packet does not contain a Certificate as one hasn't been set with identity. The following ack from the server closes the connection due to a failed handshake.

I have tried setting up the challenge block and when I call complete(identity) from within the same crash occurs.

This all leads me to believe something is wrong within the Apple Network Framework's handling of the provided Identity. I will attempt to use the same framework but with a PKCS12 file to provide the certificate and key creating the identity instead of a separate private key and certificate being added to the keychain (which successfully creates an identity that can be copied).

Question at this point is whether there is anywhere I can submit a ticket or request investigation into this being a potential bug that needs fixing. The repro steps appear to be as simple as adding a private key and certificate separately into the keychain, then retrieving them as an identity and setting them to the sec_options using sec_protocol_options_set_local_identity and then trying to establish a new connection using Mutual TLS.

I'll report back once I have attempted to setup and use PKCS12 within the same framework I am testing the above on. Thanks!

Well, PKCS#12 appears to also not be working and crashing in the exact same fashion. I am successfully importing a PKCS12 file with password into the keychain and getting an identity with the following:

int aws_secitem_import_pkcs12(
    CFAllocatorRef cf_alloc,
    const struct aws_byte_cursor *pkcs12_cursor,
    const struct aws_byte_cursor *password,
    SecIdentityRef *secitem_identity) {

    int result = AWS_OP_ERR;
    CFArrayRef items = NULL;
    CFDataRef pkcs12_data = NULL;
    pkcs12_data = CFDataCreate(cf_alloc, pkcs12_cursor->ptr, pkcs12_cursor->len);
    CFStringRef password_ref = NULL;
    if (password->len) {
        password_ref = CFStringCreateWithBytes(
            cf_alloc,
            password->ptr,
            password->len,
            kCFStringEncodingUTF8,
            false);
    } else {
        password_ref = CFSTR("");
    }

    CFMutableDictionaryRef dictionary = CFDictionaryCreateMutable(cf_alloc, 0, NULL, NULL);
    CFDictionaryAddValue(dictionary, kSecImportExportPassphrase, password_ref);

    OSStatus status = SecPKCS12Import(pkcs12_data, dictionary, &items);

    if (status != errSecSuccess || CFArrayGetCount(items) == 0) {
        AWS_LOGF_ERROR(AWS_LS_IO_PKI, "Failed to import PKCS#12 file with OSStatus:%d", (int)status);
        result = aws_raise_error(AWS_ERROR_SYS_CALL_FAILURE);
        goto done;
    }

    // Extract the identity from the first item in the array
    // identity_and_trust does not need to be released as it is not a copy or created CF object.
    CFDictionaryRef identity_and_trust = CFArrayGetValueAtIndex(items, 0);
    *secitem_identity = (SecIdentityRef)CFDictionaryGetValue(identity_and_trust, kSecImportItemIdentity);

    // Retain the identity for use outside this function
    if (*secitem_identity != NULL) {
        CFRetain(*secitem_identity);
        AWS_LOGF_INFO(
        AWS_LS_IO_PKI,
        "static: Successfully imported identity into SecItem keychain.");
    } else {
        status = errSecItemNotFound;
        AWS_LOGF_ERROR(AWS_LS_IO_PKI, "Failed to retrieve identity from PKCS#12 with OSStatus %d", (int)status);
        goto done;
    }

    result = AWS_OP_SUCCESS;

done:
    //cleanup
    if (pkcs12_data) CFRelease(pkcs12_data);
    if (dictionary) CFRelease(dictionary);
    if (password_ref) CFRelease(password_ref);
    if (items) CFRelease(items);
    return result;
}

Then using the SecIdentityRef in the same way as before resulting in the identical EXC_BAD_ACCESS crash.

Getting a backtrace on the crash I'm getting the following:

* thread #9, queue = 'com.apple.network.connections', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
  * frame #0: 0x00007ff8003f94ae CoreFoundation`CFRetain + 26
    frame #1: 0x00007ff807459c91 libboringssl.dylib`boringssl_identity_create_from_identity + 70
    frame #2: 0x00007ff80747a4a0 libboringssl.dylib`boringssl_context_set_identity + 326
    frame #3: 0x00007ff80740d756 libboringssl.dylib`__boringssl_session_apply_protocol_options_for_transport_block_invoke + 2527
    frame #4: 0x00007ff807b955cd Network`nw_protocol_options_access_handle + 93
    frame #5: 0x00007ff80740cd49 libboringssl.dylib`boringssl_session_apply_protocol_options_for_transport + 179
    frame #6: 0x00007ff8074186f0 libboringssl.dylib`nw_protocol_boringssl_begin_connection + 1255
    frame #7: 0x00007ff807415b58 libboringssl.dylib`nw_protocol_boringssl_connected + 356
    frame #8: 0x00007ff808086728 Network`invocation function for block in nw_socket_init_socket_event_source(nw_socket*, unsigned int) + 1720
    frame #9: 0x000000010671162c libclang_rt.asan_iossim_dynamic.dylib`__wrap_dispatch_source_set_event_handler_block_invoke + 204
    frame #10: 0x00000001065079f7 libdispatch.dylib`_dispatch_client_callout + 8
    frame #11: 0x000000010650ac04 libdispatch.dylib`_dispatch_continuation_pop + 812
    frame #12: 0x0000000106520a2d libdispatch.dylib`_dispatch_source_invoke + 2228
    frame #13: 0x00000001065116d4 libdispatch.dylib`_dispatch_workloop_invoke + 872
    frame #14: 0x000000010651d76e libdispatch.dylib`_dispatch_root_queue_drain_deferred_wlh + 318
    frame #15: 0x000000010651cb69 libdispatch.dylib`_dispatch_workloop_worker_thread + 590
    frame #16: 0x0000000105ca8b84 libsystem_pthread.dylib`_pthread_wqthread + 327
    frame #17: 0x0000000105ca7acf libsystem_pthread.dylib`start_wqthread + 15

It looks like the identity is probably the thing that is trying to be retained but I can't be certain. Will have to keep digging but hoping this may be something someone has some familiarity with.

Question at this point is whether there is anywhere I can submit a ticket or request investigation into this being a potential bug that needs fixing.

Sure. See my Bug Reporting: How and Why? post for my advice on that.

However, I’ve used this stuff myself and my experience is that it works just fine. I suspect there’s something wrong with the way you’re using the API from C, but that requires some serious effort to prove.


Let’s start with curl. Compare these two requests:

% curl -D /dev/stderr https://client-cert-missing.badssl.com                                    
HTTP/1.1 400 Bad Request
…

<html>
…
<center>No required SSL certificate was sent</center>
…
</html>
% curl -D /dev/stderr -E badssl.com-client.pem:badssl.com https://client-cert-missing.badssl.com
HTTP/1.1 400 Bad Request
…

<html>
…
<center>The SSL certificate error</center>
…
</html>

In both cases the response is 400 Bad Request, but in the absence of a client identity you get the very characteristic No required SSL certificate was sent error.

Note It’s annoying that the request doesn’t succeed in this case. Sadly, BadSSL seems to be suffering from bitrot these days. If you have a preferred site for testing client identity stuff, please do share!


Now let’s repeat this with Network framework. I’ve included the code for a command-line tool in a subsequent post. When I comment out the sec_protocol_options_set_local_identity, I see this:

connection will start
connection did start
connection did change state, new: preparing
connection did change state, new: ready
connection did send
connection did receive bytes, data: HTTP/1.1 400 Bad Request
…
<center>No required SSL certificate was sent</center>
…
connection did receive EOF
connection did receive failure, error: POSIXErrorCode(rawValue: 96): No message available on STREAM

IMPORTANT For this to work the BadSSL client identity must be present in your Mac keychain. Oh, and the secCall(…) stuff is from this post.

When I uncomment sec_protocol_options_set_local_identity I see this:

connection will start
connection did start
connection did change state, new: preparing
connection did change state, new: ready
connection did send
connection did receive bytes, data: HTTP/1.1 400 Bad Request
…
<center>The SSL certificate error</center>
…
connection did receive EOF
connection did receive failure, error: POSIXErrorCode(rawValue: 96): No message available on STREAM

That seems to accurately reflect the curl behaviour.


And with that working, I ported the code to Objective-C ARC. Again, I put the code in a subsequent post, just to keep the size of this post under control.

This code behaves exactly the same as the Swift code. No crashes, and commenting out sec_protocol_options_set_local_identity results in the same change in behaviour.


I suspect that your code is failing because it’s pure C, which is more like manual retain-release (MRR) Objective-C. I don’t have time today to port my code to MRR, but doing that may reveal the source of your crash. Keep in mind that you can port to MRR on a file-by-file basis, so it’s feasible to do this stuff piecemeal.

My best guess right now is that either you or Network framework has an MRR bug. Tracking those down is ‘fun’. Some general suggestions:

  • Run the static analyser, which will often highlight MRR bugs.

  • Turn on zombies, which might reveal at least the type of object involved.

Share and Enjoy

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

Here’s my Swift test code:

import Foundation
import Network

func clientIdentityNamed(_ name: String) throws -> SecIdentity {
    let identities = try! secCall { SecItemCopyMatching([
        kSecClass: kSecClassIdentity,
        kSecMatchLimit: kSecMatchLimitAll,
        kSecReturnRef: true,
    ] as NSDictionary, $0) } as! [SecIdentity]
    
    let identity = try identities.first { identity in
        let cert = try secCall { SecIdentityCopyCertificate(identity, $0) }
        let certName = try secCall { SecCertificateCopySubjectSummary(cert) } as String
        return certName == name
    }
    guard let identity else {
        throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecItemNotFound), userInfo: nil)
    }
    return identity
}

func main() {
    print("connection will start")
    
    let ident = try! clientIdentityNamed("BadSSL Client Certificate")

    let opt = NWProtocolTLS.Options()
    let secOpt = opt.securityProtocolOptions
    let secIdent = sec_identity_create_with_certificates(ident, [] as NSArray)!
    sec_protocol_options_set_local_identity(secOpt, secIdent)
    let params = NWParameters(tls: opt)

    let host = "client-cert-missing.badssl.com"
    let connection = NWConnection(to: .hostPort(host: .name(host, nil), port: 443), using: params)
    connection.stateUpdateHandler = { newState in
        print("connection did change state, new: \(newState)")
    }
    let request = """
        GET / HTTP/1.1\r
        Host: \(host)\r
        Connection: close\r
        \r
        \r\n
        """

    connection.send(content: Data(request.utf8), completion: .contentProcessed({ error in
        if let error {
            print("connection did not send, error: \(error)")
        } else {
            print("connection did send")
        }
    }))
    
    func receive() {
        connection.receive(minimumIncompleteLength: 1, maximumLength: 2048) { data, _, isComplete, error in
            if let data {
                let str = String(data: data, encoding: .utf8) ?? "\(data)"
                print("connection did receive bytes, data: \(str)")
            }
            if isComplete {
                print("connection did receive EOF")
            }
            if let error {
                print("connection did receive failure, error: \(error)")
                return
            }
            receive()
        }
    }
    receive()
    
    connection.start(queue: .main)
    print("connection did start")
    
    withExtendedLifetime(connection) {
        dispatchMain()
    }
}

main()

Share and Enjoy

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

And here’s the Objective-C ARC equivalent:

@import Foundation;
@import Network;

static SecIdentityRef clientIdentityNamed(NSString * name) {
    CFTypeRef copyResult = NULL;
    OSStatus err = SecItemCopyMatching( (__bridge CFDictionaryRef) @{
        (__bridge NSString *) kSecClass: (__bridge NSString *) kSecClassIdentity,
        (__bridge NSString *) kSecMatchLimit: (__bridge NSString *) kSecMatchLimitAll,
        (__bridge NSString *) kSecReturnRef: @YES,
    }, &copyResult);
    if (err != errSecSuccess) { return NULL; }
    NSArray * identities = CFBridgingRelease( copyResult );
    
    for (id identityNS in identities) {
        SecIdentityRef identity = (__bridge SecIdentityRef) identityNS;
        SecCertificateRef cert = NULL;
        err = SecIdentityCopyCertificate(identity, &cert);
        if (err != errSecSuccess) { return NULL; }
        CFAutorelease(cert);
        
        NSString * certName = CFBridgingRelease( SecCertificateCopySubjectSummary(cert) );
        if (certName == NULL) { return NULL; }
        
        if ([certName isEqual:name]) {
            CFAutorelease( CFRetain(identity) );
            return identity;
        }
    }
    return NULL;
}

static void receive(nw_connection_t connection) {
    nw_connection_receive(connection, 1, 2048, ^(dispatch_data_t data, nw_content_context_t context, bool is_complete, nw_error_t error) {
        #pragma unused(context)
        if (data != NULL) {
            NSData * dataNS = (NSData *) data;
            NSString * str = [[NSString alloc] initWithData:dataNS encoding:NSUTF8StringEncoding];
            if (str == nil) {
                str = dataNS.debugDescription;
            }
            fprintf(stderr, "connection did receive bytes, data: %s\n", str.UTF8String);
        }
        if (is_complete) {
            fprintf(stderr, "connection did receive EOF\n");
        }
        if (error != NULL) {
            fprintf(stderr, "connection did receive failure, error: %s\n", error.debugDescription.UTF8String);
            return;
        }
    });
}

int main(int argc, char **argv) {
    #pragma unused(argc)
    #pragma unused(argv)
    
    fprintf(stderr, "connection will start\n");
    SecIdentityRef ident = clientIdentityNamed(@"BadSSL Client Certificate");
    if (ident == NULL) { abort(); }

    nw_parameters_t params = nw_parameters_create_secure_tcp(^(nw_protocol_options_t tlsOptions) {
        sec_protocol_options_t secOptions = nw_tls_copy_sec_protocol_options(tlsOptions);

        sec_identity_t secIdent = sec_identity_create_with_certificates(ident, (__bridge CFArrayRef) @[]);
        sec_protocol_options_set_local_identity(secOptions, secIdent);
    }, NW_PARAMETERS_DEFAULT_CONFIGURATION);
    
    const char * host = "client-cert-missing.badssl.com";
    nw_endpoint_t endpoint = nw_endpoint_create_host(host, "443");
    nw_connection_t connection = nw_connection_create(endpoint, params);
    nw_connection_set_state_changed_handler(connection, ^(nw_connection_state_t state, nw_error_t error) {
        #pragma unused(error)
        fprintf(stderr, "connection did change state, new: %d\n", (int) state);
    });

    char * request = NULL;
    asprintf(
        &request,
        "GET / HTTP/1.1\r\n"
        "Host: %s\r\n"
        "Connection: close\r\n"
        "\r\n"
        "\r\n",
        host
    );
    dispatch_data_t requestData = dispatch_data_create(request, strlen(request), nil, nil);
    nw_connection_send(connection, requestData, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t  _Nullable error) {
        if (error != nil) {
            fprintf(stderr, "connection did not send, error: %s\n", error.debugDescription.UTF8String);
        } else {
            fprintf(stderr, "connection did send\n");
        }
    });

    receive(connection);

    nw_connection_set_queue(connection, dispatch_get_main_queue());
    nw_connection_start(connection);
    fprintf(stderr, "connection did start\n");
    
    dispatch_main();
    return EXIT_SUCCESS;
}

Share and Enjoy

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

Thanks for continuing down this rabbit hole with me.

I have set up the bare-bones test that you suggested and when using Native C instead of Objective C, I am getting "Segmentation fault: 11" at the exact same point I was observing the EXC_BAD_ACCESS before. (Code below)

When using Objective C, the identity appears to be passed along with no crash/segfault as expected. Unless there's something additional I'm doing wrong in the pretty straightforward native C code, I think the issue is with the Network framework... Which is truly unfortunate because that means even if it gets fixed, it's not useful for us because we cannot force the minimum version to be the latest release of the framework.

If you have the bandwidth, maybe you can reproduce the Native C issue on your end to confirm. Next step for me will be to investigate whether there's any way I can set up a more concrete way than CFRetain to manage the identity before passing it to the sec_options that will prevent the segfault. I will also need to look into what you meant by:

Keep in mind that you can port to MRR on a file-by-file basis, so it’s feasible to do this stuff piecemeal.

Other alternatives may be to somehow create a bridge that will allow the TLS handshake or parameter creation portion of the connection to Objective C and using those with the Native C Network Framework socket... Though I'm not sure that's really feasible.

Here is the Native C code implementation of what you provided:

// Function to get the client identity from the keychain
static SecIdentityRef clientIdentityNamed(const char *name) {
    CFTypeRef copyResult = NULL;
    const void *keys[] = {kSecClass, kSecMatchLimit, kSecReturnRef};
    const void *values[] = {kSecClassIdentity, kSecMatchLimitAll, kCFBooleanTrue};

    CFDictionaryRef query = CFDictionaryCreate(NULL, keys, values, 3, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    OSStatus err = SecItemCopyMatching(query, &copyResult);
    CFRelease(query);

    if (err != errSecSuccess) {
        return NULL;
    }

    CFArrayRef identities = (CFArrayRef)copyResult;

    for (CFIndex i = 0; i < CFArrayGetCount(identities); i++) {
        SecIdentityRef identity = (SecIdentityRef)CFArrayGetValueAtIndex(identities, i);
        SecCertificateRef cert = NULL;
        err = SecIdentityCopyCertificate(identity, &cert);
        if (err != errSecSuccess) { return NULL; }

        CFStringRef certName = SecCertificateCopySubjectSummary(cert);
        CFRelease(cert);
        if (certName == NULL) { return NULL; }

        // Compare certificate name with provided name
        Boolean match = CFStringCompare(certName, CFStringCreateWithCString(NULL, name, kCFStringEncodingUTF8), 0) == kCFCompareEqualTo;
        CFRelease(certName);

        if (match) {
            CFRetain(identity);
            return identity;
        }
    }
    return NULL;
}

// receive func omitted because it doesn't get hit.

int main(int argc, const char *argv[]) {
    (void)argc;
    (void)argv;

    printf("connection will start\n");

    // Get the client identity
    SecIdentityRef ident = clientIdentityNamed("AWS IoT Certificate");
    if (ident == NULL) {
        printf("Failed to get client identity\n");
        return EXIT_FAILURE;
    }

    // Create network parameters for a secure TCP connection
    nw_parameters_t params = nw_parameters_create_secure_tcp(^(nw_protocol_options_t tls_options) {
        sec_protocol_options_t sec_options = nw_tls_copy_sec_protocol_options(tls_options);
        sec_protocol_options_set_local_identity(sec_options, ident);
    }, NW_PARAMETERS_DEFAULT_CONFIGURATION);

    // Create endpoint and connection
    const char *host = "client-cert-missing.badssl.com";
    nw_endpoint_t endpoint = nw_endpoint_create_host(host, "443");
    nw_connection_t connection = nw_connection_create(endpoint, params);

    // Set state change handler for the connection
    nw_connection_set_state_changed_handler(connection, ^(nw_connection_state_t state, nw_error_t error) {
        printf("connection did change state, new: %d\n", (int)state);
        if (error != NULL) {
            printf("connection error occurred\n");
        }
    });

    // Create an HTTP request and send it over the connection
    char *request = NULL;
    asprintf(&request, "GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", host);
    dispatch_data_t requestData = dispatch_data_create(request, strlen(request), NULL, DISPATCH_DATA_DESTRUCTOR_FREE);
    nw_connection_send(connection, requestData, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t error) {
        if (error != NULL) {
            printf("connection did not send, error occurred\n");
        } else {
            printf("connection did send\n");
        }
    });

    receive(connection);

    // Set the connection queue and start the connection
    nw_connection_set_queue(connection, dispatch_get_main_queue());
    nw_connection_start(connection);
    printf("connection did start\n");

    dispatch_main();
    return EXIT_SUCCESS;
}

Running the above results in the following logs:

connection will start
connection did start
connection did change state, new: 2
Segmentation fault: 11

Commenting out the setting of identity results in the following logs:

connection will start
connection did start
connection did change state, new: 2
connection did change state, new: 3
connection did send
connection did receive bytes: HTTP/1.1 400 Bad Request
Server: nginx/1.10.3 (Ubuntu)
Date: Mon, 09 Sep 2024 21:57:00 GMT
Content-Type: text/html
Content-Length: 262
Connection: close

<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.10.3 (Ubuntu)</center>
</body>
</html>

I also tried setting the challenge block instead of setting the identity at which point the logs showed that the challenge block was hit, and then the segfault occurs at the point where complete(ident) is called.

Accepted Answer

Thanks for the C conversion!

Note that you didn’t include the receive function, but that’s OK because I could reproduce the crash without it (-:

The issue is with this line:

sec_protocol_options_set_local_identity(sec_options, ident);

It needs to be this:

sec_identity_t secIdent = sec_identity_create(ident);
sec_protocol_options_set_local_identity(sec_options, secIdent);

SecIdentityRef and sec_identity_t are different things.

Oh, and just FYI, the static analyser complains about you leaking copyResult and the temporary CFString that you create from name. Both of these are easy to fix, but you may not need to fix them because they’re in clientIdentityNamed, which is just scaffolding.

Share and Enjoy

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

sec_identity_create was definitely the key to the segfaults!

It looks like the identity is now being set properly. Unsure if this resolves ALL the issues on the identity end of establishing a mutual TLS handshake but it's got the vibe of a light at the end of the tunnel.

Immediately following I started attempting to setup a certificate authority for use as it appears Amazon's CA isn't installed on iOS by default. Running into a host of verification failures that I'm working through on that end but it feels like a separate problem from this one, which hopefully is resolved once I get through this CA thing.

Thanks for all your help! I'm rotating on-call for a week but will be back at this task afterwards. May end up opening some more "issues" in the future as I try to wrap up this task.

Glad to hear you’re making progress.

Immediately following I started attempting to setup a certificate authority for use

I have some docs for that, but it might be better if I point you at this recent thread, where I spent a bunch of time helping someone with that exact topic.

Share and Enjoy

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

Using SecIdentityRef to create a TLS connection
 
 
Q