From 61f94f39cb43c10666e1c55443a5e753374c846c Mon Sep 17 00:00:00 2001 From: liulu Date: Mon, 23 Dec 2024 15:38:09 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9D=9E=E5=AF=B9=E7=A7=B0=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=E9=AA=8C=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sunyard/chsm/mapper/AppCertMapper.java | 15 + .../chsm/service/impl/AppCertServiceImpl.java | 4 +- .../chsm/utils/gm/cert/SM2X509CertMaker.java | 283 ++++++++++++++++++ .../com/sunyard/chsm/param/AsymSignP7Req.java | 20 ++ .../sunyard/chsm/param/AsymSignP7Resp.java | 10 + .../sunyard/chsm/param/AsymSignRawReq.java | 2 +- .../sunyard/chsm/param/AsymVerifyP7Req.java | 23 ++ .../sunyard/chsm/param/AsymVerifyRawReq.java | 2 +- .../chsm/controller/AsymKeyController.java | 57 ++++ .../sunyard/chsm/service/AsymKeyService.java | 142 +++++++++ 10 files changed, 555 insertions(+), 3 deletions(-) create mode 100644 chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2X509CertMaker.java create mode 100644 chsm-params/src/main/java/com/sunyard/chsm/param/AsymSignP7Req.java create mode 100644 chsm-params/src/main/java/com/sunyard/chsm/param/AsymSignP7Resp.java create mode 100644 chsm-params/src/main/java/com/sunyard/chsm/param/AsymVerifyP7Req.java diff --git a/chsm-common/src/main/java/com/sunyard/chsm/mapper/AppCertMapper.java b/chsm-common/src/main/java/com/sunyard/chsm/mapper/AppCertMapper.java index 29d141a..3fa90e3 100644 --- a/chsm-common/src/main/java/com/sunyard/chsm/mapper/AppCertMapper.java +++ b/chsm-common/src/main/java/com/sunyard/chsm/mapper/AppCertMapper.java @@ -2,6 +2,8 @@ package com.sunyard.chsm.mapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sunyard.chsm.enums.KeyUsage; +import com.sunyard.chsm.model.Subject; import com.sunyard.chsm.model.entity.AppCert; import org.apache.ibatis.annotations.Mapper; import org.springframework.util.Assert; @@ -28,5 +30,18 @@ public interface AppCertMapper extends BaseMapper { return certs.iterator().next(); } + default AppCert selectSignBySubject(String dn) { + Assert.hasText(dn, "证书序列号不能为空"); + String subject = Subject.fromDN(dn).getDN(); + List certs = selectList(new LambdaQueryWrapper() + .eq(AppCert::getSubject, subject) + .eq(AppCert::getCertType, KeyUsage.SIGN_VERIFY.getCode()) + ); + if (CollectionUtils.isEmpty(certs)) { + return null; + } + return certs.iterator().next(); + } + } diff --git a/chsm-common/src/main/java/com/sunyard/chsm/service/impl/AppCertServiceImpl.java b/chsm-common/src/main/java/com/sunyard/chsm/service/impl/AppCertServiceImpl.java index 60fc7cf..c04cc31 100644 --- a/chsm-common/src/main/java/com/sunyard/chsm/service/impl/AppCertServiceImpl.java +++ b/chsm-common/src/main/java/com/sunyard/chsm/service/impl/AppCertServiceImpl.java @@ -11,6 +11,7 @@ import com.sunyard.chsm.mapper.AppCertMapper; import com.sunyard.chsm.mapper.ApplicationMapper; import com.sunyard.chsm.mapper.KeyInfoMapper; import com.sunyard.chsm.mapper.SpKeyRecordMapper; +import com.sunyard.chsm.model.Subject; import com.sunyard.chsm.model.dto.CertDTO; import com.sunyard.chsm.model.entity.AppCert; import com.sunyard.chsm.model.entity.Application; @@ -229,7 +230,8 @@ public class AppCertServiceImpl implements AppCertService { cert.setCertText(importCert.getCertText()); cert.setVersion(String.valueOf(x509Cert.getVersion())); - cert.setSubject(x509Cert.getSubjectX500Principal().getName()); + Subject subject = Subject.fromDN(x509Cert.getSubjectX500Principal().getName()); + cert.setSubject(subject.getDN()); cert.setSerialNumber(x509Cert.getSerialNumber().toString()); cert.setIssuerDn(x509Cert.getIssuerX500Principal().getName()); cert.setNotBefore(x509Cert.getNotBefore()); diff --git a/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2X509CertMaker.java b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2X509CertMaker.java new file mode 100644 index 0000000..dd34117 --- /dev/null +++ b/chsm-common/src/main/java/com/sunyard/chsm/utils/gm/cert/SM2X509CertMaker.java @@ -0,0 +1,283 @@ +package com.sunyard.chsm.utils.gm.cert; + +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.x500.AttributeTypeAndValue; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +public class SM2X509CertMaker { + + private static enum CertLevel { + RootCA, + SubCA, + EndEntity + } // class CertLevel + + public static final String SIGN_ALGO_SM3WITHSM2 = "SM3withSM2"; + + private long certExpire; + private X500Name issuerDN; + // private CertSNAllocator snAllocator; + private KeyPair issuerKeyPair; + + /** + * @param issuerKeyPair 证书颁发者的密钥对。 + * 其实一般的CA的私钥都是要严格保护的。 + * 一般CA的私钥都会放在加密卡/加密机里,证书的签名由加密卡/加密机完成。 + * 这里仅是为了演示BC库签发证书的用法,所以暂时不作太多要求。 + * @param certExpire 证书有效时间,单位毫秒 + * @param issuer 证书颁发者信息 + * @param snAllocator 维护/分配证书序列号的实例,证书序列号应该递增且不重复 + */ + public SM2X509CertMaker(KeyPair issuerKeyPair, long certExpire, X500Name issuer) +// , CertSNAllocator snAllocator) + { + this.issuerKeyPair = issuerKeyPair; + this.certExpire = certExpire; + this.issuerDN = issuer; +// this.snAllocator = snAllocator; + } + + /** + * 生成根CA证书 + * + * @param csr CSR + * @return 新的证书 + * @throws Exception 如果错误发生 + */ + public X509Certificate makeRootCACert(byte[] csr) + throws Exception { + KeyUsage usage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign); + return makeCertificate(CertLevel.RootCA, null, csr, usage, null); + } + + /** + * 生成SubCA证书 + * + * @param csr CSR + * @return 新的证书 + * @throws Exception 如果错误发生 + */ + public X509Certificate makeSubCACert(byte[] csr) + throws Exception { + KeyUsage usage = new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign); + return makeCertificate(CertLevel.SubCA, 0, csr, usage, null); + } + + /** + * 生成SSL用户证书 + * + * @param csr CSR + * @return 新的证书 + * @throws Exception 如果错误发生 + */ + public X509Certificate makeSSLEndEntityCert(byte[] csr) + throws Exception { + return makeEndEntityCert(csr, + new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth}); + } + + /** + * 生成用户证书 + * + * @param csr CSR + * @param extendedKeyUsages 扩展指数用途。 + * @return 新的证书 + * @throws Exception 如果错误发生 + */ + public X509Certificate makeEndEntityCert(byte[] csr, + KeyPurposeId[] extendedKeyUsages) + throws Exception { + KeyUsage usage = new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyAgreement + | KeyUsage.dataEncipherment | KeyUsage.keyEncipherment); + return makeCertificate(CertLevel.EndEntity, null, csr, usage, extendedKeyUsages); + } + + /** + * @param isCA 是否是颁发给CA的证书 + * @param keyUsage 证书用途 + * @param csr CSR + * @return + * @throws Exception + */ + private X509Certificate makeCertificate(CertLevel certLevel, Integer pathLenConstrain, + byte[] csr, KeyUsage keyUsage, KeyPurposeId[] extendedKeyUsages) + throws Exception { + if (certLevel == CertLevel.EndEntity) { + if (keyUsage.hasUsages(KeyUsage.keyCertSign)) { + throw new IllegalArgumentException( + "keyusage keyCertSign is not allowed in EndEntity Certificate"); + } + } + + PKCS10CertificationRequest request = new PKCS10CertificationRequest(csr); + SubjectPublicKeyInfo subPub = request.getSubjectPublicKeyInfo(); + + PrivateKey issPriv = issuerKeyPair.getPrivate(); + PublicKey issPub = issuerKeyPair.getPublic(); + + X500Name subject = request.getSubject(); + String email = null; + String commonName = null; + /* + * RFC 5280 §4.2.1.6 Subject + * Conforming implementations generating new certificates with + * electronic mail addresses MUST use the rfc822Name in the subject + * alternative name extension (Section 4.2.1.6) to describe such + * identities. Simultaneous inclusion of the emailAddress attribute in + * the subject distinguished name to support legacy implementations is + * deprecated but permitted. + */ + RDN[] rdns = subject.getRDNs(); + List newRdns = new ArrayList<>(rdns.length); + for (int i = 0; i < rdns.length; i++) { + RDN rdn = rdns[i]; + + AttributeTypeAndValue atv = rdn.getFirst(); + ASN1ObjectIdentifier type = atv.getType(); + if (BCStyle.EmailAddress.equals(type)) { + email = IETFUtils.valueToString(atv.getValue()); + } else { + if (BCStyle.CN.equals(type)) { + commonName = IETFUtils.valueToString(atv.getValue()); + } + newRdns.add(rdn); + } + } + + List subjectAltNames = new LinkedList<>(); + if (email != null) { + subject = new X500Name(newRdns.toArray(new RDN[0])); + subjectAltNames.add( + new GeneralName(GeneralName.rfc822Name, + new DERIA5String(email, true))); + } + + boolean selfSignedEECert = false; + switch (certLevel) { + case RootCA: + if (issuerDN.equals(subject)) { + subject = issuerDN; + } else { + throw new IllegalArgumentException("subject != issuer for certLevel " + CertLevel.RootCA); + } + break; + case SubCA: + if (issuerDN.equals(subject)) { + throw new IllegalArgumentException( + "subject MUST not equals issuer for certLevel " + certLevel); + } + break; + default: + if (issuerDN.equals(subject)) { + selfSignedEECert = true; + subject = issuerDN; + } + } + +// BigInteger serialNumber = snAllocator.nextSerialNumber(); + BigInteger serialNumber = new BigInteger(IdWorker.getIdStr()); + Date notBefore = new Date(); + Date notAfter = new Date(notBefore.getTime() + certExpire); + X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder( + issuerDN, serialNumber, + notBefore, notAfter, + subject, subPub); + + JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(); + v3CertGen.addExtension(Extension.subjectKeyIdentifier, false, + extUtils.createSubjectKeyIdentifier(subPub)); + if (certLevel != CertLevel.RootCA && !selfSignedEECert) { + v3CertGen.addExtension(Extension.authorityKeyIdentifier, false, + extUtils.createAuthorityKeyIdentifier(SubjectPublicKeyInfo.getInstance(issPub.getEncoded()))); + } + + // RFC 5280 §4.2.1.9 Basic Constraints: + // Conforming CAs MUST include this extension in all CA certificates + // that contain public keys used to validate digital signatures on + // certificates and MUST mark the extension as critical in such + // certificates. + BasicConstraints basicConstraints; + if (certLevel == CertLevel.EndEntity) { + basicConstraints = new BasicConstraints(false); + } else { + basicConstraints = pathLenConstrain == null + ? new BasicConstraints(true) : new BasicConstraints(pathLenConstrain.intValue()); + } + v3CertGen.addExtension(Extension.basicConstraints, true, basicConstraints); + + // RFC 5280 §4.2.1.3 Key Usage: When present, conforming CAs SHOULD mark this extension as critical. + v3CertGen.addExtension(Extension.keyUsage, true, keyUsage); + + if (extendedKeyUsages != null) { + ExtendedKeyUsage xku = new ExtendedKeyUsage(extendedKeyUsages); + v3CertGen.addExtension(Extension.extendedKeyUsage, false, xku); + + boolean forSSLServer = false; + for (KeyPurposeId purposeId : extendedKeyUsages) { + if (KeyPurposeId.id_kp_serverAuth.equals(purposeId)) { + forSSLServer = true; + break; + } + } + + if (forSSLServer) { + if (commonName == null) { + throw new IllegalArgumentException("commonName must not be null"); + } + GeneralName name = new GeneralName(GeneralName.dNSName, + new DERIA5String(commonName, true)); + subjectAltNames.add(name); + } + } + + if (!subjectAltNames.isEmpty()) { + v3CertGen.addExtension(Extension.subjectAlternativeName, false, + new GeneralNames(subjectAltNames.toArray(new GeneralName[0]))); + } + + JcaContentSignerBuilder contentSignerBuilder = makeContentSignerBuilder(issPub); + X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME) + .getCertificate(v3CertGen.build(contentSignerBuilder.build(issPriv))); + cert.verify(issPub); + + return cert; + } + + private JcaContentSignerBuilder makeContentSignerBuilder(PublicKey issPub) throws Exception { + if (issPub.getAlgorithm().equals("EC")) { + JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder(SIGN_ALGO_SM3WITHSM2); + contentSignerBuilder.setProvider(BouncyCastleProvider.PROVIDER_NAME); + return contentSignerBuilder; + } + throw new Exception("Unsupported PublicKey Algorithm:" + issPub.getAlgorithm()); + } +} diff --git a/chsm-params/src/main/java/com/sunyard/chsm/param/AsymSignP7Req.java b/chsm-params/src/main/java/com/sunyard/chsm/param/AsymSignP7Req.java new file mode 100644 index 0000000..ea264c6 --- /dev/null +++ b/chsm-params/src/main/java/com/sunyard/chsm/param/AsymSignP7Req.java @@ -0,0 +1,20 @@ +package com.sunyard.chsm.param; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class AsymSignP7Req { + + /** + * 签名证书的主题 + */ + @NotBlank(message = "签名证书不能为空") + private String subject; + + // 原文,使用Base64编码 + @NotBlank(message = "原文不能为空") + private String plainData; + +} diff --git a/chsm-params/src/main/java/com/sunyard/chsm/param/AsymSignP7Resp.java b/chsm-params/src/main/java/com/sunyard/chsm/param/AsymSignP7Resp.java new file mode 100644 index 0000000..7b0d8c3 --- /dev/null +++ b/chsm-params/src/main/java/com/sunyard/chsm/param/AsymSignP7Resp.java @@ -0,0 +1,10 @@ +package com.sunyard.chsm.param; + +import lombok.Data; + +@Data +public class AsymSignP7Resp { + + // 签名值 Base64编码 + private String signData; +} diff --git a/chsm-params/src/main/java/com/sunyard/chsm/param/AsymSignRawReq.java b/chsm-params/src/main/java/com/sunyard/chsm/param/AsymSignRawReq.java index 691e898..9de0327 100644 --- a/chsm-params/src/main/java/com/sunyard/chsm/param/AsymSignRawReq.java +++ b/chsm-params/src/main/java/com/sunyard/chsm/param/AsymSignRawReq.java @@ -12,7 +12,7 @@ public class AsymSignRawReq { @NotNull(message = "密钥ID不能为空") private Long keyId; - // 明文,使用Base64编码 + // 原文,使用Base64编码 @NotBlank(message = "明文不能为空") private String plainData; diff --git a/chsm-params/src/main/java/com/sunyard/chsm/param/AsymVerifyP7Req.java b/chsm-params/src/main/java/com/sunyard/chsm/param/AsymVerifyP7Req.java new file mode 100644 index 0000000..d556b4c --- /dev/null +++ b/chsm-params/src/main/java/com/sunyard/chsm/param/AsymVerifyP7Req.java @@ -0,0 +1,23 @@ +package com.sunyard.chsm.param; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class AsymVerifyP7Req { + + /** + * 签名证书的主题 + */ +// @NotBlank(message = "签名证书不能为空") +// private String subject; + + // 原文,使用Base64编码 + @NotBlank(message = "明文不能为空") + private String plainData; + + // 签名值, 使用Base64编码 + private String signData; + +} diff --git a/chsm-params/src/main/java/com/sunyard/chsm/param/AsymVerifyRawReq.java b/chsm-params/src/main/java/com/sunyard/chsm/param/AsymVerifyRawReq.java index 92df7f5..d24ad1a 100644 --- a/chsm-params/src/main/java/com/sunyard/chsm/param/AsymVerifyRawReq.java +++ b/chsm-params/src/main/java/com/sunyard/chsm/param/AsymVerifyRawReq.java @@ -23,7 +23,7 @@ public class AsymVerifyRawReq { @NotBlank(message = "密文不能为空") private String signData; - // 明文,使用Base64编码 + // 原文,使用Base64编码 @NotBlank(message = "明文不能为空") private String plainData; diff --git a/chsm-web-server/src/main/java/com/sunyard/chsm/controller/AsymKeyController.java b/chsm-web-server/src/main/java/com/sunyard/chsm/controller/AsymKeyController.java index da995b1..518c4cc 100644 --- a/chsm-web-server/src/main/java/com/sunyard/chsm/controller/AsymKeyController.java +++ b/chsm-web-server/src/main/java/com/sunyard/chsm/controller/AsymKeyController.java @@ -7,8 +7,11 @@ import com.sunyard.chsm.param.AsymDecryptReq; import com.sunyard.chsm.param.AsymDecryptResp; import com.sunyard.chsm.param.AsymEncryptReq; import com.sunyard.chsm.param.AsymEncryptResp; +import com.sunyard.chsm.param.AsymSignP7Req; +import com.sunyard.chsm.param.AsymSignP7Resp; import com.sunyard.chsm.param.AsymSignRawReq; import com.sunyard.chsm.param.AsymSignRawResp; +import com.sunyard.chsm.param.AsymVerifyP7Req; import com.sunyard.chsm.param.AsymVerifyRawReq; import com.sunyard.chsm.param.VerifyResp; import com.sunyard.chsm.service.AsymKeyService; @@ -85,5 +88,59 @@ public class AsymKeyController { return R.data(resp); } + /** + * P7 Attach签名 + * + * @param req + * @return + */ + @PostMapping("/sign/P7Attach") + @AuthCode(AuthCodeConst.sign_P7Attach) + public R signP7Attach(@Valid @RequestBody AsymSignP7Req req) { + AsymSignP7Resp resp = asymKeyService.signP7Attach(req); + return R.data(resp); + } + + /** + * P7 Attach验签 + * + * @param req + * @return + */ + @PostMapping("/verify/P7Attach") + @AuthCode(AuthCodeConst.verify_P7Attach) + public R verifyP7Attach(@Valid @RequestBody AsymVerifyP7Req req) { + VerifyResp resp = asymKeyService.verifyP7Attach(req); + return R.data(resp); + } + + /** + * P7 Detach签名 + * + * @param req + * @return + */ + @PostMapping("/sign/P7Detach") + @AuthCode(AuthCodeConst.sign_P7Detach) + public R signP7Detach(@Valid @RequestBody AsymSignP7Req req) { + AsymSignP7Resp resp = asymKeyService.signP7Detach(req); + return R.data(resp); + } + + /** + * P7 Detach验签 + * + * @param req + * @return + */ + @PostMapping("/verify/P7Detach") + @AuthCode(AuthCodeConst.verify_P7Detach) + public R verifyP7Detach(@Valid @RequestBody AsymVerifyP7Req req) { + VerifyResp resp = asymKeyService.verifyP7Detach(req); + return R.data(resp); + } + + + } diff --git a/chsm-web-server/src/main/java/com/sunyard/chsm/service/AsymKeyService.java b/chsm-web-server/src/main/java/com/sunyard/chsm/service/AsymKeyService.java index 5875ff2..9834553 100644 --- a/chsm-web-server/src/main/java/com/sunyard/chsm/service/AsymKeyService.java +++ b/chsm-web-server/src/main/java/com/sunyard/chsm/service/AsymKeyService.java @@ -5,37 +5,67 @@ import com.sunyard.chsm.enums.KeyAlg; import com.sunyard.chsm.enums.KeyCategory; import com.sunyard.chsm.enums.KeyStatus; import com.sunyard.chsm.enums.KeyUsage; +import com.sunyard.chsm.mapper.AppCertMapper; import com.sunyard.chsm.mapper.KeyInfoMapper; import com.sunyard.chsm.mapper.SpKeyRecordMapper; +import com.sunyard.chsm.model.entity.AppCert; import com.sunyard.chsm.model.entity.KeyInfo; import com.sunyard.chsm.model.entity.KeyRecord; import com.sunyard.chsm.param.AsymDecryptReq; import com.sunyard.chsm.param.AsymDecryptResp; import com.sunyard.chsm.param.AsymEncryptReq; import com.sunyard.chsm.param.AsymEncryptResp; +import com.sunyard.chsm.param.AsymSignP7Req; +import com.sunyard.chsm.param.AsymSignP7Resp; import com.sunyard.chsm.param.AsymSignRawReq; import com.sunyard.chsm.param.AsymSignRawResp; +import com.sunyard.chsm.param.AsymVerifyP7Req; import com.sunyard.chsm.param.AsymVerifyRawReq; import com.sunyard.chsm.param.VerifyResp; import com.sunyard.chsm.sdf.SdfApiService; import com.sunyard.chsm.sdf.model.EccCipher; import com.sunyard.chsm.sdf.model.EccSignature; import com.sunyard.chsm.utils.CodecUtils; +import com.sunyard.chsm.utils.gm.BCECUtils; +import com.sunyard.chsm.utils.gm.cert.BCSM2CertUtils; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.CMSTypedData; +import org.bouncycastle.cms.SignerInfoGenerator; +import org.bouncycastle.cms.SignerInformation; +import org.bouncycastle.cms.SignerInformationStore; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.cms.jcajce.JcaSignerInfoVerifierBuilder; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.bouncycastle.util.Store; import org.springframework.stereotype.Service; import org.springframework.util.Assert; +import java.security.cert.X509Certificate; import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; import java.util.Objects; /** * @author liulu * @since 2024/12/20 */ +@Slf4j @Service @RequiredArgsConstructor public class AsymKeyService { + private final AppCertMapper appCertMapper; private final KeyInfoMapper keyInfoMapper; private final SpKeyRecordMapper spKeyRecordMapper; private final SdfApiService sdfApiService; @@ -147,6 +177,118 @@ public class AsymKeyService { return resp; } + public AsymSignP7Resp signP7Attach(AsymSignP7Req req) { + byte[] plainData = CodecUtils.decodeBase64(req.getPlainData()); + AppCert appCert = appCertMapper.selectSignBySubject(req.getSubject()); + Assert.notNull(appCert, "证书主题对应的证书不存在"); + Assert.isTrue(Objects.equals(appCert.getApplicationId(), UserContext.getCurrentAppId()), "您无权使用此密钥ID"); + byte[] encPri = CodecUtils.decodeHex(appCert.getEncPriKey()); + byte[] pri = sdfApiService.decryptByTMK(encPri); + + byte[] asymSignP7Resp = p7Sign(pri, appCert.getCertText(), plainData, true); + AsymSignP7Resp resp = new AsymSignP7Resp(); + resp.setSignData(CodecUtils.encodeBase64(asymSignP7Resp)); + return resp; + } + + public VerifyResp verifyP7Attach(AsymVerifyP7Req req) { + byte[] signData = CodecUtils.decodeBase64(req.getSignData()); + boolean verify; + try { + verify = p7Verify(signData, null); + + } catch (Exception e) { + verify = false; + } + VerifyResp resp = new VerifyResp(); + resp.setVerified(verify); + return resp; + } + + public AsymSignP7Resp signP7Detach(AsymSignP7Req req) { + byte[] plainData = CodecUtils.decodeBase64(req.getPlainData()); + AppCert appCert = appCertMapper.selectSignBySubject(req.getSubject()); + Assert.notNull(appCert, "证书主题对应的证书不存在"); + Assert.isTrue(Objects.equals(appCert.getApplicationId(), UserContext.getCurrentAppId()), "您无权使用此密钥ID"); + byte[] encPri = CodecUtils.decodeHex(appCert.getEncPriKey()); + byte[] pri = sdfApiService.decryptByTMK(encPri); + + byte[] asymSignP7Resp = p7Sign(pri, appCert.getCertText(), plainData, false); + AsymSignP7Resp resp = new AsymSignP7Resp(); + resp.setSignData(CodecUtils.encodeBase64(asymSignP7Resp)); + return resp; + } + + public VerifyResp verifyP7Detach(AsymVerifyP7Req req) { + Assert.hasText(req.getPlainData(), "P7Detach验签原文不能为空"); + byte[] signData = CodecUtils.decodeBase64(req.getSignData()); + byte[] plainData = CodecUtils.decodeBase64(req.getPlainData()); + boolean verify; + try { + verify = p7Verify(signData, plainData); + + } catch (Exception e) { + verify = false; + } + VerifyResp resp = new VerifyResp(); + resp.setVerified(verify); + return resp; + } + + private static byte[] p7Sign(byte[] pri, String cert, byte[] plainData, boolean encapsulate) { + try { + BCECPrivateKey privateKey = BCECUtils.createPrivateKey(pri); + X509Certificate x509Cert = BCSM2CertUtils.getX509Cert(cert); + // 构造签名内容 + CMSTypedData cmsData = new CMSProcessableByteArray(plainData); + // 生成签名者信息 + SignerInfoGenerator signerInfoGenerator = new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build() + ).build( + new JcaContentSignerBuilder("SM3withSM2").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey), + x509Cert + ); + // 构建 CMS Signed Data + CMSSignedDataGenerator generator = new CMSSignedDataGenerator(); + generator.addSignerInfoGenerator(signerInfoGenerator); + generator.addCertificates(new JcaCertStore(Collections.singletonList(x509Cert))); + CMSSignedData signedData = generator.generate(cmsData, encapsulate); + return signedData.getEncoded(); + } catch (Exception ex) { + log.warn("", ex); + throw new IllegalArgumentException("P7Attach 签名异常"); + } + } + + public static boolean p7Verify(byte[] signedDataBytes, byte[] originalData) throws Exception { + + CMSSignedData signedData; + if (originalData == null || originalData.length == 0) { + signedData = new CMSSignedData(signedDataBytes); + } else { + CMSTypedData originalContent = new CMSProcessableByteArray(originalData); + signedData = new CMSSignedData(originalContent, signedDataBytes); + } + + Store certStore = signedData.getCertificates(); + SignerInformationStore signers = signedData.getSignerInfos(); + + for (SignerInformation signer : signers.getSigners()) { + Collection matches = certStore.getMatches(signer.getSID()); + if (matches.isEmpty()) { + throw new IllegalArgumentException("No matching certificate found for signer"); + } + X509CertificateHolder certHolder = matches.iterator().next(); // 这里进行类型安全的提取 + X509Certificate cert = new JcaX509CertificateConverter() + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .getCertificate(certHolder); + if (signer.verify(new JcaSignerInfoVerifierBuilder( + new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build()).build(cert))) { + return true; + } + } + return false; + } private KeyInfo checkKey(Long keyId, KeyUsage usage) {