# 凭证与密钥管理（生成 / 轮换 / 停用 / 应急吊销）

> docId：`XD-OP-12` ｜ version：`1.0.0` ｜ status：`PUBLISHED`

商家最关心的问题是「**怎么换钥匙不中断收款**」。本篇按实际操作顺序写清楚，包括**切换瞬间的真实行为**。

## 1. 你手上的凭证有哪些

| 凭证 | 用途 | 存放位置 |
| --- | --- | --- |
| `appId` | 应用标识（随六头发送） | 你的服务端配置 |
| `kid`（密钥标识） | 标明本次签名用的是哪把钥匙 | 你的服务端配置 |
| RSA2 密钥对 | 请求签名/验签 | **公钥在门户、私钥只在你本地** |
| IP 白名单 + 来源 IP 校验开关 | 请求来源判定（校验可自助开启/关闭） | 门户维护 |
| `notifyUrl` | 事件投递地址 | 应用配置 |
| `returnUrl` | 收银完成后的回跳地址（下单时冻结） | 下单请求 |

### 1.1 `kid` 长什么样、从哪来（集成方必读）

- **格式**：`mkid_` + 16 位小写十六进制（共 21 字符），例如 `mkid_5f3c1a7b52e84d06`。
- **来源**：`kid` **不是平台预先发放的**——在你上传公钥成功的那一刻，平台按公钥指纹派生并回显（第 2 节第 5 步）。**没有上传过公钥的应用，任何地方都不存在它的 `kid`**；对接面板里留一个「密钥标识」输入框、等你去"找"一个填，本身就是找错了方向：先生成密钥对并在门户上传公钥，`kid` 随即出现在门户「应用凭证」页。
- **本地派生（可自动计算，无需向平台索取）**：`kid` 是公钥的确定性函数，工具链可以本地算出同一个值——
  1. 从你的密钥对导出公钥 SPKI PEM（`-----BEGIN PUBLIC KEY-----` 头，RSA 2048/4096）；
  2. 规范化：CRLF→LF、去掉首尾空白（末尾不留换行）；
  3. `kid = "mkid_" + SHA-256(第 2 步文本的 UTF-8 字节) 的前 16 位小写十六进制`。

  只要上传到门户的 PEM 与本地计算用的是**同一份文本**（即「导出 → 规范化 → 上传」同一路径产出），本地算出的 `kid` 与平台回显必然一致。
- **两个 `kid` 别混**：门户「应用凭证」页同时展示两个——
  - 「**商户公钥 kid**」：**你自己的**，签名请求头 `X-XD-Key-Id` 填的就是它；
  - 「**平台公钥 kid**」：**平台的**签名密钥标识（用于你核验平台响应/事件签名），**不要**填进请求头，也不要填进对接配置的「密钥标识」项。

## 2. 生成密钥对

1. 在门户「应用凭证」页发起密钥生成。**密钥对在你的浏览器里本地生成**，私钥**从生成到保存都不经过平台**。
2. 私钥保存到你自己的受控存储（密钥管理服务或受权限保护的凭证文件），权限最小化。
3. 上传公钥时，平台会要求一次**持有证明**：用**新私钥**对一段**确定性挑战串**（与你的应用标识与公钥指纹绑定）签名。这证明你确实持有对应的私钥，而不是只复制了一个公钥。挑战串由门户当场给出、**直接复制使用**，不要自行拼装（同一应用 + 同一公钥永远得到同一串，所以「先算好再签」是可复现的）。
4. 提交后需通过**操作二次验证**（发到你绑定手机的操作码）——这是为了防止会话被盗后被人悄悄换钥匙。
5. 成功后平台回显**公钥指纹**与 `kid`；请把 `kid` 记到你的配置里。

> **挑战串是确定性的**：同样的应用与公钥指纹永远得到同一段挑战串。所以「先算好再签」是可复现的——这也是你本地自检的办法。

## 3. 轮换密钥（换钥匙）

**切换语义（必须准确理解，直接决定你会不会掉单）**：

- 轮换是**原子**的：提交成功后，**新 `kid` 立即生效**，你的请求必须立刻改用**新私钥**签名。
- **默认态下旧密钥在同一瞬间失效**：平台只接受当前生效的 `kid` 与公钥。历史 `kid` 在门户「密钥历史」里保留记录，用于审计与追溯，**默认不能继续用于签名**。
  > ⚠️ 门户的历史条目只显示「已轮换」这一事实，**不代表宽限窗已开启**——宽限窗只能由平台运营开启，且**门户目前不展示窗口状态与到期时间**（如需确认，请与对接人核对）。看到历史条目就去删旧配置的前提是「没开宽限窗」，不要反过来假设。
- 因此轮换会有一个**从平台切换完成到你服务端改配置**之间的短窗口，期间用旧私钥签名的请求会得到 `401`。
- **可选的过渡宽限窗**：如果这个短窗口对你不可接受，可在轮换**之前**联系平台运营申请开启「旧密钥验签宽限窗」——
  - 窗口内，你**已轮换下线**的旧 `kid` 仍可通过签名验证；窗口到期自动失效，旧密钥立即恢复拒绝；
  - 宽限窗**只允许验签**：它不恢复任何管理/配置权限，也不改变应用状态；
  - 时长由平台按固定上限核定（不超过 72 小时），平台可随时**立即关闭**；窗口到期或关闭后旧 `kid` **立即恢复拒绝**，且**不会**再有任何提示；
  - **密钥疑似泄露时不适用**：那种情况下**先轮换止血**，不要用宽限窗延长旧钥匙的寿命。

**推荐操作顺序（默认态：把窗口压到最小）**：

> 若已与平台约定开启宽限窗，第 1 步改为「先确认宽限窗已开启、记下到期时间」，第 4 步的 `401` 重试可以放宽；其余不变。

1. 选**低峰期**，准备好一键切换配置（新私钥 + 新 `kid`）。
2. 在门户完成轮换（生成 → 持有证明 → 二次验证 → 提交）。
3. 提交成功后**立即**切换你服务端的配置并发布（**默认态下旧私钥从提交那一刻起就签不通了**，切换越慢掉单越多）。
4. 切换期间对 `401` 做**有限次重试**（不要无限重试）；确认新配置生效后，观察下单成功率与签名相关告警恢复。
5. 确认无异常后，**销毁本地旧私钥**（避免误用）。
6. 把本次轮换的时间、`kid`、执行人、结果记进你的变更记录（后续对账时会用到）。

> 先在预发环境演练一遍上面的顺序（用同一套流程、同一份配置项），再对生产执行。

## 4. 停用与吊销

| 动作 | 谁触发 | 效果 |
| --- | --- | --- |
| 停用应用（暂停） | 平台侧（含商家申请） | **商家 API 全部不可用**：状态非 `ACTIVE` 时密钥解析在鉴权阶段就中性失败，所以你拿到的是 `401` 而不是 `403`，存量订单的查询/关单/退款**同样打不通**，善后走平台人工通道 |
| 吊销应用 | 平台侧（双人复核） | **永久终态**：表现同上（`401`），退款仅保留平台人工通道 |
| 冻结（风控） | 平台侧风控 | 新增下单/支付尝试被拒（`403`）；存量查询/关单/退款不受影响 |
| 移除 IP 白名单条目 | 门户 | 对应出口的请求立即 `403` |
| 关闭来源 IP 校验 | 门户（自助，短信二次验证） | 所有来源立即**不再**被校验；白名单条目原样保留，随时可一键重新开启。平台边缘的来源判定不受本开关影响 |

> **停用/吊销期间没有"只读自留地"**：这两种状态下应用不再 `ACTIVE`，鉴权阶段就中性拒绝（不区分应用不存在与状态异常，是防枚举的有意设计），因此存量订单也查不到。需要善后时按 [错误码与排障](./04-errors-and-troubleshooting.md) 联系平台走人工通道。
>
> **吊销后不再产生任何事件**：该应用的事件不会落库，事后也无法补发。所以吊销前先把在途订单的终态用查单补齐。

## 5. 私钥丢失或怀疑泄露（应急）

按这个顺序做，别先到处找原因：

1. **立即在门户轮换密钥**（见第 3 节）——不需要停机、不需要等平台人工介入。
2. 检查是否有人用旧密钥发起过请求：看**你自己**的调用日志与平台的 `401`/`403` 告警；有异常调用请一并反馈。
3. 若怀疑 `appId` 也已被滥用（例如出现在不该出现的地方），走官网反馈渠道申请**停用应用**并说明情况。
4. 轮换完成后，销毁本地旧私钥与任何携带它的临时文件/镜像层/构建缓存。
5. 顺手确认**来源 IP 校验处于开启**（门户「IP 白名单」页）——它在关闭状态下不会拦住任何人，而泄露场景下它是密钥之外的唯一一道门槛。

## 6. 多环境与多应用纪律

- **一应用一套密钥**；不要用同一套密钥跨应用或跨环境（测试/生产必须分开）。
- 不要**复制**已有应用的密钥去创建新应用——应当为新应用重新生成。
- 密钥配置项**不进代码仓库**：用运行时注入或密钥服务；本地开发也不要用生产密钥。

## 7. 常见误区

1. ❌「轮换后新旧钥匙能默认同时用一段时间」——**默认不能**，切换是原子的，旧密钥立即失效；唯一例外是平台**事先显式开启**的限时宽限窗（见第 3 节），它不是默认行为，也不由商家自助开启。
2. ❌ 在轮换前就把服务端切到新私钥——此时平台还没切，你会一直 `401`（顺序是：平台切完 → 你立刻切）。
3. ❌ 把私钥放进前端或移动端用于「让客户端直连」——私钥**永远只在服务端**。
4. ❌ 用同一个 `kid` 传不同的私钥（例如两台服务器各生成一套）——只有与门户登记公钥匹配的那一份能通过验签。
5. ❌ 密钥泄露后先排查后轮换——**先轮换**（止血），再排查。
6. ❌ 到处找不到「以 `kid_` 开头的密钥标识」——真实格式是 **`mkid_…`**（21 字符），且它在上传公钥那一刻才出现：没上传过公钥的应用本来就没有 `kid`。顺序永远是「生成密钥对 → 门户上传公钥 → 复制回显的 `kid`（或按第 1.1 节本地派生）」。
