feat(agent): Agent 远程恢复同样校验备份 SHA-256(补全 #75) (#76)

AgentRestoreSpec/RestoreSpec 新增 Checksum 并由 GetAgentRestoreSpec 透传;Agent ExecuteRestore 在解压前比对 SHA-256,不匹配即中止还原。两条恢复路径完整性保证一致。
This commit is contained in:
Wu Qing
2026-05-27 00:40:49 +08:00
committed by GitHub
parent 45bc210313
commit e63b8f0be8
4 changed files with 27 additions and 0 deletions

View File

@@ -190,6 +190,7 @@ type RestoreSpec struct {
Storage StorageTargetConfig `json:"storage"`
StoragePath string `json:"storagePath"`
FileName string `json:"fileName"`
Checksum string `json:"checksum,omitempty"`
}
// RestoreUpdate 与 service.AgentRestoreUpdate 对齐

View File

@@ -379,6 +379,24 @@ func (e *Executor) ExecuteRestore(ctx context.Context, restoreRecordID uint) err
return err
}
// 2.5) 完整性校验:还原前比对下载对象的 SHA-256与 Master 本地恢复路径一致)。
// 拒绝还原损坏或被篡改的备份;早期无 checksum 的备份跳过(向后兼容)。
if strings.TrimSpace(spec.Checksum) != "" {
e.appendRestoreLog(ctx, restoreRecordID, "[agent] 校验备份完整性SHA-256\n")
actual, sumErr := computeFileSHA256(artifactPath)
if sumErr != nil {
msg := fmt.Sprintf("计算校验和失败: %v", sumErr)
e.reportRestoreFailure(ctx, restoreRecordID, msg)
return fmt.Errorf("%s", msg)
}
if !strings.EqualFold(actual, spec.Checksum) {
msg := "备份文件完整性校验失败SHA-256 不匹配,文件可能已损坏或被篡改"
e.reportRestoreFailure(ctx, restoreRecordID, msg)
return fmt.Errorf("%s期望 %s实际 %s", msg, spec.Checksum, actual)
}
e.appendRestoreLog(ctx, restoreRecordID, "[agent] 完整性校验通过\n")
}
// 3) 解压Agent 不支持加密,遇到 .enc 会直接失败)
preparedPath := artifactPath
if strings.HasSuffix(strings.ToLower(preparedPath), ".enc") {

View File

@@ -463,6 +463,8 @@ type AgentRestoreSpec struct {
Storage AgentStorageTargetConfig `json:"storage"`
StoragePath string `json:"storagePath"`
FileName string `json:"fileName"`
// Checksum 源备份对象的 SHA-256小写 hexAgent 在还原前据此校验完整性。
Checksum string `json:"checksum,omitempty"`
}
// AgentRestoreUpdate Agent 回传的增量更新。
@@ -549,6 +551,7 @@ func (s *RestoreService) GetAgentRestoreSpec(ctx context.Context, node *model.No
},
StoragePath: backupRecord.StoragePath,
FileName: backupRecord.FileName,
Checksum: backupRecord.Checksum,
}, nil
}

View File

@@ -379,6 +379,7 @@ func TestRestoreServiceAgentRestoreAccessUsesRestoreRecordNode(t *testing.T) {
Status: model.BackupRecordStatusSuccess,
FileName: "remote.tar.gz",
StoragePath: "file/2026/05/09/remote.tar.gz",
Checksum: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
StartedAt: startedAt,
CompletedAt: &completedAt,
}
@@ -404,6 +405,10 @@ func TestRestoreServiceAgentRestoreAccessUsesRestoreRecordNode(t *testing.T) {
if spec.RestoreRecordID != restore.ID || spec.StoragePath != backupRecord.StoragePath {
t.Fatalf("unexpected restore spec: %#v", spec)
}
// Agent 端完整性校验依赖 spec 透传源备份 checksum。
if spec.Checksum != backupRecord.Checksum {
t.Fatalf("expected spec.Checksum=%q, got %q", backupRecord.Checksum, spec.Checksum)
}
if _, err := h.service.GetAgentRestoreSpec(ctx, other, restore.ID); err == nil {
t.Fatal("expected non-owner node to be forbidden from restore spec")
}