agent-exec transport 3cbfba3aed64b516704b0d68a7a9f2fa/82da3fb3f31d
This commit is contained in:
@@ -10,6 +10,8 @@
|
|||||||
- 凭据类验证命令不得把凭据展开进命令行、输出或证据:经 `scripts/facts-verify.sh` 探针子命令在进程内读取并发送。
|
- 凭据类验证命令不得把凭据展开进命令行、输出或证据:经 `scripts/facts-verify.sh` 探针子命令在进程内读取并发送。
|
||||||
- 旧端点 git-panel.u1680832.nyat.app:30319 已退役,一律不得使用——这是政策禁令,不随 F4 核验结果变化;
|
- 旧端点 git-panel.u1680832.nyat.app:30319 已退役,一律不得使用——这是政策禁令,不随 F4 核验结果变化;
|
||||||
F4 只登记可观测的 TLS 握手失败现象,DNS 失败、超时、连接失败或握手成功都判 FAIL,不刷新 last_verified。
|
F4 只登记可观测的 TLS 握手失败现象,DNS 失败、超时、连接失败或握手成功都判 FAIL,不刷新 last_verified。
|
||||||
|
- F4 现有 last_verified(2026-10-14)是历史日期,来自旧的宽泛检查,不证明当前 TLS 层握手失败判据;
|
||||||
|
经该判据独立重验前不得当作已核实。
|
||||||
|
|
||||||
| ID | 事实 | 验证命令 | last_verified | TTL(天) |
|
| ID | 事实 | 验证命令 | last_verified | TTL(天) |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
|
|||||||
+17
-4
@@ -4,8 +4,9 @@
|
|||||||
# 用法:make facts-verify [ARGS=--update] (--update:把 PASS 项 last_verified 刷为当天并写回)
|
# 用法:make facts-verify [ARGS=--update] (--update:把 PASS 项 last_verified 刷为当天并写回)
|
||||||
# 表内验证命令约定不含竖线;含 "\|" 转义时按字面还原后执行。
|
# 表内验证命令约定不含竖线;含 "\|" 转义时按字面还原后执行。
|
||||||
# 登记表引用的探针子命令(单独运行时在 stderr 说明判定,不输出凭据):
|
# 登记表引用的探针子命令(单独运行时在 stderr 说明判定,不输出凭据):
|
||||||
# api-token URL 进程内读 ~/.secrets/gitea HOME 段 ACCESS_TOKEN 并 GET URL;
|
# api-token URL URL 须逐字等于登记端点 https://git-home.u1680832.nyat.app:30319/api/v1/user,
|
||||||
# 0=接受 3=缺失 4=格式不符 5=请求失败/被拒
|
# 校验通过后才进程内读 ~/.secrets/gitea HOME 段 ACCESS_TOKEN 并 GET(不跟随重定向);
|
||||||
|
# 0=接受 3=缺失 4=格式不符 5=请求失败/被拒/重定向 6=非登记端点
|
||||||
# tls-handshake-fails HOST PORT 仅当 TCP 已连通且 TLS 层握手报错时 0;
|
# tls-handshake-fails HOST PORT 仅当 TCP 已连通且 TLS 层握手报错时 0;
|
||||||
# 3=DNS 失败 4=超时 5=连接失败 6=握手成功(端点仍可用)
|
# 3=DNS 失败 4=超时 5=连接失败 6=握手成功(端点仍可用)
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
@@ -15,11 +16,23 @@ case "${1:-}" in
|
|||||||
api-token)
|
api-token)
|
||||||
[ $# -eq 2 ] || { echo "usage: facts-verify.sh api-token URL" >&2; exit 2; }
|
[ $# -eq 2 ] || { echo "usage: facts-verify.sh api-token URL" >&2; exit 2; }
|
||||||
exec python3 - "$SCRIPT_DIR" "$2" <<'PY'
|
exec python3 - "$SCRIPT_DIR" "$2" <<'PY'
|
||||||
import sys
|
import sys, urllib.request
|
||||||
sys.path.insert(0, sys.argv[1])
|
sys.path.insert(0, sys.argv[1])
|
||||||
import task_triage
|
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()
|
token = task_triage.secrets_file_token()
|
||||||
if not token:
|
if not token:
|
||||||
print("api-token: ~/.secrets/gitea HOME 段 ACCESS_TOKEN 缺失", file=sys.stderr)
|
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 验证命令)。
|
"""外部事实重验契约(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 或旧端点。
|
PATH 桩记录 curl/wget argv、临时 Git 仓库与临时登记表;不读取真实 ~/.secrets,不连接 Gitea 或旧端点。
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
@@ -22,7 +23,7 @@ OLD_DATE = "2026-01-01"
|
|||||||
SECRETS = [HOME_TOKEN, IMC_TOKEN]
|
SECRETS = [HOME_TOKEN, IMC_TOKEN]
|
||||||
|
|
||||||
STUB = r'''
|
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):
|
def _log(kind, **data):
|
||||||
@@ -49,33 +50,38 @@ def _popen(self, args, *rest, **kwargs):
|
|||||||
subprocess.Popen.__init__ = _popen
|
subprocess.Popen.__init__ = _popen
|
||||||
|
|
||||||
|
|
||||||
class _Response:
|
def _response(url, code, body=b"", location=None):
|
||||||
def __init__(self, body):
|
headers = http.client.HTTPMessage()
|
||||||
self._body = body
|
if location:
|
||||||
|
headers["Location"] = location
|
||||||
def read(self):
|
resp = urllib.response.addinfourl(io.BytesIO(body), headers, url, code)
|
||||||
return self._body
|
resp.msg = "stub"
|
||||||
|
return resp
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, *exc):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _urlopen(request, *args, **kwargs):
|
REDIRECT_TARGET = "redirect-target.invalid"
|
||||||
url = getattr(request, "full_url", request)
|
|
||||||
auth = request.get_header("Authorization") if hasattr(request, "get_header") else None
|
|
||||||
|
# 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)
|
_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")
|
mode = os.environ.get("FAKE_HTTP", "200")
|
||||||
if mode == "200":
|
if mode == "200":
|
||||||
return _Response(b'{"login": "fixture"}')
|
return _response(url, 200, b'{"login": "fixture"}')
|
||||||
if mode == "leak":
|
if mode == "leak":
|
||||||
raise ValueError("Invalid header value " + repr(auth))
|
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:
|
class _Conn:
|
||||||
@@ -299,6 +305,55 @@ class FactsVerifyTests(unittest.TestCase):
|
|||||||
self.assertNotIn("Authorization", direct.stderr)
|
self.assertNotIn("Authorization", direct.stderr)
|
||||||
self.assert_no_secrets(result.stdout, result.stderr, direct.stdout, 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 层握手失败才支持该事实 -----------------------------------
|
# --- F4:只有 TLS 层握手失败才支持该事实 -----------------------------------
|
||||||
|
|
||||||
def test_f4_only_tls_handshake_failure_passes_and_refreshes(self):
|
def test_f4_only_tls_handshake_failure_passes_and_refreshes(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user