• Mac公证脚本-Web公证方式


    公证方式

    Mac 公证方式有三种

    公证方法

    优点

    缺点

    阐述

    Xcode

    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 进行脚本自动化公证

    https://developer.apple.com/documentation/notaryapi/submitting_software_for_notarization_over_the_web

    流程

    具体的流程大概如下:

    1. 获取 API密钥 (Private Key)
    2. 使用 API密钥 (Private Key) 生成 JSON Web Token (JWT) , 相当于授权令牌 token
    3. 请求 Notary API 并在 HTTP 请求头中,携带 JWT 内容
    4. 得到请求结果

    1. 获取 API密钥

    https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api

    官方文档阐述

    1. 登录App Store Connect
    2. 选择 [用户和访问] 这栏 ,然后选择API Keys选项卡
    3. 单击生成API密钥或添加(+)按钮。
    4. 输入密钥的名称。
    5. 在Access下,为密钥选择角色
    6. 点击生成
    7. 点击下载秘钥 (注意下载后苹果不再保存,你只能下载一次)

    根据上述流程获取你的 API 秘钥

    2. 生成令牌 Token

    https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests

    2.1. 环境安装
    1. 根据 https://jwt.io/libraries 所述,安装 pyjwt 库
    2. 由于本地环境 python3, 使用 pip3 install pyjwt 安装, 并参考官网使用说明 https://pyjwt.readthedocs.io/en/stable/usage.html#encoding-decoding-tokens-with-hs256
    3. pyjwt 依赖 cryptography 库,需要额外安装 pip3 install cryptography
    4. 文件上传依赖 boto3 库,pip3 install boto3

    2.2. JWT Encode/Decode

    参考 https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests 所述进行设置,值得注意的是,苹果会拒绝大多数设置过期时间为 20 分钟以上的 JWT 票据,所以我们需要间隔生成 JWT

    1. header
    1. {
    2. "alg": "ES256",
    3. "kid": "2X9R4HXF34",
    4. "typ": "JWT"
    5. }
    1. body
    1. {
    2. "iss": "57246542-96fe-1a63-e053-0824d011072a",
    3. "iat": 1528407600,
    4. "exp": 1528408800,
    5. "aud": "appstoreconnect-v1",
    6. "scope": [
    7. "GET /notary/v2/submissions",
    8. "POST /notary/v2/submissions",
    9. ]
    10. }
    1. API 秘钥

    对应的生成逻辑如下

    1. def get_jwt_token():
    2. # 读取私钥文件内容
    3. with open('./AuthKey_xxxxx.p8', 'rb') as f:
    4. jwt_secret_key = f.read()
    5. # 获取当前时间
    6. now = datetime.datetime.now()
    7. # 计算过期时间(当前时间往后 20 分钟)
    8. expires = now + datetime.timedelta(minutes=20)
    9. # 设置 JWT 的 header
    10. jwt_header = {
    11. "alg": "ES256",
    12. "kid": "2X9R4HXF34",
    13. "typ": "JWT"
    14. }
    15. # 检查文件是否存在
    16. if os.path.exists("./jwt_token"):
    17. # 读取
    18. with open('./jwt_token', 'rb') as f:
    19. jwt_pre_token = f.read()
    20. print('[info]','jwt token %s' % (jwt_pre_token))
    21. try:
    22. decoded = jwt.decode(
    23. jwt_pre_token,
    24. jwt_secret_key,
    25. algorithms="ES256",
    26. audience="appstoreconnect-v1"
    27. )
    28. except Exception as e:
    29. print('[error]', 'decode exception %s' % (e))
    30. else:
    31. exp = datetime.datetime.fromtimestamp(decoded["exp"])
    32. if exp - datetime.timedelta(seconds=60) < now:
    33. print("jwt token 已过期,重新生成")
    34. else:
    35. print("jwt token 有效,使用之前token")
    36. return jwt_pre_token
    37. # 设置 JWT 的 payload
    38. jwt_payload = {
    39. "iss": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    40. "iat": int(now.timestamp()),
    41. "exp": int(expires.timestamp()),
    42. "aud": "appstoreconnect-v1",
    43. "scope": [
    44. "GET /notary/v2/submissions",
    45. "POST /notary/v2/submissions",
    46. ]
    47. }
    48. print('[info]', 'jwt_header %s' % (jwt_header), 'jwt_payload %s' % jwt_payload)
    49. token = jwt.encode(
    50. jwt_payload,
    51. jwt_secret_key,
    52. algorithm="ES256",
    53. headers=jwt_header,
    54. )
    55. # 打开文件,如果文件不存在则创建文件
    56. with open("./jwt_token", "w") as f:
    57. # 将 token 写入文件
    58. f.write(token)
    59. print('[info]', 'jwt token is %s' % (token))
    60. return token

    3. 公证上传

    公证处理的流程如下

    1. POST https://appstoreconnect.apple.com/notary/v2/submissions 请求submission s3 上传凭证
    2. 使用 boto3 框架根据获取的凭证信息,查询一次公证状态,如果非 Accepted 状态,进行上传文件。(阻塞式)
    1. file_md5=None
    2. def post_submissison(filepath):
    3. global file_md5
    4. body = get_body(filepath)
    5. token = get_jwt_token()
    6. file_md5 = get_md5(filepath)
    7. # 指定文件夹路径
    8. folder_path = './output'
    9. # 缓存路径
    10. cache_path = f"{folder_path}/{file_md5}"
    11. # 检查文件夹是否存在
    12. if not os.path.exists(folder_path):
    13. # 如果文件夹不存在,则创建文件夹
    14. os.makedirs(folder_path)
    15. else:
    16. # 如果文件夹已经存在,则进行相应的处理
    17. print("[info]", '%s 已经存在' % folder_path)
    18. # 检查文件是否存在
    19. if os.path.exists(cache_path):
    20. # 读取
    21. with open(cache_path, 'rb') as f:
    22. string = f.read().decode()
    23. output = json.loads(string)
    24. print('[info]', '使用上次 submission s3 上传凭证 = %s' % (output))
    25. else:
    26. resp = requests.post("https://appstoreconnect.apple.com/notary/v2/submissions", json=body, headers={"Authorization": "Bearer " + token})
    27. resp.raise_for_status()
    28. output = resp.json()
    29. print('[info]', '获取 submission s3上传凭证 = %s' % (output))
    30. # 打开文件,如果文件不存在则创建文件
    31. with open(cache_path, "w") as f:
    32. # 将 resp 写入文件
    33. f.write(resp.content.decode())
    34. # 读取 output 中的内容
    35. aws_info = output["data"]["attributes"]
    36. bucket = aws_info["bucket"]
    37. key = aws_info["object"]
    38. # sub_id = output["data"]["id"]
    39. # 如果已经完成了公证
    40. state = get_submission_state(filepath, True)
    41. if state == True:
    42. print('[info]', 'file %s alreay finished notarization' % (filepath))
    43. staple_pkg(filepath)
    44. exit(0)
    45. s3 = boto3.client(
    46. "s3",
    47. aws_access_key_id=aws_info["awsAccessKeyId"],
    48. aws_secret_access_key=aws_info["awsSecretAccessKey"],
    49. aws_session_token=aws_info["awsSessionToken"],
    50. config=Config(s3={"use_accelerate_endpoint": True})
    51. )
    52. print('[info]', 'start upload files ...... please wait 2-15 mins')
    53. # 上传文件
    54. s3.upload_file(filepath, bucket, key)
    55. print('[info]', 'upload file complete ...')
    1. 查询公证状态,Accepted 、In Progress、Invalid 目前探测到这三种状态
    1. def get_submission_state(filepath, once=False):
    2. print('[info]', 'get_submission_state %s %s ' % (filepath, once))
    3. global file_md5
    4. if not file_md5:
    5. file_md5 = get_md5(filepath)
    6. # 指定文件夹路径
    7. folder_path = './output'
    8. # 缓存路径
    9. cache_path = f"{folder_path}/{file_md5}"
    10. # 检查文件是否存在
    11. if os.path.exists(cache_path):
    12. # 获取文件大小
    13. file_size = os.path.getsize(cache_path)
    14. if file_size == 0:
    15. # 文件内容为空
    16. print('[info]', ' %s 内容为空,未获取到submission信息' % (filepath))
    17. return False
    18. else:
    19. # 读取缓存内容
    20. with open(cache_path, 'rb') as f:
    21. string = f.read().decode()
    22. output = json.loads(string)
    23. else:
    24. return False
    25. sub_id = output["data"]["id"]
    26. url = f"https://appstoreconnect.apple.com/notary/v2/submissions/{sub_id}"
    27. ret = False
    28. while True:
    29. try:
    30. # 获取submission
    31. token = get_jwt_token()
    32. resp = requests.get(url, headers={"Authorization": "Bearer " + token})
    33. resp.raise_for_status()
    34. except Exception as e:
    35. # 异常处理
    36. print("[Error]", ' %s get status failed, code = %s ' % filepath % resp.status_code)
    37. return False
    38. else:
    39. # 200 正常返回处理
    40. # 检查 status
    41. resp_json = resp.json()
    42. print('[info]', 'GET %s resp is %s , header is %s' % (url,resp_json,resp.headers))
    43. status = resp_json["data"]["attributes"]["status"]
    44. if status == "Accepted":
    45. print("[info]", ' %s notarization succesfull' % filepath)
    46. ret = True
    47. break
    48. if status == "Invalid":
    49. print("[info]", ' %s notarization failed' % filepath)
    50. ret = False
    51. break
    52. if once == False:
    53. # 暂停 30
    54. time.sleep(30)
    55. else:
    56. print("[info]", 'get_submission_state run once')
    57. break
    58. if once == False:
    59. print_submission_logs(sub_id)
    60. return ret
    1. 获取日志内容
    1. def print_submission_logs(identifier):
    2. try:
    3. url = f"https://appstoreconnect.apple.com/notary/v2/submissions/{identifier}/logs"
    4. token = get_jwt_token()
    5. resp = requests.get(url, headers={"Authorization": "Bearer " + token})
    6. resp.raise_for_status()
    7. except Exception as e:
    8. print("[Error]", '/notary/v2/submissions/%s/logs failed, code = %s ' % (identifier, resp.status_code))
    9. else:
    10. resp_json = resp.json()
    11. print('[info]', 'notarization %s logs is %s' % (identifier, resp_json))
    1. 如果 步骤3 查询到结果为 Accepted,则使用 stapler 工具打上票据,进行分发
    1. def staple_pkg(filepath):
    2. global file_md5
    3. if not file_md5:
    4. file_md5 = get_md5(filepath)
    5. # 完成公证
    6. subprocess.run(["xcrun", "stapler", "staple", filepath])
    7. now = datetime.datetime.now()
    8. # 验证公证结果
    9. temp_output_file = f"./temp_file_{file_md5}"
    10. with open(temp_output_file, "w") as f:
    11. subprocess.run(["xcrun", "stapler", "validate", filepath], stdout=f, stderr=subprocess.STDOUT)
    12. # 读取验证结果
    13. with open(temp_output_file, "r") as f:
    14. validate_result = f.read()
    15. os.remove(temp_output_file)
    16. # 检查验证结果
    17. if "The validate action worked!" not in validate_result:
    18. print('[error]',"\033[31m[error] stapler validate invalid, may be notarization failed!\033[0m")
    19. return False
    20. else:
    21. print('[info]','staple_pkg succesfull')
    22. return True

    4. 脚本使用方式

    脚本文件  https://github.com/CaicaiNo/Apple-Mac-Notarized-script/blob/master/notarize-web/notarize.py

    在执行下列步骤前,请先阅读 Generating Tokens for API Requests | Apple Developer Documentation

    1. 替换你的秘钥文件 (例如 AuthKey_2X9R4HXF34.p8)
    private_key = f"./../../res/AuthKey_2X9R4HXF34.p8"
    1. 设置你的 kid
    1. # 设置 JWT 的 header
    2. jwt_header = {
    3. "alg": "ES256",
    4. "kid": "2X9R4HXF34",
    5. "typ": "JWT"
    6. }
    1. 设置你的 iss
    1. # 设置 JWT 的 payload
    2. jwt_payload = {
    3. "iss": "57246542-96fe-1a63-e053-0824d011072a",
    4. "iat": int(now.timestamp()),
    5. "exp": int(expires.timestamp()),
    6. "aud": "appstoreconnect-v1",
    7. "scope": [
    8. "GET /notary/v2/submissions",
    9. "POST /notary/v2/submissions",
    10. ]
    11. }
    1. 调用脚本
    1. python3 -u ./notarize.py --pkg "./Output/${PACKAGE_NAME}_$TIME_INDEX.pkg" --private-key "./../../res/AuthKey_2X9R4HXF34.p8"
    2. if [ $? -eq 0 ]; then
    3. echo "./Output/aTrustInstaller_$TIME_INDEX.pkg notarization successful"
    4. // 公证成功
    5. else
    6. // 公证失败
    7. echo "./Output/aTrustInstaller_$TIME_INDEX.pkg notarization failed"
    8. exit 1
    9. fi

  • 相关阅读:
    【SemiDrive源码分析】【MailBox核间通信】52 - DCF Notify 实现原理分析 及 代码实战
    OpenCV画图(画OpenCV的标志)
    windows11系统如何设置锁屏壁纸
    学习周总结
    基于SSM+SpringBoot+Vue的车辆物流管理系统
    C#上位机与PLC
    一文了解VR全景拍摄与后期制作
    Anaconda 克隆环境
    基于C++的中国行政区域图染色与信息查询 课程论文+任务书+代码
    Hust计算机组成原理实验
  • 原文地址:https://blog.csdn.net/shengpeng3344/article/details/136197460