升级签名和摘要计算

This commit is contained in:
waner
2026-04-10 15:22:35 +08:00
parent ac024c1176
commit 05546fa213
7 changed files with 251 additions and 52 deletions
@@ -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,
@@ -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();
}