第一章:微信支付Go SDK的核心架构与设计哲学
微信支付Go SDK并非简单封装HTTP请求的工具集,而是一个以领域驱动设计(DDD)为指导、兼顾安全与可扩展性的支付中间件。其核心架构采用分层解耦模型:最上层为业务接口层(如 UnifiedOrder, QueryOrder),中层为统一网关调度器(Client),底层则由签名引擎、证书管理器、HTTP传输层和序列化模块构成。各层之间通过接口契约通信,确保支付逻辑与网络细节、加密实现完全隔离。
签名与安全设计原则
SDK默认采用微信支付V3版签名机制,要求所有敏感请求携带 Authorization 头。签名过程严格遵循“时间戳+随机字符串+请求体哈希+私钥签名”四元组合,且私钥全程不暴露于HTTP客户端。开发者需通过 wechat.NewClient() 显式注入 *rsa.PrivateKey,SDK内部自动完成PKCS#1 v1.5签名与Base64编码:
// 初始化客户端时绑定商户私钥(从PEM文件加载)
privKey, _ := ioutil.ReadFile("apiclient_key.pem")
key, _ := jwt.ParseRSAPrivateKeyFromPEM(privKey)
client := wechat.NewClient("your-mch-id", key)
配置驱动的灵活性
SDK将环境差异抽象为 Config 结构体,支持沙箱模式、正式环境一键切换。关键字段包括 BaseURL、CertPath(平台证书路径)、Timeout 等,避免硬编码污染业务逻辑。
可观测性内建能力
所有HTTP请求自动注入 X-Request-ID 与 User-Agent: wechat-go-sdk/1.x,并提供 WithLogger 选项接入结构化日志系统(如 zap):
| 能力 | 默认行为 | 自定义方式 |
|---|---|---|
| 请求重试 | 最多2次指数退避重试 | client.WithRetry(3) |
| 响应解密(回调通知) | 自动校验签名并AES-256-GCM解密 | client.DecryptNotify() |
| 错误分类 | 细粒度错误码映射(如 ErrInvalidSign) | 实现 ErrorHandler 接口 |
该设计哲学强调“约定优于配置、安全内建、失败可追溯”,使开发者聚焦于支付业务语义而非基础设施细节。
第二章:签名验证失效——资金安全的第一道防线崩塌
2.1 微信签名算法(HMAC-SHA256-with-RSA)的Go语言实现原理与常见偏差
微信支付V3接口要求使用 HMAC-SHA256-with-RSA 签名:先对请求体做 HMAC-SHA256 摘要,再用商户私钥对摘要值进行 RSA 签名(非直接对原始数据签名)。
核心流程
- 提取待签名字符串(含时间戳、随机串、请求路径、请求体哈希)
- 计算
HMAC-SHA256(key=apiV3Key, data=canonicalString) - 将 HMAC 结果作为
[]byte输入,调用rsa.SignPKCS1v15
h := hmac.New(sha256.New, []byte(apiV3Key))
h.Write([]byte(canonicalString))
mac := h.Sum(nil)
sig, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, mac)
// 注意:第三个参数必须是 crypto.SHA256,与HMAC输出长度严格匹配
✅ 正确:HMAC 输出 32 字节 → 匹配
crypto.SHA256
❌ 常见偏差:误用rsa.SignPKCS1v15(..., nil, crypto.Hash(0), mac)或对原始 JSON 直接签名
| 偏差类型 | 后果 |
|---|---|
| 混淆 HMAC 与 RSA 输入 | 签名验证失败 |
| 时间戳未同步(>±300s) | 平台拒绝请求 |
graph TD
A[构造规范字符串] --> B[HMAC-SHA256 with apiV3Key]
B --> C[32字节摘要]
C --> D[RSA-PKCS#1 v1.5 签名]
2.2 商户私钥加载错误导致验签绕过的实战复现与断点调试
复现环境准备
- Spring Boot 2.7.18 + Alipay SDK 4.10.131.ALL
- 模拟商户配置中
privateKey字段为空字符串或 PEM 头缺失
关键代码片段
// com.alipay.api.internal.util.AlipaySignature#sign
public static String sign(String content, String privateKey, String charset)
throws AlipayApiException {
if (StringUtils.isEmpty(privateKey)) {
throw new AlipayApiException("Private key is empty"); // 实际SDK中此处被静默跳过!
}
// ... RSA签名逻辑
}
逻辑分析:Alipay SDK 4.10.131.ALL 中
sign()方法对空私钥仅打印 warn 日志,未抛异常;下游业务误判“签名成功”,返回伪造sign=参数。
验签绕过路径
graph TD
A[商户调用/alipay/pay] --> B{privateKey==null?}
B -->|是| C[返回空签名]
B -->|否| D[执行RSA签名]
C --> E[支付网关验签时使用公钥解密失败→降级为MD5校验]
E --> F[攻击者篡改amount参数后重放请求]
断点定位建议
- 在
AlipaySignature.verify()入口设断点,观察map.get("sign")与map.get("sign_type") - 检查
KeyFactory.getInstance("RSA").generatePrivate(...)是否返回null
2.3 V3 API中证书序列号与平台证书自动刷新机制的误用场景分析
误用根源:序列号硬编码导致签名失效
当开发者将 serial_no 字段静态写入配置而非动态读取平台证书接口响应,会导致签名验签失败:
# ❌ 错误示例:硬编码序列号(证书轮换后立即失效)
headers = {
"Wechatpay-Serial": "1a2b3c4d5e6f7890", # 硬编码,不可维护
"Authorization": generate_auth(...)
}
逻辑分析:Wechatpay-Serial 必须与当前有效平台证书的 serial_no 严格一致;微信侧每30天自动轮换证书,硬编码值在轮换后即失效,引发 401 Unauthorized。
自动刷新机制被绕过的典型路径
- 调用
/v3/certificates接口后未持久化新证书及对应serial_no - 缓存证书但未监听
Wechatpay-Cert-Signature响应头触发更新 - 多实例服务共享单点证书缓存,未实现分布式刷新通知
关键参数对照表
| 参数名 | 来源 | 更新周期 | 误用后果 |
|---|---|---|---|
serial_no |
/v3/certificates 响应体 |
~30天 | 签名验签失败 |
encrypt_certificate.nonce |
响应体加密字段 | 每次请求唯一 | 解密失败 |
graph TD
A[调用/v3/certificates] --> B{是否解析并更新serial_no?}
B -->|否| C[后续请求签名失败]
B -->|是| D[更新本地证书+serial_no缓存]
D --> E[正常签名与验签]
2.4 时间戳与随机字符串生成不合规引发的重放攻击实测案例
攻击复现环境
测试接口采用简易鉴权模式:sign=md5(timestamp+nonce+secret),但 timestamp 未校验窗口(允许±300秒),且 nonce 为固定16位UUID(服务端未去重缓存)。
关键缺陷分析
- 时间戳无单调递增校验,仅比对绝对差值
nonce生成未绑定用户会话,且未写入Redis防重库- 签名密钥硬编码在客户端APK中(逆向可提取)
漏洞利用流程
import time, hashlib, requests
# 攻击者截获合法请求:t=1717025480, nonce="a1b2c3d4e5f67890"
t = 1717025480
nonce = "a1b2c3d4e5f67890"
secret = "hardcoded_key" # 逆向获取
sign = hashlib.md5(f"{t}{nonce}{secret}".encode()).hexdigest()
# 重放100次(服务端未拒绝重复nonce)
for i in range(100):
requests.post("https://api.example.com/pay",
data={"amount": "0.01", "timestamp": t, "nonce": nonce, "sign": sign})
逻辑说明:
t在服务端校验范围(±300秒)内恒有效;nonce因缺失Redis SETNX去重逻辑,被反复接受;sign复用导致鉴权绕过。
防御对比表
| 措施 | 是否缓解 | 原因 |
|---|---|---|
| timestamp ±30s校验 | ✅ | 缩小重放时间窗 |
| nonce Redis SETNX + TTL | ✅ | 强制单次使用+自动过期 |
| 客户端签名密钥动态化 | ❌ | 仍需TLS+证书双向认证兜底 |
graph TD
A[客户端生成timestamp] --> B{服务端校验}
B --> C[|Δt| ≤ 30s?]
C -->|否| D[拒绝]
C -->|是| E[检查nonce是否已存在]
E -->|是| D
E -->|否| F[写入Redis并TTL=60s]
2.5 混淆v2/v3签名上下文(如body未规范化JSON序列化)的典型panic日志溯源
当客户端使用 v2 签名但服务端按 v3 规范校验时,若请求 body 未执行 确定性 JSON 序列化(如字段顺序混乱、空格/换行不一致、浮点数精度丢失),将触发签名不匹配 panic。
典型 panic 日志片段
panic: signature verification failed: expected "abc123", got "def456"
at verifySignatureV3(...)
at http.HandlerFunc.ServeHTTP(...)
该 panic 表明
verifySignatureV3在计算hmac-sha256(signingString)前,错误地对原始*http.Request.Body直接读取(未重置 offset),且未调用json.Marshal()的规范版本(如jsoniter.ConfigCompatibleWithStandardLibrary.Marshal()),导致signingString中 body 摘要失真。
v2/v3 签名上下文关键差异
| 维度 | v2 签名上下文 | v3 签名上下文 |
|---|---|---|
| Body 处理 | 原始字节流(不解析) | 必须规范化 JSON(排序键、无空格) |
| 时间戳格式 | 2006-01-02T15:04:05Z |
20060102T150405Z(无分隔符) |
| 签名算法 | HMAC-SHA256(credential) | HMAC-SHA256(derived-key + signing) |
根因流程图
graph TD
A[Client sends request] --> B{Body serialized?}
B -->|No: map[string]interface{} → json.Marshal| C[v3 signingString includes unordered JSON]
B -->|Yes: canonicalized jsoniter.Marshal| D[stable digest]
C --> E[Panic: signature mismatch]
第三章:异步通知处理失当——订单状态与资金流严重脱钩
3.1 未严格校验通知回调URL路径及HTTP方法导致的伪造通知注入
漏洞成因剖析
当支付网关或第三方服务向商户系统发起异步通知(如订单状态更新)时,若仅校验签名而忽略 callback_url 的路径规范性与 HTTP 方法约束,攻击者可构造如下恶意请求:
POST /api/notify?path=../../admin/webhook HTTP/1.1
Host: merchant.com
Content-Type: application/json
{"order_id":"ORD-999","status":"success"}
逻辑分析:服务端未对
callback_url做白名单路径匹配(如仅允许/api/v1/notify),也未强制要求POST方法——导致攻击者可将通知重定向至任意路径(如越权接口),甚至通过GET请求绕过 CSRF 防护。
防御关键点
- ✅ 强制校验回调 URL 的 Host、Path 前缀与协议(
https://merchant.com/api/notify) - ✅ 严格限定仅接受
POST方法,拒绝GET/PUT等非常规方法 - ❌ 禁止动态拼接回调路径(如
url.Parse(req.FormValue("url")))
| 校验维度 | 安全实现示例 | 危险模式 |
|---|---|---|
| 路径匹配 | strings.HasPrefix(rawPath, "/api/notify") |
strings.Contains(rawPath, "notify") |
| 方法约束 | if r.Method != "POST" { return } |
忽略 Method 直接处理 |
graph TD
A[收到通知请求] --> B{校验URL路径是否在白名单内?}
B -->|否| C[拒绝并记录告警]
B -->|是| D{HTTP方法是否为POST?}
D -->|否| C
D -->|是| E[验证签名+业务逻辑]
3.2 异步通知响应延迟超时(>5s)引发微信重复推送与重复入账的Go并发修复方案
微信支付异步通知要求商户服务在 5秒内返回 HTTP 200,否则将触发重试(最多5次)。当 Go 服务因数据库阻塞、未加超时控制或同步逻辑过长导致响应延迟,极易引发重复通知→重复处理→重复入账。
核心问题定位
- 微信回调无幂等令牌校验
- 处理链路未分离 I/O 与业务逻辑
- 缺乏通知唯一性去重与状态预置
并发安全修复策略
func handleWechatNotify(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) // 强制响应超时
defer cancel()
// 1. 快速解析并提取 out_trade_no + notify_id(微信唯一推送ID)
notifyID := r.FormValue("notify_id")
outTradeNo := r.FormValue("out_trade_no")
// 2. 原子预占:用 notify_id 作为 Redis 键,SETNX + EX 60s
if !redisClient.SetNX(ctx, "wx:notify:"+notifyID, "processing", 60*time.Second).Val() {
http.Error(w, "OK", http.StatusOK) // 已处理,静默响应
return
}
// 3. 异步落库(不阻塞HTTP响应)
go func() {
_ = processOrder(ctx, outTradeNo) // 含DB操作、对账、消息投递
}()
w.WriteHeader(http.StatusOK) // ≤2s 内返回
}
逻辑说明:
context.WithTimeout确保 HTTP 层绝不超时;SETNX实现基于notify_id的全局幂等锁;go processOrder将耗时操作移交后台协程,避免阻塞主线程。Redis 过期时间设为60s,覆盖微信最长重试窗口。
关键参数对照表
| 参数 | 推荐值 | 说明 |
|---|---|---|
| HTTP 响应超时 | ≤2s | 预留3s缓冲应对网络抖动 |
| Redis 锁过期 | 60s | 覆盖微信最大重试间隔(约45s) |
| notify_id 生效范围 | 全局唯一 | 微信保证同一通知事件 notify_id 不变 |
数据同步机制
使用 Redis 锁 + 异步任务解耦后,订单状态更新与资金入账通过消息队列最终一致,避免 DB 直写阻塞。
graph TD
A[微信发起通知] --> B{Go 服务接收}
B --> C[2s内校验+SETNX锁]
C -->|成功| D[立即返回200]
C -->|失败| E[静默返回200]
D --> F[goroutine异步处理]
F --> G[DB更新+发MQ]
3.3 通知解密后未原子化更新数据库状态+发送成功响应,造成资金悬空的事务陷阱
核心问题场景
当支付网关解密异步通知(如支付宝回调)后,先更新订单状态为 paid,再调用 sendSuccessResponse(),但二者未包裹在单数据库事务中——若响应发送成功而数据库写入因网络抖动失败,将导致资金已扣、状态仍为 pending。
典型错误代码
// ❌ 非原子操作:解密→更新→响应分离
String notifyData = decrypt(request.getBody());
orderMapper.updateStatus(orderId, "paid"); // 可能失败
responseWriter.write("success"); // 已返回,无法回滚
逻辑分析:updateStatus 若抛出 SQLException 或超时,HTTP 响应已发出,上游视为交易成功;下游账务系统无状态变更依据,资金滞留“悬空”。
正确原子化方案
// ✅ 使用事务控制 + 幂等键保障
@Transactional
public void handleNotify(String encrypted) {
NotifyDto dto = decryptAndValidate(encrypted);
orderMapper.updateWithVersion(dto.orderId, "paid", dto.timestamp); // 乐观锁防重放
responseWriter.write("success");
}
状态一致性保障要素
- 数据库事务边界必须覆盖状态变更与响应触发点
- 引入幂等键(如
notify_id+sign)避免重复处理 - 失败时需主动向支付平台发起查询(
queryOrder)兜底
| 风险环节 | 原子化前 | 原子化后 |
|---|---|---|
| 数据库写入失败 | 资金悬空 | 事务回滚,响应不发 |
| 网络超时 | 状态不一致 | 全链路重试或告警 |
第四章:退款与分账逻辑漏洞——商户账户余额异常蒸发的深层根源
4.1 退款金额精度丢失(float64转int分单位)在高并发退款中的雪崩式误差累积
核心问题根源
浮点数在二进制中无法精确表示十进制小数(如 0.1),float64 存储 99.99 实际为 99.98999999999999...,强制 int() 截断导致向下舍入。
典型错误代码
func floatToCentsBad(amount float64) int {
return int(amount * 100) // ❌ 错误:未四舍五入,且 float64 乘法放大误差
}
// 示例:floatToCentsBad(99.99) → 9998(丢失1分)
逻辑分析:99.99 * 100 在 IEEE 754 下计算结果约为 9998.999999999998,int() 截断得 9998;参数 amount 应始终以字符串或整数分单位传入,避免浮点参与金额计算。
正确方案对比
| 方法 | 精度保障 | 并发安全 | 推荐场景 |
|---|---|---|---|
strconv.ParseInt(fmt.Sprintf("%.2f", x)*100, 10, 64) |
✅ | ⚠️(fmt 非零分配) | 低频调试 |
字符串解析("99.99" → 9999) |
✅✅ | ✅ | 生产高并发退款 |
误差累积示意
graph TD
A[1000笔退款] --> B[每笔丢失0.01分]
B --> C[累计误差10分]
C --> D[对账不平/财务风险]
4.2 分账接收方账号类型(sub_mchid vs openid)混淆引发的资金划拨失败静默丢包
分账接口要求严格区分接收方身份:sub_mchid 表示二级商户号(需已进件且开通分账权限),openid 仅适用于个人收款(如服务商模式下向用户返佣)。二者混用将导致微信支付网关直接静默丢弃请求,无错误码返回。
常见误用场景
- 将
openid错填至sub_mchid字段(字段名正确但值类型错误) - 在
type=PERSONAL_OPENID场景下遗漏account_type参数声明
请求参数对比表
| 字段 | 正确值示例 | 适用场景 | 必填 |
|---|---|---|---|
sub_mchid |
1900012345 |
二级商户分账 | ✅(type=SUB_MCHID) |
openid |
oAbcdefghijklmnopqrstuvw |
个人收款 | ✅(type=PERSONAL_OPENID) |
account_type |
SUB_MCHID / PERSONAL_OPENID |
显式声明类型 | ✅ |
# ❌ 错误示例:openid 填入 sub_mchid 字段
data = {
"sub_mchid": "oAbcdefghijklmnopqrstuvw", # 类型错配 → 静默丢包
"amount": 100,
"description": "返佣"
}
该请求因 sub_mchid 格式校验失败(非10位纯数字),微信后台直接拦截,不进入路由与风控流程,故无日志、无回调、无异常通知。
graph TD
A[发起分账请求] --> B{校验 sub_mchid 格式}
B -->|10位数字| C[继续鉴权/路由]
B -->|非数字/长度≠10| D[静默丢弃]
D --> E[无响应、无日志、无监控告警]
4.3 未校验原支付单是否支持分账即调用分账接口,触发微信侧强制冻结资金的拦截机制
微信支付分账能力依赖支付单创建时显式声明 sub_mch_id 或启用 profit_sharing: true。若跳过前置校验直接调用 https://api.mch.weixin.qq.com/v3/profitsharing/orders,微信将返回 ERR_CODE: INVALID_REQUEST 并冻结该订单全部可分账资金(T+1解冻)。
常见误操作路径
- 未查询
GET /v3/pay/transactions/id/{transaction_id}获取profit_sharing字段值 - 忽略
transaction_id对应的scene_info.payer_client_ip与原始下单IP不一致导致校验失败 - 使用 JSAPI 支付单但未在
trade_type=JSAPI下配置分账权限
正确校验逻辑(伪代码)
def can_profit_share(transaction_id):
resp = wx_api.get(f"/v3/pay/transactions/id/{transaction_id}")
# 必须同时满足:已成功支付 + 明确支持分账 + 非退款中状态
return (resp["trade_state"] == "SUCCESS"
and resp.get("profit_sharing", False) is True
and not resp.get("refund_status"))
参数说明:
profit_sharing字段为布尔值,仅当商户在商户平台开通分账且下单时传profit_sharing=true才为True;缺失该字段视为False。
微信资金冻结触发流程
graph TD
A[调用分账接口] --> B{原单 profit_sharing === true?}
B -- 否 --> C[返回400 + ERR_CODE: INVALID_REQUEST]
C --> D[微信后台自动冻结该订单全部待分账余额]
B -- 是 --> E[执行分账路由]
4.4 退款回调未关联原始支付请求ID(out_trade_no),导致对账系统无法闭环追溯
问题根源
退款通知(如微信/支付宝异步回调)中缺失 out_trade_no 字段,仅携带 refund_out_trade_no 和 transaction_id,致使无法反向映射到原始支付订单。
典型错误响应示例
{
"refund_out_trade_no": "REF20240515001",
"refund_id": "1234567890",
"refund_fee": 100,
"result_code": "SUCCESS"
// ❌ 缺失 "out_trade_no": "PAY20240514001"
}
该结构导致对账服务无法通过 out_trade_no 关联原始支付流水、金额、渠道与时间戳,破坏“支付→退款→核销”全链路一致性。
影响范围
- 对账差异率上升(日均漏匹配订单达 12.7%)
- 财务人工补单耗时增加 3.2 小时/日
- 审计无法验证退款业务合规性
修复方案关键点
- 支付网关层强制在退款回调中注入
out_trade_no(需上游渠道支持或本地缓存兜底) - 对账系统增加
transaction_id → out_trade_no反查缓存(TTL=72h)
graph TD
A[退款回调到达] --> B{含 out_trade_no?}
B -- 是 --> C[直接关联原始订单]
B -- 否 --> D[查 transaction_id 缓存]
D -- 命中 --> C
D -- 未命中 --> E[标记为待人工介入]
第五章:避坑指南与生产级SDK演进路线
命令行参数污染导致初始化失败
某金融客户集成v2.3 SDK时,因启动脚本中误传 -Dio.netty.leakDetection.level=paranoid,触发Netty内存泄漏检测器强制阻断Channel初始化,表现为 SDK init timeout after 30s。根本原因在于SDK内部未对JVM系统属性做白名单隔离。修复方案:在 SdkBootstrap.init() 中增加 System.getProperties().keySet().stream().filter(k -> k.toString().startsWith("io.netty.")).forEach(System::clearProperty) 防御性清理。
多线程并发注册监听器引发NPE
SDK v2.1 提供的 EventBus.register(listener) 方法未加锁,当5个以上线程同时调用时,底层 CopyOnWriteArrayList 的 addIfAbsent() 在特定JDK版本(OpenJDK 11.0.12)下偶发返回 null。线上事故复现率约0.7%,日志显示 java.lang.NullPointerException at com.example.sdk.event.EventBus.dispatch(EventBus.java:142)。解决方案:升级至v2.5+,该版本已将监听器容器替换为 ConcurrentHashMap<Listener, Boolean> 并添加 synchronized 包装层。
版本兼容性断裂点统计
| SDK版本 | 破坏性变更类型 | 影响范围 | 修复方式 |
|---|---|---|---|
| v1.8 → v2.0 | 移除 ConfigBuilder.setRetryTimes(int) |
12家客户 | 必须改用 RetryPolicy.builder().maxAttempts(3).build() |
| v2.2 → v2.3 | ApiResponse 泛型擦除导致反序列化失败 |
3家Kotlin项目 | 引入 TypeReference<T> 显式声明 |
| v2.4 → v2.5 | HTTP客户端从OkHttp切换为Apache HttpClient | 全部HTTPS代理客户 | 需重配 sslContext 和 socketFactory |
日志埋点失控引发磁盘打满
某电商APP接入SDK后,发现 /data/data/com.app/files/logs/ 目录每小时增长2.1GB。排查发现 LoggerFactory.getLogger("com.example.sdk.network") 默认开启 TRACE 级别,且未按包路径分级控制。临时方案:通过 LogManager.reset() 后执行 Logger.getLogger("com.example.sdk").setLevel(Level.WARN);长期方案:SDK v2.6起引入 LogLevelStrategy 接口,支持按网络状态动态降级(弱网时自动关闭body dump)。
flowchart LR
A[SDK初始化] --> B{是否首次启动?}
B -->|是| C[执行全量兼容性检查]
B -->|否| D[加载缓存的兼容性快照]
C --> E[扫描classpath中冲突的Guava版本]
D --> F[跳过耗时检查]
E --> G[若检测到guava-19.0.jar则抛出IncompatibleDependencyException]
异步回调线程池饥饿死锁
SDK v2.2 使用 Executors.newCachedThreadPool() 处理回调,某物流系统在高并发下单场景下出现 RejectedExecutionException。根源在于未设置队列容量上限,线程数暴涨至1200+,触发Linux线程数限制(/proc/sys/kernel/threads-max=1024)。生产环境紧急补丁:通过 SdkConfig.builder().callbackExecutorService(new ThreadPoolExecutor(4, 16, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100))) 替换默认线程池。
证书固定策略硬编码风险
v2.0 SDK将根证书哈希值写死在 TrustedCertManager.class 的静态块中,当Let’s Encrypt ISRG Root X1证书于2024年9月过期时,所有未升级SDK的Android 7.0以下设备无法建立TLS连接。后续版本改为从assets目录动态加载 cert_pins.json,支持运行时热更新:
{
"pins": [
{"domain": "api.example.com", "sha256": "yUOj4VfQmGZzR7XkL9WnT2YvP5CqB8N6H3J1K0M4S7A9"},
{"domain": "cdn.example.com", "sha256": "xV9P2L8QnR5WzY3KcF7T1J4H6N0B9M2S5A8E1D4G7I6"}
]
} 