agent-exec transport 3cbfba3aed64b516704b0d68a7a9f2fa/82da3fb3f31d
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
- 凭据类验证命令不得把凭据展开进命令行、输出或证据:经 `scripts/facts-verify.sh` 探针子命令在进程内读取并发送。
|
||||
- 旧端点 git-panel.u1680832.nyat.app:30319 已退役,一律不得使用——这是政策禁令,不随 F4 核验结果变化;
|
||||
F4 只登记可观测的 TLS 握手失败现象,DNS 失败、超时、连接失败或握手成功都判 FAIL,不刷新 last_verified。
|
||||
- F4 现有 last_verified(2026-10-14)是历史日期,来自旧的宽泛检查,不证明当前 TLS 层握手失败判据;
|
||||
经该判据独立重验前不得当作已核实。
|
||||
|
||||
| ID | 事实 | 验证命令 | last_verified | TTL(天) |
|
||||
|---|---|---|---|---|
|
||||
|
||||
+17
-4
@@ -4,8 +4,9 @@
|
||||
# 用法:make facts-verify [ARGS=--update] (--update:把 PASS 项 last_verified 刷为当天并写回)
|
||||
# 表内验证命令约定不含竖线;含 "\|" 转义时按字面还原后执行。
|
||||
# 登记表引用的探针子命令(单独运行时在 stderr 说明判定,不输出凭据):
|
||||
# api-token URL 进程内读 ~/.secrets/gitea HOME 段 ACCESS_TOKEN 并 GET URL;
|
||||
# 0=接受 3=缺失 4=格式不符 5=请求失败/被拒
|
||||
# api-token URL URL 须逐字等于登记端点 https://git-home.u1680832.nyat.app:30319/api/v1/user,
|
||||
# 校验通过后才进程内读 ~/.secrets/gitea HOME 段 ACCESS_TOKEN 并 GET(不跟随重定向);
|
||||
# 0=接受 3=缺失 4=格式不符 5=请求失败/被拒/重定向 6=非登记端点
|
||||
# tls-handshake-fails HOST PORT 仅当 TCP 已连通且 TLS 层握手报错时 0;
|
||||
# 3=DNS 失败 4=超时 5=连接失败 6=握手成功(端点仍可用)
|
||||
set -uo pipefail
|
||||
@@ -15,11 +16,23 @@ case "${1:-}" in
|
||||
api-token)
|
||||
[ $# -eq 2 ] || { echo "usage: facts-verify.sh api-token URL" >&2; exit 2; }
|
||||
exec python3 - "$SCRIPT_DIR" "$2" <<'PY'
|
||||
import sys
|
||||
import sys, urllib.request
|
||||
sys.path.insert(0, sys.argv[1])
|
||||
import task_triage
|
||||
|
||||
url = sys.argv[2]
|
||||
ENDPOINT = "https://git-home.u1680832.nyat.app:30319/api/v1/user"
|
||||
if sys.argv[2] != ENDPOINT:
|
||||
print("api-token: URL 不是 policy/facts.md F2 登记端点,拒绝发送凭据", file=sys.stderr)
|
||||
sys.exit(6)
|
||||
|
||||
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
urllib.request.install_opener(urllib.request.build_opener(NoRedirect))
|
||||
url = ENDPOINT
|
||||
token = task_triage.secrets_file_token()
|
||||
if not token:
|
||||
print("api-token: ~/.secrets/gitea HOME 段 ACCESS_TOKEN 缺失", file=sys.stderr)
|
||||
|
||||
+75
-20
@@ -1,6 +1,7 @@
|
||||
"""外部事实重验契约(scripts/facts-verify.sh + policy/facts.md 的 F2/F4 验证命令)。
|
||||
|
||||
全部离线:合成 HOME/IMC 凭据、sitecustomize 桩替换 urllib/socket/ssl(真实连接一律拒绝)、
|
||||
全部离线:合成 HOME/IMC 凭据、sitecustomize 桩拦截 urllib 处理器网络边界(保留真实重定向判定)
|
||||
与 socket/ssl(真实连接一律拒绝)、
|
||||
PATH 桩记录 curl/wget argv、临时 Git 仓库与临时登记表;不读取真实 ~/.secrets,不连接 Gitea 或旧端点。
|
||||
"""
|
||||
import datetime
|
||||
@@ -22,7 +23,7 @@ OLD_DATE = "2026-01-01"
|
||||
SECRETS = [HOME_TOKEN, IMC_TOKEN]
|
||||
|
||||
STUB = r'''
|
||||
import json, os, socket, ssl, subprocess, urllib.error, urllib.request
|
||||
import http.client, io, json, os, socket, ssl, subprocess, urllib.request, urllib.response
|
||||
|
||||
|
||||
def _log(kind, **data):
|
||||
@@ -49,33 +50,38 @@ def _popen(self, args, *rest, **kwargs):
|
||||
subprocess.Popen.__init__ = _popen
|
||||
|
||||
|
||||
class _Response:
|
||||
def __init__(self, body):
|
||||
self._body = body
|
||||
|
||||
def read(self):
|
||||
return self._body
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
return False
|
||||
def _response(url, code, body=b"", location=None):
|
||||
headers = http.client.HTTPMessage()
|
||||
if location:
|
||||
headers["Location"] = location
|
||||
resp = urllib.response.addinfourl(io.BytesIO(body), headers, url, code)
|
||||
resp.msg = "stub"
|
||||
return resp
|
||||
|
||||
|
||||
def _urlopen(request, *args, **kwargs):
|
||||
url = getattr(request, "full_url", request)
|
||||
auth = request.get_header("Authorization") if hasattr(request, "get_header") else None
|
||||
REDIRECT_TARGET = "redirect-target.invalid"
|
||||
|
||||
|
||||
# Intercept only the handlers' network boundary so the opener's real redirect and
|
||||
# error processing still decides whether a follow-up request is made.
|
||||
def _open(self, request):
|
||||
url = request.full_url
|
||||
auth = request.get_header("Authorization")
|
||||
_log("http", url=url, authorization=auth)
|
||||
if REDIRECT_TARGET in url:
|
||||
return _response(url, 200, b'{"login": "redirect-target"}')
|
||||
mode = os.environ.get("FAKE_HTTP", "200")
|
||||
if mode == "200":
|
||||
return _Response(b'{"login": "fixture"}')
|
||||
return _response(url, 200, b'{"login": "fixture"}')
|
||||
if mode == "leak":
|
||||
raise ValueError("Invalid header value " + repr(auth))
|
||||
raise urllib.error.HTTPError(url, int(mode), "stub", {}, None)
|
||||
code = int(mode)
|
||||
location = "https://" + REDIRECT_TARGET + "/collect" if 300 <= code < 400 else None
|
||||
return _response(url, code, location=location)
|
||||
|
||||
|
||||
urllib.request.urlopen = _urlopen
|
||||
urllib.request.HTTPSHandler.https_open = _open
|
||||
urllib.request.HTTPHandler.http_open = _open
|
||||
|
||||
|
||||
class _Conn:
|
||||
@@ -299,6 +305,55 @@ class FactsVerifyTests(unittest.TestCase):
|
||||
self.assertNotIn("Authorization", direct.stderr)
|
||||
self.assert_no_secrets(result.stdout, result.stderr, direct.stdout, direct.stderr)
|
||||
|
||||
def test_f2_rejects_unregistered_destination_before_reading_credentials(self):
|
||||
bad_urls = [
|
||||
"http://git-home.u1680832.nyat.app:30319/api/v1/user",
|
||||
"https://evil.invalid/api/v1/user",
|
||||
"https://git-home.u1680832.nyat.app:30320/api/v1/user",
|
||||
"https://git-home.u1680832.nyat.app/api/v1/user",
|
||||
"https://user:pw@git-home.u1680832.nyat.app:30319/api/v1/user",
|
||||
"https://git-home.u1680832.nyat.app:30319@evil.invalid/api/v1/user",
|
||||
"https://git-home.u1680832.nyat.app:30319/api/v1/user#frag",
|
||||
"https://git-home.u1680832.nyat.app:30319/api/v1/user?next=https://evil.invalid",
|
||||
"https://git-home.u1680832.nyat.app:30319/api/v1/users",
|
||||
"https://git-home.u1680832.nyat.app:30319/api/v1/user/../admin",
|
||||
"https://git-home.u1680832.nyat.app:30319/api/v1/user ",
|
||||
]
|
||||
for secrets_present in (True, False):
|
||||
if not secrets_present:
|
||||
(self.home / ".secrets" / "gitea").unlink()
|
||||
for url in bad_urls:
|
||||
with self.subTest(url=url, secrets_present=secrets_present):
|
||||
self.stub_log.unlink(missing_ok=True)
|
||||
direct = self.run_script("api-token", url)
|
||||
self.assertEqual(direct.returncode, 6, direct.stderr)
|
||||
self.assertEqual(self.stub_events("http"), [], "no request to an unregistered destination")
|
||||
self.assertNotIn(url.strip(), direct.stdout + direct.stderr)
|
||||
self.assertNotIn("evil.invalid", direct.stdout + direct.stderr)
|
||||
self.assertNotIn("ACCESS_TOKEN", direct.stderr, "destination is checked before credentials")
|
||||
self.assert_no_secrets(direct.stdout, direct.stderr)
|
||||
|
||||
def test_f2_refuses_redirect_without_forwarding_authorization(self):
|
||||
for code in ("301", "302", "303", "307", "308"):
|
||||
with self.subTest(code):
|
||||
self.stub_log.unlink(missing_ok=True)
|
||||
direct = self.run_script("api-token", "https://git-home.u1680832.nyat.app:30319/api/v1/user",
|
||||
http=code)
|
||||
self.assertEqual(direct.returncode, 5, direct.stderr)
|
||||
requests = self.stub_events("http")
|
||||
self.assertEqual([r["url"] for r in requests],
|
||||
["https://git-home.u1680832.nyat.app:30319/api/v1/user"], "no follow-up request")
|
||||
self.assertNotIn("redirect-target", direct.stdout + direct.stderr)
|
||||
self.assert_no_secrets(direct.stdout, direct.stderr)
|
||||
|
||||
self.stub_log.unlink(missing_ok=True)
|
||||
self.write_registry("F2")
|
||||
result = self.run_script("--update", http=code)
|
||||
self.assert_status(result.stdout, "F2", "FAIL")
|
||||
self.assertEqual(self.dates()["F2"], OLD_DATE)
|
||||
self.assertEqual(len(self.stub_events("http")), 1)
|
||||
self.assert_no_secrets(result.stdout, result.stderr)
|
||||
|
||||
# --- F4:只有 TLS 层握手失败才支持该事实 -----------------------------------
|
||||
|
||||
def test_f4_only_tls_handshake_failure_passes_and_refreshes(self):
|
||||
|
||||
Reference in New Issue
Block a user