feat(chv): 对齐 Muse P3 收尾——authd 替身凭证 + Sentinel 出口审批

- 新测试 cloud/tests/test_chv_muse_p3.py 30 项:替身过期、真 token
  泄露面、SSRF 挡截、deny/ask 路径、authd-seed 拆分、wake 预审、
  客内 authd 守护进程、git credential helper 端到端(真 git fill)
- 8 个旧测试按新 authd 行为改写(未删测试、未放宽断言):
  GITEA_TOKEN 改走 authd-seed,build_ssh_command 不再 source 进
  codex 环境;wake 测试补 Sentinel 白名单 + 假解析器
- 7 处超 50 行函数拆到 50 行内(不改行为);补 authd/sentinel
  公共 API 注解,mypy 在改动文件上干净
- 真问题修复:git 默认不把 path 发给 helper,chv-sync boot 加
  credential.useHttpPath=true,否则 authd 路径每次都拒绝
- 全量 cloud 套件 1897 项 OK(skipped=8)

真机待验证(未做,等第三棒让出 WSL 测试机):见
docs/chv-muse-p3-gap.md §3.6 清单
This commit is contained in:
host committed 2026-11-06 21:21:55 +00:00
1 parent 06c3e172fe
commit a90cf78d84
15 files changed
+2370 -93

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""git credential helper:向 chv-authd 按次取凭证(feat-chv-muse-p3)。
git credential 协议:git 把 `protocol/host/path`(每行 `key=value`,
空行结束)写进本进程 stdin;本进程向 authd 的 Unix socket 发
一次 JSON 请求,拿到用户名/口令后按协议回给 git(属性 + 空行)。
口令只进 git 子进程内存:本进程不写文件、不打日志(拒绝时只
报 kind,不含值);`password_expiry_utc` 按释放的 expires_at
回给 git,git 在该时间后重新问 helper(不缓存过期凭证)。
authd 拒绝 / socket 不通时本进程 exit 1——git 操作随之失败
(fail-closed,绝不回落成“无凭证也照推”)。
用法(chv-sync boot 配好后 git 自动调,不用手工跑):
git config --global credential.helper /usr/local/bin/git-credential-chv-authd
git config --global credential.useHttpPath true
(后者不可少:git 默认不把 path 发给 helper,没有 path 本进程
无法推断仓库与操作,会直接拒绝。)
socket 路径可用 CHV_AUTHD_SOCKET 覆盖(测试用 /tmp 下的假 socket)。
"""
import json
import os
import socket
import sys
DEFAULT_SOCKET_PATH = "/run/chv-authd/authd.sock"
SOCKET_TIMEOUT_SECONDS = 5
def _read_credential_request():
"""读 git credential 协议的请求属性(空行结束)→ dict。"""
attrs = {}
for line in sys.stdin:
line = line.rstrip("\n")
if not line:
break
if "=" in line:
key, value = line.split("=", 1)
attrs[key.strip()] = value.strip()
return attrs
def _ask_authd(socket_path, host, path):
"""向 authd 要一次凭证:返回 (username, password, expires_at)。
拒绝/通信失败即 RuntimeError(只含 kind,不含值)。"""
req = {"connector": "gitea", "host": host, "path": path}
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(SOCKET_TIMEOUT_SECONDS)
try:
sock.connect(socket_path)
sock.sendall((json.dumps(req) + "\n").encode("utf-8"))
chunks = []
while True:
data = sock.recv(65536)
if not data:
break
chunks.append(data)
if b"\n" in data:
break
except OSError as exc:
raise RuntimeError("authd_unreachable:%s" % exc)
finally:
sock.close()
try:
resp = json.loads(b"".join(chunks).decode("utf-8"))
except ValueError:
raise RuntimeError("authd_bad_response")
if not resp.get("ok"):
raise RuntimeError("authd_refused:%s"
% resp.get("error", "refused"))
return (resp["username"], resp["password"],
resp.get("expires_at"))
def main(argv=None):
"""helper 入口:读协议 → 问 authd → 回属性。拒绝即 exit 1。"""
attrs = _read_credential_request()
host = attrs.get("host", "")
path = attrs.get("path", "")
if attrs.get("protocol") != "https" or not host or not path:
print("git-credential-chv-authd: 请求不完整(须 protocol=https "
"+ host + path),拒绝", file=sys.stderr)
return 1
if not path.startswith("/"):
path = "/" + path
# git 的 info/refs 路径带 ?service=... 查询串:Sentinel 的
# L7 规则按干净路径匹配,这里先去掉查询串再问 authd。
path = path.split("?", 1)[0]
socket_path = os.environ.get("CHV_AUTHD_SOCKET") \
or DEFAULT_SOCKET_PATH
try:
username, password, expires_at = _ask_authd(
socket_path, host, path)
except RuntimeError as exc:
print("git-credential-chv-authd: %s" % exc, file=sys.stderr)
return 1
print("username=%s" % username)
print("password=%s" % password)
if expires_at:
print("password_expiry_utc=%d" % int(expires_at))
print()
return 0
if __name__ == "__main__":
sys.exit(main())