如何手动装订Apple公证票(例如在Linux下)

aamkag61  于 2023-11-17  发布在  Linux
关注(0)|答案(2)|浏览(112)

最近(截至2023-11-01)Apple更改了他们的公证流程。
我利用这个机会放弃了苹果自己的这个过程的工具(notarytool),并使用他们的documented Web API for notarization切换到基于Python的解决方案
这很好用,还有一个额外的好处,我现在可以从Linux公证macOS应用程序(在CI的上下文中,我可以比macOS runners更快地提供linux runners)。
由于这一切进行得如此顺利,我考虑将我的协同设计过程的更多部分转移到Linux上,显然下一步是找到一个解决方案,将公证票装订到应用程序中,取代xcrun stapler staple MyApp.app
-vvonlinedocumentation的一些碎片的帮助下,如果你知道你的应用程序的代码目录哈希(CDhash),那么获得公证票是非常简单的。
下面的代码将返回一个JSON对象,其中包含base64编码的公证票,只需将其解码并复制到.app包中进行装订即可:

cdhash=8d817db79d5c07d0deb7daf4908405f6a37c34b4
curl -X POST -H "Content-Type: application/json" \
   --data "{ \"records\": { \"recordName\": \"2/2/${cdhash}\" }}" \
   https://api.apple-cloudkit.com/database/1/com.apple.gk.ticket-delivery/production/public/records/lookup \
| jq -r ".records[0] | .fields | .signedTicket | .value"

字符串
所以,我的stapler替换仍然缺少的唯一一件事是获取给定应用程序的代码目录哈希的方法。在macOS上(安装了XCode工具),我可以使用codesign -d -vvv MyApp.app获取此哈希,但这显然只有在我手头有codesign二进制文件时才有效。
我发现了几个用于装订票据的python Package 器,但它们都只是在后台调用xcrun stapler staple。这不是我想要的**。
所以我的问题是:如何在不使用macOS特定工具的情况下从macOS应用程序中提取代码目录哈希(CDhash)?(即:如何生成CDhash es?我没有找到任何相关文档)
我非常希望使用Python来完成这项任务。理想情况下,这样的解决方案应该是跨平台的(因此我可以在macOS * 和 * Linux上使用它,可能也可以在其他平台上使用它)。

deyfvvtc

deyfvvtc1#

如何在不使用macOS特定工具的情况下从macOS应用程序中提取代码目录哈希(CDhash)?
应用程序的CDhash是Contents/Info.plist中标识的Contents/MacOS中主可执行文件的CDhash
每个散列都存储在XML语句中每个体系结构的二进制段的末尾。
嵌入的cdhash是用base64编码的。第一个是intel的,第二个是apple silicon的:

% grep -i -a -A3 'cdhashes' myApp.app/Contents/MacOS/mainexec | sed -n '4p;9p' | cut -f 3  
HPhKLQv1j2SFYTmIgyUi/L6B9Yo=
TVNDrCQEL9A/DMWVmphntZAq7kc=

% printf "HPhKLQv1j2SFYTmIgyUi/L6B9Yo=" | base64 -d | hexdump -v -e '/1 "%02x" ' && echo ""
1cf84a2d0bf58f6485613988832522fcbe81f58a

% printf "TVNDrCQEL9A/DMWVmphntZAq7kc=" | base64 -d | hexdump -v -e '/1 "%02x" ' && echo ""
4d5343ac24042fd03f0cc5959a9867b5902aee47

字符串
与codesign报告的cdhash相比:

% codesign -dvvv -a arm64 myApp.app
Executable=myApp.app/Contents/MacOS/mainexec
Identifier=com.mycompany.myApp
Format=app bundle with Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=92199 flags=0x10000(runtime) hashes=2870+7 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha256=4d5343ac24042fd03f0cc5959a9867b5902aee47
CandidateCDHashFull sha256=4d5343ac24042fd03f0cc5959a9867b5902aee4725c5a75775cd711aae76b709
Hash choices=sha256
CMSDigest=4d5343ac24042fd03f0cc5959a9867b5902aee4725c5a75775cd711aae76b709
CMSDigestType=2
Launch Constraints:
    None
CDHash=4d5343ac24042fd03f0cc5959a9867b5902aee47

prdp8dxp

prdp8dxp2#

基于Richard's answer,这是我最终使用的(有更多的错误检查):

import base64
import re
import plistlib

import requests

def bin2hex(bin):
    """return exencoded representation of bytes-object <bin>"""
    return "".join(["%02x" % _ for _ in bin])

def extractEmbeddedPlists(filename):
    """extract embedded plist data sections from binary file"""
    # this is a very crude way to do it!
    # LATER properly parse the macho file (with something like 'macholibre')
    with open(filename, "rb") as f:
        data = f.read()
    try:
        return [
            plistlib.loads(_)
            for _ in re.findall(b"<plist.*?</plist>", data, flags=re.DOTALL)
        ]
    except:
        pass
    return []

def getCDHashes(app):
    """get code directory hashes from a bundle
    not all cdhashes might be valid!
    """
    info_plist = os.path.join(app, "Contents", "Info.plist")
    try:
        with open(info_plist, "rb") as f:
            info = plistlib.load(f)
    except:
        return []
    if "CFBundleExecutable" not in info:
        return []
    plists = extractEmbeddedPlists(os.path.join(app, "Contents", "MacOS", info["CFBundleExecutable"]))
    cdhashes = [_["cdhashes"] for _ in plists if "cdhashes" in _]
    hashes = {bin2hex(_) for a in cdhashes for _ in a}
    return hashes

def getTicket(cdhash):
    """retrieve a notarization ticket for a given cdhash"""
    recordName = "2/2/%s" % cdhash
    data = {
        "records": {"recordName": recordName},
    }
    r = requests.post(ticket_url, json=data)

    j = r.json()
    record = j["records"][0]
    if record.get("serverErrorCode") == "NOT_FOUND":
        return

    ticket = record["fields"]["signedTicket"]["value"]
    return base64.b64decode(ticket)

def getTicket(app):
    for hash in getCDHashes(app):
        try:
            ticket = getTicket(hash)
            if ticket: return ticket
        except: pass

字符串
仍然有一些粗糙的边缘尚未处理(例如,如果二进制文件包含多个产生不同票据的CDHash(每个CDHash用于通用二进制文件的每个体系结构),则决定装订哪个票据),但基本工作现在正在进行。

相关问题