Checking signature in sandboxed network extension

Hi,


As suggested in the previous post, I want to check the code signature to prevent my XPC service in the Network Extension from unauthorized access, but my signature checking doesn't work in a sandboxed network extension.


Here is the minimal working example, which checks if the code itself has a trusted signature:


- (void)test {
  OSStatus ret;
  SecCodeRef mycode = NULL;
  SecRequirementRef myreq = NULL;
  CFErrorRef myerr = NULL;

  do {
    ret = SecRequirementCreateWithString(CFSTR("anchor trusted"), kSecCSDefaultFlags, &myreq);
    if (ret != errSecSuccess)
      break;
    ret = SecCodeCopySelf(kSecCSDefaultFlags, &mycode);
    if (ret != errSecSuccess)
      break;
    NSLog(@"validate start");
    ret = SecCodeCheckValidityWithErrors(mycode, kSecCSDefaultFlags, myreq, &myerr);
    NSLog(@"validate return=%d err=%@", ret, myerr);
  } while ((0));

  if (myerr) {
    CFRelease(myerr);
  }
  if (myreq) {
    CFRelease(myreq);
  }
  if (mycode) {
    CFRelease(mycode);
  }
}


This snippet works in sandboxed app and UN-sandboxed network extension. In a sandboxed network extension, however, it outputs validate return=-2147416000 err=Error Domain=NSOSStatusErrorDomain Code=-2147416000 "(null)" (CSSMERR_CSP_INVALID_CONTEXT_HANDLE)


After digging into the logs from system frameworks, I find following two lines by which I believe the error is related to sandboxing.

<Security`Security::MDSSession::LockHelper::obtainLock(char const*, int)> com.mycompany: (Security) [com.apple.securityd:mdslock] obtainLock: calling open(/private/var/db/mds/system/mds.lock)
<Security`Security::MDSSession::LockHelper::obtainLock(char const*, int)> com.mycompany: (Security) [com.apple.securityd:mdslock] obtainLock: open error 1

Is this a limitation in macOS system or I have to adjust my code for the sandbox in network extension?


Thanks in advance.

Replies

In this context, MDS stands for Module Directory Service, which is a subsystem within CSSM (part of the Security framework) that handles module registration [1]. It seems that MDS uses a lock file (

/private/var/db/mds/system/mds.lock
) to coordinate its efforts, and the App Sandbox is blocking access to that file.

What version of macOS are you testing this on?

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

[1] FYI, the source for this is part of Darwin.

Hi eskimo,


Thank you for your answer.

I forgot to mention that I tested on 10.15.2 (19C57) and 10.15.3 Beta (19D49f), the snippet didn't work on either of them (with sandbox enabled).

You know, sometimes you run a test and get results that are completely the opposite of what you expect. Most discombobulating.

Yesterday I put your code into a test app and ran it here. It works, as you’d expect. I then created a share extension and ran the code there. This worked as well, which was a little surprising but there’s always the chance that a share extension runs in a different context than a network extension. So, today, after confirming your target OS release, I put your code into a network extension. The weird thing is, it works there as well.

Specifically:

  1. I rewrote your code in Swift:

    func codeSignTest(log: OSLog) {
        os_log(.debug, log: log, "will start code signing test")
        var reqQ: SecRequirement? = nil
        var err = SecRequirementCreateWithString("anchor trusted" as NSString, [], &reqQ);
        guard err == errSecSuccess else {
            os_log(.debug, log: log, "code sign test failed, could not create requirements, err: %d", err)
            return
        }
        let req = reqQ!
    
    
    var codeQ: SecCode? = nil
    err = SecCodeCopySelf([], &amp;codeQ)
    guard err == errSecSuccess else {
        os_log(.debug, log: log, "code sign test failed, could not create code object, err: %d", err)
        return
    }
    let code = codeQ!
    
    
    var errorCFQ: Unmanaged&lt;CFError&gt;? = nil
    err = SecCodeCheckValidityWithErrors(code, [], req, &amp;errorCFQ)
    guard err == errSecSuccess else {
        let error = errorCFQ!.takeRetainedValue() as Error as NSError
        os_log(.debug, log: log, "code sign test failed, check validity failed, err: %d, error: %{public}@/%zd", err, error.domain, error.code)
        return
    }
    
    
    os_log(.debug, log: log, "did complete code signing test")
    }

    .

  2. I added it to my packet tunnel provider’s

    startTunnel(options:completionHandler:)
    method:
    override func startTunnel(options: [String : NSObject]? = nil, completionHandler: @escaping (Error?) -> Void) {
        os_log(.debug, log: self.log, "provider will start")
        codeSignTest(log: self.log)
        self.queue.async {
            …
            self.setTunnelNetworkSettings(settings) { errorQ in
                …
                completionHandler(nil)
                os_log(.debug, log: self.log, "provider did start")
            }
        }
    }

    .

  3. I ran it on 10.15.2.

  4. I saw the following in the system log:

    01:47:44.670844-0800 ProviderMac init
    01:47:44.915549-0800 ProviderMac provider will start
    01:47:44.915584-0800 ProviderMac will start code signing test
    01:47:44.928369-0800 ProviderMac did complete code signing test
    01:47:45.072653-0800 ProviderMac provider did start

    As you can see, everything worked just fine.

I have no idea why it’s working for me and failing for you (honestly, I was completely shocked by my result). Alas, I’ve run out of time to explore this further on DevForums. If you need more help with it, I recommend that you open a DTS tech support incident so that I can allocate more time to investigate.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi eskimo,


Really appreciate your time for troubleshooting. I tested my Objective-C code in Content Filter (Packet Filter) and your Swift example in Packet Tunnel, both of them worked without sandbox, but broke if I enabled the sandbox for network extension.


I think there might be a misconfiguration in the Xcode project or the test environment needs a reset. I will post an update if I find the root cause.

I am seeing a similar problem. SecCodeCheckValidityWithErrors fails in the system/network extension and it appears the mds.lock file is the problem.


The system extension is running as root (euid == 0) and within the depths of the Security framework (libsecurity_mds/lib/MDSSession.cpp to be exact), the file /private/var/db/mds/system/mds.lock is being accessed because of special-cased functionality for euid==0. However, the App Sandbox disallows access to the mds.lock file.


FB7644780

I have the same problem in macOS 10.15.4. Did you find a solution (or perhaps you had an answer from a DTS that you can share) ?

A developer opened a DTS tech support incident about this and that allowed me time to investigate it in depth. In short, I believe that this is a bug in the App Sandbox, one that’s triggered by the fact that system extensions run as root (r. 63976204).

Note This combination, something that’s sandboxed but also running as root, is quite unusual, which is why I wasn’t able to reproduce it earlier. Previously I tested:

  • The code running in a standalone tool — This worked because it was neither sandboxed nor running as root.

  • That tool running as root — This worked because it wasn’t sandboxed.

  • The code running in a shared extension — This worked because, while the code was sandboxed, it didn’t run as root.

  • The code running in an NE appex — Ditto.

Pending a fix, you can work around the bug by adding a temporary exception entitlement to your sysex:

<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
<array>
  <string>/private/var/db/mds/</string>
</array>

If you ship your sysex via the Mac App Store, make sure to include a note to App Review explaining that you need this temporary entitlement due to a bug, and then reference the bug number.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
Is the given workaround still necessary? Where within the entitlements plist structure does it go?

I added it at the top level and it had no apparent effect.

Is the given workaround still necessary?

The bug in question (r. 63976204) was fixed in macOS 11. The workaround is still necessary if your deployment target is lower than that.

Where within the entitlements plist structure does it go?

The top level of a .entitlements file is a dictionary, and this is another key pair to add to that dictionary.

I added it at the top level and it had no apparent effect.

Did you add it to .entitlements file of the app? Or that of the NE sysex? ’cause it needs to be in the latter.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Hmmm, my deployment target *is* macOS 11 and I added the key pair at the top level of the .entitlements file for the System Extension.

The errors I see are mostly 100001 and 100002 and seem to only happen immediately after the extension is loaded and then enabled by my controlling app. The flows that causes the issue look like this:

Code Block Handling new flow: 
        identifier = 653961C1-DD57-4D21-911F-FFDAAB85A5C6
        hostname = gateway.icloud.com
        sourceAppIdentifier = .com.apple.Notes
        sourceAppVersion = 4.8
        procPID = 541
        eprocPID = 0
        direction = outbound
        inBytes = 0
        outBytes = 0
        signature = 32:{length = 32, bytes = 0x6fc70082 f36f6a3f 06f2f743 9d080e85 ... 2c19f9f3 158a5fd3 }
        remoteEndpoint = 17.248.242.37:443
        remoteHostname = gateway.icloud.com
        protocol = 6
        family = 2
        type = 1
        procUUID = DBA793E1-FD3D-348E-BE25-18E5C8A0DFD4
        eprocUUID = 09F24272-54CB-3550-8826-D54C7A324D99


I was able to reproduce this 100% of the time. I only have to launch my app which loads and then enables the sysex.

When I quit Notes, the error doesn't occur. If I launch Notes after loading the system extension, the error occurs. It appears that Notes.app is properly signed although I note that it is in /System/Applications:
Code Block
codesign -d --requirements - /System/Applications/Notes.app
Executable=/System/Applications/Notes.app/Contents/MacOS/Notes
designated => identifier "com.apple.Notes" and anchor apple

although I note that it is in /System/Applications

Does this only affect system apps? Or are you seeing it with third-party apps installed in /Applications?

Share and Enjoy

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

Does this only affect system apps? Or are you seeing it with third-party apps installed in /Applications?

The only application that seems to have this issue is the Notes app. I have not seen any issue with third-party apps.


In our app, the workaround temporary exception suggested by Quinn

Pending a fix, you can work around the bug by adding a temporary exception entitlement to your sysex:

<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
<array>
 <string>/private/var/db/mds/</string>
</array>

has been rejected by Mac App Store app review with the comment

Value "/private/var/db/mds/" requests read-write access to read-only system location.

Our deployment target is 10.15 so a workaround is necessary despite the issue being fixed on BigSur. I referenced both Quinn's post as well as the bug number in the submission.

As advised, I tried to replace the above .read-write entitlement with a .read-only entitlement, but this does not seem sufficient to prevent the underlying bug. Could I get confirmation or clarification whether .read-write is indeed necessary?

(I was going to open a DTS incident but then reconsidered since the form directs you to check the developer forums first and, well, I knew of this forum thread right here :) )

TL;DR If you want to use the above entitlement in the app store, you will at least have to appeal the rejection (and it might still be turned down).

A quick summary for the sake of future developers reading this: We eventually opened a DTS case to discuss options as advised by app review. The discussion concluded that there are no good options without the above entitlement, the only fix that could be accepted into the app store under this policy being to embed needed trust roots in the code instead of accessing them through the OS.

We verified that narrowing the entitlement to just the lock file

<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
<array>
 <string>/private/var/db/mds/mds.lock</string>
</array>

is sufficient and this was confirmed by DTS, but this entitlement was likewise rejected by app review as requesting "read-write access to read-only system location".

As app review did not respond to requests to further clarify their policy, we verified that on BigSur a sandboxed app running as root (in this case a network extension) can write to the /private/var/db/mds/mds.lock file (and indeed even other files like /private/var/db/mds/mds-test.lock) and pointed this out to challenge the classification as "read-only system location". After further review, this didn't change the rejection outcome. We appealed the decision and let the case run for ten days without response other than an initial notification that it was being looked at.

Eventually, we were pressed by external deadlines - as you can see this has been dragging out for about a month now - and submitted the alternative workaround that embeds trust roots in the code, which I believes withdraws the appeal. Without the entitlement, our app has been accepted. Clearly, this is an inferior solution in terms of both maintenance and security, but for now it is our only option.