From 133363ef18e9c7ad3e22af8c3971bb3df87cc4d5 Mon Sep 17 00:00:00 2001 From: cheney Date: Wed, 29 Apr 2026 17:00:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=9C=89=E5=B7=AE=E5=BC=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crypto/pcie/jna/PcieNativeLibrary.java | 1 + .../auth/service/impl/AuthServiceImpl.java | 3 +- .../openapi/service/IOpenApiService.java | 9 + .../service/dto/GenericCertificate.java | 64 ++++ .../openapi/service/impl/OpenApiService.java | 351 ++++++++++++++++++ .../openapi/OpenApiSignAuthInterceptor.java | 1 + 6 files changed, 428 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/cisd/tms/modules/openapi/service/IOpenApiService.java create mode 100644 src/main/java/com/cisd/tms/modules/openapi/service/dto/GenericCertificate.java create mode 100644 src/main/java/com/cisd/tms/modules/openapi/service/impl/OpenApiService.java diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/PcieNativeLibrary.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/PcieNativeLibrary.java index 7bfbc43..6b26f85 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/PcieNativeLibrary.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/PcieNativeLibrary.java @@ -15,6 +15,7 @@ public interface PcieNativeLibrary extends Library { int SDF_CloseSession(Pointer hSessionHandle); + int SDF_GetPrivateKeyAccessRight(Pointer hSessionHandle, int uiKeyIndex, byte[] pucPassword, int uiPwdLength); int SDF_ReleasePrivateKeyAccessRight(Pointer hSessionHandle, int uiKeyIndex); diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java index 2d1676e..6d38137 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java @@ -58,7 +58,8 @@ import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor -public class AuthServiceImpl implements AuthService { +public class +AuthServiceImpl implements AuthService { private static final int MAX_FAILED_ATTEMPTS = 5; private static final int IDLE_TIMEOUT_MINUTES = 10; diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/IOpenApiService.java b/src/main/java/com/cisd/tms/modules/openapi/service/IOpenApiService.java new file mode 100644 index 0000000..78dcdc4 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/IOpenApiService.java @@ -0,0 +1,9 @@ +package com.cisd.tms.modules.openapi.service; + +public interface IOpenApiService { + public String rawSign(byte[] origBytes, String dn); + public boolean rawVerify(byte[] origBytes, String certStr, String dn); + public String dettachedSign(byte[] origBytes, String dn); + public String dettachedVerify(byte[] origBytes, String certStr); + public String dettachedVerifySimple(byte[] origBytes, String certStr); +} diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/dto/GenericCertificate.java b/src/main/java/com/cisd/tms/modules/openapi/service/dto/GenericCertificate.java new file mode 100644 index 0000000..3472792 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/dto/GenericCertificate.java @@ -0,0 +1,64 @@ +package com.cisd.tms.modules.openapi.service.dto; + +import java.security.cert.X509Certificate; + +/** + * 通用证书类 + */ +public class GenericCertificate { + + private X509Certificate certificate; + private String dn; + private String issuer; + private String serialNumber; + + public GenericCertificate(X509Certificate certificate) { + this.certificate = certificate; + if (certificate != null) { + this.dn = certificate.getSubjectDN().getName(); + this.issuer = certificate.getIssuerDN().getName(); + this.serialNumber = certificate.getSerialNumber().toString(); + } + } + + public X509Certificate getCertificate() { + return certificate; + } + + public void setCertificate(X509Certificate certificate) { + this.certificate = certificate; + } + + public String getDn() { + return dn; + } + + public void setDn(String dn) { + this.dn = dn; + } + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public String getSerialNumber() { + return serialNumber; + } + + public void setSerialNumber(String serialNumber) { + this.serialNumber = serialNumber; + } + + @Override + public String toString() { + return "GenericCertificate{" + + "dn='" + dn + '\'' + + ", issuer='" + issuer + '\'' + + ", serialNumber='" + serialNumber + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/cisd/tms/modules/openapi/service/impl/OpenApiService.java b/src/main/java/com/cisd/tms/modules/openapi/service/impl/OpenApiService.java new file mode 100644 index 0000000..39deabc --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/openapi/service/impl/OpenApiService.java @@ -0,0 +1,351 @@ +package com.cisd.tms.modules.openapi.service.impl; + +import com.cisd.tms.integration.crypto.pcie.service.JnaPcieCryptoService; +import com.cisd.tms.modules.openapi.service.IOpenApiService; +import com.cisd.tms.modules.openapi.service.dto.GenericCertificate; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; +import lombok.RequiredArgsConstructor; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.bouncycastle.util.encoders.Base64; + +import java.security.KeyPair; +import java.security.cert.X509Certificate; + + +@RequiredArgsConstructor +public class OpenApiService implements IOpenApiService { + + private JnaPcieCryptoService sdf; + + + @Override + public String rawSign(byte[] origBytes, String dn) { + try { + // 使用固定的密钥索引 0 + int keyIndex = 0; + + // 获取证书和公钥 + X509Certificate cert = certManager.getCertificate(dn); + if (cert == null) { + throw new RuntimeException("Certificate not found for DN: " + dn); + } + + // 获取公钥字节数组 + byte[] publicKey = cert.getPublicKey().getEncoded(); + + // 使用 SDF 接口进行 SM3 哈希(包含公钥和 ID) + byte[] hash = new byte[32]; // SM3 哈希长度为 32 字节 + IntByReference hashLength = new IntByReference(); + + // 使用空指针作为会话句柄(SDFSoft 支持) + Pointer sessionHandle = Pointer.NULL; + + // 默认的 SM2 用户 ID + String userId = "1234567812345678"; + + // 初始化哈希(包含公钥和用户 ID) + int ret = sdf.SDF_HashInit(sessionHandle, 0, publicKey, userId, userId.length()); + if (ret != 0) { + throw new RuntimeException("SDF_HashInit failed with code: " + ret); + } + + // 更新哈希(添加原始数据) + ret = sdf.SDF_HashUpdate(sessionHandle, origBytes, origBytes.length); + if (ret != 0) { + throw new RuntimeException("SDF_HashUpdate failed with code: " + ret); + } + + // 完成哈希 + ret = sdf.SDF_HashFinal(sessionHandle, hash, hashLength); + if (ret != 0) { + throw new RuntimeException("SDF_HashFinal failed with code: " + ret); + } + + // 使用 SDF 接口进行 SM2 签名 + byte[] signature = new byte[64]; // SM2 签名长度为 64 字节 + ret = sdf.SDF_InternalSign_ECC(sessionHandle, keyIndex, hash, hashLength.getValue(), signature); + if (ret != 0) { + throw new RuntimeException("SDF_InternalSign_ECC failed with code: " + ret); + } + + // 返回 Base64 编码的签名 + return new String(Base64.encode(signature)); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Raw sign failed: " + e.getMessage(), e); + } + } + + @Override + public boolean rawVerify(byte[] origBytes, String certStr, String dn) { + try { + // 获取证书 + X509Certificate cert = certManager.getCertificate(dn); + if (cert == null) { + return false; + } + + // 解码签名 + byte[] signature = Base64.decode(certStr); + + // 获取公钥并转换为 X.509 编码格式 + byte[] publicKey = cert.getPublicKey().getEncoded(); + + // 使用 SDF 接口进行 SM3 哈希(包含公钥和 ID) + byte[] hash = new byte[32]; // SM3 哈希长度为 32 字节 + IntByReference hashLength = new IntByReference(); + + // 使用空指针作为会话句柄(SDFSoft 支持) + Pointer sessionHandle = Pointer.NULL; + + // 默认的 SM2 用户 ID(与签名时保持一致) + String userId = "1234567812345678"; + + // 初始化哈希(包含公钥和用户 ID) + int ret = sdf.SDF_HashInit(sessionHandle, 0, publicKey, userId, userId.length()); + if (ret != 0) { + return false; + } + + // 更新哈希(添加原始数据) + ret = sdf.SDF_HashUpdate(sessionHandle, origBytes, origBytes.length); + if (ret != 0) { + return false; + } + + // 完成哈希 + ret = sdf.SDF_HashFinal(sessionHandle, hash, hashLength); + if (ret != 0) { + return false; + } + + // 使用 SDF 接口进行 SM2 验签 + ret = sdf.SDF_ExternalVerify_ECC(sessionHandle, 0, publicKey, hash, hashLength.getValue(), signature); + return ret == 0; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public String dettachedSign(byte[] origBytes, String dn) { + try { + // 获取密钥对和证书 + KeyPair keyPair = certManager.getKeyPair(dn); + X509Certificate cert = certManager.getCertificate(dn); + if (keyPair == null || cert == null) { + throw new RuntimeException("Certificate not found for DN: " + dn); + } + + // 创建 PKCS#7 签名 + CMSSignedDataGenerator generator = new CMSSignedDataGenerator(); + generator.addSignerInfoGenerator( + new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().setProvider("BC").build()) + .build( + new org.bouncycastle.operator.jcajce.JcaContentSignerBuilder("SHA256withECDSA") + .setProvider("BC") + .build(keyPair.getPrivate()), + + cert) + ); + generator.addCertificate(new org.bouncycastle.cert.jcajce.JcaX509CertificateHolder(cert)); + + CMSSignedData signedData = generator.generate(new CMSProcessableByteArray(origBytes), false); + + // 返回 Base64 编码的签名 + return new String(Base64.encode(signedData.getEncoded())); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Detached sign failed: " + e.getMessage(), e); + } + } + + @Override + public String dettachedVerify(byte[] origBytes, String certStr) { + try { + // 解码签名 + byte[] signedDataBytes = Base64.decode(certStr); + + // 使用原始数据验证签名 + CMSSignedData signedData = new CMSSignedData(new CMSProcessableByteArray(origBytes), signedDataBytes); + + // 验证签名 + for (Object signerInfoObj : signedData.getSignerInfos().getSigners()) { + org.bouncycastle.cms.SignerInformation signerInfo = (org.bouncycastle.cms.SignerInformation) signerInfoObj; + + // 获取证书 + java.util.Collection certMatches = signedData.getCertificates().getMatches(signerInfo.getSID()); + org.bouncycastle.cert.X509CertificateHolder certHolder = + (org.bouncycastle.cert.X509CertificateHolder) certMatches.iterator().next(); + + X509Certificate cert = new org.bouncycastle.cert.jcajce.JcaX509CertificateConverter() + .setProvider("BC") + .getCertificate(certHolder); + + // 验证签名 + try { + if (signerInfo.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(cert.getPublicKey()))) { + // 验证证书有效性(检查有效期) + cert.checkValidity(new java.util.Date()); + + return new GenericCertificate(cert); + } + } catch (org.bouncycastle.cms.CMSSignerDigestMismatchException e) { + // 签名验证失败(数据不匹配),不打印堆栈信息 + continue; + } + } + + return null; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + @Override + public String dettachedVerifySimple(byte[] origBytes, String certStr) { + try { + // 解码签名 + byte[] signedDataBytes = Base64.decode(certStr); + + // 使用原始数据验证签名 + CMSSignedData signedData = new CMSSignedData(new CMSProcessableByteArray(origBytes), signedDataBytes); + + // 验证签名(不验证证书有效性) + for (Object signerInfoObj : signedData.getSignerInfos().getSigners()) { + org.bouncycastle.cms.SignerInformation signerInfo = (org.bouncycastle.cms.SignerInformation) signerInfoObj; + + // 获取证书 + java.util.Collection certMatches = signedData.getCertificates().getMatches(signerInfo.getSID()); + org.bouncycastle.cert.X509CertificateHolder certHolder = + (org.bouncycastle.cert.X509CertificateHolder) certMatches.iterator().next(); + + X509Certificate cert = new org.bouncycastle.cert.jcajce.JcaX509CertificateConverter() + .setProvider("BC") + .getCertificate(certHolder); + + // 只验证签名是否正确,不检查证书有效期 + try { + if (signerInfo.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(cert.getPublicKey()))) { + return new GenericCertificate(cert); + } + } catch (org.bouncycastle.cms.CMSSignerDigestMismatchException e) { + // 签名验证失败(数据不匹配),不打印堆栈信息 + continue; + } + } + + return null; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 将 ASN.1 DER 编码的签名转换为 r||s 格式 + */ + private byte[] derToRs(byte[] derSignature) { + int offset = 2; // 跳过 0x30 和长度字节 + + // 读取 r + int rLength = derSignature[offset + 1] & 0xFF; + byte[] r = new byte[32]; + int rStart = offset + 2; + int rCopyLength = Math.min(rLength, 32); + if (rLength <= 32) { + System.arraycopy(derSignature, rStart, r, 32 - rLength, rCopyLength); + } else { + System.arraycopy(derSignature, rStart + (rLength - 32), r, 0, 32); + } + + // 读取 s + offset += 2 + rLength; + int sLength = derSignature[offset + 1] & 0xFF; + byte[] s = new byte[32]; + int sStart = offset + 2; + int sCopyLength = Math.min(sLength, 32); + if (sLength <= 32) { + System.arraycopy(derSignature, sStart, s, 32 - sLength, sCopyLength); + } else { + System.arraycopy(derSignature, sStart + (sLength - 32), s, 0, 32); + } + + // 组合 r||s + byte[] result = new byte[64]; + System.arraycopy(r, 0, result, 0, 32); + System.arraycopy(s, 0, result, 32, 32); + return result; + } + + /** + * 将 r||s 格式的签名转换为 ASN.1 DER 编码 + */ + private byte[] rsToDer(byte[] rsSignature) { + if (rsSignature.length < 64) { + return rsSignature; + } + + byte[] r = new byte[32]; + byte[] s = new byte[32]; + System.arraycopy(rsSignature, 0, r, 0, 32); + System.arraycopy(rsSignature, 32, s, 0, 32); + + r = stripLeadingZeros(r); + s = stripLeadingZeros(s); + + if ((r[0] & 0x80) != 0) { + byte[] temp = new byte[r.length + 1]; + System.arraycopy(r, 0, temp, 1, r.length); + r = temp; + } + if ((s[0] & 0x80) != 0) { + byte[] temp = new byte[s.length + 1]; + System.arraycopy(s, 0, temp, 1, s.length); + s = temp; + } + + int totalLength = 2 + 2 + r.length + 2 + s.length; + byte[] der = new byte[totalLength]; + int offset = 0; + + der[offset++] = 0x30; + der[offset++] = (byte) (totalLength - 2); + + der[offset++] = 0x02; + der[offset++] = (byte) r.length; + System.arraycopy(r, 0, der, offset, r.length); + offset += r.length; + + der[offset++] = 0x02; + der[offset++] = (byte) s.length; + System.arraycopy(s, 0, der, offset, s.length); + + return der; + } + + /** + * 去除字节数组前面的零字节 + */ + private byte[] stripLeadingZeros(byte[] data) { + int start = 0; + while (start < data.length && data[start] == 0) { + start++; + } + if (start == data.length) { + return new byte[]{0}; + } + byte[] result = new byte[data.length - start]; + System.arraycopy(data, start, result, 0, result.length); + return result; + } +} diff --git a/src/main/java/com/cisd/tms/security/openapi/OpenApiSignAuthInterceptor.java b/src/main/java/com/cisd/tms/security/openapi/OpenApiSignAuthInterceptor.java index 6908215..d538776 100644 --- a/src/main/java/com/cisd/tms/security/openapi/OpenApiSignAuthInterceptor.java +++ b/src/main/java/com/cisd/tms/security/openapi/OpenApiSignAuthInterceptor.java @@ -27,6 +27,7 @@ import org.springframework.web.servlet.HandlerInterceptor; @Component @RequiredArgsConstructor + public class OpenApiSignAuthInterceptor implements HandlerInterceptor { private static final String APP_ID_HEADER = "X-App-Id";