From 43c370d153de7df5ef8ed38ed7628fc671b51bab Mon Sep 17 00:00:00 2001 From: wlh000 Date: Wed, 5 Aug 2026 10:52:47 +0800 Subject: [PATCH] fix: bind cert renew to authenticated agent (IK6GOV) --- .../internal/forward/asset_forwarder.go | 12 +++++ .../internal/forward/asset_forwarder_test.go | 49 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/tms/gateway/internal/forward/asset_forwarder.go b/tms/gateway/internal/forward/asset_forwarder.go index aab54da..fe329af 100644 --- a/tms/gateway/internal/forward/asset_forwarder.go +++ b/tms/gateway/internal/forward/asset_forwarder.go @@ -84,6 +84,11 @@ func (f *assetForwarder) ForwardRegister(agentID uint64, frame *protocol.Frame, // ForwardCertRenew 处理 TypeCertRenew(0x08)。同 ForwardRegister, // 但响应帧类型为 TypeCertRenewResp(0x0C)。 func (f *assetForwarder) ForwardCertRenew(agentID uint64, frame *protocol.Frame, writeBack func(*protocol.Frame) error) { + if agentID == 0 { + log.Warnf("cert-renew rejected for anonymous connection, seq: %d", frame.SeqID) + writeError(writeBack, frame.SeqID, "authentication required") + return + } in := &assetpb.RenewCertRequest{} if err := proto.Unmarshal(frame.Payload, in); err != nil { log.Warnf("cert-renew unmarshal failed, agent_id: %d, seq: %d, err: %v", @@ -91,6 +96,13 @@ func (f *assetForwarder) ForwardCertRenew(agentID uint64, frame *protocol.Frame, writeError(writeBack, frame.SeqID, "decode error") return } + if in.AgentId != 0 && in.AgentId != agentID { + log.Warnf("cert-renew identity mismatch, authenticated_agent_id: %d, requested_agent_id: %d, seq: %d", + agentID, in.AgentId, frame.SeqID) + writeError(writeBack, frame.SeqID, "identity mismatch") + return + } + in.AgentId = agentID go func() { ctx, cancel := context.WithTimeout(context.Background(), f.timeout) defer cancel() diff --git a/tms/gateway/internal/forward/asset_forwarder_test.go b/tms/gateway/internal/forward/asset_forwarder_test.go index 5c576af..20c0138 100644 --- a/tms/gateway/internal/forward/asset_forwarder_test.go +++ b/tms/gateway/internal/forward/asset_forwarder_test.go @@ -24,6 +24,7 @@ type fakeAssetClient struct { regDelay time.Duration renewResp *assetpb.RenewCertResponse renewErr error + renewReq *assetpb.RenewCertRequest regCalls int renewCalls int @@ -41,8 +42,9 @@ func (f *fakeAssetClient) Register(ctx context.Context, _ *assetpb.RegisterReque return f.regResp, f.regErr } -func (f *fakeAssetClient) RenewCert(_ context.Context, _ *assetpb.RenewCertRequest) (*assetpb.RenewCertResponse, error) { +func (f *fakeAssetClient) RenewCert(_ context.Context, req *assetpb.RenewCertRequest) (*assetpb.RenewCertResponse, error) { f.renewCalls++ + f.renewReq = proto.Clone(req).(*assetpb.RenewCertRequest) return f.renewResp, f.renewErr } @@ -177,6 +179,51 @@ func TestForwardCertRenew_Success(t *testing.T) { } } +func TestForwardCertRenew_RejectsAnonymousConnection(t *testing.T) { + fc := &frameCollector{} + cli := &fakeAssetClient{renewResp: &assetpb.RenewCertResponse{Success: true}} + f := newAssetFwdFor(cli, 500*time.Millisecond) + payload := mustMarshal(t, &assetpb.RenewCertRequest{AgentId: 9}) + f.ForwardCertRenew(0, &protocol.Frame{SeqID: 4, Payload: payload}, fc.writeBack) + fc.waitFor(t, 1) + if got := fc.snapshot()[0]; got.Type != protocol.TypeError { + t.Fatalf("anonymous renew must return TypeError, got %s", got.Type) + } + if cli.renewCalls != 0 { + t.Fatalf("backend must not be called for anonymous renew, got %d calls", cli.renewCalls) + } +} + +func TestForwardCertRenew_RejectsMismatchedAgentID(t *testing.T) { + fc := &frameCollector{} + cli := &fakeAssetClient{renewResp: &assetpb.RenewCertResponse{Success: true}} + f := newAssetFwdFor(cli, 500*time.Millisecond) + payload := mustMarshal(t, &assetpb.RenewCertRequest{AgentId: 2002}) + f.ForwardCertRenew(1001, &protocol.Frame{SeqID: 5, Payload: payload}, fc.writeBack) + fc.waitFor(t, 1) + if got := fc.snapshot()[0]; got.Type != protocol.TypeError { + t.Fatalf("mismatched identity must return TypeError, got %s", got.Type) + } + if cli.renewCalls != 0 { + t.Fatalf("backend must not be called for mismatched identity, got %d calls", cli.renewCalls) + } +} + +func TestForwardCertRenew_BindsAuthenticatedAgentWhenPayloadOmitsID(t *testing.T) { + fc := &frameCollector{} + cli := &fakeAssetClient{renewResp: &assetpb.RenewCertResponse{Success: true}} + f := newAssetFwdFor(cli, 500*time.Millisecond) + payload := mustMarshal(t, &assetpb.RenewCertRequest{}) + f.ForwardCertRenew(1001, &protocol.Frame{SeqID: 6, Payload: payload}, fc.writeBack) + fc.waitFor(t, 1) + if got := fc.snapshot()[0]; got.Type != protocol.TypeCertRenewResp { + t.Fatalf("renew with omitted agent ID must succeed, got %s", got.Type) + } + if cli.renewReq == nil || cli.renewReq.AgentId != 1001 { + t.Fatalf("backend request must use authenticated agent ID 1001, got %+v", cli.renewReq) + } +} + func TestForwardCertRenew_BackendErrorWritesTypeError(t *testing.T) { fc := &frameCollector{} cli := &fakeAssetClient{renewErr: errors.New("upstream down")} -- Gitee