I have run into an interesting topic today.
So far, I have been under the impression that when I am using the setCodeSigningRequirement() function on an NSXPCConnection, I am completely removing any chance of receiving AND sending messages to untrusted XPC Peers.
However, I created a malicious replacement for my daemon, and I wanted to check if my application can still send and receive messages to it.
- I checked with
codesign --verifythat the replacement does NOT fulfil the code signing requirement. - I put a system log instruction in the malicious tool's XPC function.
When calling the XPC Peer, I expected to see:
XPC connection to <redacted> failed! [Error Domain=NSCocoaErrorDomain Code=4102 "The code signature requirement failed." UserInfo={NSDebugDescription=The code signature requirement failed.}]
and I did.
However, I also saw the system log from the malicious tool's XPC function.
Then, I checked all XPC documentation, and I found for the original C implementation - xpc_connection_set_peer_code_signing_requirement() - the following in the discussion section:
All messages received on this connection will be checked to ensure they come from a peer who satisfies the code signing requirement. For a listener connection, requests that do not satisfy the requirement are dropped. When a reply is expected on the connection and the peer does not satisfy the requirement
XPC_ERROR_PEER_CODE_SIGNING_REQUIREMENTwill be delivered instead of the reply.
(this is in xpc/connection.h)
which seems to align with the observed results. However, this is (embarassingly?) new for me, I would have never expected this, given how in my head pre-checking before any connection is made seems straightforward, even with public Apple SDK APIs:
- Grab a
SecCode(notSecStaticCode) object of the daemon (malicious or not). This is running code, so it cannot be substituted between the check and the outgoing message. - Perform validations on the
SecCodeobject in some form - on macOS 15.0+ it's pretty easy with LightweightCodeRequirement'sSecCodeCheckValidityWithProcessRequirement(). - Immediately drop the connection if the peer is untrusted, before any message is sent.
Am I overlooking something or making wrong assumptions here?
or
Am I right and this is something that I have to accept that's implemented less than ideally and I can perform above steps 1-3 myself and make a difference?
Thanks in advance!
I also saw the system log from the malicious tool's XPC function.
Right. That’s what I’d expect based on my understanding of how XPC works, but I agree that this is non-obvious |-:
Most folks don’t run into this because they don’t implement a client-authenticates-server check. Rather:
- The server authenticates the client.
- And the client sets the privileged flag (see this post) to ensure that the server is in the global namespace.
That second step is a reasonable level of security IMO. Keep in mind that it’s basically impossible for a non-privileged client to protect itself from a privileged server. The server could, for example, rewrite the client’s code to remove any checks that you implement in the client.
given how in my head pre-checking before any connection is made seems straightforward
Your confusion stems from the fact that XPC has a connection-oriented API but the underlying transport, Mach messaging, is not connection-oriented. So, you can’t run this check on the connection, because there isn’t one. Rather, you have to run this check on a received message. That received message includes an audit token in a message trailer, and it’s that audit token that the system checks.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"