Parcourir la source

1.新增ocsp验证逻辑和验证接口

wangkun il y a 3 semaines
Parent
commit
a61426b4dc

+ 6 - 2
src/main/java/com/ymwk/icvp/pki/controller/CertificateController.java

@@ -48,11 +48,16 @@ public class CertificateController {
     public ResponseDTO<CertificateEntity> revokeCertificate(@RequestBody RevokeCertificateRequest request) throws Exception {
         return certificateService.revokeCertificate(request);
     }
-    //下载crl
+    // Deprecated: use OCSP for certificate status checks.
+    @Deprecated
     @PostMapping("/crl")
     public ResponseEntity<String> downloadCrl(@RequestBody DownloadRequest request, HttpServletResponse response) {
         return certificateService.downloadCrl(request.getId(), response);
     }
+    @PostMapping(value = "/ocsp", consumes = "application/ocsp-request", produces = "application/ocsp-response")
+    public ResponseEntity<byte[]> ocsp(@RequestBody byte[] requestBytes) throws Exception {
+        return certificateService.respondToOcspRequest(requestBytes);
+    }
     // 上传根证书(接收合并 PEM:cert [+ chain] + private key)
     @PostMapping("/uploadRoot")
     public ResponseDTO<String> uploadRootCertificate(@RequestParam("file") MultipartFile file,
@@ -105,7 +110,6 @@ public class CertificateController {
     @PostMapping("/download/chain")
     public ResponseEntity<String> downloadChain(@RequestBody String id, HttpServletResponse response) {
        return certificateService.downloadChain(id, response);
-
     }
 
 

+ 1 - 1
src/main/java/com/ymwk/icvp/pki/model/IssueRequest.java

@@ -47,7 +47,7 @@ public class IssueRequest {
     private String certUsage;
 
     @Min(1)
-    @Max(3650)
+    @Max(7300)
     private Integer validityDays = 365;
 
     /**

+ 1 - 0
src/main/java/com/ymwk/icvp/pki/model/certListRequest.java

@@ -12,4 +12,5 @@ public class certListRequest extends PageParam {
     private String serialNumberStr;
     private String certUsage;
     private String status;
+    private Integer certAlgo;
 }

+ 2 - 0
src/main/java/com/ymwk/icvp/pki/service/CertificateService.java

@@ -36,5 +36,7 @@ public interface CertificateService {
 
     ResponseEntity<String> downloadCrl(Long id, HttpServletResponse response);
 
+    ResponseEntity<byte[]> respondToOcspRequest(byte[] requestBytes) throws Exception;
+
     ResponseDTO<String> uploadRootCertificate(String content, String certName) throws Exception;
 }

+ 223 - 19
src/main/java/com/ymwk/icvp/pki/service/impl/CertificateServiceImpl.java

@@ -13,6 +13,17 @@ import com.ymwk.icvp.pki.service.CertificateService;
 import com.ymwk.icvp.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;
@@ -25,11 +36,16 @@ 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 lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
 
 import java.io.ByteArrayInputStream;
 import java.math.BigInteger;
@@ -43,6 +59,7 @@ 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;
@@ -63,6 +80,9 @@ public class CertificateServiceImpl implements CertificateService {
     @Resource
     private CertificateStorageService storageService;
 
+    @Value("${ocsp.responder-url:}")
+    private String ocspResponderUrl;
+
     @Override
     public ResponseDTO<PageResult<CertificateEntity>> getCertificateList(certListRequest certListRequest) {
         LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<>();
@@ -70,6 +90,7 @@ public class CertificateServiceImpl implements CertificateService {
         wrapper.eq(certListRequest.getCertUsage() != null, CertificateEntity::getCertUsage, certListRequest.getCertUsage());
         wrapper.like(certListRequest.getSerialNumberStr() != null, CertificateEntity::getCertSerialNumber, certListRequest.getSerialNumberStr());
         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);
         Long count = certificateMapper.selectCount(wrapper);
@@ -80,10 +101,7 @@ public class CertificateServiceImpl implements CertificateService {
 
     @Override
     public ResponseDTO<CertificateEntity> issuePersonalCertificate(IssueRequest request) throws Exception {
-        // 1. 生成密钥对(申请者自己的密钥对)
-        KeyPair keyPair = issueCertUtil.generateKeyPair(request.getKeyAlgorithm(), request.getKeySize());
-
-        // 2. 找到用于签发的 Issuer(这里默认用 isRoot=1 的 Root CA,取第一个有效的)
+        // 1. 找到用于签发的 Issuer(这里默认用 isRoot=1 的 Root CA,取第一个有效的)
         Long issuerId = request.getIssuerId();
         if (issuerId == null) {
             throw new IllegalArgumentException("issuerId is required");
@@ -100,18 +118,28 @@ public class CertificateServiceImpl implements CertificateService {
             throw new IllegalStateException("冻结证书不能用于签发");
         }
 
-        // 3. 从存储读取 Issuer 的证书与私钥(privateKey 已在存储中加密,readPrivateKeyFile 会解密)
+        // 2. 从存储读取 Issuer 的证书与私钥(privateKey 已在存储中加密,readPrivateKeyFile 会解密)
         String issuerSerial = issuerEntity.getCertSerialNumber();
         String issuerCertPem = storageService.readCertificateFile(issuerSerial);
         String issuerPrivateKeyPem = storageService.readPrivateKeyFile(issuerSerial);
 
-        // 4. 解析 Issuer 证书与私钥
+        // 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))
@@ -128,8 +156,8 @@ public class CertificateServiceImpl implements CertificateService {
         X500Name subjectDN = x500NameBuilder.build();
 
         BigInteger serialNumber = new BigInteger(serialNumberStr, 16);
-        Date notBefore = new Date();
-        Date notAfter = Date.from(Instant.now().plusSeconds(request.getValidityDays() * 86400L));
+        Date notBefore = Date.from(issueTime);
+        Date notAfter = Date.from(issueTime.plusSeconds(validityDays * 86400L));
 
         // 6. 构建证书(Issuer = issuerCert.getSubjectX500Principal())
         X500Name issuerDN = X500Name.getInstance(issuerCert.getSubjectX500Principal().getEncoded());
@@ -147,6 +175,7 @@ public class CertificateServiceImpl implements CertificateService {
         // KeyUsage:普通证书常见 digitalSignature + keyEncipherment
         certBuilder.addExtension(Extension.keyUsage, true,
                 new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment));
+        addOcspAiaExtension(certBuilder);
 
         // ExtendedKeyUsage:根据请求 certUsage 可定制;这里默认 serverAuth + clientAuth
         KeyPurposeId[] purposes = new KeyPurposeId[] {
@@ -223,14 +252,36 @@ public class CertificateServiceImpl implements CertificateService {
         BigInteger serialNumber = new BigInteger(serialNumberStr, 16);
         // 生成证书请求
         X500NameBuilder x500NameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
-        x500NameBuilder.addRDN(BCStyle.C, request.getCountry());
-        x500NameBuilder.addRDN(BCStyle.CN, request.getCommonName());
-        x500NameBuilder.addRDN(BCStyle.OU, request.getOrganizationalUnit());
-        x500NameBuilder.addRDN(BCStyle.O, request.getOrganization());
-        x500NameBuilder.addRDN(BCStyle.ST, request.getState());
-        x500NameBuilder.addRDN(BCStyle.L, request.getLocality());
-        x500NameBuilder.addRDN(BCStyle.SERIALNUMBER, serialNumberStr);
-        x500NameBuilder.addRDN(BCStyle.TELEPHONE_NUMBER, request.getPhone());
+
+// 必填字段: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();
@@ -383,10 +434,18 @@ public class CertificateServiceImpl implements CertificateService {
             issuerAlgo = issuerEntity.getCertAlgo();
         }
 
+        Instant issueTime = Instant.now();
+        if (!"1".equals(entity.getIsRoot())) {
+            long availableDays = Math.max(0, Duration.between(issueTime, issuerCert.getNotAfter().toInstant()).toDays());
+            if (validityDays > availableDays) {
+                return ResponseDTO.error("申请有效期超出颁发者证书有效期,当前颁发者证书最多可用 " + availableDays + " 天", null);
+            }
+        }
+
         // 5. 构建新证书:沿用原序列号,更新有效期
         BigInteger serialNumber = new BigInteger(serial, 16);
-        Date notBefore = new Date();
-        Date notAfter = Date.from(Instant.now().plusSeconds((long) validityDays * 86400L));
+        Date notBefore = Date.from(issueTime);
+        Date notAfter = Date.from(issueTime.plusSeconds((long) validityDays * 86400L));
 
         X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
                 issuerDN,
@@ -407,6 +466,9 @@ public class CertificateServiceImpl implements CertificateService {
             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))
@@ -471,6 +533,8 @@ public class CertificateServiceImpl implements CertificateService {
         }
 
         entity.setStatus("3");
+        entity.setRevokedTime(LocalDateTime.now());
+        entity.setRevokeReason("certificateHold");
 
         certificateMapper.updateById(entity);
 
@@ -493,7 +557,8 @@ public class CertificateServiceImpl implements CertificateService {
         }
 
         entity.setStatus("1");
-
+        entity.setRevokedTime(null);
+        entity.setRevokeReason(null);
 
         certificateMapper.updateById(entity);
 
@@ -597,6 +662,145 @@ public class CertificateServiceImpl implements CertificateService {
         }
     }
 
+    @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()) {

+ 304 - 12
src/main/java/com/ymwk/icvp/signature/controller/SignatureController.java

@@ -28,9 +28,28 @@ import org.bouncycastle.cms.CMSProcessableByteArray;
 import org.bouncycastle.cms.CMSSignedData;
 import org.bouncycastle.cms.SignerInformation;
 import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
+import org.bouncycastle.asn1.ASN1OctetString;
+import org.bouncycastle.asn1.ASN1Primitive;
+import org.bouncycastle.asn1.x509.AccessDescription;
+import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
 import org.bouncycastle.cert.X509CertificateHolder;
 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.ocsp.BasicOCSPResp;
+import org.bouncycastle.cert.ocsp.CertificateID;
+import org.bouncycastle.cert.ocsp.CertificateStatus;
+import org.bouncycastle.cert.ocsp.OCSPReq;
+import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
+import org.bouncycastle.cert.ocsp.OCSPResp;
+import org.bouncycastle.cert.ocsp.OCSPRespBuilder;
+import org.bouncycastle.cert.ocsp.RevokedStatus;
+import org.bouncycastle.cert.ocsp.UnknownStatus;
+import org.bouncycastle.cert.ocsp.jcajce.JcaCertificateID;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.DigestCalculatorProvider;
+import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
 import org.bouncycastle.util.Store;
 import org.apache.commons.io.FileUtils;
 import org.ofdrw.core.basicType.ST_Box;
@@ -57,6 +76,10 @@ import org.springframework.web.multipart.MultipartFile;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.math.BigInteger;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.security.MessageDigest;
@@ -65,6 +88,7 @@ import java.security.PrivateKey;
 import java.security.GeneralSecurityException;
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
+import java.time.Instant;
 import java.util.*;
 import java.util.zip.ZipException;
 
@@ -434,6 +458,7 @@ public class SignatureController {
                         ? Collections.emptyList() : signatureList.getSignatures();
                 List<Map<String, Object>> signatures = new ArrayList<>();
                 boolean allValid = !signatureRecords.isEmpty();
+                boolean allCertificateChecksPerformed = !signatureRecords.isEmpty();
                 SESV4ValidateContainer signatureValidator = new SESV4ValidateContainer();
 
                 for (int i = 0; i < signatureRecords.size(); i++) {
@@ -447,6 +472,8 @@ public class SignatureController {
                     var resourceLocator = reader.getResourceLocator();
                     boolean contentIntegrityValid = false;
                     boolean cryptographicSignatureValid = false;
+                    boolean certificateStatusValid = false;
+                    boolean certificateStatusChecked = false;
                     Boolean sealMatched = null;
                     try {
                         ST_Loc signaturesPath = reader.getDefaultDocSignaturesPath();
@@ -523,6 +550,14 @@ public class SignatureController {
                                                             certificateBytes));
                                     signatureResult.put("signerName",
                                             certificate.getSubjectX500Principal().getName());
+                                    CertificateValidation certificateValidation =
+                                            validateSignerCertificate(certificate);
+                                    certificateStatusChecked = true;
+                                    certificateStatusValid = certificateValidation.valid();
+                                    signatureResult.put("certificateStatusValid", certificateStatusValid);
+                                    signatureResult.put("certificateStatusMessage",
+                                            certificateValidation.message());
+                                    signatureResult.put("certificateStatusChecked", true);
                                 }
                             } finally {
                                 resourceLocator.restore();
@@ -533,9 +568,12 @@ public class SignatureController {
                         signatureResult.put("contentIntegrityValid", contentIntegrityValid);
                         signatureResult.put("cryptographicSignatureValid",
                                 cryptographicSignatureValid);
+                        signatureResult.putIfAbsent("certificateStatusValid", certificateStatusValid);
+                        signatureResult.put("certificateStatusChecked", certificateStatusChecked);
                         signatureResult.put("sealMatched", sealMatched);
                         signatureResult.put("valid", contentIntegrityValid
                                 && cryptographicSignatureValid
+                                && certificateStatusValid
                                 && Boolean.TRUE.equals(signatureResult.get("structureValid"))
                                 && (sealMatched == null || sealMatched)
                                 && (signatureRecord.getType() == null
@@ -543,11 +581,16 @@ public class SignatureController {
                                         == org.ofdrw.core.signatures.SigType.Seal));
                         if (!contentIntegrityValid) {
                             signatureResult.put("message", "签章引用的文档内容摘要校验失败");
+                        } else if (!certificateStatusValid) {
+                            signatureResult.put("message", signatureResult.getOrDefault(
+                                    "certificateStatusMessage", "签名证书状态校验失败"));
                         }
                     } catch (Exception e) {
                         signatureResult.put("contentIntegrityValid", contentIntegrityValid);
                         signatureResult.put("cryptographicSignatureValid",
                                 cryptographicSignatureValid);
+                        signatureResult.put("certificateStatusValid", certificateStatusValid);
+                        signatureResult.put("certificateStatusChecked", certificateStatusChecked);
                         signatureResult.put("sealMatched", sealMatched);
                         signatureResult.putIfAbsent("structureValid", false);
                         signatureResult.put("valid", false);
@@ -555,6 +598,8 @@ public class SignatureController {
                     }
 
                     boolean signatureValid = Boolean.TRUE.equals(signatureResult.get("valid"));
+                    allCertificateChecksPerformed &= Boolean.TRUE.equals(
+                            signatureResult.get("certificateStatusChecked"));
                     signatures.add(signatureResult);
                     allValid &= signatureValid;
                 }
@@ -562,10 +607,10 @@ public class SignatureController {
                 verification.put("valid", allValid);
                 verification.put("signatureCount", signatures.size());
                 verification.put("signatures", signatures);
-                verification.put("trustChecked", false);
+                verification.put("trustChecked", allCertificateChecksPerformed);
                 verification.put("message", signatureRecords.isEmpty()
                         ? "OFD文件中没有电子签章"
-                        : allValid ? "所有OFD电子签章均通过校验" : "存在无效电子签章");
+                        : allValid ? "所有OFD电子签章及证书状态均通过校验" : "存在无效电子签章或证书状态异常");
             }
             return ofdSignatureResult(result, DataResponse.CODE_SUCCESS, "验签完成", verification);
         } catch (Exception e) {
@@ -608,7 +653,9 @@ public class SignatureController {
                     }
                 }
                 List<Map<String, Object>> signatures = new ArrayList<>();
-                boolean allValid = !signatureFields.isEmpty();
+                boolean allSignaturesValid = !signatureFields.isEmpty();
+                boolean allCertificateChecksPerformed = !signatureFields.isEmpty();
+                long latestSignedRevisionEnd = -1;
                 BouncyCastleProvider provider = new BouncyCastleProvider();
 
                 for (PDSignatureField signatureField : signatureFields) {
@@ -624,6 +671,15 @@ public class SignatureController {
                             && byteRange[2] >= byteRange[1]
                             && byteRange[3] >= 0
                             && (long) byteRange[2] + byteRange[3] == pdfBytes.length;
+                    if (byteRange != null
+                            && byteRange.length == 4
+                            && byteRange[0] == 0
+                            && byteRange[1] >= 0
+                            && byteRange[2] >= byteRange[1]
+                            && byteRange[3] >= 0) {
+                        latestSignedRevisionEnd = Math.max(
+                                latestSignedRevisionEnd, (long) byteRange[2] + byteRange[3]);
+                    }
                     signatureResult.put("coversWholeDocument", coversWholeDocument);
                     try {
                         byte[] signedContent = pdfSignature.getSignedContent(pdfBytes);
@@ -633,12 +689,17 @@ public class SignatureController {
                         Store<X509CertificateHolder> certificateStore = cmsSignedData.getCertificates();
                         Collection<SignerInformation> signerInfos = cmsSignedData.getSignerInfos().getSigners();
                         boolean valid = !signerInfos.isEmpty();
+                        boolean certificateStatusValid = !signerInfos.isEmpty();
+                        boolean certificateStatusChecked = !signerInfos.isEmpty();
                         String signerName = pdfSignature.getName();
+                        String certificateStatusMessage = "签名证书未能验证";
 
                         for (SignerInformation signerInfo : signerInfos) {
                             Collection<X509CertificateHolder> matchingCertificates =
                                     certificateStore.getMatches(signerInfo.getSID());
                             boolean signerValid = false;
+                            boolean signerCertificateStatusValid = false;
+                            boolean signerCertificateStatusChecked = false;
                             for (X509CertificateHolder certificateHolder : matchingCertificates) {
                                 var certificate = new JcaX509CertificateConverter()
                                         .setProvider(provider)
@@ -648,42 +709,61 @@ public class SignatureController {
                                         .build(certificate))) {
                                     signerValid = true;
                                     signerName = certificate.getSubjectX500Principal().getName();
+                                    CertificateValidation certificateValidation =
+                                            validateSignerCertificate(certificate);
+                                    signerCertificateStatusChecked = true;
+                                    signerCertificateStatusValid = certificateValidation.valid();
+                                    certificateStatusMessage = certificateValidation.message();
                                     break;
                                 }
                             }
                             valid &= signerValid;
+                            certificateStatusValid &= signerCertificateStatusValid;
+                            certificateStatusChecked &= signerCertificateStatusChecked;
                         }
 
-                        signatureResult.put("valid", valid);
+                        signatureResult.put("cryptographicSignatureValid", valid);
+                        signatureResult.put("certificateStatusValid", certificateStatusValid);
+                        signatureResult.put("certificateStatusChecked", certificateStatusChecked);
+                        signatureResult.put("certificateStatusMessage", certificateStatusMessage);
+                        signatureResult.put("valid", valid && certificateStatusValid);
                         signatureResult.put("signerName", signerName);
                         if (pdfSignature.getSignDate() != null) {
                             signatureResult.put("signingTime", pdfSignature.getSignDate().getTime());
                         }
                         if (!valid) {
                             signatureResult.put("message", "签名摘要或签名值校验失败");
-                            allValid = false;
+                        } else if (!certificateStatusValid) {
+                            signatureResult.put("message", certificateStatusMessage);
                         } else if (!coversWholeDocument) {
-                            signatureResult.put("message", "签名有效,但未覆盖整个PDF文档,签名后可能有内容变更");
-                            allValid = false;
+                            signatureResult.put("message", "签名有效,签名后文档中追加了后续增量内容");
                         }
                     } catch (Exception e) {
                         signatureResult.put("valid", false);
+                        signatureResult.put("certificateStatusValid", false);
+                        signatureResult.put("certificateStatusChecked", false);
                         signatureResult.put("message", e.getMessage());
-                        allValid = false;
                     }
+                    allSignaturesValid &= Boolean.TRUE.equals(signatureResult.get("valid"));
+                    allCertificateChecksPerformed &= Boolean.TRUE.equals(
+                            signatureResult.get("certificateStatusChecked"));
                     signatures.add(signatureResult);
                 }
 
+                boolean finalRevisionSigned = latestSignedRevisionEnd == pdfBytes.length;
+                boolean allValid = allSignaturesValid && finalRevisionSigned;
                 Map<String, Object> verification = new HashMap<>();
                 verification.put("valid", allValid);
                 verification.put("signatureCount", signatures.size());
                 verification.put("signatures", signatures);
+                verification.put("trustChecked", allCertificateChecksPerformed);
                 verification.put("message", signatureFields.isEmpty()
                         ? "PDF文件中没有电子签名"
                         : allValid
-                            ? "所有PDF签名均通过完整性校验"
-                            : "存在无效签名或签名后文档内容发生变化");
-                verification.put("trustChecked", false);
+                            ? "所有PDF签名及证书状态均有效,最终文档版本已签名"
+                            : !allSignaturesValid
+                                ? "存在无效PDF签名或签名证书状态异常"
+                                : "最终签名后文档内容发生变化或未被签名");
                 return ofdSignatureResult(result, DataResponse.CODE_SUCCESS, "验签完成", verification);
             }
         } catch (Exception e) {
@@ -710,6 +790,218 @@ public class SignatureController {
         return false;
     }
 
+    private CertificateValidation validateSignerCertificate(X509Certificate signerCertificate) {
+        try {
+            Set<String> visitedSerials = new HashSet<>();
+            X509Certificate currentCertificate = signerCertificate;
+            for (int depth = 0; depth < 10; depth++) {
+                String serialNumber = currentCertificate.getSerialNumber().toString(16)
+                        .toUpperCase(Locale.ROOT);
+                if (!visitedSerials.add(serialNumber)) {
+                    return new CertificateValidation(false, "证书链存在循环");
+                }
+
+                CertificateEntity certificateEntity = findCertificateBySerial(
+                        currentCertificate.getSerialNumber());
+                if (certificateEntity == null) {
+                    return new CertificateValidation(false, "签名证书链未在本地CA中登记");
+                }
+                X509Certificate registeredCertificate =
+                        certificateAssembleService.getCertificate(certificateEntity.getCertSerialNumber());
+                if (!MessageDigest.isEqual(
+                        currentCertificate.getEncoded(), registeredCertificate.getEncoded())) {
+                    return new CertificateValidation(false, "签名证书与本地登记证书不一致");
+                }
+                String status = certificateEntity.getStatus();
+                if ("2".equals(status)) {
+                    return new CertificateValidation(false, "证书已过期");
+                }
+                if ("3".equals(status)) {
+                    return new CertificateValidation(false, "证书已冻结");
+                }
+                if ("4".equals(status)) {
+                    return new CertificateValidation(false, "证书已吊销");
+                }
+                if (!"1".equals(status)) {
+                    return new CertificateValidation(false, "证书状态未知");
+                }
+                currentCertificate.checkValidity();
+
+                if ("1".equals(certificateEntity.getIsRoot())) {
+                    if (!currentCertificate.getSubjectX500Principal()
+                            .equals(currentCertificate.getIssuerX500Principal())) {
+                        return new CertificateValidation(false, "根证书不是自签名证书");
+                    }
+                    currentCertificate.verify(currentCertificate.getPublicKey());
+                    return new CertificateValidation(true, "证书链有效,OCSP状态正常");
+                }
+
+                String issuerSerial = certificateEntity.getParentSerial();
+                if (issuerSerial == null || issuerSerial.isBlank()) {
+                    return new CertificateValidation(false, "证书缺少颁发者信息");
+                }
+                CertificateEntity issuerEntity = certificateMapper.selectOne(
+                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CertificateEntity>()
+                                .eq(CertificateEntity::getCertSerialNumber, issuerSerial)
+                );
+                if (issuerEntity == null) {
+                    return new CertificateValidation(false, "未找到证书颁发者");
+                }
+                X509Certificate issuerCertificate =
+                        certificateAssembleService.getCertificate(issuerEntity.getCertSerialNumber());
+                currentCertificate.verify(issuerCertificate.getPublicKey());
+
+                CertificateValidation ocspValidation =
+                        checkOcspStatus(currentCertificate, issuerCertificate);
+                if (!ocspValidation.valid()) {
+                    return ocspValidation;
+                }
+                currentCertificate = issuerCertificate;
+            }
+            return new CertificateValidation(false, "证书链深度超过允许范围");
+        } catch (Exception e) {
+            log.warn("签名证书状态校验失败: {}", e.getMessage());
+            return new CertificateValidation(false, "证书链或OCSP状态校验失败: " + e.getMessage());
+        }
+    }
+
+    private CertificateEntity findCertificateBySerial(BigInteger serialNumber) {
+        String normalizedSerial = serialNumber.toString(16).toUpperCase(Locale.ROOT);
+        String paddedSerial = String.format("%032X", serialNumber);
+        return certificateMapper.selectOne(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CertificateEntity>()
+                        .in(CertificateEntity::getCertSerialNumber, normalizedSerial, paddedSerial)
+                        .last("LIMIT 1")
+        );
+    }
+
+    private CertificateValidation checkOcspStatus(
+            X509Certificate certificate, X509Certificate issuerCertificate) throws Exception {
+        String responderUrl = getOcspResponderUrl(certificate);
+        if (responderUrl == null) {
+            return new CertificateValidation(false, "签名证书未配置OCSP地址");
+        }
+
+        DigestCalculatorProvider digestProvider =
+                new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
+        X509CertificateHolder issuerHolder = new X509CertificateHolder(issuerCertificate.getEncoded());
+        CertificateID certificateId = new JcaCertificateID(
+                digestProvider.get(CertificateID.HASH_SHA1),
+                issuerCertificate,
+                certificate.getSerialNumber()
+        );
+        OCSPReq request = new OCSPReqBuilder().addRequest(certificateId).build();
+
+        URL url = URI.create(responderUrl).toURL();
+        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        connection.setRequestMethod("POST");
+        connection.setConnectTimeout(5000);
+        connection.setReadTimeout(5000);
+        connection.setDoOutput(true);
+        connection.setRequestProperty("Content-Type", "application/ocsp-request");
+        connection.setRequestProperty("Accept", "application/ocsp-response");
+        byte[] requestBytes = request.getEncoded();
+        connection.setFixedLengthStreamingMode(requestBytes.length);
+
+        byte[] responseBytes;
+        try {
+            try (var output = connection.getOutputStream()) {
+                output.write(requestBytes);
+            }
+            int httpStatus = connection.getResponseCode();
+            if (httpStatus != HttpURLConnection.HTTP_OK) {
+                return new CertificateValidation(false, "OCSP服务返回HTTP " + httpStatus);
+            }
+            if (connection.getContentLengthLong() > 1_048_576) {
+                return new CertificateValidation(false, "OCSP响应超过允许大小");
+            }
+            try (InputStream input = connection.getInputStream();
+                 ByteArrayOutputStream responseOutput = new ByteArrayOutputStream()) {
+                byte[] buffer = new byte[4096];
+                int read;
+                while ((read = input.read(buffer)) != -1) {
+                    responseOutput.write(buffer, 0, read);
+                    if (responseOutput.size() > 1_048_576) {
+                        return new CertificateValidation(false, "OCSP响应超过允许大小");
+                    }
+                }
+                responseBytes = responseOutput.toByteArray();
+            }
+        } finally {
+            connection.disconnect();
+        }
+
+        OCSPResp response = new OCSPResp(responseBytes);
+        if (response.getStatus() != OCSPRespBuilder.SUCCESSFUL
+                || !(response.getResponseObject() instanceof BasicOCSPResp basicResponse)) {
+            return new CertificateValidation(false, "OCSP响应状态异常");
+        }
+        if (!basicResponse.isSignatureValid(
+                new JcaContentVerifierProviderBuilder()
+                        .setProvider("BC")
+                        .build(issuerHolder))) {
+            return new CertificateValidation(false, "OCSP响应签名无效");
+        }
+
+        Instant now = Instant.now();
+        for (var singleResponse : basicResponse.getResponses()) {
+            CertificateID responseCertificateId = singleResponse.getCertID();
+            if (!responseCertificateId.getSerialNumber().equals(certificate.getSerialNumber())
+                    || !responseCertificateId.matchesIssuer(issuerHolder, digestProvider)) {
+                continue;
+            }
+            Date thisUpdate = singleResponse.getThisUpdate();
+            Date nextUpdate = singleResponse.getNextUpdate();
+            if (thisUpdate == null || thisUpdate.toInstant().isAfter(now.plusSeconds(300))
+                    || nextUpdate == null || nextUpdate.toInstant().isBefore(now)) {
+                return new CertificateValidation(false, "OCSP响应已过期或时间无效");
+            }
+
+            CertificateStatus status = singleResponse.getCertStatus();
+            if (status == CertificateStatus.GOOD) {
+                return new CertificateValidation(true, "OCSP状态正常");
+            }
+            if (status instanceof RevokedStatus revokedStatus) {
+                return new CertificateValidation(false,
+                        revokedStatus.hasRevocationReason()
+                                ? "OCSP状态为已冻结或已吊销,原因码 " + revokedStatus.getRevocationReason()
+                                : "OCSP状态为已冻结或已吊销");
+            }
+            if (status instanceof UnknownStatus) {
+                return new CertificateValidation(false, "OCSP服务无法识别该证书");
+            }
+            return new CertificateValidation(false, "OCSP返回未知证书状态");
+        }
+        return new CertificateValidation(false, "OCSP响应中没有对应证书状态");
+    }
+
+    private String getOcspResponderUrl(X509Certificate certificate) throws Exception {
+        byte[] aiaExtension = certificate.getExtensionValue(Extension.authorityInfoAccess.getId());
+        if (aiaExtension == null) {
+            return null;
+        }
+        byte[] aiaBytes = ASN1OctetString.getInstance(
+                ASN1Primitive.fromByteArray(aiaExtension)).getOctets();
+        AuthorityInformationAccess aia = AuthorityInformationAccess.getInstance(
+                ASN1Primitive.fromByteArray(aiaBytes));
+        for (AccessDescription description : aia.getAccessDescriptions()) {
+            if (AccessDescription.id_ad_ocsp.equals(description.getAccessMethod())
+                    && description.getAccessLocation().getTagNo() == GeneralName.uniformResourceIdentifier) {
+                String value = description.getAccessLocation().getName().toString();
+                URI uri = URI.create(value);
+                if ("http".equalsIgnoreCase(uri.getScheme())
+                        || "https".equalsIgnoreCase(uri.getScheme())) {
+                    return value;
+                }
+                return null;
+            }
+        }
+        return null;
+    }
+
+    private record CertificateValidation(boolean valid, String message) {
+    }
+
     private Map<String, Object> ofdSignatureResult(Map<String, Object> result, int code, String message, Object data) {
         result.put("code", code);
         result.put("message", message);
@@ -737,4 +1029,4 @@ public class SignatureController {
             return false;
         }
     }
-}
+}

+ 3 - 0
src/main/resources/application.yaml

@@ -12,6 +12,9 @@ spring:
     # 数据库密码
     password: 4RtmJimtscsK7XiN
 
+ocsp:
+  responder-url: http://localhost:8080/api/v1/certificate/ocsp
+
 minio:
   endpoint: http://10.10.10.25:9020
   bucket-name: test