|
@@ -0,0 +1,1222 @@
|
|
|
|
|
+package com.ymwk.supervisionserver.pki.service.impl;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
|
|
+import com.ymwk.supervisionserver.component.PageResult;
|
|
|
|
|
+import com.ymwk.supervisionserver.component.ResponseDTO;
|
|
|
|
|
+import com.ymwk.supervisionserver.component.SmartPageUtil;
|
|
|
|
|
+import com.ymwk.supervisionserver.pki.entity.CertificateEntity;
|
|
|
|
|
+import com.ymwk.supervisionserver.pki.mapper.CertificateMapper;
|
|
|
|
|
+import com.ymwk.supervisionserver.pki.model.*;
|
|
|
|
|
+import com.ymwk.supervisionserver.pki.service.CertificateService;
|
|
|
|
|
+
|
|
|
|
|
+import com.ymwk.supervisionserver.pki.utils.issueCertUtil;
|
|
|
|
|
+import jakarta.annotation.Resource;
|
|
|
|
|
+import jakarta.servlet.http.HttpServletResponse;
|
|
|
|
|
+import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
|
|
|
|
|
+import org.bouncycastle.asn1.x509.Extensions;
|
|
|
|
|
+import org.bouncycastle.cert.ocsp.BasicOCSPResp;
|
|
|
|
|
+import org.bouncycastle.cert.ocsp.BasicOCSPRespBuilder;
|
|
|
|
|
+import org.bouncycastle.cert.ocsp.CertificateID;
|
|
|
|
|
+import org.bouncycastle.cert.ocsp.CertificateStatus;
|
|
|
|
|
+import org.bouncycastle.cert.ocsp.OCSPReq;
|
|
|
|
|
+import org.bouncycastle.cert.ocsp.OCSPRespBuilder;
|
|
|
|
|
+import org.bouncycastle.cert.ocsp.RevokedStatus;
|
|
|
|
|
+import org.bouncycastle.cert.ocsp.UnknownStatus;
|
|
|
|
|
+import org.bouncycastle.cert.ocsp.jcajce.JcaBasicOCSPRespBuilder;
|
|
|
|
|
+import org.bouncycastle.asn1.x500.X500Name;
|
|
|
|
|
+import org.bouncycastle.asn1.x500.X500NameBuilder;
|
|
|
|
|
+import org.bouncycastle.asn1.x500.style.BCStyle;
|
|
|
|
|
+import org.bouncycastle.asn1.x509.*;
|
|
|
|
|
+import org.bouncycastle.cert.X509CRLHolder;
|
|
|
|
|
+import org.bouncycastle.cert.X509CertificateHolder;
|
|
|
|
|
+import org.bouncycastle.cert.X509v2CRLBuilder;
|
|
|
|
|
+import org.bouncycastle.cert.X509v3CertificateBuilder;
|
|
|
|
|
+import org.bouncycastle.cert.jcajce.JcaX509CRLConverter;
|
|
|
|
|
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
|
|
|
|
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
|
|
|
|
+import org.bouncycastle.operator.ContentSigner;
|
|
|
|
|
+import org.bouncycastle.operator.DigestCalculatorProvider;
|
|
|
|
|
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
|
|
|
|
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.http.HttpStatus;
|
|
|
|
|
+import org.springframework.http.MediaType;
|
|
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
|
|
+import java.math.BigInteger;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.security.KeyFactory;
|
|
|
|
|
+import java.security.KeyPair;
|
|
|
|
|
+import java.security.PrivateKey;
|
|
|
|
|
+import java.security.PublicKey;
|
|
|
|
|
+import java.security.cert.CertificateFactory;
|
|
|
|
|
+import java.security.cert.X509CRL;
|
|
|
|
|
+import java.security.cert.X509Certificate;
|
|
|
|
|
+import java.security.spec.PKCS8EncodedKeySpec;
|
|
|
|
|
+import java.security.spec.X509EncodedKeySpec;
|
|
|
|
|
+import java.time.Duration;
|
|
|
|
|
+import java.time.Instant;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.ZoneId;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+
|
|
|
|
|
+@Service
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+public class CertificateServiceImpl implements CertificateService {
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private CertificateMapper certificateMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private CertificateStorageService certificateStorageService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private CertificateAssembleService assembleService;
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private CertificateStorageService storageService;
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${ocsp.responder-url:}")
|
|
|
|
|
+ private String ocspResponderUrl;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseDTO<PageResult<CertificateEntity>> getCertificateList(certListRequest certListRequest) {
|
|
|
|
|
+ LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+
|
|
|
|
|
+ wrapper.eq(certListRequest.getCertUsage() != null, CertificateEntity::getCertUsage, certListRequest.getCertUsage());
|
|
|
|
|
+ wrapper.like(certListRequest.getCertName() != null, CertificateEntity::getCertName, certListRequest.getCertName());
|
|
|
|
|
+ wrapper.eq(certListRequest.getStatus() != null, CertificateEntity::getStatus, certListRequest.getStatus());
|
|
|
|
|
+ wrapper.eq(certListRequest.getCertAlgo() != null, CertificateEntity::getCertAlgo, certListRequest.getCertAlgo());
|
|
|
|
|
+ Page<CertificateEntity> page = new Page<>(certListRequest.getPageNum(), certListRequest.getPageSize());
|
|
|
|
|
+ Page<CertificateEntity> resultPage = certificateMapper.selectPage(page, wrapper);
|
|
|
|
|
+ PageResult<CertificateEntity> pageResult = SmartPageUtil.convert2PageResult(resultPage, resultPage.getRecords());
|
|
|
|
|
+ return ResponseDTO.ok(pageResult);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseDTO<String> deleteCertificate(Long id) {
|
|
|
|
|
+ if (id == null) {
|
|
|
|
|
+ return ResponseDTO.error(ResponseDTO.ERROR_CODE, "证书ID不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ CertificateEntity certificate = certificateMapper.selectById(id);
|
|
|
|
|
+ if (certificate == null) {
|
|
|
|
|
+ return ResponseDTO.error(ResponseDTO.ERROR_CODE, "证书不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ long activeIssuedCertificateCount = certificateMapper.selectCount(
|
|
|
|
|
+ new LambdaQueryWrapper<CertificateEntity>()
|
|
|
|
|
+ .eq(CertificateEntity::getParentSerial, certificate.getCertSerialNumber())
|
|
|
|
|
+ .ne(CertificateEntity::getStatus, "4")
|
|
|
|
|
+ );
|
|
|
|
|
+ if (activeIssuedCertificateCount > 0) {
|
|
|
|
|
+ return ResponseDTO.error(
|
|
|
|
|
+ ResponseDTO.ERROR_CODE,
|
|
|
|
|
+ "该证书仍有未吊销的下级证书,不能删除"
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ certificateMapper.deleteById(id);
|
|
|
|
|
+ return ResponseDTO.ok("删除成功");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseDTO<CertificateEntity> issuePersonalCertificate(IssueRequest request) throws Exception {
|
|
|
|
|
+ // 1. 找到用于签发的 Issuer(这里默认用 isRoot=1 的 Root CA,取第一个有效的)
|
|
|
|
|
+ Long issuerId = request.getIssuerId();
|
|
|
|
|
+ if (issuerId == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("issuerId is required");
|
|
|
|
|
+ }
|
|
|
|
|
+ CertificateEntity issuerEntity = certificateMapper.selectById(issuerId);
|
|
|
|
|
+ if (issuerEntity == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("Issuer certificate not found: " + issuerId);
|
|
|
|
|
+ }
|
|
|
|
|
+// 可选:校验 issuer 是否为有效 CA
|
|
|
|
|
+ if (!"1".equals(issuerEntity.getIsCa()) && !"1".equals(issuerEntity.getIsRoot())) {
|
|
|
|
|
+ throw new IllegalArgumentException("Issuer is not a CA");
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("3".equals(issuerEntity.getStatus())) {
|
|
|
|
|
+ throw new IllegalStateException("冻结证书不能用于签发");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 从存储读取 Issuer 的证书与私钥(privateKey 已在存储中加密,readPrivateKeyFile 会解密)
|
|
|
|
|
+ String issuerSerial = issuerEntity.getCertSerialNumber();
|
|
|
|
|
+ String issuerCertPem = storageService.readCertificateFile(issuerSerial);
|
|
|
|
|
+ String issuerPrivateKeyPem = storageService.readPrivateKeyFile(issuerSerial);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 解析 Issuer 证书与私钥
|
|
|
|
|
+ java.security.cert.CertificateFactory cf =
|
|
|
|
|
+ java.security.cert.CertificateFactory.getInstance("X.509", "BC");
|
|
|
|
|
+ X509Certificate issuerCert;
|
|
|
|
|
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(issuerCertPem.getBytes(StandardCharsets.UTF_8))) {
|
|
|
|
|
+ issuerCert = (X509Certificate) cf.generateCertificate(bais);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int validityDays = request.getValidityDays() == null ? 365 : request.getValidityDays();
|
|
|
|
|
+ Instant issueTime = Instant.now();
|
|
|
|
|
+ long availableDays = Math.max(0, Duration.between(issueTime, issuerCert.getNotAfter().toInstant()).toDays());
|
|
|
|
|
+ if (validityDays > availableDays) {
|
|
|
|
|
+ return ResponseDTO.error("申请有效期超出颁发者证书有效期,当前颁发者证书最多可用 " + availableDays + " 天", null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 生成密钥对(申请者自己的密钥对)
|
|
|
|
|
+ KeyPair keyPair = issueCertUtil.generateKeyPair(request.getKeyAlgorithm(), request.getKeySize());
|
|
|
|
|
+ PrivateKey issuerPrivateKey = parsePrivateKeyPem(issuerPrivateKeyPem, issuerEntity.getCertAlgo());
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 构造 Subject DN(和根证书构造类似,但 BasicConstraints(false))
|
|
|
|
|
+ X500NameBuilder x500NameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
|
|
|
|
+ if (request.getCountry() != null) x500NameBuilder.addRDN(BCStyle.C, request.getCountry());
|
|
|
|
|
+ if (request.getOrganization() != null) x500NameBuilder.addRDN(BCStyle.O, request.getOrganization());
|
|
|
|
|
+ if (request.getOrganizationalUnit() != null) x500NameBuilder.addRDN(BCStyle.OU, request.getOrganizationalUnit());
|
|
|
|
|
+ if (request.getState() != null) x500NameBuilder.addRDN(BCStyle.ST, request.getState());
|
|
|
|
|
+ if (request.getLocality() != null) x500NameBuilder.addRDN(BCStyle.L, request.getLocality());
|
|
|
|
|
+ if (request.getPhone() != null) x500NameBuilder.addRDN(BCStyle.TELEPHONE_NUMBER, request.getPhone());
|
|
|
|
|
+ x500NameBuilder.addRDN(BCStyle.CN, request.getCommonName());
|
|
|
|
|
+ String serialNumberStr = UUID.randomUUID().toString().replace("-", "").toUpperCase();
|
|
|
|
|
+ x500NameBuilder.addRDN(BCStyle.SERIALNUMBER, serialNumberStr);
|
|
|
|
|
+ X500Name subjectDN = x500NameBuilder.build();
|
|
|
|
|
+
|
|
|
|
|
+ BigInteger serialNumber = new BigInteger(serialNumberStr, 16);
|
|
|
|
|
+ Date notBefore = Date.from(issueTime);
|
|
|
|
|
+ Date notAfter = Date.from(issueTime.plusSeconds(validityDays * 86400L));
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 构建证书(Issuer = issuerCert.getSubjectX500Principal())
|
|
|
|
|
+ X500Name issuerDN = X500Name.getInstance(issuerCert.getSubjectX500Principal().getEncoded());
|
|
|
|
|
+ X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
|
|
|
|
+ issuerDN,
|
|
|
|
|
+ serialNumber,
|
|
|
|
|
+ notBefore,
|
|
|
|
|
+ notAfter,
|
|
|
|
|
+ subjectDN,
|
|
|
|
|
+ keyPair.getPublic()
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // BasicConstraints:普通证书 false
|
|
|
|
|
+ certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
|
|
|
|
|
+ // KeyUsage:普通证书常见 digitalSignature + keyEncipherment
|
|
|
|
|
+ certBuilder.addExtension(Extension.keyUsage, true,
|
|
|
|
|
+ new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment));
|
|
|
|
|
+ addOcspAiaExtension(certBuilder);
|
|
|
|
|
+
|
|
|
|
|
+ // ExtendedKeyUsage:根据请求 certUsage 可定制;这里默认 serverAuth + clientAuth
|
|
|
|
|
+ KeyPurposeId[] purposes = new KeyPurposeId[] {
|
|
|
|
|
+ KeyPurposeId.id_kp_serverAuth,
|
|
|
|
|
+ KeyPurposeId.id_kp_clientAuth
|
|
|
|
|
+ };
|
|
|
|
|
+ ExtendedKeyUsage eku = new ExtendedKeyUsage(purposes);
|
|
|
|
|
+ certBuilder.addExtension(Extension.extendedKeyUsage, false, eku);
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 签名(用 Issuer 的私钥和 Issuer 的签名算法)
|
|
|
|
|
+ ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(issuerEntity.getCertAlgo()))
|
|
|
|
|
+ .setProvider("BC")
|
|
|
|
|
+ .build(issuerPrivateKey);
|
|
|
|
|
+
|
|
|
|
|
+ X509CertificateHolder certHolder = certBuilder.build(signer);
|
|
|
|
|
+ X509Certificate issuedCert = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder);
|
|
|
|
|
+
|
|
|
|
|
+ // 验证(可选)
|
|
|
|
|
+ issuedCert.verify(issuerCert.getPublicKey());
|
|
|
|
|
+
|
|
|
|
|
+ // 8. 转 PEM 并存储(同根证书逻辑)
|
|
|
|
|
+ String certPem = convertCertificateToPem(issuedCert);
|
|
|
|
|
+ String privateKeyPem = convertPrivateKeyToPem(keyPair.getPrivate());
|
|
|
|
|
+ String publicKeyPem = convertPublicKeyToPem(keyPair.getPublic());
|
|
|
|
|
+ // String chainPem = certPem; // 如果需要完整链,可把 issuerCert prepended;此处简单只放自身证书
|
|
|
|
|
+ // 若想把链做成 [issuedCert + issuerCert], 使用:
|
|
|
|
|
+ String chainPem = certPem + convertCertificateToPem(issuerCert);
|
|
|
|
|
+
|
|
|
|
|
+ CertificateFileInfo fileInfo = certificateStorageService.storeCertificateFiles(
|
|
|
|
|
+ serialNumberStr,
|
|
|
|
|
+ certPem,
|
|
|
|
|
+ privateKeyPem,
|
|
|
|
|
+ publicKeyPem,
|
|
|
|
|
+ chainPem
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 9. 保存到数据库
|
|
|
|
|
+ CertificateEntity entity = new CertificateEntity();
|
|
|
|
|
+ // 不设置 id(数据库自增)
|
|
|
|
|
+ entity.setCertName(request.getCertName());
|
|
|
|
|
+ entity.setCertSerialNumber(serialNumberStr);
|
|
|
|
|
+ entity.setCertSubject(issuedCert.getSubjectX500Principal().getName());
|
|
|
|
|
+ entity.setCertAlgo(request.getKeyAlgorithm());
|
|
|
|
|
+ entity.setValidFrom(LocalDateTime.ofInstant(issuedCert.getNotBefore().toInstant(), ZoneId.systemDefault()));
|
|
|
|
|
+ entity.setValidTo(LocalDateTime.ofInstant(issuedCert.getNotAfter().toInstant(), ZoneId.systemDefault()));
|
|
|
|
|
+ entity.setStatus("1");
|
|
|
|
|
+ entity.setIsCa("0");
|
|
|
|
|
+ entity.setIsRoot("0");
|
|
|
|
|
+ entity.setParentSerial(issuerSerial);
|
|
|
|
|
+ entity.setCertUsage(request.getCertUsage() == null ? "PERSONAL" : request.getCertUsage());
|
|
|
|
|
+ entity.setCertFilePath(fileInfo.getCertPath());
|
|
|
|
|
+ entity.setPrivateKeyPath(fileInfo.getPrivateKeyPath());
|
|
|
|
|
+ entity.setPublicKeyPath(fileInfo.getPublicKeyPath());
|
|
|
|
|
+ entity.setChainPath(fileInfo.getChainPath());
|
|
|
|
|
+
|
|
|
|
|
+ certificateMapper.insert(entity);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("普通证书签发成功: serial={}", serialNumberStr);
|
|
|
|
|
+ return ResponseDTO.ok(entity);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseDTO<CertificateEntity> issueRootCertificate(IssueRequest request) throws Exception {
|
|
|
|
|
+ // 生成密钥对
|
|
|
|
|
+ KeyPair keyPair = issueCertUtil.generateKeyPair(request.getKeyAlgorithm(), request.getKeySize());
|
|
|
|
|
+
|
|
|
|
|
+ // 生成序列号
|
|
|
|
|
+ String serialNumberStr = UUID.randomUUID()
|
|
|
|
|
+ .toString()
|
|
|
|
|
+ .replace("-", "")
|
|
|
|
|
+ .toUpperCase();
|
|
|
|
|
+
|
|
|
|
|
+ BigInteger serialNumber = new BigInteger(serialNumberStr, 16);
|
|
|
|
|
+ // 生成证书请求
|
|
|
|
|
+ X500NameBuilder x500NameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
|
|
|
|
+
|
|
|
|
|
+// 必填字段:CN(建议校验非空,否则证书主题无意义)
|
|
|
|
|
+ if (StringUtils.hasText(request.getCommonName())) {
|
|
|
|
|
+ x500NameBuilder.addRDN(BCStyle.CN, request.getCommonName());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new IllegalArgumentException("Common Name (CN) is required");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+// 选填字段:非空才添加
|
|
|
|
|
+ if (StringUtils.hasText(request.getCountry())) {
|
|
|
|
|
+ x500NameBuilder.addRDN(BCStyle.C, request.getCountry());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.hasText(request.getOrganizationalUnit())) {
|
|
|
|
|
+ x500NameBuilder.addRDN(BCStyle.OU, request.getOrganizationalUnit());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.hasText(request.getOrganization())) {
|
|
|
|
|
+ x500NameBuilder.addRDN(BCStyle.O, request.getOrganization());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.hasText(request.getState())) {
|
|
|
|
|
+ x500NameBuilder.addRDN(BCStyle.ST, request.getState());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.hasText(request.getLocality())) {
|
|
|
|
|
+ x500NameBuilder.addRDN(BCStyle.L, request.getLocality());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.hasText(serialNumberStr)) {
|
|
|
|
|
+ x500NameBuilder.addRDN(BCStyle.SERIALNUMBER, serialNumberStr);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.hasText(request.getPhone())) {
|
|
|
|
|
+ x500NameBuilder.addRDN(BCStyle.TELEPHONE_NUMBER, request.getPhone());
|
|
|
|
|
+ }
|
|
|
|
|
+ X500Name subjectDN = x500NameBuilder.build();
|
|
|
|
|
+ // 设置证书有效期
|
|
|
|
|
+ Date notBefore = new Date();
|
|
|
|
|
+ Date notAfter = Date.from(
|
|
|
|
|
+ Instant.now().plusSeconds(request.getValidityDays() * 86400L)
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 创建证书构建器(颁发者=主体,因为是自签名)
|
|
|
|
|
+ X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
|
|
|
|
+ subjectDN, // 颁发者(Issuer)= 主体(Subject)
|
|
|
|
|
+ serialNumber, // 序列号
|
|
|
|
|
+ notBefore, // 生效时间
|
|
|
|
|
+ notAfter, // 过期时间
|
|
|
|
|
+ subjectDN, // 主体
|
|
|
|
|
+ keyPair.getPublic() // 公钥
|
|
|
|
|
+ );
|
|
|
|
|
+ // 基本约束:标记为CA证书(true表示是CA)
|
|
|
|
|
+ certBuilder.addExtension(
|
|
|
|
|
+ Extension.basicConstraints,
|
|
|
|
|
+ true, // critical = true
|
|
|
|
|
+ new BasicConstraints(true) // true = 是CA,可以签发子证书
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 密钥用途:CA需要证书签名和CRL签名能力
|
|
|
|
|
+ certBuilder.addExtension(
|
|
|
|
|
+ Extension.keyUsage,
|
|
|
|
|
+ true, // critical = true
|
|
|
|
|
+ new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 5.3 使用私钥签名证书
|
|
|
|
|
+ ContentSigner signer = new JcaContentSignerBuilder(
|
|
|
|
|
+ getSignatureAlgorithm(request.getKeyAlgorithm())
|
|
|
|
|
+ )
|
|
|
|
|
+ .setProvider("BC")
|
|
|
|
|
+ .build(keyPair.getPrivate());
|
|
|
|
|
+
|
|
|
|
|
+ X509CertificateHolder certHolder = certBuilder.build(signer);
|
|
|
|
|
+ X509Certificate rootCert = new JcaX509CertificateConverter()
|
|
|
|
|
+ .setProvider("BC")
|
|
|
|
|
+ .getCertificate(certHolder);
|
|
|
|
|
+
|
|
|
|
|
+ // ====== 6. 验证自签名证书(可选但推荐) ======
|
|
|
|
|
+ rootCert.verify(keyPair.getPublic());
|
|
|
|
|
+
|
|
|
|
|
+ // ====== 7. 转换为PEM格式 ======
|
|
|
|
|
+ String chainPem = convertCertificateToPem(rootCert);
|
|
|
|
|
+ String privateKeyPem = convertPrivateKeyToPem(keyPair.getPrivate());
|
|
|
|
|
+ String publicKeyPem = convertPublicKeyToPem(keyPair.getPublic());
|
|
|
|
|
+
|
|
|
|
|
+ // 根证书的证书链就是自己
|
|
|
|
|
+
|
|
|
|
|
+ // ====== 8. 存储到MinIO(分开存储,私钥加密) ======
|
|
|
|
|
+ CertificateFileInfo fileInfo = certificateStorageService.storeCertificateFiles(
|
|
|
|
|
+ serialNumberStr,
|
|
|
|
|
+ chainPem,
|
|
|
|
|
+ privateKeyPem,
|
|
|
|
|
+ publicKeyPem,
|
|
|
|
|
+ chainPem
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // ====== 9. 保存到数据库 ======
|
|
|
|
|
+ CertificateEntity entity = new CertificateEntity();
|
|
|
|
|
+ entity.setCertName(request.getCertName());
|
|
|
|
|
+ entity.setCertSerialNumber(serialNumberStr);
|
|
|
|
|
+ entity.setCertSubject(rootCert.getSubjectX500Principal().getName());
|
|
|
|
|
+ entity.setCertAlgo(request.getKeyAlgorithm());
|
|
|
|
|
+ entity.setValidFrom(LocalDateTime.ofInstant(rootCert.getNotBefore().toInstant(), ZoneId.systemDefault()));
|
|
|
|
|
+ entity.setValidTo(LocalDateTime.ofInstant(rootCert.getNotAfter().toInstant(), ZoneId.systemDefault()));
|
|
|
|
|
+ entity.setStatus("1");
|
|
|
|
|
+ entity.setIsCa("1");
|
|
|
|
|
+ entity.setIsRoot("1");
|
|
|
|
|
+ entity.setParentSerial(null);
|
|
|
|
|
+ entity.setCertUsage("ROOT_CA");
|
|
|
|
|
+
|
|
|
|
|
+ // 只存储文件路径,不存储下载URL(动态生成)
|
|
|
|
|
+ entity.setCertFilePath(fileInfo.getCertPath());
|
|
|
|
|
+ entity.setPrivateKeyPath(fileInfo.getPrivateKeyPath());
|
|
|
|
|
+ entity.setPublicKeyPath(fileInfo.getPublicKeyPath());
|
|
|
|
|
+ entity.setChainPath(fileInfo.getChainPath());
|
|
|
|
|
+
|
|
|
|
|
+ certificateMapper.insert(entity);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("根证书签发成功: serial={}", serialNumberStr);
|
|
|
|
|
+
|
|
|
|
|
+ return ResponseDTO.ok(entity);
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public ResponseDTO<CertificateEntity> renewCertificate(RenewRequest request) throws Exception {
|
|
|
|
|
+ if (request == null || request.getId() == null) {
|
|
|
|
|
+ return ResponseDTO.error("证书ID不能为空", null);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request.getValidityDays() == null || request.getValidityDays() < 1) {
|
|
|
|
|
+ return ResponseDTO.error("延期天数必须大于0", null);
|
|
|
|
|
+ }
|
|
|
|
|
+ Long id = request.getId();
|
|
|
|
|
+ int validityDays = request.getValidityDays();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 查询证书数据库记录
|
|
|
|
|
+ CertificateEntity entity = certificateMapper.selectById(id);
|
|
|
|
|
+ if (entity == null) {
|
|
|
|
|
+ return ResponseDTO.error("当前证书未找到", null);
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("3".equals(entity.getStatus())) {
|
|
|
|
|
+ return ResponseDTO.error("冻结状态证书不能续期", null);
|
|
|
|
|
+ }
|
|
|
|
|
+ String oldSerial = entity.getCertSerialNumber();
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 读取原有证书文件、公私钥文件(复用原有密钥)
|
|
|
|
|
+ String subjectCertPem = storageService.readCertificateFile(oldSerial);
|
|
|
|
|
+ String subjectPublicKeyPem = storageService.readPublicKeyFile(oldSerial);
|
|
|
|
|
+ String subjectPrivateKeyPem = storageService.readPrivateKeyFile(oldSerial);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 解析旧证书 & 公钥
|
|
|
|
|
+ CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC");
|
|
|
|
|
+ X509Certificate subjectOldCert;
|
|
|
|
|
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(subjectCertPem.getBytes(StandardCharsets.UTF_8))) {
|
|
|
|
|
+ subjectOldCert = (X509Certificate) cf.generateCertificate(bais);
|
|
|
|
|
+ }
|
|
|
|
|
+ X500Name subjectDN = X500Name.getInstance(subjectOldCert.getSubjectX500Principal().getEncoded());
|
|
|
|
|
+ PublicKey subjectPublicKey = parsePublicKeyPem(subjectPublicKeyPem, entity.getCertAlgo());
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 获取签发者信息:根证书自签 / 非根证书找父签发者
|
|
|
|
|
+ X509Certificate issuerCert;
|
|
|
|
|
+ PrivateKey issuerPrivateKey;
|
|
|
|
|
+ X500Name issuerDN;
|
|
|
|
|
+ String issuerAlgo = entity.getCertAlgo();
|
|
|
|
|
+
|
|
|
|
|
+ if ("1".equals(entity.getIsRoot())) {
|
|
|
|
|
+ // 根证书:使用自身作为签发者,自签名
|
|
|
|
|
+ issuerCert = subjectOldCert;
|
|
|
|
|
+ issuerPrivateKey = parsePrivateKeyPem(subjectPrivateKeyPem, entity.getCertAlgo());
|
|
|
|
|
+ issuerDN = subjectDN;
|
|
|
|
|
+ issuerAlgo = entity.getCertAlgo();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 子证书:根据 parentSerial 查找签发CA证书
|
|
|
|
|
+ String issuerSerial = entity.getParentSerial();
|
|
|
|
|
+ if (issuerSerial == null) {
|
|
|
|
|
+ return ResponseDTO.error("证书缺少父级序列号,无法续期", null);
|
|
|
|
|
+ }
|
|
|
|
|
+ CertificateEntity issuerEntity = certificateMapper.selectOne(
|
|
|
|
|
+ new LambdaQueryWrapper<CertificateEntity>()
|
|
|
|
|
+ .eq(CertificateEntity::getCertSerialNumber, issuerSerial)
|
|
|
|
|
+ );
|
|
|
|
|
+ if (issuerEntity == null) {
|
|
|
|
|
+ return ResponseDTO.error("签发者未找到: serial=" + issuerSerial, null);
|
|
|
|
|
+ }
|
|
|
|
|
+ String issuerCertPem = storageService.readCertificateFile(issuerSerial);
|
|
|
|
|
+ String issuerPrivateKeyPem = storageService.readPrivateKeyFile(issuerSerial);
|
|
|
|
|
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(issuerCertPem.getBytes(StandardCharsets.UTF_8))) {
|
|
|
|
|
+ issuerCert = (X509Certificate) cf.generateCertificate(bais);
|
|
|
|
|
+ }
|
|
|
|
|
+ issuerPrivateKey = parsePrivateKeyPem(issuerPrivateKeyPem, issuerEntity.getCertAlgo());
|
|
|
|
|
+ issuerDN = X500Name.getInstance(issuerCert.getSubjectX500Principal().getEncoded());
|
|
|
|
|
+ issuerAlgo = issuerEntity.getCertAlgo();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Instant newNotAfter = subjectOldCert.getNotAfter().toInstant()
|
|
|
|
|
+ .plusSeconds((long) validityDays * 86400L);
|
|
|
|
|
+ if (!"1".equals(entity.getIsRoot())) {
|
|
|
|
|
+ if (newNotAfter.isAfter(issuerCert.getNotAfter().toInstant())) {
|
|
|
|
|
+ return ResponseDTO.error("延期后的证书有效期不能超过签发者证书有效期", null);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 保留原证书起始时间,顺延到期时间,并生成新序列号
|
|
|
|
|
+ String newSerial;
|
|
|
|
|
+ do {
|
|
|
|
|
+ newSerial = UUID.randomUUID().toString().replace("-", "").toUpperCase(Locale.ROOT);
|
|
|
|
|
+ } while (certificateMapper.selectCount(
|
|
|
|
|
+ new LambdaQueryWrapper<CertificateEntity>()
|
|
|
|
|
+ .eq(CertificateEntity::getCertSerialNumber, newSerial)
|
|
|
|
|
+ ) > 0);
|
|
|
|
|
+ BigInteger serialNumber = new BigInteger(newSerial, 16);
|
|
|
|
|
+ Date notBefore = subjectOldCert.getNotBefore();
|
|
|
|
|
+ Date notAfter = Date.from(newNotAfter);
|
|
|
|
|
+
|
|
|
|
|
+ X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
|
|
|
|
+ issuerDN,
|
|
|
|
|
+ serialNumber,
|
|
|
|
|
+ notBefore,
|
|
|
|
|
+ notAfter,
|
|
|
|
|
+ subjectDN,
|
|
|
|
|
+ subjectPublicKey
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 设置证书扩展项:CA证书 / 业务证书区分
|
|
|
|
|
+ if ("1".equals(entity.getIsCa()) || "1".equals(entity.getIsRoot())) {
|
|
|
|
|
+ certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
|
|
|
|
|
+ certBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ certBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
|
|
|
|
|
+ certBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment));
|
|
|
|
|
+ KeyPurposeId[] purposes = new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth};
|
|
|
|
|
+ certBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(purposes));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!"1".equals(entity.getIsRoot())) {
|
|
|
|
|
+ addOcspAiaExtension(certBuilder);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 使用签发者私钥签名
|
|
|
|
|
+ ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(issuerAlgo))
|
|
|
|
|
+ .setProvider("BC")
|
|
|
|
|
+ .build(issuerPrivateKey);
|
|
|
|
|
+
|
|
|
|
|
+ X509CertificateHolder certHolder = certBuilder.build(signer);
|
|
|
|
|
+ X509Certificate newCert = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder);
|
|
|
|
|
+
|
|
|
|
|
+ // 校验新证书签名合法性
|
|
|
|
|
+ newCert.verify(issuerCert.getPublicKey());
|
|
|
|
|
+
|
|
|
|
|
+ // 6. PEM格式化,使用原有密钥对,并按新序列号存储证书文件
|
|
|
|
|
+ String newCertPem = convertCertificateToPem(newCert);
|
|
|
|
|
+ String newPublicKeyPem = subjectPublicKeyPem;
|
|
|
|
|
+ String newPrivateKeyPem = subjectPrivateKeyPem;
|
|
|
|
|
+
|
|
|
|
|
+ String chainPem;
|
|
|
|
|
+ if ("1".equals(entity.getIsRoot())) {
|
|
|
|
|
+ chainPem = newCertPem;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ String issuerCertPem = storageService.readCertificateFile(entity.getParentSerial());
|
|
|
|
|
+ chainPem = newCertPem + issuerCertPem;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CertificateFileInfo fileInfo = certificateStorageService.storeCertificateFiles(
|
|
|
|
|
+ newSerial,
|
|
|
|
|
+ newCertPem,
|
|
|
|
|
+ newPrivateKeyPem,
|
|
|
|
|
+ newPublicKeyPem,
|
|
|
|
|
+ chainPem
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 更新证书序列号、有效期与文件路径
|
|
|
|
|
+ entity.setCertSerialNumber(newSerial);
|
|
|
|
|
+ entity.setValidFrom(LocalDateTime.ofInstant(newCert.getNotBefore().toInstant(), ZoneId.systemDefault()));
|
|
|
|
|
+ entity.setValidTo(LocalDateTime.ofInstant(newCert.getNotAfter().toInstant(), ZoneId.systemDefault()));
|
|
|
|
|
+ if ("2".equals(entity.getStatus()) && entity.getValidTo().isAfter(LocalDateTime.now())) {
|
|
|
|
|
+ entity.setStatus("1");
|
|
|
|
|
+ }
|
|
|
|
|
+ entity.setCertFilePath(fileInfo.getCertPath());
|
|
|
|
|
+ entity.setPrivateKeyPath(fileInfo.getPrivateKeyPath());
|
|
|
|
|
+ entity.setPublicKeyPath(fileInfo.getPublicKeyPath());
|
|
|
|
|
+ entity.setChainPath(fileInfo.getChainPath());
|
|
|
|
|
+ certificateMapper.updateById(entity);
|
|
|
|
|
+ certificateMapper.update(
|
|
|
|
|
+ null,
|
|
|
|
|
+ new LambdaUpdateWrapper<CertificateEntity>()
|
|
|
|
|
+ .eq(CertificateEntity::getParentSerial, oldSerial)
|
|
|
|
|
+ .set(CertificateEntity::getParentSerial, newSerial)
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ log.info("证书续期成功: oldSerial={}, newSerial={}, id={}", oldSerial, newSerial, id);
|
|
|
|
|
+ return ResponseDTO.ok(entity);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseDTO<CertificateEntity> freezeCertificate(FreezeCertificateRequest request) {
|
|
|
|
|
+ if (request == null || request.getId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("证书ID不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CertificateEntity entity = certificateMapper.selectById(request.getId());
|
|
|
|
|
+ if (entity == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("证书未找到: id=" + request.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 防止重复冻结
|
|
|
|
|
+ if ("3".equals(entity.getStatus())) {
|
|
|
|
|
+ return ResponseDTO.ok(entity);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ entity.setStatus("3");
|
|
|
|
|
+ entity.setRevokedTime(LocalDateTime.now());
|
|
|
|
|
+ entity.setRevokeReason("certificateHold");
|
|
|
|
|
+
|
|
|
|
|
+ certificateMapper.updateById(entity);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("证书已冻结: id={}, serial={}", entity.getId(), entity.getCertSerialNumber());
|
|
|
|
|
+ return ResponseDTO.ok(entity);
|
|
|
|
|
+ }
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseDTO<CertificateEntity> unfreezeCertificate(FreezeCertificateRequest request) {
|
|
|
|
|
+ if (request == null || request.getId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("证书ID不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CertificateEntity entity = certificateMapper.selectById(request.getId());
|
|
|
|
|
+ if (entity == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("证书未找到: id=" + request.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!"3".equals(entity.getStatus())) {
|
|
|
|
|
+ return ResponseDTO.ok(entity);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ entity.setStatus("1");
|
|
|
|
|
+ entity.setRevokedTime(null);
|
|
|
|
|
+ entity.setRevokeReason(null);
|
|
|
|
|
+
|
|
|
|
|
+ certificateMapper.updateById(entity);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("证书已解冻: id={}, serial={}", entity.getId(), entity.getCertSerialNumber());
|
|
|
|
|
+ return ResponseDTO.ok(entity);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseDTO<CertificateEntity> revokeCertificate(RevokeCertificateRequest request) {
|
|
|
|
|
+ if (request == null || request.getId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("证书ID不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CertificateEntity cert = certificateMapper.selectById(request.getId());
|
|
|
|
|
+ if (cert == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("证书未找到: id=" + request.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ("4".equals(cert.getStatus())) {
|
|
|
|
|
+ return ResponseDTO.ok(cert);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cert.setStatus("4");
|
|
|
|
|
+ cert.setRevokedTime(LocalDateTime.now());
|
|
|
|
|
+ cert.setRevokeReason(
|
|
|
|
|
+ request.getReason() == null || request.getReason().trim().isEmpty()
|
|
|
|
|
+ ? "certificate revoked"
|
|
|
|
|
+ : request.getReason()
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ certificateMapper.updateById(cert);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("证书已吊销: id={}, serial={}", cert.getId(), cert.getCertSerialNumber());
|
|
|
|
|
+ return ResponseDTO.ok(cert);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseEntity<String> downloadCrl(Long issuerId, HttpServletResponse response) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ CertificateEntity issuer = certificateMapper.selectById(issuerId);
|
|
|
|
|
+ if (issuer == null) {
|
|
|
|
|
+ return ResponseEntity.notFound().build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String issuerSerial = issuer.getCertSerialNumber();
|
|
|
|
|
+
|
|
|
|
|
+ List<CertificateEntity> revokedList = certificateMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<CertificateEntity>()
|
|
|
|
|
+ .eq(CertificateEntity::getStatus, "2")
|
|
|
|
|
+ .and(w -> w.eq(CertificateEntity::getParentSerial, issuerSerial)
|
|
|
|
|
+ .or()
|
|
|
|
|
+ .eq(CertificateEntity::getCertSerialNumber, issuerSerial))
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ String issuerCertPem = storageService.readCertificateFile(issuerSerial);
|
|
|
|
|
+ String issuerPrivateKeyPem = storageService.readPrivateKeyFile(issuerSerial);
|
|
|
|
|
+
|
|
|
|
|
+ CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC");
|
|
|
|
|
+ X509Certificate issuerCert;
|
|
|
|
|
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(issuerCertPem.getBytes(StandardCharsets.UTF_8))) {
|
|
|
|
|
+ issuerCert = (X509Certificate) cf.generateCertificate(bais);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ PrivateKey issuerPrivateKey = parsePrivateKeyPem(issuerPrivateKeyPem, issuer.getCertAlgo());
|
|
|
|
|
+
|
|
|
|
|
+ X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(
|
|
|
|
|
+ X500Name.getInstance(issuerCert.getSubjectX500Principal().getEncoded()),
|
|
|
|
|
+ new Date()
|
|
|
|
|
+ );
|
|
|
|
|
+ crlBuilder.setNextUpdate(Date.from(Instant.now().plusSeconds(86400L)));
|
|
|
|
|
+
|
|
|
|
|
+ for (CertificateEntity revoked : revokedList) {
|
|
|
|
|
+ if (revoked.getRevokedTime() == null) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ BigInteger serial = new BigInteger(revoked.getCertSerialNumber(), 16);
|
|
|
|
|
+ Date revocationDate = Date.from(revoked.getRevokedTime().atZone(ZoneId.systemDefault()).toInstant());
|
|
|
|
|
+ crlBuilder.addCRLEntry(serial, revocationDate, CRLReason.privilegeWithdrawn);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(issuer.getCertAlgo()))
|
|
|
|
|
+ .setProvider("BC")
|
|
|
|
|
+ .build(issuerPrivateKey);
|
|
|
|
|
+
|
|
|
|
|
+ X509CRLHolder crlHolder = crlBuilder.build(signer);
|
|
|
|
|
+ X509CRL crl = new JcaX509CRLConverter().setProvider("BC").getCRL(crlHolder);
|
|
|
|
|
+
|
|
|
|
|
+ String crlPem = convertCrlToPem(crl);
|
|
|
|
|
+
|
|
|
|
|
+ response.setContentType("application/pkix-crl");
|
|
|
|
|
+ response.setHeader("Content-Disposition", "attachment; filename=" + issuerSerial + "_crl.pem");
|
|
|
|
|
+
|
|
|
|
|
+ return ResponseEntity.ok()
|
|
|
|
|
+ .header("Content-Disposition", "attachment; filename=" + issuerSerial + "_crl.pem")
|
|
|
|
|
+ .header("Content-Type", "application/pkix-crl")
|
|
|
|
|
+ .body(crlPem);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("生成CRL失败", e);
|
|
|
|
|
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseEntity<byte[]> respondToOcspRequest(byte[] requestBytes) throws Exception {
|
|
|
|
|
+ OCSPReq request;
|
|
|
|
|
+ try {
|
|
|
|
|
+ request = new OCSPReq(requestBytes);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return ocspResponse(OCSPRespBuilder.MALFORMED_REQUEST, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ org.bouncycastle.cert.ocsp.Req[] requests = request.getRequestList();
|
|
|
|
|
+ if (requests.length == 0) {
|
|
|
|
|
+ return ocspResponse(OCSPRespBuilder.MALFORMED_REQUEST, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ DigestCalculatorProvider digestProvider = new JcaDigestCalculatorProviderBuilder()
|
|
|
|
|
+ .setProvider("BC")
|
|
|
|
|
+ .build();
|
|
|
|
|
+ CertificateID firstCertId = requests[0].getCertID();
|
|
|
|
|
+ CertificateEntity issuerEntity = null;
|
|
|
|
|
+ X509Certificate issuerCert = null;
|
|
|
|
|
+ X509CertificateHolder issuerHolder = null;
|
|
|
|
|
+
|
|
|
|
|
+ List<CertificateEntity> issuers = certificateMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<CertificateEntity>()
|
|
|
|
|
+ .eq(CertificateEntity::getIsCa, "1")
|
|
|
|
|
+ .or()
|
|
|
|
|
+ .eq(CertificateEntity::getIsRoot, "1")
|
|
|
|
|
+ );
|
|
|
|
|
+ for (CertificateEntity candidate : issuers) {
|
|
|
|
|
+ X509Certificate candidateCert = readCertificate(candidate.getCertSerialNumber());
|
|
|
|
|
+ X509CertificateHolder candidateHolder = new X509CertificateHolder(candidateCert.getEncoded());
|
|
|
|
|
+ if (firstCertId.matchesIssuer(candidateHolder, digestProvider)) {
|
|
|
|
|
+ issuerEntity = candidate;
|
|
|
|
|
+ issuerCert = candidateCert;
|
|
|
|
|
+ issuerHolder = candidateHolder;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (issuerEntity == null) {
|
|
|
|
|
+ return ocspResponse(OCSPRespBuilder.UNAUTHORIZED, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ BasicOCSPRespBuilder responseBuilder = new JcaBasicOCSPRespBuilder(
|
|
|
|
|
+ issuerCert.getPublicKey(),
|
|
|
|
|
+ digestProvider.get(CertificateID.HASH_SHA1)
|
|
|
|
|
+ );
|
|
|
|
|
+ Extension nonce = request.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
|
|
|
|
|
+ if (nonce != null) {
|
|
|
|
|
+ responseBuilder.setResponseExtensions(new Extensions(
|
|
|
|
|
+ new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, nonce.getExtnValue())
|
|
|
|
|
+ ));
|
|
|
|
|
+ }
|
|
|
|
|
+ Date thisUpdate = new Date();
|
|
|
|
|
+ Date nextUpdate = Date.from(Instant.now().plusSeconds(300));
|
|
|
|
|
+ String issuerSerial = issuerEntity.getCertSerialNumber();
|
|
|
|
|
+ for (org.bouncycastle.cert.ocsp.Req ocspRequest : requests) {
|
|
|
|
|
+ CertificateID certId = ocspRequest.getCertID();
|
|
|
|
|
+ if (!certId.matchesIssuer(issuerHolder, digestProvider)) {
|
|
|
|
|
+ return ocspResponse(OCSPRespBuilder.UNAUTHORIZED, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String serialNumber = certId.getSerialNumber().toString(16).toUpperCase(Locale.ROOT);
|
|
|
|
|
+ String paddedSerialNumber = String.format("%032X", certId.getSerialNumber());
|
|
|
|
|
+ CertificateEntity certificate = certificateMapper.selectOne(
|
|
|
|
|
+ new LambdaQueryWrapper<CertificateEntity>()
|
|
|
|
|
+ .in(CertificateEntity::getCertSerialNumber, serialNumber, paddedSerialNumber)
|
|
|
|
|
+ .and(wrapper -> wrapper.eq(CertificateEntity::getParentSerial, issuerSerial)
|
|
|
|
|
+ .or()
|
|
|
|
|
+ .eq(CertificateEntity::getCertSerialNumber, issuerSerial))
|
|
|
|
|
+ .last("LIMIT 1")
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ CertificateStatus status;
|
|
|
|
|
+ String certificateStatus = certificate == null ? null : certificate.getStatus();
|
|
|
|
|
+ if (certificateStatus == null
|
|
|
|
|
+ || !certificateStatus.equals("1")
|
|
|
|
|
+ && !certificateStatus.equals("2")
|
|
|
|
|
+ && !certificateStatus.equals("3")
|
|
|
|
|
+ && !certificateStatus.equals("4")) {
|
|
|
|
|
+ status = new UnknownStatus();
|
|
|
|
|
+ } else if ("3".equals(certificateStatus) || "4".equals(certificateStatus)) {
|
|
|
|
|
+ LocalDateTime revokedAt = certificate.getRevokedTime() == null
|
|
|
|
|
+ ? LocalDateTime.now()
|
|
|
|
|
+ : certificate.getRevokedTime();
|
|
|
|
|
+ int reason = "3".equals(certificateStatus)
|
|
|
|
|
+ ? CRLReason.certificateHold
|
|
|
|
|
+ : CRLReason.unspecified;
|
|
|
|
|
+ status = new RevokedStatus(
|
|
|
|
|
+ Date.from(revokedAt.atZone(ZoneId.systemDefault()).toInstant()),
|
|
|
|
|
+ reason
|
|
|
|
|
+ );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // OCSP has no expired status; clients validate the certificate validity period.
|
|
|
|
|
+ status = CertificateStatus.GOOD;
|
|
|
|
|
+ }
|
|
|
|
|
+ responseBuilder.addResponse(certId, status, thisUpdate, nextUpdate);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String issuerPrivateKeyPem = storageService.readPrivateKeyFile(issuerSerial);
|
|
|
|
|
+ PrivateKey issuerPrivateKey = parsePrivateKeyPem(issuerPrivateKeyPem, issuerEntity.getCertAlgo());
|
|
|
|
|
+ ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(issuerEntity.getCertAlgo()))
|
|
|
|
|
+ .setProvider("BC")
|
|
|
|
|
+ .build(issuerPrivateKey);
|
|
|
|
|
+ BasicOCSPResp basicResponse = responseBuilder.build(
|
|
|
|
|
+ signer,
|
|
|
|
|
+ new X509CertificateHolder[]{issuerHolder},
|
|
|
|
|
+ thisUpdate
|
|
|
|
|
+ );
|
|
|
|
|
+ return ocspResponse(OCSPRespBuilder.SUCCESSFUL, basicResponse);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private X509Certificate readCertificate(String serialNumber) throws Exception {
|
|
|
|
|
+ String certificatePem = storageService.readCertificateFile(serialNumber);
|
|
|
|
|
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509", "BC");
|
|
|
|
|
+ try (ByteArrayInputStream input = new ByteArrayInputStream(certificatePem.getBytes(StandardCharsets.UTF_8))) {
|
|
|
|
|
+ return (X509Certificate) certificateFactory.generateCertificate(input);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private ResponseEntity<byte[]> ocspResponse(int status, BasicOCSPResp response) throws Exception {
|
|
|
|
|
+ byte[] encoded = new OCSPRespBuilder().build(status, response).getEncoded();
|
|
|
|
|
+ return ResponseEntity.ok()
|
|
|
|
|
+ .contentType(MediaType.parseMediaType("application/ocsp-response"))
|
|
|
|
|
+ .body(encoded);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void addOcspAiaExtension(X509v3CertificateBuilder certificateBuilder) throws Exception {
|
|
|
|
|
+ if (ocspResponderUrl == null || ocspResponderUrl.isBlank()) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ AuthorityInformationAccess aia = new AuthorityInformationAccess(
|
|
|
|
|
+ new AccessDescription(
|
|
|
|
|
+ AccessDescription.id_ad_ocsp,
|
|
|
|
|
+ new GeneralName(GeneralName.uniformResourceIdentifier, ocspResponderUrl)
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
|
|
+ certificateBuilder.addExtension(Extension.authorityInfoAccess, false, aia);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseDTO<String> uploadRootCertificate(String pemCombined, String certName) throws Exception {
|
|
|
|
|
+ if (pemCombined == null || pemCombined.trim().isEmpty()) {
|
|
|
|
|
+ return ResponseDTO.error(100001, "上传文件内容为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 提取第一个证书 PEM
|
|
|
|
|
+ int certBegin = pemCombined.indexOf("-----BEGIN CERTIFICATE-----");
|
|
|
|
|
+ int certEnd = pemCombined.indexOf("-----END CERTIFICATE-----", certBegin);
|
|
|
|
|
+ if (certBegin < 0 || certEnd < 0) {
|
|
|
|
|
+ return ResponseDTO.error(100001, "未找到证书内容(PEM 格式)");
|
|
|
|
|
+ }
|
|
|
|
|
+ String certPem = pemCombined.substring(certBegin, certEnd + "-----END CERTIFICATE-----".length()).trim();
|
|
|
|
|
+
|
|
|
|
|
+ // 提取私钥 PEM(支持 PKCS#8 与 RSA PKCS#1)
|
|
|
|
|
+ String privateKeyPem = null;
|
|
|
|
|
+ String[][] privMarkers = {
|
|
|
|
|
+ {"-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----"},
|
|
|
|
|
+ {"-----BEGIN RSA PRIVATE KEY-----", "-----END RSA PRIVATE KEY-----"},
|
|
|
|
|
+ {"-----BEGIN EC PRIVATE KEY-----", "-----END EC PRIVATE KEY-----"}
|
|
|
|
|
+ };
|
|
|
|
|
+ for (String[] m : privMarkers) {
|
|
|
|
|
+ int b = pemCombined.indexOf(m[0]);
|
|
|
|
|
+ if (b >= 0) {
|
|
|
|
|
+ int e = pemCombined.indexOf(m[1], b);
|
|
|
|
|
+ if (e > b) {
|
|
|
|
|
+ privateKeyPem = pemCombined.substring(b, e + m[1].length()).trim();
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (privateKeyPem == null) {
|
|
|
|
|
+ return ResponseDTO.error(100001, "未找到私钥内容(PEM 格式),上传文件必须包含私钥");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析证书对象
|
|
|
|
|
+ java.security.cert.CertificateFactory cf =
|
|
|
|
|
+ java.security.cert.CertificateFactory.getInstance("X.509", "BC");
|
|
|
|
|
+ X509Certificate cert;
|
|
|
|
|
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(certPem.getBytes(StandardCharsets.UTF_8))) {
|
|
|
|
|
+ cert = (X509Certificate) cf.generateCertificate(bais);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (cert.getBasicConstraints() < 0) {
|
|
|
|
|
+ return ResponseDTO.error(100001, "上传证书不是 CA 证书(BasicConstraints 未标记 CA)");
|
|
|
|
|
+ }
|
|
|
|
|
+ boolean[] keyUsage = cert.getKeyUsage();
|
|
|
|
|
+ if (keyUsage == null || keyUsage.length <= 5 || !keyUsage[5]) {
|
|
|
|
|
+ return ResponseDTO.error(100001, "上传证书的 KeyUsage 未包含 keyCertSign");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
|
|
|
|
|
+ return ResponseDTO.error(100001, "上传的根证书不是自签名证书");
|
|
|
|
|
+ }
|
|
|
|
|
+ cert.verify(cert.getPublicKey());
|
|
|
|
|
+ cert.checkValidity();
|
|
|
|
|
+
|
|
|
|
|
+ String pubAlgo = cert.getPublicKey().getAlgorithm();
|
|
|
|
|
+ String certAlgo = ("EC".equalsIgnoreCase(pubAlgo) || "SM2".equalsIgnoreCase(pubAlgo)) ? "2" : "RSA";
|
|
|
|
|
+ PrivateKey privateKey = parsePrivateKeyPem(privateKeyPem, certAlgo);
|
|
|
|
|
+ java.security.Signature keyMatchSignature =
|
|
|
|
|
+ java.security.Signature.getInstance(cert.getSigAlgName(), "BC");
|
|
|
|
|
+ byte[] challenge = UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
+ keyMatchSignature.initSign(privateKey);
|
|
|
|
|
+ keyMatchSignature.update(challenge);
|
|
|
|
|
+ byte[] proof = keyMatchSignature.sign();
|
|
|
|
|
+ keyMatchSignature.initVerify(cert.getPublicKey());
|
|
|
|
|
+ keyMatchSignature.update(challenge);
|
|
|
|
|
+ if (!keyMatchSignature.verify(proof)) {
|
|
|
|
|
+ return ResponseDTO.error(100001, "上传的私钥与证书公钥不匹配");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 构造序列号(与系统其它处一致的十六进制大写)
|
|
|
|
|
+ String serialHex = cert.getSerialNumber().toString(16).toUpperCase();
|
|
|
|
|
+
|
|
|
|
|
+ // 唯一性检查:若已有该序列的记录,幂等返回
|
|
|
|
|
+ CertificateEntity exist = certificateMapper.selectOne(
|
|
|
|
|
+ new LambdaQueryWrapper<CertificateEntity>().eq(CertificateEntity::getCertSerialNumber, serialHex)
|
|
|
|
|
+ );
|
|
|
|
|
+ if (exist != null) {
|
|
|
|
|
+ log.info("根证书已存在,返回已有记录: serial={}", serialHex);
|
|
|
|
|
+ return ResponseDTO.error(100001,"证书已存在");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 公钥 PEM
|
|
|
|
|
+ String publicKeyPem = convertPublicKeyToPem(cert.getPublicKey());
|
|
|
|
|
+
|
|
|
|
|
+ // 证书链:这里以单个根证书为链(如需要可从 pemCombined 提取更多证书)
|
|
|
|
|
+ String chainPem = certPem;
|
|
|
|
|
+
|
|
|
|
|
+ // 存储文件(MinIO),storeCertificateFiles 对私钥做加密存储
|
|
|
|
|
+ CertificateFileInfo fileInfo = certificateStorageService.storeCertificateFiles(
|
|
|
|
|
+ serialHex,
|
|
|
|
|
+ certPem,
|
|
|
|
|
+ privateKeyPem,
|
|
|
|
|
+ publicKeyPem,
|
|
|
|
|
+ chainPem
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 保存到数据库(字段与 issueRootCertificate 保持一致)
|
|
|
|
|
+ CertificateEntity entity = new CertificateEntity();
|
|
|
|
|
+ entity.setCertName((certName != null && !certName.trim().isEmpty()) ? certName : cert.getSubjectX500Principal().getName());
|
|
|
|
|
+ entity.setCertSerialNumber(serialHex);
|
|
|
|
|
+ entity.setCertSubject(cert.getSubjectX500Principal().getName());
|
|
|
|
|
+ entity.setCertAlgo(certAlgo);
|
|
|
|
|
+ entity.setValidFrom(LocalDateTime.ofInstant(cert.getNotBefore().toInstant(), ZoneId.systemDefault()));
|
|
|
|
|
+ entity.setValidTo(LocalDateTime.ofInstant(cert.getNotAfter().toInstant(), ZoneId.systemDefault()));
|
|
|
|
|
+ entity.setStatus("1");
|
|
|
|
|
+ entity.setIsCa("1");
|
|
|
|
|
+ entity.setIsRoot("1");
|
|
|
|
|
+ entity.setParentSerial(null);
|
|
|
|
|
+ entity.setCertUsage("ROOT_CA");
|
|
|
|
|
+ entity.setCertFilePath(fileInfo.getCertPath());
|
|
|
|
|
+ entity.setPrivateKeyPath(fileInfo.getPrivateKeyPath());
|
|
|
|
|
+ entity.setPublicKeyPath(fileInfo.getPublicKeyPath());
|
|
|
|
|
+ entity.setChainPath(fileInfo.getChainPath());
|
|
|
|
|
+
|
|
|
|
|
+ certificateMapper.insert(entity);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("上传根证书并保存成功: serial={}", serialHex);
|
|
|
|
|
+ return ResponseDTO.ok();
|
|
|
|
|
+ }
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseEntity<Map<String, String>> downloadPemSeparate(Long id) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ CertificateEntity cert = certificateMapper.selectById(id);
|
|
|
|
|
+ if (cert == null) {
|
|
|
|
|
+ return ResponseEntity.notFound().build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ DownloadResponse response = assembleService.downloadPemSeparate(cert.getCertSerialNumber());
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, String> result = new HashMap<>();
|
|
|
|
|
+ result.put("certificate", response.getCertContent());
|
|
|
|
|
+ result.put("privateKey", response.getPrivateKeyContent());
|
|
|
|
|
+ result.put("publicKey", response.getPublicKeyContent());
|
|
|
|
|
+
|
|
|
|
|
+ return ResponseEntity.ok(result);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("下载PEM失败", e);
|
|
|
|
|
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseEntity<byte[]> downloadPemCombined(Long id, HttpServletResponse response) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ CertificateEntity cert = certificateMapper.selectById(id);
|
|
|
|
|
+ if (cert == null) {
|
|
|
|
|
+ return ResponseEntity.notFound().build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ DownloadResponse downloadResponse = assembleService.downloadPemCombined(cert.getCertSerialNumber());
|
|
|
|
|
+
|
|
|
|
|
+ response.setContentType("application/x-pem-file");
|
|
|
|
|
+ response.setHeader("Content-Disposition",
|
|
|
|
|
+ "attachment; filename=" + cert.getCertSerialNumber() + "_full.pem");
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ return ResponseEntity.ok()
|
|
|
|
|
+ .header("Content-Disposition",
|
|
|
|
|
+ "attachment; filename=" + cert.getCertSerialNumber() + "_full.pem")
|
|
|
|
|
+ .header("Content-Type", "application/x-pem-file")
|
|
|
|
|
+ .body(downloadResponse.getCertContent().getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("下载合并PEM失败", e);
|
|
|
|
|
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseEntity<byte[]> downloadPkcs12(Pkcs12DownloadRequest request, HttpServletResponse response) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ CertificateEntity cert = certificateMapper.selectById(request.getId());
|
|
|
|
|
+ if (cert == null) {
|
|
|
|
|
+ return ResponseEntity.notFound().build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证密码强度
|
|
|
|
|
+ String password = request.getPassword();
|
|
|
|
|
+ if (password == null || password.length() < 6) {
|
|
|
|
|
+ return ResponseEntity.badRequest()
|
|
|
|
|
+ .header("X-Error", "密码至少6位")
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ DownloadResponse downloadResponse = assembleService.downloadPkcs12(
|
|
|
|
|
+ cert.getCertSerialNumber(),
|
|
|
|
|
+ password
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 设置响应头
|
|
|
|
|
+ response.setContentType("application/x-pkcs12");
|
|
|
|
|
+ response.setHeader("Content-Disposition",
|
|
|
|
|
+ "attachment; filename=" + cert.getCertSerialNumber() + ".p12");
|
|
|
|
|
+ response.setHeader("X-Password", password); // 返回密码给用户(可选)
|
|
|
|
|
+
|
|
|
|
|
+ return ResponseEntity.ok()
|
|
|
|
|
+ .header("Content-Disposition",
|
|
|
|
|
+ "attachment; filename=" + cert.getCertSerialNumber() + ".p12")
|
|
|
|
|
+ .header("Content-Type", "application/x-pkcs12")
|
|
|
|
|
+ .body(downloadResponse.getBinaryContent());
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("下载PKCS#12失败", e);
|
|
|
|
|
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
|
|
|
+ .header("X-Error", e.getMessage())
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public ResponseEntity<String> downloadChain(String id, HttpServletResponse response) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ CertificateEntity cert = certificateMapper.selectById(id);
|
|
|
|
|
+ if (cert == null) {
|
|
|
|
|
+ return ResponseEntity.notFound().build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 从MinIO读取证书链(注意:MinIO 路径按证书序列号组织,不是数据库 id)
|
|
|
|
|
+ String chain = storageService.readChainFile(cert.getCertSerialNumber());
|
|
|
|
|
+ if (chain == null || chain.isEmpty()) {
|
|
|
|
|
+ // 如果没有证书链,返回证书本身
|
|
|
|
|
+ chain = storageService.readCertificateFile(cert.getCertSerialNumber());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ response.setContentType("application/x-pem-file");
|
|
|
|
|
+ response.setHeader("Content-Disposition",
|
|
|
|
|
+ "attachment; filename=" + id + "_chain.pem");
|
|
|
|
|
+
|
|
|
|
|
+ return ResponseEntity.ok()
|
|
|
|
|
+ .header("Content-Disposition",
|
|
|
|
|
+ "attachment; filename=" + id + "_chain.pem")
|
|
|
|
|
+ .header("Content-Type", "application/x-pem-file")
|
|
|
|
|
+ .body(chain);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("下载证书链失败", e);
|
|
|
|
|
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String getSignatureAlgorithm(String keyAlgorithm) {
|
|
|
|
|
+ if ("2".equalsIgnoreCase(keyAlgorithm)) {
|
|
|
|
|
+ return "SM3withSM2";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return "SHA256WithRSA"; // RSA默认
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 证书转PEM
|
|
|
|
|
+ */
|
|
|
|
|
+ private String convertCertificateToPem(X509Certificate cert) throws Exception {
|
|
|
|
|
+ String base64 = Base64.getEncoder().encodeToString(cert.getEncoded());
|
|
|
|
|
+ StringBuilder pem = new StringBuilder();
|
|
|
|
|
+ pem.append("-----BEGIN CERTIFICATE-----\n");
|
|
|
|
|
+ for (int i = 0; i < base64.length(); i += 64) {
|
|
|
|
|
+ int end = Math.min(i + 64, base64.length());
|
|
|
|
|
+ pem.append(base64, i, end).append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ pem.append("-----END CERTIFICATE-----\n");
|
|
|
|
|
+ return pem.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 公钥转PEM
|
|
|
|
|
+ */
|
|
|
|
|
+ private String convertPublicKeyToPem(PublicKey publicKey) throws Exception {
|
|
|
|
|
+ String base64 = Base64.getEncoder().encodeToString(publicKey.getEncoded());
|
|
|
|
|
+ StringBuilder pem = new StringBuilder();
|
|
|
|
|
+ pem.append("-----BEGIN PUBLIC KEY-----\n");
|
|
|
|
|
+ for (int i = 0; i < base64.length(); i += 64) {
|
|
|
|
|
+ int end = Math.min(i + 64, base64.length());
|
|
|
|
|
+ pem.append(base64, i, end).append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ pem.append("-----END PUBLIC KEY-----\n");
|
|
|
|
|
+ return pem.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 私钥转PEM
|
|
|
|
|
+ */
|
|
|
|
|
+ private String convertPrivateKeyToPem(PrivateKey privateKey) throws Exception {
|
|
|
|
|
+ String base64 = Base64.getEncoder().encodeToString(privateKey.getEncoded());
|
|
|
|
|
+ StringBuilder pem = new StringBuilder();
|
|
|
|
|
+ pem.append("-----BEGIN PRIVATE KEY-----\n");
|
|
|
|
|
+ for (int i = 0; i < base64.length(); i += 64) {
|
|
|
|
|
+ int end = Math.min(i + 64, base64.length());
|
|
|
|
|
+ pem.append(base64, i, end).append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ pem.append("-----END PRIVATE KEY-----\n");
|
|
|
|
|
+ return pem.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析 PEM 私钥(支持 PKCS8 与 RSA PKCS#1),根据算法选择 KeyFactory(RSA 或 EC)
|
|
|
|
|
+ */
|
|
|
|
|
+ private PrivateKey parsePrivateKeyPem(String privateKeyPem, String algoHint) throws Exception {
|
|
|
|
|
+ String base64 = privateKeyPem
|
|
|
|
|
+ .replace("-----BEGIN PRIVATE KEY-----", "")
|
|
|
|
|
+ .replace("-----END PRIVATE KEY-----", "")
|
|
|
|
|
+ .replace("-----BEGIN RSA PRIVATE KEY-----", "")
|
|
|
|
|
+ .replace("-----END RSA PRIVATE KEY-----", "")
|
|
|
|
|
+ .replaceAll("\\s", "");
|
|
|
|
|
+ byte[] keyBytes = Base64.getDecoder().decode(base64);
|
|
|
|
|
+ boolean ecKey = isEcKeyAlgorithm(algoHint);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
|
|
|
|
+ if (ecKey) {
|
|
|
|
|
+ return KeyFactory.getInstance("EC", "BC").generatePrivate(spec);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return KeyFactory.getInstance("RSA", "BC").generatePrivate(spec);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ if (ecKey) {
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ org.bouncycastle.asn1.pkcs.RSAPrivateKey rsa = org.bouncycastle.asn1.pkcs.RSAPrivateKey.getInstance(keyBytes);
|
|
|
|
|
+ java.security.spec.RSAPrivateCrtKeySpec crtSpec =
|
|
|
|
|
+ new java.security.spec.RSAPrivateCrtKeySpec(
|
|
|
|
|
+ rsa.getModulus(),
|
|
|
|
|
+ rsa.getPublicExponent(),
|
|
|
|
|
+ rsa.getPrivateExponent(),
|
|
|
|
|
+ rsa.getPrime1(),
|
|
|
|
|
+ rsa.getPrime2(),
|
|
|
|
|
+ rsa.getExponent1(),
|
|
|
|
|
+ rsa.getExponent2(),
|
|
|
|
|
+ rsa.getCoefficient()
|
|
|
|
|
+ );
|
|
|
|
|
+ return KeyFactory.getInstance("RSA", "BC").generatePrivate(crtSpec);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * PEM公钥解析工具
|
|
|
|
|
+ */
|
|
|
|
|
+ private PublicKey parsePublicKeyPem(String publicKeyPem, String algoHint) throws Exception {
|
|
|
|
|
+ String base64 = publicKeyPem
|
|
|
|
|
+ .replace("-----BEGIN PUBLIC KEY-----", "")
|
|
|
|
|
+ .replace("-----END PUBLIC KEY-----", "")
|
|
|
|
|
+ .replaceAll("\\s", "");
|
|
|
|
|
+ byte[] keyBytes = Base64.getDecoder().decode(base64);
|
|
|
|
|
+ X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
|
|
|
|
|
+ if (isEcKeyAlgorithm(algoHint)) {
|
|
|
|
|
+ return KeyFactory.getInstance("EC", "BC").generatePublic(spec);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return KeyFactory.getInstance("RSA", "BC").generatePublic(spec);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isEcKeyAlgorithm(String algorithm) {
|
|
|
|
|
+ return "2".equalsIgnoreCase(algorithm)
|
|
|
|
|
+ || "SM2".equalsIgnoreCase(algorithm)
|
|
|
|
|
+ || "EC".equalsIgnoreCase(algorithm);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String convertCrlToPem(X509CRL crl) throws Exception {
|
|
|
|
|
+ String base64 = Base64.getEncoder().encodeToString(crl.getEncoded());
|
|
|
|
|
+ StringBuilder pem = new StringBuilder();
|
|
|
|
|
+ pem.append("-----BEGIN X509 CRL-----\n");
|
|
|
|
|
+ for (int i = 0; i < base64.length(); i += 64) {
|
|
|
|
|
+ int end = Math.min(i + 64, base64.length());
|
|
|
|
|
+ pem.append(base64, i, end).append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ pem.append("-----END X509 CRL-----\n");
|
|
|
|
|
+ return pem.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|