diff --git a/device-fingerprint-tool/pom.xml b/device-fingerprint-tool/pom.xml index 76be577..546f370 100644 --- a/device-fingerprint-tool/pom.xml +++ b/device-fingerprint-tool/pom.xml @@ -6,7 +6,7 @@ com.sunyard.cisd device-fingerprint-tool - 1.0.0 + 1.0.1 device-fingerprint-tool 设备指纹信息获取工具 diff --git a/device-fingerprint-tool/src/main/java/com/sunyard/cisd/device/tool/Main.java b/device-fingerprint-tool/src/main/java/com/sunyard/cisd/device/tool/Main.java index db27117..651b26a 100644 --- a/device-fingerprint-tool/src/main/java/com/sunyard/cisd/device/tool/Main.java +++ b/device-fingerprint-tool/src/main/java/com/sunyard/cisd/device/tool/Main.java @@ -10,24 +10,27 @@ import java.util.Map; public class Main { private static final String DEVICE_DIR = "/home/tms/device"; - private static final String SIGN_FILE = DEVICE_DIR + "/dev.sign"; + public static final String SIGN_FILE = DEVICE_DIR + "/dev.sign"; private static final String SIGN_SM3_FILE = DEVICE_DIR + "/dev.sign.sm3"; private static final String PUBLIC_KEY_FILE = DEVICE_DIR + "/dev.fingerprint.pub"; private static final String PUBLIC_KEY_SM3_FILE = DEVICE_DIR + "/dev.fingerprint.pub.sm3"; - private static final String FINGERPRINT_DATA_FILE = DEVICE_DIR + "/dev.fingerprint.data"; + public static final String FINGERPRINT_DATA_FILE = DEVICE_DIR + "/dev.fingerprint.data"; private static final String FINGERPRINT_PRIVATE_KEY_PATH = "/fingerprint_sm2_private_key.pem"; - private static final String FINGERPRINT_PUBLIC_KEY_PATH = "/fingerprint_sm2_public_key.pem"; + public static final String FINGERPRINT_PUBLIC_KEY_PATH = "/fingerprint_sm2_public_key.pem"; private static final String MAC_PRIVATE_KEY_PATH = "/mac_sm2_private_key.pem"; - private static final String MAC_PUBLIC_KEY_PATH = "/mac_sm2_public_key.pem"; + public static final String MAC_PUBLIC_KEY_PATH = "/mac_sm2_public_key.pem"; private static final String ROOT_CERT_FILE = "/CFCA_TEST_CS_SM2_CA.cer"; private static final String ROOT_CERT_OUTPUT_FILE = DEVICE_DIR + "/CFCA_TEST_CS_SM2_CA.cer"; private static final String ROOT_CERT_SIGN_FILE = DEVICE_DIR + "/rootcert.sign"; private static final String MAC_PUBLIC_KEY_OUTPUT_FILE = DEVICE_DIR + "/dev.mac.pub"; private static final String MAC_PUBLIC_KEY_SM3_FILE = DEVICE_DIR + "/dev.mac.pub.sm3"; - private static final String MAC_LIST_FILE = "/home/tms/mac.list"; - private static final String MAC_SIGN_FILE = DEVICE_DIR + "/dev.mac.sign"; + public static final String MAC_LIST_FILE = "/home/tms/mac.list"; + public static final String MAC_SIGN_FILE = DEVICE_DIR + "/dev.mac.sign"; - private static final String[] MAC_TARGETS = { + public static final String CMEP_MAC_LIST_FILE = "/home/cmep4i/cmep.mac.list"; + public static final String CMEP_MAC_SIGN_FILE = "/home/cmep4i/cmep.mac.sign"; + + public static final String[] MAC_TARGETS = { "/home/tms/tms-framework.jar", "/home/tms/config", "/home/tms/libs", @@ -36,6 +39,11 @@ public class Main { "/home/tms/scripts/tms.sh" }; + public static final String[] CMEP_TARGETS = { + "/home/cmep4i/cmsp/CMEP-CMSP.jar", + "/home/cmep4i/cmtp/CMEP-CMSP.jar" + }; + public static void main(String[] args) { if (args.length == 0) { printHelp(); @@ -59,12 +67,18 @@ public class Main { case "fingerprint-vertify": verifyFingerprint(); break; - case "mac-gen": + case "tms-mac-gen": generateMAC(); break; - case "mac-vertify": + case "tms-mac-vertify": verifyMAC(); break; + case "cmep-mac-gen": + generateCMEPMAC(); + break; + case "cmep-mac-vertify": + verifyCMEPMAC(); + break; case "rootcert-mac-gen": generateRootCertMAC(); break; @@ -90,8 +104,10 @@ public class Main { System.out.println(" info - 获取并显示设备信息"); System.out.println(" fingerprint-gen - 获取设备信息并生成设备指纹输出到文件"); System.out.println(" fingerprint-vertify - 获取设备信息并验证设备指纹"); - System.out.println(" mac-gen - 生成系统文件MAC校验值并签名"); - System.out.println(" mac-vertify - 验证系统文件MAC校验值"); + System.out.println(" tms-mac-gen - 生成TMS系统文件MAC校验值并签名"); + System.out.println(" tms-mac-vertify - 验证TMS系统文件MAC校验值"); + System.out.println(" cmep-mac-gen - 生成CMEP文件MAC校验值并签名"); + System.out.println(" cmep-mac-vertify - 验证CMEP文件MAC校验值"); System.out.println(" rootcert-mac-gen - 生成根证书MAC签名"); System.out.println(" rootcert-mac-vertify - 验证根证书MAC签名"); System.out.println(); @@ -111,7 +127,7 @@ public class Main { System.out.println("硬盘序列号: " + fingerprint.getHardDiskSerialNumber()); } - private static String buildPlainText(DeviceFingerprint fingerprint) { + public static String buildPlainText(DeviceFingerprint fingerprint) { return fingerprint.getMotherboardSerialNumber() + fingerprint.getDeviceSerialNumber() + fingerprint.getCpuModel() + @@ -316,8 +332,7 @@ public class Main { throw new IllegalArgumentException("根证书文件未找到: " + ROOT_CERT_FILE); } - byte[] certData = certInputStream.readAllBytes(); - certInputStream.close(); + byte[] certData = readInputStream(certInputStream); SM3Util.writeFile(ROOT_CERT_OUTPUT_FILE, certData); System.out.println("根证书已输出: " + ROOT_CERT_OUTPUT_FILE); @@ -353,8 +368,7 @@ public class Main { if (certInputStream == null) { throw new IllegalArgumentException("根证书文件未找到: " + ROOT_CERT_FILE); } - byte[] certData = certInputStream.readAllBytes(); - certInputStream.close(); + byte[] certData = readInputStream(certInputStream); byte[] signature = SM3Util.readFileBytes(ROOT_CERT_SIGN_FILE); @@ -365,4 +379,120 @@ public class Main { System.out.println("=== 验证结果 ==="); System.out.println("总体验证: " + (publicKeyValid && signatureVerified)); } + + private static byte[] readInputStream(InputStream is) throws Exception { + try { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int len; + while ((len = is.read(buffer)) != -1) { + baos.write(buffer, 0, len); + } + return baos.toByteArray(); + } finally { + if (is != null) { + is.close(); + } + } + } + + private static void generateCMEPMAC() throws Exception { + System.out.println("=== 生成CMEP文件MAC校验值 ==="); + System.out.println("目标路径:"); + for (String target : CMEP_TARGETS) { + System.out.println(" " + target); + } + + Map sm3Map = new java.util.LinkedHashMap<>(); + + for (String target : CMEP_TARGETS) { + File file = new File(target); + if (!file.exists()) { + System.out.println("警告: 路径不存在 - " + target); + continue; + } + + if (file.isFile()) { + String sm3Value = SM3Util.sm3FileHex(target); + sm3Map.put(target, sm3Value); + System.out.println("文件: " + target + " -> " + sm3Value); + } + } + + StringBuilder macListContent = new StringBuilder(); + for (Map.Entry entry : sm3Map.entrySet()) { + macListContent.append(entry.getKey()).append("|").append(entry.getValue()).append("\n"); + } + + SM3Util.writeFile(CMEP_MAC_LIST_FILE, macListContent.toString()); + System.out.println("\nCMEP MAC列表文件已保存: " + CMEP_MAC_LIST_FILE); + + byte[] signature = SM2Util.sign(macListContent.toString().getBytes(StandardCharsets.UTF_8), MAC_PRIVATE_KEY_PATH); + SM3Util.writeFile(CMEP_MAC_SIGN_FILE, signature); + System.out.println("CMEP MAC签名文件已保存: " + CMEP_MAC_SIGN_FILE); + + PublicKey publicKey = SM2Util.loadPublicKey(MAC_PUBLIC_KEY_PATH); + boolean verified = SM2Util.verify(macListContent.toString().getBytes(StandardCharsets.UTF_8), signature, publicKey); + System.out.println("\n签名验证结果: " + verified); + } + + private static void verifyCMEPMAC() throws Exception { + System.out.println("=== 验证CMEP文件MAC校验值 ==="); + + PublicKey publicKey = SM2Util.loadPublicKey(MAC_PUBLIC_KEY_PATH); + byte[] savedSignature = SM3Util.readFileBytes(CMEP_MAC_SIGN_FILE); + String savedMacList = SM3Util.readFile(CMEP_MAC_LIST_FILE); + + boolean macListSignatureValid = SM2Util.verify(savedMacList.getBytes(StandardCharsets.UTF_8), savedSignature, publicKey); + System.out.println("CMEP MAC列表签名验证: " + (macListSignatureValid ? "通过" : "失败")); + + if (!macListSignatureValid) { + System.out.println("错误: cmep.mac.list 签名验证失败,可能被篡改!"); + System.out.println("\n=== 验证结果 ==="); + System.out.println("完整性验证: false"); + return; + } + + Map savedSm3Map = SM3Util.readSM3File(CMEP_MAC_LIST_FILE); + + Map currentSm3Map = new java.util.LinkedHashMap<>(); + for (String target : CMEP_TARGETS) { + File file = new File(target); + if (!file.exists()) { + continue; + } + + if (file.isFile()) { + String sm3Value = SM3Util.sm3FileHex(target); + currentSm3Map.put(target, sm3Value); + } + } + + boolean allMatch = true; + + for (Map.Entry entry : savedSm3Map.entrySet()) { + String filePath = entry.getKey(); + String savedHash = entry.getValue(); + String currentHash = currentSm3Map.get(filePath); + + if (currentHash == null) { + System.out.println("文件缺失: " + filePath); + allMatch = false; + } else if (!savedHash.equals(currentHash)) { + System.out.println("文件被修改: " + filePath + " (期望: " + savedHash + ", 实际: " + currentHash + ")"); + allMatch = false; + } + } + + for (String filePath : currentSm3Map.keySet()) { + if (!savedSm3Map.containsKey(filePath)) { + System.out.println("新增文件: " + filePath); + allMatch = false; + } + } + + System.out.println(); + System.out.println("=== 验证结果 ==="); + System.out.println("完整性验证: " + allMatch); + } } diff --git a/pom.xml b/pom.xml index ef7d946..4b97e1a 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,8 @@ 3.5.10.1 2.8.5 5.17.0 + UTF-8 + UTF-8 @@ -119,7 +121,7 @@ com.sunyard.cisd device-fingerprint-tool - 1.0.0 + 1.0.1 diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..84cc32f --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,43 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +projectJDK: "17" #(Applied in CI/CD pipeline) + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +# Quality gate. Will fail the CI/CD pipeline if any condition is not met +# severityThresholds - configures maximum thresholds for different problem severities +# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code +# Code Coverage is available in Ultimate and Ultimate Plus plans +#failureConditions: +# severityThresholds: +# any: 15 +# critical: 5 +# testCoverageThresholds: +# fresh: 70 +# total: 50 + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-jvm:2025.2 diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/Gm0018AlgorithmIds.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/Gm0018AlgorithmIds.java index 4e18a5e..decdeb8 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/Gm0018AlgorithmIds.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/Gm0018AlgorithmIds.java @@ -32,6 +32,7 @@ public final class Gm0018AlgorithmIds { public static final int RSA_ENCRYPT = 0x00010002; public static final int SM2_SIGN = 0x00020100; + public static final int SM2_SIGN_1 = 0x00020200; public static final int SM2_KEY_EXCHANGE = 0x00020200; public static final int SM2_ENCRYPT = 0x00020400; diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/EccRefPublicKey.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/EccRefPublicKey.java index 39615d5..4ae3726 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/EccRefPublicKey.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/EccRefPublicKey.java @@ -1,6 +1,16 @@ package com.cisd.tms.integration.crypto.pcie.jna; import com.sun.jna.Structure; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; + +import java.security.PublicKey; +import java.security.Security; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.util.Arrays; import java.util.List; @Structure.FieldOrder({"bits", "x", "y"}) @@ -17,4 +27,62 @@ public class EccRefPublicKey extends Structure { protected List getFieldOrder() { return List.of("bits", "x", "y"); } + public static byte[] fromPublicKeyToBlob(PublicKey publicKey){ + EccRefPublicKey eccRefPublicKey = EccRefPublicKey.fromPublicKey(publicKey); + return toBytes(eccRefPublicKey); + } + + private static byte[] toBytes(Structure structure) { + structure.write(); + return structure.getPointer().getByteArray(0, structure.size()); + } + + public static EccRefPublicKey fromPublicKey(PublicKey publicKey) { + try { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + + ECNamedCurveParameterSpec bcSpec = ECNamedCurveTable.getParameterSpec("sm2p256v1"); + ECNamedCurveSpec spec = new ECNamedCurveSpec("sm2p256v1", bcSpec.getCurve(), bcSpec.getG(), bcSpec.getN()); + + ECPoint w = ((java.security.interfaces.ECPublicKey) publicKey).getW(); + + EccRefPublicKey result = new EccRefPublicKey(); + result.bits = 256; + + byte[] xBytes = w.getAffineX().toByteArray(); + byte[] yBytes = w.getAffineY().toByteArray(); + + // 填充 x 坐标 + int xOffset = result.x.length - xBytes.length; + if (xOffset >= 0) { + System.arraycopy(xBytes, 0, result.x, xOffset, xBytes.length); + } else { + // 如果字节太长,去掉前面的0 + int start = 0; + while (start < xBytes.length && xBytes[start] == 0) { + start++; + } + System.arraycopy(xBytes, start, result.x, result.x.length - (xBytes.length - start), xBytes.length - start); + } + + // 填充 y 坐标 + int yOffset = result.y.length - yBytes.length; + if (yOffset >= 0) { + System.arraycopy(yBytes, 0, result.y, yOffset, yBytes.length); + } else { + // 如果字节太长,去掉前面的0 + int start = 0; + while (start < yBytes.length && yBytes[start] == 0) { + start++; + } + System.arraycopy(yBytes, start, result.y, result.y.length - (yBytes.length - start), yBytes.length - start); + } + + return result; + } catch (Exception e) { + throw new IllegalArgumentException("Failed to convert PublicKey to EccRefPublicKey", e); + } + } } diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/PcieNativeLibrary.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/PcieNativeLibrary.java index 7bfbc43..6b26f85 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/PcieNativeLibrary.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/PcieNativeLibrary.java @@ -15,6 +15,7 @@ public interface PcieNativeLibrary extends Library { int SDF_CloseSession(Pointer hSessionHandle); + int SDF_GetPrivateKeyAccessRight(Pointer hSessionHandle, int uiKeyIndex, byte[] pucPassword, int uiPwdLength); int SDF_ReleasePrivateKeyAccessRight(Pointer hSessionHandle, int uiKeyIndex); diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/ExternalSm2VerifyRequest.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/ExternalSm2VerifyRequest.java new file mode 100644 index 0000000..3192b8b --- /dev/null +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/ExternalSm2VerifyRequest.java @@ -0,0 +1,61 @@ +package com.cisd.tms.integration.crypto.pcie.model; + +/** + * ExternalSm2VerifyRequest + * 外部 SM2 验签请求参数 + * 包含数据的 SM3 哈希处理(带公钥)和验签逻辑 + */ +public class ExternalSm2VerifyRequest { + + /** + * 外部公钥(原始字节,如从证书中获取的 PublicKey.getEncoded()) + */ + private byte[] publicKey; + + /** + * 待验签的数据 + */ + private byte[] data; + + /** + * 签名值(DER 格式或原始格式的签名) + */ + private byte[] signature; + + /** + * SM2 用户 ID(可选,默认 "1234567812345678") + */ + private byte[] userId; + + public byte[] getPublicKey() { + return publicKey; + } + + public void setPublicKey(byte[] publicKey) { + this.publicKey = publicKey; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + + public byte[] getSignature() { + return signature; + } + + public void setSignature(byte[] signature) { + this.signature = signature; + } + + public byte[] getUserId() { + return userId; + } + + public void setUserId(byte[] userId) { + this.userId = userId; + } +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java index 586683d..60049e9 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java @@ -2624,4 +2624,4 @@ public class JnaPcieCryptoService implements PcieCryptoService { } return Arrays.copyOf(userId, userId.length); } -} +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java index 2d1676e..6d38137 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java @@ -58,7 +58,8 @@ import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor -public class AuthServiceImpl implements AuthService { +public class +AuthServiceImpl implements AuthService { private static final int MAX_FAILED_ATTEMPTS = 5; private static final int IDLE_TIMEOUT_MINUTES = 10; diff --git a/src/main/java/com/cisd/tms/modules/cert/enums/CertUsage.java b/src/main/java/com/cisd/tms/modules/cert/enums/CertUsage.java new file mode 100644 index 0000000..b9caf7e --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/enums/CertUsage.java @@ -0,0 +1,16 @@ +package com.cisd.tms.modules.cert.enums; + +/** + * 证书用途 + */ +public enum CertUsage { + digitalSignature, + nonRepudiation, + keyEncipherment, + dataEncipherment, + keyAgreement, + keyCertSign, + cRLSign, + encipherOnly, + decipherOnly +} diff --git a/src/main/java/com/cisd/tms/modules/cert/repository/CertificateRepository.java b/src/main/java/com/cisd/tms/modules/cert/repository/CertificateRepository.java index 501a4cb..6be1b0d 100644 --- a/src/main/java/com/cisd/tms/modules/cert/repository/CertificateRepository.java +++ b/src/main/java/com/cisd/tms/modules/cert/repository/CertificateRepository.java @@ -10,8 +10,10 @@ public interface CertificateRepository { void deleteById(Long id); Optional findById(Long id); Optional findByFingerprint(String fingerprint); + Optional findBySubjectDn(String subjectDn); Optional findByIssuerDnAndSerialNumber(String issuerDn, String serialNumber); boolean existsEntityCertByEntityId(Long entityId); long countByIssuerDn(String issuerDn); Page page(CertificateListRequest request); } + diff --git a/src/main/java/com/cisd/tms/modules/cert/repository/impl/CertificateRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/cert/repository/impl/CertificateRepositoryImpl.java index 77bddd9..048230e 100644 --- a/src/main/java/com/cisd/tms/modules/cert/repository/impl/CertificateRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/cert/repository/impl/CertificateRepositoryImpl.java @@ -49,6 +49,15 @@ public class CertificateRepositoryImpl implements CertificateRepository { return Optional.ofNullable(mapper.selectOne(wrapper)); } + @Override + public Optional findBySubjectDn(String subjectDn) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(CertificateEntity::getSubjectDn, subjectDn) + .orderByDesc(CertificateEntity::getImportTime) + .last("LIMIT 1"); + return Optional.ofNullable(mapper.selectOne(wrapper)); + } + @Override public Optional findByIssuerDnAndSerialNumber(String issuerDn, String serialNumber) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() @@ -85,11 +94,9 @@ public class CertificateRepositoryImpl implements CertificateRepository { wrapper.eq(CertificateEntity::getAlgoType, request.getAlgoType().toUpperCase()); } if (StringUtils.hasText(request.getCertType())) { - wrapper.eq(CertificateEntity::getCertType, request.getCertType().toUpperCase()); - } - if (StringUtils.hasText(request.getSubject())) { - wrapper.like(CertificateEntity::getSubjectDn, request.getSubject()); + wrapper.eq(CertificateEntity::getCertType, request.getCertType()); } + wrapper.orderByDesc(CertificateEntity::getImportTime); long total = mapper.selectCount(wrapper); long offset = (long) (request.getPageNum() - 1) * request.getPageSize(); @@ -100,3 +107,4 @@ public class CertificateRepositoryImpl implements CertificateRepository { return page; } } + diff --git a/src/main/java/com/cisd/tms/modules/cert/service/CertificateService.java b/src/main/java/com/cisd/tms/modules/cert/service/CertificateService.java index 69915fc..cc0fc72 100644 --- a/src/main/java/com/cisd/tms/modules/cert/service/CertificateService.java +++ b/src/main/java/com/cisd/tms/modules/cert/service/CertificateService.java @@ -11,6 +11,7 @@ import com.cisd.tms.modules.cert.entity.CertificateEntity; import com.cisd.tms.modules.cert.repository.CertificateRepository; import com.cisd.tms.modules.cert.support.CertPemSupport; import com.cisd.tms.modules.cert.support.CertRuntimeStatusResolver; +import com.cisd.tms.modules.cert.support.CertUtil; import org.bouncycastle.asn1.x509.KeyPurposeId; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -18,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.security.cert.X509Certificate; import java.time.LocalDateTime; import java.time.ZoneId; @@ -59,13 +61,26 @@ public class CertificateService { result.setRecords(page.getRecords().stream().map(this::toItem).toList()); return result; } - @Transactional public ImportCertificateResponse importCertificate(MultipartFile file) { - try { - //todo file空值判断 + if ( null == file || file.isEmpty() || file.getSize() < 1) { + throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书文件为空"); + } - X509Certificate certificate = CertPemSupport.parseCertificate(file.getBytes()); + byte[] data = null; + try { + data = file.getBytes(); + } catch (IOException e) { + throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书文件读取时 IO 异常"); + } + + return importCertificate(data); + } + + @Transactional + public ImportCertificateResponse importCertificate(byte[] fileData) { + try { + X509Certificate certificate = CertPemSupport.parseCertificate(fileData); // 终端证书入库前先固定 PKI 边界:只接受当前有效、非 CA、用途满足业务认证的实体/用户证书。 assertValidTime(certificate); assertEndEntityCertificate(certificate); @@ -200,6 +215,13 @@ public class CertificateService { .orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书不存在")); } + public X509Certificate getBySubjectDn(String dn) { + CertificateEntity cert = certificateRepository.findBySubjectDn(dn) + .orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书不存在")); + return CertUtil.convertToX509Cert( cert.getCertData() ); + } + + private void assertValidTime(X509Certificate certificate) { LocalDateTime now = LocalDateTime.now(); LocalDateTime notBefore = LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault()); diff --git a/src/main/java/com/cisd/tms/modules/cert/service/EntityService.java b/src/main/java/com/cisd/tms/modules/cert/service/EntityService.java index 62daed4..c6ac026 100644 --- a/src/main/java/com/cisd/tms/modules/cert/service/EntityService.java +++ b/src/main/java/com/cisd/tms/modules/cert/service/EntityService.java @@ -376,7 +376,7 @@ public class EntityService { } } - private static final class CardContentSigner implements ContentSigner { + public static final class CardContentSigner implements ContentSigner { private final java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream(); private final int keyIdx; diff --git a/src/main/java/com/cisd/tms/modules/cert/service/TrustedCertService.java b/src/main/java/com/cisd/tms/modules/cert/service/TrustedCertService.java index 7f1a7a2..b9385a0 100644 --- a/src/main/java/com/cisd/tms/modules/cert/service/TrustedCertService.java +++ b/src/main/java/com/cisd/tms/modules/cert/service/TrustedCertService.java @@ -21,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.Security; import java.security.cert.X509CRL; @@ -83,8 +84,24 @@ public class TrustedCertService { @Transactional public ImportCertificateResponse importTrusted(MultipartFile file, String alias) { + if ( null == file || file.isEmpty() || file.getSize() < 1) { + throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书文件为空"); + } + + byte[] data = null; try { - X509Certificate certificate = CertPemSupport.parseCertificate(file.getBytes()); + data = file.getBytes(); + } catch (IOException e) { + throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书文件读取时 IO 异常"); + } + + return importTrusted(data, alias); + } + + @Transactional + public ImportCertificateResponse importTrusted(byte[] fileData, String alias) { + try { + X509Certificate certificate = CertPemSupport.parseCertificate(fileData); // 可信库只保存 CA 证书;终端证书必须走 CertificateService,避免信任锚和业务证书混用。 assertValidTime(certificate); assertTrustedCaCertificate(certificate); diff --git a/src/main/java/com/cisd/tms/modules/cert/support/CertUtil.java b/src/main/java/com/cisd/tms/modules/cert/support/CertUtil.java new file mode 100644 index 0000000..7124cab --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/support/CertUtil.java @@ -0,0 +1,84 @@ +package com.cisd.tms.modules.cert.support; + + +import com.cisd.tms.modules.cert.enums.CertUsage; +import lombok.SneakyThrows; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.HashSet; +import java.util.Set; +import java.util.Base64; + + +public class CertUtil { + static { + Security.addProvider(new BouncyCastleProvider()); + } + + @SneakyThrows + public static X509Certificate convertToX509Cert(String certificateString) { +// X509Certificate certificate = null; +// CertificateFactory cf = null; +// try { +// if (certificateString != null && !certificateString.trim().isEmpty()) { +// certificateString = certificateString.replace("-----BEGIN CERTIFICATE-----\n", "") +// .replace("-----END CERTIFICATE-----", ""); // NEED FOR PEM FORMAT CERT STRING +// byte[] certificateData = Base64.decode(certificateString); +// cf = CertificateFactory.getInstance("X509"); +// certificate = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(certificateData)); +// } +// } catch (CertificateException e) { +// throw new CertificateException(e); +// } +// return certificate; + CertificateFactory CF = CertificateFactory.getInstance("X.509", "BC"); // 从证书工厂中获取X.509的单例类 + return (X509Certificate) CF.generateCertificate(new ByteArrayInputStream(certificateString.getBytes())); // 将文件流的证书转化为证书类 + } + + public static String convertToPem(X509Certificate cert) throws CertificateEncodingException { + + String cert_begin = "-----BEGIN CERTIFICATE-----\n"; + String end_cert = "\n-----END CERTIFICATE-----"; + + byte[] derCert = cert.getEncoded(); + String pemCertPre = Base64.getMimeEncoder().encodeToString(derCert); + String pemCert = cert_begin + pemCertPre + end_cert; + return pemCert; + } + + + public static boolean certUsageCheck(String cert , CertUsage... us ) throws CertificateException, NoSuchProviderException { + if ( null == us || us.length == 0) { + return true; + } + + X509Certificate certificate = convertToX509Cert(cert); + boolean[] usage = certificate.getKeyUsage(); + + if ( null == usage ) { + return false; + } + + boolean checkRet = true; + Set uset = new HashSet(); + for ( CertUsage u : us ) { + uset.add( u.ordinal() ); + } + + for ( int i = 0; i < usage.length; i++ ) { + if ( usage[ i ] ) { + checkRet &= uset.contains( i ); + } + } + + return checkRet; + } + +} diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/IOpenApiService.java b/src/main/java/com/cisd/tms/modules/openapi/service/IOpenApiService.java new file mode 100644 index 0000000..78dcdc4 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/IOpenApiService.java @@ -0,0 +1,9 @@ +package com.cisd.tms.modules.openapi.service; + +public interface IOpenApiService { + public String rawSign(byte[] origBytes, String dn); + public boolean rawVerify(byte[] origBytes, String certStr, String dn); + public String dettachedSign(byte[] origBytes, String dn); + public String dettachedVerify(byte[] origBytes, String certStr); + public String dettachedVerifySimple(byte[] origBytes, String certStr); +} diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/controller/OpenApiController.java b/src/main/java/com/cisd/tms/modules/openapi/service/controller/OpenApiController.java new file mode 100644 index 0000000..17c50c8 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/controller/OpenApiController.java @@ -0,0 +1,185 @@ +package com.cisd.tms.modules.openapi.service.controller; + +import com.cisd.tms.common.api.ApiResponse; +import com.cisd.tms.modules.cert.dto.ImportCertificateResponse; +import com.cisd.tms.modules.cert.service.CertificateService; +import com.cisd.tms.modules.openapi.service.dto.DettachedSignRequest; +import com.cisd.tms.modules.openapi.service.dto.DettachedVerifyRequest; +import com.cisd.tms.modules.openapi.service.dto.RawSignRequest; +import com.cisd.tms.modules.openapi.service.dto.RawSignResponse; +import com.cisd.tms.modules.openapi.service.dto.RawVerifyRequest; +import com.cisd.tms.modules.openapi.service.dto.RawVerifyResponse; +import com.cisd.tms.modules.openapi.service.dto.UploadCertRequest; +import com.cisd.tms.modules.openapi.service.impl.OpenApiService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Base64; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/openapi") +@Tag(name = "OpenApi 管理", description = "对外开放接口") +public class OpenApiController { + + private static final Logger log = LoggerFactory.getLogger(OpenApiController.class); + + private final OpenApiService openApiService; + private final CertificateService certificateService; + + + @PostMapping("/rawSign") + @Operation(summary = "裸签名", description = "基于 SM3 SM2 算法进行签名,返回 Base64 编码的签名值") + public ApiResponse rawSign(@Valid @RequestBody RawSignRequest request) { + log.info("收到裸签名请求, DN: {}, sessionId: {}", request.getDn(), request.getSessionId()); + + // 入参检查 + if (request.getOrigBytes() == null || request.getOrigBytes().trim().isEmpty()) { + log.error("签名请求失败:待签名数据为空"); + return ApiResponse.fail(400, "待签名数据不能为空", null); + } + if (request.getDn() == null || request.getDn().trim().isEmpty()) { + log.error("签名请求失败:证书DN为空"); + return ApiResponse.fail(400, "证书DN不能为空", null); + } + + try { + // Base64 解码原始数据 + byte[] origBytes = Base64.getDecoder().decode(request.getOrigBytes()); + log.info("待签名数据长度: {} bytes", origBytes.length); + + // 调用签名服务 + String signature = openApiService.rawSign(origBytes, request.getDn()); + + log.info("签名成功"); + return ApiResponse.success(new RawSignResponse(signature)); + } catch (IllegalArgumentException e) { + log.error("签名请求参数错误: {}", e.getMessage()); + return ApiResponse.fail(400, e.getMessage(), null); + } catch (Exception e) { + log.error("签名失败: {}", e.getMessage(), e); + return ApiResponse.fail(500, "签名失败: " + e.getMessage(), null); + } + } + + @PostMapping("/rawVerify") + @Operation(summary = "裸验签", description = "基于 SM3 SM2 算法进行验签,验证签名的有效性") + public ApiResponse rawVerify(@Valid @RequestBody RawVerifyRequest request) { + log.info("收到裸验签请求, DN: {}, sessionId: {}", request.getDn(), request.getSessionId()); + + // 入参检查 + if (request.getOrigBytes() == null || request.getOrigBytes().trim().isEmpty()) { + log.error("验签请求失败:待验签数据为空"); + return ApiResponse.fail(400, "待验签数据不能为空", null); + } + if (request.getSignature() == null || request.getSignature().trim().isEmpty()) { + log.error("验签请求失败:签名值为空"); + return ApiResponse.fail(400, "签名值不能为空", null); + } + if (request.getDn() == null || request.getDn().trim().isEmpty()) { + log.error("验签请求失败:证书DN为空"); + return ApiResponse.fail(400, "证书DN不能为空", null); + } + + try { + // Base64 解码原始数据 + byte[] origBytes = Base64.getDecoder().decode(request.getOrigBytes()); + log.info("待验签数据长度: {} bytes", origBytes.length); + log.info("签名数据长度: {} bytes", request.getSignature().length()); + + // 调用验签服务 + boolean verified = openApiService.rawVerify(origBytes, request.getSignature(), request.getDn()); + + if (verified) { + log.info("验签成功"); + return ApiResponse.success(RawVerifyResponse.success(request.getDn())); + } else { + log.warn("验签失败"); + return ApiResponse.success(RawVerifyResponse.failure(request.getDn(), "签名验证失败")); + } + } catch (IllegalArgumentException e) { + log.error("验签请求参数错误: {}", e.getMessage()); + return ApiResponse.fail(400, e.getMessage(), null); + } catch (Exception e) { + log.error("验签失败: {}", e.getMessage(), e); + return ApiResponse.success(RawVerifyResponse.failure(request.getDn(), e.getMessage())); + } + } + + @PostMapping("/dettachedSign") + @Operation(summary = "分离签名", description = "基于 SM3 SM2 算法进行分离签名,返回 Base64 编码的签名值") + public ApiResponse dettachedSign(@Valid @RequestBody DettachedSignRequest request) { + log.info("收到分离签名请求, DN: {}, sessionId: {}", request.getDn(), request.getSessionId()); + + RawSignRequest rawSignRequest = new RawSignRequest(); + rawSignRequest.setDn(request.getDn()); + rawSignRequest.setOrigBytes(request.getOrigBytes()); + rawSignRequest.setSessionId(request.getSessionId()); + + return rawSign(rawSignRequest); + } + + @PostMapping("/dettachedVerify") + @Operation(summary = "分离验签", description = "基于 SM3 SM2 算法进行分离验签,验证签名的有效性") + public ApiResponse dettachedVerify(@Valid @RequestBody DettachedVerifyRequest request) { + log.info("收到分离验签请求, DN: {}, sessionId: {}", request.getDn(), request.getSessionId()); + + RawVerifyRequest rawVerifyRequest = new RawVerifyRequest(); + rawVerifyRequest.setDn(request.getDn()); + rawVerifyRequest.setOrigBytes(request.getOrigBytes()); + rawVerifyRequest.setSignature(request.getSignature()); + rawVerifyRequest.setSessionId(request.getSessionId()); + + return rawVerify(rawVerifyRequest); + } + + @PostMapping("/dettachedVerifySimple") + @Operation(summary = "分离验签(简化版)", description = "基于 SM3 SM2 算法进行分离验签,验证签名的有效性(简化版)") + public ApiResponse dettachedVerifySimple(@Valid @RequestBody DettachedVerifyRequest request) { + log.info("收到分离验签(简化版)请求, DN: {}, sessionId: {}", request.getDn(), request.getSessionId()); + + RawVerifyRequest rawVerifyRequest = new RawVerifyRequest(); + rawVerifyRequest.setDn(request.getDn()); + rawVerifyRequest.setOrigBytes(request.getOrigBytes()); + rawVerifyRequest.setSignature(request.getSignature()); + rawVerifyRequest.setSessionId(request.getSessionId()); + + return rawVerify(rawVerifyRequest); + } + + @PostMapping("/uploadCert") + @Operation(summary = "上传证书", description = "上传并导入证书") + public ApiResponse uploadCert(@Valid @RequestBody UploadCertRequest request) { + log.info("收到上传证书请求, sessionId: {}", request.getSessionId()); + + if (request.getCertData() == null || request.getCertData().trim().isEmpty()) { + log.error("上传证书请求失败:证书数据为空"); + return ApiResponse.fail(400, "证书数据不能为空", null); + } + + try { + byte[] certBytes = Base64.getDecoder().decode(request.getCertData()); + log.info("证书数据长度: {} bytes", certBytes.length); + + ImportCertificateResponse response = certificateService.importCertificate(certBytes); + + log.info("证书上传成功, certId: {}, subjectDn: {}", response.getCertId(), response.getSubjectDn()); + return ApiResponse.success(response); + } catch (IllegalArgumentException e) { + log.error("上传证书请求参数错误: {}", e.getMessage()); + return ApiResponse.fail(400, e.getMessage(), null); + } catch (Exception e) { + log.error("证书上传失败: {}", e.getMessage(), e); + return ApiResponse.fail(500, "证书上传失败: " + e.getMessage(), null); + } + } + +} diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/dto/DettachedSignRequest.java b/src/main/java/com/cisd/tms/modules/openapi/service/dto/DettachedSignRequest.java new file mode 100644 index 0000000..2206edc --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/dto/DettachedSignRequest.java @@ -0,0 +1,20 @@ +package com.cisd.tms.modules.openapi.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "分离签名请求") +public class DettachedSignRequest { + @NotBlank(message = "证书DN不能为空") + @Schema(description = "证书DN", example = "CN=Initial Entity,OU=Initial OU,O=Initial Org,L=Beijing,ST=Beijing,C=CN") + private String dn; + + @NotBlank(message = "待签名原文不能为空") + @Schema(description = "待签名原文的Base64编码", example = "dGVzdCBtZXNzYWdl") + private String origBytes; + + @Schema(description = "会话ID", example = "session-12345") + private String sessionId; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/dto/DettachedVerifyRequest.java b/src/main/java/com/cisd/tms/modules/openapi/service/dto/DettachedVerifyRequest.java new file mode 100644 index 0000000..c24c871 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/dto/DettachedVerifyRequest.java @@ -0,0 +1,11 @@ +package com.cisd.tms.modules.openapi.service.dto; + +import lombok.Data; + +@Data +public class DettachedVerifyRequest { + private String dn; + private String origBytes; + private String sessionId; + private String signature; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/dto/GenericCertificate.java b/src/main/java/com/cisd/tms/modules/openapi/service/dto/GenericCertificate.java new file mode 100644 index 0000000..3472792 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/dto/GenericCertificate.java @@ -0,0 +1,64 @@ +package com.cisd.tms.modules.openapi.service.dto; + +import java.security.cert.X509Certificate; + +/** + * 通用证书类 + */ +public class GenericCertificate { + + private X509Certificate certificate; + private String dn; + private String issuer; + private String serialNumber; + + public GenericCertificate(X509Certificate certificate) { + this.certificate = certificate; + if (certificate != null) { + this.dn = certificate.getSubjectDN().getName(); + this.issuer = certificate.getIssuerDN().getName(); + this.serialNumber = certificate.getSerialNumber().toString(); + } + } + + public X509Certificate getCertificate() { + return certificate; + } + + public void setCertificate(X509Certificate certificate) { + this.certificate = certificate; + } + + public String getDn() { + return dn; + } + + public void setDn(String dn) { + this.dn = dn; + } + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public String getSerialNumber() { + return serialNumber; + } + + public void setSerialNumber(String serialNumber) { + this.serialNumber = serialNumber; + } + + @Override + public String toString() { + return "GenericCertificate{" + + "dn='" + dn + '\'' + + ", issuer='" + issuer + '\'' + + ", serialNumber='" + serialNumber + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/dto/RawSignRequest.java b/src/main/java/com/cisd/tms/modules/openapi/service/dto/RawSignRequest.java new file mode 100644 index 0000000..8fbf51a --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/dto/RawSignRequest.java @@ -0,0 +1,33 @@ +package com.cisd.tms.modules.openapi.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * RawSignRequest + * 签名请求参数 + */ +@Data +@Schema(description = "签名请求") +public class RawSignRequest { + /** + * 证书 DN(Distinguished Name) + */ + @NotBlank(message = "证书DN不能为空") + @Schema(description = "证书DN", example = "CN=Initial Entity,OU=Initial OU,O=Initial Org,L=Beijing,ST=Beijing,C=CN") + private String dn; + + /** + * 待签名原文的 Base64 编码 + */ + @NotBlank(message = "待签名原文不能为空") + @Schema(description = "待签名原文的Base64编码", example = "dGVzdCBtZXNzYWdl") + private String origBytes; + + /** + * 会话ID(可选) + */ + @Schema(description = "会话ID", example = "session-12345") + private String sessionId; +} diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/dto/RawSignResponse.java b/src/main/java/com/cisd/tms/modules/openapi/service/dto/RawSignResponse.java new file mode 100644 index 0000000..b9f0dd8 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/dto/RawSignResponse.java @@ -0,0 +1,13 @@ +package com.cisd.tms.modules.openapi.service.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class RawSignResponse { + /** + * 裸签名串(PKCS#1 格式,Base64 编码) + */ + private String signature; +} diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/dto/RawVerifyRequest.java b/src/main/java/com/cisd/tms/modules/openapi/service/dto/RawVerifyRequest.java new file mode 100644 index 0000000..0a6f39b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/dto/RawVerifyRequest.java @@ -0,0 +1,19 @@ +package com.cisd.tms.modules.openapi.service.dto; + + +import lombok.Data; + +@Data +public class RawVerifyRequest { + /** + * 证书 DN 或机构号 + */ + private String dn; + private String origBytes; + private String sessionId; + /** + * 裸签名串(Base64) + */ + private String signature; + +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/dto/RawVerifyResponse.java b/src/main/java/com/cisd/tms/modules/openapi/service/dto/RawVerifyResponse.java new file mode 100644 index 0000000..c3186d2 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/dto/RawVerifyResponse.java @@ -0,0 +1,42 @@ +package com.cisd.tms.modules.openapi.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * RawVerifyResponse + * 验签响应结果 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "验签响应") +public class RawVerifyResponse { + /** + * 验签结果:true表示验签通过,false表示验签失败 + */ + @Schema(description = "验签结果", example = "true") + private boolean verified; + + /** + * 结果描述信息 + */ + @Schema(description = "结果描述", example = "验签成功") + private String message; + + /** + * 证书DN + */ + @Schema(description = "证书DN", example = "CN=Initial Entity,OU=Initial OU,O=Initial Org,L=Beijing,ST=Beijing,C=CN") + private String dn; + + public static RawVerifyResponse success(String dn) { + return new RawVerifyResponse(true, "验签成功", dn); + } + + public static RawVerifyResponse failure(String dn, String reason) { + return new RawVerifyResponse(false, "验签失败: " + reason, dn); + } +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/dto/UploadCertRequest.java b/src/main/java/com/cisd/tms/modules/openapi/service/dto/UploadCertRequest.java new file mode 100644 index 0000000..fe7bc6f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/dto/UploadCertRequest.java @@ -0,0 +1,16 @@ +package com.cisd.tms.modules.openapi.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "上传证书请求") +public class UploadCertRequest { + @NotBlank(message = "证书数据不能为空") + @Schema(description = "证书文件的Base64编码", example = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...") + private String certData; + + @Schema(description = "会话ID", example = "session-12345") + private String sessionId; +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/impl/OpenApiService.java b/src/main/java/com/cisd/tms/modules/openapi/service/impl/OpenApiService.java new file mode 100644 index 0000000..3da54c1 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/impl/OpenApiService.java @@ -0,0 +1,302 @@ +package com.cisd.tms.modules.openapi.service.impl; + +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.PcieSessionTemplate; +import com.cisd.tms.integration.crypto.pcie.jna.EccRefPublicKey; +import com.cisd.tms.integration.crypto.pcie.jna.EccSignature; +import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; +import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest; +import com.cisd.tms.integration.crypto.pcie.model.UserKeySm2SignRequest; +import com.cisd.tms.integration.crypto.pcie.service.JnaPcieCryptoService; +import com.cisd.tms.modules.cert.entity.KeyEntity; +import com.cisd.tms.modules.cert.service.CertificateService; +import com.cisd.tms.modules.cert.service.EntityService; +import com.cisd.tms.modules.openapi.service.IOpenApiService; +import com.sun.jna.ptr.IntByReference; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; + +@Service +@RequiredArgsConstructor +public class OpenApiService implements IOpenApiService { + + private static final byte[] DEFAULT_SM2_USER_ID = "1234567812345678".getBytes(StandardCharsets.UTF_8); + private static final Logger log = LoggerFactory.getLogger(OpenApiService.class); + + private final PcieSessionTemplate sessionTemplate; + private final JnaPcieCryptoService sdf; + + // 签名验签均从证书 dn 作为索引,所以引入证书服务类 + private final CertificateService certificateService; + // 签名依赖实体,所以引入实体服务类 + private final EntityService entityService; + + @Override + public String rawSign(byte[] origBytes, String dn) { + log.info("===== 开始执行 SM2 签名 ====="); + + // 入参检查 + if (origBytes == null || origBytes.length == 0) { + log.error("签名失败:待签名数据为空"); + throw new IllegalArgumentException("待签名数据不能为空"); + } + if (dn == null || dn.trim().isEmpty()) { + log.error("签名失败:证书DN为空"); + throw new IllegalArgumentException("证书DN不能为空"); + } + + log.info("待签名数据长度: {} bytes", origBytes.length); + log.info("证书DN: {}", dn); + + try { + // 根据 dn 查找证书 + X509Certificate cert = certificateService.getBySubjectDn(dn); + log.info("找到证书: {}", cert.getSubjectDN()); + + // 根据公钥查找实体 + byte[] publicKeyBytes = cert.getPublicKey().getEncoded(); + var entityIdOpt = entityService.findByCertificatePublicKey(publicKeyBytes); + + if (entityIdOpt.isEmpty()) { + log.error("签名失败:证书 {} 未绑定实体", dn); + throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书未绑定实体,无法进行签名"); + } + + Long entityId = entityIdOpt.get(); + KeyEntity entity = entityService.getById(entityId); + int keyIdx = entity.getKeyIdx(); + log.info("找到实体密钥索引: {}", keyIdx); + + UserKeySm2SignRequest request = new UserKeySm2SignRequest(); + request.setKeyIndex(keyIdx); + request.setData(origBytes); + request.setUserId(DEFAULT_SM2_USER_ID); + + log.info("调用密码卡 SM2 签名接口..."); + BackupDataResult result = sdf.userKeySignWithSm2Sm3(request); + + // Base64 格式返回 + String signature = java.util.Base64.getEncoder().encodeToString(result.getData()); + log.info("签名成功,签名长度: {} bytes", result.getLength()); + log.info("===== SM2 签名完成 ====="); + + return signature; + } catch (BizException e) { + log.error("签名业务异常: {}", e.getMessage()); + throw e; + } catch (Exception e) { + log.error("签名失败: {}", e.getMessage(), e); + throw new RuntimeException("签名失败: " + e.getMessage(), e); + } + } + private static String toHex(byte[] bytes) { + StringBuilder builder = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + builder.append(String.format("%02x", b)); + } + return builder.toString(); + } + + @Override + public boolean rawVerify(byte[] origBytes, String signature, String dn) { + log.info("===== 开始执行 SM2 外部公钥验签 ====="); + + // 入参检查 + if (origBytes == null || origBytes.length == 0) { + log.error("验签失败:待验签数据为空"); + throw new IllegalArgumentException("待验签数据不能为空"); + } + if (signature == null || signature.trim().isEmpty()) { + log.error("验签失败:签名值为空"); + throw new IllegalArgumentException("签名值不能为空"); + } + if (dn == null || dn.trim().isEmpty()) { + log.error("验签失败:证书DN为空"); + throw new IllegalArgumentException("证书DN不能为空"); + } + + log.info("待验签数据长度: {} bytes", origBytes.length); + log.info("证书DN: {}", dn); + + try { + // 获取证书 + X509Certificate cert = certificateService.getBySubjectDn(dn); + if (cert == null) { + log.error("验签失败:未找到DN为 {} 的证书", dn); + return false; + } + + log.info("找到证书: {}", cert.getSubjectDN()); + + // 将证书中的公钥转换为 EccRefPublicKey 结构 + EccRefPublicKey publicKey = EccRefPublicKey.fromPublicKey(cert.getPublicKey()); + log.info("公钥 x: {}, y: {}", toHex(publicKey.x), toHex(publicKey.y)); + byte[] publicKeyBlob = EccRefPublicKey.fromPublicKeyToBlob(cert.getPublicKey()); + log.info("公钥Blob {} 长度: {} bytes", toHex(publicKeyBlob), publicKeyBlob.length); + + + // 解码签名 + byte[] sign = java.util.Base64.getDecoder().decode(signature); + log.info("签名数据长度: {} bytes", sign.length); + + // 使用带公钥的 SM3 计算数据哈希 + log.info("使用带公钥的 SM3 计算数据哈希..."); + byte[] digest = sessionTemplate.withSession("SDF_HashFinal_SM2", (lib, deviceHandle, sessionHandle) -> { + sessionTemplate.ensureSuccess( + "SDF_HashInit", + lib.SDF_HashInit(sessionHandle, Gm0018AlgorithmIds.SM3, publicKey, DEFAULT_SM2_USER_ID, DEFAULT_SM2_USER_ID.length) + ); + if (origBytes.length > 0) { + sessionTemplate.ensureSuccess("SDF_HashUpdate", lib.SDF_HashUpdate(sessionHandle, origBytes, origBytes.length)); + } + byte[] out = new byte[32]; + IntByReference outLen = new IntByReference(out.length); + sessionTemplate.ensureSuccess("SDF_HashFinal", lib.SDF_HashFinal(sessionHandle, out, outLen)); + return out; + }); + log.info("SM3 哈希计算完成,哈希{} 长度: {} bytes", toHex(digest), digest.length); + + // 使用外部公钥验签(注意:传递的是哈希值而不是原始数据) + EccExternalVerifyRequest request = new EccExternalVerifyRequest(); + request.setAlgId(Gm0018AlgorithmIds.SM2_SIGN_1); + request.setPublicKeyBlob(publicKeyBlob); + request.setData(digest); + request.setSignature(sign); + + log.info("调用密码卡 SM2 外部公钥验签接口..."); + sdf.eccExternalVerify(request); + + log.info("验签成功"); + log.info("===== SM2 外部公钥验签完成 ====="); + return true; + + } catch (Exception e) { + log.error("验签失败: {}", e.getMessage(), e); + return false; + } + } + + @Override + public String dettachedSign(byte[] origBytes, String dn) { + // 暂时不实现,避免编译错误 + throw new UnsupportedOperationException("dettachedSign is not implemented yet"); + } + + @Override + public String dettachedVerify(byte[] origBytes, String certStr) { + // 暂时不实现,避免编译错误 + throw new UnsupportedOperationException("dettachedSign is not implemented yet"); + } + + @Override + public String dettachedVerifySimple(byte[] origBytes, String certStr) { + // 暂时不实现,避免编译错误 + throw new UnsupportedOperationException("dettachedSign is not implemented yet"); + } + + + /** + * 将 ASN.1 DER 编码的签名转换为 r||s 格式 + */ + private byte[] derToRs(byte[] derSignature) { + int offset = 2; // 跳过 0x30 和长度字节 + + // 读取 r + int rLength = derSignature[offset + 1] & 0xFF; + byte[] r = new byte[32]; + int rStart = offset + 2; + int rCopyLength = Math.min(rLength, 32); + if (rLength <= 32) { + System.arraycopy(derSignature, rStart, r, 32 - rLength, rCopyLength); + } else { + System.arraycopy(derSignature, rStart + (rLength - 32), r, 0, 32); + } + + // 读取 s + offset += 2 + rLength; + int sLength = derSignature[offset + 1] & 0xFF; + byte[] s = new byte[32]; + int sStart = offset + 2; + int sCopyLength = Math.min(sLength, 32); + if (sLength <= 32) { + System.arraycopy(derSignature, sStart, s, 32 - sLength, sCopyLength); + } else { + System.arraycopy(derSignature, sStart + (sLength - 32), s, 0, 32); + } + + // 组合 r||s + byte[] result = new byte[64]; + System.arraycopy(r, 0, result, 0, 32); + System.arraycopy(s, 0, result, 32, 32); + return result; + } + + /** + * 将 r||s 格式的签名转换为 ASN.1 DER 编码 + */ + private byte[] rsToDer(byte[] rsSignature) { + if (rsSignature.length < 64) { + return rsSignature; + } + + byte[] r = new byte[32]; + byte[] s = new byte[32]; + System.arraycopy(rsSignature, 0, r, 0, 32); + System.arraycopy(rsSignature, 32, s, 0, 32); + + r = stripLeadingZeros(r); + s = stripLeadingZeros(s); + + if ((r[0] & 0x80) != 0) { + byte[] temp = new byte[r.length + 1]; + System.arraycopy(r, 0, temp, 1, r.length); + r = temp; + } + if ((s[0] & 0x80) != 0) { + byte[] temp = new byte[s.length + 1]; + System.arraycopy(s, 0, temp, 1, s.length); + s = temp; + } + + int totalLength = 2 + 2 + r.length + 2 + s.length; + byte[] der = new byte[totalLength]; + int offset = 0; + + der[offset++] = 0x30; + der[offset++] = (byte) (totalLength - 2); + + der[offset++] = 0x02; + der[offset++] = (byte) r.length; + System.arraycopy(r, 0, der, offset, r.length); + offset += r.length; + + der[offset++] = 0x02; + der[offset++] = (byte) s.length; + System.arraycopy(s, 0, der, offset, s.length); + + return der; + } + + /** + * 去除字节数组前面的零字节 + */ + private byte[] stripLeadingZeros(byte[] data) { + int start = 0; + while (start < data.length && data[start] == 0) { + start++; + } + if (start == data.length) { + return new byte[]{0}; + } + byte[] result = new byte[data.length - start]; + System.arraycopy(data, start, result, 0, result.length); + return result; + } +} diff --git a/src/main/java/com/cisd/tms/modules/system/dto/SelfCheckResult.java b/src/main/java/com/cisd/tms/modules/system/dto/SelfCheckResult.java new file mode 100644 index 0000000..ec83ad0 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/system/dto/SelfCheckResult.java @@ -0,0 +1,69 @@ +package com.cisd.tms.modules.system.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "自检结果") +public class SelfCheckResult { + + @Schema(description = "自检项目名称") + private String checkName; + + @Schema(description = "自检是否通过") + private boolean passed; + + @Schema(description = "自检详情信息") + private String message; + + @Schema(description = "自检耗时(毫秒)") + private long durationMs; + + public static SelfCheckResult success(String checkName, String message, long durationMs) { + SelfCheckResult result = new SelfCheckResult(); + result.checkName = checkName; + result.passed = true; + result.message = message; + result.durationMs = durationMs; + return result; + } + + public static SelfCheckResult failure(String checkName, String message, long durationMs) { + SelfCheckResult result = new SelfCheckResult(); + result.checkName = checkName; + result.passed = false; + result.message = message; + result.durationMs = durationMs; + return result; + } + + public String getCheckName() { + return checkName; + } + + public void setCheckName(String checkName) { + this.checkName = checkName; + } + + public boolean isPassed() { + return passed; + } + + public void setPassed(boolean passed) { + this.passed = passed; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public long getDurationMs() { + return durationMs; + } + + public void setDurationMs(long durationMs) { + this.durationMs = durationMs; + } +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/system/runner/SelfCheckStartupRunner.java b/src/main/java/com/cisd/tms/modules/system/runner/SelfCheckStartupRunner.java new file mode 100644 index 0000000..37e9d34 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/system/runner/SelfCheckStartupRunner.java @@ -0,0 +1,67 @@ +package com.cisd.tms.modules.system.runner; + +import com.cisd.tms.modules.system.dto.SelfCheckResult; +import com.cisd.tms.modules.system.service.SelfCheckService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class SelfCheckStartupRunner implements CommandLineRunner { + + private static final Logger log = LoggerFactory.getLogger(SelfCheckStartupRunner.class); + + @Autowired + private SelfCheckService selfCheckService; + + private static final String SELFCHECK_SKIP_ENV = "TMS_SELFCHECK_SKIP"; + + @Override + public void run(String... args) throws Exception { + log.info("══════════════════════════════════════════════════════════════"); + log.info(" 系统启动自检流程"); + log.info("══════════════════════════════════════════════════════════════"); + + String skipSelfCheck = System.getenv(SELFCHECK_SKIP_ENV); + if ("true".equalsIgnoreCase(skipSelfCheck)) { + log.warn("检测到环境变量 {}=true,跳过自检流程", SELFCHECK_SKIP_ENV); + log.info("══════════════════════════════════════════════════════════════"); + return; + } + + try { + List results = selfCheckService.executeAllChecks(); + + boolean allPassed = results.stream().allMatch(SelfCheckResult::isPassed); + + log.info("══════════════════════════════════════════════════════════════"); + if (allPassed) { + log.info(" 自检流程全部通过"); + log.info("══════════════════════════════════════════════════════════════"); + } else { + log.error(" 自检流程存在失败项"); + log.error("══════════════════════════════════════════════════════════════"); + for (SelfCheckResult result : results) { + if (!result.isPassed()) { + log.error(" 失败项: {} - {}", result.getCheckName(), result.getMessage()); + } + } + log.warn("系统将在 10 秒后关闭..."); + try { + Thread.sleep(10000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + log.error("系统自检未通过,进程退出"); + System.exit(-9); + } + + } catch (Exception e) { + log.error("自检流程执行异常: {}", e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/system/service/SelfCheckService.java b/src/main/java/com/cisd/tms/modules/system/service/SelfCheckService.java new file mode 100644 index 0000000..e5e6b2a --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/system/service/SelfCheckService.java @@ -0,0 +1,16 @@ +package com.cisd.tms.modules.system.service; + +import com.cisd.tms.modules.system.dto.SelfCheckResult; + +import java.util.List; + +public interface SelfCheckService { + + SelfCheckResult verifyDeviceFingerprint(); + + SelfCheckResult verifyTmsIntegrity(); + + SelfCheckResult verifyStandardTransceiverIntegrity(); + + List executeAllChecks(); +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/system/service/impl/SelfCheckServiceImpl.java b/src/main/java/com/cisd/tms/modules/system/service/impl/SelfCheckServiceImpl.java new file mode 100644 index 0000000..17b86ab --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/system/service/impl/SelfCheckServiceImpl.java @@ -0,0 +1,315 @@ +package com.cisd.tms.modules.system.service.impl; + +import com.cisd.tms.modules.system.dto.SelfCheckResult; +import com.cisd.tms.modules.system.service.SelfCheckService; +import com.sunyard.cisd.device.tool.DeviceFingerprint; +import com.sunyard.cisd.device.tool.DeviceFingerprintService; +import com.sunyard.cisd.device.tool.SM2Util; +import com.sunyard.cisd.device.tool.SM3Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.sunyard.cisd.device.tool.Main.*; + +@Service +public class SelfCheckServiceImpl implements SelfCheckService { + + private static final Logger log = LoggerFactory.getLogger(SelfCheckServiceImpl.class); + + private static final String CHECK_DEVICE_FINGERPRINT = "设备指纹验证"; + private static final String CHECK_TMS_INTEGRITY = "终端管理系统主程序完整性验证"; + private static final String CHECK_STANDARD_TRANSCEIVER = "标准收发器主程序完整性验证"; + + @Override + public SelfCheckResult verifyDeviceFingerprint() { + long startTime = System.currentTimeMillis(); + log.info("========== 开始执行: {} ==========", CHECK_DEVICE_FINGERPRINT); + + try { + boolean result = doVerifyDeviceFingerprint(); + long duration = System.currentTimeMillis() - startTime; + + if (result) { + String message = "设备指纹验证通过"; + log.info("[PASS] {} - {}", CHECK_DEVICE_FINGERPRINT, message); + return SelfCheckResult.success(CHECK_DEVICE_FINGERPRINT, message, duration); + } else { + String message = "设备指纹验证失败"; + log.error("[FAIL] {} - {}", CHECK_DEVICE_FINGERPRINT, message); + return SelfCheckResult.failure(CHECK_DEVICE_FINGERPRINT, message, duration); + } + } catch (Exception e) { + long duration = System.currentTimeMillis() - startTime; + String message = "设备指纹验证异常: " + e.getMessage(); + log.error("[ERROR] {} - {}", CHECK_DEVICE_FINGERPRINT, message, e); + return SelfCheckResult.failure(CHECK_DEVICE_FINGERPRINT, message, duration); + } + } + + @Override + public SelfCheckResult verifyTmsIntegrity() { + long startTime = System.currentTimeMillis(); + log.info("========== 开始执行: {} ==========", CHECK_TMS_INTEGRITY); + + try { + boolean result = doVerifyTmsIntegrity(); + long duration = System.currentTimeMillis() - startTime; + + if (result) { + String message = "终端管理系统主程序完整性验证通过"; + log.info("[PASS] {} - {}", CHECK_TMS_INTEGRITY, message); + return SelfCheckResult.success(CHECK_TMS_INTEGRITY, message, duration); + } else { + String message = "终端管理系统主程序完整性验证失败"; + log.error("[FAIL] {} - {}", CHECK_TMS_INTEGRITY, message); + return SelfCheckResult.failure(CHECK_TMS_INTEGRITY, message, duration); + } + } catch (Exception e) { + long duration = System.currentTimeMillis() - startTime; + String message = "终端管理系统主程序完整性验证异常: " + e.getMessage(); + log.error("[ERROR] {} - {}", CHECK_TMS_INTEGRITY, message, e); + return SelfCheckResult.failure(CHECK_TMS_INTEGRITY, message, duration); + } + } + + @Override + public SelfCheckResult verifyStandardTransceiverIntegrity() { + long startTime = System.currentTimeMillis(); + log.info("========== 开始执行: {} ==========", CHECK_STANDARD_TRANSCEIVER); + + try { + boolean result = doVerifyStandardTransceiverIntegrity(); + long duration = System.currentTimeMillis() - startTime; + + if (result) { + String message = "标准收发器主程序完整性验证通过"; + log.info("[PASS] {} - {}", CHECK_STANDARD_TRANSCEIVER, message); + return SelfCheckResult.success(CHECK_STANDARD_TRANSCEIVER, message, duration); + } else { + String message = "标准收发器主程序完整性验证失败"; + log.error("[FAIL] {} - {}", CHECK_STANDARD_TRANSCEIVER, message); + return SelfCheckResult.failure(CHECK_STANDARD_TRANSCEIVER, message, duration); + } + } catch (Exception e) { + long duration = System.currentTimeMillis() - startTime; + String message = "标准收发器主程序完整性验证异常: " + e.getMessage(); + log.error("[ERROR] {} - {}", CHECK_STANDARD_TRANSCEIVER, message, e); + return SelfCheckResult.failure(CHECK_STANDARD_TRANSCEIVER, message, duration); + } + } + + @Override + public List executeAllChecks() { + log.info("======================================"); + log.info(" 系统启动自检流程开始"); + log.info("======================================"); + + long totalStartTime = System.currentTimeMillis(); + List results = new ArrayList<>(); + + results.add(verifyDeviceFingerprint()); + results.add(verifyTmsIntegrity()); + results.add(verifyStandardTransceiverIntegrity()); + + long totalDuration = System.currentTimeMillis() - totalStartTime; + long passedCount = results.stream().filter(SelfCheckResult::isPassed).count(); + long failedCount = results.size() - passedCount; + + log.info("======================================"); + log.info(" 系统启动自检流程结束"); + log.info("======================================"); + log.info("自检总耗时: {} ms", totalDuration); + log.info("检测项总数: {}, 通过: {}, 失败: {}", results.size(), passedCount, failedCount); + + if (failedCount > 0) { + log.warn("存在未通过的自检项,请检查相关日志"); + } else { + log.info("所有自检项均已通过"); + } + + return results; + } + + private boolean doVerifyDeviceFingerprint() { + try { + DeviceFingerprintService service = new DeviceFingerprintService(); + DeviceFingerprint fingerprint = service.getDeviceFingerprint(); + + log.info("=== 当前设备信息 ==="); + log.info("主板序列号: " + fingerprint.getMotherboardSerialNumber()); + log.info("设备序列号: " + fingerprint.getDeviceSerialNumber()); + log.info("CPU型号: " + fingerprint.getCpuModel()); + log.info("CPU序列号: " + fingerprint.getCpuSerialNumber()); + log.info("内存大小: " + fingerprint.getMemorySize()); + log.info("首块网卡MAC地址: " + fingerprint.getFirstNetworkMacAddress()); + log.info("硬盘序列号: " + fingerprint.getHardDiskSerialNumber()); + + String plainText = buildPlainText(fingerprint); + log.info("\n当前拼接原文: " + plainText); + + String savedPlainText = SM3Util.readFile(FINGERPRINT_DATA_FILE).trim(); + log.info("保存的拼接原文: " + savedPlainText); + + byte[] signature = SM3Util.readFileBytes(SIGN_FILE); + PublicKey publicKey = SM2Util.loadPublicKey(FINGERPRINT_PUBLIC_KEY_PATH); + + boolean dataMatch = plainText.equals(savedPlainText); + boolean signatureVerified = SM2Util.verify(savedPlainText.getBytes(StandardCharsets.UTF_8), signature, publicKey); + + log.info("\n=== 验证结果 ==="); + log.info("设备信息匹配: " + dataMatch); + log.info("签名验证: " + signatureVerified); + log.info("总体验证: " + (dataMatch && signatureVerified)); + return (dataMatch && signatureVerified); + } catch (Exception e) { + log.error("[ERROR] {} - {}", CHECK_DEVICE_FINGERPRINT, e.getMessage()); + return false; + } + } + + private boolean doVerifyTmsIntegrity() { + try { + log.info("=== 验证系统文件MAC校验值 ==="); + + PublicKey publicKey = SM2Util.loadPublicKey(MAC_PUBLIC_KEY_PATH); + byte[] savedSignature = SM3Util.readFileBytes(MAC_SIGN_FILE); + String savedMacList = SM3Util.readFile(MAC_LIST_FILE); + + boolean macListSignatureValid = SM2Util.verify(savedMacList.getBytes(StandardCharsets.UTF_8), savedSignature, publicKey); + log.info("MAC列表签名验证: " + (macListSignatureValid ? "通过" : "失败")); + + if (!macListSignatureValid) { + log.info("错误: mac.list 签名验证失败,可能被篡改!"); + log.info("\n=== 验证结果 ==="); + log.info("完整性验证: false"); + return false; + } + + Map savedSm3Map = SM3Util.readSM3File(MAC_LIST_FILE); + + Map currentSm3Map = new java.util.LinkedHashMap<>(); + for (String target : MAC_TARGETS) { + File file = new File(target); + if (!file.exists()) { + continue; + } + + if (file.isFile()) { + String sm3Value = SM3Util.sm3FileHex(target); + currentSm3Map.put(target, sm3Value); + } else if (file.isDirectory()) { + Map dirSm3Map = SM3Util.calculateDirectorySM3(target); + for (Map.Entry entry : dirSm3Map.entrySet()) { + String absolutePath = target + "/" + entry.getKey(); + currentSm3Map.put(absolutePath, entry.getValue()); + } + } + } + + boolean allMatch = true; + + for (Map.Entry entry : savedSm3Map.entrySet()) { + String filePath = entry.getKey(); + String savedHash = entry.getValue(); + String currentHash = currentSm3Map.get(filePath); + + if (currentHash == null) { + log.info("文件缺失: " + filePath); + allMatch = false; + } else if (!savedHash.equals(currentHash)) { + log.info("文件被修改: " + filePath + " (期望: " + savedHash + ", 实际: " + currentHash + ")"); + allMatch = false; + } + } + + for (String filePath : currentSm3Map.keySet()) { + if (!savedSm3Map.containsKey(filePath)) { + log.info("新增文件: " + filePath); + allMatch = false; + } + } + + log.info(""); + log.info("=== 验证结果 ==="); + log.info("完整性验证: " + allMatch); + return allMatch; + } catch (Exception e) { + log.error("[ERROR] {} - {}", CHECK_TMS_INTEGRITY, e.getMessage()); + return false; + } + } + + private boolean doVerifyStandardTransceiverIntegrity() { + try { + log.info("=== 验证CMEP文件MAC校验值 ==="); + + PublicKey publicKey = SM2Util.loadPublicKey(MAC_PUBLIC_KEY_PATH); + byte[] savedSignature = SM3Util.readFileBytes(CMEP_MAC_SIGN_FILE); + String savedMacList = SM3Util.readFile(CMEP_MAC_LIST_FILE); + + boolean macListSignatureValid = SM2Util.verify(savedMacList.getBytes(StandardCharsets.UTF_8), savedSignature, publicKey); + log.info("CMEP MAC列表签名验证: " + (macListSignatureValid ? "通过" : "失败")); + + if (!macListSignatureValid) { + log.info("错误: cmep.mac.list 签名验证失败,可能被篡改!"); + log.info("\n=== 验证结果 ==="); + log.info("完整性验证: false"); + return false; + } + + Map savedSm3Map = SM3Util.readSM3File(CMEP_MAC_LIST_FILE); + + Map currentSm3Map = new java.util.LinkedHashMap<>(); + for (String target : CMEP_TARGETS) { + File file = new File(target); + if (!file.exists()) { + continue; + } + + if (file.isFile()) { + String sm3Value = SM3Util.sm3FileHex(target); + currentSm3Map.put(target, sm3Value); + } + } + + boolean allMatch = true; + + for (Map.Entry entry : savedSm3Map.entrySet()) { + String filePath = entry.getKey(); + String savedHash = entry.getValue(); + String currentHash = currentSm3Map.get(filePath); + + if (currentHash == null) { + log.info("文件缺失: " + filePath); + allMatch = false; + } else if (!savedHash.equals(currentHash)) { + log.info("文件被修改: " + filePath + " (期望: " + savedHash + ", 实际: " + currentHash + ")"); + allMatch = false; + } + } + + for (String filePath : currentSm3Map.keySet()) { + if (!savedSm3Map.containsKey(filePath)) { + log.info("新增文件: " + filePath); + allMatch = false; + } + } + + log.info(""); + log.info("=== 验证结果 ==="); + log.info("完整性验证: " + allMatch); + return allMatch; + } catch (Exception e) { + log.error("[ERROR] {} - {}", CHECK_STANDARD_TRANSCEIVER, e.getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/security/openapi/OpenApiSignAuthInterceptor.java b/src/main/java/com/cisd/tms/security/openapi/OpenApiSignAuthInterceptor.java index 6908215..d538776 100644 --- a/src/main/java/com/cisd/tms/security/openapi/OpenApiSignAuthInterceptor.java +++ b/src/main/java/com/cisd/tms/security/openapi/OpenApiSignAuthInterceptor.java @@ -27,6 +27,7 @@ import org.springframework.web.servlet.HandlerInterceptor; @Component @RequiredArgsConstructor + public class OpenApiSignAuthInterceptor implements HandlerInterceptor { private static final String APP_ID_HEADER = "X-App-Id";