Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correct signing order for macOS bundles #1910

Merged
merged 9 commits into from
Jul 25, 2024
Merged

Conversation

freakboy3742
Copy link
Member

Fixes #1891.

Corrects the order in which files are signed in a macOS app bundle.

This manifests if you sign an PySide app that includes PySide-Addons. This package contains a number of frameworks; these frameworks contain embedded applications. These applications must be signed before the library content of the framework, or the signing of the framework library fails with the error:

In subcomponent: .../QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app

The previous approach to signing used reverse lexical sorting; this was enough to guarantee that content in a folder would be signed before the folder; but it wouldn't guarantee that a file in a folder would be signed before subfolders in the same folder.

Using the PySide6 example, reverse lexical sorting would result in a sort order of:

.../Qt/lib/QtWebEngineCore.framework/Versions/A/QtWebEngineCore
.../Qt/lib/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess
.../Qt/lib/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app
.../Qt/lib/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess
.../Qt/lib/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app

Because "QtWebEngineCore" sorts alphabetically after "Helpers", and the sort order is reversed, QtWebEngineCore is signed before the QtWebEngineProcess.app, which causes the error.

The new sorting approach results in the signing order:

.../Qt/lib/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess
.../Qt/lib/QtWebEngineCore.framework/Helpers/QtWebEngineProcess.app
.../Qt/lib/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess
.../Qt/lib/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app
.../Qt/lib/QtWebEngineCore.framework/Versions/A/QtWebEngineCore

so QtWebEngineCore is signed after the app that uses it.

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

@mhsmith
Copy link
Member

mhsmith commented Jul 15, 2024

This package contains a number of frameworks; these frameworks contain embedded applications. These applications must be signed before the library content of the framework

Why?

or the signing of the framework library fails with the error:

In subcomponent: .../QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app

That doesn't look like an error. What's the actual problem? Based on the log in #1891, Briefcase has once again captured the stderr but decided not to show it to the user or even to log it (#1907, #1918):

│ ╭─────────────────────────────────────────────────────────────────────────────────── locals ───────────────────────────────────────────────────────────────────────────────────╮ │
│ │        args = [                                                                                                                                                              │ │
│ │               │   'codesign',                                                                                                                                                │ │
│ │               │                                                                                                                                                              │ │
│ │               PosixPath('/Users/user/tmp/beeware/pysidetest/build/pysidetest/macos/app/pysidetest.app/Contents/Resources/app_packages/PySide6/Qt/lib/QtWebEngineCore.framew… │ │
│ │               │   '--sign',                                                                                                                                                  │ │
│ │               │   '-',                                                                                                                                                       │ │
│ │               │   '--force',                                                                                                                                                 │ │
│ │               │   '--entitlements',                                                                                                                                          │ │
│ │               │   PosixPath('/Users/user/tmp/beeware/pysidetest/build/pysidetest/macos/app/Entitlements.plist')                                                              │ │
│ │               ]                                                                                                                                                              │ │
│ │       check = True                                                                                                                                                           │ │
│ │ filter_func = None                                                                                                                                                           │ │
│ │      kwargs = {'stderr': -1, 'stdout': -1, 'bufsize': 1}                                                                                                                     │ │
│ │     process = <Popen: returncode: 1 args: ['codesign', '/Users/user/tmp/beeware/pysidetest...>                                                                               │ │
│ │ return_code = 1                                                                                                                                                              │ │
│ │        self = <briefcase.integrations.subprocess.Subprocess object at 0x1031175f0>                                                                                           │ │
│ │      stderr = '/Users/user/tmp/beeware/pysidetest/build/pysidetest/macos/app/pysidetest.app/Con'+340                                                                         │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │

@freakboy3742
Copy link
Member Author

This package contains a number of frameworks; these frameworks contain embedded applications. These applications must be signed before the library content of the framework

Why?

The macOS docs on signing order say that you need to sign code "inside out - That is, if component A depends on component B, sign B before you sign A."

I'm not sure how it identifies this and fails on signing the binary, in this case, but it definitely is failing.

or the signing of the framework library fails with the error:

In subcomponent: .../QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app

That doesn't look like an error. What's the actual problem? Based on the log in #1891, Briefcase has once again captured the stderr but decided not to show it to the user or even to log it (#1907):

It might not look like an error, but that is the full output of a manual invocation of the code signing process.

If you attempt to sign QtWebEngineCore, you get an error:

(venv3.10) rkm@eunectes hellopyside % codesign '/Users/rkm/beeware/briefcase/local/hellopyside/build/hellopyside/macos/app/Hello Pyside.app/Contents/Resources/app_packages/PySide6/Qt/lib/QtWebEngineCore.framework/Versions/A/QtWebEngineCore' --sign - --force --entitlements /Users/rkm/beeware/briefcase/local/hellopyside/build/hellopyside/macos/app/Entitlements.plist
/Users/rkm/beeware/briefcase/local/hellopyside/build/hellopyside/macos/app/Hello Pyside.app/Contents/Resources/app_packages/PySide6/Qt/lib/QtWebEngineCore.framework/Versions/A/QtWebEngineCore: replacing existing signature
/Users/rkm/beeware/briefcase/local/hellopyside/build/hellopyside/macos/app/Hello Pyside.app/Contents/Resources/app_packages/PySide6/Qt/lib/QtWebEngineCore.framework/Versions/A/QtWebEngineCore: code object is not signed at all
In subcomponent: /Users/rkm/beeware/briefcase/local/hellopyside/build/hellopyside/macos/app/Hello Pyside.app/Contents/Resources/app_packages/PySide6/Qt/lib/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app
(venv3.10) rkm@eunectes hellopyside % echo $?
1

But, if you sign the app that uses the library first, it succeeds:

(venv3.10) rkm@eunectes hellopyside % codesign '/Users/rkm/beeware/briefcase/local/hellopyside/build/hellopyside/macos/app/Hello Pyside.app/Contents/Resources/app_packages/PySide6/Qt/lib/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess' --sign - --force --entitlements /Users/rkm/beeware/briefcase/local/hellopyside/build/hellopyside/macos/app/Entitlements.plist 
/Users/rkm/beeware/briefcase/local/hellopyside/build/hellopyside/macos/app/Hello Pyside.app/Contents/Resources/app_packages/PySide6/Qt/lib/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess: replacing existing signature
(venv3.10) rkm@eunectes hellopyside % codesign '/Users/rkm/beeware/briefcase/local/hellopyside/build/hellopyside/macos/app/Hello Pyside.app/Contents/Resources/app_packages/PySide6/Qt/lib/QtWebEngineCore.framework/Versions/A/QtWebEngineCore' --sign - --force --entitlements /Users/rkm/beeware/briefcase/local/hellopyside/build/hellopyside/macos/app/Entitlements.plist
/Users/rkm/beeware/briefcase/local/hellopyside/build/hellopyside/macos/app/Hello Pyside.app/Contents/Resources/app_packages/PySide6/Qt/lib/QtWebEngineCore.framework/Versions/A/QtWebEngineCore: replacing existing signature
(venv3.10) rkm@eunectes hellopyside % echo $?
0

src/briefcase/integrations/file.py Outdated Show resolved Hide resolved
src/briefcase/integrations/file.py Outdated Show resolved Hide resolved
src/briefcase/integrations/file.py Outdated Show resolved Hide resolved
src/briefcase/platforms/macOS/__init__.py Outdated Show resolved Hide resolved
Comment on lines 612 to 615
# documentation for groupby() says that a new break is created every time a new
# group is found in the input data; sorting the input in reverse order ensures
# that only one group is found per folder, and that the deepest folder is found
# first.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it doesn't guarantee that subfolders and files in the same folder are put into separate groups. For example, if there was no Helpers directory, the group separation would be:

.../Qt/lib/QtWebEngineCore.framework/Versions/A/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess
---
.../Qt/lib/QtWebEngineCore.framework/Versions/A/QtWebEngineProcess.app
.../Qt/lib/QtWebEngineCore.framework/Versions/A/QtWebEngineCore

Because the last two paths have the same parent, they would be signed in parallel.

Possible solution: make the key for both sorting and grouping be (path.parent, path.is_dir()), and sort with reverse=True. Those two elements map directly onto the two requirements stated in the comment.

If the grouping was factored out into a utility function, it could be covered by a test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - I hadn't considered that although the sort order will put the directory first, if they're grouped together parallelism won't guarantee their signed in that order.

I've modified the grouping as you've described (well - close - the sorting already puts directories before files, so we just need a different grouping key, not a full reverse sort order).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIth reverse=True, I think the sorting and grouping key can both be (path.parent, path.is_dir()), and all the complexity of sorted_depth_first can be removed.

Copy link
Member

@mhsmith mhsmith Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ordering within the group shouldn't matter, but to make the build a bit more reproducible (and pass the tests), it would also be a good idea to sort within each group by the full paths.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key can't be exactly the same, because the sorting process needs to sort the actual files, whereas the grouping needs to sort on the containers of the files. So - we need to have a different key; the three-element key gives the sort order that matches a normal directory listings, so in any debug log it's going to be marginally less confusing. The 3-element key also matches the "complexity" of the actual search - sorting by "depth, then by directory vs file, then by filename" is literally what the algorithm is sorting on, rather than relying on a subtle edge effect of reverse interacting with the boolean value of is_dir().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so we can sort the whole list bykey=(path.parent, path.is_dir()), reverse=True, then group by the same key, then sort each group by path. That's still much easier to understand than the current version of sorted_depth_first, which creates keys which are lists of tuples.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless the file sorting contains the actual path in the key, the ordering of files/folders inside any given group will be at least partially unpredictable. This won't matter to the signing order, but it makes testing a little harder because we can't completely guarantee the order of the output.

It also seems a little silly to me have a sort method that doesn't fully sort. Plus, we're already sorting, so requiring a second pass of sorting (in the test harness, and potentially in the usage of the final groups) seems a little silly.

However, I can't argue that the simple tuple is much easier to understand than the list of tuples, even if the resulting order isn't quite a "lexically pleasing" as the more complex option; and we can fix the filename sorting by adding the actual path to the initial sort key (using the simpler 2 part key for the grouping key).

@freakboy3742
Copy link
Member Author

The test failure on Ubuntu is a mystery to me... flatpak packaging on Ubuntu failing after these changes makes no sense at all...

@rmartin16
Copy link
Member

rmartin16 commented Jul 17, 2024

The test failure on Ubuntu is a mystery to me... flatpak packaging on Ubuntu failing after these changes makes no sense at all...

The run for PySide6 on Linux was taking longer than 30 minutes....at which point, the run times out. For some reason, the Docker builds were taking significantly longer...

[edit]
I'm thinking this issue must have stemmed from degraded disk access. If you look at the timestamped logs for the run, the disk-heavy steps are taking a long time, for instance:

Fedora build:

2024-07-17T07:33:44.9298388Z Building source archive... started
2024-07-17T07:34:59.2274131Z Building source archive... done
2024-07-17T07:35:06.6574958Z Docker| Checking for unpackaged file(s): /usr/lib/rpm/check-files /app/rpmbuild/BUILDROOT/verifyapp-0.0.1-1.fc39.x86_64
2024-07-17T07:38:47.8996446Z Docker| Wrote: /app/rpmbuild/RPMS/x86_64/verifyapp-0.0.1-1.fc39.x86_64.rpm

Arch build:

2024-07-17T07:40:43.6228629Z �[2m[verifyapp]�[0m Building .pkg.tar.zst package...
2024-07-17T07:40:43.6236575Z Generating pkgbuild layout... started
2024-07-17T07:40:43.6246348Z Generating pkgbuild layout... done
2024-07-17T07:40:43.6253599Z Building source archive... started
2024-07-17T07:41:57.7478767Z Building source archive... done
2024-07-17T07:42:16.8779086Z Docker| ==> Creating package "verifyapp"...
2024-07-17T07:42:16.8814737Z Docker|   -> Generating .PKGINFO file...
2024-07-17T07:42:17.5688898Z Docker|   -> Generating .BUILDINFO file...
2024-07-17T07:42:17.6144095Z Docker|   -> Adding changelog file...
2024-07-17T07:42:17.6651082Z Docker|   -> Generating .MTREE file...
2024-07-17T07:42:18.5412639Z Docker|   -> Compressing package...
2024-07-17T07:44:45.2512467Z Docker| ==> Leaving fakeroot environment.
2024-07-17T07:44:45.2572968Z Docker| ==> Finished making: verifyapp 0.0.1-1 (Wed Jul 17 07:44:45 2024)

@freakboy3742
Copy link
Member Author

I've just worked out what is going on. Since this issue was revealed by signing PySide6-Addons, I added that package to the automation bootstrap for PySide6, so that the macOS backend would need to sign a full PySide6 install as a validation test.

But - PySide6 is a 137MB download zipped... and it will be installed on Ubuntu as well. So, the binaries on Linux just doubled in size. 🤦

We could fix this by increasing the timeout... but since we only need the signing validation on macOS, I think I might modify the bootstrap so that -addons is only a requirement on macOS.

@mhsmith mhsmith merged commit 5ee1a34 into beeware:main Jul 25, 2024
52 checks passed
@freakboy3742 freakboy3742 deleted the sign-order branch July 25, 2024 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Code Signing Error
3 participants