#!/usr/bin/env python3 """Checks for a project's signing adapter; component policy stays in that project.""" import json import os import plistlib import re import subprocess import sys from pathlib import Path def bundle_info(app): with (app / 'Contents/Info.plist').open('rb') as stream: info = plistlib.load(stream) if not info.get('CFBundleIdentifier') or not info.get('CFBundleExecutable'): raise ValueError('App identifier and main executable must be present.') return info def preflight(app): info = bundle_info(app) main = app / 'Contents/MacOS' / info['CFBundleExecutable'] if not main.resolve().is_relative_to((app / 'Contents/MacOS').resolve()) or not main.is_file(): raise ValueError('Main executable is missing.') nested = any((app / 'Contents' / name).exists() and any((app / 'Contents' / name).iterdir()) for name in ['Frameworks', 'PlugIns', 'XPCServices', 'Helpers']) magic = {bytes.fromhex(value) for value in ['feedface', 'cefaedfe', 'feedfacf', 'cffaedfe', 'cafebabe', 'bebafeca', 'cafebabf', 'bfbafeca']} for path in app.rglob('*'): if path.is_file() and path.resolve() != main.resolve(): with path.open('rb') as stream: if stream.read(4) in magic: nested = True if nested and not os.getenv('NESTED_SIGN_SCRIPT'): raise ValueError('Nested code requires the project NESTED_SIGN_SCRIPT with component-specific entitlements.') def identity(path): proc = subprocess.run(['codesign', '-dv', '--verbose=4', str(path)], capture_output=True, text=True, check=True) text = proc.stdout + proc.stderr expected_identity = os.environ['SIGN_IDENTITY'] match = re.search(r'\(([^()]+)\)$', expected_identity) team = os.getenv('SIGN_TEAM_ID') or (match.group(1) if match else None) actual_team = re.findall(r'^TeamIdentifier=(.+)$', text, re.M) authorities = re.findall(r'^Authority=(.+)$', text, re.M) if not team or actual_team != [team] or not authorities or authorities[0] != expected_identity: raise ValueError('Signing identity/team mismatch.') def main(): command, value = sys.argv[1:] path = Path(value) if command == 'preflight': preflight(path) elif command == 'identity': identity(path) elif command == 'bundle-id': print(bundle_info(path)['CFBundleIdentifier']) elif command == 'accepted': result = json.loads(path.read_text()) if result.get('status') != 'Accepted': raise ValueError('Apple did not accept this notarization submission.') print('Notarization accepted:', result.get('id', 'unknown request')) else: raise ValueError('Unknown check.') if __name__ == '__main__': try: main() except (ValueError, OSError, subprocess.CalledProcessError) as exc: print(f'Signing check failed: {exc}', file=sys.stderr) raise SystemExit(1)