forked from alvincrisuy/clashX
-
Notifications
You must be signed in to change notification settings - Fork 1
/
SMJobBlessUtil.py
460 lines (372 loc) · 18.9 KB
/
SMJobBlessUtil.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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
#! /usr/bin/python3
#
# File: SMJobBlessUtil.py
#
# Contains: Tool for checking and correcting apps that use SMJobBless.
#
# Written by: DTS
#
# Copyright: Copyright (c) 2012 Apple Inc. All Rights Reserved.
#
# Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc.
# ("Apple") in consideration of your agreement to the following
# terms, and your use, installation, modification or
# redistribution of this Apple software constitutes acceptance of
# these terms. If you do not agree with these terms, please do
# not use, install, modify or redistribute this Apple software.
#
# In consideration of your agreement to abide by the following
# terms, and subject to these terms, Apple grants you a personal,
# non-exclusive license, under Apple's copyrights in this
# original Apple software (the "Apple Software"), to use,
# reproduce, modify and redistribute the Apple Software, with or
# without modifications, in source and/or binary forms; provided
# that if you redistribute the Apple Software in its entirety and
# without modifications, you must retain this notice and the
# following text and disclaimers in all such redistributions of
# the Apple Software. Neither the name, trademarks, service marks
# or logos of Apple Inc. may be used to endorse or promote
# products derived from the Apple Software without specific prior
# written permission from Apple. Except as expressly stated in
# this notice, no other rights or licenses, express or implied,
# are granted by Apple herein, including but not limited to any
# patent rights that may be infringed by your derivative works or
# by other works in which the Apple Software may be incorporated.
#
# The Apple Software is provided by Apple on an "AS IS" basis.
# APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
# WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT,
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING
# THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
# COMBINATION WITH YOUR PRODUCTS.
#
# IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT,
# INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY
# OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
# OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY
# OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR
# OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
import sys
import os
import getopt
import subprocess
import plistlib
import operator
import platform
class UsageException (Exception):
"""
Raised when the progam detects a usage issue; the top-level code catches this
and prints a usage message.
"""
pass
class CheckException (Exception):
"""
Raised when the "check" subcommand detects a problem; the top-level code catches
this and prints a nice error message.
"""
def __init__(self, message, path=None):
self.message = message
self.path = path
def checkCodeSignature(programPath, programType):
"""Checks the code signature of the referenced program."""
# Use the codesign tool to check the signature. The second "-v" is required to enable
# verbose mode, which causes codesign to do more checking. By default it does the minimum
# amount of checking ("Is the program properly signed?"). If you enabled verbose mode it
# does other sanity checks, which we definitely want. The specific thing I'd like to
# detect is "Does the code satisfy its own designated requirement?" and I need to enable
# verbose mode to get that.
args = [
# "false",
"codesign",
"-v",
"-v",
programPath
]
try:
subprocess.check_call(args, stderr=open("/dev/null"))
except subprocess.CalledProcessError as e:
raise CheckException("%s code signature invalid" % programType, programPath)
def readDesignatedRequirement(programPath, programType):
"""Returns the designated requirement of the program as a string."""
args = [
# "false",
"codesign",
"-d",
"-r",
"-",
programPath
]
try:
req = subprocess.check_output(args, stderr=open("/dev/null"), encoding="utf-8")
except subprocess.CalledProcessError as e:
raise CheckException("%s designated requirement unreadable" % programType, programPath)
reqLines = req.splitlines()
if len(reqLines) != 1 or not req.startswith("designated => "):
raise CheckException("%s designated requirement malformed" % programType, programPath)
return reqLines[0][len("designated => "):]
def readInfoPlistFromPath(infoPath):
"""Reads an "Info.plist" file from the specified path."""
try:
with open(infoPath, 'rb') as fp:
info = plistlib.load(fp)
except:
raise CheckException("'Info.plist' not readable", infoPath)
if not isinstance(info, dict):
raise CheckException("'Info.plist' root must be a dictionary", infoPath)
return info
def readPlistFromToolSection(toolPath, segmentName, sectionName):
"""Reads a dictionary property list from the specified section within the specified executable."""
# Run otool -s to get a hex dump of the section.
args = [
# "false",
"otool",
"-V",
"-arch",
platform.machine(),
"-s",
segmentName,
sectionName,
toolPath
]
try:
plistDump = subprocess.check_output(args, encoding="utf-8")
except subprocess.CalledProcessError as e:
raise CheckException("tool %s / %s section unreadable" % (segmentName, sectionName), toolPath)
# Convert that dump to an property list.
plistLines = plistDump.strip().splitlines(keepends=True)
if len(plistLines) < 3:
raise CheckException("tool %s / %s section dump malformed (1)" % (segmentName, sectionName), toolPath)
header = plistLines[1].strip()
if not header.endswith("(%s,%s) section" % (segmentName, sectionName)):
raise CheckException("tool %s / %s section dump malformed (2)" % (segmentName, sectionName), toolPath)
del plistLines[0:2]
try:
if header.startswith('Contents of'):
data = []
for line in plistLines:
# line looks like this:
#
# '100000000 3c 3f 78 6d 6c 20 76 65 72 73 69 6f 6e 3d 22 31 |<?xml version="1|'
parts = line.split('|')
assert len(parts) == 3
columns = parts[0].split()
assert len(columns) >= 2
del columns[0]
for hexStr in columns:
data.append(int(hexStr, 16))
data = bytes(data)
else:
data = bytes("".join(plistLines), encoding="utf-8")
plist = plistlib.loads(data)
except:
raise CheckException("tool %s / %s section dump malformed (3)" % (segmentName, sectionName), toolPath)
# Check the root of the property list.
if not isinstance(plist, dict):
raise CheckException("tool %s / %s property list root must be a dictionary" % (segmentName, sectionName), toolPath)
return plist
def checkStep1(appPath):
"""Checks that the app and the tool are both correctly code signed."""
if not os.path.isdir(appPath):
raise CheckException("app not found", appPath)
# Check the app's code signature.
checkCodeSignature(appPath, "app")
# Check the tool directory.
toolDirPath = os.path.join(appPath, "Contents", "Library", "LaunchServices")
if not os.path.isdir(toolDirPath):
raise CheckException("tool directory not found", toolDirPath)
# Check each tool's code signature.
toolPathList = []
for toolName in os.listdir(toolDirPath):
if toolName != ".DS_Store":
toolPath = os.path.join(toolDirPath, toolName)
if not os.path.isfile(toolPath):
raise CheckException("tool directory contains a directory", toolPath)
checkCodeSignature(toolPath, "tool")
toolPathList.append(toolPath)
# Check that we have at least one tool.
if len(toolPathList) == 0:
raise CheckException("no tools found", toolDirPath)
return toolPathList
def checkStep2(appPath, toolPathList):
"""Checks the SMPrivilegedExecutables entry in the app's "Info.plist"."""
# Create a map from the tool name (not path) to its designated requirement.
toolNameToReqMap = dict()
for toolPath in toolPathList:
req = readDesignatedRequirement(toolPath, "tool")
toolNameToReqMap[os.path.basename(toolPath)] = req
# Read the Info.plist for the app and extract the SMPrivilegedExecutables value.
infoPath = os.path.join(appPath, "Contents", "Info.plist")
info = readInfoPlistFromPath(infoPath)
if "SMPrivilegedExecutables" not in info:
raise CheckException("'SMPrivilegedExecutables' not found", infoPath)
infoToolDict = info["SMPrivilegedExecutables"]
if not isinstance(infoToolDict, dict):
raise CheckException("'SMPrivilegedExecutables' must be a dictionary", infoPath)
# Check that the list of tools matches the list of SMPrivilegedExecutables entries.
if sorted(infoToolDict.keys()) != sorted(toolNameToReqMap.keys()):
raise CheckException("'SMPrivilegedExecutables' and tools in 'Contents/Library/LaunchServices' don't match")
# Check that all the requirements match.
# This is an interesting policy choice. Technically the tool just needs to match
# the requirement listed in SMPrivilegedExecutables, and we can check that by
# putting the requirement into tmp.req and then running
#
# $ codesign -v -R tmp.req /path/to/tool
#
# However, for a Developer ID signed tool we really want to have the SMPrivilegedExecutables
# entry contain the tool's designated requirement because Xcode has built a
# more complex DR that does lots of useful and important checks. So, as a matter
# of policy we require that the value in SMPrivilegedExecutables match the tool's DR.
for toolName in infoToolDict:
if infoToolDict[toolName] != toolNameToReqMap[toolName]:
raise CheckException("tool designated requirement (%s) doesn't match entry in 'SMPrivilegedExecutables' (%s)" % (toolNameToReqMap[toolName], infoToolDict[toolName]))
def checkStep3(appPath, toolPathList):
"""Checks the "Info.plist" embedded in each helper tool."""
# First get the app's designated requirement.
appReq = readDesignatedRequirement(appPath, "app")
# Then check that the tool's SMAuthorizedClients value matches it.
for toolPath in toolPathList:
info = readPlistFromToolSection(toolPath, "__TEXT", "__info_plist")
if "CFBundleInfoDictionaryVersion" not in info or info["CFBundleInfoDictionaryVersion"] != "6.0":
raise CheckException("'CFBundleInfoDictionaryVersion' in tool __TEXT / __info_plist section must be '6.0'", toolPath)
if "CFBundleIdentifier" not in info or info["CFBundleIdentifier"] != os.path.basename(toolPath):
raise CheckException("'CFBundleIdentifier' in tool __TEXT / __info_plist section must match tool name", toolPath)
if "SMAuthorizedClients" not in info:
raise CheckException("'SMAuthorizedClients' in tool __TEXT / __info_plist section not found", toolPath)
infoClientList = info["SMAuthorizedClients"]
if not isinstance(infoClientList, list):
raise CheckException("'SMAuthorizedClients' in tool __TEXT / __info_plist section must be an array", toolPath)
if len(infoClientList) != 1:
raise CheckException("'SMAuthorizedClients' in tool __TEXT / __info_plist section must have one entry", toolPath)
# Again, as a matter of policy we require that the SMAuthorizedClients entry must
# match exactly the designated requirement of the app.
if infoClientList[0] != appReq:
raise CheckException("app designated requirement (%s) doesn't match entry in 'SMAuthorizedClients' (%s)" % (appReq, infoClientList[0]), toolPath)
def checkStep4(appPath, toolPathList):
"""Checks the "launchd.plist" embedded in each helper tool."""
for toolPath in toolPathList:
launchd = readPlistFromToolSection(toolPath, "__TEXT", "__launchd_plist")
if "Label" not in launchd or launchd["Label"] != os.path.basename(toolPath):
raise CheckException("'Label' in tool __TEXT / __launchd_plist section must match tool name", toolPath)
# We don't need to check that the label matches the bundle identifier because
# we know it matches the tool name and step 4 checks that the tool name matches
# the bundle identifier.
def checkStep5(appPath):
"""There's nothing to do here; we effectively checked for this is steps 1 and 2."""
pass
def check(appPath):
"""Checks the SMJobBless setup of the specified app."""
# Each of the following steps matches a bullet point in the SMJobBless header doc.
toolPathList = checkStep1(appPath)
checkStep2(appPath, toolPathList)
checkStep3(appPath, toolPathList)
checkStep4(appPath, toolPathList)
checkStep5(appPath)
def setreq(appPath, appInfoPlistPath, toolInfoPlistPaths):
"""
Reads information from the built app and uses it to set the SMJobBless setup
in the specified app and tool Info.plist source files.
"""
if not os.path.isdir(appPath):
raise CheckException("app not found", appPath)
if not os.path.isfile(appInfoPlistPath):
raise CheckException("app 'Info.plist' not found", appInfoPlistPath)
for toolInfoPlistPath in toolInfoPlistPaths:
if not os.path.isfile(toolInfoPlistPath):
raise CheckException("app 'Info.plist' not found", toolInfoPlistPath)
# Get the designated requirement for the app and each of the tools.
appReq = readDesignatedRequirement(appPath, "app")
toolDirPath = os.path.join(appPath, "Contents", "Library", "LaunchServices")
if not os.path.isdir(toolDirPath):
raise CheckException("tool directory not found", toolDirPath)
toolNameToReqMap = {}
for toolName in os.listdir(toolDirPath):
req = readDesignatedRequirement(os.path.join(toolDirPath, toolName), "tool")
toolNameToReqMap[toolName] = req
if len(toolNameToReqMap) > len(toolInfoPlistPaths):
raise CheckException("tool directory has more tools (%d) than you've supplied tool 'Info.plist' paths (%d)" % (len(toolNameToReqMap), len(toolInfoPlistPaths)), toolDirPath)
if len(toolNameToReqMap) < len(toolInfoPlistPaths):
raise CheckException("tool directory has fewer tools (%d) than you've supplied tool 'Info.plist' paths (%d)" % (len(toolNameToReqMap), len(toolInfoPlistPaths)), toolDirPath)
# Build the new value for SMPrivilegedExecutables.
appToolDict = {}
toolInfoPlistPathToToolInfoMap = {}
for toolInfoPlistPath in toolInfoPlistPaths:
toolInfo = readInfoPlistFromPath(toolInfoPlistPath)
toolInfoPlistPathToToolInfoMap[toolInfoPlistPath] = toolInfo
if "CFBundleIdentifier" not in toolInfo:
raise CheckException("'CFBundleIdentifier' not found", toolInfoPlistPath)
bundleID = toolInfo["CFBundleIdentifier"]
if not isinstance(bundleID, str):
raise CheckException("'CFBundleIdentifier' must be a string", toolInfoPlistPath)
appToolDict[bundleID] = toolNameToReqMap[bundleID]
# Set the SMPrivilegedExecutables value in the app "Info.plist".
appInfo = readInfoPlistFromPath(appInfoPlistPath)
needsUpdate = "SMPrivilegedExecutables" not in appInfo
if not needsUpdate:
oldAppToolDict = appInfo["SMPrivilegedExecutables"]
if not isinstance(oldAppToolDict, dict):
raise CheckException("'SMPrivilegedExecutables' must be a dictionary", appInfoPlistPath)
appToolDictSorted = sorted(appToolDict.items(), key=operator.itemgetter(0))
oldAppToolDictSorted = sorted(oldAppToolDict.items(), key=operator.itemgetter(0))
needsUpdate = (appToolDictSorted != oldAppToolDictSorted)
if needsUpdate:
appInfo["SMPrivilegedExecutables"] = appToolDict
with open(appInfoPlistPath, 'wb') as fp:
plistlib.dump(appInfo, fp)
print ("%s: updated" % appInfoPlistPath, file = sys.stdout)
# Set the SMAuthorizedClients value in each tool's "Info.plist".
toolAppListSorted = [ appReq ] # only one element, so obviously sorted (-:
for toolInfoPlistPath in toolInfoPlistPaths:
toolInfo = toolInfoPlistPathToToolInfoMap[toolInfoPlistPath]
needsUpdate = "SMAuthorizedClients" not in toolInfo
if not needsUpdate:
oldToolAppList = toolInfo["SMAuthorizedClients"]
if not isinstance(oldToolAppList, list):
raise CheckException("'SMAuthorizedClients' must be an array", toolInfoPlistPath)
oldToolAppListSorted = sorted(oldToolAppList)
needsUpdate = (toolAppListSorted != oldToolAppListSorted)
if needsUpdate:
toolInfo["SMAuthorizedClients"] = toolAppListSorted
with open(toolInfoPlistPath, 'wb') as f:
plistlib.dump(toolInfo, f)
print("%s: updated" % toolInfoPlistPath, file = sys.stdout)
def main():
options, appArgs = getopt.getopt(sys.argv[1:], "d")
debug = False
for opt, val in options:
if opt == "-d":
debug = True
else:
raise UsageException()
if len(appArgs) == 0:
raise UsageException()
command = appArgs[0]
if command == "check":
if len(appArgs) != 2:
raise UsageException()
check(appArgs[1])
elif command == "setreq":
if len(appArgs) < 4:
raise UsageException()
setreq(appArgs[1], appArgs[2], appArgs[3:])
else:
raise UsageException()
if __name__ == "__main__":
try:
main()
except CheckException as e:
if e.path is None:
print("%s: %s" % (os.path.basename(sys.argv[0]), e.message), file = sys.stderr)
else:
path = e.path
if path.endswith("/"):
path = path[:-1]
print("%s: %s" % (path, e.message), file = sys.stderr)
sys.exit(1)
except UsageException as e:
print("usage: %s check /path/to/app" % os.path.basename(sys.argv[0]), file = sys.stderr)
print(" %s setreq /path/to/app /path/to/app/Info.plist /path/to/tool/Info.plist..." % os.path.basename(sys.argv[0]), file = sys.stderr)
sys.exit(1)