Require approved CA for local Mock HTTPS
This commit is contained in:
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"log"
|
||||
"maps"
|
||||
@@ -61,7 +63,14 @@ func newCurrentAgentServer(ctx context.Context, settings config.AgentEnvironment
|
||||
for index := range scenario.Script.Turns {
|
||||
scenario.Script.Turns[index].ReplyPCM16 = bytes.Clone(scenario.Script.Turns[index].ReplyPCM16)
|
||||
}
|
||||
uploadHTTP := localMockHTTPClient()
|
||||
ca, err := readAgentPEM("MTLS_CA_FILE", settings.CAFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uploadHTTP, err := localMockHTTPClient(ca)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var handler *rpc.Server
|
||||
worker := &rpc.ApprovedCallWorker{
|
||||
Lifecycle: ctx,
|
||||
@@ -109,9 +118,14 @@ func newCurrentAgentServer(ctx context.Context, settings config.AgentEnvironment
|
||||
|
||||
// The isolated Mock uploader may reach localhost only, even if a grant or
|
||||
// redirect unexpectedly names a real OSS endpoint. It never logs signed URLs.
|
||||
func localMockHTTPClient() *http.Client {
|
||||
func localMockHTTPClient(trustPEM []byte) (*http.Client, error) {
|
||||
roots := x509.NewCertPool()
|
||||
if !roots.AppendCertsFromPEM(trustPEM) {
|
||||
return nil, errors.New("Mock HTTPS requires an approved trust bundle")
|
||||
}
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = nil
|
||||
transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: roots}
|
||||
transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
@@ -123,5 +137,5 @@ func localMockHTTPClient() *http.Client {
|
||||
}
|
||||
return (&net.Dialer{}).DialContext(ctx, network, address)
|
||||
}
|
||||
return &http.Client{Transport: transport, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
|
||||
return &http.Client{Transport: transport, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -32,6 +35,10 @@ func currentAgentSetupFixture(t *testing.T) (config.AgentEnvironment, approvedMo
|
||||
CAFile: filepath.Join(root, "ca.pem"), CertFile: filepath.Join(root, "agent.pem"),
|
||||
KeyFile: filepath.Join(root, "agent.key"), PeerFingerprints: map[string]struct{}{strings.Repeat("a", 64): {}},
|
||||
}
|
||||
ca, _, _, _, _, _ := localCommandCertificates(t)
|
||||
if err := os.WriteFile(settings.CAFile, ca, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
scenario := approvedMockScenario{
|
||||
InboundPCM16: bytes.Repeat([]byte{1, 0}, 1600),
|
||||
Script: ai.ApprovedMockScript{Turns: []ai.ApprovedMockTurn{{Transcript: "synthetic ASR fixture"}}},
|
||||
@@ -98,8 +105,43 @@ func TestNewCurrentAgentServerRefusesUnsafeAdaptersBeforeResources(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalMockHTTPClientTrustsOnlyApprovedLocalHTTPSCertificate(t *testing.T) {
|
||||
local := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer local.Close()
|
||||
trustedPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: local.Certificate().Raw})
|
||||
trusted, err := localMockHTTPClient(trustedPEM)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, err := trusted.Get(local.URL)
|
||||
if err != nil || response.StatusCode != http.StatusOK {
|
||||
t.Fatalf("verified local HTTPS was refused: response=%v err=%v", response, err)
|
||||
}
|
||||
_ = response.Body.Close()
|
||||
|
||||
unrelatedCA, _, _, _, _, _ := localCommandCertificates(t)
|
||||
untrusted, err := localMockHTTPClient(unrelatedCA)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, err = untrusted.Get(local.URL)
|
||||
var unknownAuthority x509.UnknownAuthorityError
|
||||
if response != nil || !errors.As(err, &unknownAuthority) {
|
||||
t.Fatalf("unknown local HTTPS certificate was admitted: response=%v err=%v", response, err)
|
||||
}
|
||||
if client, err := localMockHTTPClient([]byte("invalid trust bundle")); err == nil || client != nil {
|
||||
t.Fatalf("invalid trust bundle was accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalMockHTTPClientRefusesExternalAndRedirectedTargets(t *testing.T) {
|
||||
client := localMockHTTPClient()
|
||||
ca, _, _, _, _, _ := localCommandCertificates(t)
|
||||
client, err := localMockHTTPClient(ca)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response, err := client.Get("https://oss.example.invalid/approved"); err == nil || response != nil || !strings.Contains(err.Error(), "not local") {
|
||||
t.Fatalf("Mock uploader contacted an external target: %v", err)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,10 @@ func runCurrentDispatcher(ctx context.Context, mode string) (result error) {
|
||||
if err != nil {
|
||||
return errors.New("Dispatcher mTLS Agent certificate configuration is invalid")
|
||||
}
|
||||
httpClient := localMockHTTPClient()
|
||||
httpClient, err := localMockHTTPClient(ca)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpClient.Timeout = 10 * time.Second
|
||||
_, reader, err := dispatcherConfigurationClient(mode, httpClient)
|
||||
if err != nil {
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
- 最终结果隔离组件:`ApprovedExecution` 已保留获批任务的 `caller_profile_id`;`FinalResultPayload` 只从批准的任务身份、确认时间及实际收到的用户媒体窗口生成当前唯一结果;最终 ASR 文本与字面关键词拒联写入完整转写,未播放的助手候选内容不冒充转写,未知 SIP 响应码保留 `null`,无录音保留 `{}`。单元测试直接核验当前 MQ Schema、字段缺失/乱序时窗与无应答负例;开场白发送失败不得计入已播放音频。此组件已接入 Agent 合成 runner,但未证明真实 RTP 转写时间精度。
|
||||
- 本地隔离链路:`RunApprovedCall` 显式使用合成 `ApprovedMockPipeline`,让共享批准通话流程按 ASR-only 限制消费实际读出的 PCM Mock 媒体帧;`RecordingSession` 生成内存 WAV,`FinalResultPayload` 仅用最终模拟识别及实测采集时窗构造结果。Agent 经双向 TLS gRPC 向 Dispatcher 确认结束、领取原始资产签名授权,再对本地 HTTPS OSS Mock 单次 PUT;Dispatcher 将原上传事实与唯一最终结果 outbox 同事务提交。测试确认一次 PUT、一次结果、无业务文件,使用官方 SDK 生成的路径;Mock ASR 与 OSS 服务不验证真实供应商协议或签名。这不是主入口执行、RabbitMQ 投递或外部验收。
|
||||
- `ApprovedRecordedMockCall` 将显式批准的 ASR-only AI 快照、合成 PCM 媒体、每通话独立脚本、实际采集的有界 WAV、用户最终识别时窗和 `RecordingDelivery` 组合到一个可供 Agent 工人调用的 Mock runner;隔离测试验证先结束事实、一次本地 OSS PUT、唯一最终结果及正常路径无业务文件。无实际读入媒体时明确报告失败并以空录音、生成失败原因收口,不伪造上传或转写;缺失每通话交付器、交付器与签发 D/数字租户/事件不一致、私有恢复目录并非 `0700` 或模拟脚本不满足已批准 AI 时,均在发外呼接受回执前拒绝。此 runner 已接到隔离 Mock 主 CLI 的 `ApprovedCallWorker`,但还没有从主进程完成 D→A 执行、Agent→D 录音与 MQ 出站的端到端联测;现有 OSS/RPC 测试均为隔离替身,不能代表真实线路或供应商通过。
|
||||
- 本机 HTTPS Mock 信任:Agent 上传及 Dispatcher 配置读取使用现有 mTLS CA 信任束校验证书,不放宽主机名或证书验证,仍拒绝非本机目标及跨域重定向。测试确认已批准的本机 HTTPS 证书可访问,未知证书与无效信任束均失败;未使用 `InsecureSkipVerify`。这不是对真实 OSS 的验证。
|
||||
- 已验证:`go test ./... -count=1`、`go test -race ./... -count=1`、`go vet ./...`、`go build ./...`、`PATH=/tmp/sip-go-agent-tools/bin:$PATH bash scripts/check-current-contracts.sh`、`PATH=/tmp/sip-go-agent-tools/bin:$PATH bash scripts/check-proto.sh`、`git diff --check`。主 Agent 入口已组装隔离合成媒体和录音交付,但尚未通过主进程 D↔A 会话/执行/录音、实际上传事实与最终结果交付、MQ 发布及重启恢复的整体联测,不能宣称 P06 通过。
|
||||
|
||||
## 验收台账
|
||||
|
||||
Reference in New Issue
Block a user