升级签名和摘要计算
This commit is contained in:
parent
ac024c1176
commit
05546fa213
@ -192,6 +192,7 @@ Open:
|
|||||||
- `sql/`
|
- `sql/`
|
||||||
- `firmware/`
|
- `firmware/`
|
||||||
- `scripts/`
|
- `scripts/`
|
||||||
|
- 外层升级包实际包含的是 `payload.zip`,`payload.zip` 内部再包含上述 `payload/` 目录
|
||||||
- `signature.sig` 需要能被配置的 PEM 公钥使用 `SM3withSM2` 软验签通过
|
- `signature.sig` 需要能被配置的 PEM 公钥使用 `SM3withSM2` 软验签通过
|
||||||
- 服务端需要配置 `tms.upgrade.signature-public-key-pem-path`(公钥 PEM 文件路径)
|
- 服务端需要配置 `tms.upgrade.signature-public-key-pem-path`(公钥 PEM 文件路径)
|
||||||
- `manifest.json` 当前要求包含:
|
- `manifest.json` 当前要求包含:
|
||||||
@ -202,9 +203,11 @@ Open:
|
|||||||
- `minCompatibleVersion`
|
- `minCompatibleVersion`
|
||||||
- `description`
|
- `description`
|
||||||
- `entrypoints.execute`
|
- `entrypoints.execute`
|
||||||
|
- `payloadSm3`
|
||||||
- 可选 `entrypoints.precheck`
|
- 可选 `entrypoints.precheck`
|
||||||
- 可选 `entrypoints.verify`
|
- 可选 `entrypoints.verify`
|
||||||
- 可选 `entrypoints.rollback`
|
- 可选 `entrypoints.rollback`
|
||||||
|
- `payloadSm3` 是整个 `payload.zip` 文件的 `SM3` 摘要;后端验签 `manifest.json` 后,会校验 `payload.zip` 摘要并解压出 `payload/` 供脚本执行
|
||||||
|
|
||||||
当前执行规则:
|
当前执行规则:
|
||||||
- 同一时刻只允许一个升级任务处于 `RUNNING`
|
- 同一时刻只允许一个升级任务处于 `RUNNING`
|
||||||
|
|||||||
@ -19,6 +19,8 @@ public class UpgradeManifest {
|
|||||||
private String description;
|
private String description;
|
||||||
@Schema(description = "升级脚本入口")
|
@Schema(description = "升级脚本入口")
|
||||||
private Entrypoints entrypoints = new Entrypoints();
|
private Entrypoints entrypoints = new Entrypoints();
|
||||||
|
@Schema(description = "payload.zip 文件 SM3 摘要")
|
||||||
|
private String payloadSm3;
|
||||||
|
|
||||||
public String getPackageId() {
|
public String getPackageId() {
|
||||||
return packageId;
|
return packageId;
|
||||||
@ -76,6 +78,14 @@ public class UpgradeManifest {
|
|||||||
this.entrypoints = entrypoints;
|
this.entrypoints = entrypoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPayloadSm3() {
|
||||||
|
return payloadSm3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPayloadSm3(String payloadSm3) {
|
||||||
|
this.payloadSm3 = payloadSm3;
|
||||||
|
}
|
||||||
|
|
||||||
public static class Entrypoints {
|
public static class Entrypoints {
|
||||||
private String execute;
|
private String execute;
|
||||||
private String precheck;
|
private String precheck;
|
||||||
|
|||||||
@ -56,6 +56,7 @@ public class UpgradeTaskRunner {
|
|||||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + task.getPackageFileId()));
|
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + task.getPackageFileId()));
|
||||||
Path stagedDir = upgradePackageStagingService.stage(task.getTaskId(), Path.of(record.getStoragePath()));
|
Path stagedDir = upgradePackageStagingService.stage(task.getTaskId(), Path.of(record.getStoragePath()));
|
||||||
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
|
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
|
||||||
|
upgradePackageStagingService.extractPayloadZip(stagedDir);
|
||||||
Path root = stagedDir.normalize().toAbsolutePath();
|
Path root = stagedDir.normalize().toAbsolutePath();
|
||||||
|
|
||||||
// 包内脚本按固定阶段执行;可选阶段未声明时跳过,execute 阶段必须存在。
|
// 包内脚本按固定阶段执行;可选阶段未声明时跳过,execute 阶段必须存在。
|
||||||
@ -137,6 +138,7 @@ public class UpgradeTaskRunner {
|
|||||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + 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()));
|
Path stagedDir = upgradePackageStagingService.stage(task.getTaskId() + "-rollback", Path.of(record.getStoragePath()));
|
||||||
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
|
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
|
||||||
|
upgradePackageStagingService.extractPayloadZip(stagedDir);
|
||||||
String rollbackPath = manifest.getEntrypoints() == null ? "" : trim(manifest.getEntrypoints().getRollback());
|
String rollbackPath = manifest.getEntrypoints() == null ? "" : trim(manifest.getEntrypoints().getRollback());
|
||||||
if (rollbackPath.isEmpty()) {
|
if (rollbackPath.isEmpty()) {
|
||||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade rollback script not found");
|
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade rollback script not found");
|
||||||
|
|||||||
@ -16,7 +16,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -56,6 +58,9 @@ public class UpgradePackageService {
|
|||||||
validateProductType(manifest.getProductType());
|
validateProductType(manifest.getProductType());
|
||||||
// 签名对象固定为 manifest.json 原始字节,具体算法由 UpgradePackageSignatureVerifier 实现。
|
// 签名对象固定为 manifest.json 原始字节,具体算法由 UpgradePackageSignatureVerifier 实现。
|
||||||
signatureVerifier.verify(manifest, stagedPackage.manifestPath(), stagedPackage.signaturePath());
|
signatureVerifier.verify(manifest, stagedPackage.manifestPath(), stagedPackage.signaturePath());
|
||||||
|
// 校验 payload.zip 摘要,确保已签名的 manifest 能约束实际载荷压缩包内容。
|
||||||
|
validatePayloadZipSm3(stagedPackage.stagedDir(), manifest);
|
||||||
|
upgradePackageStagingService.extractPayloadZip(stagedPackage.stagedDir());
|
||||||
validateExecuteScript(stagedPackage.stagedDir(), manifest);
|
validateExecuteScript(stagedPackage.stagedDir(), manifest);
|
||||||
|
|
||||||
// 当前版本来自设备软件版本表;空版本允许继续预检,由前端展示“未记录版本”。
|
// 当前版本来自设备软件版本表;空版本允许继续预检,由前端展示“未记录版本”。
|
||||||
@ -144,6 +149,24 @@ public class UpgradePackageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validatePayloadZipSm3(Path stagedDir, UpgradeManifest manifest) {
|
||||||
|
String expected = normalize(manifest.getPayloadSm3()).toLowerCase(Locale.ROOT);
|
||||||
|
if (expected.isEmpty()) {
|
||||||
|
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package payload sm3 missing");
|
||||||
|
}
|
||||||
|
if (!expected.matches("[0-9a-f]{64}")) {
|
||||||
|
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package payload sm3 invalid");
|
||||||
|
}
|
||||||
|
Path root = stagedDir.normalize().toAbsolutePath();
|
||||||
|
Path payloadZip = root.resolve("payload.zip").normalize().toAbsolutePath();
|
||||||
|
if (!payloadZip.startsWith(root) || !Files.exists(payloadZip) || !Files.isRegularFile(payloadZip)) {
|
||||||
|
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade payload.zip not found");
|
||||||
|
}
|
||||||
|
if (!expected.equals(sm3Hex(payloadZip))) {
|
||||||
|
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package payload sm3 mismatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void validateExecuteScript(Path stagedDir, UpgradeManifest manifest) {
|
private void validateExecuteScript(Path stagedDir, UpgradeManifest manifest) {
|
||||||
Path executePath = stagedDir.resolve(normalize(manifest.getEntrypoints().getExecute())).normalize().toAbsolutePath();
|
Path executePath = stagedDir.resolve(normalize(manifest.getEntrypoints().getExecute())).normalize().toAbsolutePath();
|
||||||
// execute 路径必须在 staging 目录内,避免 manifest 声明外部系统脚本。
|
// execute 路径必须在 staging 目录内,避免 manifest 声明外部系统脚本。
|
||||||
@ -166,6 +189,23 @@ public class UpgradePackageService {
|
|||||||
return normalize(value).toUpperCase();
|
return normalize(value).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String sm3Hex(Path path) {
|
||||||
|
try {
|
||||||
|
byte[] content = Files.readAllBytes(path);
|
||||||
|
SM3Digest digest = new SM3Digest();
|
||||||
|
digest.update(content, 0, content.length);
|
||||||
|
byte[] hash = new byte[digest.getDigestSize()];
|
||||||
|
digest.doFinal(hash, 0);
|
||||||
|
StringBuilder builder = new StringBuilder(hash.length * 2);
|
||||||
|
for (byte value : hash) {
|
||||||
|
builder.append(String.format("%02x", value));
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package checksum invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private record StagedUpgradePackage(
|
private record StagedUpgradePackage(
|
||||||
Path stagedDir,
|
Path stagedDir,
|
||||||
Path manifestPath,
|
Path manifestPath,
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import java.io.InputStream;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -26,18 +28,36 @@ public class UpgradePackageStagingService {
|
|||||||
Path taskDir = root.resolve(fileId).normalize().toAbsolutePath();
|
Path taskDir = root.resolve(fileId).normalize().toAbsolutePath();
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(taskDir);
|
Files.createDirectories(taskDir);
|
||||||
unzip(packagePath, taskDir);
|
unzip(packagePath, taskDir, "");
|
||||||
return taskDir;
|
return taskDir;
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to stage upgrade package");
|
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to stage upgrade package");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unzip(Path zipFile, Path targetDir) throws IOException {
|
public void extractPayloadZip(Path stagedDir) {
|
||||||
|
Path root = stagedDir.normalize().toAbsolutePath();
|
||||||
|
Path payloadZip = root.resolve("payload.zip").normalize().toAbsolutePath();
|
||||||
|
if (!payloadZip.startsWith(root) || !Files.exists(payloadZip) || !Files.isRegularFile(payloadZip)) {
|
||||||
|
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade payload.zip not found");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
deleteDirectoryIfExists(root.resolve("payload"));
|
||||||
|
unzip(payloadZip, root, "payload/");
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to extract upgrade payload.zip");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unzip(Path zipFile, Path targetDir, String requiredEntryPrefix) throws IOException {
|
||||||
try (InputStream inputStream = Files.newInputStream(zipFile);
|
try (InputStream inputStream = Files.newInputStream(zipFile);
|
||||||
ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
|
ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
|
||||||
ZipEntry entry;
|
ZipEntry entry;
|
||||||
while ((entry = zipInputStream.getNextEntry()) != null) {
|
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||||
|
String entryName = entry.getName();
|
||||||
|
if (!requiredEntryPrefix.isEmpty() && !entryName.startsWith(requiredEntryPrefix)) {
|
||||||
|
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "invalid upgrade payload entry");
|
||||||
|
}
|
||||||
Path target = targetDir.resolve(entry.getName()).normalize().toAbsolutePath();
|
Path target = targetDir.resolve(entry.getName()).normalize().toAbsolutePath();
|
||||||
// 防止恶意 zip 条目通过 ../ 写出 staging 目录。
|
// 防止恶意 zip 条目通过 ../ 写出 staging 目录。
|
||||||
if (!target.startsWith(targetDir)) {
|
if (!target.startsWith(targetDir)) {
|
||||||
@ -53,6 +73,26 @@ public class UpgradePackageStagingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void deleteDirectoryIfExists(Path directory) throws IOException {
|
||||||
|
if (!Files.exists(directory)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (Stream<Path> stream = Files.walk(directory)) {
|
||||||
|
stream.sorted(Comparator.reverseOrder()).forEach(path -> {
|
||||||
|
try {
|
||||||
|
Files.delete(path);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IllegalStateException ex) {
|
||||||
|
if (ex.getCause() instanceof IOException ioException) {
|
||||||
|
throw ioException;
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String trim(String value) {
|
private String trim(String value) {
|
||||||
return value == null ? "" : value.trim();
|
return value == null ? "" : value.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import com.cisd.tms.modules.upgrade.entity.UpgradeTaskEntity;
|
|||||||
import com.cisd.tms.modules.upgrade.repository.UpgradeTaskRepository;
|
import com.cisd.tms.modules.upgrade.repository.UpgradeTaskRepository;
|
||||||
import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService;
|
import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@ -19,6 +20,7 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@ -189,6 +191,17 @@ class UpgradeTaskRunnerTest {
|
|||||||
entrypoints.setRollback("payload/scripts/rollback.sh");
|
entrypoints.setRollback("payload/scripts/rollback.sh");
|
||||||
}
|
}
|
||||||
manifest.setEntrypoints(entrypoints);
|
manifest.setEntrypoints(entrypoints);
|
||||||
|
Map<String, byte[]> payloadFiles = new HashMap<>();
|
||||||
|
if (includeOptionalScripts) {
|
||||||
|
payloadFiles.put("payload/scripts/precheck.sh", "#!/bin/sh\necho precheck\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||||
|
payloadFiles.put("payload/scripts/verify.sh", "#!/bin/sh\necho verify\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
payloadFiles.put("payload/scripts/execute.sh", executeScript.getBytes(StandardCharsets.UTF_8));
|
||||||
|
if (includeRollback) {
|
||||||
|
payloadFiles.put("payload/scripts/rollback.sh", "#!/bin/sh\necho rollback\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
byte[] payloadZip = payloadZip(payloadFiles);
|
||||||
|
manifest.setPayloadSm3(sm3Hex(payloadZip));
|
||||||
|
|
||||||
try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(packagePath))) {
|
try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(packagePath))) {
|
||||||
outputStream.putNextEntry(new ZipEntry("manifest.json"));
|
outputStream.putNextEntry(new ZipEntry("manifest.json"));
|
||||||
@ -199,30 +212,9 @@ class UpgradeTaskRunnerTest {
|
|||||||
outputStream.write("signature".getBytes(StandardCharsets.UTF_8));
|
outputStream.write("signature".getBytes(StandardCharsets.UTF_8));
|
||||||
outputStream.closeEntry();
|
outputStream.closeEntry();
|
||||||
|
|
||||||
outputStream.putNextEntry(new ZipEntry("payload/"));
|
outputStream.putNextEntry(new ZipEntry("payload.zip"));
|
||||||
|
outputStream.write(payloadZip);
|
||||||
outputStream.closeEntry();
|
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();
|
FileRecordEntity entity = new FileRecordEntity();
|
||||||
@ -233,6 +225,30 @@ class UpgradeTaskRunnerTest {
|
|||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] payloadZip(Map<String, byte[]> payloadFiles) throws Exception {
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
try (ZipOutputStream outputStream = new ZipOutputStream(bytes)) {
|
||||||
|
for (Map.Entry<String, byte[]> entry : payloadFiles.entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) {
|
||||||
|
outputStream.putNextEntry(new ZipEntry(entry.getKey()));
|
||||||
|
outputStream.write(entry.getValue());
|
||||||
|
outputStream.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sm3Hex(byte[] content) {
|
||||||
|
SM3Digest digest = new SM3Digest();
|
||||||
|
digest.update(content, 0, content.length);
|
||||||
|
byte[] hash = new byte[digest.getDigestSize()];
|
||||||
|
digest.doFinal(hash, 0);
|
||||||
|
StringBuilder builder = new StringBuilder(hash.length * 2);
|
||||||
|
for (byte value : hash) {
|
||||||
|
builder.append(String.format("%02x", value));
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private UpgradeTaskEntity task(String taskId, String fileId, String taskType, String targetVersion) {
|
private UpgradeTaskEntity task(String taskId, String fileId, String taskType, String targetVersion) {
|
||||||
UpgradeTaskEntity entity = new UpgradeTaskEntity();
|
UpgradeTaskEntity entity = new UpgradeTaskEntity();
|
||||||
entity.setTaskId(taskId);
|
entity.setTaskId(taskId);
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import com.cisd.tms.modules.upgrade.support.UpgradePackageSignatureVerifier;
|
|||||||
import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService;
|
import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService;
|
||||||
import com.cisd.tms.modules.upgrade.support.UpgradeVersionComparator;
|
import com.cisd.tms.modules.upgrade.support.UpgradeVersionComparator;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@ -30,6 +31,7 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.bouncycastle.util.io.pem.PemObject;
|
import org.bouncycastle.util.io.pem.PemObject;
|
||||||
import org.bouncycastle.util.io.pem.PemWriter;
|
import org.bouncycastle.util.io.pem.PemWriter;
|
||||||
@ -167,6 +169,48 @@ class UpgradePackageServiceTest {
|
|||||||
Assertions.assertEquals("upgrade execute script not found", exception.getMessage());
|
Assertions.assertEquals("upgrade execute script not found", exception.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectWhenPayloadSm3IsMissing() throws Exception {
|
||||||
|
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||||
|
FileRecordEntity fileRecord = buildPackage(tempDir, "RECEIVER", "DIRECT", "V1.4.0", "V1.3.0", true, 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 package payload sm3 missing", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectWhenPayloadZipSm3DoesNotMatch() throws Exception {
|
||||||
|
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||||
|
FileRecordEntity fileRecord = buildPackage(tempDir, "RECEIVER", "DIRECT", "V1.4.0", "V1.3.0", true, true, true);
|
||||||
|
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 package payload sm3 mismatch", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldPreviewUpgradePackageWithSoftSignatureVerification() throws Exception {
|
void shouldPreviewUpgradePackageWithSoftSignatureVerification() throws Exception {
|
||||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||||
@ -218,8 +262,37 @@ class UpgradePackageServiceTest {
|
|||||||
|
|
||||||
private FileRecordEntity buildPackage(Path tempDir, String taskType, String productType, String version, String minCompatibleVersion, boolean includeExecute)
|
private FileRecordEntity buildPackage(Path tempDir, String taskType, String productType, String version, String minCompatibleVersion, boolean includeExecute)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
return buildPackage(tempDir, taskType, productType, version, minCompatibleVersion, includeExecute, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileRecordEntity buildPackage(
|
||||||
|
Path tempDir,
|
||||||
|
String taskType,
|
||||||
|
String productType,
|
||||||
|
String version,
|
||||||
|
String minCompatibleVersion,
|
||||||
|
boolean includeExecute,
|
||||||
|
boolean includePayloadSm3
|
||||||
|
) throws IOException {
|
||||||
|
return buildPackage(tempDir, taskType, productType, version, minCompatibleVersion, includeExecute, includePayloadSm3, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileRecordEntity buildPackage(
|
||||||
|
Path tempDir,
|
||||||
|
String taskType,
|
||||||
|
String productType,
|
||||||
|
String version,
|
||||||
|
String minCompatibleVersion,
|
||||||
|
boolean includeExecute,
|
||||||
|
boolean includePayloadSm3,
|
||||||
|
boolean corruptPayloadSm3
|
||||||
|
) throws IOException {
|
||||||
Path packagePath = tempDir.resolve("upgrade-package.zip");
|
Path packagePath = tempDir.resolve("upgrade-package.zip");
|
||||||
byte[] signatureBytes = "signature".getBytes(StandardCharsets.UTF_8);
|
byte[] signatureBytes = "signature".getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] executeScript = "#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] precheckScript = "#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] verifyScript = "#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] rollbackScript = "#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
UpgradeManifest manifest = new UpgradeManifest();
|
UpgradeManifest manifest = new UpgradeManifest();
|
||||||
manifest.setPackageId("PKG-001");
|
manifest.setPackageId("PKG-001");
|
||||||
@ -234,6 +307,14 @@ class UpgradePackageServiceTest {
|
|||||||
entrypoints.setVerify("payload/scripts/verify.sh");
|
entrypoints.setVerify("payload/scripts/verify.sh");
|
||||||
entrypoints.setRollback("payload/scripts/rollback.sh");
|
entrypoints.setRollback("payload/scripts/rollback.sh");
|
||||||
manifest.setEntrypoints(entrypoints);
|
manifest.setEntrypoints(entrypoints);
|
||||||
|
byte[] payloadZip = payloadZip(Map.of(
|
||||||
|
"payload/scripts/precheck.sh", precheckScript,
|
||||||
|
"payload/scripts/verify.sh", verifyScript,
|
||||||
|
"payload/scripts/rollback.sh", rollbackScript
|
||||||
|
), includeExecute ? Map.of("payload/scripts/execute.sh", executeScript) : Map.of());
|
||||||
|
if (includePayloadSm3) {
|
||||||
|
manifest.setPayloadSm3(corruptPayloadSm3 ? "0".repeat(64) : sm3Hex(payloadZip));
|
||||||
|
}
|
||||||
|
|
||||||
byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest);
|
byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest);
|
||||||
|
|
||||||
@ -246,27 +327,8 @@ class UpgradePackageServiceTest {
|
|||||||
outputStream.write(signatureBytes);
|
outputStream.write(signatureBytes);
|
||||||
outputStream.closeEntry();
|
outputStream.closeEntry();
|
||||||
|
|
||||||
outputStream.putNextEntry(new ZipEntry("payload/"));
|
outputStream.putNextEntry(new ZipEntry("payload.zip"));
|
||||||
outputStream.closeEntry();
|
outputStream.write(payloadZip);
|
||||||
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();
|
outputStream.closeEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,6 +360,9 @@ class UpgradePackageServiceTest {
|
|||||||
UpgradeManifest.Entrypoints entrypoints = new UpgradeManifest.Entrypoints();
|
UpgradeManifest.Entrypoints entrypoints = new UpgradeManifest.Entrypoints();
|
||||||
entrypoints.setExecute("payload/scripts/execute.sh");
|
entrypoints.setExecute("payload/scripts/execute.sh");
|
||||||
manifest.setEntrypoints(entrypoints);
|
manifest.setEntrypoints(entrypoints);
|
||||||
|
byte[] executeScript = "#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] payloadZip = payloadZip(Map.of("payload/scripts/execute.sh", executeScript), Map.of());
|
||||||
|
manifest.setPayloadSm3(sm3Hex(payloadZip));
|
||||||
|
|
||||||
byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest);
|
byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest);
|
||||||
byte[] signatureBytes = sign(manifestBytes, keyPair);
|
byte[] signatureBytes = sign(manifestBytes, keyPair);
|
||||||
@ -311,12 +376,8 @@ class UpgradePackageServiceTest {
|
|||||||
outputStream.write(signatureBytes);
|
outputStream.write(signatureBytes);
|
||||||
outputStream.closeEntry();
|
outputStream.closeEntry();
|
||||||
|
|
||||||
outputStream.putNextEntry(new ZipEntry("payload/"));
|
outputStream.putNextEntry(new ZipEntry("payload.zip"));
|
||||||
outputStream.closeEntry();
|
outputStream.write(payloadZip);
|
||||||
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();
|
outputStream.closeEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,6 +410,33 @@ class UpgradePackageServiceTest {
|
|||||||
return signature.sign();
|
return signature.sign();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] payloadZip(Map<String, byte[]> required, Map<String, byte[]> optional) throws IOException {
|
||||||
|
Map<String, byte[]> all = new HashMap<>();
|
||||||
|
all.putAll(required);
|
||||||
|
all.putAll(optional);
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
try (ZipOutputStream outputStream = new ZipOutputStream(bytes)) {
|
||||||
|
for (Map.Entry<String, byte[]> entry : all.entrySet().stream().sorted(Map.Entry.comparingByKey()).toList()) {
|
||||||
|
outputStream.putNextEntry(new ZipEntry(entry.getKey()));
|
||||||
|
outputStream.write(entry.getValue());
|
||||||
|
outputStream.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sm3Hex(byte[] content) {
|
||||||
|
SM3Digest digest = new SM3Digest();
|
||||||
|
digest.update(content, 0, content.length);
|
||||||
|
byte[] hash = new byte[digest.getDigestSize()];
|
||||||
|
digest.doFinal(hash, 0);
|
||||||
|
StringBuilder builder = new StringBuilder(hash.length * 2);
|
||||||
|
for (byte value : hash) {
|
||||||
|
builder.append(String.format("%02x", value));
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private String toPem(String type, byte[] content) throws Exception {
|
private String toPem(String type, byte[] content) throws Exception {
|
||||||
StringWriter writer = new StringWriter();
|
StringWriter writer = new StringWriter();
|
||||||
try (PemWriter pemWriter = new PemWriter(writer)) {
|
try (PemWriter pemWriter = new PemWriter(writer)) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user