Merge remote-tracking branch 'origin/V1.00' into V1.00
# Conflicts: # src/main/java/com/cisd/tms/modules/cert/repository/impl/CertificateRepositoryImpl.java
This commit is contained in:
commit
60002f6c31
@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.sunyard.cisd</groupId>
|
||||
<artifactId>device-fingerprint-tool</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.0.1</version>
|
||||
<name>device-fingerprint-tool</name>
|
||||
<description>设备指纹信息获取工具</description>
|
||||
|
||||
|
||||
@ -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<String, String> 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<String, String> 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<String, String> savedSm3Map = SM3Util.readSM3File(CMEP_MAC_LIST_FILE);
|
||||
|
||||
Map<String, String> 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<String, String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
4
pom.xml
4
pom.xml
@ -21,6 +21,8 @@
|
||||
<mybatis-plus.version>3.5.10.1</mybatis-plus.version>
|
||||
<springdoc.version>2.8.5</springdoc.version>
|
||||
<mockito.version>5.17.0</mockito.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@ -119,7 +121,7 @@
|
||||
<dependency>
|
||||
<groupId>com.sunyard.cisd</groupId>
|
||||
<artifactId>device-fingerprint-tool</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.0.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
43
qodana.yaml
Normal file
43
qodana.yaml
Normal file
@ -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: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
#exclude:
|
||||
# - name: <SomeDisabledInspectionId>
|
||||
# paths:
|
||||
# - <path/where/not/run/inspection>
|
||||
|
||||
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> #(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
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -2624,4 +2624,4 @@ public class JnaPcieCryptoService implements PcieCryptoService {
|
||||
}
|
||||
return Arrays.copyOf(userId, userId.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
16
src/main/java/com/cisd/tms/modules/cert/enums/CertUsage.java
Normal file
16
src/main/java/com/cisd/tms/modules/cert/enums/CertUsage.java
Normal file
@ -0,0 +1,16 @@
|
||||
package com.cisd.tms.modules.cert.enums;
|
||||
|
||||
/**
|
||||
* 证书用途
|
||||
*/
|
||||
public enum CertUsage {
|
||||
digitalSignature,
|
||||
nonRepudiation,
|
||||
keyEncipherment,
|
||||
dataEncipherment,
|
||||
keyAgreement,
|
||||
keyCertSign,
|
||||
cRLSign,
|
||||
encipherOnly,
|
||||
decipherOnly
|
||||
}
|
||||
@ -10,8 +10,10 @@ public interface CertificateRepository {
|
||||
void deleteById(Long id);
|
||||
Optional<CertificateEntity> findById(Long id);
|
||||
Optional<CertificateEntity> findByFingerprint(String fingerprint);
|
||||
Optional<CertificateEntity> findBySubjectDn(String subjectDn);
|
||||
Optional<CertificateEntity> findByIssuerDnAndSerialNumber(String issuerDn, String serialNumber);
|
||||
boolean existsEntityCertByEntityId(Long entityId);
|
||||
long countByIssuerDn(String issuerDn);
|
||||
Page<CertificateEntity> page(CertificateListRequest request);
|
||||
}
|
||||
|
||||
|
||||
@ -49,6 +49,15 @@ public class CertificateRepositoryImpl implements CertificateRepository {
|
||||
return Optional.ofNullable(mapper.selectOne(wrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CertificateEntity> findBySubjectDn(String subjectDn) {
|
||||
LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<CertificateEntity>()
|
||||
.eq(CertificateEntity::getSubjectDn, subjectDn)
|
||||
.orderByDesc(CertificateEntity::getImportTime)
|
||||
.last("LIMIT 1");
|
||||
return Optional.ofNullable(mapper.selectOne(wrapper));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CertificateEntity> findByIssuerDnAndSerialNumber(String issuerDn, String serialNumber) {
|
||||
LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<CertificateEntity>()
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<Integer> uset = new HashSet<Integer>();
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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<RawSignResponse> 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<RawVerifyResponse> 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<RawSignResponse> 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<RawVerifyResponse> 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<RawVerifyResponse> 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<ImportCertificateResponse> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<SelfCheckResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<SelfCheckResult> executeAllChecks();
|
||||
}
|
||||
@ -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<SelfCheckResult> executeAllChecks() {
|
||||
log.info("======================================");
|
||||
log.info(" 系统启动自检流程开始");
|
||||
log.info("======================================");
|
||||
|
||||
long totalStartTime = System.currentTimeMillis();
|
||||
List<SelfCheckResult> 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<String, String> savedSm3Map = SM3Util.readSM3File(MAC_LIST_FILE);
|
||||
|
||||
Map<String, String> 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<String, String> dirSm3Map = SM3Util.calculateDirectorySM3(target);
|
||||
for (Map.Entry<String, String> entry : dirSm3Map.entrySet()) {
|
||||
String absolutePath = target + "/" + entry.getKey();
|
||||
currentSm3Map.put(absolutePath, entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean allMatch = true;
|
||||
|
||||
for (Map.Entry<String, String> 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<String, String> savedSm3Map = SM3Util.readSM3File(CMEP_MAC_LIST_FILE);
|
||||
|
||||
Map<String, String> 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<String, String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user