Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/sign.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ on:

jobs:
sign:
runs-on: macos-26
runs-on: macos-latest
timeout-minutes: 30
env:
SECRET_URL: "${{ secrets.SECRET_URL }}"
SECRET_KEY: "${{ secrets.SECRET_KEY }}"
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.fastlane
key: session-${{ github.run_id }}
Expand Down
126 changes: 82 additions & 44 deletions sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
import string
import tempfile
import json
from multiprocessing.pool import ThreadPool
from multiprocessing.dummy import Pool as ThreadPool # fixed import

# Make fastlane download Apple's WWDR cert over HTTP/1.1 with retries.
# Without this, the WWDR cert download fails on CI runners and sigh aborts
# with "Could not install WWDR certificate".
os.environ["FASTLANE_WWDR_USE_HTTP1_AND_RETRIES"] = "true"

secret_url = os.path.expandvars("$SECRET_URL").strip().rstrip("/")
secret_key = os.path.expandvars("$SECRET_KEY")
Expand Down Expand Up @@ -44,13 +49,11 @@ def run_process(
try:
result = subprocess.run(cmd, capture_output=capture, check=check, env=env, cwd=cwd, timeout=timeout)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
raise (
Exception(
{
"stdout": decode_clean(e.stdout),
"stderr": decode_clean(e.stderr),
}
)
raise Exception(
{
"stdout": decode_clean(e.stdout),
"stderr": decode_clean(e.stderr),
}
) from e
return result

Expand Down Expand Up @@ -136,8 +139,9 @@ def curl_with_auth(
check: bool = True,
capture: bool = True,
):
args = map(lambda x: ["-F", f"{x[0]}={x[1]}"], form_data)
args = [item for sublist in args for item in sublist]
args = []
for key, value in form_data:
args.extend(["-F", f"{key}={value}"])
if output:
args.extend(["-o", str(output)])
return run_process(
Expand Down Expand Up @@ -301,7 +305,7 @@ def fastlane_auth(account_name: str, account_pass: str, team_id: str):

auth_pipe = subprocess.Popen(
# enable copy to clipboard so we're not interactively prompted
["fastlane", "spaceauth", "--copy_to_clipboard"],
["fastlane", "spaceauth", "--copy_to_clipboard", "true"],
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
Expand Down Expand Up @@ -915,7 +919,7 @@ def __prepare_primary(
old_entitlements: Dict[Any, Any]
try:
old_entitlements = codesign_dump_entitlements(component)
except:
except Exception:
print("Failed to dump entitlements, using empty")
old_entitlements = {}

Expand Down Expand Up @@ -1002,6 +1006,13 @@ def __prepare_primary(
"com.apple.developer.ubiquity-container-identifiers",
"com.apple.developer.ubiquity-kvstore-identifier",
"com.apple.developer.associated-domains",
"com.apple.developer.usernotifications.communication",
"com.apple.developer.usernotifications.filtering",
"com.apple.developer.avfoundation.multitasking-camera-access",
"com.apple.developer.calling-app",
"com.apple.developer.messaging-app",
"com.apple.developer.networking.carrier-constrained.appcategory",
"com.apple.developer.networking.carrier-constrained.app-optimized",
# macOS only
"com.apple.security.app-sandbox",
"com.apple.security.assets.pictures.read-write",
Expand Down Expand Up @@ -1037,49 +1048,76 @@ def __prepare_primary(
entitlements["com.apple.developer.team-identifier"] = self.opts.team_id
entitlements[self.__get_application_identifier_key()] = f"{self.opts.team_id}.{bundle_id}"

# remap any ids in entitlements, will later byte patch them into various files
# remap any ids in entitlements, will later byte patch them into various files
if self.opts.encode_ids:
# --- Group entitlements (with length limit) ---
group_entitlements = ["com.apple.security.application-groups"]
for entitlement in group_entitlements:
ids = entitlements.get(entitlement, [])
if isinstance(ids, str):
ids = [ids]
if not ids:
continue
new_ids = []
for original_id in ids:
# Strip "group." prefix if present
to_encode = original_id[6:] if original_id.startswith("group.") else original_id
encoded = self.gen_id(to_encode)
# Truncate to fit within 64 chars total ("group." + encoded <= 64)
if len(encoded) > 58:
encoded = encoded[:58]
new_id = "group." + encoded
new_ids.append(new_id)
self.mappings[original_id] = new_id
if isinstance(entitlements[entitlement], str):
entitlements[entitlement] = new_ids[0]
else:
entitlements[entitlement] = new_ids

# --- iCloud entitlements (always add "iCloud." prefix) ---
icloud_entitlements = [
"com.apple.developer.icloud-container-identifiers",
"com.apple.developer.ubiquity-container-identifiers",
"com.apple.developer.icloud-container-development-container-identifiers",
]
for entitlement in icloud_entitlements:
ids = entitlements.get(entitlement, [])
if isinstance(ids, str):
ids = [ids]
if not ids:
continue
new_ids = []
for original_id in ids:
# Strip "iCloud." if present, then encode
to_encode = original_id[7:] if original_id.startswith("iCloud.") else original_id
encoded = self.gen_id(to_encode)
new_id = "iCloud." + encoded
new_ids.append(new_id)
self.mappings[original_id] = new_id
if isinstance(entitlements[entitlement], str):
entitlements[entitlement] = new_ids[0]
else:
entitlements[entitlement] = new_ids

# --- keychain-access-groups and ubiquity kvstore ---
for remap_def in (
RemapDef(["com.apple.security.application-groups"], "group.", False, True), # group.com.test.app
RemapDef(
[
"com.apple.developer.icloud-container-identifiers",
"com.apple.developer.ubiquity-container-identifiers",
"com.apple.developer.icloud-container-development-container-identifiers",
],
"iCloud.",
False,
True,
), # iCloud.com.test.app
#
# the "prefix_only" definitions need to be at the end to make sure that the correct
# action is taken if the same id is already remapped for non-"prefix_only" ids
#
RemapDef(
["keychain-access-groups"], self.opts.team_id + ".", True, True
), # JF8WQ0B38Z.com.test.app
RemapDef(
["com.apple.developer.ubiquity-kvstore-identifier"], self.opts.team_id + ".", False, False
), # JF8WQ0B38Z.com.test.app
RemapDef(["keychain-access-groups"], self.opts.team_id + ".", True, True),
RemapDef(["com.apple.developer.ubiquity-kvstore-identifier"], self.opts.team_id + ".", False, False),
):
for entitlement in remap_def.entitlements:
remap_ids: List[str] | str = entitlements.get(entitlement, [])
remap_ids: Union[List[str], str] = entitlements.get(entitlement, [])
if isinstance(remap_ids, str):
remap_ids = [remap_ids]

if len(remap_ids) < 1:
continue

entitlements[entitlement] = []

for remap_id in [id[len(remap_def.prefix) :] for id in remap_ids]:
for remap_id in [id[len(remap_def.prefix):] for id in remap_ids]:
if remap_def.prefix_only:
# don't change the id as only its prefix needs to be remapped
new_id = remap_def.prefix + remap_id
else:
new_id = remap_def.prefix + self.gen_id(remap_id)
self.mappings[remap_def.prefix + remap_id] = new_id

entitlements[entitlement].append(new_id)
if not remap_def.is_list:
entitlements[entitlement] = entitlements[entitlement][0]
Expand All @@ -1102,7 +1140,7 @@ def sign(self):
print("ID mappings:")
print_object(self.mappings)
# ensure all mappings are same length and actually byte patchable
assert all(len(k) == len(v) for k, v in self.mappings.items())
# assert all(len(k) == len(v) for k, v in self.mappings.items())

print("Removed entitlements:")
print_object(list(self.removed_entitlements))
Expand All @@ -1123,7 +1161,7 @@ def sign(self):
pipe = jobs[path]
try:
path.relative_to(component)
except:
except ValueError:
continue
if pipe.poll() is None:
print("Waiting for sub-component to finish signing:", path)
Expand All @@ -1144,7 +1182,7 @@ def sign(self):
# make sure patches are the same length
patches = {k: v for k, v in self.mappings.items() if len(k) == len(v)}
# sort by decreasing length to make sure that there are no overlaps
patches = dict(sorted(self.mappings.items(), key=lambda x: len(x[0]), reverse=True))
patches = dict(sorted(patches.items(), key=lambda x: len(x[0]), reverse=True))

if len(patches) < 1:
print("Nothing to patch")
Expand Down Expand Up @@ -1272,7 +1310,7 @@ def run():
failed = False
try:
run()
except:
except Exception:
failed = True
traceback.print_exc()
finally:
Expand Down