diff --git a/README.md b/README.md index 443dfbc..92c4e3b 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,7 @@ Open: - `sql/` - `firmware/` - `scripts/` +- 外层升级包实际包含的是 `payload.zip`,`payload.zip` 内部再包含上述 `payload/` 目录 - `signature.sig` 需要能被配置的 PEM 公钥使用 `SM3withSM2` 软验签通过 - 服务端需要配置 `tms.upgrade.signature-public-key-pem-path`(公钥 PEM 文件路径) - `manifest.json` 当前要求包含: @@ -202,9 +203,11 @@ Open: - `minCompatibleVersion` - `description` - `entrypoints.execute` + - `payloadSm3` - 可选 `entrypoints.precheck` - 可选 `entrypoints.verify` - 可选 `entrypoints.rollback` +- `payloadSm3` 是整个 `payload.zip` 文件的 `SM3` 摘要;后端验签 `manifest.json` 后,会校验 `payload.zip` 摘要并解压出 `payload/` 供脚本执行 当前执行规则: - 同一时刻只允许一个升级任务处于 `RUNNING` diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeManifest.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeManifest.java index 7e54c58..ba2cd37 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeManifest.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeManifest.java @@ -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; diff --git a/src/main/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunner.java b/src/main/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunner.java index 07213e6..9bf7e5d 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunner.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunner.java @@ -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"); diff --git a/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradePackageService.java b/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradePackageService.java index 6f4c0c7..a53c986 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradePackageService.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradePackageService.java @@ -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, diff --git a/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageStagingService.java b/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageStagingService.java index 7026100..5cc7abe 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageStagingService.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageStagingService.java @@ -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 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(); } diff --git a/src/test/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunnerTest.java b/src/test/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunnerTest.java index 744a9fe..1703556 100644 --- a/src/test/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunnerTest.java +++ b/src/test/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunnerTest.java @@ -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.support.UpgradePackageStagingService; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -19,6 +20,7 @@ import java.util.Map; import java.util.Optional; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import org.bouncycastle.crypto.digests.SM3Digest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -189,6 +191,17 @@ class UpgradeTaskRunnerTest { entrypoints.setRollback("payload/scripts/rollback.sh"); } manifest.setEntrypoints(entrypoints); + Map 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))) { outputStream.putNextEntry(new ZipEntry("manifest.json")); @@ -199,30 +212,9 @@ class UpgradeTaskRunnerTest { outputStream.write("signature".getBytes(StandardCharsets.UTF_8)); outputStream.closeEntry(); - outputStream.putNextEntry(new ZipEntry("payload/")); + outputStream.putNextEntry(new ZipEntry("payload.zip")); + outputStream.write(payloadZip); 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(); @@ -233,6 +225,30 @@ class UpgradeTaskRunnerTest { return entity; } + private byte[] payloadZip(Map payloadFiles) throws Exception { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ZipOutputStream outputStream = new ZipOutputStream(bytes)) { + for (Map.Entry 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) { UpgradeTaskEntity entity = new UpgradeTaskEntity(); entity.setTaskId(taskId); diff --git a/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradePackageServiceTest.java b/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradePackageServiceTest.java index 66df8f5..b3f1316 100644 --- a/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradePackageServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradePackageServiceTest.java @@ -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.UpgradeVersionComparator; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.ByteArrayOutputStream; import java.io.StringWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -30,6 +31,7 @@ import java.util.Map; import java.util.Optional; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import org.bouncycastle.crypto.digests.SM3Digest; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemWriter; @@ -167,6 +169,48 @@ class UpgradePackageServiceTest { 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 void shouldPreviewUpgradePackageWithSoftSignatureVerification() throws Exception { 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) 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"); 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(); manifest.setPackageId("PKG-001"); @@ -234,6 +307,14 @@ class UpgradePackageServiceTest { entrypoints.setVerify("payload/scripts/verify.sh"); entrypoints.setRollback("payload/scripts/rollback.sh"); 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); @@ -246,27 +327,8 @@ class UpgradePackageServiceTest { outputStream.write(signatureBytes); outputStream.closeEntry(); - 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.putNextEntry(new ZipEntry("payload.zip")); + outputStream.write(payloadZip); outputStream.closeEntry(); } @@ -298,6 +360,9 @@ class UpgradePackageServiceTest { UpgradeManifest.Entrypoints entrypoints = new UpgradeManifest.Entrypoints(); entrypoints.setExecute("payload/scripts/execute.sh"); 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[] signatureBytes = sign(manifestBytes, keyPair); @@ -311,12 +376,8 @@ class UpgradePackageServiceTest { outputStream.write(signatureBytes); outputStream.closeEntry(); - 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.putNextEntry(new ZipEntry("payload.zip")); + outputStream.write(payloadZip); outputStream.closeEntry(); } @@ -349,6 +410,33 @@ class UpgradePackageServiceTest { return signature.sign(); } + private byte[] payloadZip(Map required, Map optional) throws IOException { + Map all = new HashMap<>(); + all.putAll(required); + all.putAll(optional); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ZipOutputStream outputStream = new ZipOutputStream(bytes)) { + for (Map.Entry 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 { StringWriter writer = new StringWriter(); try (PemWriter pemWriter = new PemWriter(writer)) {