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:
waner 2026-05-15 17:12:03 +08:00
commit 60002f6c31
33 changed files with 1668 additions and 30 deletions

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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
View 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

View File

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

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -2624,4 +2624,4 @@ public class JnaPcieCryptoService implements PcieCryptoService {
}
return Arrays.copyOf(userId, userId.length);
}
}
}

View File

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

View File

@ -0,0 +1,16 @@
package com.cisd.tms.modules.cert.enums;
/**
* 证书用途
*/
public enum CertUsage {
digitalSignature,
nonRepudiation,
keyEncipherment,
dataEncipherment,
keyAgreement,
keyCertSign,
cRLSign,
encipherOnly,
decipherOnly
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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());

View File

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

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 + '\'' +
'}';
}
}

View File

@ -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 {
/**
* 证书 DNDistinguished 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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}

View File

@ -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;
}
}
}

View File

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