Require pinned mTLS peer for recording RPCs

This commit is contained in:
2026-09-30 10:35:11 +08:00
parent 8fb1f9410c
commit 20a43f80a7
4 changed files with 81 additions and 1 deletions
@@ -98,6 +98,7 @@
- 数据归属根因:现行 SQLite 的 `CloseAdmission` 原本只按传入的 Dispatcher ID 插入或更新状态;把已有未投递 outbox 的 D1 数据库交给 D2 启动时,会错误建立第二个归属状态。新测试先证实该问题,再要求同一事务内检查既有归属,冲突时返回身份错误且保留原归属和未确认 outbox;本轮不迁移、不删除任何既有数据库。旧入口及名称残留仍须分批清理,不能因该测试通过便宣称 P07 完成。
- CLI 去旧入口:`main.go` 先由 AST 回归测试证实还有 14 个与进程入口无关的函数,随后收敛为 `main` 与唯一 `newRootCommand`;未注册的旧 Agent/Dispatcher 入口及旧上传重试命令直接移除,根命令仍仅提供当前 Agent/Dispatcher 两个显式角色。根角色/废弃参数、非 Mock 预检已有现行命令测试覆盖;原 SQLite 身份/未投递回执保护已由现行 Store 回归接管,线路前缀与 ASR-only 的行为由现行策略及 AI 测试覆盖。不迁移或删除任何实际 SQLite、spool 或 outbox。其余旧模块、契约、引用及代次命名尚待清查,不能将本批当作 P07 整体完成。
- RPC 信任边界:录音服务原先复用旧 Dispatcher server 的可选 peer 校验函数;新 `peer_test.go` 先验证当前录音必须具备经 mTLS 验证的证书和预授权指纹,随后移入无可选关闭分支的独立校验函数。录音授权及原有双向 TLS 传输测试继续通过;旧 Dispatcher 上传/事件服务尚未删除,此批不能算作 RPC 清理完成。
## 验收台账
+32
View File
@@ -0,0 +1,32 @@
package rpc
import (
"context"
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)
func verifyRecordingPeer(ctx context.Context, meta *agentpb.RequestMeta, fingerprints map[string]struct{}) error {
if meta == nil {
return status.Error(codes.InvalidArgument, "agent request metadata is required")
}
p, ok := peer.FromContext(ctx)
if !ok || p.AuthInfo == nil {
return status.Error(codes.Unauthenticated, "verified mTLS agent certificate is required")
}
tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo)
if !ok || len(tlsInfo.State.VerifiedChains) == 0 || len(tlsInfo.State.VerifiedChains[0]) == 0 {
return status.Error(codes.Unauthenticated, "verified mTLS agent certificate is required")
}
if len(fingerprints) == 0 {
return status.Error(codes.PermissionDenied, "approved agent certificate fingerprints are required")
}
if _, allowed := fingerprints[CertificateFingerprint(tlsInfo.State.VerifiedChains[0][0])]; !allowed {
return status.Error(codes.PermissionDenied, "agent certificate fingerprint is not approved")
}
return nil
}
+47
View File
@@ -0,0 +1,47 @@
package rpc
import (
"context"
"crypto/tls"
"crypto/x509"
"testing"
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)
func TestRecordingPeerRequiresVerifiedPinnedCertificate(t *testing.T) {
meta := &agentpb.RequestMeta{AgentId: "agent-mock"}
certificate := &x509.Certificate{Raw: []byte("isolated-agent-certificate")}
fingerprint := CertificateFingerprint(certificate)
approved := map[string]struct{}{fingerprint: {}}
verified := peer.NewContext(context.Background(), &peer.Peer{AuthInfo: credentials.TLSInfo{
State: tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{{certificate}}},
}})
unverified := peer.NewContext(context.Background(), &peer.Peer{AuthInfo: credentials.TLSInfo{
State: tls.ConnectionState{PeerCertificates: []*x509.Certificate{certificate}},
}})
for _, tc := range []struct {
name string
ctx context.Context
meta *agentpb.RequestMeta
fingerprints map[string]struct{}
want codes.Code
}{
{"missing metadata", verified, nil, approved, codes.InvalidArgument},
{"missing mTLS", context.Background(), meta, approved, codes.Unauthenticated},
{"unverified certificate", unverified, meta, approved, codes.Unauthenticated},
{"missing fingerprints", verified, meta, nil, codes.PermissionDenied},
{"unapproved certificate", verified, meta, map[string]struct{}{"different": {}}, codes.PermissionDenied},
{"approved certificate", verified, meta, approved, codes.OK},
} {
t.Run(tc.name, func(t *testing.T) {
if got := status.Code(verifyRecordingPeer(tc.ctx, tc.meta, tc.fingerprints)); got != tc.want {
t.Fatalf("peer decision = %v, want %v", got, tc.want)
}
})
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ func (s *RecordingServer) authorize(ctx context.Context, meta *agentpb.RequestMe
if dispatcherID != s.DispatcherID {
return status.Error(codes.PermissionDenied, "recording fact targets another Dispatcher")
}
if err := validateDispatcherPeer(ctx, meta, true, s.TrustedFingerprints, nil); err != nil {
if err := verifyRecordingPeer(ctx, meta, s.TrustedFingerprints); err != nil {
return err
}
if err := s.AuthorizeSession(meta); err != nil {