升级签名和摘要计算
This commit is contained in:
@@ -19,6 +19,8 @@ public class UpgradeManifest {
|
||||
private String description;
|
||||
@Schema(description = "升级脚本入口")
|
||||
private Entrypoints entrypoints = new Entrypoints();
|
||||
@Schema(description = "payload.zip 文件 SM3 摘要")
|
||||
private String payloadSm3;
|
||||
|
||||
public String getPackageId() {
|
||||
return packageId;
|
||||
@@ -76,6 +78,14 @@ public class UpgradeManifest {
|
||||
this.entrypoints = entrypoints;
|
||||
}
|
||||
|
||||
public String getPayloadSm3() {
|
||||
return payloadSm3;
|
||||
}
|
||||
|
||||
public void setPayloadSm3(String payloadSm3) {
|
||||
this.payloadSm3 = payloadSm3;
|
||||
}
|
||||
|
||||
public static class Entrypoints {
|
||||
private String execute;
|
||||
private String precheck;
|
||||
|
||||
@@ -56,6 +56,7 @@ 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);
|
||||
upgradePackageStagingService.extractPayloadZip(stagedDir);
|
||||
Path root = stagedDir.normalize().toAbsolutePath();
|
||||
|
||||
// 包内脚本按固定阶段执行;可选阶段未声明时跳过,execute 阶段必须存在。
|
||||
@@ -137,6 +138,7 @@ public class UpgradeTaskRunner {
|
||||
.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);
|
||||
upgradePackageStagingService.extractPayloadZip(stagedDir);
|
||||
String rollbackPath = manifest.getEntrypoints() == null ? "" : trim(manifest.getEntrypoints().getRollback());
|
||||
if (rollbackPath.isEmpty()) {
|
||||
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.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@@ -56,6 +58,9 @@ public class UpgradePackageService {
|
||||
validateProductType(manifest.getProductType());
|
||||
// 签名对象固定为 manifest.json 原始字节,具体算法由 UpgradePackageSignatureVerifier 实现。
|
||||
signatureVerifier.verify(manifest, stagedPackage.manifestPath(), stagedPackage.signaturePath());
|
||||
// 校验 payload.zip 摘要,确保已签名的 manifest 能约束实际载荷压缩包内容。
|
||||
validatePayloadZipSm3(stagedPackage.stagedDir(), manifest);
|
||||
upgradePackageStagingService.extractPayloadZip(stagedPackage.stagedDir());
|
||||
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) {
|
||||
Path executePath = stagedDir.resolve(normalize(manifest.getEntrypoints().getExecute())).normalize().toAbsolutePath();
|
||||
// execute 路径必须在 staging 目录内,避免 manifest 声明外部系统脚本。
|
||||
@@ -166,6 +189,23 @@ public class UpgradePackageService {
|
||||
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(
|
||||
Path stagedDir,
|
||||
Path manifestPath,
|
||||
|
||||
+42
-2
@@ -8,6 +8,8 @@ import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Comparator;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -26,18 +28,36 @@ public class UpgradePackageStagingService {
|
||||
Path taskDir = root.resolve(fileId).normalize().toAbsolutePath();
|
||||
try {
|
||||
Files.createDirectories(taskDir);
|
||||
unzip(packagePath, taskDir);
|
||||
unzip(packagePath, taskDir, "");
|
||||
return taskDir;
|
||||
} catch (IOException ex) {
|
||||
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);
|
||||
ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
|
||||
ZipEntry entry;
|
||||
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();
|
||||
// 防止恶意 zip 条目通过 ../ 写出 staging 目录。
|
||||
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) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user