Require pinned mTLS peer for recording RPCs
This commit is contained in:
@@ -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 清理完成。
|
||||
|
||||
## 验收台账
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user