Mac 公证方式有三种
| 公证方法 | 优点 | 缺点 | 阐述 |
| Xcode携带的图形界面,使用方便 | 无法进行自动化公证 | 单个App应用上架使用较多 | |
| altool(旧版) | 支持pkg,dmg,脚本自动化 | 2023/11/01 将会过期 | 已经是弃用,不建议使用 Mac开发-公证流程记录Notarization-附带脚本_xcrun altool --notarize-app --primary-bundle-id-CSDN博客 |
| notarytool(新版) | 支持pkg,dmg,脚本自动化 | 必须依赖macos环境 | 必须要依赖macos环境,并且更新Xcode版本和mac版本,有一定的环境限制 |
| Notary API | 支持pkg,dmg,脚本自动化 | 无需依赖macos环境 | 使用的是苹果官方提供的Web API进行公证,不受运行环境限制 |
这里 Notary API 有较大的优势,之前 altool 脚本公证的方式我们已经做过,由于 2023/11/01 将被弃用,考虑后续跨平台的需要,使用 notary API 进行脚本自动化公证
具体的流程大概如下:
官方文档阐述
根据上述流程获取你的 API 秘钥
https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests
参考 https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests 所述进行设置,值得注意的是,苹果会拒绝大多数设置过期时间为 20 分钟以上的 JWT 票据,所以我们需要间隔生成 JWT
- {
- "alg": "ES256",
- "kid": "2X9R4HXF34",
- "typ": "JWT"
- }
- {
- "iss": "57246542-96fe-1a63-e053-0824d011072a",
- "iat": 1528407600,
- "exp": 1528408800,
- "aud": "appstoreconnect-v1",
- "scope": [
- "GET /notary/v2/submissions",
- "POST /notary/v2/submissions",
- ]
- }
对应的生成逻辑如下
- def get_jwt_token():
- # 读取私钥文件内容
- with open('./AuthKey_xxxxx.p8', 'rb') as f:
- jwt_secret_key = f.read()
-
-
- # 获取当前时间
- now = datetime.datetime.now()
- # 计算过期时间(当前时间往后 20 分钟)
- expires = now + datetime.timedelta(minutes=20)
-
- # 设置 JWT 的 header
- jwt_header = {
- "alg": "ES256",
- "kid": "2X9R4HXF34",
- "typ": "JWT"
- }
-
- # 检查文件是否存在
- if os.path.exists("./jwt_token"):
- # 读取
- with open('./jwt_token', 'rb') as f:
- jwt_pre_token = f.read()
-
-
- print('[info]','jwt token %s' % (jwt_pre_token))
- try:
- decoded = jwt.decode(
- jwt_pre_token,
- jwt_secret_key,
- algorithms="ES256",
- audience="appstoreconnect-v1"
- )
- except Exception as e:
- print('[error]', 'decode exception %s' % (e))
- else:
- exp = datetime.datetime.fromtimestamp(decoded["exp"])
-
- if exp - datetime.timedelta(seconds=60) < now:
- print("jwt token 已过期,重新生成")
- else:
- print("jwt token 有效,使用之前token")
- return jwt_pre_token
-
-
-
- # 设置 JWT 的 payload
- jwt_payload = {
- "iss": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
- "iat": int(now.timestamp()),
- "exp": int(expires.timestamp()),
- "aud": "appstoreconnect-v1",
- "scope": [
- "GET /notary/v2/submissions",
- "POST /notary/v2/submissions",
- ]
- }
- print('[info]', 'jwt_header %s' % (jwt_header), 'jwt_payload %s' % jwt_payload)
- token = jwt.encode(
- jwt_payload,
- jwt_secret_key,
- algorithm="ES256",
- headers=jwt_header,
- )
-
- # 打开文件,如果文件不存在则创建文件
- with open("./jwt_token", "w") as f:
- # 将 token 写入文件
- f.write(token)
-
- print('[info]', 'jwt token is %s' % (token))
- return token
公证处理的流程如下
- file_md5=None
- def post_submissison(filepath):
- global file_md5
- body = get_body(filepath)
- token = get_jwt_token()
- file_md5 = get_md5(filepath)
-
- # 指定文件夹路径
- folder_path = './output'
- # 缓存路径
- cache_path = f"{folder_path}/{file_md5}"
- # 检查文件夹是否存在
- if not os.path.exists(folder_path):
- # 如果文件夹不存在,则创建文件夹
- os.makedirs(folder_path)
- else:
- # 如果文件夹已经存在,则进行相应的处理
- print("[info]", '%s 已经存在' % folder_path)
-
-
- # 检查文件是否存在
- if os.path.exists(cache_path):
- # 读取
- with open(cache_path, 'rb') as f:
- string = f.read().decode()
- output = json.loads(string)
- print('[info]', '使用上次 submission s3 上传凭证 = %s' % (output))
-
- else:
- resp = requests.post("https://appstoreconnect.apple.com/notary/v2/submissions", json=body, headers={"Authorization": "Bearer " + token})
- resp.raise_for_status()
- output = resp.json()
- print('[info]', '获取 submission s3上传凭证 = %s' % (output))
- # 打开文件,如果文件不存在则创建文件
- with open(cache_path, "w") as f:
- # 将 resp 写入文件
- f.write(resp.content.decode())
-
- # 读取 output 中的内容
- aws_info = output["data"]["attributes"]
- bucket = aws_info["bucket"]
- key = aws_info["object"]
- # sub_id = output["data"]["id"]
- # 如果已经完成了公证
- state = get_submission_state(filepath, True)
- if state == True:
- print('[info]', 'file %s alreay finished notarization' % (filepath))
- staple_pkg(filepath)
- exit(0)
-
- s3 = boto3.client(
- "s3",
- aws_access_key_id=aws_info["awsAccessKeyId"],
- aws_secret_access_key=aws_info["awsSecretAccessKey"],
- aws_session_token=aws_info["awsSessionToken"],
- config=Config(s3={"use_accelerate_endpoint": True})
- )
-
- print('[info]', 'start upload files ...... please wait 2-15 mins')
- # 上传文件
- s3.upload_file(filepath, bucket, key)
-
- print('[info]', 'upload file complete ...')
- def get_submission_state(filepath, once=False):
- print('[info]', 'get_submission_state %s %s ' % (filepath, once))
- global file_md5
- if not file_md5:
- file_md5 = get_md5(filepath)
- # 指定文件夹路径
- folder_path = './output'
- # 缓存路径
- cache_path = f"{folder_path}/{file_md5}"
- # 检查文件是否存在
- if os.path.exists(cache_path):
- # 获取文件大小
- file_size = os.path.getsize(cache_path)
- if file_size == 0:
- # 文件内容为空
- print('[info]', ' %s 内容为空,未获取到submission信息' % (filepath))
- return False
- else:
- # 读取缓存内容
- with open(cache_path, 'rb') as f:
- string = f.read().decode()
- output = json.loads(string)
- else:
- return False
-
- sub_id = output["data"]["id"]
- url = f"https://appstoreconnect.apple.com/notary/v2/submissions/{sub_id}"
- ret = False
- while True:
- try:
- # 获取submission
- token = get_jwt_token()
- resp = requests.get(url, headers={"Authorization": "Bearer " + token})
- resp.raise_for_status()
- except Exception as e:
- # 异常处理
- print("[Error]", ' %s get status failed, code = %s ' % filepath % resp.status_code)
- return False
- else:
- # 200 正常返回处理
- # 检查 status
- resp_json = resp.json()
- print('[info]', 'GET %s resp is %s , header is %s' % (url,resp_json,resp.headers))
-
- status = resp_json["data"]["attributes"]["status"]
- if status == "Accepted":
- print("[info]", ' %s notarization succesfull' % filepath)
- ret = True
- break
- if status == "Invalid":
- print("[info]", ' %s notarization failed' % filepath)
- ret = False
- break
-
- if once == False:
- # 暂停 30 秒
- time.sleep(30)
- else:
- print("[info]", 'get_submission_state run once')
- break
- if once == False:
- print_submission_logs(sub_id)
- return ret
- def print_submission_logs(identifier):
- try:
- url = f"https://appstoreconnect.apple.com/notary/v2/submissions/{identifier}/logs"
- token = get_jwt_token()
- resp = requests.get(url, headers={"Authorization": "Bearer " + token})
- resp.raise_for_status()
- except Exception as e:
- print("[Error]", '/notary/v2/submissions/%s/logs failed, code = %s ' % (identifier, resp.status_code))
- else:
- resp_json = resp.json()
- print('[info]', 'notarization %s logs is %s' % (identifier, resp_json))
- def staple_pkg(filepath):
- global file_md5
- if not file_md5:
- file_md5 = get_md5(filepath)
- # 完成公证
- subprocess.run(["xcrun", "stapler", "staple", filepath])
- now = datetime.datetime.now()
- # 验证公证结果
- temp_output_file = f"./temp_file_{file_md5}"
- with open(temp_output_file, "w") as f:
- subprocess.run(["xcrun", "stapler", "validate", filepath], stdout=f, stderr=subprocess.STDOUT)
-
- # 读取验证结果
- with open(temp_output_file, "r") as f:
- validate_result = f.read()
-
- os.remove(temp_output_file)
- # 检查验证结果
- if "The validate action worked!" not in validate_result:
- print('[error]',"\033[31m[error] stapler validate invalid, may be notarization failed!\033[0m")
- return False
- else:
- print('[info]','staple_pkg succesfull')
- return True
脚本文件 https://github.com/CaicaiNo/Apple-Mac-Notarized-script/blob/master/notarize-web/notarize.py
在执行下列步骤前,请先阅读 Generating Tokens for API Requests | Apple Developer Documentation
private_key = f"./../../res/AuthKey_2X9R4HXF34.p8"
- # 设置 JWT 的 header
- jwt_header = {
- "alg": "ES256",
- "kid": "2X9R4HXF34",
- "typ": "JWT"
- }
- # 设置 JWT 的 payload
- jwt_payload = {
- "iss": "57246542-96fe-1a63-e053-0824d011072a",
- "iat": int(now.timestamp()),
- "exp": int(expires.timestamp()),
- "aud": "appstoreconnect-v1",
- "scope": [
- "GET /notary/v2/submissions",
- "POST /notary/v2/submissions",
- ]
- }
- python3 -u ./notarize.py --pkg "./Output/${PACKAGE_NAME}_$TIME_INDEX.pkg" --private-key "./../../res/AuthKey_2X9R4HXF34.p8"
-
- if [ $? -eq 0 ]; then
- echo "./Output/aTrustInstaller_$TIME_INDEX.pkg notarization successful"
- // 公证成功
- else
- // 公证失败
- echo "./Output/aTrustInstaller_$TIME_INDEX.pkg notarization failed"
- exit 1
- fi