Code signing of dylib for use under iOS

We are in the process of developing our text to speech and speech analysis tools over to mobile platforms = cross platform development.

The tools are written in C++ and are compiled using CMake with an ios specific toolchain targetting the correct platform sdk.

One of the parts of this toolkit is the dynamic loading of language specific dylibs via dlopen.

I have seen that this can only be done if the dylib has been signed with the same certificate as the application.

Note that we are still using "free" developer certificates generated automatically by XCode.

When I run the test application, at the point where the dylib should be loaded via dlopen, the load fails and dlerror returns the following :

dlopen(<path to dylib>, 0x0001): code signature invalid (errno=1) sliceOffset=0x00000000, codeBlobOffset=0x0205D0F0, codeBlobSize=0x000453B0 for '<path to dylib>'

However, when I check the code signature with :

codesign -d --verbose=2 --extract-certificates <path>

I get the same certificate output from both the application bundle and the dylib in question.
For example :


Identifier=libnormaliser_fr
Format=Mach-O thin (arm64)
CodeDirectory v=20400 size=265332 flags=0x0(none) hashes=8286+2 location=embedded
Signature size=4755
Authority=Apple Development: <our apple id>
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CA
Signed Time=4 Jan 2021 at 09:27:14
Info.plist=not bound
TeamIdentifier=<our team id>
Sealed Resources=none
Internal requirements count=1 size=192

Now, when I run the command :

codesign --verify --deep --verbose=2

it outputs :

./libxxx.dylib: valid on disk
./libxxx.dylib: satisfies its Designated Requirement

I am testing on an iPad with iOS version 14.0.1, by the way.

So, something is still missing, but what ?
Can anyone help me with this please ?

Replies

iOS does not support ‘nak‍ed’ shared libraries (that is, .dylib files) [1]. If you want to create a shared library for iOS, you must package the code as a framework. This will mean adjusting your build system to wrap the resulting shared library in a .framework bundle. This is a bit tricky but not super hard. A good way to get started is to use Xcode to create a framework and then examine the resulting bundle to see what it did.

Finally, if you plan to distribute this shared library widely — for example, you’re building an SDK that you give to your customers — you should wrap this framework in an XCFramework. For more on that see WWDC 2019 Session 416 Binary Frameworks in Swift (despite the session’s title, the XCFramework parts of this talk also apply to non-Swift frameworks).

Share and Enjoy

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

[1] The only exception here is the Swift runtime libraries.
Thank you Quinn for your message.

However, unless I have missed something here, your answer would appear to be different to that which you gave 9 months ago here : https://developer.apple.com/forums/thread/130502, where you said that it was a problem of shipping a dylib as opposed to using dlopen to load it (which you said was not a problem).

So, just to make sure I have understood, wrapping our libraries into separate frameworks, one per language specific analysis code, will allow us to load and unload dynamically the language analysis code as needed?

If this is the case, where can I find instructions as to how to create such framework projects, please ?

Oh, one thing I forgot to mention is that the solution must work from the platform neutral c++ code - currently using dlopen.
Can you confirm that this is the case ?

So, just to make sure I have understood, wrapping our libraries into
separate frameworks, one per language specific analysis code, will
allow us to load and unload dynamically the language analysis code as
needed?

Last I checked iOS has no problem loading a framework dynamically using dlopen. Indeed, I just ran a test here in my office and it worked as expected. You can find my code at the end of this post.

IMPORTANT I can’t guarantee that this will work in a production app. There are some subtle differences in the runtime environment between Xcode and the App Store. While these don’t normally change things, it’s possible that it might in this case. Once you get your prototype up and running a I recommend that you publish a beta via TestFlight. If things work there you can be reasonably assured that they’ll work in the App Store.

The unloading side is more complex. Unloading a shared library is a challenge even on the Mac. It typically works for vanilla C code but:
  • Using Swift or Objective-C definitely prevents the library from unloading.

  • Using C++ may prevent the library from unloading, depending on how your code interacts with the system via C++’s One Definition Rule.

If this is the case, where can I find instructions as to how to create
such framework projects, please ?

In CMake? I’ve no idea, sorry.

In Xcode it’s as simple as New > Target > iOS > Framework. And that brings me back to my recommendation from yesterday:

A good way to get started is to use Xcode to create a framework and
then examine the resulting bundle to see what it did.

Finally, you wrote:

the solution must work from the platform neutral c++ code

As you can see I’m using dlopen and friends to do the heavy lifting here. You will, of course, need iOS-specific code to locate your framework.

Share and Enjoy

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



Code Block
- (void)testIsLoaded {
uint32_t imageCount = _dyld_image_count();
BOOL found = NO;
for (uint32_t i = 0; i < imageCount; i++) {
if (strstr(_dyld_get_image_name(i), "FFF") != nil) {
found = YES;
break;
}
}
NSLog(@"found: %d", (int) found);
}
- (void)load {
if (self.dl != nil) {
NSLog(@"already loaded");
return;
}
NSLog(@"will load");
NSURL * frameworksDir = [[NSBundle mainBundle] privateFrameworksURL];
NSURL * framework = [frameworksDir URLByAppendingPathComponent:@"FFF.framework"];
NSURL * image = [framework URLByAppendingPathComponent:@"FFF"];
self.dl = dlopen(image.fileSystemRepresentation, RTLD_LAZY);
if (self.dl == nil) {
NSLog(@"did not load, %s", dlerror());
return;
}
NSLog(@"did load");
}
- (void)call {
if (self.dl == nil) {
NSLog(@"not loaded");
return;
}
NSLog(@"will call");
void * s = dlsym(self.dl, "FFFMain");
if (s == nil) {
NSLog(@"did not call, no sym");
return;
}
int (*f)(int, int) = s;
int r = f(1, 2);
NSLog(@"did call, 1 + 2 = %d", r);
}
- (void)unload {
if (self.dl == nil) {
NSLog(@"not loaded");
return;
}
NSLog(@"will unload");
int err = dlclose(self.dl);
if (err < 0) {
NSLog(@"did not unload, %s", dlerror());
return;
}
self.dl = nil;
NSLog(@"did unload");
}

Thank you for all this.
I will be offline for at least the next week (operation on my hand), so will start implementing once back.
I'll keep this thread up to date afterwards.

operation on my hand

Best of luck with that.

Share and Enjoy

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

I have been searching for a more detailed example/tutorial of how to "package" a C++ shared library into a framework, as you suggested - with not much success.

Do I need to include all my C++ files in the project ?
Or, can I statically link the library into the framework ?

If the second option is valid, how would I expose the entry points ?

Hope you'll have some time to help me see more clearly.
Best regards

Do I need to include all my C++ files in the project ? Or, can I
statically link the library into the framework ?

I would avoid changing the parts of the build system that actually build your .dylib, but instead create a post-processing step that creates a .frameworkshell and copies the .dylib into that.

Which brings me back to my earlier recommendation: Don’t try to build this framework wrapper based on the documentation, but rather use Xcode to create a trivial framework. You can then look at what Xcode created and use that to guide your own wrapping process.

This process is much simpler than you might imagine. Consider the following framework, built with Xcode 12.4:

Code Block
% find MyFramework.framework
MyFramework.framework
MyFramework.framework/_CodeSignature
MyFramework.framework/_CodeSignature/CodeResources
MyFramework.framework/MyFramework
MyFramework.framework/Headers
MyFramework.framework/Headers/MyFramework.h
MyFramework.framework/Modules
MyFramework.framework/Modules/module.modulemap
MyFramework.framework/Info.plist


The Headers and Modules directories are irrelevant in this case because the client is calling the code via dlopen. You don’t need to worry about the _CodeSignature directory because it’s created by the code signing process and you can ship this unsigned (your client will need to re-sign it before shipping regardless). That leaves:
  • MyFramework.framework/MyFramework — This is a Mach-O shared library (MH_DYLIB), which is exactly what you already have in your .dylib. You can just move that into place.

  • MyFramework.framework/Info.plist — That’s pretty simple to grok. You can leave out all the DTxxx keys, because they are Xcode-specific things. And most of the other keys will be the same for all your products. The only things that really change across your various ‘frameworks’ are CFBundleIdentifier, CFBundleExecutable, and CFBundleName.

Share and Enjoy

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

Ok, I have managed to create the iOS Framework project and find where I should add the reference to my dylib.
I can do the build to create the framework structure you showed below.

My project is entitled, with a lot of originality ... test !
So the output structure is as follows :

Code Block
drwxr-xr-x 3 simon staff 96 1 fév 14:03 Frameworks
drwxr-xr-x 3 simon staff 96 1 fév 13:50 Headers
drwxr-xr-x 3 simon staff 96 1 fév 13:50 Modules
drwxr-xr-x 3 simon staff 96 1 fév 14:03 _CodeSignature
-rw-r--r--@ 1 simon staff 738 1 fév 14:03 Info.plist
-rwxr-xr-x 1 simon staff 68688 1 fév 14:03 test


Can you confirm that I call manually put the executable image test onto the iPad, point to it in the app and load it with dylib ?


Can you confirm that I call manually put the executable image test
onto the iPad, point to it in the app and load it with dylib ?

I think you’re the only one who can confirm that. Give it a try and let us know how you get along.

I can confirm that:
  • If the framework and the containing app are built correctly, you can dynamically load code from the framework (per my earlier post).

  • iOS does not care how you built the code, just that the code is built correctly. So there’s no theoretical obstacle to getting this working.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Some progress made.
I have found that I can successfully copy the framework to the apps Library directory, ensuring that file permissions stay the same (ie: read/write/execute permissions).

However, when trying to load the framework, I have discovered that my dylib has stored the build directory in the LC_ID_DYLIB entry.
Before :
Code Block
Load command 4
cmd LC_ID_DYLIB
cmdsize 104
name /Users/simon/DevProjects/speech-recognition/ios/lib/libnormaliser_fr.dylib (offset 24)
time stamp 1 Thu Jan 1 01:00:01 1970
current version 0.0.0
compatibility version 0.0.0

So, I proceeded to update it with :

Code Block
install_name_tool -id "@loader_path/Frameworks/libnormaliser_fr.dylib" libnormaliser_fr.dylib


After :
Code Block
Load command 4
cmd LC_ID_DYLIB
cmdsize 72
name @loader_path/Frameworks/libnormaliser_fr.dylib (offset 24)
time stamp 1 Thu Jan 1 01:00:01 1970
current version 0.0.0
compatibility version 0.0.0


Now, here is the strange effect - where XCode would generate the framework without any problem before, it now complains saying that the library does not contain bitcode !

I ran KDiff3 on the library before installnametool and after to see if it would remove anything.
I just changes what I requested.

What am I doing wrong ?
Was the framework signed before you ran install_name_tool against it? If so, you’ll need to re-sign it afterwards because its modifications will break the seal on the code signature.

Also, I thought you were loading the code dynamically using dlopen? If so, the LC_ID_DYLIB doesn’t matter (you should still fix it, but the wonky value won’t prevent you from calling it via dlopen).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
You are right, I am indeed calling it with dlopen, but it wasn't clear to me if I should call the dylib directly or via the frameworks executable.

Taking into account the framework structure :

Code Block
drwxr-xr-x 3 simon staff 96 4 fév 15:41 Frameworks <-- contains libnormaliser_fr.dylib
drwxr-xr-x 3 simon staff 96 4 fév 15:41 Headers
-rw-r--r-- 1 simon staff 763 4 fév 15:41 Info.plist
drwxr-xr-x 3 simon staff 96 4 fév 15:41 Modules
drwxr-xr-x 3 simon staff 96 4 fév 15:41 _CodeSignature
-rwxr-xr-x@ 1 simon staff 69296 4 fév 15:41 normaliser_fr

I was calling :

Code Block
dlopen(<app id path>/Library/normaliser_fr.framework/normaliser_fr);

Which was giving the error :

dependent dylib '/Users/simon/DevProjects/speech-recognition/ios/lib/libnormaliser_fr.dylib' not found


Perhaps you can clarify, please, if this correct, or should I call the dylib directly ?

Here are the steps I was taking :
  1. Build the dylib

  2. Copy it to the XCode framework project

  3. Build the framework, which handles the signing

It was between steps 1 & 2 that I tried to change the LC_ID_DYLIB entry - so before rebuilding and resigning.
It was at stage 3 that XCode complained.
Without doing the change, everything compiled correctly, having said that.

3. Build the framework

Hmmm, that’s not what I was aiming for. My suggestion was that you use Xcode to build a framework so that you can then study the structure of that framework and replicate it in your native build system. Your current approach seems to be including two copies of your library, one at the top level of your framework and one within Frameworks. This is bad in general (twice the code) but it also won’t work because iOS does not support nested frameworks.

Share and Enjoy

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

Thanks for your reply.
In fact, there is only one instance of my dylib, the one inside the Frameworks directory.
I have managed to fix the double problem of LC_ID_DYLIB and bit code complaints - it builds all correctly now.

However, when loading on the iPad, I am getting an error of code signature invalid - which indicates that the system is trying to load the correct stuff, at least.

Both the application and the framework are signed with the same team identifier.
However, they obviously do not have the same bundle identifier -
  • fr.teamexpression.irisa.normaliser-fr

  • fr.teamexpression.irisa.SeptemberDemo

Both projects are set to do automatic signing, ie XCode manages the generation
If I query the system for the signing certificates, I get:
Code Block
$ /usr/bin/env xcrun security find-identity -v -p codesigning
1) 7E5412AB42D2F6DC955FF8809079E33AAF3ADB8B "Apple Development: team-email@email.com (9KFL2H38SM)" (CSSMERR_TP_CERT_REVOKED)
2) 78037F819B5AA1522177B06DA8E7C5343DDBC0CF "Apple Development: team-email@email.com (9KFL2H38SM)"
2 valid identities found

Note : email address changed by security

So, logically speaking, only the second certificate would be used for both the application and the dylib.

Might you have any idea what is causing the security to choke on the dylib ?