第一章:iPad AirDrop协议Go语言复现实录概览
AirDrop 并非简单基于蓝牙或 Wi-Fi 直连的“文件发送”功能,而是苹果构建的一套精密协同发现与安全传输协议栈,融合了Bonjour(mDNS)、TLS 1.2+、NSNetService、Peer-to-Peer Connectivity Framework 及私有加密握手流程。在 iPad 上,AirDrop 启用时会广播带有特定 TXT 记录的 _airdrop._tcp 服务,其中包含设备标识符(如 deviceid=8A7F...)、支持的协议版本(vers=200)、加密能力标识(pk=ecdsa-p256)及临时公钥摘要(pkh=...)等关键字段。
复现其核心发现层需绕过 iOS 系统限制,在 macOS 或 Linux 环境中用 Go 构建 mDNS 客户端监听 _airdrop._tcp 服务。以下为最小可行发现代码片段:
package main
import (
"log"
"github.com/miekg/dns" // 需 go get github.com/miekg/dns
)
func main() {
c := new(dns.Client)
m := new(dns.Msg)
m.SetQuestion("_airdrop._tcp.local.", dns.TypePTR) // 查询 AirDrop 服务实例名
m.RecursionDesired = false
r, _, err := c.Exchange(m, "224.0.0.251:5353") // 发送至本地 mDNS 组播地址
if err != nil {
log.Fatal(err)
}
for _, ans := range r.Answer {
if ptr, ok := ans.(*dns.PTR); ok {
log.Printf("发现 AirDrop 设备:%s", ptr.Ptr)
// 后续可对 PTR 名发起 SRV/TXT 查询获取完整元数据
}
}
}
该代码仅完成被动发现环节;完整复现需补充:
- TXT 记录解析逻辑(提取
deviceid、pkh、vers字段) - TLS 握手模拟(使用 Apple 公开文档中定义的证书链验证规则)
- 临时密钥协商(ECDH over secp256r1,配合 HKDF-SHA256 派生会话密钥)
值得注意的是,iOS 17+ 引入了更严格的 AirDrop 隐私控制(如“仅限联系人”模式),其服务广播行为受 com.apple.sharingd 守护进程动态调控,真实环境需结合 CoreBluetooth 扫描辅助判断设备活跃状态。协议逆向过程中,建议优先参考苹果开源项目 mDNSResponder 中的 dnssd_clientlib.c 与 mDNSEmbeddedAPI.h 接口定义。
第二章:Bonjour服务发现与mDNS协议解析
2.1 Bonjour协议原理与AirDrop设备广播机制分析
Bonjour 是 Apple 实现的零配置网络(Zeroconf)协议栈,基于 DNS-SD(DNS Service Discovery)、mDNS(multicast DNS)和 DHCP 辅助发现机制。AirDrop 利用其构建可信邻近设备发现通道。
广播服务类型与端口
AirDrop 注册的服务类型为:
# AirDrop 在 mDNS 中广播的服务实例名与类型
_airdrop._tcp.local. # 服务类型
12345 # 动态分配端口(通常为随机高危端口)
该记录包含 TXT 记录字段如 txtvers=1, dvty=MacBookPro, model=MacBookPro18,3,用于设备能力协商。
设备发现流程
graph TD
A[本机启动AirDrop] --> B[通过mDNS向224.0.0.251:5353发送SRV+TXT查询]
B --> C[局域网内响应 _airdrop._tcp.local. 服务]
C --> D[解析TXT获取设备标识与公钥指纹]
D --> E[建立TLS 1.3加密直连]
关键参数说明
| 字段 | 含义 | 示例 |
|---|---|---|
pk=... |
Base64编码的Ed25519公钥 | pk=MEIw... |
uuid= |
设备唯一标识(非硬件ID,经哈希脱敏) | uuid=3A2B... |
ph= |
设备照片哈希(用于UI识别) | ph=sha256:... |
2.2 Go语言实现mDNS查询与响应的底层封装
mDNS协议依赖UDP多播通信,Go标准库未直接提供完整支持,需基于net.PacketConn与net.Interface进行精细化控制。
核心通信层封装
// 创建绑定到IPv4多播地址224.0.0.251:5353的UDP连接
conn, err := net.ListenPacket("udp4", ":5353")
if err != nil {
panic(err) // 实际应使用结构化错误处理
}
// 设置多播TTL和接口绑定(关键:避免跨网段泛洪)
conn.(*net.UDPConn).SetReadBuffer(65536)
该代码建立监听端点,":5353"表示通配所有本地接口;SetReadBuffer提升高并发场景下的包接收吞吐能力。
协议解析关键字段映射
| 字段名 | 类型 | 说明 |
|---|---|---|
QR |
bool | 查询(0)/响应(1)标志位 |
QDCOUNT |
uint16 | 问题节区数量(通常为1) |
ANCOUNT |
uint16 | 回答资源记录数(响应时非零) |
数据同步机制
graph TD
A[收到UDP包] --> B{解析DNS报文头}
B -->|QR==0| C[构造响应报文]
B -->|QR==1| D[匹配本地服务注册表]
C --> E[异步写入conn]
D --> E
2.3 设备名解析、TXT记录提取与服务实例动态注册
DNS-SD 协议核心三要素
在基于 mDNS 的服务发现中,设备名解析、TXT 记录提取和服务实例注册构成闭环流程:
- 设备名(如
printer._ipp._tcp.local)用于反向解析到 IPv4/IPv6 地址 - TXT 记录携带元数据(如
ty=HP LaserJet,note=Floor 3) - 动态注册需响应 TTL 过期、网络切换等事件,触发
DNSServiceRegister()重注册
TXT 记录解析示例
// 解析 mDNS 响应中的 TXT 数据(RFC 6763 §6)
uint8_t txt_data[] = {0x0B, 't', 'y', '=', 'H', 'P', ' ', 'L', 'a', 's', 'e', 'r',
0x05, 'n', 'o', 't', 'e', '=', 'F', 'l', 'o', 'o', 'r', ' ', '3'};
// 首字节为长度前缀,后续为键值对(无空格分隔,= 为分界)
逻辑分析:每个 TXT 元素以 1 字节长度开头,随后是 UTF-8 编码的 key=value 字符串;解析器需逐段跳过长度字节,并按 = 分割键值,忽略缺失 = 的非法项。
服务注册状态机
graph TD
A[启动注册] --> B{mDNS 可用?}
B -- 是 --> C[调用 DNSServiceRegister]
B -- 否 --> D[退避重试]
C --> E{注册成功?}
E -- 是 --> F[监听 TXT 更新]
E -- 否 --> D
| 注册阶段 | 关键参数 | 说明 |
|---|---|---|
| 实例名 | printer-lab-01 |
必须全局唯一,建议含位置/序列信息 |
| 协议名 | _ipp._tcp |
标准服务类型,决定端口与语义 |
| TXT 数据 | ty=HP LaserJet |
键值对数组,最大 65535 字节 |
2.4 多网卡环境下的mDNS包路由与接口绑定策略
在多网卡主机中,mDNS(RFC 6762)默认通过所有支持IPv4/IPv6的活跃接口广播查询,但响应需严格绑定到接收查询的同一接口,否则违反“源接口一致性”原则。
接口选择逻辑
- 系统按
ifindex顺序遍历接口,跳过lo、down或无链路本地地址(169.254.0.0/16或fe80::/10)的接口 - 每个接口独立运行 mDNS responder 实例(如
avahi-daemon的 per-interface mode)
绑定配置示例(avahi-daemon.conf)
[server]
# 显式指定监听接口(优先级高于自动发现)
allow-interfaces=eth0,wlan0
# 禁用特定接口(如Docker桥接网卡)
deny-interfaces=docker0,virbr0
此配置强制 avahi 仅在
eth0和wlan0上注册服务并响应查询;deny-interfaces优先级高于allow-interfaces,用于隔离虚拟网络平面。
常见路由冲突场景
| 场景 | 影响 | 推荐策略 |
|---|---|---|
| 同一子网多物理接口(如双千兆聚合) | 查询重复、响应竞争 | 启用 reflector=no + 手动 bind-to-interface=yes |
| IPv4/IPv6 双栈同接口 | 两套独立 mDNS 流量 | 无需干预,默认隔离 |
# 查看当前 mDNS 绑定状态(Linux)
ip -br link | awk '$3 ~ /UP/ {print $1}' | \
xargs -I{} sh -c 'echo "{}: $(ip -4 addr show dev {} 2>/dev/null | grep "inet 169.254" | wc -l) IPv4LL"'
该命令枚举所有 UP 状态接口,并统计其 IPv4 链路本地地址数量——仅含有效
169.254.0.0/16地址的接口才参与 mDNS。无此地址的接口即使 UP 也不会被 avahi 选中。
2.5 实时服务列表维护与超时剔除的并发安全设计
数据同步机制
服务注册/下线需原子更新内存列表与心跳时间戳,避免读写竞争。
// 使用 ConcurrentHashMap + ScheduledExecutorService 实现无锁剔除
private final ConcurrentHashMap<String, ServiceInstance> registry = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Long> lastHeartbeat = new ConcurrentHashMap<>();
public void heartbeat(String serviceId) {
lastHeartbeat.compute(serviceId, (k, v) -> System.currentTimeMillis()); // 原子更新时间
}
compute() 保证时间戳更新线程安全;ConcurrentHashMap 支持高并发读,规避 synchronized 全局锁瓶颈。
超时剔除策略
定时扫描(如每5秒)并移除超时实例(默认30秒无心跳):
| 检查项 | 值 | 说明 |
|---|---|---|
| 心跳超时阈值 | 30_000 ms | 可动态配置 |
| 扫描间隔 | 5_000 ms | 平衡实时性与CPU开销 |
| 剔除前校验 | CAS compareAndSet | 防止重复删除 |
状态一致性保障
graph TD
A[心跳上报] --> B{registry.putIfAbsent?}
B -->|新实例| C[写入registry & lastHeartbeat]
B -->|已存在| D[仅更新lastHeartbeat]
E[定时剔除线程] --> F[遍历lastHeartbeat]
F --> G[CAS移除过期entry]
第三章:TLS 1.3信道建立与身份认证体系
3.1 AirDrop TLS握手流程逆向与证书链信任模型解构
AirDrop 在 iOS/macOS 设备间建立 TLS 连接时,并非使用系统默认信任根,而是依赖 com.apple.idmsa 签发的临时设备证书,由 Apple Mobile File Integrity (AMFI) 动态注入信任锚。
TLS 握手关键阶段
- 客户端发送
ClientHello,含ALPN: "airdrop"扩展 - 服务端响应
ServerHello+Certificate(含设备证书、中间 CAApple Airdrop Signing Intermediate CA) - 双方验证证书链是否可追溯至
Apple Root CA - G3(硬编码于Security.framework)
证书链信任路径示例
| 证书类型 | 颁发者 | 有效期 | 是否嵌入 Trust Store |
|---|---|---|---|
| Leaf(设备证书) | Apple Airdrop Signing Intermediate CA | 24h(动态轮换) | 否 |
| Intermediate CA | Apple Root CA – G3 | 多年 | 是(/System/Library/Keychains/SystemRootCertificates.keychain) |
// 逆向提取的证书验证逻辑片段(来自 Security.framework 符号化分析)
func validateAirdropCert(_ cert: SecCertificateRef) -> Bool {
let trust = SecTrustCreateWithCertificates(
[cert],
SecPolicyCreateSSL(true, "airdrop.apple.com") // 强制主机名匹配 air-drop.apple.com
)
SecTrustSetAnchorCertificates(trust, [appleRootCA]) // 显式设置信任锚,绕过系统默认根列表
return SecTrustEvaluateWithError(trust, nil)
}
该调用强制将 Apple Root CA - G3 设为唯一信任锚,忽略用户安装的第三方根证书,体现 AirDrop 的封闭信任模型。证书吊销通过 OCSP Stapling 实现,响应由 idmsa.apple.com 动态签发。
graph TD
A[Device A ClientHello] --> B[Device B ServerHello + Certificate]
B --> C{Validate Chain}
C --> D[Leaf → Intermediate → Apple Root CA - G3]
D --> E[OCSP Staple check via idmsa.apple.com]
E --> F[TLS 1.2 Session Key Exchange]
3.2 Go crypto/tls定制化配置:禁用不安全套件与强制ECDHE密钥交换
TLS 安全性高度依赖于协商时的密码套件选择。Go 默认启用部分已过时的套件(如 TLS_RSA_WITH_AES_128_CBC_SHA),其缺乏前向安全性且易受填充预言攻击。
禁用不安全套件与强制 ECDHE
config := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP256, tls.CurveP384},
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
}
此配置显式排除所有 RSA 密钥交换套件(无前向安全)和 CBC 模式套件(易受 Lucky13 攻击),仅保留 AEAD 类型(GCM/CCM)并强制使用 ECDHE——确保每次握手生成临时密钥,抵御长期私钥泄露风险。
推荐套件兼容性对照表
| 套件名称 | 前向安全 | AEAD | Go 1.12+ 支持 | 适用场景 |
|---|---|---|---|---|
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 |
✅ | ✅ | ✅ | 通用服务(RSA 证书) |
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 |
✅ | ✅ | ✅ | 高安全要求 + EC 证书 |
协商流程关键约束(mermaid)
graph TD
A[ClientHello] --> B{Server 检查 CipherSuites 列表}
B --> C[仅匹配 config.CipherSuites 中项]
C --> D[强制 ECDHE 密钥交换]
D --> E[拒绝 TLS_RSA_* / *_CBC_* 套件]
3.3 基于设备UID派生的临时证书生成与双向mTLS验证实现
设备首次接入时,服务端依据其唯一硬件 UID(如 Secure Element 中的 ECDSA 公钥哈希)动态派生临时证书密钥对,避免预置证书带来的生命周期管理难题。
证书派生流程
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
def derive_device_key(uid: bytes) -> bytes:
# 使用 HKDF-SHA256 从 UID 派生 32 字节密钥
return HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=b"mtls-device-key-salt",
info=b"device-ephemeral-key"
).derive(uid)
逻辑说明:uid 为不可克隆的硬件标识(如芯片 OTP 区哈希值);salt 和 info 实现上下文隔离,确保不同用途密钥不重叠。
双向验证关键参数
| 角色 | 证书有效期 | OCSP Stapling | CRL 检查 |
|---|---|---|---|
| 设备端 | 15 分钟 | 启用 | 禁用(带内吊销通过 JWT 声明) |
| 服务端 | 24 小时 | 启用 | 启用 |
交互流程
graph TD
A[设备提交 UID + 签名挑战] --> B[服务端派生密钥并签发临时证书]
B --> C[设备加载证书并发起 mTLS 握手]
C --> D[服务端校验证书签名链 & 设备 UID 绑定]
D --> E[双向身份确认,建立加密信道]
第四章:CBOR序列化协议与三重握手状态机
4.1 AirDrop CBOR消息结构逆向:Announce/Invite/Accept/Confirm语义解析
AirDrop底层通信依赖精简的CBOR编码消息,其四类核心交互承载明确状态机语义:
消息类型语义映射
Announce:广播设备存在,含签名公钥与服务标识(kCBServiceTypeAirDrop)Invite:单播发起传输请求,携带目标文件元数据哈希与加密NonceAccept:接收方授权,返回对称密钥派生参数(HKDF-salt + info)Confirm:最终握手,含AES-GCM认证标签与传输会话ID
典型CBOR解码片段(Announce)
# Announce payload (hex: a3016a...), decoded:
{
1: "AirDrop", # service name (key 1)
2: h'abcd1234...', # device public key (key 2)
3: 1 # protocol version (key 3)
}
逻辑分析:键值采用整数标签(RFC 7049)压缩体积;key 2为X.509 SPKI DER序列的SHA-256哈希,用于无证书身份锚定;key 3版本号控制协议兼容性降级策略。
状态流转约束
graph TD
A[Announce] -->|unicast trigger| B[Invite]
B -->|signed consent| C[Accept]
C -->|GCM-tag verification| D[Confirm]
D -->|session close| E[Data Transfer]
| 字段 | 类型 | 长度 | 用途 |
|---|---|---|---|
nonce |
bytes | 12 | AES-CTR 初始化向量 |
auth_tag |
bytes | 16 | GCM 认证标签 |
session_id |
uint64 | 8 | 唯一会话标识(时间戳+随机) |
4.2 Go cbor库深度定制:自定义Tag映射与零拷贝解码优化
自定义Tag映射:脱离json标签束缚
CBOR解码默认忽略结构体字段标签,需显式启用cbor标签支持:
type User struct {
ID uint64 `cbor:"id,keyasint"`
Name string `cbor:"name"`
Tags []byte `cbor:"tags,raw"` // 零拷贝保留原始CBOR字节
}
keyasint使字段名以整数键编码(如代替"id"),raw跳过解析直接透传字节流,为后续延迟解码预留接口。
零拷贝解码:避免中间内存分配
使用Unmarshaler接口配合[]byte切片视图:
| 优化项 | 传统方式 | 零拷贝方式 |
|---|---|---|
| 字段解码开销 | 复制+反序列化 | 直接切片引用 |
| 内存分配次数 | ≥3次 | 0次(仅指针偏移) |
| 适用场景 | 小对象通用解码 | 日志/消息批量解析 |
解码流程示意
graph TD
A[CBOR字节流] --> B{含raw标签字段?}
B -->|是| C[跳过解析,保存切片指针]
B -->|否| D[标准字段解码]
C --> E[按需调用cbor.Unmarshal]
4.3 三重握手状态机建模:从Discover到DataChannel Ready的完整流转
WebRTC连接建立并非原子操作,而是由RTCPeerConnection驱动的确定性状态跃迁过程。其核心是三重握手协议——在信令层协调下,完成能力协商、密钥交换与数据通道就绪确认。
状态跃迁关键节点
stable→have-local-offer(调用createOffer()后)have-remote-offer→stable(setRemoteDescription()成功)DataChannel触发open事件前,必须满足ICE连接状态为connected且DTLS为connected
状态机核心逻辑(简化版)
// 状态监听示例:仅响应有效跃迁
pc.onconnectionstatechange = () => {
if (pc.connectionState === 'connected') {
// 此时DTLS已握手完成,但DataChannel仍需显式open()
const dc = pc.createDataChannel('app');
dc.onopen = () => console.log('✅ DataChannel Ready'); // 最终态
}
};
该回调不保证ICE或DTLS已就绪,需组合监听
iceConnectionState与dtlsTransport.state。connectionState是聚合视图,仅当二者均为connected时才置为connected。
状态流转约束表
| 源状态 | 目标状态 | 触发条件 |
|---|---|---|
| stable | have-local-offer | createOffer() + setLocalDescription() |
| have-remote-offer | stable | setRemoteDescription()成功 |
| connecting | connected | ICE+DTLS双栈同时就绪 |
graph TD
A[Discover] -->|offer/answer exchange| B[have-local-offer]
B --> C[have-remote-offer]
C --> D[stable<br>ICE: checking → connected<br>DTLS: connecting → connected]
D --> E[DataChannel Ready]
4.4 握手失败回退机制与重传指数退避的Go协程安全实现
协程安全的退避状态管理
使用 sync/atomic 管理当前重试阶数,避免锁竞争:
type BackoffState struct {
attempt uint32 // 原子递增的尝试次数(0起始)
baseDelay time.Duration // 初始延迟,如 100ms
}
func (b *BackoffState) NextDelay() time.Duration {
n := atomic.AddUint32(&b.attempt, 1)
// 指数退避:delay = base × 2^n,上限 5s
delay := b.baseDelay << n
if delay > 5*time.Second {
delay = 5 * time.Second
}
return delay
}
逻辑说明:
<< n实现base × 2^n的位移加速计算;atomic.AddUint32保证多协程并发调用NextDelay()时attempt严格单调递增且无竞态。
退避策略对比表
| 策略 | 优点 | 协程安全性风险 |
|---|---|---|
time.Sleep + 全局变量 |
简单直观 | 高(需 mutex 保护) |
atomic + 无锁计数 |
零分配、无锁 | 无 |
channel 控制节流 |
可取消、可中断 | 中(需额外同步) |
重试流程(mermaid)
graph TD
A[发起握手] --> B{成功?}
B -- 否 --> C[调用 NextDelay]
C --> D[time.After(delay)]
D --> E[启动新协程重试]
B -- 是 --> F[返回连接]
E --> B
第五章:限时开源前最后24小时:代码审计与性能压测总结
关键漏洞修复清单
在凌晨3:17完成的静态扫描(SonarQube 9.9 + Semgrep 自定义规则集)共捕获127处高危问题,其中8处被标记为阻断级(Blocker)。典型案例如下:
auth/jwt.go中硬编码的调试密钥dev-secret-2024已替换为环境变量注入;api/v2/transfer.go的 SQL 拼接逻辑重构为参数化查询,消除全部sql.ErrNoRows误判路径;- 所有
/debug/*路由已通过中间件强制校验X-Internal-Token,生产环境配置文件中该 header 值设为空字符串触发拦截。
压测场景与真实数据对比
使用 k6 v0.45.1 对核心支付链路执行阶梯式负载测试,持续时间180分钟,结果如下表:
| 并发用户数 | P95 响应延迟(ms) | 错误率 | CPU 峰值(8核) | 内存泄漏(GB/h) |
|---|---|---|---|---|
| 500 | 86 | 0.02% | 62% | 0.03 |
| 2000 | 214 | 0.87% | 94% | 0.11 |
| 5000 | 492 | 12.3% | 100%(持续3min) | 1.8 |
定位到瓶颈源于 redis.Client.Pipeline() 在连接池耗尽时未设置 context.WithTimeout,已补全超时控制并增加熔断阈值(maxFailures=50/s)。
审计工具链协同工作流
flowchart LR
A[Git Pre-commit Hook] --> B[TruffleHog 扫描密钥]
B --> C[GoSec 检查 unsafe 包调用]
C --> D[自研 DiffGuard 分析 PR 修改行]
D --> E[自动触发 CI 环境压测]
E --> F[结果写入 GitHub Checks API]
生产就绪检查项
- [x] Prometheus metrics 端点
/metrics返回格式验证(确认包含http_request_duration_seconds_bucket标签) - [x] 所有日志输出经
zap.With(zap.String(\"trace_id\", reqID))统一注入上下文 - [x] Docker 镜像基础层切换为
gcr.io/distroless/static:nonroot,移除 shell 与包管理器 - [x] OpenAPI v3 spec 中
429 Too Many Requests响应体字段与实际限流中间件返回完全一致
紧急回滚机制验证
在 staging 环境模拟主库不可用场景,验证了以下流程:
- 应用启动时检测
DB_URL连通性失败 → 触发healthcheck-fallback模式; - 所有写操作降级为本地 LevelDB 缓存,读请求返回缓存副本(TTL=30s);
- 通过
/v1/fallback/status接口可实时查看降级状态及缓存命中率; - 主库恢复后,后台 goroutine 自动同步差异数据(基于 WAL 日志序列号比对)。
开源合规性终审
- SPDX License Identifier 已嵌入全部 Go 文件头部:
// SPDX-License-Identifier: Apache-2.0; NOTICE文件明确列出第三方组件github.com/gorilla/mux v1.8.0的版权归属;SECURITY.md中定义的漏洞披露流程已通过 HackerOne 模拟提交验证(响应时间≤2小时)。
性能基线归档
本次压测生成的完整 trace 数据(含 Jaeger JSON 导出包、pprof CPU profile、heap profile)已加密上传至 S3 存储桶 oss-audit-2024-q3,对象键名格式为 build-7a3f2c/{timestamp}/perf-{env}.tar.gz,密钥由 HashiCorp Vault 动态分发。
最终镜像签名验证
使用 Cosign v2.2.1 对 ghcr.io/myorg/payment-service:v1.0.0-rc1 执行签名验证:
cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp 'https://github\.com/myorg/.*/.github/workflows/ci.yml@refs/heads/main' \
ghcr.io/myorg/payment-service:v1.0.0-rc1
输出显示签名证书由 GitHub Actions OIDC 提供商颁发,且 subject 字段匹配预设正则表达式。
