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";