This commit is contained in:
yicheng 2023-04-26 14:57:43 +08:00
parent dd5143bea8
commit 5d83896080
2 changed files with 143 additions and 124 deletions

View File

@ -49,7 +49,7 @@ jobs:
cd ..
bundle exec fastlane build
echo "Checking SMJobBless Vailded"
python SMJobBlessUtil.py check ClashX.app
python3 SMJobBlessUtil.py check ClashX.app
echo "Check done"
- name: setup node

View File

@ -1,20 +1,20 @@
#! /usr/bin/python
#
#! /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
@ -32,14 +32,14 @@
# 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.
#
# 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,
@ -49,7 +49,7 @@
# 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
@ -57,17 +57,18 @@ 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
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
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):
@ -77,38 +78,38 @@ class CheckException (Exception):
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
# 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",
# "false",
"codesign",
"-v",
"-v",
programPath
]
try:
subprocess.check_call(args, stderr=open("/dev/null"))
except subprocess.CalledProcessError, e:
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",
"-",
# "false",
"codesign",
"-d",
"-r",
"-",
programPath
]
try:
req = subprocess.check_output(args, stderr=open("/dev/null"))
except subprocess.CalledProcessError, e:
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()
@ -119,77 +120,97 @@ def readDesignatedRequirement(programPath, programType):
def readInfoPlistFromPath(infoPath):
"""Reads an "Info.plist" file from the specified path."""
try:
info = plistlib.readPlist(infoPath)
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",
"-s",
segmentName,
sectionName,
# "false",
"otool",
"-V",
"-arch",
platform.machine(),
"-s",
segmentName,
sectionName,
toolPath
]
try:
plistDump = subprocess.check_output(args)
except subprocess.CalledProcessError, e:
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 hex dump to an property list.
plistLines = plistDump.splitlines()
if len(plistLines) < 3 or plistLines[1] != ("Contents of (%s,%s) section" % (segmentName, sectionName)):
# 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:
bytes = []
for line in plistLines:
# line looks like this:
#
# '0000000100000b80\t3c 3f 78 6d 6c 20 76 65 72 73 69 6f 6e 3d 22 31 '
columns = line.split("\t")
assert len(columns) == 2
for hexStr in columns[1].split():
bytes.append(int(hexStr, 16))
plist = plistlib.readPlistFromString(bytearray(bytes))
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 (2)" % (segmentName, sectionName), toolPath)
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":
@ -200,50 +221,50 @@ def checkStep1(appPath):
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 not info.has_key("SMPrivilegedExecutables"):
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
# 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
# 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]))
@ -252,29 +273,29 @@ 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.
# Then check that the tool's SMAuthorizedClients value matches it.
for toolPath in toolPathList:
info = readPlistFromToolSection(toolPath, "__TEXT", "__info_plist")
if not info.has_key("CFBundleInfoDictionaryVersion") or info["CFBundleInfoDictionaryVersion"] != "6.0":
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 not info.has_key("CFBundleIdentifier") or info["CFBundleIdentifier"] != os.path.basename(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 not info.has_key("SMAuthorizedClients"):
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
# 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:
@ -286,22 +307,22 @@ def checkStep4(appPath, toolPathList):
for toolPath in toolPathList:
launchd = readPlistFromToolSection(toolPath, "__TEXT", "__launchd_plist")
if not launchd.has_key("Label") or launchd["Label"] != os.path.basename(toolPath):
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
# 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)
@ -314,7 +335,7 @@ def check(appPath):
def setreq(appPath, appInfoPlistPath, toolInfoPlistPaths):
"""
Reads information from the built app and uses it to set the SMJobBless setup
Reads information from the built app and uses it to set the SMJobBless setup
in the specified app and tool Info.plist source files.
"""
@ -328,20 +349,16 @@ def setreq(appPath, appInfoPlistPath, toolInfoPlistPaths):
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 = {}
print os.listdir(toolDirPath)
for toolName in os.listdir(toolDirPath):
req = readDesignatedRequirement(os.path.join(toolDirPath, toolName), "tool")
print '-----'
print toolName
print '-----'
toolNameToReqMap[toolName] = req
if len(toolNameToReqMap) > len(toolInfoPlistPaths):
@ -350,58 +367,60 @@ def setreq(appPath, appInfoPlistPath, 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 not toolInfo.has_key("CFBundleIdentifier"):
if "CFBundleIdentifier" not in toolInfo:
raise CheckException("'CFBundleIdentifier' not found", toolInfoPlistPath)
bundleID = toolInfo["CFBundleIdentifier"]
if not isinstance(bundleID, basestring):
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 = not appInfo.has_key("SMPrivilegedExecutables")
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.iteritems(), key=operator.itemgetter(0))
oldAppToolDictSorted = sorted(oldAppToolDict.iteritems(), key=operator.itemgetter(0))
appToolDictSorted = sorted(appToolDict.items(), key=operator.itemgetter(0))
oldAppToolDictSorted = sorted(oldAppToolDict.items(), key=operator.itemgetter(0))
needsUpdate = (appToolDictSorted != oldAppToolDictSorted)
if needsUpdate:
appInfo["SMPrivilegedExecutables"] = appToolDict
plistlib.writePlist(appInfo, appInfoPlistPath)
print >> sys.stdout, "%s: updated" % appInfoPlistPath
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 = not toolInfo.has_key("SMAuthorizedClients")
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
plistlib.writePlist(toolInfo, toolInfoPlistPath)
print >> sys.stdout, "%s: updated" % toolInfoPlistPath
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":
@ -426,16 +445,16 @@ def main():
if __name__ == "__main__":
try:
main()
except CheckException, e:
except CheckException as e:
if e.path is None:
print >> sys.stderr, "%s: %s" % (os.path.basename(sys.argv[0]), e.message)
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 >> sys.stderr, "%s: %s" % (path, e.message)
sys.exit(1)
except UsageException, e:
print >> sys.stderr, "usage: %s check /path/to/app" % os.path.basename(sys.argv[0])
print >> sys.stderr, " %s setreq /path/to/app /path/to/app/Info.plist /path/to/tool/Info.plist..." % os.path.basename(sys.argv[0])
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)