Passing NSURLCredential in XPC connection fail in decoder

Hi,

I’d like to perform client-side certificate authentication from https based connection in macOS.

I’m using the method didReceiveChallenge from URLSession. However, I cannot read the keychain directly since my process is running as Daemon, and my client certificate reside in login keychain.

So I've followed the guidance from this question https://developer.apple.com/forums/thread/106851, and sent this authentication request to a user-based process which is running in the current user so it has access to the keychain.

After I acquire the NSURLCredential object, I’d like to return it back to the Daemon, so it may run the completionHandler with that credential.

However, After I successfully create the NSURLCredential in the user process, and send it back using some reply callback. It looks like the object didn’t serialized properly and I get the following error :

Exception: decodeObjectForKey: Object of class "NSURLCredential" returned nil from -initWithCoder: while being decoded for key <no key>

Here’s my client side code ( I made sure that the server side create a valid NSURLCredential object). and the problem occur after I send the XPC request, right when i’m about to get the callback response (reply)

- (void)URLSession:(NSURLSession *)session
    didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
      completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { 
  if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate) {
    [myXpcService getCertIdentityWithAcceptedIssuers:challenge.protectionSpace.distinguishedNames
                                 withReply:^(NSURLCredential *cred, NSError *error) {
        if (error != nil) {
          completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
        } else {
          completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
        }
    }];
}

Perhaps anybody can tell me what did I do wrong here ? Does XPC is capable to pass complex objects like NSURLCredentials ?

thanks !

Replies

After I acquire the NSURLCredential object, I’d like to return it back to the Daemon, so it may run the completionHandler with that credential.

Sadly, this won’t work. An NSURLCredential object holding a client identity [1] cannot be passed between processes. Well, you can pass it, but the resulting credential won’t work.

This is the same fundamental issue that prevents client identity credentials working on background sessions, which is a significant pain point for iOS developers (r. 19181642).

As to workarounds, it kinda depends on how the client identity got into the login keychain. If you have control over that process, you can avoid the problem by having your client pass it to your daemon and having it import it into the system keychain.

Share and Enjoy

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

[1] Other types of credentials will work.

Luckily I control the user process code as well. This is how I create the NSURLCredentials in my user process

  NSURLCredential *certificateBasedCredential = [NSURLCredential credentialWithIdentity:identityRef
                                      certificates:certificateArray
                                      persistence:NSURLCredentialPersistenceForSession];

where certificateArray contains of the certificates from the chain (until the agreed self signed CA).

So your proposal is to simply pass this array of certificates to the Daemon so it can save them on the system keychain and use them when needed, right ?

But is it possible to pass reference objects from type SecCertificateRef on the XPC ? or should I use different type ?

Thanks !

Hi Eskimo, Perhaps you can advise about the right approach in this matter ?

I'm thinking about converting the SecCertificateRef objects in the certificate chain, into NSData type using method like SecCertificateCopyData. and convert them back to certificate type on the other side (the daemon side) before import each such certificate into the keychain.

Do you recommend this approach ?

thanks !

You are mixing up certificates and identities. For an explanation of that, see Certificate Signing Requests Explained > What’s This My Certificates Thing?.

The only parameter to +credentialWithIdentity:certificates:persistence: that really matters is the first one. This is a digital identity, and it matters because TLS Crypto Magic™ passes the certificate part of this to the remote peer with a guarantee that you hold the matching private key. See TLS for App Developers for more on this.

The problem here is not moving the certificate part; calling SecCertificateCopyData and moving that as NSData is just fine. Rather, the problem is the private key part. On macOS it’s almost certain that this private key was imported into the keychain. It’s also very likely that the import was done in such a way that the key is not extractable, that is, you can’t go from the SecKey object to its underlying bytes using routines like SecKeyCopyExternalRepresentation. That makes it impossible to move the private key via XPC [1].

Which brings me back to my earlier question: How did the client process import this digital identity into the login keychain?

Share and Enjoy

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

[1] This is exactly what trips up the XPC support in NSURLCredential.

Hi Eskimo, thanks for the thorough explanation.

To your question, my user signing certificate (leaf certificate with private key installed in login keychain) is installed so that you cannot export the private key (once trying to import the object to p12 file - I get the following error An error has occcurred. Unable to export an item... The contents of this item cannot be retrieved")

Basically, there's another software that deploy this certificate in the keychain, and I've no control over it (it's JAMF Self Service).

So in this case, what are my options to pass the digital identity to the Daemon process ?

Hi Eskimo, So if I understand you correctly, since my identity (which represent the private key) cannot be exported from the login keychain, therefore I cannot set it in system keychain.

In this case, what options do I left ? my main objective is to establish the https connection from the Daemon, and unfortunately, the certificate is installed only to the login keychain... maybe it's worth considering seteuid to the active user ?

thanks !

maybe it's worth considering seteuid to the active user ?

That probably won’t work and is definitely not supported. Switching execution contexts on macOS is not easy because of additional state above and beyond the traditional Unix-y state. For all the gory details, see the Execution Contexts section of Technote 2083 Daemons and Agents.

if I understand you correctly, since my identity (which represent the private key) cannot be exported from the login keychain, therefore I cannot set it in system keychain.

Correct.

what options do I left ? … the certificate digital identity is installed only to the login keychain

The question is, how did the identity get into the login keychain in the first place? That’s the best way to attack this problem. If you can change the import process, you can either import it into the system keychain the first time around, or import it into the login keychain so that the key is extractable.

Share and Enjoy

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

Hi Eskimo, So regarding the installation process of the certificate + privatekey in the login keychain, Sadly I don't have any control on this flow and I have to deal with non exportable private key in login keychain, and my goal is to be able to use it as part of client-side authentication in TLS.

After reading the links you showed me I think I have some workaround to the problem... Once I reach didReceiveChallenge call in the Daemon side, I will first execute the service's mach-o file but in user-mode process, in such a way that it will send dummy https request to the server. This will trigger the didReceiveChallenge once again but from the user-mode process, and now I can extract the identity easily and pass it to completion handler. This will likely present popup window that I should confirm using the private key for signing (see attached image)

  • Once the user press "always allow" then the request will be sent and we go back to the original Daemon now that this key is accessible for signing in this machine.

Now I don't need access to the window server to display any popups, since I already accepted the use of this private key. and If i'll do seteuid to the current active user. I can get the identityRef in the same way I did in the user-mode instance of this Daemon, and when I send it on the completionHandler, It's already have access to this key, so the whole process will go without any user interaction.

This flow is basically meant to bypass the Daemon failed attempt to allow keychain signing with the private key. since this isn't possible as the key reside in login keychain, and the Daemon cannot interact with the Windows server to triggers some popup windows.

it's relatively complicated solution, but I don't have other choice giving how the certificate was installed in the keychain.

Honestly, I don’t think this is a good approach. To start, stuff like this:

I will first execute the service's mach-o file but in user-mode process

and this:

I'll do seteuid to the current active user

is very worrying. macOS has execution context above and beyond the traditional Unix-y user ID stuff, and you can’t switch execution contexts easily [1].

My other concern relates to the availability of the user keychain. That’s a file in the user’s home directory, so it’s only guaranteed to be available if the user is logged in. If your daemon needs this digital identity while the user isn’t logged in, you may not be able to get it.

This approach triggers an authorisation alert, which is annoying but probably inevitable given this:

Sadly I don't have any control on this flow

However, if you’re OK with an authorisation alert then there is another option: Export the digital identity from the keychain, pass its serialised format to your daemon, and have the daemon re-import it into the System keychain.

Note that I misspoke early about extractability. In the file-based keychain most keys are extractable [2], but only with user-interaction. You can check this in Keychain Access: Select the digital identity, choose File > Export Items, and export as a .p12. If that works, you should be able to do the same using SecItemExport.

Share and Enjoy

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

[1] For a somewhat dated but still mostly OK explanation of this, see the Execution Contexts section of Technote 2083 Daemons and Agents.

[2] Some keys, most notably those on smart cards, can’t be extracted at all.

Hi Eskimo,

Unfortunately, when i follow your guidance to export the certificate I get the following message :

Does it means that the key is completely non-exportable ?

when i follow your guidance

My Keychain Access guidance? Or my SecItemExport guidance? I suspect that this alert is coming from the former, but I wanna be sure.

Share and Enjoy

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

  • Your suspicion is correct. I simply followed your steps : Select the digital identity, choose File > Export Items, and export as a .p12. Do you think that If I use the programmatic approach of SecItemExport, then I may get different results ?

Add a Comment

Do you think that If I use the programmatic approach of SecItemExport, then I may get different results ?

No. Well, more accurately, not better results. If you’re unable to export the key from Keychain Access, you won’t be able to export it programmatically.

Which brings us back to how this key is imported. Fixing that is, AFAICT, the only supported way forward here.

Share and Enjoy

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