feat(chv): 对齐 Muse P10 支付安全——凭证隔离 + 结账检测 + 每次购买人工审批 + 一次性卡号

- 支付凭证隔离:PaymentCredentialVault(只活内存、白名单
  stripe-link/shop-pay、只存不透明 id + 掩码标签、原始卡号
  PAN 拒绝入库、按次释放绑定商家/金额/货币、单次 + TTL)
- 结账页检测:detect_checkout(URL + 无障碍树信号,确定性
  规则、不读原始 DOM、与 P4 detect_form_risk 同输入形状)
- 每次购买人工审批:PaymentGate 经 P5 对话外通道发审批,
  payload 含确切购买详情;完成时先验签、再验详情摘要
  (篡改即拒且审批保持待处理)、最后单次消费答复
- 一次性卡号:OneTimeCard 绑定商家/金额/货币/有效期,单次
  兑现;SimulatedWalletBackend 沙箱钱包(假 PAN 只在后端
  私有内存、charge 只记账不联网、取消即 void)
- 限额:SpendLimits 三档 minor units 比对,限额不可用时
  不编造上限(limits_checked=False)
- 审计:PaymentAudit 只留允许字段,秘密字段名丢弃 + redacted
- 审批通道最小扩展:ApprovalRequest 增加可选 details 字段,
  不传时 payload 与 P5 完全一致
- 真实支付与扣款不做:全部沙箱验证,真实钱包集成待用户
  另行批准;新测试 79 项,全量 cloud 套件 2037 OK

round-12 集成说明:本提交为 acf98f8 的 cherry-pick(只含 P10
自有 diff,基线 25bcd35 已含的 P3/P5 内容未重复合入;
approval_channel.py 仅加可选 details 字段)。
This commit is contained in:
host committed 2026-11-07 00:09:55 +00:00
1 parent 90c42f6e1d
commit b55b49b358
7 files changed
+1939 -8

No files matched your search

+23 -8
View File
@@ -11,6 +11,9 @@
"reasons": [str, ...], "expires_at": float}
``target`` 只有目标描述(连接器/方法/scheme/主机/端口/路径),
``reasons`` 只有原因名;载荷里绝无凭证值与用户数据内容。
P10 扩展:可选 ``details`` 字段(支付审批的确切购买详情,
商家/商品/金额/货币,供客户端 UI 展示;不传时 payload 与
P5 完全一致)。
``nonce`` 是随机 16 字节 hex(32 字符),每次请求唯一。
``expires_at`` 默认请求后 15 分钟(900 秒),可注入时钟。
- 答复(``Answer``,客户端答完直接回 Sentinel)::
@@ -72,38 +75,50 @@ class ApprovalRequest:
reasons: Iterable[str] | None,
nonce: str | None = None,
now: float | None = None,
ttl_seconds: int = DEFAULT_TTL_SECONDS) -> None:
ttl_seconds: int = DEFAULT_TTL_SECONDS,
details: dict | None = None) -> None:
self.pending_id = pending_id
self.target = dict(target)
self.reasons = list(reasons or [])
self.nonce = nonce or secrets.token_hex(16)
at = now if now is not None else time.time()
self.expires_at = float(at) + ttl_seconds
# 可选的确切详情(P10 支付审批用:商家、商品、金额、
# 货币——供客户端 UI 展示;无 details 时 payload 与
# P5 完全一致)。只含描述字段,无凭证值。
self.details = dict(details) if details is not None else None
def payload(self) -> dict:
"""可外发给客户端的请求载荷(只含描述字段 + nonce +
过期,无密钥、无凭证值、无用户数据内容)。"""
return {"pending_id": self.pending_id, "nonce": self.nonce,
"target": dict(self.target),
"reasons": list(self.reasons),
"expires_at": self.expires_at}
过期,无密钥、无凭证值、无用户数据内容)。有 details
时带上(支付审批的确切购买详情)。"""
out: dict = {"pending_id": self.pending_id, "nonce": self.nonce,
"target": dict(self.target),
"reasons": list(self.reasons),
"expires_at": self.expires_at}
if self.details is not None:
out["details"] = dict(self.details)
return out
def request_approval(queue_record: dict, secret: str,
now: float | None = None,
ttl_seconds: int = DEFAULT_TTL_SECONDS
ttl_seconds: int = DEFAULT_TTL_SECONDS,
details: dict | None = None
) -> ApprovalRequest:
"""为一条队列记录生成通道请求并登记(nonce + 过期)。
``secret`` 是调用方持有的通道密钥:本模块不保存它、不进
载荷、不进日志;答复方用同一密钥签名,校验时对上才算数。
``details`` 可选(P10 支付审批的确切购买详情,进 payload
供客户端 UI 展示;不传时行为与 P5 完全一致)。
返回的 ``ApprovalRequest.payload()`` 即发给客户端的东西。
"""
pending_id = queue_record["pending_id"]
req = ApprovalRequest(
pending_id, _target_from_record(queue_record),
queue_record.get("reasons", []),
now=now, ttl_seconds=ttl_seconds)
now=now, ttl_seconds=ttl_seconds, details=details)
_REQUESTS[pending_id] = {"nonce": req.nonce,
"expires_at": req.expires_at}
return req