#!/usr/bin/env python3 """Extract signing entitlements from an Apple provisioning profile.""" from __future__ import annotations import argparse import plistlib import subprocess import sys from pathlib import Path PROFILE_REQUIRED_KEYS = { "application-identifier", "beta-reports-active", "com.apple.developer.team-identifier", "com.apple.developer.default-data-protection", "keychain-access-groups", } def decode_profile(path: Path) -> dict: commands = [ ["security", "cms", "-D", "-i", str(path)], ["openssl", "smime", "-inform", "DER", "-verify", "-noverify", "-in", str(path)], ["openssl", "cms", "-inform", "DER", "-verify", "-noverify", "-in", str(path)], ] errors: list[str] = [] for command in commands: try: proc = subprocess.run( command, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) except FileNotFoundError: continue if proc.returncode == 0 and proc.stdout: return plistlib.loads(proc.stdout) errors.append(f"{command[0]} exited {proc.returncode}: {proc.stderr.decode(errors='replace').strip()}") raise RuntimeError(f"unable to decode provisioning profile {path}: {'; '.join(errors)}") def load_entitlements_template(path: Path) -> dict: value = plistlib.loads(path.read_bytes()) if not isinstance(value, dict): raise RuntimeError(f"entitlements template is not a dictionary: {path}") return value def _has_build_setting(value: object) -> bool: if isinstance(value, str): return "$(" in value if isinstance(value, list): return any(_has_build_setting(item) for item in value) if isinstance(value, dict): return any(_has_build_setting(item) for item in value.values()) return False def _filter_list(template_value: list, profile_value: object) -> list: if not isinstance(profile_value, list): return template_value profile_set = set(profile_value) filtered = [item for item in template_value if item in profile_set] return filtered def filter_entitlements(profile_entitlements: dict, template_entitlements: dict) -> dict: filtered = { key: profile_entitlements[key] for key in PROFILE_REQUIRED_KEYS if key in profile_entitlements } for key, template_value in template_entitlements.items(): if key not in profile_entitlements: print( f"warning: entitlement {key} requested by template but absent from provisioning profile; omitting", file=sys.stderr, ) continue profile_value = profile_entitlements[key] if _has_build_setting(template_value): filtered[key] = profile_value elif isinstance(template_value, list): filtered[key] = _filter_list(template_value, profile_value) else: filtered[key] = template_value return dict(sorted(filtered.items())) def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("profile", type=Path) parser.add_argument("--output", "-o", type=Path, required=True) parser.add_argument( "--template", type=Path, help="Optional entitlements template used to filter profile entitlements for a target.", ) args = parser.parse_args(argv) profile = decode_profile(args.profile) entitlements = profile.get("Entitlements") if not isinstance(entitlements, dict): raise RuntimeError(f"profile has no Entitlements dictionary: {args.profile}") if args.template: entitlements = filter_entitlements(entitlements, load_entitlements_template(args.template)) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_bytes(plistlib.dumps(entitlements, fmt=plistlib.FMT_XML)) return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))