feat(tunnel): zero-touch tunnel enrollment — control-plane PKI, host agent, iOS, nginx isolation
Customers install one command / log in once; hardware-generated keys never leave the device; CSRs return certs + subdomain; frpc + base-app run as durable services. No .p12, no manual cert import. Implements the MVP fast-path of docs/PLAN_TUNNEL_AUTOMATION.md. Control-plane / PKI (control-plane/): - ca/x509-assembler.ts: single KMS-signed real X.509 issuance primitive (Ed25519 + P-256) - ca/csr-ec.ts: P-256 PKCS#10 proof-of-possession (verifyCsrPoPEc) + CSR-key routing - ca/frpclient-issue.ts, ca/device-issue.ts: P-256 frp-client + device leaf signers - ca/rotate.ts + api/renew.ts: real-X.509 /renew + /device/:id/renew (mTLS current cert) - registry/devices.ts: device registry + per-account cap/rate-limit - auth/session.ts: device:enroll capability token mint/verify - api/device-enroll.ts: POST /device/enroll (ownership-gated, deny-by-default) - pairing/native-redeem.ts + shared gateAndConsumePairingCode; api/provision.ts native arm - boot/native-ca.ts + main.ts: wire two P-256 CAs + issuers + routers (dev / KMS fail-fast) Contracts: relay-contracts enroll right; relay-auth SPIFFE /device/ arm + spiffeIdFor(kind) Host agent (agent/): - transport/frpcToml.ts; provision/frpcBinary.ts + untar.ts (verify-download + traversal-safe extract) - keys P-256 keygen/CSR/loadIdentity; service two-unit install + BIND_HOST loopback S-GATE - net/loopbackLiteral.ts strict guard; health/probe.ts + transport/frpSupervise.ts; cli pair --install iOS (ios/Packages/ClientTLS): SecureEnclaveKey + CertificateSigningRequest + DeviceEnrollmentClient + Keychain enroll refactor (SecKey/Security.framework end-to-end, avoids the -25300 trap) Isolation (deploy/nginx): njs/getCertSub.js SAN parser + zone-anchored map -> 403 Verified: 758 tests green (control-plane 246, agent 267, relay-auth 133, relay-contracts 85, iOS ClientTLS 27), all tsc clean; real nginx+njs docker 403/200/400; Swift CSR accepted by the real control-plane verifier; frpc extract byte-identical to `tar -xO`. Cross-validation caught + fixed 5 real defects (1 critical, 4 high). Remaining = infra (KMS, nginx deploy, VPS frps, physical iPhone) per PROGRESS_LOG runbook. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,85 @@
|
||||
|
||||
> 新会话读到的第一块。保持准确,只描述"此刻"。
|
||||
|
||||
### 🔐 TUNNEL AUTOMATION — 零接触隧道注入(客户永不碰证书/密钥;2026-07-08)
|
||||
- **计划**: [PLAN_TUNNEL_AUTOMATION.md](./PLAN_TUNNEL_AUTOMATION.md)(design-locked)。目标:host 一条命令 onboard、device 登录一次即在硬件里生成不可导出密钥→CSR→拿证书,**无 .p12/AirDrop,私钥永不离设备**。三轨:A 控制面/PKI、B host agent、C 原生客户端(iOS/Android/desktop)。基础设施(frps/device-CA/frp-client-CA/nginx:8470 mTLS)已在 VPS M1 上线;本工作流建的是**自动化**(签发端点、njs cert→Host 绑定、硬件 keygen、host onboard)。
|
||||
- **分支**: `feat/tunnel-automation`(自 develop)。走 **MVP fast-path §7**(10 个 tracked task,见任务表 #1–#10),依赖图强制:A1 crypto → A2 契约 → A3 nginx 绑定为顺序地基,之后按职责(backend/host/iOS)扇出。
|
||||
- **编排(loop 模式)**: 每个 task = 一个 Workflow(TDD builder → `typescript-reviewer`+`security-reviewer` 并行交叉验证 → 自动修 confirmed critical/high → orchestrator 独立复验 tests 绿 → 记本日志 → 解锁下游)。三个 workflow 并行:A1(backend/crypto)、A2a(backend/契约)、B2h+B3(host/agent),文件互斥不冲突。
|
||||
- **地基三块全绿(均 orchestrator 独立复验,非仅采信 agent)**:**[x] A1**(x509-assembler+csr-ec;40 测试+openssl verify+tsc 干净)、**[x] A2a**(契约编辑;relay-auth 133 + relay-contracts 85)、**[x] B2h+B3**(frpcToml+frpcBinary;agent 全套 184)。详见下方条目。**[x] A3**(nginx cert→Host 绑定)DONE — orchestrator 已亲自审计 njs 解析器 + zone-anchor 加固(见下方条目)。
|
||||
- **Wave 2 全绿**: **[x] B1**(frp-client P-256 签发 + host keygen)、**[x] A4**(device-enroll + 会话令牌)。**统一复验**:control-plane 全套 **206/206**、`tsc --noEmit` exit 0(B1+A4 合并态干净,交叉噪声已消)。**A4 交叉验证抓到并修复 1 个 CRITICAL**:enroll 路由曾把客户端传的 `subdomain` 直接烧进 device 叶子 dNSName SAN 且无归属校验→account A 的 bearer 可铸 `victim.*` 证书绕过隔离;已修(归属校验 `ownerOfSubdomain==accountId` + 复用 host 的 charset/reserved 规则 + 用规范化后的标签),负例测试覆盖(B 请求 A 的 subdomain→403、未认领→403、非法→400、自有→201),orchestrator 已亲读 device-enroll.ts 审计确认。
|
||||
- **Wave 3 三路扇出**(不同职责/包,文件全互斥):**[x] C-iOS**(iOS:SecureEnclave keygen + CSR + Keychain 重构)DONE — orchestrator 独立复验 `swift build` 干净 + `swift test` **27/27** + **跨语言:真 control-plane verifyCsrPoPEc 接受 Swift 生成的 CSR(ok=true)**;**[x] B5**(host:两服务安装 + BIND_HOST S-GATE + `pair --install`)DONE — 独立复验 agent **214/214** + tsc 干净;**[x] A6**(backend:/renew + rotate.ts 真 X.509)DONE — 独立复验 control-plane **224/224** + tsc 干净;**已复用 A4 教训**(/renew 身份取自当前 mTLS 证书 + 注册表记录、绝不信 body,anti-smuggling 测试覆盖)。**[x] B4**(host 原生 run-loop + frpc 监督 + health probe + keystore P-256)DONE — 独立复验 agent **247/247**。
|
||||
- **✅ MVP 全 10 任务代码完成 + 集成复验全绿(2026-07-08)**:**716 测试跨全部包**(relay-contracts 85 / relay-auth 133 / control-plane 224 / agent 247 / iOS ClientTLS 27),4 个 TS 包 tsc 全 exit 0,swift 27。MVP 分层保证证实(契约→auth→控制面 签发/注入/续期→host agent 安装/运行/keystore→iOS 硬件注入 全共存绿)。
|
||||
- **交叉验证共抓修 5 个真缺陷(否则静默上线)**:A4 客户端 subdomain 跨租户绕过(CRITICAL)、B5 loopback 守卫绕过 ×2(HIGH)、A6 device 续期不延期(HIGH)、B4 health monitor 永不 healthy + frpc 管道不排空致挂起(HIGH)——每个安全关键修复 orchestrator 均亲自审计。
|
||||
- **[x] 加固 pass DONE**:9 项 confirmed-review 修复(Ed25519 长度守卫、csr-ec 不可变 + timingSafeEqualBytes、getCertSub readTlv 父容器边界[附 crafted-DER 回归:修前泄 'evil' 修后 '']、renew 有界限流 + csr max + 当前证书链/过期校验、systemd 全路径控制符守卫、keystore prime256v1 校验、@peculiar/asn1-* 声明为直接依赖)。**最终全绿 734 测试**(relay-contracts 85 / relay-auth 133 / control-plane 235 / agent 254 / iOS ClientTLS 27),4 个 TS 包 tsc 全干净。
|
||||
- **✅ MVP 代码全部完成(10/10 任务)+ 5 个真缺陷经交叉验证抓修。剩余纯属"需真基础设施"**(见下 ACC RUNBOOK):不能在本环境伪造。
|
||||
- **「继续」round — 集成推进(2026-07-09)**:**[x] #11 frpc tar.gz 解压**(traversal-safe USTAR + 真 frp v0.61.1 sha256 pins;**对真 frp 归档解出的 frpc 与 `tar -xO` 逐字节一致** 14,259,442 字节;agent 254→267)。**[x] #14 原生 boot 接线**(boot/native-ca.ts 建 frp-client-CA+device-CA 两个 P-256 CA[DEV 自签/PROD KMS fail-fast];main.ts 组合 issuers 共享一个 DeviceStore + 注册 device-enroll + renew 路由 + 真 anchors + 生产 fail-closed 断言;Ed25519 relay /enroll 原样不动)。**wired app e2e 证实**(integration-native.test.ts 6 测试,fastify inject):device mint→/device/enroll(自有 alice)→201 真叶子链到 device-CA + dNSName → /device/:id/renew→201 同 subdomain;归属门 e2e:未认领→403、跨租户 B-on-A→403、缺 token→401;host 叶子→/renew→201 链到 frp-client-CA、不信任 CA→401。**[x] #15 原生 host /enroll P-256 arm DONE**:/enroll 按 CSR 密钥类型分叉(EC-P256→原生 frp-client 叶子 via 已接线 hostSigner;Ed25519→relay 原样);**把安全关键的配对门 `gateAndConsumePairingCode` 从 redeem.ts 抽出供两臂共用**(单次 CAS+锁定+过期+PoP/防替换 顺序不变,relay 逐字节证实不变);**anti-smuggling 再守**(dNSName SAN 只来自服务端分配的 subdomain,body 的 subdomain 惰性——负例证实);hostContentSecret:null。host onboard e2e 证实:配对码→/enroll(EC CSR)→201 叶子链到 frp-client-CA + dNSName → /renew→201 同 subdomain;单次重放/防替换负例绿。**当前全绿 758 测试**(cp 246 / agent 267 / relay-auth 133 / relay-contracts 85 / iOS 27),4 个 TS 包 tsc 全干净。
|
||||
- **🎯 后端 onboard 全链路已接线 + wired-app e2e 证实**:host enroll + device enroll + host/device renew + 归属隔离门 + 链校验 全部跑通(fastify inject)。**代码侧至此完成**;剩余小对齐(#16:agent pair.ts 解析原生响应形状、生产 NODE_ENV 守卫、machineId 去重需 relay-contracts 加列)+ 真基础设施(KMS/nginx 部署/真机 iOS/真 VPS frps)。
|
||||
- **ACC RUNBOOK(#10;需 VPS `8.138.1.192` + 真 iPhone + KMS,不可本地伪造)**:
|
||||
1. **KMS/CA 物料**:为 frp-client-CA(P-256)+ device-CA(P-256)provision KMS 密钥,`buildCaSigner` fail-fast 需真 KMS ref;boot 装配 frpclient-issue/device-issue/rotate 用真签发器。
|
||||
2. **接线(#14)**:control-plane main.ts 注册 device-enroll + renew(**并传 hostCaAnchorsDer/deviceCaAnchorsDer**,否则链/过期校验静默 no-op)+ device-grant/session。
|
||||
3. **nginx(部署,安全关键)**:装 getCertSub.js 到 /etc/nginx/njs/ + main-context `load_module ...js_module.so`;frp-mtls.conf 已有 cert→Host map;renew 若走 header 认证,**nginx 必须由 $ssl_client_cert 设 x-client-cert 并 strip 客户端自带**。
|
||||
4. **frpc 真二进制(#11.9)**:B3 tar.gz 解压 + 换真 sha256 校验和(frp 发布 checksums),host `run` 才能真监督 frpc。
|
||||
5. **host onboard**:真 Mac/Linux 跑 `pair <code> --install`(BIND_HOST loopback S-GATE 已守)→ 两 systemd/launchd unit → `https://<sub>.terminal.yaojia.wang` 可达。
|
||||
6. **iOS 真机**:Apple entitlement + 物理设备构建 App(接 ClientTLS enroll seam,#12)→ 登录 → SE keygen → enroll → mTLS 连。
|
||||
- **已在本环境证实(无需 VPS)**:真 nginx+njs docker 跨租户 **403 / 自有 200 / no-cert 400**;Swift CSR 被真 control-plane verifyCsrPoPEc 接受;BIND_HOST S-GATE fail-closed;734 单元/集成测试。**剩余为真机/真网/真 KMS 验收。**
|
||||
- **⚠️ B5 评审揭出两个真集成缺口(记 task #13,阻塞完整 host-side ACC)**:① keystore.loadIdentity 仍写死 Ed25519、不认 B1 的 `alg` 判别式 → 存的 P-256 身份加载错;② cli.ts `run` 对所有 host 无条件走 legacy runTunnel(relay rendezvous)→ 原生 host 未监督 frpc(即 B4/H4 原生 run-loop,我 10-task 分解从 B4 跳到了 B5)。这两块 + frpc tar.gz 解压(#11.9)是 host 真正打通隧道所需。**A2b 会话令牌子系统(login→device:enroll token)已并入 A4**——它是门控所有 device 注入的唯一 bootstrap 凭据,过于安全敏感,orchestrator 在建 A4(消费该 token 的端点)时亲自设计(计划允许 MVP 用 stub OAuth / 手贴 pairing code)。
|
||||
- **HARDENING backlog(task #11,非阻塞,confirmed review findings 延后)**: A1 —Ed25519 签名长度守卫、`@peculiar/asn1-*` 声明为直接依赖、csr-ec `UNIFORM_FAILURE` 改每次新字面量、复用 `timingSafeEqualBytes`、signer/sigAlg 运行时交叉校验;B3 —**tar.gz 解压(frp 发 .tar.gz;当前校验+放置的是压缩包非内层 frpc 二进制,B5 pair --install 端到端前必须补)**、把占位 sha256(64 个 0)换成 frp 发布校验和。
|
||||
|
||||
#### [x] A2a — 契约编辑: `enroll` right + SPIFFE `/device/` arm + `spiffeIdFor(kind)`(FIX C-native-1, H-cp-2, H-native-6;2026-07-08)
|
||||
- **交付**: 向后兼容的冻结契约编辑(**会话令牌子系统刻意不在本轮**)。① `relay-contracts/src/capability/token.ts`:`CapabilityRight` 联合类型 + `CapabilityRightSchema` z.enum 各加 `'enroll'`(`relay-auth/capability/issue.ts` 自动接受,已由 issue→verify 测试证实)。② `relay-auth/src/types.ts`:`SPIFFE_ID_RE`+`SpiffeIdSchema` 用 `(?:host|device)` 同时接受两 arm,保留 `..`/`*` 拒绝;host 形式仍原样通过。③ `relay-auth/src/agent/spiffe.ts`:`spiffeIdFor(accountId, id, domain?, kind:'host'|'device'='host')`——现有 3-arg 调用者(`control-plane/ca/issue.ts`)不变;`PARSE_RE` 捕获 kind;`parseSpiffeId → {accountId,id,kind}`;`index.ts` 导出 `SpiffeKind`。④ `relay-auth/src/agent/verify-mtls.ts`:新 parse 形状 + **host-only kind 守卫**(`not_host_cert`)——`/device/` 证书永不能在 host 控制信道被接受(防 device-id 与 hostId 碰撞跨租户),**收紧**信任而非放宽。`relay.` trust-domain 前缀刻意保留(按 FIX H-2 延后;强制点是 dNSName SAN 非 SPIFFE URI)。
|
||||
- **测试(新)**: `relay-auth/test/spiffe.test.ts`(host 向后兼容;device arm;**cross-track**:CP 签发端将来盖的原始 device SAN 能被 relay-auth schema+parser 接受且等于共享 builder 输出→签发方/验证方不漂移;malformed/traversal/wildcard/unknown-kind 拒绝)、`relay-auth/test/mtls.test.ts`(第 29 行 round-trip 断言改 `{accountId,id,kind}` 锁步 + device 证书在 host 信道被拒)、`relay-auth/test/capability.test.ts`(issue→verify `rights:['enroll']`)、`relay-contracts/test/capability-pairing.test.ts`(`parse('enroll')` ok / 未知仍抛)。
|
||||
- **验证(orchestrator 独立实测,非仅采信 agent)**: TDD RED-first(6 relay-auth + 2 relay-contracts 先红)。`cd relay-auth && npx vitest run` → **133 passed**;`cd relay-contracts && npx vitest run` → **85 passed**;`git diff` 复核 3 处信任关键 hunk(regex 非捕获组无放宽 / verify-mtls 守卫为收紧 / token 加法式)。交叉验证:两评审均 **approve**,仅 2 个 LOW(`m[2]! as SpiffeKind` 断言可换运行时守卫;trust-domain 清理延后)——不阻塞。
|
||||
- **偏差/决策**: `parseSpiffeId` 返回 `{accountId,id,kind}` 无 hostId 别名(锁步更新唯一调用者 verify-mtls);新增 `not_host_cert` 防御分支。**阻塞**: 无。**下一步**: A4 device-issue 用 `spiffeIdFor(accountId, deviceId, trustDomain, 'device')` 盖 device SAN。
|
||||
|
||||
#### [x] A1 — x509-assembler + csr-ec(KMS-signed 真 X.509 + P-256 PoP;FIX C-1;2026-07-08)
|
||||
- **交付**: 单一签发原语,附加式(不改 issue.ts/rotate.ts,那是 A6)。① `control-plane/src/ca/x509-assembler.ts`(NEW):`assembleCertificate(input)` 用 `@peculiar/asn1-x509` schema 建 v3 TBSCertificate + `AsnConvert.serialize`(不手写整证 DER),把**序列化后的 TBS** 交 `CaSigner.sign(tbsDer)` 签(KMS 边界——原始 CA 私钥永不载入,INV9),再包成 Certificate(嵌入 exact `tbsCertificateRaw`)。两签名族:`ed25519`(OID 1.3.101.112,raw 64B 入 BIT STRING)与 `ecdsa-p256`(OID 1.2.840.10045.4.3.2,导出 `normalizeEcdsaSignatureToDer` 把 raw P1363 r‖s 转 DER ECDSA-Sig-Value,正整数前导零处理;已 DER 则透传);tbs.signature 与外层 signatureAlgorithm 同一 OID。subject pubkey 收 CryptoKey 或 SPKI DER。② `control-plane/src/ca/csr-ec.ts`(NEW):`verifyCsrPoPEc(csr)→{ok,embeddedPubSpki}` 解析 P-256 PKCS#10、强制 EC P-256、验自签 PoP,**fail-closed + uniform**(malformed/非 P256/坏签统一 `{ok:false, []}`);`buildCsrEc()` 测试助手。③ `boot/ca-wiring.ts`(EDIT):加 `inProcessP256CaSigner`(TEST/DEV P-256 CA,返回 raw P1363)。
|
||||
- **验证(orchestrator 实测)**: `npx vitest run test/x509-assembler.test.ts test/csr-ec.test.ts test/ca.test.ts test/interop.test.ts test/rotate.test.ts` → **5 files / 40 passed**;`npx tsc --noEmit` exit 0;全套 132 passed 无回归。5 个验收门全过:(a) Ed25519 与 P-256 叶子各 `new x509.X509Certificate(der)` 重解析;(b) 各自对 CA 公钥验签 + 错 CA 负例;(c) **SAN 含 dNSName `alice.terminal.yaojia.wang` + URI `spiffe://…/host/alice` 双值 round-trip**(A3 njs 要解析的正是这些字节);(d) 合法 CSR ok:true、篡改/垃圾/Ed25519/P-384 统一 ok:false;(e) **真 openssl 3.0.18 `x509 -text` + `verify -CAfile ca.pem leaf.pem` → OK**。
|
||||
- **交叉验证**: ts + security 两评审,verdict warn/approve,**0 confirmed critical/high**(未触发自动修);9 个 medium/low 记入 **task #11 HARDENING**(依赖声明、Ed25519 长度守卫、UNIFORM_FAILURE 不可变、signer/sigAlg 交叉校验等,均非阻塞)。**阻塞**: 无。**下一步**: B1 `frpclient-issue.ts` / A4 `device-issue.ts` 经 `assembleCertificate` 出叶子;设备/host P-256 CSR 经 `verifyCsrPoPEc` 验。
|
||||
|
||||
#### [x] B2h+B3 — 原生 frpc.toml 写入器 + 固定版 frpc 校验下载(host-agent;2026-07-08)
|
||||
- **交付**(严守 lane,未碰 service/install、cli、keys、CA、control-plane、relay-*)。① `agent/src/transport/frpcToml.ts`(NEW):`buildNativeFrpcToml(opts)` 出 frp v0.61 TOML(PLAN_NATIVE_TUNNEL §4:serverAddr 默认 8.138.1.192/可配、serverPort 443、transport.tls.enable/serverName=frp.terminal.yaojia.wang/disableCustomTLSFirstByte/certFile/keyFile/trustedCaFile、auth.token、`[[proxies]]` type=http subdomain/localIP=127.0.0.1/localPort);**反 SSRF 硬不变量**:localIP 非 loopback → `FrpcTomlError`(负例测试);边界校验 DNS-label subdomain、port 1-65535、非空路径/token、控制符拒绝、Windows 路径转义;弃用 v0.8 `[common]/tls_enable`(断言不存在)。② `agent/src/provision/frpcBinary.ts`(NEW):`detectFrpcPlatform`(darwin/linux×arm64/amd64)、`FRPC_RELEASES` 固定 {version,url,sha256}、`provisionFrpc` 走 buildBinary 纪律:fetch→写临时到盘→**SHA-256 校验先于 place/exec**→符合 chmod 0755+原子 rename、不符 rm 临时+throw(什么都不放);fetch/fs/sha256 皆可注入,默认 fetch 强制 https(无 -k)。
|
||||
- **验证(orchestrator 实测)**: `cd agent && npx vitest run test/frpcToml.test.ts test/frpcBinary.test.ts` → **18 passed**;全套 184 passed;tsc exit 0。TDD RED-first 确认。
|
||||
- **交叉验证 + orchestrator 已修**: 评审 verdict warn,2 个 confirmed(未达自动修的 critical/high 门,orchestrator 亲手补):**MEDIUM** `defaultFetch` 的 `fetch()` 未包 try/catch→网络失败抛未类型化错(已包成 `FrpcProvisionError`);**LOW** 固定临时名 TOCTOU(已改 `${TMP_NAME}.${pid}.${randomBytes}` 每次唯一)。修后 frpcBinary 7/7 仍绿。**延后(task #11)**: **tar.gz 解压(当前放置的是压缩包,B5 端到端前必补)**、占位 sha256 换真校验和、可选签名校验。**阻塞**: 无。
|
||||
|
||||
#### [x] A3 — nginx cert→Host 绑定(FIX C-native-3;单一承重租户隔离控制;2026-07-08)
|
||||
- **交付**: 在 `ssl_verify_client on`(device-CA)之上,njs 解析叶子 dNSName SAN,`map` 要求其最左标签 == 请求 SNI 最左标签、否则 `return 403`,关闭 Model A skeleton-key。① `deploy/nginx/njs/getCertSub.js`(NEW):纯函数 `extractLeftmostDnsLabel(pem)`——手写 base64→**定长 DER 遍历**到 SAN(OID 55 1D 11)→取第一个 dNSName(0x82,而非 spiffe URI)→最左标签;主机名字节限 `[0-9A-Za-z.-]`(挡 `:` map-key 走私);**任何** 畸形/缺 SAN/截断 DER → `''`(fail-closed,空值永不满足 map)。只用 njs∩Node API,故同一源码 Node 单测 + njs 生产同跑。② `deploy/nginx/frp-mtls.conf`(EXTEND):`js_import`+`js_set $cert_sub`+**zone-anchored map** `"~^(?<s>[^:]+):(?P=s)\.terminal\.yaojia\.wang$"`+单个 `if ($cert_host_ok = 0) { return 403; }`(规则在 map,不散 if)。③ docker 集成测试 `deploy/nginx/test/integration/{Dockerfile,run.sh,…}`。
|
||||
- **验证(orchestrator 亲自审计 + 独立实测)**: 亲读 getCertSub.js 全文确认解析器结构正确且真 fail-closed(PEM body 锚 `CERTIFICATE-----` 尾防头字母泄入 base64、拒不定长/超长长度、结构化走 TBS→[3]→Extensions→SAN)。`npx vitest run test/getcertsub.test.ts` → **22 passed**(含真 A1-assembler 铸的 P-256 叶子);全套 control-plane 153 passed;tsc exit 0。**真 nginx+njs docker 集成 5/5**:alice-cert@alice=200、**bob-cert@alice=403(跨租户负例)**、alice-cert@bob=403、bob-cert@bob=200、no-cert=400。
|
||||
- **orchestrator 加固(评审 confirmed MEDIUM,亲手补)**: map 原为 label-only `(?P=s)\.`,不锚全 zone——虽被 stream SNI 路由 + `server_name` 正则**双重**约束(实际不可利用),仍改为**锚定全 zone `.terminal.yaojia.wang$`** 使 map **自足**(不依赖 server_name 正确性),加 `alice.evil.com→deny` 断言(label-only 会误放)。评审 verdict approve/warn,自动修移除了 security 评审遗留的 scratch 测试文件(曾破 tsc);2 个 LOW(readTlv 父容器边界、critical=true 覆盖)记 task #11(均无利用路径,cert 已过 ssl_verify_client)。**阻塞**: 无。
|
||||
|
||||
#### [x] B1 — frp-client P-256 host 叶子签发 + host P-256 keygen/CSR(FIX H-host-2/H-host-4;2026-07-08)
|
||||
- **交付**: ① `control-plane/src/ca/frpclient-issue.ts`(NEW)`createFrpClientLeafSigner` — 仿 issue.ts 形状但**P-256**:CSR 经 `verifyCsrPoPEc`(A1)验、host 注册表门控(bound/active/non-revoked + 嵌入 EC-SPKI==注册 pubkey,uniform `LeafSignError`,拒绝时 CA `sign()` 不可达)、经 `assembleCertificate`(A1)`sigAlg:'ecdsa-p256'` + frp-client-CA 签;SAN = dNSName `<sub>.terminal.yaojia.wang`(A3 强制点)+ URI `spiffeIdFor(accountId,sub,trustDomain,'host')`;CA:false、EKU clientAuth、KU digitalSignature、[now-60s,now+24h]。② `agent/src/keys/identity.ts`(EXTEND)加 `alg:'ed25519'|'p256'` 判别式 + `generateP256Identity`/`p256IdentityFromPrivatePem`(私钥永不出机);③ `agent/src/enroll/csr.ts`(EXTEND)P-256 ecdsa-with-SHA256 PKCS#10,Ed25519 路径逐字节不变。
|
||||
- **验证(orchestrator 独立复验,A4 并发期隔离跑)**: control-plane `frpclient-issue.test.ts` **10/10**(叶子 re-parse + 对 frp-client-CA 验签、SAN 双值 + relay-auth parseSpiffeId 认 kind 'host'、EKU/CA:false、gate uniform 拒绝、**真 agent enroll/csr 铸的 P-256 CSR 过 verifyCsrPoPEc 且 pubkey 逐字节流入叶子**);agent `identity/csr/cli` **27/27**(全套 194/194)。my-files tsc 干净。
|
||||
- **交叉验证**: 评审 approve/warn;自动修 no-op(唯一 "high" 是**误判**——实为并发 A4 的 `DeviceLeafSignerDeps.devices` tsc 缺失,fix agent 正确拒碰 A4 文件)。B1 自身 findings 全 LOW(记 #11):**frp-client 签发器尚未接入 enroll 路径(redeem.ts:110 仍用通用签发器)**、gate step-2 单测缺、cli.test `as never`。**偏差**:AgentIdentity 加必填 `alg` 判别式(改了一处 mock);P-256 pubkey 存 EC SPKI DER(匹配注册表比对)。**阻塞**: 无。frps refused-without-cert bring-up 未本地跑(无 frps 二进制;已在 VPS M1 证实)→ VPS 验收。
|
||||
|
||||
#### [x] A4 — device enrollment + 最小会话令牌子系统(FIX C-2, C-native-1;2026-07-08)
|
||||
- **交付**(control-plane,全 deny-by-default / uniform-reject / immutable / Zod-boundary)。① `auth/session.ts`:`mintDeviceEnrollToken`/`verifyDeviceEnrollToken`/`loginToAccountId`(单租户 STUB seam)/`requireEnrollRight`;device:enroll = §4.3 capability token(`rights:['enroll']`,sub=accountId,aud='device-enroll',10m TTL 夹 [60s,60m]),用 relay-auth `signPaseto` 铸(**非** `issueCapabilityToken`——后者硬夹 ≤60s connect clamp + 要求 host/DPoP,与"独立于 connect clamp 的无 host 分钟级 enroll token"[FIX C-native-1] 结构冲突;同一 Ed25519 v4.public 原语 + 同 §4.3 body,冻结验证器原样接受)。② `registry/devices.ts`:`DeviceRecord`+`DeviceRegistry`+`createMemoryDeviceStore`(镜像 hosts.ts,INV8 版本化状态原子交换)+ 每账户 CAP + 滑窗 rate-limit。③ `ca/device-issue.ts`:P-256 device 叶子经 assembleCertificate,注册表门控(verifyCsrPoPEc + bound/active/non-revoked + pubkey-match,uniform),SAN dNSName + `spiffeIdFor(...,'device')`,EKU clientAuth/CA:false。④ `api/device-enroll.ts`:`buildDeviceEnrollRouter`(可注册 fastify 插件),POST /device/enroll + /device/attest/challenge(stub);401/403/429/400 uniform。⑤ `store/ports.ts` ADD-ONLY DeviceStore 端口。
|
||||
- **交叉验证抓到并修复 CRITICAL**(两评审均 block): enroll 路由曾把客户端 `subdomain` 直烧进叶子 dNSName SAN 且**无归属校验 + 无 charset/reserved 限制** → account A bearer 可铸 account B 的 `victim.terminal.yaojia.wang` 证书,A3 nginx 会放行 → **击穿整个隔离模型**。自动修已闭合:`accountId` 只来自已验 token(`token.sub`);`normalizeSubdomain`+`isValidSubdomain`(复用 host onboarding 规则)+ 归属门控 `ownerOfSubdomain(sub)===accountId`(否则 403;未认领与外账户均 403,无存在性 oracle);SAN 用规范化后标签(非原始客户端字段)。
|
||||
- **验证(orchestrator 独立 + 亲审)**: 亲读 `device-enroll.ts` 确认修复正确;`grep` 确认负例测试覆盖(B 请求 A 的 subdomain→403 line190、未认领→403、非法→400、wrong-right bearer→403、自有→201)。**统一 `npx vitest run` → 25 files / 206 passed**;`npx tsc --noEmit` exit 0(含并发 B1 的 frpclient-issue.ts,无交叉噪声)。
|
||||
- **偏差/延后(记 #11)**: enroll token 的 `cnf.jkt` 是随机占位符 → **DPoP 绑定推迟,当前是 bearer token**(§5 bootstrap 滥用仅靠 短 TTL+cap+rate 缓解,符合 MVP 计划);cap/rate 与 registerDevice 非原子(TOCTOU MEDIUM);rate-limit 在 CSR PoP 之后(LOW);device.* 审计未加(共享 enum,推迟);DeviceRecord 类型 + 内存 store 暂放 registry/devices.ts(model/records.ts + store/memory.ts 本任务冻结)。插件未接入 main.ts(集成步骤)。**阻塞**: 无。
|
||||
|
||||
#### [x] C-iOS — SecureEnclave keygen + 手写 CSR + DeviceEnrollmentClient + Keychain enroll 重构(FIX C-native ClientTLS trap;2026-07-08)
|
||||
- **交付**(全在 `ios/Packages/ClientTLS` leaf 包——身份/mTLS 类型所在处,仅依赖 Security/Foundation;**全程 SecKey/Security.framework,避开 ClientTLS 陷阱**:不用 CryptoKit SecureEnclave.P256,不用 SecKeyCreateWithData-over-SE(-25300))。① `SecureEnclaveKey.swift`(NEW):`SecKeyCreateRandomKey` + `kSecAttrTokenIDSecureEnclave` + ECSECPrimeRandom(256) + `kSecAttrIsPermanent` + `SecAccessControl(.privateKeyUsage)`;私钥不可导出/永不出机;Simulator 有软件 fallback + SE-unavailable 错误。② `CertificateSigningRequest.swift`(NEW):手写 canonical DER PKCS#10(id-ecPublicKey+prime256v1 SPKI),`SecKeyCreateSignature(.ecdsaSignatureMessageX962SHA256)` 自签,sig OID ecdsa-with-SHA256。③ `DeviceEnrollmentClient.swift`(NEW):POST /device/enroll(bearer + A4 body + base64 CSR)+ attest/challenge + /device/:id/renew seam + `EnrollmentResult.isRenewalDue` 轮换 seam。④ `KeychainClientIdentityStore.swift`(MOD):加 `enroll()/renew()/remove()` —— 留 SE key、`SecItemAdd(kSecClassCertificate)` 存回叶子、`loadIdentity` 经 `SecItemCopyMatching(kSecClassIdentity)`,使 **MutualTLSChallengeResponder/ClientIdentity/urlCredential 原样复用**;`loadIdentity` 优先 SE 身份、回退 legacy .p12(双信任迁移窗);保留 PKCS12Importer。
|
||||
- **验证(orchestrator 独立实测)**: `swift build` 干净;`swift test` **27/27**(CSR 自签经 `SecKeyVerifySignature` 验 + SPKI/alg OID 逐字节 + PKCS#10 三元素结构 + enroll 请求映射 + save→loadIdentity roundtrip + 既有 mTLS responder 测试仍过)。**跨语言金标准**:Swift 生成的真 CSR 喂给真 control-plane `verifyCsrPoPEc` → **ok=true**(91 字节 P-256 SPKI)。SourceKit 曾误报"cannot find type"——已证伪(build 干净);`ZZEmitCSRCrossCheck.swift` 是一次性交叉验证临时文件,已删(仅剩 .build 索引残留)。
|
||||
- **交叉验证**: swift + security 两评审 verdict warn,**0 critical/high**;security 评审**正向确认属性 1–5 成立**(私钥不可导出/bearer 仅用于 enroll 请求/PoP 是真 SE 签名/轮换干净替换/SE-优先回退)。findings 全 medium/low 记 #12(2 处 `as!` 强转、corrupt-record、**App 层 enroll UI 注入 + URLSessionHTTPTransport 适配**)。**真机 SE keygen + SecIdentity roundtrip 需物理设备 + Keychain entitlement → 设备验收**。**阻塞**: 无。
|
||||
|
||||
#### [x] B5 — 两服务安装 + BIND_HOST loopback S-GATE + `pair --install`(FIX C-host-1/M-host-2service/L-host-zone;2026-07-08)
|
||||
- **交付**(agent 包,复用模块全 import 不改)。① **BIND_HOST S-GATE(FIX C-host-1,CRITICAL)**:`normalizeBindHost()`+`BindHostError`;缺省→127.0.0.1,非 loopback(0.0.0.0/LAN IP/::)抛;在 `buildInstallOptions`(读 env)与 `installService`(任何 write 前)双重强制,拒绝的安装零输出。② **两个 unit(FIX M-host-2service)**:参数化 launchd/systemd(label/ExecStart/programArguments/description),`installService` 出 base-app(`node dist/server.js` + base-app env)与 agent(`<bin> run` 监督 frpc、无 base-app env),`uninstallService` 双拆。③ **zone terminal(FIX L-host-zone)**:`assertNativeZone()`,base-app origin `https://<sub>.terminal.<domain>`(复用 originConfig,不削弱)。④ **cli `pair <CODE> --install`**:generateP256Identity(FIX H-host-2)→saveIdentity→enrollNative(P-256 CSR + POST /enroll)→provisionFrpc(B3)→writeFrpcConfig(buildNativeFrpcToml)→installService(双 unit)→打印 URL;legacy `pair`(无 --install)不变。
|
||||
- **交叉验证抓到并修复 HIGH×2(loopback 守卫绕过,同一 bug class)**:`isLoopbackBindHost`(install.ts)与**姊妹 anti-SSRF 守卫** `isLoopbackWsUrl`(agentConfig.ts)都用 `startsWith('127.')` → `127.0.0.1.attacker.example.com`/`127.evil.net` 被当 loopback(Node 会 DNS 解析再 bind → 正是 S-GATE 要防的绕过)。自动修抽出共享 `agent/src/net/loopbackLiteral.ts`(`node:net isIPv4` 要求整串是 dotted-quad + 首段 127,或 localhost/::1/[::1],余皆拒),两处守卫同修。
|
||||
- **验证(orchestrator 独立 + 亲审)**: 亲读 `loopbackLiteral.ts` 确认严格 fail-closed;`grep` 确认绕过回归测试在(`normalizeBindHost('127.0.0.1.attacker.example.com')`/`127.evil.net` 抛 + 专门 loopbackLiteral.test.ts)。**agent 全套 `npx vitest run` → 30 files / 214 passed**;`tsc --noEmit` exit 0;S-GATE 负例确认 0 写 0 运行。
|
||||
- **偏差/延后**: 原生 `run` frpc 监督 = B4/H4(未建,记 **#13**);keystore.loadIdentity P-256 分支(记 #13);frpcBinary 占位 sha256(#11.9);systemd `Environment=` 注入守卫一致性(MEDIUM)+ BASE_APP_ENV_KEYS 漏 SCROLLBACK_BYTES/MAX_PAYLOAD_BYTES(LOW)+ cli 若干 minor 记入 backlog。**阻塞**: 无(B5 本体完成;端到端 host 隧道待 #13)。
|
||||
|
||||
#### [x] A6-min — POST /renew(host)+ /device/:id/renew + rotate.ts 真 X.509(FIX H-host-3;2026-07-08)
|
||||
- **交付**: ① `ca/rotate.ts`(UPGRADE)弃 DEV JSON 占位,`createLeafRenewer` 委托 P-256 签发器(frpclient-issue/device-issue → assembleCertificate,sigAlg ecdsa-p256,CA 签在 KMS 后)出真 X.509;`renewHostLeaf(hostId, subjectPubkey, csr)` + `renewDeviceLeaf(deviceId, csr)` → `RenewedLeaf{cert,caChain,notAfter}`(notAfter 从 cert 读回)。② `api/renew.ts`(NEW)fastify 插件:POST /renew(host)+ POST /device/:id/renew,**仅由当前 mTLS 客户端证书认证**(可注入 PresentedClientCert seam,默认从 x-client-cert 读 base64-DER);身份(accountId + subdomain/deviceId + subject pubkey)靠**重解析当前证书的 SPIFFE SAN**(relay-auth parseSpiffeId),**绝不取自 body**(body 仅 {csr})。**anti-smuggling(A4 教训)**:重签 SAN 从注册表记录取同一 subdomain;把当前证书 pubkey 作 subject key 使委托门控强制 MVP 同密钥(不换钥)。每身份滑窗限流。uniform:无/坏证书 401、撤销/未知/账户或 deviceId 或密钥不符 403、换钥/坏 CSR 400、超限 429、成功 201。
|
||||
- **交叉验证抓到并修复 HIGH**: `renewDeviceLeaf` 曾每次复用**原始** notAfter(读静态 DeviceRecord.notAfter,enroll 时设一次)→ 续期不延期、最终发已过期证书。自动修:续期先让注册表对 active 设备重算+持久化 `now()+ttl` 再签,notAfter 从发出的 cert 读回。
|
||||
- **验证(orchestrator 独立 + 亲查修复)**: 亲读 rotate.ts 确认 device 续期先延期;**统一 control-plane `npx vitest run` → 26 files / 224 passed**;`tsc --noEmit` exit 0;rotate.test 升级为断言真 re-parse+CA 验签的 X.509(host+device 同 subdomain、换钥拒、撤销拒、no-smuggling),renew.test 13 路。
|
||||
- **延后(记 #14,上线前必办)**: buildRenewRouter 未接 main.ts;**部署安全:nginx 必须由 $ssl_client_cert 设 x-client-cert 且 strip 客户端自带的**(否则头可伪造身份);当前证书链/过期校验 + "过期证书→拒" 测试;rate-limiter Map 有界化;csr 加 max 长度。**阻塞**: 无。
|
||||
|
||||
#### [x] B4/H4 — 原生 run-loop(frpc 监督 + loopback health)+ keystore P-256 loadIdentity(2026-07-08)
|
||||
- **交付**(agent 包)。① `keys/keystore.ts` loadIdentity 按存储密钥 alg 分支(`createPrivateKey(pem).asymmetricKeyType`:ed25519→原路径逐字节不变;ec→p256IdentityFromPrivatePem)——存的 P-256 frp-client 身份能 round-trip(alg:p256 + 可用 ECDSA 签名钥)。② `health/probe.ts`(NEW):4 个可注入 seam 子检查(frpc alive、base-app loopback GET http://127.0.0.1:PORT、frpc 'start proxy success' 日志扫描、cert 未近过期 8h 窗)、`renderHealthStatus` 只印非敏感标识(subdomain/host id/过期 ISO/布尔,绝无 key/cert/token/CSR,INV9)、`startHealthMonitor`。③ `transport/frpSupervise.ts`(NEW——计划标 REUSE 但从未存在):spawn frpc + backoff 重启(1s..cap30s,稳定 60s 后 reset),IO 全注入。④ `cli.ts` run 分支:P-256 身份 + 写好的 frpc.toml → superviseFrpc;否则 legacy runTunnel(保留)。
|
||||
- **交叉验证抓到并修复 HIGH**: `readFrpcLog` 读 `<stateDir>/frpc.log` 但**无人写它** → 恒返 '' → HealthReport.healthy **永不为真**(H4 监控形同虚设);且 frpc 子进程 stdio pipe 不排空 → 缓冲填满致 write(2) 阻塞挂起。自动修:`createFileLoggingSpawn` 把 frpc stdout/stderr tee 进持久化(每次 spawn 截断)日志,同时排空管道(一并解决挂起 MEDIUM)。
|
||||
- **验证(orchestrator 独立)**: agent 全套 **247/247**(29+ 新:probe 19、frpSupervise 5、keystore/cli),tsc 干净;keystore P-256 round-trip 断言(alg + 原 pubkey 验签);status 无敏感泄漏断言(无 PRIVATE KEY/CERTIFICATE/token/csr)。
|
||||
- **偏差/延后**: 真 frpc 二进制运行仍待 B3 tar.gz 解压(#11);frpc-stdout→log 已由 tee 打通(生产可用);keystore 仅按 asymmetricKeyType='ec' 分支未细究 namedCurve(LOW,记 #11);cli args.code! 无 runCli 内守卫(LOW)。**阻塞**: 无。
|
||||
|
||||
### 🤖 ANDROID CLIENT — 与 iOS 对齐的原生客户端(计划 + 地基已落;2026-07-08)
|
||||
- **计划**: [ANDROID_CLIENT_PLAN.md](./ANDROID_CLIENT_PLAN.md)(多 agent 探索 iOS→综合→4 视角评审→终版)。镜像 iOS 的 SPM 包为 Gradle 模块;36 任务 AW0–AW6。三个开放问题已定:R2(Termux terminal-view/emulator 是 **Apache-2.0** + JitPack,非 GPL-阻塞,独立验证更正)、R1(接受 FCM 尽力而为后台送达)、R7(服务端用 `google-auth-library`)。
|
||||
- **本机环境约束**: JDK+Gradle 可用,但 **Android SDK 为空** → 仅**纯 Kotlin/JVM 模块**能真构建验证;Android 框架层(`:app`/`:terminal-view`/UI/FCM)只能写、待有 SDK 的机器构建。
|
||||
|
||||
Reference in New Issue
Block a user