Skip to content

安全

本章汇整散落在认证沙盒错误回调各章的整合安全指引。当你准备上线、执行安全审查,或稽核人员询问 Payment Gateway 在传输层提供哪些保证、你这端又该做什么时,请阅读本章。安全是共同责任模型:Payment Gateway 强化平台、你强化整合,任一方独自都不足够。

威胁模型

下表列出商户 API 接口所要抵御的威胁,以及每项缓解措施的责任归属。「双方」代表两侧都存在控制机制,缺一不可。

威胁缓解措施责任归属
对入站请求的重放攻击服务器端强制 X-Timestamp ±5 分钟窗口;单靠签名无法防止重放。Payment Gateway
对入站回调的重放攻击你的回调处理器套用相同的 ±5 分钟窗口;并搭配 (order_no, status) 幂等。商户
对入站请求的主体篡改每笔请求都必须带 X-Signatureverify_md5_signature 会在任何业务逻辑之前执行。Payment Gateway
对入站回调的主体篡改你的回调处理器在套用任何副作用之前先验证 X-Signature商户
对 API 的 IP 伪造来源 IP 由 Cloudflare 边缘(WAF / Access)统一管控。Payment Gateway
凭证外泄(api_key可依需求提供轮换程序;你需把密钥存在 Secret Manager(密码管理工具)中并在日志中脱敏。双方
DNS 污染/MITM 攻击端对端强制 TLS 1.2+;网关在生产环境中拒绝 HTTP 回调 URL 与商户端自签凭证。双方
暴力破解凭证猜测Cloudflare 在边缘对 API 主机名套用速率限制;你也应对自身的认证失败做速率限制。双方
日志文件遭窃导致凭证外泄下方记录脱敏规范;由你在日志器中实作。商户
并发重放与你的处理器抢跑(order_no, status) 做幂等键;在套用副作用前先去重。商户

清单上没有任何一项是纯粹网关的责任。即使由 Payment Gateway 负责的控制项,商户端也有对应的责任不可省略。

凭证生命周期

发放

你的 api_key 在开通阶段由客户经理配发。你会拿到两把密钥:一把沙盒、一把生产环境。两者彼此独立——沙盒密钥无法用于生产环境,反之亦然。密钥是由平台端通过 secrets.token_hex(16) 产生的 32 字元 hex token,并通过外部渠道交付。

轮换触发时机

下列任一情况发生时立即轮换:

  • 密钥已知或疑似外泄(提交到源代码控制、贴进工单、以明文记录、通过不受信任渠道传送)。
  • 持有密钥存取权限的员工离职或调任至无需知情的职位。
  • 持有密钥的供应商或整合商结束合作。
  • 排程轮换周期到期。建议最低周期为每季一次。

如何轮换

轮换由运维人员协作。联络你的客户经理。流程如下:

  1. 客户经理在你的商户记录上新增第二把有效密钥。
  2. 你部署新密钥并与旧密钥并存,验证流量正常。
  3. 客户经理在短暂的重叠窗口(约 5 分钟)后撤销旧密钥,期间两把密钥皆可接受,允许热切换而不中断请求。

目前商户后台不提供自助轮换。请在你的事件响应手册中为运维人员交接预留流程。

储存

api_key 等同于长期使用的密码凭证。请依此规格储存:

  • 使用 Secret Manager(密码管理工具):AWS Secrets Manager、GCP Secret Manager、HashiCorp Vault 或同等方案。
  • 在执行时从 Secret Manager 注入应用程序的程序环境或内存。
  • 绝对不要提交到源代码控制,即使加密过也不行。Secret Manager 端的静态加密不等同于「在 repo 中加密」——后者可从任何历史 clone 还原。
  • 绝对不要烘焙进容器映像;映像层会泄漏。
  • 绝对不要包进客户端程式码、行动 App 或任何送到浏览器的东西。商户 API 接口仅限服务器对服务器。

团队内部分发

把生产环境 api_key 的知情范围限制在运维与待命工程师。在平台支援的情境下使用即时授权。稽核每一次对该机密的直接读取。将「谁知道生产环境密钥」清单视为需要与轮换同样周期审视的项目。

传输

出站(你的服务 → Payment Gateway)

所有商户 API 主机名只接受 HTTPS。网关在边缘终止 TLS 并拒绝明文 HTTP。可接受的 TLS 最低版本为 TLS 1.2;TLS 1.0、TLS 1.1 与 SSLv3 不会被协商。将你的 client 锁定为 TLS 1.2 或以上;不要设置回退到较旧套件。

入站(Payment Gateway → 你的回调 URL)

  • 生产环境强制 HTTPS。callback_url 若不以 https:// 开头,会在建单时被拒。没有 opt-out 选项。
  • **必须提供有效的凭证链。**网关会以公开 CA bundle 验证你的 TLS 凭证。自签凭证会被拒绝(webhook_ssl_verify = True 为预设)。请使用任何公开 CA 签发的真实凭证。
  • **你端也要 TLS 1.2+。**停用回调端点的 TLS 1.0 与 1.1。网关不会与其协商。

加密套件与现代安全规范

停用 export 加密、RC4、3DES 与任何标记为「EXPORT」或「anonymous」的套件。启用前向安全(ECDHE)。任何当代 TLS 函式库的现代预设值都是正确的。

网络层存取控制

商户 API 的入站请求会在 Cloudflare 边缘(WAF / Access)通过来源 IP 允许清单的检查。完整模型与其失败模式请见沙盒。生产环境同样套用此边缘管控。把自己从 Cloudflare Access 允许清单移除,是「昨天还能用」工单最常见的单一原因。

请求完整性

签名演算法

每笔入站商户请求与每笔出站回调都会以 MD5(sorted_params + api_key) 签名,输出为小写 hex。完整的逐步演算法与四种参考实作见认证与签名。本章不再重新推导。

此处选用 MD5 反映支付通道的业界惯例;它被当作搭配共享密钥的 MAC 风格完整性检查使用,而不是抗碰撞杂凑。由于 api_key 只串接但从不在传输中送出,即使攻击者拥有任意 MD5 碰撞能力,没有密钥也无法伪造签名。

未来可能会宣布迁移到 HMAC-SHA256。届时会在变更纪录中公告,附稳定的过渡计画、并行接受窗口与弃用日期。不要预先重新实作;契约以本文件所载为准。

时间戳窗口

X-Timestamp 为 Unix epoch 秒数。服务器端窗口为网关时钟的 ±5 分钟(300 秒),在签名验证之前强制执行。超出窗口则响应 401 INVALID_TIMESTAMP(见错误)。

在你的回调处理器中镜像这项检查。网关在每次派送与重试时都会送出新的 X-Timestamp;拒绝任何时间戳与你的时钟相差超过 5 分钟的回调。把你的时钟同步到 NTP——不要靠信任网关送来的时间戳来「修正」时钟漂移。

常数时间比较

验证入站回调签名时,使用常数时间相等函式:

  • Python:hmac.compare_digest(expected, received)
  • PHP:hash_equals($expected, $received)
  • Node.js:crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
  • Go:subtle.ConstantTimeCompare([]byte(expected), []byte(received)) == 1

一般字串比较(==)会透过时间变化泄漏首个不同位元组的位置。样本累积足够后,攻击者可逐位元组推回伪造签名。网关在服务器端使用 hmac.compare_digest;你这端也应采用相同方式。认证章节随附的 verify 范例在各语言中皆使用正确原语。

时间戳窗口内的重放

有效签名加新鲜的时间戳,本身并不能防止 5 分钟窗口内的重放。拦截到回调的网络攻击者可在 5 分钟内重放。能击败此问题的控制是商户端(order_no, status) 做幂等——在下一节说明。签名加时间戳窗口缩小了重放机会;幂等则彻底化解。

把幂等当成安全属性

合法网络重试所产生的重复回调(网关对同一笔 NotificationLog 列最多再派送 5 次)与恶意重放尝试,在应用层是无法区分的。两者送达的都是相同主体上的合法签名。两者皆由同一控制处理:在套用任何副作用之前,以 (order_no, status) 去重。

这就是为什么回调建议在 (order_no, status) 上使用主键资料表,并采取 insert-or-no-op 的处理器。从安全角度看,那张资料表是你的重放防御层,不只是重试正确性层。

为什么不用其他键:

  • notification_id——回调主体中并未提供 per-attempt 事件 id。网关在所有重试中重用同一笔 NotificationLog 列。
  • X-Timestamp——每次重试都会变动,因此以它去重会让同一事件被合法套用 5 次(合法情境)以及任意次数(恶意情境)。
  • (order_no, status)——跨重试稳定,跨合法状态转移又有区别(例如 MATCHEDCOMPLETED 两者都是真实事件、都该处理)。

日志规范

当机密以明文记录时,泄漏的日志文件就等同于泄漏的凭证。下列纪律请在日志器层落实——而不是呼叫端,否则会被跳过。

该记录的(搭配脱敏)

  • HTTP method、请求路径、响应状态。
  • 响应信封中的 code(见错误)。
  • 你自己的 merchant_order_no 与网关的 order_no
  • 回调中的 notification_type / event 字段。
  • 签名验证结果的布林值(passfail)。
  • 你内部的 trace ID 或 request ID 以利关联。
  • X-Timestamp 值以及你观测到相对于本机时钟的偏移。

不该记录的(等同凭证或属于 PII)

  • **完整的 api_key。**只记录前 8 个字元加省略号:pre_xxxxxxxx...。绝不记录末尾位元组;绝不把整串接到 query string 里。
  • **完整的 X-Signature 值。**只记录验证通过或失败。Hex 值没有除错价值——若它是错的,你也无法从日志反推应有的值。
  • **完整的 bank_card_number。**遮蔽除末 4 码之外的所有位元组。
  • **完整的 account_no。**遮蔽除末 4 码之外的所有位元组。
  • **PII 组合。**收款人姓名 + 银行 + 帐号合起来就能对应到某银行的真实人员。请脱敏、省略,或在独立存取控制下加密储存。
  • **签名验证前的原始请求主体。**尚未验证的主体属攻击者可控内容。只在 X-Signature 通过后,并套用上述脱敏,才记录它。

日志的存放位置

  • 静态加密。多数受管理日志储存预设会这么做——确认你的也是。
  • 对日志储存实施存取控制:读取权是特权角色,而非开发者预设。
  • 保留期:合理预设为 热存 90 天、冷存 1 年。依你所在的法规环境调整。
  • 备份沿用同样的控制。泄漏的备份就是泄漏的日志。

SIEM 整合

将下列项目作为安全事件转发到你的 SIEM:

  • 来自网关的 401 响应(AUTHENTICATION_ERRORINVALID_SIGNATUREINVALID_TIMESTAMP)。
  • 来自网关的 403 响应(AUTHORIZATION_ERROR)。
  • 你的回调处理器上的签名验证失败。
  • 任何持续对上述项目产生大量爆发的来源 IP。

在一段干净期间后出现 401 模式通常代表时钟漂移或部分密钥轮换。来源 IP 漂移则改在 Cloudflare 边缘呈现(TLS 握手失败或 HTML 挑战/封锁页,而非 JSON 403);JSON 403 现在代表商户停用或缺少权限。两者都是运维上的紧急事件。

常见陷阱

下列错误皆已在真实商户整合中上到生产环境。每一项都附带你会看到的错误代码或症状。

  1. **在任何地方记录完整 api_key。**Debug 日志、例外回报器、应用程序指标、错误追踪 SaaS 的 payload。只记录前 8 个字元加省略号。任何日志列中出现的 api_key 子字串都视为凭证外泄并启动轮换。
  2. **记录 X-Signature 值。**该值没有除错用途。只记录布林 pass/fail。被记录的签名配上被记录的主体,会泄漏 chosen-message 攻击的输入。
  3. **生产环境使用 HTTP 的 callback_url。**建单时即被拒。你会看到 400 VALIDATION_ERROR 或相当消息(见错误)。请使用 HTTPS。
  4. **生产环境的回调端点使用自签凭证。**网关会拒绝连线(webhook_ssl_verify = True)。请使用公开 CA。
  5. **跨沙盒与生产环境重用同一把 api_key。**设计上不可能(沙盒密钥会被生产环境拒绝、反之亦然),但若重用处理模式——例如同一个 Secret Manager 条目、同一个日志器、同一条事件路径——就会把两个环境耦合。请从头到尾保持分离。
  6. **把 api_key 存在源代码控制里。**即使加密、即使用 git-crypt、即使在私有 repo。加密层在源代码控制层之下游;历史 clone 会跨越轮换幸存。
  7. **跳过「内部」回调的签名验证。**沙盒回调由网关以与生产环境相同的演算法签署。如果你在沙盒跳过验证,会把那段程式码路径送到生产环境。两边都要验证。见回调
  8. message 文字而非 code 做分支判断。message 为资讯性质、措辞可能变动。code 才是稳定契约。见错误
  9. **在 client 端把 5 分钟时间戳窗口写死。**该窗口是服务器端政策。若网关把它收紧,以 now() - 4m59s 签名的 client 会开始失败。用 now() 签名,把窗口判断交给服务器。
  10. **验证签名时忘记使用常数时间比较。**字串相等会泄漏。依语言使用 hmac.compare_digesthash_equalscrypto.timingSafeEqual
  11. **Cloudflare Access 允许清单漂移。**你的 NAT 出口 IP 变动(新资料中心、新 CI runner pool、云端供应商 IP 刷新)但 Cloudflare Access 允许清单没跟上。来源 IP 管控只在 Cloudflare 边缘执行,因此被挡的征兆是 TLS 握手失败或 HTML 挑战/封锁页,而不是带 AUTHORIZATION_ERROR 的 JSON 403。请把出口 IP 同步到 Cloudflare Access 允许清单。见沙盒——生产环境套用相同模型。
  12. **在回调持久化层以未加密形式储存 PII(bank_card_numberaccount_no、收款人姓名)。**静态加密不等同于资料库字段本身的加密。如果你的运维人员或唯读副本能看到明文,你的稽核姿态就错了。
  13. 未验证链路就信任 CDN 边缘标头。X-Forwarded-For 会被链路中每个 proxy 附加。清单中第一个 IP 是直接 client 写的;只有最右侧、由你自己的可信 proxy 附加的 IP 才有权威性。请以可信跳数计算来验证,而不是信任最左边的值。
  14. **持有密钥存取权的员工离职后忘记轮换 api_key。**轮换是离职流程的一部分,不是季度后事。离职手册必须含此步骤。
  15. **捕捉签名验证例外后继续执行。**在 verify 呼叫外套 try/except 而吞掉失败、继续处理主体,就是权限提升攻击向量。回 401 并停止。见回调
  16. **在验证前记录原始请求主体。**攻击者可控的内容会落入你的日志储存,包含可能与日志脱敏规则模式匹配的任意 JSON key,让机密溜走。先验证,再记录。

上线安全检查清单

把生产环境部署切到 api.open-pay.co 之前,逐项确认:

  • [ ] 回调 URL 为 HTTPS 且凭证链可被公开 CA 验证。
  • [ ] api_key 存放在 Secret Manager 中,不在 repo 的 env 文件、不在映像层、不在客户端程式码。
  • [ ] **每个回调处理器的请求路径上都有签名验证。**不藏在 feature flag 后面、不在「内部」环境略过、不会在带 debug 标头时被绕过。
  • [ ] (order_no, status) 为键的幂等资料表或快取已部署,并以重复回调情境测试过。
  • [ ] **日志脱敏已测试。**用 grep 在 staging 日志中搜寻 api_key=pre_X-Signature: 与其他凭证样式。grep 应回传零笔。
  • [ ] **Cloudflare Access 允许清单与你的生产环境 NAT 出口 IP 对齐。**有文件、有负责人、每季审视。
  • [ ] SIEM 收到认证失败事件:来自网关的 401/403,以及你处理器上的签名验证失败。
  • [ ] **密钥轮换周期已与客户经理协定。**最低每季一次。轮换手册已写成,并由作者以外的人读过。
  • [ ] **事件响应手册涵盖 api_key 外泄情境。**该打给谁、要求什么、重叠窗口多长、如何确认旧密钥已失效。
  • [ ] **沙盒与生产环境凭证在部署设置中明确分离。**不同的 Secret Manager 路径、不同的环境变量、不同的日志前缀。没有在生产环境未设时 fall-through 到沙盒的预设值。
  • [ ] **每台签署请求或验证回调的主机都设置了时钟同步(NTP)。**漂移超过 5 分钟是硬性失败。
  • [ ] **双向皆强制 TLS 1.2+。**不回退至 1.0/1.1。

交叉参考

  • 认证与签名——完整签名演算法、5 分钟时间戳窗口,以及四种语言的参考实作。
  • 沙盒——Cloudflare 边缘来源 IP 允许清单模型,以及来源 IP 在边缘被拒绝时如何辨识。
  • 错误——安全相关错误代码(401 INVALID_SIGNATURE401 INVALID_TIMESTAMP403 AUTHORIZATION_ERROR)与响应信封契约。
  • 回调——回调签名验证、以 (order_no, status) 做幂等,以及重试政策。
  • 快速开始——从第一笔请求到第一笔验证通过的回调,15 分钟上手。
  • API 参考——完整端点清单、请求与响应 schema。