Mac App's container changing when the app is signed

Context

I have an app that uses the container for two things: store a realm database, and store a log.txt file with some debugging logs for myself.

  • The realm database path is set via Realm.Configuration.defaultConfiguration
  • The log.txt file path is set with FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("log.txt")

I already have released and distributed the version 1 to a few users. Given the complexity around signing an app in Xcode, the app was released and distributed unsigned. And now I'm trying to release the version 2 signed, but here's when I'm getting this weird issue.

All unsigned apps (version 1, and the unsigned version 2) are using as container the path ~/Library/Containers/com.dgrcode.{appName}/Data. The signed version 2, however, is using ~/ as the container. So for example

  • log.txt is located at:

    • unsinged apps: ~/Library/Containers/com.dgrcode.{appName}/Data/Documents/log.txt.
    • signed app: ~/Documents/log.txt
  • Realm's default.realm file is located at:

    • unsigned apps: ~/Library/Containers/com.dgrcode.{appName}/Data/Library/Application Support/default.realm
    • signed app: I haven't found it yet 😓

The obvious problem is that any user from version 1 that installs version 2 signed, will start using a new database instead of continuing using the existing database. And obviously having my app's data spread through their ~/ directory is far from ideal.

Here's what I get when I run codesign -v -d on the first version (everything between {} has been redacted for clarity:

Executable={/path/to/app}
Identifier=com.dgrcode.{appName}
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=56564 flags=0x10002(adhoc,runtime) hashes=1757+7 location=embedded
Signature=adhoc
Info.plist entries=29
TeamIdentifier=not set
Runtime Version=13.3.0
Sealed Resources version=2 rules=13 files=2
Internal requirements count=0 size=12

Where I think the most relevant part is the Signature=adhoc and TeamIdentifier=not set.

Now I archive a version 2 of the app. I make a copy and I sign only one of them using codesign -v --sign. I package them inside a .dmg (not sure if this affects anything, but just in case), and check the .app inside each of the .dmg after mounting them.

Here's the result of codesign -v -d for each one:

  • unsigned:
    Executable={path/to/app}
    Identifier=com.dgrcode.{appName}
    Format=app bundle with Mach-O universal (x86_64 arm64)
    CodeDirectory v=20400 size=57452 flags=0x2(adhoc) hashes=1785+7 location=embedded
    Signature=adhoc
    Info.plist entries=31
    TeamIdentifier=not set
    Sealed Resources version=2 rules=13 files=4
    Internal requirements count=0 size=12
    
  • signed
    Executable={path/to/app}
    Identifier=com.dgrcode.{appName}
    Format=app bundle with Mach-O universal (x86_64 arm64)
    CodeDirectory v=20400 size=57335 flags=0x0(none) hashes=1785+3 location=embedded
    Signature size=4798
    Signed Time=13 Nov 2023 at 12:17:24
    Info.plist entries=31
    TeamIdentifier=2W564BCY7Z
    Sealed Resources version=2 rules=13 files=4
    Internal requirements count=1 size=188
    

The unsigned app works as expected. When I open the unsigned app, it continues using the realm database in the previous location ~/Library/Containers/com.dgrcode.{appName}/Data, and I can see the log.txt update its content.

The signed app, however, doesn't use the same database, and is no longer writing to the log.txt file at ~/Library/Containers/com.dgrcode.{appName}/Data, but it's writing at ~/Documents/log.txt instead. It does use a database, but I have no clue where it is.


Questions

  1. How can I make the signed app use the path ~/Library/Containers/com.dgrcode.{appName}/Data/ as its container lcoation?

  2. How can something like this happen just by signing the .app?

Probably you are signing it with different entitlements. It looks like your unsigned app is sandboxed, but when you sign it you use an entitlements file with sandbox disabled (or no entitlements file at all).

Expanding on galad87 said…

If your app is accessing ~/Documents, it can’t be sandboxed. If your previous app was accessing a container, it must necessarily be sandboxed. Thus, your re-signing process has dropped the entitlements that enable the App Sandbox.

Now I archive a version 2 of the app. I make a copy and I sign only one of them using codesign -v --sign.

Why are you signing them manually? If you have an Xcode archive, and it’s a plain ol’ app, it’s best to click Distribute App and let Xcode take care of the signing for you. This will, amongst other things, preserve the entitlement that enables the App Sandbox.

I already have released and distributed the version 1 to a few users. Given the complexity around signing an app in Xcode, the app was released and distributed unsigned.

To be clear, once you do that there’s no hassle free forward path for your users, at least on macOS 14. macOS 14 tracks the identity of the app that owns each container so that it can warn the user if some other app tries to access it. There’s no way for the system to track the identity of ad hoc signed code, so they’re always going to see that warning.

Note For more on this feature, see the WWDC 2023 session I linked to from Trusted Execution Resources.

I haven’t explored this case in depth but I think that the only way out of it is for the user to manually delete the container. Alternatively, you could change the bundle ID of your app, which will give it a completely different container, and then offer a migration path for the users with this problem.

Share and Enjoy

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

Alright so I think my settings in Xcode would make the app not sandboxed, but because they were unsigned they are sandboxed by force. Does that make sense?

That would explain why after signing the app it's no longer sandboxed.

Why are you signing them manually?

I am signing manually with codesign -v --sign because somehow my Xcode is messing with my certificates (trying to solve it here).

To be clear, once you do that there’s no hassle free forward path for your users

What if I sandbox and sign my app? Would that continue using the same container but now with a signed app? That way my users can have both a signed app without security warnings and not lose their data

I've tested the option of sandboxing the app and signing it. I enabled app sandbox via Target -> Build Settings/Signing/Enable App Sandbox = Yes

Using this approach I've found the database used is the existing one from the unsigned previous version. This is true for the new version, both signed and unsigned.

The log.txt file however, is still created at ~/Documents for the new version signed, and ~/Library/Containers/com.dgrcode.{appName}/Data/Documents for the new version unsigned.

I'm guessing fixing that should be fairly easy, but I want to make sure this approach makes sense. Thoughts?

I think my settings in Xcode would make the app not sandboxed, but because they were unsigned they are sandboxed by force. Does that make sense?

I’m sorry but I can’t parse that sentence.

I am signing manually … because

Signing manually is tricky. If you’re going to do it, follow the process described in Creating Distribution-Signed Code for Mac.

I’ll reply to your other thread separately.

What if I sandbox and sign my app? Would that continue using the same container but now with a signed app?

I’ve not tested this but I don’t think so. Your original app was signed, it was just ad hoc signed. So, when the system created the app’s container, it associated it with that ad hoc signature. Now, some new code is coming along trying to access the container. It doesn’t have the same ad hoc signature, and so the system can’t tell that the user thinks that it’s the same app.

This works when you sign your app with a stable signing identity because the system can use the app’s code signature to verify that the new code is the same as the old code. I talk about this in general in TN3127 Inside Code Signing: Requirements, although in this specific case I think the system isn’t using the DR but rather just your Team ID.

Share and Enjoy

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

Mac App's container changing when the app is signed
 
 
Q