-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathresigner.py
executable file
·120 lines (97 loc) · 4.46 KB
/
resigner.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
"""resigner is an iOS app re-signer."""
from __future__ import annotations
from typing import TYPE_CHECKING
import re
import os
import logging
log = logging.getLogger(__name__)
if TYPE_CHECKING:
from typing import Optional
__version__ = '1.0.0'
class ShellProcess:
def __init__(self, cmdline: str, cwd: Optional[str] = None, check: bool = False) -> None:
self._cmdline = cmdline
self._cwd = cwd
self._check = check
def invoked(self) -> str:
from subprocess import run, PIPE
return self._as_str(run(self._cmdline, cwd=self._cwd, shell=True, check=self._check, stdout=PIPE).stdout)
def _as_str(self, x: bytes) -> str:
return x.decode('utf-8')
def resolved_path_of(path: str, mask: str) -> str:
from glob import glob
return glob(os.path.join(path, mask))[0]
def decoded_profile(profile: bytes) -> bytes:
m = re.search(rb'<\?xml version="1.0".*</plist>', profile, flags=re.DOTALL)
assert m
return bytes(m.group(0))
def merged_entitlements(profile: bytes, entitlements: Optional[bytes]) -> bytes:
import plistlib
a = plistlib.loads(decoded_profile(profile))['Entitlements']
if entitlements is not None:
b = plistlib.loads(entitlements, fmt=plistlib.FMT_XML)
for k in 'get-task-allow',:
if k in b:
log.warn(f'[*] merged_entitilements: dropping entitlement key "{k}"')
del b[k]
if '.*' in a['application-identifier']:
for k in 'aps-environment',:
if k in b:
log.warn(f'[*] merged_entitilements: dropping entitlement key "{k}" due to we are signing with wildcard provisioning profile')
del b[k]
a.update(b)
return plistlib.dumps(a)
def do_resign(identity: str, provisioning_profile: str, entitlement: Optional[str], target: str, output: str) -> None:
import shlex
import shutil
import tempfile
identity = shlex.quote(identity)
provisioning_profile = shlex.quote(provisioning_profile)
target = shlex.quote(target)
output = shlex.quote(output)
with tempfile.TemporaryDirectory() as t:
os.chdir(t)
log.info('[.] extracting ipa')
ShellProcess(f'unzip -q {target}', check=True).invoked()
bundle_path = resolved_path_of('Payload', '*.app')
log.info('[.] manipulating profile and entitlements')
profiled_paths = [l for l in ShellProcess(f'find "{bundle_path}" -name "embedded.mobileprovision" -print0', check=True).invoked().split('\0') if l]
for l in profiled_paths:
shutil.copyfile(provisioning_profile, l)
if entitlement is not None:
shutil.copyfile(entitlement, os.path.join(bundle_path, 'ent.xcent'))
with tempfile.NamedTemporaryFile() as tf:
try:
ent = open(resolved_path_of(bundle_path, '*.xcent'), 'rb').read()
except IndexError:
ent = None
tf.write(merged_entitlements(open(provisioning_profile, 'rb').read(), ent))
tf.flush()
log.info('[.] replacing signatures')
ShellProcess(r'find -E "{}" -depth -regex "^.*\.(app|appex|framework|dylib|car)" -print0 | xargs -0 codesign -vvvvf -s "{}" --deep --entitlements {}'.format(bundle_path, identity, tf.name), check=True).invoked()
log.info('[.] generating ipa')
ShellProcess('rm -f {target} && zip -qr {target} *'.format(target=output), check=True).invoked()
def entry() -> None:
from argparse import ArgumentParser
logging.basicConfig(level=logging.INFO, format='%(message)s')
parser = ArgumentParser(description='iOS app resigner.')
parser.add_argument('target')
parser.add_argument('-o', '--output', help='Output filename')
parser.add_argument('-i', '--identity', required=True, help='Identity to use, typically fingerprint of the certificate')
parser.add_argument('-p', '--profile', required=True, help='Provisioning profile file to use')
parser.add_argument('-e', '--entitlement', help='Entitlement to include, if any')
args = parser.parse_args()
if not args.output:
args.output = re.sub(r'(.ipa)$', r'-resigned\g<1>', args.target, flags=re.IGNORECASE)
if not args.entitlement:
log.info(f'[+] resigning {args.target} with profile {args.profile} and identity {args.identity}')
else:
log.info(f'[+] resigning {args.target} with profile {args.profile} and identity {args.identity}, including entitlements {args.entitlement}')
do_resign(
identity=args.identity,
provisioning_profile=os.path.realpath(args.profile),
entitlement=os.path.realpath(args.entitlement) if args.entitlement else None,
target=os.path.realpath(args.target),
output=os.path.realpath(args.output),
)
log.info('[+] done')