升级管理
This commit is contained in:
@@ -363,12 +363,12 @@ public interface PcieCryptoService {
|
||||
|
||||
//----------------------------------------SDF通用算法运算类接口------------------------------------------
|
||||
/**
|
||||
* 使用通用内部 ECC 密钥执行签名。
|
||||
* 使用通用内部 ECC 密钥执行签名。(用户密钥使用)
|
||||
*/
|
||||
BackupDataResult eccInternalSign(EccInternalSignRequest request);
|
||||
|
||||
/**
|
||||
* 使用外部 ECC 私钥执行签名。
|
||||
* 使用外部 ECC 私钥执行签名。(用户密钥使用)
|
||||
*/
|
||||
BackupDataResult eccExternalSign(EccExternalSignRequest request);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ public class UpgradeProperties {
|
||||
|
||||
private String stagingRootDir = "/home/tmp/tms-upgrade-staging";
|
||||
private String logDir = "/home/tms/tmp/tms-upgrade-logs";
|
||||
private String signaturePublicKeyBase64 = "";
|
||||
private String signaturePublicKeyPemPath = "";
|
||||
|
||||
public String getStagingRootDir() {
|
||||
return stagingRootDir;
|
||||
@@ -25,11 +25,11 @@ public class UpgradeProperties {
|
||||
this.logDir = logDir;
|
||||
}
|
||||
|
||||
public String getSignaturePublicKeyBase64() {
|
||||
return signaturePublicKeyBase64;
|
||||
public String getSignaturePublicKeyPemPath() {
|
||||
return signaturePublicKeyPemPath;
|
||||
}
|
||||
|
||||
public void setSignaturePublicKeyBase64(String signaturePublicKeyBase64) {
|
||||
this.signaturePublicKeyBase64 = signaturePublicKeyBase64;
|
||||
public void setSignaturePublicKeyPemPath(String signaturePublicKeyPemPath) {
|
||||
this.signaturePublicKeyPemPath = signaturePublicKeyPemPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,15 +67,24 @@ public class UpgradeController {
|
||||
return ApiResponse.success(upgradeService.executeTask(taskId));
|
||||
}
|
||||
|
||||
@PostMapping("/upgrades/{taskId}/rollback")
|
||||
@Operation(summary = "回滚离线升级任务", description = "异步受理升级回滚请求,立即返回任务当前快照。")
|
||||
public ApiResponse<UpgradeTaskResponse> rollback(
|
||||
@Parameter(description = "升级任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(upgradeService.rollbackTask(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/upgrades")
|
||||
@Operation(summary = "查询升级记录列表", description = "分页查询升级任务列表,支持按升级对象和状态筛选。")
|
||||
public ApiResponse<UpgradeTaskPageResponse> list(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) String targetComponent,
|
||||
@RequestParam(required = false) String taskType,
|
||||
@RequestParam(required = false) String status
|
||||
) {
|
||||
return ApiResponse.success(upgradeService.listTasks(page, pageSize, targetComponent, status));
|
||||
return ApiResponse.success(upgradeService.listTasks(page, pageSize, taskType, status));
|
||||
}
|
||||
|
||||
@GetMapping("/upgrades/{taskId}")
|
||||
|
||||
@@ -9,8 +9,10 @@ public class UpgradeCreateTaskResponse {
|
||||
private String taskId;
|
||||
@Schema(description = "升级任务状态", example = "PENDING_CONFIRM")
|
||||
private String status;
|
||||
@Schema(description = "升级对象", example = "STANDARD_APP")
|
||||
private String targetComponent;
|
||||
@Schema(description = "升级任务类型", example = "RECEIVER")
|
||||
private String taskType;
|
||||
@Schema(description = "设备产品类型", example = "DIRECT")
|
||||
private String productType;
|
||||
@Schema(description = "当前版本", example = "V1.3.1")
|
||||
private String currentVersion;
|
||||
@Schema(description = "目标版本", example = "V1.3.2")
|
||||
@@ -32,12 +34,20 @@ public class UpgradeCreateTaskResponse {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
|
||||
@@ -7,18 +7,18 @@ public class UpgradeManifest {
|
||||
|
||||
@Schema(description = "升级包唯一标识", example = "PKG_20260319_001")
|
||||
private String packageId;
|
||||
@Schema(description = "升级对象", example = "STANDARD_APP")
|
||||
private String targetComponent;
|
||||
@Schema(description = "目标版本", example = "V1.3.2")
|
||||
private String targetVersion;
|
||||
@Schema(description = "升级任务类型", example = "RECEIVER")
|
||||
private String taskType;
|
||||
@Schema(description = "设备产品类型", example = "DIRECT")
|
||||
private String productType;
|
||||
@Schema(description = "升级版本", example = "V1.3.2")
|
||||
private String version;
|
||||
@Schema(description = "兼容的最低旧版本", example = "V1.3.0")
|
||||
private String minCompatibleVersion;
|
||||
@Schema(description = "升级包描述", example = "修复标准收发器已知问题")
|
||||
private String description;
|
||||
@Schema(description = "升级执行脚本路径", example = "scripts/orchestrator.sh")
|
||||
private String orchestrator;
|
||||
@Schema(description = "升级包校验信息")
|
||||
private FileChecksums fileChecksums = new FileChecksums();
|
||||
@Schema(description = "升级脚本入口")
|
||||
private Entrypoints entrypoints = new Entrypoints();
|
||||
|
||||
public String getPackageId() {
|
||||
return packageId;
|
||||
@@ -28,20 +28,28 @@ public class UpgradeManifest {
|
||||
this.packageId = packageId;
|
||||
}
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getTargetVersion() {
|
||||
return targetVersion;
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setTargetVersion(String targetVersion) {
|
||||
this.targetVersion = targetVersion;
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getMinCompatibleVersion() {
|
||||
@@ -60,31 +68,50 @@ public class UpgradeManifest {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getOrchestrator() {
|
||||
return orchestrator;
|
||||
public Entrypoints getEntrypoints() {
|
||||
return entrypoints;
|
||||
}
|
||||
|
||||
public void setOrchestrator(String orchestrator) {
|
||||
this.orchestrator = orchestrator;
|
||||
public void setEntrypoints(Entrypoints entrypoints) {
|
||||
this.entrypoints = entrypoints;
|
||||
}
|
||||
|
||||
public FileChecksums getFileChecksums() {
|
||||
return fileChecksums;
|
||||
}
|
||||
public static class Entrypoints {
|
||||
private String execute;
|
||||
private String precheck;
|
||||
private String verify;
|
||||
private String rollback;
|
||||
|
||||
public void setFileChecksums(FileChecksums fileChecksums) {
|
||||
this.fileChecksums = fileChecksums;
|
||||
}
|
||||
|
||||
public static class FileChecksums {
|
||||
private String payloadZip;
|
||||
|
||||
public String getPayloadZip() {
|
||||
return payloadZip;
|
||||
public String getExecute() {
|
||||
return execute;
|
||||
}
|
||||
|
||||
public void setPayloadZip(String payloadZip) {
|
||||
this.payloadZip = payloadZip;
|
||||
public void setExecute(String execute) {
|
||||
this.execute = execute;
|
||||
}
|
||||
|
||||
public String getPrecheck() {
|
||||
return precheck;
|
||||
}
|
||||
|
||||
public void setPrecheck(String precheck) {
|
||||
this.precheck = precheck;
|
||||
}
|
||||
|
||||
public String getVerify() {
|
||||
return verify;
|
||||
}
|
||||
|
||||
public void setVerify(String verify) {
|
||||
this.verify = verify;
|
||||
}
|
||||
|
||||
public String getRollback() {
|
||||
return rollback;
|
||||
}
|
||||
|
||||
public void setRollback(String rollback) {
|
||||
this.rollback = rollback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@Schema(description = "离线升级包预检响应")
|
||||
public class UpgradePreviewResponse {
|
||||
|
||||
@Schema(description = "升级对象", example = "STANDARD_APP")
|
||||
private String targetComponent;
|
||||
@Schema(description = "升级任务类型", example = "RECEIVER")
|
||||
private String taskType;
|
||||
@Schema(description = "设备产品类型", example = "DIRECT")
|
||||
private String productType;
|
||||
@Schema(description = "当前版本", example = "V1.3.1")
|
||||
private String currentVersion;
|
||||
@Schema(description = "目标版本", example = "V1.3.2")
|
||||
@@ -24,12 +26,20 @@ public class UpgradePreviewResponse {
|
||||
@Schema(description = "升级包说明", example = "修复标准收发器已知问题")
|
||||
private String description;
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
|
||||
@@ -9,8 +9,10 @@ public class UpgradeTaskResponse {
|
||||
private String taskId;
|
||||
@Schema(description = "升级方式", example = "OFFLINE")
|
||||
private String upgradeMode;
|
||||
@Schema(description = "升级对象", example = "STANDARD_APP")
|
||||
private String targetComponent;
|
||||
@Schema(description = "升级任务类型", example = "RECEIVER")
|
||||
private String taskType;
|
||||
@Schema(description = "设备产品类型", example = "DIRECT")
|
||||
private String productType;
|
||||
@Schema(description = "升级包文件标识", example = "FILE-20260319-000001")
|
||||
private String packageFileId;
|
||||
@Schema(description = "升级包名称", example = "upgrade-package.zip")
|
||||
@@ -48,12 +50,20 @@ public class UpgradeTaskResponse {
|
||||
this.upgradeMode = upgradeMode;
|
||||
}
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
|
||||
@@ -9,7 +9,8 @@ public class UpgradeTaskEntity extends BaseEntity {
|
||||
|
||||
private String taskId;
|
||||
private String upgradeMode;
|
||||
private String targetComponent;
|
||||
private String taskType;
|
||||
private String productType;
|
||||
private String packageFileId;
|
||||
private String packageName;
|
||||
private String packageHash;
|
||||
@@ -41,12 +42,20 @@ public class UpgradeTaskEntity extends BaseEntity {
|
||||
this.upgradeMode = upgradeMode;
|
||||
}
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
|
||||
@@ -2,6 +2,6 @@ package com.cisd.tms.modules.upgrade.enums;
|
||||
|
||||
public enum UpgradeTargetComponent {
|
||||
TMS,
|
||||
STANDARD_APP,
|
||||
DIRECT_APP
|
||||
RECEIVER,
|
||||
FIRMWARE
|
||||
}
|
||||
|
||||
@@ -4,5 +4,8 @@ public enum UpgradeTaskStatus {
|
||||
PENDING_CONFIRM,
|
||||
RUNNING,
|
||||
SUCCESS,
|
||||
FAILED
|
||||
FAILED,
|
||||
ROLLING_BACK,
|
||||
ROLLBACK_SUCCESS,
|
||||
ROLLBACK_FAILED
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.lang.ProcessBuilder.Redirect;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@@ -55,23 +56,40 @@ public class UpgradeTaskRunner {
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + task.getPackageFileId()));
|
||||
Path stagedDir = upgradePackageStagingService.stage(task.getTaskId(), Path.of(record.getStoragePath()));
|
||||
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
|
||||
Path payloadDir = upgradePackageStagingService.stage(task.getTaskId() + "-payload", stagedDir.resolve("payload.zip"));
|
||||
Path orchestrator = payloadDir.resolve(manifest.getOrchestrator()).normalize().toAbsolutePath();
|
||||
if (!Files.exists(orchestrator)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade orchestrator not found");
|
||||
}
|
||||
Path root = stagedDir.normalize().toAbsolutePath();
|
||||
|
||||
Process process = new ProcessBuilder("bash", orchestrator.toString(), task.getTaskId())
|
||||
.directory(payloadDir.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.redirectOutput(logPath.toFile())
|
||||
.start();
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
int precheckCode = runOptionalScript("precheck", root, manifest.getEntrypoints() == null ? "" : manifest.getEntrypoints().getPrecheck(), task, logPath);
|
||||
if (precheckCode != 0) {
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"FAILED",
|
||||
"orchestrator exited with " + exitCode,
|
||||
"precheck exited with " + precheckCode,
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
int executeCode = runRequiredScript("execute", root, manifest.getEntrypoints() == null ? "" : manifest.getEntrypoints().getExecute(), task, logPath);
|
||||
if (executeCode != 0) {
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"FAILED",
|
||||
"execute exited with " + executeCode,
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
int verifyCode = runOptionalScript("verify", root, manifest.getEntrypoints() == null ? "" : manifest.getEntrypoints().getVerify(), task, logPath);
|
||||
if (verifyCode != 0) {
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"FAILED",
|
||||
"verify exited with " + verifyCode,
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
@@ -80,8 +98,8 @@ public class UpgradeTaskRunner {
|
||||
}
|
||||
|
||||
DeviceSoftwareVersionEntity versionEntity = new DeviceSoftwareVersionEntity();
|
||||
versionEntity.setComponentCode(task.getTargetComponent());
|
||||
versionEntity.setComponentName(componentName(task.getTargetComponent()));
|
||||
versionEntity.setComponentCode(task.getTaskType());
|
||||
versionEntity.setComponentName(componentName(task.getTaskType()));
|
||||
versionEntity.setCurrentVersion(task.getTargetVersion());
|
||||
versionEntity.setSourceType("UPGRADE");
|
||||
versionEntity.setDetectedAt(LocalDateTime.now());
|
||||
@@ -113,15 +131,88 @@ public class UpgradeTaskRunner {
|
||||
}
|
||||
}
|
||||
|
||||
public void rollback(UpgradeTaskEntity task) {
|
||||
LocalDateTime startedAt = LocalDateTime.now();
|
||||
Path logPath = logPath(task.getTaskId() + "-rollback");
|
||||
try {
|
||||
Files.createDirectories(logPath.getParent());
|
||||
Files.writeString(logPath, "");
|
||||
|
||||
FileRecordEntity record = fileRecordRepository.findByFileId(task.getPackageFileId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + task.getPackageFileId()));
|
||||
Path stagedDir = upgradePackageStagingService.stage(task.getTaskId() + "-rollback", Path.of(record.getStoragePath()));
|
||||
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
|
||||
String rollbackPath = manifest.getEntrypoints() == null ? "" : trim(manifest.getEntrypoints().getRollback());
|
||||
if (rollbackPath.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade rollback script not found");
|
||||
}
|
||||
int exitCode = runRequiredScript("rollback", stagedDir.normalize().toAbsolutePath(), rollbackPath, task, logPath);
|
||||
if (exitCode != 0) {
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"ROLLBACK_FAILED",
|
||||
"rollback exited with " + exitCode,
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"ROLLBACK_SUCCESS",
|
||||
"",
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
} catch (Exception ex) {
|
||||
try {
|
||||
Files.createDirectories(logPath.getParent());
|
||||
Files.writeString(logPath, ex.getMessage() == null ? "upgrade rollback failed" : ex.getMessage());
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"ROLLBACK_FAILED",
|
||||
ex.getMessage(),
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private Path logPath(String taskId) {
|
||||
return Path.of(trim(upgradeProperties.getLogDir())).normalize().toAbsolutePath().resolve(taskId + ".log");
|
||||
}
|
||||
|
||||
private int runOptionalScript(String phase, Path stagedDir, String scriptPath, UpgradeTaskEntity task, Path logPath) throws IOException, InterruptedException {
|
||||
if (trim(scriptPath).isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return runRequiredScript(phase, stagedDir, scriptPath, task, logPath);
|
||||
}
|
||||
|
||||
private int runRequiredScript(String phase, Path stagedDir, String scriptPath, UpgradeTaskEntity task, Path logPath) throws IOException, InterruptedException {
|
||||
Path resolved = stagedDir.resolve(trim(scriptPath)).normalize().toAbsolutePath();
|
||||
if (!resolved.startsWith(stagedDir) || !Files.exists(resolved)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade " + phase + " script not found");
|
||||
}
|
||||
Process process = new ProcessBuilder("bash", resolved.toString(), task.getTaskId())
|
||||
.directory(stagedDir.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.redirectOutput(Redirect.appendTo(logPath.toFile()))
|
||||
.start();
|
||||
return process.waitFor();
|
||||
}
|
||||
|
||||
private String componentName(String componentCode) {
|
||||
return switch (trim(componentCode)) {
|
||||
case "TMS" -> "设备管理软件";
|
||||
case "STANDARD_APP" -> "标准收发器";
|
||||
case "DIRECT_APP" -> "直参轻量化终端";
|
||||
case "RECEIVER" -> "标准收发器";
|
||||
case "FIRMWARE" -> "固件";
|
||||
default -> componentCode;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,14 +15,14 @@ public interface UpgradeTaskMapper extends BaseMapperX<UpgradeTaskEntity> {
|
||||
UpgradeTaskEntity selectRunningTask();
|
||||
|
||||
List<UpgradeTaskEntity> selectPage(
|
||||
@Param("targetComponent") String targetComponent,
|
||||
@Param("taskType") String taskType,
|
||||
@Param("status") String status,
|
||||
@Param("offset") int offset,
|
||||
@Param("limit") int limit
|
||||
);
|
||||
|
||||
long countPage(
|
||||
@Param("targetComponent") String targetComponent,
|
||||
@Param("taskType") String taskType,
|
||||
@Param("status") String status
|
||||
);
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ public interface UpgradeTaskRepository {
|
||||
|
||||
Optional<UpgradeTaskEntity> findRunningTask();
|
||||
|
||||
List<UpgradeTaskEntity> findPage(String targetComponent, String status, int offset, int limit);
|
||||
List<UpgradeTaskEntity> findPage(String taskType, String status, int offset, int limit);
|
||||
|
||||
long countPage(String targetComponent, String status);
|
||||
long countPage(String taskType, String status);
|
||||
|
||||
void updateStatus(
|
||||
String taskId,
|
||||
|
||||
+4
-4
@@ -33,13 +33,13 @@ public class UpgradeTaskRepositoryImpl implements UpgradeTaskRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UpgradeTaskEntity> findPage(String targetComponent, String status, int offset, int limit) {
|
||||
return upgradeTaskMapper.selectPage(targetComponent, status, offset, limit);
|
||||
public List<UpgradeTaskEntity> findPage(String taskType, String status, int offset, int limit) {
|
||||
return upgradeTaskMapper.selectPage(taskType, status, offset, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countPage(String targetComponent, String status) {
|
||||
return upgradeTaskMapper.countPage(targetComponent, status);
|
||||
public long countPage(String taskType, String status) {
|
||||
return upgradeTaskMapper.countPage(taskType, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.cisd.tms.modules.upgrade.service;
|
||||
|
||||
import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
||||
@@ -13,12 +14,8 @@ import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService;
|
||||
import com.cisd.tms.modules.upgrade.support.UpgradeVersionComparator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -30,6 +27,7 @@ public class UpgradePackageService {
|
||||
private final UpgradePackageStagingService upgradePackageStagingService;
|
||||
private final UpgradePackageSignatureVerifier signatureVerifier;
|
||||
private final UpgradeVersionComparator versionComparator;
|
||||
private final CisdPresetProperties cisdPresetProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public UpgradePackageService(
|
||||
@@ -38,6 +36,7 @@ public class UpgradePackageService {
|
||||
UpgradePackageStagingService upgradePackageStagingService,
|
||||
UpgradePackageSignatureVerifier signatureVerifier,
|
||||
UpgradeVersionComparator versionComparator,
|
||||
CisdPresetProperties cisdPresetProperties,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.fileRecordRepository = fileRecordRepository;
|
||||
@@ -45,32 +44,20 @@ public class UpgradePackageService {
|
||||
this.upgradePackageStagingService = upgradePackageStagingService;
|
||||
this.signatureVerifier = signatureVerifier;
|
||||
this.versionComparator = versionComparator;
|
||||
this.cisdPresetProperties = cisdPresetProperties;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public UpgradePreviewResponse preview(String fileId) {
|
||||
FileRecordEntity record = fileRecordRepository.findByFileId(normalize(fileId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + normalize(fileId)));
|
||||
Path packagePath = Path.of(record.getStoragePath()).normalize().toAbsolutePath();
|
||||
if (!Files.exists(packagePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found on disk");
|
||||
}
|
||||
StagedUpgradePackage stagedPackage = stagePackage(fileId);
|
||||
UpgradeManifest manifest = stagedPackage.manifest();
|
||||
validateManifest(manifest);
|
||||
validateProductType(manifest.getProductType());
|
||||
signatureVerifier.verify(manifest, stagedPackage.manifestPath(), stagedPackage.signaturePath());
|
||||
validateExecuteScript(stagedPackage.stagedDir(), manifest);
|
||||
|
||||
Path stagedDir = upgradePackageStagingService.stage(record.getFileId(), packagePath);
|
||||
Path manifestPath = stagedDir.resolve("manifest.json");
|
||||
Path payloadZipPath = stagedDir.resolve("payload.zip");
|
||||
Path signaturePath = stagedDir.resolve("signature.sig");
|
||||
|
||||
if (!Files.exists(manifestPath) || !Files.exists(payloadZipPath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package structure invalid");
|
||||
}
|
||||
|
||||
UpgradeManifest manifest = readManifest(manifestPath);
|
||||
verifyPayloadChecksum(manifest, payloadZipPath);
|
||||
signatureVerifier.verify(manifest, manifestPath, payloadZipPath, signaturePath);
|
||||
|
||||
String currentVersion = currentVersion(manifest.getTargetComponent());
|
||||
String targetVersion = normalize(manifest.getTargetVersion());
|
||||
String currentVersion = currentVersion(manifest.getTaskType());
|
||||
String targetVersion = normalize(manifest.getVersion());
|
||||
|
||||
if (!currentVersion.isEmpty() && versionComparator.compare(targetVersion, currentVersion) < 0) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "target version is lower than current version");
|
||||
@@ -81,10 +68,11 @@ public class UpgradePackageService {
|
||||
}
|
||||
|
||||
UpgradePreviewResponse response = new UpgradePreviewResponse();
|
||||
response.setTargetComponent(normalize(manifest.getTargetComponent()));
|
||||
response.setTaskType(normalize(manifest.getTaskType()));
|
||||
response.setProductType(normalize(manifest.getProductType()));
|
||||
response.setCurrentVersion(currentVersion);
|
||||
response.setTargetVersion(targetVersion);
|
||||
response.setVersionChangeSummary(buildVersionSummary(manifest.getTargetComponent(), currentVersion, targetVersion));
|
||||
response.setVersionChangeSummary(buildVersionSummary(currentVersion, targetVersion));
|
||||
response.setVerifyPassed(true);
|
||||
response.setVerifyMessage("校验通过");
|
||||
response.setAllowed(true);
|
||||
@@ -93,6 +81,10 @@ public class UpgradePackageService {
|
||||
return response;
|
||||
}
|
||||
|
||||
public UpgradeManifest loadManifest(String fileId) {
|
||||
return stagePackage(fileId).manifest();
|
||||
}
|
||||
|
||||
private UpgradeManifest readManifest(Path manifestPath) {
|
||||
try {
|
||||
return objectMapper.readValue(Files.readString(manifestPath), UpgradeManifest.class);
|
||||
@@ -101,30 +93,37 @@ public class UpgradePackageService {
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyPayloadChecksum(UpgradeManifest manifest, Path payloadZipPath) {
|
||||
String expected = normalize(manifest.getFileChecksums() == null ? "" : manifest.getFileChecksums().getPayloadZip());
|
||||
if (expected.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "payload checksum missing");
|
||||
private StagedUpgradePackage stagePackage(String fileId) {
|
||||
FileRecordEntity record = fileRecordRepository.findByFileId(normalize(fileId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + normalize(fileId)));
|
||||
Path packagePath = Path.of(record.getStoragePath()).normalize().toAbsolutePath();
|
||||
if (!Files.exists(packagePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found on disk");
|
||||
}
|
||||
String actual = sha256Hex(payloadZipPath);
|
||||
if (!expected.equalsIgnoreCase(actual)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "payload checksum mismatch");
|
||||
|
||||
Path stagedDir = upgradePackageStagingService.stage(record.getFileId(), packagePath);
|
||||
Path manifestPath = stagedDir.resolve("manifest.json");
|
||||
Path signaturePath = stagedDir.resolve("signature.sig");
|
||||
if (!Files.exists(manifestPath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package structure invalid");
|
||||
}
|
||||
return new StagedUpgradePackage(stagedDir, manifestPath, signaturePath, readManifest(manifestPath));
|
||||
}
|
||||
|
||||
private String sha256Hex(Path file) {
|
||||
try (InputStream inputStream = Files.newInputStream(file)) {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer)) >= 0) {
|
||||
if (read > 0) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
} catch (IOException | NoSuchAlgorithmException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to calculate payload checksum");
|
||||
private void validateManifest(UpgradeManifest manifest) {
|
||||
if (manifest == null) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json is invalid");
|
||||
}
|
||||
if (normalize(manifest.getPackageId()).isEmpty()
|
||||
|| normalize(manifest.getTaskType()).isEmpty()
|
||||
|| normalize(manifest.getProductType()).isEmpty()
|
||||
|| normalize(manifest.getVersion()).isEmpty()
|
||||
|| normalize(manifest.getMinCompatibleVersion()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json is invalid");
|
||||
}
|
||||
UpgradeManifest.Entrypoints entrypoints = manifest.getEntrypoints();
|
||||
if (entrypoints == null || normalize(entrypoints.getExecute()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade execute script not found");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,19 +132,40 @@ public class UpgradePackageService {
|
||||
return cached.map(DeviceSoftwareVersionEntity::getCurrentVersion).map(this::normalize).orElse("");
|
||||
}
|
||||
|
||||
private String buildVersionSummary(String componentCode, String currentVersion, String targetVersion) {
|
||||
String label = switch (normalize(componentCode)) {
|
||||
case "TMS" -> "设备管理软件";
|
||||
case "STANDARD_APP" -> "标准收发器";
|
||||
case "DIRECT_APP" -> "直参轻量化终端";
|
||||
default -> "目标组件";
|
||||
};
|
||||
private void validateProductType(String packageProductType) {
|
||||
String currentProductType = normalizeUpper(cisdPresetProperties.getProductType());
|
||||
String packageType = normalizeUpper(packageProductType);
|
||||
if (!currentProductType.isEmpty() && !packageType.equals(currentProductType)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package product type does not match current device");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateExecuteScript(Path stagedDir, UpgradeManifest manifest) {
|
||||
Path executePath = stagedDir.resolve(normalize(manifest.getEntrypoints().getExecute())).normalize().toAbsolutePath();
|
||||
if (!executePath.startsWith(stagedDir.normalize().toAbsolutePath()) || !Files.exists(executePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade execute script not found");
|
||||
}
|
||||
}
|
||||
|
||||
private String buildVersionSummary(String currentVersion, String targetVersion) {
|
||||
String from = currentVersion.isEmpty() ? "未记录版本" : currentVersion;
|
||||
String to = targetVersion.isEmpty() ? "未知目标版本" : targetVersion;
|
||||
return label + "将从 " + from + " 升级到 " + to;
|
||||
return "当前设备将从 " + from + " 升级到 " + to;
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private String normalizeUpper(String value) {
|
||||
return normalize(value).toUpperCase();
|
||||
}
|
||||
|
||||
private record StagedUpgradePackage(
|
||||
Path stagedDir,
|
||||
Path manifestPath,
|
||||
Path signaturePath,
|
||||
UpgradeManifest manifest
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskRequest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeLogResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradePreviewRequest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeTaskPageResponse;
|
||||
@@ -102,7 +103,8 @@ public class UpgradeService {
|
||||
UpgradeTaskEntity entity = new UpgradeTaskEntity();
|
||||
entity.setTaskId(buildTaskId());
|
||||
entity.setUpgradeMode("OFFLINE");
|
||||
entity.setTargetComponent(preview.getTargetComponent());
|
||||
entity.setTaskType(preview.getTaskType());
|
||||
entity.setProductType(preview.getProductType());
|
||||
entity.setPackageFileId(request.getFileId().trim());
|
||||
entity.setPackageName(fileRecord.getOriginalFilename());
|
||||
entity.setPackageHash("");
|
||||
@@ -120,7 +122,8 @@ public class UpgradeService {
|
||||
UpgradeCreateTaskResponse response = new UpgradeCreateTaskResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setTargetComponent(entity.getTargetComponent());
|
||||
response.setTaskType(entity.getTaskType());
|
||||
response.setProductType(entity.getProductType());
|
||||
response.setCurrentVersion(entity.getCurrentVersion());
|
||||
response.setTargetVersion(entity.getTargetVersion());
|
||||
return response;
|
||||
@@ -150,12 +153,37 @@ public class UpgradeService {
|
||||
return toResponse(loadTask(taskId));
|
||||
}
|
||||
|
||||
public UpgradeTaskPageResponse listTasks(int page, int pageSize, String targetComponent, String status) {
|
||||
public UpgradeTaskResponse rollbackTask(String taskId) {
|
||||
UpgradeTaskEntity task = loadTask(taskId);
|
||||
UpgradeManifest manifest = upgradePackageService.loadManifest(task.getPackageFileId());
|
||||
if (manifest.getEntrypoints() == null || trim(manifest.getEntrypoints().getRollback()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade rollback script not found");
|
||||
}
|
||||
UpgradeTaskEntity runningTask = upgradeTaskRepository.findRunningTask().orElse(null);
|
||||
if (runningTask != null && !runningTask.getTaskId().equals(taskId)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "another upgrade task is running");
|
||||
}
|
||||
if (activeExecutions.containsKey(taskId)) {
|
||||
return toResponse(loadTask(taskId));
|
||||
}
|
||||
upgradeTaskRepository.updateStatus(taskId, "ROLLING_BACK", "", task.getDetailLogPath(), LocalDateTime.now(), null);
|
||||
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
upgradeTaskRunner.rollback(loadTask(taskId));
|
||||
} finally {
|
||||
activeExecutions.remove(taskId);
|
||||
}
|
||||
}, backgroundExecutor);
|
||||
activeExecutions.put(taskId, future);
|
||||
return toResponse(loadTask(taskId));
|
||||
}
|
||||
|
||||
public UpgradeTaskPageResponse listTasks(int page, int pageSize, String taskType, String status) {
|
||||
int normalizedPage = Math.max(1, page);
|
||||
int normalizedPageSize = Math.max(1, pageSize);
|
||||
int offset = (normalizedPage - 1) * normalizedPageSize;
|
||||
List<UpgradeTaskEntity> entities = upgradeTaskRepository.findPage(trim(targetComponent), trim(status), offset, normalizedPageSize);
|
||||
long total = upgradeTaskRepository.countPage(trim(targetComponent), trim(status));
|
||||
List<UpgradeTaskEntity> entities = upgradeTaskRepository.findPage(trim(taskType), trim(status), offset, normalizedPageSize);
|
||||
long total = upgradeTaskRepository.countPage(trim(taskType), trim(status));
|
||||
|
||||
UpgradeTaskPageResponse response = new UpgradeTaskPageResponse();
|
||||
response.setPage(normalizedPage);
|
||||
@@ -203,7 +231,8 @@ public class UpgradeService {
|
||||
UpgradeTaskResponse response = new UpgradeTaskResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setUpgradeMode(entity.getUpgradeMode());
|
||||
response.setTargetComponent(entity.getTargetComponent());
|
||||
response.setTaskType(entity.getTaskType());
|
||||
response.setProductType(entity.getProductType());
|
||||
response.setPackageFileId(entity.getPackageFileId());
|
||||
response.setPackageName(entity.getPackageName());
|
||||
response.setCurrentVersion(entity.getCurrentVersion());
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import java.nio.file.Path;
|
||||
public class NoopUpgradePackageSignatureVerifier implements UpgradePackageSignatureVerifier {
|
||||
|
||||
@Override
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) {
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath) {
|
||||
if (!Files.exists(signaturePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found");
|
||||
}
|
||||
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
package com.cisd.tms.modules.upgrade.support;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PcieUpgradePackageSignatureVerifier implements UpgradePackageSignatureVerifier {
|
||||
|
||||
private final UpgradeProperties upgradeProperties;
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
|
||||
public PcieUpgradePackageSignatureVerifier(UpgradeProperties upgradeProperties, PcieCryptoService pcieCryptoService) {
|
||||
this.upgradeProperties = upgradeProperties;
|
||||
this.pcieCryptoService = pcieCryptoService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) {
|
||||
if (!Files.exists(signaturePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found");
|
||||
}
|
||||
|
||||
byte[] publicKeyBlob = decodeConfiguredPublicKey();
|
||||
byte[] signature = loadSignature(signaturePath);
|
||||
byte[] manifestBytes = readBytes(manifestPath, "failed to read manifest for signature verification");
|
||||
|
||||
EccExternalVerifyRequest request = new EccExternalVerifyRequest();
|
||||
request.setAlgId(Gm0018AlgorithmIds.SM2_SIGN);
|
||||
request.setPublicKeyBlob(publicKeyBlob);
|
||||
request.setData(manifestBytes);
|
||||
request.setSignature(signature);
|
||||
|
||||
try {
|
||||
pcieCryptoService.eccExternalVerify(request);
|
||||
} catch (BizException ex) {
|
||||
throw ex;
|
||||
} catch (RuntimeException ex) {
|
||||
throw new BizException(ErrorCode.CRYPTO_CARD_ERROR.getCode(), "upgrade package signature verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decodeConfiguredPublicKey() {
|
||||
String configured = normalize(upgradeProperties.getSignaturePublicKeyBase64());
|
||||
if (configured.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key not configured");
|
||||
}
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(configured);
|
||||
if (decoded.length == 0) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key is invalid");
|
||||
}
|
||||
return decoded;
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] loadSignature(Path signaturePath) {
|
||||
byte[] raw = readBytes(signaturePath, "failed to read upgrade signature file");
|
||||
if (raw.length == 0) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file is empty");
|
||||
}
|
||||
|
||||
String text = new String(raw, StandardCharsets.UTF_8).trim();
|
||||
if (!text.isEmpty()) {
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(text);
|
||||
if (decoded.length > 0) {
|
||||
return decoded;
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Keep backward compatibility with raw binary signature files.
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
private byte[] readBytes(Path path, String failureMessage) {
|
||||
try {
|
||||
return Files.readAllBytes(path);
|
||||
} catch (IOException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), failureMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package com.cisd.tms.modules.upgrade.support;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.util.Base64;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SoftUpgradePackageSignatureVerifier implements UpgradePackageSignatureVerifier {
|
||||
|
||||
private static final String PROVIDER = "BC";
|
||||
private static final String SIGNATURE_ALGORITHM = "SM3withSM2";
|
||||
|
||||
static {
|
||||
if (Security.getProvider(PROVIDER) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
}
|
||||
|
||||
private final UpgradeProperties upgradeProperties;
|
||||
|
||||
public SoftUpgradePackageSignatureVerifier(UpgradeProperties upgradeProperties) {
|
||||
this.upgradeProperties = upgradeProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath) {
|
||||
if (!Files.exists(signaturePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found");
|
||||
}
|
||||
|
||||
PublicKey publicKey = loadPublicKey();
|
||||
byte[] manifestBytes = readBytes(manifestPath, "failed to read manifest for signature verification");
|
||||
byte[] signatureBytes = loadSignature(signaturePath);
|
||||
|
||||
try {
|
||||
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, PROVIDER);
|
||||
signature.initVerify(publicKey);
|
||||
signature.update(manifestBytes);
|
||||
if (!signature.verify(signatureBytes)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature verification failed");
|
||||
}
|
||||
} catch (BizException ex) {
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
private PublicKey loadPublicKey() {
|
||||
Path pemPath = Path.of(normalize(upgradeProperties.getSignaturePublicKeyPemPath())).normalize().toAbsolutePath();
|
||||
if (normalize(upgradeProperties.getSignaturePublicKeyPemPath()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem path not configured");
|
||||
}
|
||||
if (!Files.exists(pemPath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem file not found");
|
||||
}
|
||||
try {
|
||||
String pemContent = Files.readString(pemPath, StandardCharsets.UTF_8);
|
||||
try (Reader reader = new StringReader(pemContent); PEMParser parser = new PEMParser(reader)) {
|
||||
Object parsed = parser.readObject();
|
||||
if (!(parsed instanceof SubjectPublicKeyInfo publicKeyInfo)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem is invalid");
|
||||
}
|
||||
return new JcaPEMKeyConverter().setProvider(PROVIDER).getPublicKey(publicKeyInfo);
|
||||
}
|
||||
} catch (BizException ex) {
|
||||
throw ex;
|
||||
} catch (IOException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to read upgrade signature public key pem");
|
||||
} catch (Exception ex) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] loadSignature(Path signaturePath) {
|
||||
byte[] raw = readBytes(signaturePath, "failed to read upgrade signature file");
|
||||
if (raw.length == 0) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file is empty");
|
||||
}
|
||||
|
||||
String text = new String(raw, StandardCharsets.UTF_8).trim();
|
||||
if (!text.isEmpty()) {
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(text);
|
||||
if (decoded.length > 0) {
|
||||
return decoded;
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Keep compatibility with raw binary signature files.
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
private byte[] readBytes(Path path, String failureMessage) {
|
||||
try {
|
||||
return Files.readAllBytes(path);
|
||||
} catch (IOException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), failureMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -5,5 +5,9 @@ import java.nio.file.Path;
|
||||
|
||||
public interface UpgradePackageSignatureVerifier {
|
||||
|
||||
void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath);
|
||||
void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath);
|
||||
|
||||
default void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) {
|
||||
verify(manifest, manifestPath, signaturePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,8 +172,8 @@ tms:
|
||||
staging-root-dir: ${TMS_UPGRADE_STAGING_ROOT_DIR:/home/tmp/tms-upgrade-staging}
|
||||
# 升级任务统一日志目录。
|
||||
log-dir: ${TMS_UPGRADE_LOG_DIR:/home/tms/tmp/tms-upgrade-logs}
|
||||
# 升级包验签使用的“固件完整性密钥”公钥 blob(Base64 编码);为空时升级预检会拒绝通过。
|
||||
signature-public-key-base64: ${TMS_UPGRADE_SIGNATURE_PUBLIC_KEY_BASE64:}
|
||||
# 升级包验签使用的公钥 PEM 文件路径;为空时升级预检会拒绝通过。
|
||||
signature-public-key-pem-path: ${TMS_UPGRADE_SIGNATURE_PUBLIC_KEY_PEM_PATH:}
|
||||
mk:
|
||||
init-identify:
|
||||
# SDFE_InitIdentify 旧 PIN(Base64 编码);主密钥初始化接口会读取该值。
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE t_upgrade_task
|
||||
ADD COLUMN product_type VARCHAR(16) NULL AFTER target_component;
|
||||
|
||||
UPDATE t_upgrade_task
|
||||
SET product_type = 'ENTERPRISE'
|
||||
WHERE product_type IS NULL OR product_type = '';
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE t_upgrade_task
|
||||
CHANGE COLUMN target_component task_type VARCHAR(32) NOT NULL;
|
||||
|
||||
ALTER TABLE t_upgrade_task
|
||||
DROP INDEX idx_t_upgrade_task_component,
|
||||
ADD INDEX idx_t_upgrade_task_task_type (task_type);
|
||||
@@ -8,7 +8,8 @@
|
||||
<id property="id" column="id"/>
|
||||
<result property="taskId" column="task_id"/>
|
||||
<result property="upgradeMode" column="upgrade_mode"/>
|
||||
<result property="targetComponent" column="target_component"/>
|
||||
<result property="taskType" column="task_type"/>
|
||||
<result property="productType" column="product_type"/>
|
||||
<result property="packageFileId" column="package_file_id"/>
|
||||
<result property="packageName" column="package_name"/>
|
||||
<result property="packageHash" column="package_hash"/>
|
||||
@@ -28,7 +29,7 @@
|
||||
</resultMap>
|
||||
|
||||
<sql id="UpgradeTaskColumns">
|
||||
id, task_id, upgrade_mode, target_component, package_file_id, package_name, package_hash,
|
||||
id, task_id, upgrade_mode, task_type, product_type, package_file_id, package_name, package_hash,
|
||||
current_version, target_version, status, verify_passed, verify_message, manifest_json,
|
||||
detail_log_path, error_message, operator, started_at, finished_at, create_time, update_time
|
||||
</sql>
|
||||
@@ -52,8 +53,8 @@
|
||||
SELECT <include refid="UpgradeTaskColumns"/>
|
||||
FROM t_upgrade_task
|
||||
<where>
|
||||
<if test="targetComponent != null and targetComponent != ''">
|
||||
AND target_component = #{targetComponent}
|
||||
<if test="taskType != null and taskType != ''">
|
||||
AND task_type = #{taskType}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
AND status = #{status}
|
||||
@@ -67,8 +68,8 @@
|
||||
SELECT COUNT(1)
|
||||
FROM t_upgrade_task
|
||||
<where>
|
||||
<if test="targetComponent != null and targetComponent != ''">
|
||||
AND target_component = #{targetComponent}
|
||||
<if test="taskType != null and taskType != ''">
|
||||
AND task_type = #{taskType}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
AND status = #{status}
|
||||
|
||||
@@ -15,21 +15,23 @@ class UpgradeDtoContractTest {
|
||||
@Test
|
||||
void shouldSerializeUpgradePreviewFields() throws Exception {
|
||||
UpgradePreviewResponse response = new UpgradePreviewResponse();
|
||||
response.setTargetComponent("STANDARD_APP");
|
||||
response.setTaskType("RECEIVER");
|
||||
response.setProductType("DIRECT");
|
||||
response.setCurrentVersion("V1.3.1");
|
||||
response.setTargetVersion("V1.3.2");
|
||||
response.setVersionChangeSummary("标准收发器企业版将从 V1.3.1 升级到 V1.3.2");
|
||||
response.setTargetVersion("V1.4.0");
|
||||
response.setVersionChangeSummary("当前设备将从 V1.3.1 升级到 V1.4.0");
|
||||
response.setVerifyPassed(true);
|
||||
response.setVerifyMessage("校验通过");
|
||||
response.setAllowed(true);
|
||||
response.setBlockReason("");
|
||||
response.setDescription("修复标准收发器已知问题");
|
||||
response.setDescription("receiver upgrade package");
|
||||
|
||||
String json = objectMapper.writeValueAsString(response);
|
||||
|
||||
Assertions.assertTrue(json.contains("\"targetComponent\":\"STANDARD_APP\""));
|
||||
Assertions.assertTrue(json.contains("\"taskType\":\"RECEIVER\""));
|
||||
Assertions.assertTrue(json.contains("\"productType\":\"DIRECT\""));
|
||||
Assertions.assertTrue(json.contains("\"currentVersion\":\"V1.3.1\""));
|
||||
Assertions.assertTrue(json.contains("\"targetVersion\":\"V1.3.2\""));
|
||||
Assertions.assertTrue(json.contains("\"targetVersion\":\"V1.4.0\""));
|
||||
Assertions.assertTrue(json.contains("\"versionChangeSummary\""));
|
||||
Assertions.assertTrue(json.contains("\"verifyPassed\":true"));
|
||||
Assertions.assertTrue(json.contains("\"allowed\":true"));
|
||||
@@ -46,8 +48,8 @@ class UpgradeDtoContractTest {
|
||||
@Test
|
||||
void shouldExposeExpectedUpgradeEnums() {
|
||||
Assertions.assertEquals("TMS", UpgradeTargetComponent.TMS.name());
|
||||
Assertions.assertEquals("STANDARD_APP", UpgradeTargetComponent.STANDARD_APP.name());
|
||||
Assertions.assertEquals("DIRECT_APP", UpgradeTargetComponent.DIRECT_APP.name());
|
||||
Assertions.assertEquals("RECEIVER", UpgradeTargetComponent.RECEIVER.name());
|
||||
Assertions.assertEquals("FIRMWARE", UpgradeTargetComponent.FIRMWARE.name());
|
||||
Assertions.assertEquals("PENDING_CONFIRM", UpgradeTaskStatus.PENDING_CONFIRM.name());
|
||||
Assertions.assertEquals("RUNNING", UpgradeTaskStatus.RUNNING.name());
|
||||
Assertions.assertEquals("SUCCESS", UpgradeTaskStatus.SUCCESS.name());
|
||||
|
||||
@@ -46,7 +46,8 @@ class UpgradeControllerTest {
|
||||
FileService fileService = Mockito.mock(FileService.class);
|
||||
|
||||
UpgradePreviewResponse preview = new UpgradePreviewResponse();
|
||||
preview.setTargetComponent("STANDARD_APP");
|
||||
preview.setTaskType("RECEIVER");
|
||||
preview.setProductType("DIRECT");
|
||||
preview.setCurrentVersion("V1.3.1");
|
||||
preview.setTargetVersion("V1.3.2");
|
||||
preview.setAllowed(true);
|
||||
@@ -56,7 +57,8 @@ class UpgradeControllerTest {
|
||||
UpgradeCreateTaskResponse create = new UpgradeCreateTaskResponse();
|
||||
create.setTaskId("UPG-001");
|
||||
create.setStatus("PENDING_CONFIRM");
|
||||
create.setTargetComponent("STANDARD_APP");
|
||||
create.setTaskType("RECEIVER");
|
||||
create.setProductType("DIRECT");
|
||||
create.setCurrentVersion("V1.3.1");
|
||||
create.setTargetVersion("V1.3.2");
|
||||
Mockito.when(upgradeService.createTask(Mockito.any())).thenReturn(create);
|
||||
@@ -64,7 +66,10 @@ class UpgradeControllerTest {
|
||||
UpgradeTaskResponse task = new UpgradeTaskResponse();
|
||||
task.setTaskId("UPG-001");
|
||||
task.setStatus("RUNNING");
|
||||
task.setTaskType("RECEIVER");
|
||||
task.setProductType("DIRECT");
|
||||
Mockito.when(upgradeService.executeTask("UPG-001")).thenReturn(task);
|
||||
Mockito.when(upgradeService.rollbackTask("UPG-001")).thenReturn(task);
|
||||
Mockito.when(upgradeService.getTask("UPG-001")).thenReturn(task);
|
||||
|
||||
UpgradeTaskPageResponse page = new UpgradeTaskPageResponse();
|
||||
@@ -80,7 +85,8 @@ class UpgradeControllerTest {
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"fileId\":\"FILE-001\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"targetComponent\":\"STANDARD_APP\"")));
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"taskType\":\"RECEIVER\"")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"productType\":\"DIRECT\"")));
|
||||
|
||||
mockMvc.perform(post("/api/v1/upgrades")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
@@ -92,10 +98,14 @@ class UpgradeControllerTest {
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"status\":\"RUNNING\"")));
|
||||
|
||||
mockMvc.perform(post("/api/v1/upgrades/UPG-001/rollback"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"status\":\"RUNNING\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/upgrades")
|
||||
.param("page", "1")
|
||||
.param("pageSize", "10")
|
||||
.param("targetComponent", "")
|
||||
.param("taskType", "")
|
||||
.param("status", ""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"total\":1")));
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
package com.cisd.tms.modules.upgrade.executor;
|
||||
|
||||
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
||||
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import com.cisd.tms.modules.upgrade.entity.UpgradeTaskEntity;
|
||||
import com.cisd.tms.modules.upgrade.repository.UpgradeTaskRepository;
|
||||
import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class UpgradeTaskRunnerTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void shouldRunPrecheckExecuteAndVerifyScriptsInOrder() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-runner-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, true, "#!/bin/sh\necho execute\nexit 0\n", true);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
InMemoryUpgradeTaskRepository taskRepository = new InMemoryUpgradeTaskRepository();
|
||||
InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository();
|
||||
|
||||
UpgradeTaskRunner runner = new UpgradeTaskRunner(
|
||||
taskRepository,
|
||||
fileRepository,
|
||||
versionRepository,
|
||||
stagingService(tempDir.resolve("staging"), tempDir.resolve("logs")),
|
||||
properties(tempDir.resolve("staging"), tempDir.resolve("logs")),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
runner.run(task("UPG-001", fileRecord.getFileId(), "TMS", "V1.0.1"));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", taskRepository.status);
|
||||
Assertions.assertEquals("V1.0.1", versionRepository.store.get("TMS").getCurrentVersion());
|
||||
String log = Files.readString(Path.of(taskRepository.logPath));
|
||||
Assertions.assertTrue(log.contains("precheck"));
|
||||
Assertions.assertTrue(log.contains("execute"));
|
||||
Assertions.assertTrue(log.contains("verify"));
|
||||
Assertions.assertTrue(log.indexOf("precheck") < log.indexOf("execute"));
|
||||
Assertions.assertTrue(log.indexOf("execute") < log.indexOf("verify"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMarkTaskFailedWhenExecuteScriptFails() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-runner-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, false, "#!/bin/sh\necho execute\nexit 2\n", false);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
InMemoryUpgradeTaskRepository taskRepository = new InMemoryUpgradeTaskRepository();
|
||||
|
||||
UpgradeTaskRunner runner = new UpgradeTaskRunner(
|
||||
taskRepository,
|
||||
fileRepository,
|
||||
new InMemoryDeviceSoftwareVersionRepository(),
|
||||
stagingService(tempDir.resolve("staging"), tempDir.resolve("logs")),
|
||||
properties(tempDir.resolve("staging"), tempDir.resolve("logs")),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
runner.run(task("UPG-002", fileRecord.getFileId(), "RECEIVER", "V1.3.2"));
|
||||
|
||||
Assertions.assertEquals("FAILED", taskRepository.status);
|
||||
Assertions.assertTrue(taskRepository.errorMessage.contains("execute"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunRollbackScriptWhenRequested() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-runner-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, false, "#!/bin/sh\necho execute\nexit 0\n", true);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
InMemoryUpgradeTaskRepository taskRepository = new InMemoryUpgradeTaskRepository();
|
||||
|
||||
UpgradeTaskRunner runner = new UpgradeTaskRunner(
|
||||
taskRepository,
|
||||
fileRepository,
|
||||
new InMemoryDeviceSoftwareVersionRepository(),
|
||||
stagingService(tempDir.resolve("staging"), tempDir.resolve("logs")),
|
||||
properties(tempDir.resolve("staging"), tempDir.resolve("logs")),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
runner.rollback(task("UPG-003", fileRecord.getFileId(), "FIRMWARE", "V3.0.0"));
|
||||
|
||||
Assertions.assertEquals("ROLLBACK_SUCCESS", taskRepository.status);
|
||||
String log = Files.readString(Path.of(taskRepository.logPath));
|
||||
Assertions.assertTrue(log.contains("rollback"));
|
||||
}
|
||||
|
||||
private UpgradeProperties properties(Path stagingRoot, Path logDir) {
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setStagingRootDir(stagingRoot.toString());
|
||||
properties.setLogDir(logDir.toString());
|
||||
return properties;
|
||||
}
|
||||
|
||||
private UpgradePackageStagingService stagingService(Path stagingRoot, Path logDir) {
|
||||
try {
|
||||
Files.createDirectories(logDir);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
return new UpgradePackageStagingService(properties(stagingRoot, logDir));
|
||||
}
|
||||
|
||||
private FileRecordEntity buildPackage(Path tempDir, boolean includeOptionalScripts, String executeScript, boolean includeRollback)
|
||||
throws Exception {
|
||||
Path packagePath = tempDir.resolve("upgrade-package.zip");
|
||||
|
||||
UpgradeManifest manifest = new UpgradeManifest();
|
||||
manifest.setPackageId("PKG-001");
|
||||
manifest.setTaskType("TMS");
|
||||
manifest.setProductType("ENTERPRISE");
|
||||
manifest.setVersion("V1.0.1");
|
||||
manifest.setMinCompatibleVersion("V1.0.0");
|
||||
UpgradeManifest.Entrypoints entrypoints = new UpgradeManifest.Entrypoints();
|
||||
entrypoints.setExecute("payload/scripts/execute.sh");
|
||||
if (includeOptionalScripts) {
|
||||
entrypoints.setPrecheck("payload/scripts/precheck.sh");
|
||||
entrypoints.setVerify("payload/scripts/verify.sh");
|
||||
}
|
||||
if (includeRollback) {
|
||||
entrypoints.setRollback("payload/scripts/rollback.sh");
|
||||
}
|
||||
manifest.setEntrypoints(entrypoints);
|
||||
|
||||
try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(packagePath))) {
|
||||
outputStream.putNextEntry(new ZipEntry("manifest.json"));
|
||||
outputStream.write(objectMapper.writeValueAsBytes(manifest));
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("signature.sig"));
|
||||
outputStream.write("signature".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload/"));
|
||||
outputStream.closeEntry();
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/"));
|
||||
outputStream.closeEntry();
|
||||
|
||||
if (includeOptionalScripts) {
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/precheck.sh"));
|
||||
outputStream.write("#!/bin/sh\necho precheck\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/verify.sh"));
|
||||
outputStream.write("#!/bin/sh\necho verify\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/execute.sh"));
|
||||
outputStream.write(executeScript.getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
|
||||
if (includeRollback) {
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/rollback.sh"));
|
||||
outputStream.write("#!/bin/sh\necho rollback\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
FileRecordEntity entity = new FileRecordEntity();
|
||||
entity.setFileId("FILE-001");
|
||||
entity.setOriginalFilename("upgrade-package.zip");
|
||||
entity.setStoragePath(packagePath.toString());
|
||||
entity.setStatus("ACTIVE");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private UpgradeTaskEntity task(String taskId, String fileId, String taskType, String targetVersion) {
|
||||
UpgradeTaskEntity entity = new UpgradeTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setPackageFileId(fileId);
|
||||
entity.setTaskType(taskType);
|
||||
entity.setTargetVersion(targetVersion);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static class InMemoryUpgradeTaskRepository implements UpgradeTaskRepository {
|
||||
private String status;
|
||||
private String errorMessage;
|
||||
private String logPath;
|
||||
|
||||
@Override
|
||||
public void save(UpgradeTaskEntity entity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UpgradeTaskEntity> findByTaskId(String taskId) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UpgradeTaskEntity> findRunningTask() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<UpgradeTaskEntity> findPage(String taskType, String status, int offset, int limit) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countPage(String taskType, String status) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(String taskId, String status, String errorMessage, String detailLogPath, LocalDateTime startedAt, LocalDateTime finishedAt) {
|
||||
this.status = status;
|
||||
this.errorMessage = errorMessage;
|
||||
this.logPath = detailLogPath;
|
||||
}
|
||||
}
|
||||
|
||||
private static class InMemoryFileRecordRepository implements FileRecordRepository {
|
||||
private final Map<String, FileRecordEntity> store = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Optional<FileRecordEntity> findByFileId(String fileId) {
|
||||
return Optional.ofNullable(store.get(fileId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(FileRecordEntity entity) {
|
||||
store.put(entity.getFileId(), entity);
|
||||
}
|
||||
}
|
||||
|
||||
private static class InMemoryDeviceSoftwareVersionRepository implements DeviceSoftwareVersionRepository {
|
||||
private final Map<String, DeviceSoftwareVersionEntity> store = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Optional<DeviceSoftwareVersionEntity> findByComponentCode(String componentCode) {
|
||||
return Optional.ofNullable(store.get(componentCode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveOrUpdate(DeviceSoftwareVersionEntity entity) {
|
||||
store.put(entity.getComponentCode(), entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -25,7 +25,8 @@ class UpgradeTaskRepositoryTest {
|
||||
UpgradeTaskEntity found = repository.findByTaskId("UPG-001").orElseThrow();
|
||||
|
||||
Assertions.assertEquals("UPG-001", found.getTaskId());
|
||||
Assertions.assertEquals("STANDARD_APP", found.getTargetComponent());
|
||||
Assertions.assertEquals("RECEIVER", found.getTaskType());
|
||||
Assertions.assertEquals("DIRECT", found.getProductType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -88,7 +89,8 @@ class UpgradeTaskRepositoryTest {
|
||||
UpgradeTaskEntity entity = new UpgradeTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setUpgradeMode("OFFLINE");
|
||||
entity.setTargetComponent("STANDARD_APP");
|
||||
entity.setTaskType("RECEIVER");
|
||||
entity.setProductType("DIRECT");
|
||||
entity.setPackageFileId("FILE-001");
|
||||
entity.setPackageName("upgrade-package.zip");
|
||||
entity.setPackageHash("abc123");
|
||||
@@ -97,7 +99,7 @@ class UpgradeTaskRepositoryTest {
|
||||
entity.setStatus(status);
|
||||
entity.setVerifyPassed(Boolean.TRUE);
|
||||
entity.setVerifyMessage("ok");
|
||||
entity.setManifestJson("{\"targetComponent\":\"STANDARD_APP\"}");
|
||||
entity.setManifestJson("{\"taskType\":\"RECEIVER\",\"productType\":\"DIRECT\"}");
|
||||
entity.setOperator("admin");
|
||||
return entity;
|
||||
}
|
||||
|
||||
+188
-46
@@ -2,6 +2,7 @@ package com.cisd.tms.modules.upgrade.service;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
||||
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
@@ -9,37 +10,51 @@ import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse;
|
||||
import com.cisd.tms.modules.upgrade.support.SoftUpgradePackageSignatureVerifier;
|
||||
import com.cisd.tms.modules.upgrade.support.UpgradePackageSignatureVerifier;
|
||||
import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService;
|
||||
import com.cisd.tms.modules.upgrade.support.UpgradeVersionComparator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.StringWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.util.HashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.util.io.pem.PemObject;
|
||||
import org.bouncycastle.util.io.pem.PemWriter;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class UpgradePackageServiceTest {
|
||||
|
||||
private static final String PROVIDER = "BC";
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
if (Security.getProvider(PROVIDER) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreviewUpgradePackageWithVersionChange() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "STANDARD_APP", "V1.3.2", "V1.3.0", "修复标准收发器问题");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "RECEIVER", "DIRECT", "V1.4.0", "V1.3.0", true);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository();
|
||||
versionRepository.save(component("STANDARD_APP", "标准收发器企业版", "V1.3.1"));
|
||||
versionRepository.save(component("RECEIVER", "标准收发器", "V1.3.1"));
|
||||
|
||||
UpgradePackageService service = new UpgradePackageService(
|
||||
fileRepository,
|
||||
@@ -47,47 +62,48 @@ class UpgradePackageServiceTest {
|
||||
stagingService(tempDir.resolve("staging")),
|
||||
new PassThroughSignatureVerifier(),
|
||||
new UpgradeVersionComparator(),
|
||||
preset("DIRECT"),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
UpgradePreviewResponse response = service.preview(fileRecord.getFileId());
|
||||
|
||||
Assertions.assertEquals("STANDARD_APP", response.getTargetComponent());
|
||||
Assertions.assertEquals("RECEIVER", response.getTaskType());
|
||||
Assertions.assertEquals("DIRECT", response.getProductType());
|
||||
Assertions.assertEquals("V1.3.1", response.getCurrentVersion());
|
||||
Assertions.assertEquals("V1.3.2", response.getTargetVersion());
|
||||
Assertions.assertEquals("V1.4.0", response.getTargetVersion());
|
||||
Assertions.assertTrue(response.isVerifyPassed());
|
||||
Assertions.assertTrue(response.isAllowed());
|
||||
Assertions.assertTrue(response.getVersionChangeSummary().contains("V1.3.1"));
|
||||
Assertions.assertTrue(response.getVersionChangeSummary().contains("V1.3.2"));
|
||||
Assertions.assertTrue(response.getVersionChangeSummary().contains("V1.4.0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPayloadChecksumMismatch() throws Exception {
|
||||
void shouldRejectWhenProductTypeDoesNotMatchCurrentDevice() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "TMS", "V1.0.1", "V1.0.0", "修复 TMS 问题");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "RECEIVER", "DIRECT", "V1.4.0", "V1.3.0", true);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
|
||||
tamperManifestChecksum(Path.of(fileRecord.getStoragePath()));
|
||||
|
||||
UpgradePackageService service = new UpgradePackageService(
|
||||
fileRepository,
|
||||
new InMemoryDeviceSoftwareVersionRepository(),
|
||||
stagingService(tempDir.resolve("staging")),
|
||||
new PassThroughSignatureVerifier(),
|
||||
new UpgradeVersionComparator(),
|
||||
preset("ENTERPRISE"),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class, () -> service.preview(fileRecord.getFileId()));
|
||||
Assertions.assertEquals(ErrorCode.BIZ_ERROR.getCode(), exception.getCode());
|
||||
Assertions.assertEquals("payload checksum mismatch", exception.getMessage());
|
||||
Assertions.assertEquals("upgrade package product type does not match current device", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectDowngrade() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "TMS", "V1.0.0", "V0.9.0", "尝试降级");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "TMS", "ENTERPRISE", "V1.0.0", "V0.9.0", true);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository();
|
||||
@@ -99,6 +115,7 @@ class UpgradePackageServiceTest {
|
||||
stagingService(tempDir.resolve("staging")),
|
||||
new PassThroughSignatureVerifier(),
|
||||
new UpgradeVersionComparator(),
|
||||
preset("ENTERPRISE"),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
@@ -109,11 +126,11 @@ class UpgradePackageServiceTest {
|
||||
@Test
|
||||
void shouldRejectWhenCurrentVersionIsLowerThanMinCompatibleVersion() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "DIRECT_APP", "V2.0.0", "V1.5.0", "需要更高起始版本");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "RECEIVER", "DIRECT", "V2.0.0", "V1.5.0", true);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository();
|
||||
versionRepository.save(component("DIRECT_APP", "直参轻量化终端", "V1.4.9"));
|
||||
versionRepository.save(component("RECEIVER", "标准收发器", "V1.4.9"));
|
||||
|
||||
UpgradePackageService service = new UpgradePackageService(
|
||||
fileRepository,
|
||||
@@ -121,6 +138,7 @@ class UpgradePackageServiceTest {
|
||||
stagingService(tempDir.resolve("staging")),
|
||||
new PassThroughSignatureVerifier(),
|
||||
new UpgradeVersionComparator(),
|
||||
preset("DIRECT"),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
@@ -128,31 +146,96 @@ class UpgradePackageServiceTest {
|
||||
Assertions.assertEquals("current version does not satisfy minimum compatible version", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWhenExecuteScriptIsMissing() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "FIRMWARE", "DIRECT", "V3.0.0", "V2.0.0", false);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
|
||||
UpgradePackageService service = new UpgradePackageService(
|
||||
fileRepository,
|
||||
new InMemoryDeviceSoftwareVersionRepository(),
|
||||
stagingService(tempDir.resolve("staging")),
|
||||
new PassThroughSignatureVerifier(),
|
||||
new UpgradeVersionComparator(),
|
||||
preset("DIRECT"),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class, () -> service.preview(fileRecord.getFileId()));
|
||||
Assertions.assertEquals("upgrade execute script not found", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreviewUpgradePackageWithSoftSignatureVerification() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||
KeyPair keyPair = generateSm2KeyPair();
|
||||
Path publicKeyPath = tempDir.resolve("upgrade-public-key.pem");
|
||||
Files.writeString(publicKeyPath, toPem("PUBLIC KEY", keyPair.getPublic().getEncoded()), StandardCharsets.UTF_8);
|
||||
|
||||
FileRecordEntity fileRecord = buildSignedPackage(
|
||||
tempDir,
|
||||
"TMS",
|
||||
"ENTERPRISE",
|
||||
"V1.1.0",
|
||||
"V1.0.0",
|
||||
keyPair
|
||||
);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setSignaturePublicKeyPemPath(publicKeyPath.toString());
|
||||
|
||||
UpgradePackageService service = new UpgradePackageService(
|
||||
fileRepository,
|
||||
new InMemoryDeviceSoftwareVersionRepository(),
|
||||
stagingService(tempDir.resolve("staging")),
|
||||
new SoftUpgradePackageSignatureVerifier(properties),
|
||||
new UpgradeVersionComparator(),
|
||||
preset("ENTERPRISE"),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
UpgradePreviewResponse response = service.preview(fileRecord.getFileId());
|
||||
|
||||
Assertions.assertTrue(response.isVerifyPassed());
|
||||
Assertions.assertEquals("TMS", response.getTaskType());
|
||||
}
|
||||
|
||||
private UpgradePackageStagingService stagingService(Path stagingRoot) {
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setStagingRootDir(stagingRoot.toString());
|
||||
return new UpgradePackageStagingService(properties);
|
||||
}
|
||||
|
||||
private FileRecordEntity buildPackage(Path tempDir, String targetComponent, String targetVersion, String minCompatibleVersion, String description)
|
||||
throws IOException, NoSuchAlgorithmException {
|
||||
private CisdPresetProperties preset(String productType) {
|
||||
CisdPresetProperties properties = new CisdPresetProperties();
|
||||
properties.setProductType(productType);
|
||||
return properties;
|
||||
}
|
||||
|
||||
private FileRecordEntity buildPackage(Path tempDir, String taskType, String productType, String version, String minCompatibleVersion, boolean includeExecute)
|
||||
throws IOException {
|
||||
Path packagePath = tempDir.resolve("upgrade-package.zip");
|
||||
Path payloadPath = tempDir.resolve("payload.zip");
|
||||
Files.write(payloadPath, "payload-content".getBytes(StandardCharsets.UTF_8));
|
||||
byte[] signatureBytes = "signature".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
UpgradeManifest manifest = new UpgradeManifest();
|
||||
manifest.setPackageId("PKG-001");
|
||||
manifest.setTargetComponent(targetComponent);
|
||||
manifest.setTargetVersion(targetVersion);
|
||||
manifest.setTaskType(taskType);
|
||||
manifest.setProductType(productType);
|
||||
manifest.setVersion(version);
|
||||
manifest.setMinCompatibleVersion(minCompatibleVersion);
|
||||
manifest.setDescription(description);
|
||||
manifest.setOrchestrator("scripts/orchestrator.sh");
|
||||
UpgradeManifest.FileChecksums checksums = new UpgradeManifest.FileChecksums();
|
||||
checksums.setPayloadZip(sha256Hex(payloadPath));
|
||||
manifest.setFileChecksums(checksums);
|
||||
manifest.setDescription(taskType.toLowerCase() + " upgrade package");
|
||||
UpgradeManifest.Entrypoints entrypoints = new UpgradeManifest.Entrypoints();
|
||||
entrypoints.setExecute("payload/scripts/execute.sh");
|
||||
entrypoints.setPrecheck("payload/scripts/precheck.sh");
|
||||
entrypoints.setVerify("payload/scripts/verify.sh");
|
||||
entrypoints.setRollback("payload/scripts/rollback.sh");
|
||||
manifest.setEntrypoints(entrypoints);
|
||||
|
||||
byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest);
|
||||
byte[] signatureBytes = "signature".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(packagePath))) {
|
||||
outputStream.putNextEntry(new ZipEntry("manifest.json"));
|
||||
@@ -163,8 +246,27 @@ class UpgradePackageServiceTest {
|
||||
outputStream.write(signatureBytes);
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload.zip"));
|
||||
outputStream.write(Files.readAllBytes(payloadPath));
|
||||
outputStream.putNextEntry(new ZipEntry("payload/"));
|
||||
outputStream.closeEntry();
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/"));
|
||||
outputStream.closeEntry();
|
||||
|
||||
if (includeExecute) {
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/execute.sh"));
|
||||
outputStream.write("#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/precheck.sh"));
|
||||
outputStream.write("#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/verify.sh"));
|
||||
outputStream.write("#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/rollback.sh"));
|
||||
outputStream.write("#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
|
||||
@@ -176,15 +278,29 @@ class UpgradePackageServiceTest {
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void tamperManifestChecksum(Path packagePath) throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-manifest-tamper");
|
||||
UpgradePackageStagingService stagingService = stagingService(tempDir.resolve("staging"));
|
||||
Path staged = stagingService.stage("FILE-TAMPER", packagePath);
|
||||
UpgradeManifest manifest = objectMapper.readValue(Files.readString(staged.resolve("manifest.json")), UpgradeManifest.class);
|
||||
manifest.getFileChecksums().setPayloadZip("deadbeef");
|
||||
private FileRecordEntity buildSignedPackage(
|
||||
Path tempDir,
|
||||
String taskType,
|
||||
String productType,
|
||||
String version,
|
||||
String minCompatibleVersion,
|
||||
KeyPair keyPair
|
||||
) throws Exception {
|
||||
Path packagePath = tempDir.resolve("signed-upgrade-package.zip");
|
||||
|
||||
UpgradeManifest manifest = new UpgradeManifest();
|
||||
manifest.setPackageId("PKG-002");
|
||||
manifest.setTaskType(taskType);
|
||||
manifest.setProductType(productType);
|
||||
manifest.setVersion(version);
|
||||
manifest.setMinCompatibleVersion(minCompatibleVersion);
|
||||
manifest.setDescription(taskType.toLowerCase() + " signed upgrade package");
|
||||
UpgradeManifest.Entrypoints entrypoints = new UpgradeManifest.Entrypoints();
|
||||
entrypoints.setExecute("payload/scripts/execute.sh");
|
||||
manifest.setEntrypoints(entrypoints);
|
||||
|
||||
byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest);
|
||||
byte[] payloadBytes = Files.readAllBytes(staged.resolve("payload.zip"));
|
||||
byte[] signatureBytes = Files.readAllBytes(staged.resolve("signature.sig"));
|
||||
byte[] signatureBytes = sign(manifestBytes, keyPair);
|
||||
|
||||
try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(packagePath))) {
|
||||
outputStream.putNextEntry(new ZipEntry("manifest.json"));
|
||||
@@ -195,16 +311,21 @@ class UpgradePackageServiceTest {
|
||||
outputStream.write(signatureBytes);
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload.zip"));
|
||||
outputStream.write(payloadBytes);
|
||||
outputStream.putNextEntry(new ZipEntry("payload/"));
|
||||
outputStream.closeEntry();
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/"));
|
||||
outputStream.closeEntry();
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/execute.sh"));
|
||||
outputStream.write("#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private static String sha256Hex(Path file) throws IOException, NoSuchAlgorithmException {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
digest.update(Files.readAllBytes(file));
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
FileRecordEntity entity = new FileRecordEntity();
|
||||
entity.setFileId("FILE-UPGRADE-002");
|
||||
entity.setOriginalFilename("signed-upgrade-package.zip");
|
||||
entity.setStoragePath(packagePath.toString());
|
||||
entity.setStatus("ACTIVE");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static DeviceSoftwareVersionEntity component(String code, String name, String version) {
|
||||
@@ -215,9 +336,30 @@ class UpgradePackageServiceTest {
|
||||
return entity;
|
||||
}
|
||||
|
||||
private KeyPair generateSm2KeyPair() throws Exception {
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", PROVIDER);
|
||||
generator.initialize(new ECGenParameterSpec("sm2p256v1"));
|
||||
return generator.generateKeyPair();
|
||||
}
|
||||
|
||||
private byte[] sign(byte[] data, KeyPair keyPair) throws Exception {
|
||||
Signature signature = Signature.getInstance("SM3withSM2", PROVIDER);
|
||||
signature.initSign(keyPair.getPrivate());
|
||||
signature.update(data);
|
||||
return signature.sign();
|
||||
}
|
||||
|
||||
private String toPem(String type, byte[] content) throws Exception {
|
||||
StringWriter writer = new StringWriter();
|
||||
try (PemWriter pemWriter = new PemWriter(writer)) {
|
||||
pemWriter.writeObject(new PemObject(type, content));
|
||||
}
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
private static class PassThroughSignatureVerifier implements UpgradePackageSignatureVerifier {
|
||||
@Override
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) {
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskRequest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeTaskPageResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeTaskResponse;
|
||||
@@ -40,7 +41,8 @@ class UpgradeServiceTest {
|
||||
Mockito.when(fileRecordRepository.findByFileId("FILE-001")).thenReturn(Optional.of(fileRecord));
|
||||
|
||||
UpgradePreviewResponse preview = new UpgradePreviewResponse();
|
||||
preview.setTargetComponent("STANDARD_APP");
|
||||
preview.setTaskType("RECEIVER");
|
||||
preview.setProductType("DIRECT");
|
||||
preview.setCurrentVersion("V1.3.1");
|
||||
preview.setTargetVersion("V1.3.2");
|
||||
preview.setAllowed(true);
|
||||
@@ -64,7 +66,8 @@ class UpgradeServiceTest {
|
||||
UpgradeCreateTaskResponse response = service.createTask(request);
|
||||
|
||||
Assertions.assertEquals("PENDING_CONFIRM", response.getStatus());
|
||||
Assertions.assertEquals("STANDARD_APP", response.getTargetComponent());
|
||||
Assertions.assertEquals("RECEIVER", response.getTaskType());
|
||||
Assertions.assertEquals("DIRECT", response.getProductType());
|
||||
Assertions.assertEquals("V1.3.1", response.getCurrentVersion());
|
||||
Assertions.assertEquals("V1.3.2", response.getTargetVersion());
|
||||
Assertions.assertTrue(response.getTaskId().startsWith("UPG-"));
|
||||
@@ -134,6 +137,38 @@ class UpgradeServiceTest {
|
||||
Assertions.assertEquals("SUCCESS", response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRollbackWhenPackageDoesNotDeclareRollbackScript() {
|
||||
InMemoryUpgradeTaskRepository repository = new InMemoryUpgradeTaskRepository();
|
||||
repository.save(entity("UPG-001", "FAILED"));
|
||||
|
||||
UpgradePackageService packageService = Mockito.mock(UpgradePackageService.class);
|
||||
UpgradeManifest manifest = new UpgradeManifest();
|
||||
manifest.setPackageId("PKG-001");
|
||||
manifest.setTaskType("RECEIVER");
|
||||
manifest.setProductType("DIRECT");
|
||||
manifest.setVersion("V1.3.2");
|
||||
manifest.setMinCompatibleVersion("V1.3.0");
|
||||
UpgradeManifest.Entrypoints entrypoints = new UpgradeManifest.Entrypoints();
|
||||
entrypoints.setExecute("payload/scripts/execute.sh");
|
||||
manifest.setEntrypoints(entrypoints);
|
||||
Mockito.when(packageService.loadManifest("FILE-001")).thenReturn(manifest);
|
||||
|
||||
UpgradeService service = new UpgradeService(
|
||||
repository,
|
||||
Mockito.mock(FileRecordRepository.class),
|
||||
packageService,
|
||||
Mockito.mock(UpgradeTaskRunner.class),
|
||||
properties(),
|
||||
Runnable::run,
|
||||
new ConcurrentHashMap<>()
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class, () -> service.rollbackTask("UPG-001"));
|
||||
|
||||
Assertions.assertEquals("upgrade rollback script not found", exception.getMessage());
|
||||
}
|
||||
|
||||
private static UpgradeProperties properties() {
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setLogDir("/home/tms/tmp/tms-upgrade-logs");
|
||||
@@ -144,7 +179,8 @@ class UpgradeServiceTest {
|
||||
UpgradeTaskEntity entity = new UpgradeTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setUpgradeMode("OFFLINE");
|
||||
entity.setTargetComponent("STANDARD_APP");
|
||||
entity.setTaskType("RECEIVER");
|
||||
entity.setProductType("DIRECT");
|
||||
entity.setPackageFileId("FILE-001");
|
||||
entity.setPackageName("upgrade-package.zip");
|
||||
entity.setCurrentVersion("V1.3.1");
|
||||
@@ -178,9 +214,9 @@ class UpgradeServiceTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UpgradeTaskEntity> findPage(String targetComponent, String status, int offset, int limit) {
|
||||
public List<UpgradeTaskEntity> findPage(String taskType, String status, int offset, int limit) {
|
||||
return store.values().stream()
|
||||
.filter(entity -> targetComponent == null || targetComponent.isBlank() || targetComponent.equals(entity.getTargetComponent()))
|
||||
.filter(entity -> taskType == null || taskType.isBlank() || taskType.equals(entity.getTaskType()))
|
||||
.filter(entity -> status == null || status.isBlank() || status.equals(entity.getStatus()))
|
||||
.sorted(Comparator.comparing(UpgradeTaskEntity::getTaskId).reversed())
|
||||
.skip(offset)
|
||||
@@ -189,8 +225,8 @@ class UpgradeServiceTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countPage(String targetComponent, String status) {
|
||||
return findPage(targetComponent, status, 0, Integer.MAX_VALUE).size();
|
||||
public long countPage(String taskType, String status) {
|
||||
return findPage(taskType, status, 0, Integer.MAX_VALUE).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
package com.cisd.tms.modules.upgrade.support;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class PcieUpgradePackageSignatureVerifierTest {
|
||||
|
||||
@Test
|
||||
void shouldVerifyUpgradeManifestWithConfiguredIntegrityKey() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path payloadZipPath = tempDir.resolve("payload.zip");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8);
|
||||
Files.write(payloadZipPath, new byte[] {0x01, 0x02});
|
||||
byte[] signatureBytes = new byte[128];
|
||||
Files.writeString(signaturePath, Base64.getEncoder().encodeToString(signatureBytes), StandardCharsets.UTF_8);
|
||||
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
byte[] publicKeyBlob = new byte[132];
|
||||
properties.setSignaturePublicKeyBase64(Base64.getEncoder().encodeToString(publicKeyBlob));
|
||||
|
||||
PcieCryptoService cryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
PcieUpgradePackageSignatureVerifier verifier = new PcieUpgradePackageSignatureVerifier(properties, cryptoService);
|
||||
|
||||
verifier.verify(new UpgradeManifest(), manifestPath, payloadZipPath, signaturePath);
|
||||
|
||||
ArgumentCaptor<EccExternalVerifyRequest> requestCaptor = ArgumentCaptor.forClass(EccExternalVerifyRequest.class);
|
||||
Mockito.verify(cryptoService).eccExternalVerify(requestCaptor.capture());
|
||||
EccExternalVerifyRequest request = requestCaptor.getValue();
|
||||
Assertions.assertEquals(Gm0018AlgorithmIds.SM2_SIGN, request.getAlgId());
|
||||
Assertions.assertArrayEquals(publicKeyBlob, request.getPublicKeyBlob());
|
||||
Assertions.assertArrayEquals(signatureBytes, request.getSignature());
|
||||
Assertions.assertArrayEquals(Files.readAllBytes(manifestPath), request.getData());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWhenIntegrityKeyIsNotConfigured() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path payloadZipPath = tempDir.resolve("payload.zip");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8);
|
||||
Files.write(payloadZipPath, new byte[] {0x01, 0x02});
|
||||
Files.write(signaturePath, new byte[128]);
|
||||
|
||||
PcieUpgradePackageSignatureVerifier verifier =
|
||||
new PcieUpgradePackageSignatureVerifier(new UpgradeProperties(), Mockito.mock(PcieCryptoService.class));
|
||||
|
||||
BizException exception = Assertions.assertThrows(
|
||||
BizException.class,
|
||||
() -> verifier.verify(new UpgradeManifest(), manifestPath, payloadZipPath, signaturePath)
|
||||
);
|
||||
|
||||
Assertions.assertEquals(ErrorCode.BIZ_ERROR.getCode(), exception.getCode());
|
||||
Assertions.assertEquals("upgrade signature public key not configured", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWrapCryptoCardVerifyFailure() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path payloadZipPath = tempDir.resolve("payload.zip");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8);
|
||||
Files.write(payloadZipPath, new byte[] {0x01, 0x02});
|
||||
Files.write(signaturePath, new byte[128]);
|
||||
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setSignaturePublicKeyBase64(Base64.getEncoder().encodeToString(new byte[132]));
|
||||
PcieCryptoService cryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
Mockito.doThrow(new IllegalStateException("verify failed")).when(cryptoService).eccExternalVerify(Mockito.any());
|
||||
PcieUpgradePackageSignatureVerifier verifier = new PcieUpgradePackageSignatureVerifier(properties, cryptoService);
|
||||
|
||||
BizException exception = Assertions.assertThrows(
|
||||
BizException.class,
|
||||
() -> verifier.verify(new UpgradeManifest(), manifestPath, payloadZipPath, signaturePath)
|
||||
);
|
||||
|
||||
Assertions.assertEquals(ErrorCode.CRYPTO_CARD_ERROR.getCode(), exception.getCode());
|
||||
Assertions.assertEquals("upgrade package signature verification failed", exception.getMessage());
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package com.cisd.tms.modules.upgrade.support;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.util.Base64;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.util.io.pem.PemObject;
|
||||
import org.bouncycastle.util.io.pem.PemWriter;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SoftUpgradePackageSignatureVerifierTest {
|
||||
|
||||
private static final String PROVIDER = "BC";
|
||||
|
||||
static {
|
||||
if (Security.getProvider(PROVIDER) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldVerifyUpgradeManifestWithPemPublicKey() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Path publicKeyPath = tempDir.resolve("public_key.pem");
|
||||
byte[] manifestBytes = "{\"packageId\":\"PKG-001\"}".getBytes(StandardCharsets.UTF_8);
|
||||
Files.write(manifestPath, manifestBytes);
|
||||
|
||||
KeyPair keyPair = generateSm2KeyPair();
|
||||
Files.writeString(publicKeyPath, toPem("PUBLIC KEY", keyPair.getPublic().getEncoded()), StandardCharsets.UTF_8);
|
||||
Files.write(signaturePath, sign(manifestBytes, keyPair));
|
||||
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setSignaturePublicKeyPemPath(publicKeyPath.toString());
|
||||
|
||||
SoftUpgradePackageSignatureVerifier verifier = new SoftUpgradePackageSignatureVerifier(properties);
|
||||
|
||||
verifier.verify(new UpgradeManifest(), manifestPath, signaturePath);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldVerifyBase64SignatureContent() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Path publicKeyPath = tempDir.resolve("public_key.pem");
|
||||
byte[] manifestBytes = "{\"packageId\":\"PKG-001\"}".getBytes(StandardCharsets.UTF_8);
|
||||
Files.write(manifestPath, manifestBytes);
|
||||
|
||||
KeyPair keyPair = generateSm2KeyPair();
|
||||
Files.writeString(publicKeyPath, toPem("PUBLIC KEY", keyPair.getPublic().getEncoded()), StandardCharsets.UTF_8);
|
||||
Files.writeString(signaturePath, Base64.getEncoder().encodeToString(sign(manifestBytes, keyPair)), StandardCharsets.UTF_8);
|
||||
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setSignaturePublicKeyPemPath(publicKeyPath.toString());
|
||||
|
||||
SoftUpgradePackageSignatureVerifier verifier = new SoftUpgradePackageSignatureVerifier(properties);
|
||||
|
||||
verifier.verify(new UpgradeManifest(), manifestPath, signaturePath);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWhenPemPathIsNotConfigured() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8);
|
||||
Files.write(signaturePath, new byte[] {0x01});
|
||||
|
||||
SoftUpgradePackageSignatureVerifier verifier = new SoftUpgradePackageSignatureVerifier(new UpgradeProperties());
|
||||
|
||||
BizException exception = Assertions.assertThrows(
|
||||
BizException.class,
|
||||
() -> verifier.verify(new UpgradeManifest(), manifestPath, signaturePath)
|
||||
);
|
||||
|
||||
Assertions.assertEquals(ErrorCode.BIZ_ERROR.getCode(), exception.getCode());
|
||||
Assertions.assertEquals("upgrade signature public key pem path not configured", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWhenSignatureIsInvalid() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Path publicKeyPath = tempDir.resolve("public_key.pem");
|
||||
Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8);
|
||||
Files.write(signaturePath, "invalid-signature".getBytes(StandardCharsets.UTF_8));
|
||||
Files.writeString(publicKeyPath, toPem("PUBLIC KEY", generateSm2KeyPair().getPublic().getEncoded()), StandardCharsets.UTF_8);
|
||||
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setSignaturePublicKeyPemPath(publicKeyPath.toString());
|
||||
SoftUpgradePackageSignatureVerifier verifier = new SoftUpgradePackageSignatureVerifier(properties);
|
||||
|
||||
BizException exception = Assertions.assertThrows(
|
||||
BizException.class,
|
||||
() -> verifier.verify(new UpgradeManifest(), manifestPath, signaturePath)
|
||||
);
|
||||
|
||||
Assertions.assertEquals(ErrorCode.BIZ_ERROR.getCode(), exception.getCode());
|
||||
Assertions.assertEquals("upgrade package signature verification failed", exception.getMessage());
|
||||
}
|
||||
|
||||
private KeyPair generateSm2KeyPair() throws Exception {
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", PROVIDER);
|
||||
generator.initialize(new ECGenParameterSpec("sm2p256v1"));
|
||||
return generator.generateKeyPair();
|
||||
}
|
||||
|
||||
private byte[] sign(byte[] data, KeyPair keyPair) throws Exception {
|
||||
Signature signature = Signature.getInstance("SM3withSM2", PROVIDER);
|
||||
signature.initSign(keyPair.getPrivate());
|
||||
signature.update(data);
|
||||
return signature.sign();
|
||||
}
|
||||
|
||||
private String toPem(String type, byte[] content) throws Exception {
|
||||
StringWriter writer = new StringWriter();
|
||||
try (PemWriter pemWriter = new PemWriter(writer)) {
|
||||
pemWriter.writeObject(new PemObject(type, content));
|
||||
}
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user