feat:吊销列表、证书管理功能

This commit is contained in:
waner
2026-04-30 16:57:34 +08:00
parent e6fea0bef6
commit 66bf3f34c6
64 changed files with 2918 additions and 140 deletions
@@ -3,6 +3,7 @@ package com.cisd.tms.modules.auth.security;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.util.HttpResponseUtil;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.log.service.OperationAuditService;
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -41,7 +42,7 @@ public class InternalAuthorizationInterceptor implements HandlerInterceptor {
return false;
}
String currentAuthLevel = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL);
if (!requireInternalAuth.authLevel().name().equals(currentAuthLevel)) {
if (!authLevelAllowed(currentAuthLevel, requireInternalAuth.authLevel())) {
writeForbidden(response, "auth level not allowed");
operationAuditService.recordDeniedLog(request, handlerMethod, "auth level not allowed");
return false;
@@ -51,6 +52,18 @@ public class InternalAuthorizationInterceptor implements HandlerInterceptor {
return true;
}
private boolean authLevelAllowed(String currentAuthLevel, AuthLevel requiredAuthLevel) {
if (currentAuthLevel == null) {
return false;
}
try {
AuthLevel current = AuthLevel.valueOf(currentAuthLevel);
return current.ordinal() >= requiredAuthLevel.ordinal();
} catch (IllegalArgumentException ex) {
return false;
}
}
private <T extends java.lang.annotation.Annotation> T findAnnotation(HandlerMethod handlerMethod, Class<T> annotationType) {
T methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getMethod(), annotationType);
if (methodAnnotation != null) {
@@ -2,25 +2,33 @@ package com.cisd.tms.modules.cert.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.security.RequireInternalAuth;
import com.cisd.tms.modules.cert.dto.CertificateDetailResponse;
import com.cisd.tms.modules.cert.dto.CertificateItemResponse;
import com.cisd.tms.modules.cert.dto.CertificateListRequest;
import com.cisd.tms.modules.cert.dto.IdRequest;
import com.cisd.tms.modules.cert.dto.ImportCertificateResponse;
import com.cisd.tms.modules.cert.service.CertificateService;
import com.cisd.tms.modules.log.annotation.AuditedOperation;
import com.cisd.tms.modules.log.enums.ActionType;
import com.cisd.tms.modules.log.enums.ModuleCode;
import com.cisd.tms.security.internal.ReplayProtected;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/v1/certificates")
@Tag(name = "Certificates")
@Tag(name = "证书管理", description = "实体证书和用户证书的导入、查询、详情和删除接口")
public class CertificateController {
private final CertificateService certificateService;
@@ -30,28 +38,38 @@ public class CertificateController {
}
@PostMapping("/list")
@Operation(summary = "查询证书列表", description = "按机构号、算法类型、证书类型和证书主题分页查询实体/用户证书,返回证书有效期、运行状态和指纹信息。")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.LIMITED)
public ApiResponse<Page<CertificateItemResponse>> list(@Valid @RequestBody CertificateListRequest request) {
return ApiResponse.success(certificateService.list(request));
}
@PostMapping("/import")
@Operation(summary = "导入证书", description = "导入 PEM 或 DER 格式的实体/用户证书。系统会校验证书有效期、证书链、KeyUsage、CRL 吊销状态,并根据公钥匹配结果识别实体证书或用户证书。")
// @ReplayProtected
@AuditedOperation(module = ModuleCode.KEY, action = ActionType.IMPORT, summary = "导入证书")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<ImportCertificateResponse> importCertificate(
@RequestPart("file") MultipartFile file,
@RequestParam("certType") String certType,
@RequestParam(value = "entityId", required = false) Long entityId
@Parameter(description = "证书文件,支持 PEM/DER X.509 证书")
@RequestPart("file") MultipartFile file
) {
return ApiResponse.success(certificateService.importCertificate(file, certType, entityId));
return ApiResponse.success(certificateService.importCertificate(file));
}
@PostMapping("/detail")
@Operation(summary = "查询证书详情", description = "根据证书 ID 查询证书详情,包含证书 PEM 内容、DN、序列号、算法、有效期和运行状态。")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.LIMITED)
public ApiResponse<CertificateDetailResponse> detail(@Valid @RequestBody IdRequest request) {
return ApiResponse.success(certificateService.detail(request.getId()));
}
@PostMapping("/delete")
@Operation(summary = "删除证书", description = "根据证书 ID 删除实体/用户证书记录。该操作需要 KEY_ADMIN FULL 会话并启用重放保护和审计。")
// @ReplayProtected
@AuditedOperation(module = ModuleCode.KEY, action = ActionType.DELETE, summary = "删除证书")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<Void> delete(@Valid @RequestBody IdRequest request) {
certificateService.delete(request.getId());
return ApiResponse.success();
}
}
@@ -0,0 +1,67 @@
package com.cisd.tms.modules.cert.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.security.RequireInternalAuth;
import com.cisd.tms.modules.cert.dto.CrlListRequest;
import com.cisd.tms.modules.cert.dto.CrlRevokedItemResponse;
import com.cisd.tms.modules.cert.dto.IdRequest;
import com.cisd.tms.modules.cert.service.CrlService;
import com.cisd.tms.modules.log.annotation.AuditedOperation;
import com.cisd.tms.modules.log.enums.ActionType;
import com.cisd.tms.modules.log.enums.ModuleCode;
import com.cisd.tms.security.internal.ReplayProtected;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/v1/crls")
@Tag(name = "CRL管理", description = "证书吊销列表导入、吊销明细查询和 CRL 删除接口")
public class CrlController {
private final CrlService crlService;
public CrlController(CrlService crlService) {
this.crlService = crlService;
}
@PostMapping("/import")
@Operation(summary = "导入CRL", description = "导入 PEM 或 DER 格式的 X.509 CRL。系统会校验 CRL 时间窗口、重复指纹、签发者可信 CA、KeyUsage.cRLSign 和 CRL 签名。")
// @ReplayProtected
@AuditedOperation(module = ModuleCode.KEY, action = ActionType.IMPORT, summary = "导入证书吊销列表")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<Void> importCrl(
@Parameter(description = "CRL 文件,支持 PEM/DER X.509 CRL")
@RequestPart("file") MultipartFile file
) {
crlService.importCrl(file);
return ApiResponse.success();
}
@PostMapping("/list")
@Operation(summary = "查询CRL吊销明细", description = "按算法类型和证书主题分页查询已导入 CRL 中的吊销证书明细,返回签发者、序列号、吊销原因和吊销时间。")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.LIMITED)
public ApiResponse<Page<CrlRevokedItemResponse>> list(@Valid @RequestBody CrlListRequest request) {
return ApiResponse.success(crlService.list(request));
}
@PostMapping("/delete")
@Operation(summary = "删除CRL", description = "根据 CRL ID 删除 CRL 元数据及其吊销明细。删除后对应吊销记录不再参与证书运行状态判断。")
// @ReplayProtected
@AuditedOperation(module = ModuleCode.KEY, action = ActionType.DELETE, summary = "删除证书吊销列表")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<Void> delete(@Valid @RequestBody IdRequest request) {
crlService.delete(request.getId());
return ApiResponse.success();
}
}
@@ -2,6 +2,9 @@ package com.cisd.tms.modules.cert.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.security.RequireInternalAuth;
import com.cisd.tms.modules.cert.dto.EntityCreateRequest;
import com.cisd.tms.modules.cert.dto.EntityCreateResponse;
import com.cisd.tms.modules.cert.dto.EntityGenP10Request;
@@ -11,6 +14,11 @@ import com.cisd.tms.modules.cert.dto.EntityUpdateRequest;
import com.cisd.tms.modules.cert.dto.IdRequest;
import com.cisd.tms.modules.cert.dto.P10Response;
import com.cisd.tms.modules.cert.service.EntityService;
import com.cisd.tms.modules.log.annotation.AuditedOperation;
import com.cisd.tms.modules.log.enums.ActionType;
import com.cisd.tms.modules.log.enums.ModuleCode;
import com.cisd.tms.security.internal.ReplayProtected;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
@@ -20,7 +28,7 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/entities")
@Tag(name = "Cert Entities")
@Tag(name = "证书实体管理", description = "证书实体密钥生成、机构信息维护、P10 生成和 P10 查询接口")
public class EntityController {
private final EntityService entityService;
@@ -30,35 +38,54 @@ public class EntityController {
}
@PostMapping("/list")
@Operation(summary = "查询证书实体列表", description = "按机构号、算法类型和实体状态分页查询证书实体,返回密钥索引、证书主题、允许访问 IP 和状态信息。")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.LIMITED)
public ApiResponse<Page<EntityItemResponse>> list(@Valid @RequestBody EntityListRequest request) {
return ApiResponse.success(entityService.list(request));
}
@PostMapping("/create")
@Operation(summary = "创建证书实体", description = "为机构创建证书实体并在密码卡中生成签名密钥对。支持 SM2/RSA,当前 P10 生成仅支持 SM2 实体。")
// @ReplayProtected
@AuditedOperation(module = ModuleCode.KEY, action = ActionType.CREATE, summary = "创建证书实体")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<EntityCreateResponse> create(@Valid @RequestBody EntityCreateRequest request) {
return ApiResponse.success(entityService.create(request));
}
@PostMapping("/update")
@Operation(summary = "更新证书实体", description = "更新证书实体的机构号、机构名称和允许访问 IP 列表,不修改算法类型、密钥索引或密钥材料。")
// @ReplayProtected
@AuditedOperation(module = ModuleCode.KEY, action = ActionType.UPDATE, summary = "更新证书实体")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<Void> update(@Valid @RequestBody EntityUpdateRequest request) {
entityService.update(request);
return ApiResponse.success();
}
@PostMapping("/delete")
@Operation(summary = "删除证书实体", description = "删除证书实体并删除密码卡中的对应用户密钥。若实体已绑定证书,需要先删除证书。")
// @ReplayProtected
@AuditedOperation(module = ModuleCode.KEY, action = ActionType.DELETE, summary = "删除证书实体")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<Void> delete(@Valid @RequestBody IdRequest request) {
entityService.delete(request.getId());
return ApiResponse.success();
}
@PostMapping("/gen-p10")
@Operation(summary = "生成P10证书请求", description = "使用实体签名密钥生成 PKCS#10 证书请求,并保存证书主题、P10 内容和证书模式。当前仅支持 SM2。")
// @ReplayProtected
@AuditedOperation(module = ModuleCode.KEY, action = ActionType.EXECUTE, summary = "生成证书请求")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<P10Response> genP10(@Valid @RequestBody EntityGenP10Request request) {
return ApiResponse.success(new P10Response(entityService.generateP10(request)));
}
@PostMapping("/p10")
@Operation(summary = "查询P10证书请求", description = "根据实体 ID 查询已生成的 PKCS#10 PEM 内容。")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.LIMITED)
public ApiResponse<P10Response> p10(@Valid @RequestBody IdRequest request) {
return ApiResponse.success(new P10Response(entityService.getP10(request.getId())));
}
}
@@ -2,6 +2,9 @@ package com.cisd.tms.modules.cert.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.security.RequireInternalAuth;
import com.cisd.tms.modules.cert.dto.DeleteDependencyResponse;
import com.cisd.tms.modules.cert.dto.IdRequest;
import com.cisd.tms.modules.cert.dto.ImportCertificateResponse;
@@ -10,19 +13,24 @@ import com.cisd.tms.modules.cert.dto.TrustedCertDetailResponse;
import com.cisd.tms.modules.cert.dto.TrustedCertItemResponse;
import com.cisd.tms.modules.cert.dto.TrustedCertListRequest;
import com.cisd.tms.modules.cert.service.TrustedCertService;
import com.cisd.tms.modules.log.annotation.AuditedOperation;
import com.cisd.tms.modules.log.enums.ActionType;
import com.cisd.tms.modules.log.enums.ModuleCode;
import com.cisd.tms.security.internal.ReplayProtected;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/v1/trusted-certificates")
@Tag(name = "Trusted Certificates")
@Tag(name = "可信CA证书管理", description = "可信根 CA 和中间 CA 证书的导入、查询、详情和删除接口")
public class TrustedCertController {
private final TrustedCertService trustedCertService;
@@ -32,26 +40,37 @@ public class TrustedCertController {
}
@PostMapping("/list")
@Operation(summary = "查询可信CA证书列表", description = "按算法类型和证书主题分页查询可信 CA 证书,返回 CA 层级、有效期和指纹信息。")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.LIMITED)
public ApiResponse<Page<TrustedCertItemResponse>> list(@Valid @RequestBody TrustedCertListRequest request) {
return ApiResponse.success(trustedCertService.list(request));
}
@PostMapping("/import")
@Operation(summary = "导入可信CA证书", description = "导入 PEM 或 DER 格式的可信 CA 证书。系统会校验证书有效期、CA 属性、KeyUsage.keyCertSign,以及非根 CA 的签发链。")
// @ReplayProtected
@AuditedOperation(module = ModuleCode.KEY, action = ActionType.IMPORT, summary = "导入可信证书")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<ImportCertificateResponse> importTrusted(
@RequestPart("file") MultipartFile file,
@RequestParam("alias") String alias
@Parameter(description = "可信 CA 证书文件,支持 PEM/DER X.509 证书")
@RequestPart("file") MultipartFile file
) {
return ApiResponse.success(trustedCertService.importTrusted(file, alias));
return ApiResponse.success(trustedCertService.importTrusted(file, null));
}
@PostMapping("/detail")
@Operation(summary = "查询可信CA证书详情", description = "根据证书 ID 查询可信 CA 证书详情,包含证书 PEM 内容、DN、序列号、算法、CA 层级和有效期。")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.LIMITED)
public ApiResponse<TrustedCertDetailResponse> detail(@Valid @RequestBody IdRequest request) {
return ApiResponse.success(trustedCertService.detail(request.getId()));
}
@PostMapping("/delete")
@Operation(summary = "删除可信CA证书", description = "删除可信 CA 证书。存在下级 CA 或已签发本地证书时,默认返回依赖数量;传入 force=true 时执行强制删除。")
// @ReplayProtected
@AuditedOperation(module = ModuleCode.KEY, action = ActionType.DELETE, summary = "删除可信证书")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<DeleteDependencyResponse> delete(@Valid @RequestBody TrustedCertDeleteRequest request) {
return ApiResponse.success(trustedCertService.delete(request));
}
}
@@ -1,22 +1,46 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "证书详情响应")
public class CertificateDetailResponse {
@Schema(description = "证书 ID", example = "1")
private Long id;
@Schema(description = "关联证书实体 ID;用户证书为空", example = "10")
private Long entityId;
@Schema(description = "证书类型,ENTITY 表示实体证书,USER 表示用户证书", example = "ENTITY")
private String certType;
@Schema(description = "证书主题 DN", example = "C=CN,O=CISD,CN=entity")
private String subjectDn;
@Schema(description = "签发者 DN", example = "C=CN,O=GMSSL,CN=MiddleCA for Test")
private String issuerDn;
@Schema(description = "证书序列号,十六进制大写", example = "19DDD25B8D0")
private String serialNumber;
@Schema(description = "算法类型", example = "SM2")
private String algoType;
@Schema(description = "证书 SHA-256 指纹,十六进制大写", example = "083C476C26DFD8876E0580601EA66726DF06BB20989DFD862BB34B9DD8B68094")
private String fingerprint;
@Schema(description = "证书 PEM 内容")
private String certData;
@Schema(description = "证书生效时间", example = "2026-04-30T00:00:00")
private LocalDateTime validFrom;
@Schema(description = "证书失效时间", example = "2027-04-30T00:00:00")
private LocalDateTime validTo;
@Schema(description = "运行状态,VALID/NOT_YET_VALID/EXPIRED/REVOKED", example = "VALID")
private String runtimeStatus;
}
@@ -1,21 +1,52 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "证书列表项")
public class CertificateItemResponse {
@Schema(description = "证书 ID", example = "1")
private Long id;
@Schema(description = "关联证书实体 ID;用户证书为空", example = "10")
private Long entityId;
@Schema(description = "关联证书实体机构号;用户证书为空", example = "00000001")
private String orgCode;
@Schema(description = "证书类型,ENTITY 表示实体证书,USER 表示用户证书", example = "ENTITY")
private String certType;
@Schema(description = "证书类型中文名称", example = "实体证书")
private String certTypeName;
@Schema(description = "证书主题 DN", example = "C=CN,O=CISD,CN=entity")
private String subjectDn;
@Schema(description = "签发者 DN", example = "C=CN,O=GMSSL,CN=MiddleCA for Test")
private String issuerDn;
@Schema(description = "证书序列号,十六进制大写", example = "19DDD25B8D0")
private String serialNumber;
@Schema(description = "算法类型", example = "SM2")
private String algoType;
@Schema(description = "证书 SHA-256 指纹,十六进制大写", example = "083C476C26DFD8876E0580601EA66726DF06BB20989DFD862BB34B9DD8B68094")
private String fingerprint;
@Schema(description = "证书生效时间", example = "2026-04-30T00:00:00")
private LocalDateTime validFrom;
@Schema(description = "证书失效时间", example = "2027-04-30T00:00:00")
private LocalDateTime validTo;
@Schema(description = "导入时间", example = "2026-04-30T15:44:45")
private LocalDateTime importTime;
@Schema(description = "运行状态,VALID/NOT_YET_VALID/EXPIRED/REVOKED", example = "VALID")
private String runtimeStatus;
}
@@ -1,12 +1,22 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "证书列表查询请求")
public class CertificateListRequest extends CommonPageRequest {
private String certType;
private String keyword;
}
@Schema(description = "机构号,按证书实体机构号模糊匹配;仅实体证书有关联机构号", example = "00000001")
private String orgCode;
@Schema(description = "算法类型,支持 SM2/RSA", example = "SM2")
private String algoType;
@Schema(description = "证书类型,ENTITY 表示实体证书,USER 表示用户证书", example = "ENTITY")
private String certType;
@Schema(description = "证书主题 DN 模糊查询关键字", example = "CN=C")
private String subject;
}
@@ -1,13 +1,17 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import lombok.Data;
@Data
@Schema(description = "通用分页请求")
public class CommonPageRequest {
@Min(1)
@Schema(description = "页码,从 1 开始", example = "1")
@Min(value = 1, message = "页码必须大于等于1")
private int pageNum = 1;
@Min(1)
@Schema(description = "每页数量", example = "10")
@Min(value = 1, message = "每页数量必须大于等于1")
private int pageSize = 10;
}
@@ -0,0 +1,16 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "CRL吊销明细列表查询请求")
public class CrlListRequest extends CommonPageRequest {
@Schema(description = "算法类型,支持 SM2/RSA", example = "SM2")
private String algoType;
@Schema(description = "被吊销证书主题 DN 模糊查询关键字", example = "CN=C")
private String subject;
}
@@ -0,0 +1,28 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "证书吊销信息")
public class CrlRevocationInfo {
@Schema(description = "CRL 签发者 DN", example = "C=CN,O=GMSSL,CN=MiddleCA for Test")
private String issuerDn;
@Schema(description = "被吊销证书序列号,十六进制大写", example = "19DDD25B8D0")
private String serialNumber;
@Schema(description = "被吊销证书主题 DN", example = "C=CN,O=CISD,CN=entity")
private String subjectDn;
@Schema(description = "CRL 编号扩展值", example = "123")
private String crlNumber;
@Schema(description = "吊销原因", example = "keyCompromise")
private String revocationReason;
@Schema(description = "吊销时间", example = "2026-04-30T15:00:00")
private LocalDateTime revocationTime;
}
@@ -0,0 +1,37 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "CRL吊销明细列表项")
public class CrlRevokedItemResponse {
@Schema(description = "吊销明细 ID", example = "1")
private Long id;
@Schema(description = "被吊销证书主题 DN;本地尚未导入证书时可能为空", example = "C=CN,O=CISD,CN=entity")
private String subjectDn;
@Schema(description = "CRL 签发者 DN", example = "C=CN,O=GMSSL,CN=MiddleCA for Test")
private String issuerDn;
@Schema(description = "被吊销证书序列号,十六进制大写", example = "19DDD25B8D0")
private String serialNumber;
@Schema(description = "算法类型", example = "SM2")
private String algoType;
@Schema(description = "CRL 编号扩展值", example = "123")
private String crlNumber;
@Schema(description = "吊销原因", example = "keyCompromise")
private String revocationReason;
@Schema(description = "吊销时间", example = "2026-04-30T15:00:00")
private LocalDateTime revocationTime;
@Schema(description = "CRL 导入/发布时间", example = "2026-04-30T15:30:00")
private LocalDateTime publishTime;
}
@@ -1,12 +1,16 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@Schema(description = "删除依赖统计响应")
public class DeleteDependencyResponse {
@Schema(description = "依赖该 CA 的下级 CA 数量", example = "0")
private long childCaCount;
@Schema(description = "该 CA 已签发的本地证书数量", example = "1")
private long issuedCertCount;
}
@@ -1,17 +1,26 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "创建证书实体请求")
public class EntityCreateRequest {
@NotBlank
@Schema(description = "机构代码", example = "00000001")
@NotBlank(message = "机构代码不能为空")
private String orgCode;
@NotBlank
@Schema(description = "机构名称", example = "测试机构")
@NotBlank(message = "机构名称不能为空")
private String orgName;
@NotBlank
@Schema(description = "算法类型,支持 SM2/RSA", example = "SM2")
@NotBlank(message = "算法类型不能为空")
private String algoType;
@Schema(description = "允许访问该实体证书能力的来源 IP 列表", example = "[\"127.0.0.1\"]")
private List<String> allowIps;
}
@@ -1,11 +1,13 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@Schema(description = "创建证书实体响应")
public class EntityCreateResponse {
@Schema(description = "创建后的证书实体 ID", example = "1")
private Long entityId;
}
@@ -1,16 +1,23 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "生成 P10 证书请求")
public class EntityGenP10Request {
@NotNull
@Schema(description = "证书实体 ID", example = "1")
@NotNull(message = "实体ID不能为空")
private Long id;
@NotNull
@Schema(description = "证书模式,SINGLE 表示单证,DOUBLE 表示双证", example = "SINGLE")
@NotNull(message = "证书模式不能为空")
private String certMode;
@Schema(description = "证书主题信息")
@Valid
@NotNull
@NotNull(message = "证书主题不能为空")
private Subject subject;
}
@@ -1,21 +1,44 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
@Schema(description = "证书实体列表项")
public class EntityItemResponse {
@Schema(description = "证书实体 ID", example = "1")
private Long id;
@Schema(description = "机构代码", example = "00000001")
private String orgCode;
@Schema(description = "机构名称", example = "测试机构")
private String orgName;
@Schema(description = "算法类型", example = "SM2")
private String algoType;
@Schema(description = "密钥长度,SM2 为 256,RSA 通常为 2048", example = "256")
private Integer keyLen;
@Schema(description = "密码卡用户密钥索引", example = "2")
private Integer keyIdx;
@Schema(description = "已生成 P10 的证书主题 DN", example = "C=CN,O=CISD,CN=entity")
private String subject;
@Schema(description = "允许访问该实体证书能力的来源 IP 列表", example = "[\"127.0.0.1\"]")
private List<String> allowIps;
@Schema(description = "实体状态,CREATED/P10_GENERATED/CERT_IMPORTED", example = "CREATED")
private String status;
@Schema(description = "实体状态中文名称", example = "已创建")
private String statusName;
@Schema(description = "创建时间", example = "2026-04-30T15:30:00")
private LocalDateTime createTime;
}
@@ -1,12 +1,19 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "证书实体列表查询请求")
public class EntityListRequest extends CommonPageRequest {
@Schema(description = "机构代码,模糊匹配", example = "00000001")
private String orgCode;
@Schema(description = "算法类型,支持 SM2/RSA", example = "SM2")
private String algoType;
@Schema(description = "实体状态,CREATED/P10_GENERATED/CERT_IMPORTED", example = "CREATED")
private String status;
}
@@ -1,15 +1,24 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "更新证书实体请求")
public class EntityUpdateRequest {
@NotNull
@Schema(description = "证书实体 ID", example = "1")
@NotNull(message = "实体ID不能为空")
private Long id;
@Schema(description = "机构代码", example = "00000001")
private String orgCode;
@Schema(description = "机构名称", example = "测试机构")
private String orgName;
@Schema(description = "允许访问该实体证书能力的来源 IP 列表", example = "[\"127.0.0.1\"]")
private List<String> allowIps;
}
@@ -1,11 +1,13 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "通用 ID 请求")
public class IdRequest {
@NotNull
@Schema(description = "记录 ID", example = "1")
@NotNull(message = "ID不能为空")
private Long id;
}
@@ -1,5 +1,6 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -7,9 +8,14 @@ import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@Schema(description = "证书导入响应")
public class ImportCertificateResponse {
@Schema(description = "导入后的证书 ID", example = "1")
private Long certId;
@Schema(description = "证书主题 DN", example = "C=CN,O=CISD,CN=entity")
private String subjectDn;
@Schema(description = "证书失效时间", example = "2027-04-30T00:00:00")
private LocalDateTime validTo;
}
@@ -1,11 +1,13 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@Schema(description = "P10 证书请求响应")
public class P10Response {
@Schema(description = "PKCS#10 PEM 内容")
private String p10;
}
@@ -1,5 +1,6 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
@@ -8,13 +9,25 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@Data
@Schema(description = "证书主题信息")
public class Subject {
@Schema(description = "通用名称 CN", example = "entity")
private String commonName;
@Schema(description = "国家代码 C,必须为 2 位", example = "CN")
private String country;
@Schema(description = "省份 ST", example = "浙江省")
private String province;
@Schema(description = "城市 L", example = "杭州市")
private String city;
@Schema(description = "组织 O", example = "CISD")
private String org;
@Schema(description = "组织单元 OU", example = "Cert")
private String orgUnit;
public String getDN() {
@@ -22,7 +35,7 @@ public class Subject {
}
public X500Name toX500Name() {
Assert.hasText(commonName, "commonName must not be blank");
Assert.hasText(commonName, "通用名称不能为空");
X500NameBuilder builder = new X500NameBuilder(BCStyle.INSTANCE);
if (StringUtils.hasText(commonName)) {
builder.addRDN(BCStyle.CN, commonName);
@@ -40,14 +53,14 @@ public class Subject {
builder.addRDN(BCStyle.ST, province);
}
if (StringUtils.hasText(country)) {
Assert.isTrue(country.length() == 2, "country must be a 2-letter country code");
Assert.isTrue(country.length() == 2, "国家代码必须为2位");
builder.addRDN(BCStyle.C, country);
}
return builder.build();
}
public static Subject fromDN(String dn) {
Assert.hasText(dn, "dn must not be blank");
Assert.hasText(dn, "DN不能为空");
Subject subject = new Subject();
String[] splits = dn.trim().split(",");
for (String split : splits) {
@@ -71,7 +84,7 @@ public class Subject {
subject.setCommonName(trim.substring(3));
}
}
Assert.hasText(subject.commonName, "commonName must not be blank");
Assert.hasText(subject.commonName, "通用名称不能为空");
return subject;
}
}
@@ -1,12 +1,16 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "删除可信CA证书请求")
public class TrustedCertDeleteRequest {
@NotNull
@Schema(description = "可信 CA 证书 ID", example = "1")
@NotNull(message = "可信CA证书ID不能为空")
private Long id;
@Schema(description = "是否强制删除;存在下级 CA 或已签发证书时需要显式传 true", example = "false")
private Boolean force = Boolean.FALSE;
}
@@ -1,22 +1,46 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "可信CA证书详情响应")
public class TrustedCertDetailResponse {
@Schema(description = "可信 CA 证书 ID", example = "1")
private Long id;
@Schema(description = "证书别名", example = "middle-ca")
private String alias;
@Schema(description = "证书主题 DN", example = "C=CN,O=GMSSL,CN=MiddleCA for Test")
private String subjectDn;
@Schema(description = "签发者 DN", example = "C=CN,O=GMSSL,CN=RootCA for Test")
private String issuerDn;
@Schema(description = "证书序列号,十六进制大写", example = "1001")
private String serialNumber;
@Schema(description = "算法类型", example = "SM2")
private String algoType;
@Schema(description = "证书 SHA-256 指纹,十六进制大写")
private String fingerprint;
@Schema(description = "证书 PEM 内容")
private String certData;
@Schema(description = "CA 层级,0 表示根 CA,1 表示中间 CA", example = "1")
private Integer caLevel;
@Schema(description = "证书生效时间", example = "2026-04-30T00:00:00")
private LocalDateTime validFrom;
@Schema(description = "证书失效时间", example = "2036-04-30T00:00:00")
private LocalDateTime validTo;
@Schema(description = "导入时间", example = "2026-04-30T15:30:00")
private LocalDateTime importTime;
}
@@ -1,21 +1,43 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "可信CA证书列表项")
public class TrustedCertItemResponse {
@Schema(description = "可信 CA 证书 ID", example = "1")
private Long id;
@Schema(description = "证书别名", example = "middle-ca")
private String alias;
@Schema(description = "证书主题 DN", example = "C=CN,O=GMSSL,CN=MiddleCA for Test")
private String subjectDn;
@Schema(description = "签发者 DN", example = "C=CN,O=GMSSL,CN=RootCA for Test")
private String issuerDn;
@Schema(description = "证书序列号,十六进制大写", example = "1001")
private String serialNumber;
@Schema(description = "算法类型", example = "SM2")
private String algoType;
@Schema(description = "证书 SHA-256 指纹,十六进制大写")
private String fingerprint;
@Schema(description = "CA 层级,0 表示根 CA,1 表示中间 CA", example = "1")
private Integer caLevel;
@Schema(description = "证书生效时间", example = "2026-04-30T00:00:00")
private LocalDateTime validFrom;
@Schema(description = "证书失效时间", example = "2036-04-30T00:00:00")
private LocalDateTime validTo;
@Schema(description = "导入时间", example = "2026-04-30T15:30:00")
private LocalDateTime importTime;
}
@@ -1,11 +1,16 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "可信CA证书列表查询请求")
public class TrustedCertListRequest extends CommonPageRequest {
private String keyword;
}
@Schema(description = "算法类型,支持 SM2/RSA", example = "SM2")
private String algoType;
@Schema(description = "证书主题 DN 模糊查询关键字", example = "MiddleCA")
private String subject;
}
@@ -0,0 +1,27 @@
package com.cisd.tms.modules.cert.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@TableName("t_certificate_crl")
public class CertificateCrlEntity extends BaseEntity {
private String issuerDn;
private String crlNumber;
private String algoType;
private String fingerprint;
private String crlData;
private LocalDateTime thisUpdate;
private LocalDateTime nextUpdate;
private Integer revokedCount;
private LocalDateTime importTime;
private String createBy;
}
@@ -0,0 +1,25 @@
package com.cisd.tms.modules.cert.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@TableName("t_certificate_crl_revoked")
public class CertificateCrlRevokedEntity extends BaseEntity {
private Long crlId;
private String issuerDn;
private String serialNumber;
private String subjectDn;
private String algoType;
private String crlNumber;
private LocalDateTime revocationTime;
private String revocationReason;
}
@@ -29,6 +29,6 @@ public enum EntityCertMode {
return Arrays.stream(values())
.filter(value -> value.code.equalsIgnoreCase(code))
.findFirst()
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "invalid entity cert mode"));
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "实体证书模式无效"));
}
}
@@ -30,6 +30,6 @@ public enum EntityStatus {
return Arrays.stream(values())
.filter(value -> value.code.equalsIgnoreCase(code))
.findFirst()
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "invalid entity status"));
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "实体状态无效"));
}
}
@@ -0,0 +1,9 @@
package com.cisd.tms.modules.cert.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.cert.entity.CertificateCrlEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CertificateCrlEntityMapper extends BaseMapperX<CertificateCrlEntity> {
}
@@ -0,0 +1,9 @@
package com.cisd.tms.modules.cert.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.cert.entity.CertificateCrlRevokedEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CertificateCrlRevokedEntityMapper extends BaseMapperX<CertificateCrlRevokedEntity> {
}
@@ -10,8 +10,8 @@ public interface CertificateRepository {
void deleteById(Long id);
Optional<CertificateEntity> findById(Long id);
Optional<CertificateEntity> findByFingerprint(String fingerprint);
Optional<CertificateEntity> findByIssuerDnAndSerialNumber(String issuerDn, String serialNumber);
boolean existsEntityCertByEntityId(Long entityId);
long countByIssuerDn(String issuerDn);
Page<CertificateEntity> page(CertificateListRequest request);
}
@@ -0,0 +1,21 @@
package com.cisd.tms.modules.cert.repository;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.modules.cert.dto.CrlListRequest;
import com.cisd.tms.modules.cert.entity.CertificateCrlEntity;
import com.cisd.tms.modules.cert.entity.CertificateCrlRevokedEntity;
import java.util.Optional;
public interface CrlRepository {
CertificateCrlEntity saveCrl(CertificateCrlEntity entity);
CertificateCrlRevokedEntity saveRevoked(CertificateCrlRevokedEntity entity);
Optional<CertificateCrlEntity> findCrlById(Long id);
Optional<CertificateCrlEntity> findCrlByFingerprint(String fingerprint);
void deleteCrlById(Long id);
void deleteRevokedByCrlId(Long crlId);
Page<CertificateCrlRevokedEntity> pageRevoked(CrlListRequest request);
boolean existsRevoked(String issuerDn, String serialNumber);
Optional<CertificateCrlRevokedEntity> findRevocation(String issuerDn, String serialNumber);
void updateRevokedSubjectIfMissing(String issuerDn, String serialNumber, String subjectDn, String algoType);
}
@@ -11,6 +11,7 @@ public interface KeyEntityRepository {
void deleteById(Long id);
Optional<KeyEntity> findById(Long id);
Optional<KeyEntity> findByKeyIdx(Integer keyIdx);
List<KeyEntity> findAll();
List<Integer> findUsedKeyIdx();
Page<KeyEntity> page(EntityListRequest request);
void updateStatus(Long id, String status);
@@ -13,6 +13,6 @@ public interface TrustedCertRepository {
Optional<TrustedCertEntity> findByFingerprint(String fingerprint);
List<TrustedCertEntity> findBySubjectDn(String subjectDn);
long countByIssuerDn(String issuerDn);
long countByIssuerDnExcludingId(String issuerDn, Long excludedId);
Page<TrustedCertEntity> page(TrustedCertListRequest request);
}
@@ -49,6 +49,15 @@ public class CertificateRepositoryImpl implements CertificateRepository {
return Optional.ofNullable(mapper.selectOne(wrapper));
}
@Override
public Optional<CertificateEntity> findByIssuerDnAndSerialNumber(String issuerDn, String serialNumber) {
LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<CertificateEntity>()
.eq(CertificateEntity::getIssuerDn, issuerDn)
.eq(CertificateEntity::getSerialNumber, serialNumber)
.last("LIMIT 1");
return Optional.ofNullable(mapper.selectOne(wrapper));
}
@Override
public boolean existsEntityCertByEntityId(Long entityId) {
LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<CertificateEntity>()
@@ -66,12 +75,20 @@ public class CertificateRepositoryImpl implements CertificateRepository {
@Override
public Page<CertificateEntity> page(CertificateListRequest request) {
LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(request.getCertType())) {
wrapper.eq(CertificateEntity::getCertType, request.getCertType());
if (StringUtils.hasText(request.getOrgCode())) {
wrapper.apply(
"entity_id IN (SELECT id FROM t_key_entity WHERE org_code LIKE CONCAT('%', {0}, '%'))",
request.getOrgCode()
);
}
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(CertificateEntity::getSubjectDn, request.getKeyword())
.or().like(CertificateEntity::getIssuerDn, request.getKeyword()));
if (StringUtils.hasText(request.getAlgoType())) {
wrapper.eq(CertificateEntity::getAlgoType, request.getAlgoType().toUpperCase());
}
if (StringUtils.hasText(request.getCertType())) {
wrapper.eq(CertificateEntity::getCertType, request.getCertType().toUpperCase());
}
if (StringUtils.hasText(request.getSubject())) {
wrapper.like(CertificateEntity::getSubjectDn, request.getSubject());
}
wrapper.orderByDesc(CertificateEntity::getImportTime);
long total = mapper.selectCount(wrapper);
@@ -83,4 +100,3 @@ public class CertificateRepositoryImpl implements CertificateRepository {
return page;
}
}
@@ -0,0 +1,117 @@
package com.cisd.tms.modules.cert.repository.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.cisd.tms.modules.cert.dto.CrlListRequest;
import com.cisd.tms.modules.cert.entity.CertificateCrlEntity;
import com.cisd.tms.modules.cert.entity.CertificateCrlRevokedEntity;
import com.cisd.tms.modules.cert.mapper.CertificateCrlEntityMapper;
import com.cisd.tms.modules.cert.mapper.CertificateCrlRevokedEntityMapper;
import com.cisd.tms.modules.cert.repository.CrlRepository;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Optional;
@Repository
public class CrlRepositoryImpl implements CrlRepository {
private final CertificateCrlEntityMapper crlMapper;
private final CertificateCrlRevokedEntityMapper revokedMapper;
public CrlRepositoryImpl(CertificateCrlEntityMapper crlMapper, CertificateCrlRevokedEntityMapper revokedMapper) {
this.crlMapper = crlMapper;
this.revokedMapper = revokedMapper;
}
@Override
public CertificateCrlEntity saveCrl(CertificateCrlEntity entity) {
if (entity.getId() == null) {
crlMapper.insert(entity);
} else {
crlMapper.updateById(entity);
}
return entity;
}
@Override
public CertificateCrlRevokedEntity saveRevoked(CertificateCrlRevokedEntity entity) {
if (entity.getId() == null) {
revokedMapper.insert(entity);
} else {
revokedMapper.updateById(entity);
}
return entity;
}
@Override
public Optional<CertificateCrlEntity> findCrlById(Long id) {
return Optional.ofNullable(crlMapper.selectById(id));
}
@Override
public Optional<CertificateCrlEntity> findCrlByFingerprint(String fingerprint) {
return Optional.ofNullable(crlMapper.selectOne(new LambdaQueryWrapper<CertificateCrlEntity>()
.eq(CertificateCrlEntity::getFingerprint, fingerprint)
.last("LIMIT 1")));
}
@Override
public void deleteCrlById(Long id) {
crlMapper.deleteById(id);
}
@Override
public void deleteRevokedByCrlId(Long crlId) {
revokedMapper.delete(new LambdaQueryWrapper<CertificateCrlRevokedEntity>()
.eq(CertificateCrlRevokedEntity::getCrlId, crlId));
}
@Override
public Page<CertificateCrlRevokedEntity> pageRevoked(CrlListRequest request) {
LambdaQueryWrapper<CertificateCrlRevokedEntity> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(request.getAlgoType())) {
wrapper.eq(CertificateCrlRevokedEntity::getAlgoType, request.getAlgoType().toUpperCase());
}
if (StringUtils.hasText(request.getSubject())) {
wrapper.like(CertificateCrlRevokedEntity::getSubjectDn, request.getSubject());
}
wrapper.orderByDesc(CertificateCrlRevokedEntity::getRevocationTime);
long total = revokedMapper.selectCount(wrapper);
long offset = (long) (request.getPageNum() - 1) * request.getPageSize();
wrapper.last("LIMIT " + offset + "," + request.getPageSize());
List<CertificateCrlRevokedEntity> records = revokedMapper.selectList(wrapper);
Page<CertificateCrlRevokedEntity> page = new Page<>(request.getPageNum(), request.getPageSize(), total);
page.setRecords(records);
return page;
}
@Override
public boolean existsRevoked(String issuerDn, String serialNumber) {
return revokedMapper.selectCount(revocationWrapper(issuerDn, serialNumber)) > 0;
}
@Override
public Optional<CertificateCrlRevokedEntity> findRevocation(String issuerDn, String serialNumber) {
return Optional.ofNullable(revokedMapper.selectOne(revocationWrapper(issuerDn, serialNumber).last("LIMIT 1")));
}
@Override
public void updateRevokedSubjectIfMissing(String issuerDn, String serialNumber, String subjectDn, String algoType) {
revokedMapper.update(null, new LambdaUpdateWrapper<CertificateCrlRevokedEntity>()
.eq(CertificateCrlRevokedEntity::getIssuerDn, issuerDn)
.eq(CertificateCrlRevokedEntity::getSerialNumber, serialNumber)
.isNull(CertificateCrlRevokedEntity::getSubjectDn)
.set(CertificateCrlRevokedEntity::getSubjectDn, subjectDn)
.set(CertificateCrlRevokedEntity::getAlgoType, algoType));
}
private LambdaQueryWrapper<CertificateCrlRevokedEntity> revocationWrapper(String issuerDn, String serialNumber) {
return new LambdaQueryWrapper<CertificateCrlRevokedEntity>()
.eq(CertificateCrlRevokedEntity::getIssuerDn, issuerDn)
.eq(CertificateCrlRevokedEntity::getSerialNumber, serialNumber)
.apply("EXISTS (SELECT 1 FROM t_certificate_crl c WHERE c.id = crl_id AND (c.next_update IS NULL OR c.next_update >= NOW()))");
}
}
@@ -51,6 +51,12 @@ public class KeyEntityRepositoryImpl implements KeyEntityRepository {
return Optional.ofNullable(mapper.selectOne(wrapper));
}
@Override
public List<KeyEntity> findAll() {
return mapper.selectList(new LambdaQueryWrapper<KeyEntity>()
.orderByDesc(KeyEntity::getCreateTime));
}
@Override
public List<Integer> findUsedKeyIdx() {
LambdaQueryWrapper<KeyEntity> wrapper = new LambdaQueryWrapper<KeyEntity>()
@@ -62,13 +62,21 @@ public class TrustedCertRepositoryImpl implements TrustedCertRepository {
.eq(TrustedCertEntity::getIssuerDn, issuerDn));
}
@Override
public long countByIssuerDnExcludingId(String issuerDn, Long excludedId) {
return mapper.selectCount(new LambdaQueryWrapper<TrustedCertEntity>()
.eq(TrustedCertEntity::getIssuerDn, issuerDn)
.ne(excludedId != null, TrustedCertEntity::getId, excludedId));
}
@Override
public Page<TrustedCertEntity> page(TrustedCertListRequest request) {
LambdaQueryWrapper<TrustedCertEntity> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(TrustedCertEntity::getAlias, request.getKeyword())
.or().like(TrustedCertEntity::getSubjectDn, request.getKeyword())
.or().like(TrustedCertEntity::getIssuerDn, request.getKeyword()));
if (StringUtils.hasText(request.getAlgoType())) {
wrapper.eq(TrustedCertEntity::getAlgoType, request.getAlgoType().toUpperCase());
}
if (StringUtils.hasText(request.getSubject())) {
wrapper.like(TrustedCertEntity::getSubjectDn, request.getSubject());
}
wrapper.orderByDesc(TrustedCertEntity::getImportTime);
long total = mapper.selectCount(wrapper);
@@ -80,4 +88,3 @@ public class TrustedCertRepositoryImpl implements TrustedCertRepository {
return page;
}
}
@@ -8,10 +8,10 @@ import com.cisd.tms.modules.cert.dto.CertificateItemResponse;
import com.cisd.tms.modules.cert.dto.CertificateListRequest;
import com.cisd.tms.modules.cert.dto.ImportCertificateResponse;
import com.cisd.tms.modules.cert.entity.CertificateEntity;
import com.cisd.tms.modules.cert.entity.KeyEntity;
import com.cisd.tms.modules.cert.repository.CertificateRepository;
import com.cisd.tms.modules.cert.support.CertPemSupport;
import com.cisd.tms.modules.cert.support.CertRuntimeStatusResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@@ -19,7 +19,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
import java.util.Optional;
@Service
public class CertificateService {
@@ -27,15 +27,27 @@ public class CertificateService {
private final CertificateRepository certificateRepository;
private final EntityService entityService;
private final TrustedCertService trustedCertService;
private final CrlRevocationService crlRevocationService;
public CertificateService(
CertificateRepository certificateRepository,
EntityService entityService,
TrustedCertService trustedCertService
) {
this(certificateRepository, entityService, trustedCertService, null);
}
@Autowired
public CertificateService(
CertificateRepository certificateRepository,
EntityService entityService,
TrustedCertService trustedCertService,
CrlRevocationService crlRevocationService
) {
this.certificateRepository = certificateRepository;
this.entityService = entityService;
this.trustedCertService = trustedCertService;
this.crlRevocationService = crlRevocationService;
}
public Page<CertificateItemResponse> list(CertificateListRequest request) {
@@ -46,43 +58,36 @@ public class CertificateService {
}
@Transactional
public ImportCertificateResponse importCertificate(MultipartFile file, String certType, Long entityId) {
public ImportCertificateResponse importCertificate(MultipartFile file) {
try {
//todo file空值判断
X509Certificate certificate = CertPemSupport.parseCertificate(file.getBytes());
assertValidTime(certificate);
assertEndEntityCertificate(certificate);
String algoType = CertPemSupport.detectAlgoType(certificate);
String fingerprint = CertPemSupport.sha256Fingerprint(certificate);
if (certificateRepository.findByFingerprint(fingerprint).isPresent()) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "certificate already exists");
throw new BizException(ErrorCode.CONFLICT.getCode(), "证书已存在");
}
Long finalEntityId = null;
if ("ENTITY".equalsIgnoreCase(certType)) {
if (entityId == null) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "entityId is required for ENTITY cert");
}
KeyEntity entity = entityService.getById(entityId);
byte[] certKey = certificate.getPublicKey().getEncoded();
byte[] entityKey = Base64.getDecoder().decode(entity.getPublicKey());
byte[] entityKeyX509 = com.cisd.tms.modules.cert.support.Sm2KeySupport.toSm2PublicKey(entityKey).getEncoded();
if (!java.util.Arrays.equals(certKey, entityKeyX509)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "certificate public key does not match entity");
}
finalEntityId = entityId;
Optional<Long> matchedEntityId = entityService.findByCertificatePublicKey(certificate.getPublicKey().getEncoded());
Long finalEntityId = matchedEntityId.orElse(null);
String certType = matchedEntityId.isPresent() ? "ENTITY" : "USER";
var issuer = trustedCertService.resolveIssuer(certificate);
trustedCertService.validateParentChain(issuer);
String issuerDn = certificate.getIssuerX500Principal().getName();
String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
if (isRevoked(issuerDn, serialNumber)) {
fillRevokedSubjectIfMissing(issuerDn, serialNumber, certificate.getSubjectX500Principal().getName(), algoType);
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书已被CRL吊销");
}
trustedCertService.validateParentChain(trustedCertService.resolveIssuer(certificate.getIssuerX500Principal().getName()));
certificate.verify(
CertPemSupport.parseCertificate(
trustedCertService.resolveIssuer(certificate.getIssuerX500Principal().getName())
.getCertData().getBytes()
).getPublicKey(),
"BC"
);
CertificateEntity entity = new CertificateEntity()
.setEntityId(finalEntityId)
.setCertType(certType)
.setSubjectDn(certificate.getSubjectX500Principal().getName())
.setIssuerDn(certificate.getIssuerX500Principal().getName())
.setSerialNumber(certificate.getSerialNumber().toString(16).toUpperCase())
.setAlgoType("SM2")
.setIssuerDn(issuerDn)
.setSerialNumber(serialNumber)
.setAlgoType(algoType)
.setFingerprint(fingerprint)
.setCertData(CertPemSupport.toPem(certificate))
.setValidFrom(LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault()))
@@ -99,7 +104,7 @@ public class CertificateService {
} catch (BizException ex) {
throw ex;
} catch (Exception ex) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "certificate import failed: " + ex.getMessage());
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书导入失败");
}
}
@@ -117,7 +122,7 @@ public class CertificateService {
response.setCertData(entity.getCertData());
response.setValidFrom(entity.getValidFrom());
response.setValidTo(entity.getValidTo());
response.setRuntimeStatus(CertRuntimeStatusResolver.resolve(entity.getValidFrom(), entity.getValidTo()));
response.setRuntimeStatus(resolveRuntimeStatus(entity));
return response;
}
@@ -131,7 +136,11 @@ public class CertificateService {
CertificateItemResponse response = new CertificateItemResponse();
response.setId(entity.getId());
response.setEntityId(entity.getEntityId());
if (entity.getEntityId() != null) {
entityService.findOrgCodeById(entity.getEntityId()).ifPresent(response::setOrgCode);
}
response.setCertType(entity.getCertType());
response.setCertTypeName(certTypeName(entity.getCertType()));
response.setSubjectDn(entity.getSubjectDn());
response.setIssuerDn(entity.getIssuerDn());
response.setSerialNumber(entity.getSerialNumber());
@@ -139,13 +148,41 @@ public class CertificateService {
response.setFingerprint(entity.getFingerprint());
response.setValidFrom(entity.getValidFrom());
response.setValidTo(entity.getValidTo());
response.setRuntimeStatus(CertRuntimeStatusResolver.resolve(entity.getValidFrom(), entity.getValidTo()));
response.setImportTime(entity.getImportTime());
response.setRuntimeStatus(resolveRuntimeStatus(entity));
return response;
}
private String resolveRuntimeStatus(CertificateEntity entity) {
if (isRevoked(entity.getIssuerDn(), entity.getSerialNumber())) {
return "REVOKED";
}
return CertRuntimeStatusResolver.resolve(entity.getValidFrom(), entity.getValidTo());
}
private boolean isRevoked(String issuerDn, String serialNumber) {
return crlRevocationService != null && crlRevocationService.isRevoked(issuerDn, serialNumber);
}
private void fillRevokedSubjectIfMissing(String issuerDn, String serialNumber, String subjectDn, String algoType) {
if (crlRevocationService != null && crlRevocationService.findRevocation(issuerDn, serialNumber).isPresent()) {
crlRevocationService.fillSubjectIfMissing(issuerDn, serialNumber, subjectDn, algoType);
}
}
private String certTypeName(String certType) {
if ("ENTITY".equalsIgnoreCase(certType)) {
return "实体证书";
}
if ("USER".equalsIgnoreCase(certType)) {
return "用户证书";
}
return certType;
}
private CertificateEntity getById(Long id) {
return certificateRepository.findById(id)
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "certificate not found"));
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书不存在"));
}
private void assertValidTime(X509Certificate certificate) {
@@ -153,7 +190,17 @@ public class CertificateService {
LocalDateTime notBefore = LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault());
LocalDateTime notAfter = LocalDateTime.ofInstant(certificate.getNotAfter().toInstant(), ZoneId.systemDefault());
if (now.isBefore(notBefore) || now.isAfter(notAfter)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "certificate is not in valid time window");
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书不在有效期内");
}
}
private void assertEndEntityCertificate(X509Certificate certificate) {
if (certificate.getBasicConstraints() >= 0) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "实体/用户证书不能是CA证书");
}
boolean[] keyUsage = certificate.getKeyUsage();
if (keyUsage != null && (keyUsage.length <= 0 || !keyUsage[0])) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "实体/用户证书必须允许数字签名");
}
}
}
@@ -0,0 +1,41 @@
package com.cisd.tms.modules.cert.service;
import com.cisd.tms.modules.cert.dto.CrlRevocationInfo;
import com.cisd.tms.modules.cert.entity.CertificateCrlRevokedEntity;
import com.cisd.tms.modules.cert.repository.CrlRepository;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class CrlRevocationService {
private final CrlRepository crlRepository;
public CrlRevocationService(CrlRepository crlRepository) {
this.crlRepository = crlRepository;
}
public boolean isRevoked(String issuerDn, String serialNumber) {
return crlRepository.existsRevoked(issuerDn, serialNumber);
}
public Optional<CrlRevocationInfo> findRevocation(String issuerDn, String serialNumber) {
return crlRepository.findRevocation(issuerDn, serialNumber).map(this::toInfo);
}
public void fillSubjectIfMissing(String issuerDn, String serialNumber, String subjectDn, String algoType) {
crlRepository.updateRevokedSubjectIfMissing(issuerDn, serialNumber, subjectDn, algoType);
}
private CrlRevocationInfo toInfo(CertificateCrlRevokedEntity entity) {
CrlRevocationInfo info = new CrlRevocationInfo();
info.setIssuerDn(entity.getIssuerDn());
info.setSerialNumber(entity.getSerialNumber());
info.setSubjectDn(entity.getSubjectDn());
info.setCrlNumber(entity.getCrlNumber());
info.setRevocationReason(entity.getRevocationReason());
info.setRevocationTime(entity.getRevocationTime());
return info;
}
}
@@ -0,0 +1,182 @@
package com.cisd.tms.modules.cert.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.cert.dto.CrlListRequest;
import com.cisd.tms.modules.cert.dto.CrlRevokedItemResponse;
import com.cisd.tms.modules.cert.entity.CertificateCrlEntity;
import com.cisd.tms.modules.cert.entity.CertificateCrlRevokedEntity;
import com.cisd.tms.modules.cert.entity.CertificateEntity;
import com.cisd.tms.modules.cert.entity.TrustedCertEntity;
import com.cisd.tms.modules.cert.repository.CertificateRepository;
import com.cisd.tms.modules.cert.repository.CrlRepository;
import com.cisd.tms.modules.cert.support.CertPemSupport;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.x509.CRLNumber;
import org.bouncycastle.asn1.x509.Extension;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Optional;
@Service
public class CrlService {
private final CrlRepository crlRepository;
private final CertificateRepository certificateRepository;
private final TrustedCertService trustedCertService;
public CrlService(
CrlRepository crlRepository,
CertificateRepository certificateRepository,
TrustedCertService trustedCertService
) {
this.crlRepository = crlRepository;
this.certificateRepository = certificateRepository;
this.trustedCertService = trustedCertService;
}
@Transactional
public void importCrl(MultipartFile file) {
try {
X509CRL crl = CertPemSupport.parseCrl(file.getBytes());
assertEffective(crl);
String fingerprint = CertPemSupport.sha256Fingerprint(crl);
if (crlRepository.findCrlByFingerprint(fingerprint).isPresent()) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "CRL已存在");
}
TrustedCertEntity issuer = trustedCertService.resolveCrlIssuer(crl);
trustedCertService.validateParentChain(issuer);
String issuerDn = crl.getIssuerX500Principal().getName();
String algoType = issuer.getAlgoType();
if (algoType == null) {
algoType = "SM2";
}
String crlNumber = crlNumber(crl);
CertificateCrlEntity entity = new CertificateCrlEntity()
.setIssuerDn(issuerDn)
.setCrlNumber(crlNumber)
.setAlgoType(algoType)
.setFingerprint(fingerprint)
.setCrlData(CertPemSupport.toPem(crl))
.setThisUpdate(toLocalDateTime(crl.getThisUpdate()))
.setNextUpdate(crl.getNextUpdate() == null ? null : toLocalDateTime(crl.getNextUpdate()))
.setRevokedCount(crl.getRevokedCertificates() == null ? 0 : crl.getRevokedCertificates().size())
.setImportTime(LocalDateTime.now())
.setCreateBy("system");
entity.setCreateTime(LocalDateTime.now());
entity.setUpdateTime(LocalDateTime.now());
crlRepository.saveCrl(entity);
if (crl.getRevokedCertificates() != null) {
for (X509CRLEntry entry : crl.getRevokedCertificates()) {
saveRevoked(entity.getId(), issuerDn, crlNumber, algoType, entry);
}
}
} catch (BizException ex) {
throw ex;
} catch (Exception ex) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL导入失败");
}
}
public Page<CrlRevokedItemResponse> list(CrlListRequest request) {
Page<CertificateCrlRevokedEntity> page = crlRepository.pageRevoked(request);
Page<CrlRevokedItemResponse> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
result.setRecords(page.getRecords().stream().map(this::toItem).toList());
return result;
}
@Transactional
public void delete(Long id) {
crlRepository.findCrlById(id)
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL不存在"));
crlRepository.deleteRevokedByCrlId(id);
crlRepository.deleteCrlById(id);
}
private void saveRevoked(Long crlId, String issuerDn, String crlNumber, String crlAlgoType, X509CRLEntry entry) {
String serialNumber = entry.getSerialNumber().toString(16).toUpperCase();
Optional<CertificateEntity> certificate = certificateRepository.findByIssuerDnAndSerialNumber(issuerDn, serialNumber);
CertificateCrlRevokedEntity revoked = new CertificateCrlRevokedEntity()
.setCrlId(crlId)
.setIssuerDn(issuerDn)
.setSerialNumber(serialNumber)
.setSubjectDn(certificate.map(CertificateEntity::getSubjectDn).orElse(null))
.setAlgoType(certificate.map(CertificateEntity::getAlgoType).orElse(crlAlgoType))
.setCrlNumber(crlNumber)
.setRevocationTime(toLocalDateTime(entry.getRevocationDate()))
.setRevocationReason(reasonName(entry));
revoked.setCreateTime(LocalDateTime.now());
revoked.setUpdateTime(LocalDateTime.now());
crlRepository.saveRevoked(revoked);
}
private CrlRevokedItemResponse toItem(CertificateCrlRevokedEntity entity) {
CrlRevokedItemResponse response = new CrlRevokedItemResponse();
response.setId(entity.getId());
response.setSubjectDn(entity.getSubjectDn());
response.setIssuerDn(entity.getIssuerDn());
response.setSerialNumber(entity.getSerialNumber());
response.setAlgoType(entity.getAlgoType());
response.setCrlNumber(entity.getCrlNumber());
response.setRevocationReason(entity.getRevocationReason());
response.setRevocationTime(entity.getRevocationTime());
response.setPublishTime(entity.getCreateTime());
return response;
}
private void assertEffective(X509CRL crl) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime thisUpdate = toLocalDateTime(crl.getThisUpdate());
if (thisUpdate.isAfter(now)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL生效时间晚于当前时间");
}
if (crl.getNextUpdate() != null && toLocalDateTime(crl.getNextUpdate()).isBefore(now)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL已过期");
}
}
private String reasonName(X509CRLEntry entry) {
if (entry.getRevocationReason() == null) {
return null;
}
return switch (entry.getRevocationReason()) {
case KEY_COMPROMISE -> "keyCompromise";
case CA_COMPROMISE -> "cACompromise";
case AFFILIATION_CHANGED -> "affiliationChanged";
case SUPERSEDED -> "superseded";
case CESSATION_OF_OPERATION -> "cessationOfOperation";
case CERTIFICATE_HOLD -> "certificateHold";
case REMOVE_FROM_CRL -> "removeFromCRL";
case PRIVILEGE_WITHDRAWN -> "privilegeWithdrawn";
case AA_COMPROMISE -> "aACompromise";
case UNUSED -> "unused";
default -> entry.getRevocationReason().name();
};
}
private String crlNumber(X509CRL crl) {
try {
byte[] extensionValue = crl.getExtensionValue(Extension.cRLNumber.getId());
if (extensionValue == null) {
return null;
}
ASN1OctetString octets = ASN1OctetString.getInstance(ASN1Primitive.fromByteArray(extensionValue));
CRLNumber number = CRLNumber.getInstance(ASN1Primitive.fromByteArray(octets.getOctets()));
return number.getCRLNumber().toString();
} catch (Exception ex) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL编号扩展无效");
}
}
private LocalDateTime toLocalDateTime(java.util.Date date) {
return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
}
}
@@ -39,9 +39,11 @@ import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@Service
public class EntityService {
@@ -75,7 +77,7 @@ public class EntityService {
}
@Transactional
public EntityCreateResponse create(EntityCreateRequest request) {
public synchronized EntityCreateResponse create(EntityCreateRequest request) {
String algoType = normalizeAlgoType(request.getAlgoType());
int keyIdx = resolveNextKeyIdx();
int keyLen = SM2.equals(algoType) ? 256 : 2048;
@@ -137,7 +139,7 @@ public class EntityService {
public void delete(Long id) {
KeyEntity entity = getById(id);
if (certificateRepository.existsEntityCertByEntityId(id)) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "entity certificate exists, delete certificate first");
throw new BizException(ErrorCode.CONFLICT.getCode(), "实体已绑定证书,请先删除证书");
}
pcieCryptoService.deleteUserKey(entity.getKeyIdx());
keyEntityRepository.deleteById(id);
@@ -147,7 +149,7 @@ public class EntityService {
public String generateP10(EntityGenP10Request request) {
KeyEntity entity = getById(request.getId());
if (!SM2.equalsIgnoreCase(entity.getAlgoType())) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "only SM2 P10 is supported");
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "仅支持SM2生成P10请求");
}
EntityCertMode certMode = EntityCertMode.requireValid(request.getCertMode());
Subject subject = request.getSubject();
@@ -173,7 +175,19 @@ public class EntityService {
public KeyEntity getById(Long id) {
return keyEntityRepository.findById(id)
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "entity not found"));
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "实体不存在"));
}
public Optional<Long> findByCertificatePublicKey(byte[] certificatePublicKey) {
return keyEntityRepository.findAll().stream()
.filter(entity -> certificatePublicKeyMatches(entity, certificatePublicKey))
.map(KeyEntity::getId)
.findFirst();
}
public Optional<String> findOrgCodeById(Long id) {
return keyEntityRepository.findById(id)
.map(KeyEntity::getOrgCode);
}
@Transactional
@@ -201,15 +215,28 @@ public class EntityService {
private String normalizeAlgoType(String algoType) {
if (!StringUtils.hasText(algoType)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "algoType is required");
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "算法类型不能为空");
}
String normalized = algoType.toUpperCase(Locale.ROOT);
if (!SM2.equals(normalized) && !RSA.equals(normalized)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "algoType must be SM2 or RSA");
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "算法类型必须为SM2或RSA");
}
return normalized;
}
private boolean certificatePublicKeyMatches(KeyEntity entity, byte[] certificatePublicKey) {
if (entity == null || !SM2.equalsIgnoreCase(entity.getAlgoType()) || !StringUtils.hasText(entity.getPublicKey())) {
return false;
}
try {
byte[] entityPublicKey = Base64.getDecoder().decode(entity.getPublicKey());
byte[] entityX509Key = Sm2KeySupport.toSm2PublicKey(entityPublicKey).getEncoded();
return Arrays.equals(certificatePublicKey, entityX509Key);
} catch (Exception ex) {
return false;
}
}
private int resolveNextKeyIdx() {
List<Integer> used = keyEntityRepository.findUsedKeyIdx();
for (int idx = 2; idx <= 8; idx++) {
@@ -217,14 +244,14 @@ public class EntityService {
return idx;
}
}
throw new BizException(ErrorCode.CONFLICT.getCode(), "no available key index in [2..8]");
throw new BizException(ErrorCode.CONFLICT.getCode(), "无可用密钥索引[2..8]");
}
private String writeAllowIps(List<String> allowIps) {
try {
return allowIps == null ? "[]" : objectMapper.writeValueAsString(allowIps);
} catch (JsonProcessingException e) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "invalid allowIps");
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "允许访问IP格式无效");
}
}
@@ -245,7 +272,7 @@ public class EntityService {
.encodeToString(csr.getEncoded());
return "-----BEGIN CERTIFICATE REQUEST-----\n" + base64 + "\n-----END CERTIFICATE REQUEST-----";
} catch (Exception e) {
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to encode csr");
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "P10请求编码失败");
}
}
@@ -19,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.security.Security;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.time.ZoneId;
@@ -27,6 +28,9 @@ import java.util.List;
@Service
public class TrustedCertService {
private static final int KEY_USAGE_KEY_CERT_SIGN = 5;
private static final int KEY_USAGE_CRL_SIGN = 6;
static {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
@@ -56,17 +60,16 @@ public class TrustedCertService {
try {
X509Certificate certificate = CertPemSupport.parseCertificate(file.getBytes());
assertValidTime(certificate);
assertTrustedCaCertificate(certificate);
String fingerprint = CertPemSupport.sha256Fingerprint(certificate);
if (trustedCertRepository.findByFingerprint(fingerprint).isPresent()) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "trusted certificate already exists");
throw new BizException(ErrorCode.CONFLICT.getCode(), "可信CA证书已存在");
}
int caLevel = isSelfSigned(certificate) ? 0 : 1;
if (caLevel == 0) {
certificate.verify(certificate.getPublicKey(), "BC");
} else {
TrustedCertEntity issuer = resolveIssuer(certificate.getIssuerX500Principal().getName());
X509Certificate issuerCert = CertPemSupport.parseCertificate(issuer.getCertData().getBytes());
certificate.verify(issuerCert.getPublicKey(), "BC");
TrustedCertEntity issuer = resolveIssuer(certificate);
validateParentChain(issuer);
}
TrustedCertEntity entity = new TrustedCertEntity()
@@ -74,7 +77,7 @@ public class TrustedCertService {
.setSubjectDn(certificate.getSubjectX500Principal().getName())
.setIssuerDn(certificate.getIssuerX500Principal().getName())
.setSerialNumber(certificate.getSerialNumber().toString(16).toUpperCase())
.setAlgoType("SM2")
.setAlgoType(CertPemSupport.detectAlgoType(certificate))
.setFingerprint(fingerprint)
.setCertData(CertPemSupport.toPem(certificate))
.setCaLevel(caLevel)
@@ -89,7 +92,7 @@ public class TrustedCertService {
} catch (BizException ex) {
throw ex;
} catch (Exception ex) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "trusted certificate import failed: " + ex.getMessage());
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "可信CA证书导入失败");
}
}
@@ -114,10 +117,10 @@ public class TrustedCertService {
@Transactional
public DeleteDependencyResponse delete(TrustedCertDeleteRequest request) {
TrustedCertEntity entity = getById(request.getId());
long childCaCount = trustedCertRepository.countByIssuerDn(entity.getSubjectDn());
long childCaCount = trustedCertRepository.countByIssuerDnExcludingId(entity.getSubjectDn(), entity.getId());
long issuedCertCount = certificateRepository.countByIssuerDn(entity.getSubjectDn());
if ((childCaCount > 0 || issuedCertCount > 0) && !Boolean.TRUE.equals(request.getForce())) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "trusted certificate has dependent certificates");
throw new BizException(ErrorCode.CONFLICT.getCode(), "可信CA证书存在下级证书,请先删除依赖证书或强制删除");
}
trustedCertRepository.deleteById(request.getId());
return new DeleteDependencyResponse(childCaCount, issuedCertCount);
@@ -126,25 +129,64 @@ public class TrustedCertService {
public TrustedCertEntity resolveIssuer(String subjectDn) {
List<TrustedCertEntity> matched = trustedCertRepository.findBySubjectDn(subjectDn);
if (matched.isEmpty()) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "issuer certificate not found");
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "签发者证书不存在");
}
return matched.get(0);
}
public void validateParentChain(TrustedCertEntity entity) {
if (entity.getSubjectDn().equals(entity.getIssuerDn())) {
return;
public TrustedCertEntity resolveIssuer(X509Certificate certificate) {
List<TrustedCertEntity> matched = trustedCertRepository.findBySubjectDn(
certificate.getIssuerX500Principal().getName()
);
if (matched.isEmpty()) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "签发者证书不存在");
}
for (TrustedCertEntity candidate : matched) {
try {
X509Certificate issuerCert = CertPemSupport.parseCertificate(candidate.getCertData().getBytes());
assertTrustedCaCertificate(issuerCert);
certificate.verify(issuerCert.getPublicKey(), "BC");
return candidate;
} catch (Exception ignored) {
}
}
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "未找到有效签发者证书或证书签名无效");
}
public TrustedCertEntity resolveCrlIssuer(X509CRL crl) {
List<TrustedCertEntity> matched = trustedCertRepository.findBySubjectDn(
crl.getIssuerX500Principal().getName()
);
if (matched.isEmpty()) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL签发者证书不存在");
}
for (TrustedCertEntity candidate : matched) {
try {
X509Certificate issuerCert = CertPemSupport.parseCertificate(candidate.getCertData().getBytes());
assertCrlIssuerCertificate(issuerCert);
crl.verify(issuerCert.getPublicKey(), "BC");
return candidate;
} catch (Exception ignored) {
}
}
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "未找到有效CRL签发者证书或CRL签名无效");
}
public void validateParentChain(TrustedCertEntity entity) {
try {
X509Certificate certificate = CertPemSupport.parseCertificate(entity.getCertData().getBytes());
TrustedCertEntity issuer = resolveIssuer(entity.getIssuerDn());
X509Certificate issuerCert = CertPemSupport.parseCertificate(issuer.getCertData().getBytes());
certificate.verify(issuerCert.getPublicKey(), "BC");
assertValidTime(certificate);
assertTrustedCaCertificate(certificate);
if (entity.getSubjectDn().equals(entity.getIssuerDn())) {
certificate.verify(certificate.getPublicKey(), "BC");
return;
}
TrustedCertEntity issuer = resolveIssuer(certificate);
validateParentChain(issuer);
} catch (BizException ex) {
throw ex;
} catch (Exception ex) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "certificate chain validation failed");
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书链校验失败");
}
}
@@ -166,7 +208,7 @@ public class TrustedCertService {
private TrustedCertEntity getById(Long id) {
return trustedCertRepository.findById(id)
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "trusted certificate not found"));
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "可信CA证书不存在"));
}
private void assertValidTime(X509Certificate certificate) {
@@ -174,7 +216,27 @@ public class TrustedCertService {
LocalDateTime notBefore = LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault());
LocalDateTime notAfter = LocalDateTime.ofInstant(certificate.getNotAfter().toInstant(), ZoneId.systemDefault());
if (now.isBefore(notBefore) || now.isAfter(notAfter)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "certificate is not in valid time window");
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书不在有效期内");
}
}
private void assertTrustedCaCertificate(X509Certificate certificate) {
if (certificate.getBasicConstraints() < 0) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "可信证书必须是CA证书");
}
boolean[] keyUsage = certificate.getKeyUsage();
if (keyUsage == null || keyUsage.length <= KEY_USAGE_KEY_CERT_SIGN || !keyUsage[KEY_USAGE_KEY_CERT_SIGN]) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "可信CA证书必须允许签发证书");
}
}
private void assertCrlIssuerCertificate(X509Certificate certificate) {
if (certificate.getBasicConstraints() < 0) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL签发者必须是CA证书");
}
boolean[] keyUsage = certificate.getKeyUsage();
if (keyUsage == null || keyUsage.length <= KEY_USAGE_CRL_SIGN || !keyUsage[KEY_USAGE_CRL_SIGN]) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL签发者必须允许签发CRL");
}
}
@@ -5,10 +5,13 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Locale;
public final class CertPemSupport {
@@ -25,11 +28,21 @@ public final class CertPemSupport {
public static X509Certificate parseCertificate(byte[] bytes) {
try {
byte[] normalized = toDer(bytes);
byte[] normalized = toDer(bytes, "CERTIFICATE");
CertificateFactory factory = CertificateFactory.getInstance("X.509", PROVIDER);
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(normalized));
} catch (Exception e) {
throw new IllegalArgumentException("invalid certificate file", e);
throw new IllegalArgumentException("证书文件无效", e);
}
}
public static X509CRL parseCrl(byte[] bytes) {
try {
byte[] normalized = toDer(bytes, "X509 CRL", "CRL");
CertificateFactory factory = CertificateFactory.getInstance("X.509", PROVIDER);
return (X509CRL) factory.generateCRL(new ByteArrayInputStream(normalized));
} catch (Exception e) {
throw new IllegalArgumentException("CRL文件无效", e);
}
}
@@ -39,7 +52,17 @@ public final class CertPemSupport {
.encodeToString(certificate.getEncoded());
return "-----BEGIN CERTIFICATE-----\n" + base64 + "\n-----END CERTIFICATE-----";
} catch (Exception e) {
throw new IllegalArgumentException("failed to encode certificate pem", e);
throw new IllegalArgumentException("证书PEM编码失败", e);
}
}
public static String toPem(X509CRL crl) {
try {
String base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8))
.encodeToString(crl.getEncoded());
return "-----BEGIN X509 CRL-----\n" + base64 + "\n-----END X509 CRL-----";
} catch (Exception e) {
throw new IllegalArgumentException("CRL PEM编码失败", e);
}
}
@@ -48,20 +71,48 @@ public final class CertPemSupport {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return hex(digest.digest(certificate.getEncoded()));
} catch (Exception e) {
throw new IllegalArgumentException("failed to calculate fingerprint", e);
throw new IllegalArgumentException("证书指纹计算失败", e);
}
}
private static byte[] toDer(byte[] bytes) {
String text = new String(bytes, StandardCharsets.UTF_8);
if (!text.contains("BEGIN CERTIFICATE")) {
return bytes;
public static String sha256Fingerprint(X509CRL crl) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return hex(digest.digest(crl.getEncoded()));
} catch (Exception e) {
throw new IllegalArgumentException("CRL指纹计算失败", e);
}
String normalized = text
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replaceAll("\\s+", "");
return Base64.getDecoder().decode(normalized);
}
public static String detectAlgoType(X509Certificate certificate) {
PublicKey publicKey = certificate.getPublicKey();
if ("RSA".equalsIgnoreCase(publicKey.getAlgorithm())) {
return "RSA";
}
String signatureName = certificate.getSigAlgName();
String signatureOid = certificate.getSigAlgOID();
if ((signatureName != null && signatureName.toUpperCase(Locale.ROOT).contains("SM2"))
|| (signatureOid != null && signatureOid.startsWith("1.2.156.10197.1."))) {
return "SM2";
}
if ("EC".equalsIgnoreCase(publicKey.getAlgorithm())) {
return "SM2";
}
throw new IllegalArgumentException("不支持的证书算法:" + publicKey.getAlgorithm());
}
private static byte[] toDer(byte[] bytes, String... labels) {
String text = new String(bytes, StandardCharsets.UTF_8);
for (String label : labels) {
if (text.contains("BEGIN " + label)) {
String normalized = text
.replace("-----BEGIN " + label + "-----", "")
.replace("-----END " + label + "-----", "")
.replaceAll("\\s+", "");
return Base64.getDecoder().decode(normalized);
}
}
return bytes;
}
private static String hex(byte[] bytes) {
@@ -72,4 +123,3 @@ public final class CertPemSupport {
return builder.toString();
}
}
@@ -49,7 +49,7 @@ public final class Sm2KeySupport {
);
return KeyFactory.getInstance("EC", PROVIDER).generatePublic(publicKeySpec);
} catch (Exception e) {
throw new IllegalArgumentException("failed to parse sm2 public key", e);
throw new IllegalArgumentException("SM2公钥解析失败", e);
}
}
@@ -65,7 +65,7 @@ public final class Sm2KeySupport {
try {
return new DERSequence(vector).getEncoded();
} catch (Exception e) {
throw new IllegalArgumentException("failed to encode sm2 signature", e);
throw new IllegalArgumentException("SM2签名编码失败", e);
}
}
@@ -81,4 +81,3 @@ public final class Sm2KeySupport {
return trimmed;
}
}
+1 -1
View File
@@ -223,7 +223,7 @@ tms:
# 是否启用 /api/** 的内部登录鉴权;开发联调时可临时关闭。
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:false}
# 关闭内部鉴权后,注入请求上下文的调试角色。
debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:OPS_ADMIN}
debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:KEY_ADMIN}
# 关闭内部鉴权后,注入请求上下文的调试认证等级。
debug-auth-level: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_AUTH_LEVEL:FULL}
# 关闭内部鉴权后,注入请求上下文的调试 session token。
@@ -607,3 +607,41 @@ CREATE TABLE IF NOT EXISTS t_trusted_cert (
KEY idx_t_trusted_cert_issuer_dn (issuer_dn(512)),
KEY idx_t_trusted_cert_valid_to (valid_to)
);
CREATE TABLE IF NOT EXISTS t_certificate_crl (
id BIGINT PRIMARY KEY,
issuer_dn VARCHAR(1024) NOT NULL,
crl_number VARCHAR(128) NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
fingerprint VARCHAR(128) NOT NULL COMMENT 'SHA-256 fingerprint',
crl_data MEDIUMTEXT NOT NULL COMMENT 'CRL PEM content',
this_update DATETIME(3) NOT NULL,
next_update DATETIME(3) NULL,
revoked_count INT NOT NULL,
import_time DATETIME(3) NOT NULL,
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_t_certificate_crl_fingerprint (fingerprint),
KEY idx_t_certificate_crl_issuer_dn (issuer_dn(512)),
KEY idx_t_certificate_crl_this_update (this_update),
KEY idx_t_certificate_crl_next_update (next_update)
);
CREATE TABLE IF NOT EXISTS t_certificate_crl_revoked (
id BIGINT PRIMARY KEY,
crl_id BIGINT NOT NULL,
issuer_dn VARCHAR(1024) NOT NULL,
serial_number VARCHAR(128) NOT NULL,
subject_dn VARCHAR(1024) NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
crl_number VARCHAR(128) NULL,
revocation_time DATETIME(3) NOT NULL,
revocation_reason VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
KEY idx_t_certificate_crl_revoked_crl_id (crl_id),
KEY idx_t_certificate_crl_revoked_issuer_serial (issuer_dn(512), serial_number),
KEY idx_t_certificate_crl_revoked_subject_dn (subject_dn(512)),
KEY idx_t_certificate_crl_revoked_revocation_time (revocation_time)
);
@@ -74,3 +74,41 @@ CREATE TABLE IF NOT EXISTS t_trusted_cert (
KEY idx_t_trusted_cert_issuer_dn (issuer_dn(512)),
KEY idx_t_trusted_cert_valid_to (valid_to)
);
CREATE TABLE IF NOT EXISTS t_certificate_crl (
id BIGINT PRIMARY KEY,
issuer_dn VARCHAR(1024) NOT NULL,
crl_number VARCHAR(128) NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
fingerprint VARCHAR(128) NOT NULL COMMENT 'SHA-256 fingerprint',
crl_data MEDIUMTEXT NOT NULL COMMENT 'CRL PEM content',
this_update DATETIME(3) NOT NULL,
next_update DATETIME(3) NULL,
revoked_count INT NOT NULL,
import_time DATETIME(3) NOT NULL,
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_t_certificate_crl_fingerprint (fingerprint),
KEY idx_t_certificate_crl_issuer_dn (issuer_dn(512)),
KEY idx_t_certificate_crl_this_update (this_update),
KEY idx_t_certificate_crl_next_update (next_update)
);
CREATE TABLE IF NOT EXISTS t_certificate_crl_revoked (
id BIGINT PRIMARY KEY,
crl_id BIGINT NOT NULL,
issuer_dn VARCHAR(1024) NOT NULL,
serial_number VARCHAR(128) NOT NULL,
subject_dn VARCHAR(1024) NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
crl_number VARCHAR(128) NULL,
revocation_time DATETIME(3) NOT NULL,
revocation_reason VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
KEY idx_t_certificate_crl_revoked_crl_id (crl_id),
KEY idx_t_certificate_crl_revoked_issuer_serial (issuer_dn(512), serial_number),
KEY idx_t_certificate_crl_revoked_subject_dn (subject_dn(512)),
KEY idx_t_certificate_crl_revoked_revocation_time (revocation_time)
);