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
流程
具体的流程大概如下:
- 获取 API密钥 (Private Key)
- 使用 API密钥 (Private Key) 生成 JSON Web Token (JWT) , 相当于授权令牌 token
- 请求 Notary API 并在 HTTP 请求头中,携带 JWT 内容
- 得到请求结果
1. 获取 API密钥
https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api
官方文档阐述
- 登录App Store Connect
- 选择 [用户和访问] 这栏 ,然后选择API Keys选项卡
- 单击生成API密钥或添加(+)按钮。
- 输入密钥的名称。
- 在Access下,为密钥选择角色
- 点击生成
- 点击下载秘钥 (注意下载后苹果不再保存,你只能下载一次)
根据上述流程获取你的 API 秘钥
2. 生成令牌 Token
https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests
2.1. 环境安装
- 根据 https://jwt.io/libraries 所述,安装 pyjwt 库
- 由于本地环境 python3, 使用 pip3 install pyjwt 安装, 并参考官网使用说明 https://pyjwt.readthedocs.io/en/stable/usage.html#encoding-decoding-tokens-with-hs256
- pyjwt 依赖 cryptography 库,需要额外安装 pip3 install cryptography
- 文件上传依赖 boto3 库,pip3 install boto3
2.2. JWT Encode/Decode
参考 https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests 所述进行设置,值得注意的是,苹果会拒绝大多数设置过期时间为 20 分钟以上的 JWT 票据,所以我们需要间隔生成 JWT
- header
{"alg": "ES256","kid": "2X9R4HXF34","typ": "JWT"
}
- body
{"iss": "57246542-96fe-1a63-e053-0824d011072a","iat": 1528407600,"exp": 1528408800,"aud": "appstoreconnect-v1","scope": ["GET /notary/v2/submissions","POST /notary/v2/submissions",]
}
- API 秘钥
对应的生成逻辑如下
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 的 headerjwt_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 的 payloadjwt_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
3. 公证上传
公证处理的流程如下
- POST https://appstoreconnect.apple.com/notary/v2/submissions 请求submission s3 上传凭证
- 使用 boto3 框架根据获取的凭证信息,查询一次公证状态,如果非 Accepted 状态,进行上传文件。(阻塞式)
file_md5=None
def post_submissison(filepath): global file_md5body = 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 ...')
- 查询公证状态,Accepted 、In Progress、Invalid 目前探测到这三种状态
def get_submission_state(filepath, once=False):print('[info]', 'get_submission_state %s %s ' % (filepath, once))global file_md5if 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 Falseelse:# 读取缓存内容with open(cache_path, 'rb') as f:string = f.read().decode()output = json.loads(string)else:return Falsesub_id = output["data"]["id"]url = f"https://appstoreconnect.apple.com/notary/v2/submissions/{sub_id}"ret = Falsewhile True:try:# 获取submissiontoken = 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 Falseelse:# 200 正常返回处理# 检查 statusresp_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 = Truebreakif status == "Invalid":print("[info]", ' %s notarization failed' % filepath)ret = Falsebreakif once == False:# 暂停 30 秒time.sleep(30)else:print("[info]", 'get_submission_state run once')breakif 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))
- 如果 步骤3 查询到结果为 Accepted,则使用 stapler 工具打上票据,进行分发
def staple_pkg(filepath):global file_md5if 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 Falseelse:print('[info]','staple_pkg succesfull')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
- 替换你的秘钥文件 (例如 AuthKey_2X9R4HXF34.p8)
private_key = f"./../../res/AuthKey_2X9R4HXF34.p8"
- 设置你的 kid
# 设置 JWT 的 headerjwt_header = {"alg": "ES256","kid": "2X9R4HXF34","typ": "JWT"}
- 设置你的 iss
# 设置 JWT 的 payloadjwt_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 ]; thenecho "./Output/aTrustInstaller_$TIME_INDEX.pkg notarization successful"// 公证成功
else// 公证失败echo "./Output/aTrustInstaller_$TIME_INDEX.pkg notarization failed"exit 1
fi
相关文章:

Mac公证脚本-Web公证方式
公证方式 Mac 公证方式有三种 公证方法 优点 缺点 阐述 Xcode Xcode携带的图形界面,使用方便 无法进行自动化公证 单个App应用上架使用较多 altool(旧版) 支持pkg,dmg,脚本自动化 2023/11/01 将会过期 已经…...

让你专注工作的思维模板,进入每天的专注生活
开启专注生活,打造高效氛围,踏上传奇之路。 如何专注工作? 阻止内部干扰阻止外部干扰结论 专注象限图如下:(幸福是一种不断增加难度的活动) A1是你开始做某事的时候。 A2是当任务变得过于简单的时候。 A3是…...

Java之获取Nginx代理之后的客户端IP
Java之获取Nginx代理之后的客户端IP Nginx代理接口之后,后台获取的IP地址都是127.0.0.1,解决办法是需要配置Nginx搭配后台获取的方法,获得设备的真实地址。我们想要获取的就是nginx代理日志中的这个IP nginx配置 首先在nginx代理的对应lo…...

【springboot+vue项目(十五)】基于Oauth2的SSO单点登录(二)vue-element-admin框架改造整合Oauth2.0
Vue-element-admin 是一个基于 Vue.js 和 Element UI 的后台管理系统框架,提供了丰富的组件和功能,可以帮助开发者快速搭建现代化的后台管理系统。 一、基本知识 (一)Vue-element-admin 的主要文件和目录 vue-element-admin/ |…...

音频的传输链路与延迟优化点
麦克风->系统采集模块->APP采集模块->3A、混响等音效->混音->音频编码->RTC网络发送-> MediaServer->RTC网络接收->音频jitter buffer->音频解码->音频的后处理(均衡)->APP播放模块->x系统播放模块->扬声器/耳机。 整个链路如上&a…...

【51单片机】直流电机驱动(PWM)(江科大)
1.直流电机介绍 直流电机是一种将电能转换为机械能的装置。一般的直流电机有两个电极,当电极正接时,电机正转,当电极反接时,电机反转 直流电机主要由永磁体(定子)、线圈(转子)和换向器组成 除直流电机外,常见的电机还有步进电机、舵机、无刷电机、空心杯电机等 2.电机驱动…...

腾讯文档(excel也一样)设置单元格的自动行高列宽
1. 选中单元格 可选择任意一个或者几个 2. 设置自动 行高和列宽 即可生效...

vue-router 提供的几种导航守卫
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。 1、全局前置守卫 你可以使用 router.beforeEach 注册一个全局前置守卫: const route…...

Element UI 组件的安装及使用
Element UI 组件的安装及使用 Element UI 是一套基于 Vue.js 的桌面端 UI 组件库,提供了丰富的、高质量的 UI 组件,可以帮助开发者快速构建用户界面。 1、安装 Element UI 使用 npm 安装 npm install element-ui -S2、使用 CDN 安装 在 HTML 页面中引…...

网站架构演变、LNP+Mariadb数据库分离、Web服务器集群、Keepalived高可用
目录 day02 深入理解程序的数据存储 验证 配置NFS服务器 配置代理服务器 配置名称解析 day02 深入理解程序的数据存储 程序将文字数据保存到数据库中程序将非文字数据(如图片、视频、压缩包等)保存到相应的文件目录中 验证 发一篇文章…...

设计模式(七):策略模式(行为型模式)
FullDiscount Strategy,策略模式:定义一系列的算法,把他们一个个封装起来, 并使他们可以互相替换,本模式使得算法可以独立于使用它们的客户。 场景:购物车结算时,根据不同的客户,…...

人工智能|深度学习——基于对抗网络的室内定位系统
代码下载: 基于CSI的工业互联网深度学习定位.zip资源-CSDN文库 摘要 室内定位技术是工业互联网相关技术的关键一环。该技术旨在解决于室外定位且取得良好效果的GPS由于建筑物阻挡无法应用于室内的问题。实现室内定位技术,能够在真实工业场景下实时追踪和…...

MySQL的配置文件my.cnf正常的配置项目
my.cnf(或my.ini)是MySQL的配置文件,其中包含了多种设置,用于控制MySQL服务器的运行方式。以下是my.cnf中一些常见的配置项目: 服务器设置 - [mysqld]:服务器的配置部分。 - user:指定M…...

小程序API能力集成指南——界面导航栏API汇总
ty.setNavigationBarColor 设置页面导航条颜色 需引入MiniKit,且在>2.0.0版本才可使用 参数 Object object 属性类型默认值必填说明frontColorstring是前景颜色值,包括按钮、标题、状态栏的颜色,仅支持 #ffffff 和 #000000backgroundCo…...

onlyoffice基础环境搭建+部署+demo可直接运行 最简单的入门
office这个体系分为四个大教程 1、【document server文档服务器基础搭建】 2、【连接器(connector)或者jsApi调用操作office】-进阶 3、【document builder文档构造器使用】-进阶 4、【Conversion API(文档转化服务)】-进阶 如果需要连接器,可以查看:onl…...

ubuntu 22.04 图文安装
ubuntu 22.04.3 live server图文安装 一、在Vmware里安装ubuntu 22.04.3 live server操作系统 选择第一个选项开始安装 选择English语言 选择中间选项不更新安装,这是因为后续通过更换源之后再更新会比较快 键盘设计继续选择英文,可以通过语言选择…...

Dockerfile文件中只指定挂载点会发生什么?
当你在VOLUME指令中只指定容器内的路径(挂载点)而不指定宿主机的目录时,Docker会为该挂载点自动生成一个匿名卷。这个匿名卷存储在宿主机的某个位置,但这个具体位置是由Docker自动管理的,用户通常不需要关心这个存储位…...

详解 leetcode_078. 合并K个升序链表.小顶堆实现
/*** 构造单链表节点*/ class ListNode{int value;//节点值ListNode next;//指向后继节点的引用public ListNode(){}public ListNode(int value){this.valuevalue;}public ListNode(int value,ListNode next){this.valuevalue;this.nextnext;} }package com.ag; import java.ut…...

OpenHarmony下gn相关使用
OpenHarmony下gn相关使用 引言 为了提高OpenHarmony下移植vivante gpu的成功率,先得把准备工作做足了,这样后续就好搞了。所以本文档的核心工作介绍GN构建工具在OpenHarmony中的常见使用方法,指导三方库由cmake或者其它的脚本构建到GN构建的…...

怎样重置ubuntu mysql8密码
密码很难记住,所以如果您忘记了 MySQL root 密码,幸运的是,有一种方法可以更改它。这篇文章是为您而写的,在这篇文章结束时,您将成功更改 MySQL 的密码。 本博客演示了如何在 Ubuntu 上重置使用包管理器安装的 MySQL …...

SpringBoot+WebSocket实现即时通讯(三)
前言 紧接着上文《SpringBootWebSocket实现即时通讯(二)》 本博客姊妹篇 SpringBootWebSocket实现即时通讯(一)SpringBootWebSocket实现即时通讯(二)SpringBootWebSocket实现即时通讯(三&…...

vue3前端项目开发,具备纯天然的防止爬虫采集的特征
vue3前端项目开发,具备纯天然的防止爬虫采集的特征!众所周知,网络爬虫可以在网上爬取到一些数据,很多公司,为了自己公司的数据安全, 尤其是web端项目,不希望被爬虫采集。那么,您可以使用vue技术…...

js 多对象去重(多属性去重)
需求中发现后端可能没有处理重复数据,这个时候前段可以直接解决。 在 JavaScript 中,可以使用 Set 数据结构来进行多对象的去重。Set 是 ES6 新引入的集合类型,其特点是元素不会重复且无序。 下面是一个示例代码,展示如何通过 S…...

在 JavaScript 中,Map 与 object 的差别?为什么有 object 还需要 Map?
ES6 推出了Map 物件,让开发者可以透过这个特制资料结构进行键值对(key-value pairs) 的操作。然而 JavaScript 原始物件 (plain object) 就可以用来做键值对的操作,为什么还需要 Map 物件呢? Map 物件解决了什么问题? 原始物件的键 (key) 只可以是字串…...

【研究生复试】计算机软件工程人工智能研究生复试——资料整理(速记版)——自我介绍(英文)
1、JAVA 2、计算机网络 3、计算机体系结构 4、数据库 5、计算机租场原理 6、软件工程 7、大数据 8、英文 自我介绍 自我介绍 英文 自我介绍 英文 第一段: Good afternoon, dear professors, thank you for the chance to introduce myself. My name is Yan Zhen …...

ACP科普:IDEAL含义及应用
一、IDEAL介绍 IDEAL模型是一种组织改进模型,描述了组织在实施变革过程中可能经历的五个阶段: 启动诊断确立执行学习 这个模型可以应用于各种组织,包括软件开发团队、项目管理团队以及整个组织的变革过程。 二、IDEAL拆解 当应用IDEAL模型…...

【GO语言卵细胞级别教程】06.GO语言的字符串操作
【GO语言卵细胞级别教程】06.GO语言的字符串操作 温馨提示: 本文中使用的项目模块均是 【05.项目创建和函数讲解】 中创建的,具体如何创建项目,请参考 【GO语言卵细胞级别教程】05.项目创建和函数讲解 目录: 【GO语言卵细胞级别…...

【笔记】【算法设计与分析 - 北航童咏昕教授】绪论
算法设计与分析 - 北航童咏昕教授 文章目录 算法的定义定义性质 算法的表示自然语言编程语言伪代码 算法的分析算法分析的原则渐近分析 算法的定义 定义 给定计算问题,算法是一系列良定义的计算步骤,逐一执行计算步骤即可得预期的输出。 性质 有穷性确…...

大语言模型LLM中Transformer模型的调用过程与步骤
在LLM(Language Model)中,Transformer是一种用来处理自然语言任务的模型架构。下面是Transformer模型中的调用过程和步骤的简要介绍: 数据预处理:将原始文本转换为模型可以理解的数字形式。这通常包括分词、编码和填充…...

mysql connect unblock with mysqladmin flush-hosts
原因 同一个ip在短时间内产生太多(超过max_connect_errors的最大值)中断的数据库连接而导致的阻塞。 查看 max_connect_errors show variables like max_connect_errors; 解决 前提:需要换一个IP地址连接 方法一 增大 max_connect_err…...