diff --git a/README.md b/README.md index 572acc4..3ad81ae 100644 --- a/README.md +++ b/README.md @@ -92,9 +92,28 @@ Open: - Upgrade create task API: `POST /api/v1/upgrades` - Upgrade execute API: `POST /api/v1/upgrades/{taskId}/execute` - Upgrade list API: `GET /api/v1/upgrades` +- CRL import API: `POST /api/v1/crls/import` +- CRL revoked list API: `POST /api/v1/crls/list` +- CRL delete API: `POST /api/v1/crls/delete` - PCIe crypto count API: `GET /api/v1/device/crypto/device-count` - PCIe crypto HMAC API: `POST /api/v1/device/crypto/hmac` +## Certificate CRL Management + +最小调用顺序: +- `POST /api/v1/crls/import` +- `POST /api/v1/crls/list` +- `POST /api/v1/crls/delete` + +当前实现边界: +- CRL 文件支持 PEM / DER 格式,导入后保存 CRL metadata 和 CRL 内每条 revoked certificate 明细。 +- CRL issuer 通过可信 CA 的 issuer DN 候选和实际 CRL 签名验证解析。 +- CRL issuer 必须是可信 CA,且 KeyUsage 必须允许 `cRLSign`。 +- 重复 CRL 通过 SHA-256 fingerprint 拒绝。 +- revoked 明细查询按 `algoType` 和证书 `subjectDn` 模糊过滤,返回的是 revoked certificate 明细列表,不是 CRL 文件列表。 +- 证书导入时如果 `issuerDn + serialNumber` 已命中已导入 CRL,会拒绝导入。 +- 证书列表和详情的 `runtimeStatus` 在 CRL 命中时优先返回 `REVOKED`,优先级高于时间有效期状态。 + ## CISD Reset 最小调用顺序: diff --git a/src/main/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptor.java b/src/main/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptor.java index fa37a96..f8b4cad 100644 --- a/src/main/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptor.java +++ b/src/main/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptor.java @@ -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 findAnnotation(HandlerMethod handlerMethod, Class annotationType) { T methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getMethod(), annotationType); if (methodAnnotation != null) { diff --git a/src/main/java/com/cisd/tms/modules/cert/controller/CertificateController.java b/src/main/java/com/cisd/tms/modules/cert/controller/CertificateController.java index 4e2c911..8206574 100644 --- a/src/main/java/com/cisd/tms/modules/cert/controller/CertificateController.java +++ b/src/main/java/com/cisd/tms/modules/cert/controller/CertificateController.java @@ -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> 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 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 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 delete(@Valid @RequestBody IdRequest request) { certificateService.delete(request.getId()); return ApiResponse.success(); } } - diff --git a/src/main/java/com/cisd/tms/modules/cert/controller/CrlController.java b/src/main/java/com/cisd/tms/modules/cert/controller/CrlController.java new file mode 100644 index 0000000..2caf746 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/controller/CrlController.java @@ -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 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> 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 delete(@Valid @RequestBody IdRequest request) { + crlService.delete(request.getId()); + return ApiResponse.success(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/cert/controller/EntityController.java b/src/main/java/com/cisd/tms/modules/cert/controller/EntityController.java index 73dd622..edb1900 100644 --- a/src/main/java/com/cisd/tms/modules/cert/controller/EntityController.java +++ b/src/main/java/com/cisd/tms/modules/cert/controller/EntityController.java @@ -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> 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 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 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 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 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 p10(@Valid @RequestBody IdRequest request) { return ApiResponse.success(new P10Response(entityService.getP10(request.getId()))); } } - diff --git a/src/main/java/com/cisd/tms/modules/cert/controller/TrustedCertController.java b/src/main/java/com/cisd/tms/modules/cert/controller/TrustedCertController.java index 79c39d0..460f433 100644 --- a/src/main/java/com/cisd/tms/modules/cert/controller/TrustedCertController.java +++ b/src/main/java/com/cisd/tms/modules/cert/controller/TrustedCertController.java @@ -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> 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 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 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 delete(@Valid @RequestBody TrustedCertDeleteRequest request) { return ApiResponse.success(trustedCertService.delete(request)); } } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/CertificateDetailResponse.java b/src/main/java/com/cisd/tms/modules/cert/dto/CertificateDetailResponse.java index 2eaeaff..8d3716c 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/CertificateDetailResponse.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/CertificateDetailResponse.java @@ -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; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/CertificateItemResponse.java b/src/main/java/com/cisd/tms/modules/cert/dto/CertificateItemResponse.java index cfd4dad..9f14beb 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/CertificateItemResponse.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/CertificateItemResponse.java @@ -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; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/CertificateListRequest.java b/src/main/java/com/cisd/tms/modules/cert/dto/CertificateListRequest.java index c42ce5a..3fe3cc1 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/CertificateListRequest.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/CertificateListRequest.java @@ -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; +} diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/CommonPageRequest.java b/src/main/java/com/cisd/tms/modules/cert/dto/CommonPageRequest.java index 7dd1d93..d9e304e 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/CommonPageRequest.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/CommonPageRequest.java @@ -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; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/CrlListRequest.java b/src/main/java/com/cisd/tms/modules/cert/dto/CrlListRequest.java new file mode 100644 index 0000000..98d56fd --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/dto/CrlListRequest.java @@ -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; +} diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/CrlRevocationInfo.java b/src/main/java/com/cisd/tms/modules/cert/dto/CrlRevocationInfo.java new file mode 100644 index 0000000..11d1ae4 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/dto/CrlRevocationInfo.java @@ -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; +} diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/CrlRevokedItemResponse.java b/src/main/java/com/cisd/tms/modules/cert/dto/CrlRevokedItemResponse.java new file mode 100644 index 0000000..0e01259 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/dto/CrlRevokedItemResponse.java @@ -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; +} diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/DeleteDependencyResponse.java b/src/main/java/com/cisd/tms/modules/cert/dto/DeleteDependencyResponse.java index 55876aa..17d2148 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/DeleteDependencyResponse.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/DeleteDependencyResponse.java @@ -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; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/EntityCreateRequest.java b/src/main/java/com/cisd/tms/modules/cert/dto/EntityCreateRequest.java index ba5d36b..e68d7f1 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/EntityCreateRequest.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/EntityCreateRequest.java @@ -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 allowIps; } diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/EntityCreateResponse.java b/src/main/java/com/cisd/tms/modules/cert/dto/EntityCreateResponse.java index 09d1f25..3092dd5 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/EntityCreateResponse.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/EntityCreateResponse.java @@ -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; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/EntityGenP10Request.java b/src/main/java/com/cisd/tms/modules/cert/dto/EntityGenP10Request.java index 7dad1d8..026d1da 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/EntityGenP10Request.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/EntityGenP10Request.java @@ -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; } diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/EntityItemResponse.java b/src/main/java/com/cisd/tms/modules/cert/dto/EntityItemResponse.java index cdc6a2a..26b299c 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/EntityItemResponse.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/EntityItemResponse.java @@ -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 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; } diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/EntityListRequest.java b/src/main/java/com/cisd/tms/modules/cert/dto/EntityListRequest.java index 3817891..4edb568 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/EntityListRequest.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/EntityListRequest.java @@ -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; } diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/EntityUpdateRequest.java b/src/main/java/com/cisd/tms/modules/cert/dto/EntityUpdateRequest.java index b7f160b..75139e8 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/EntityUpdateRequest.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/EntityUpdateRequest.java @@ -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 allowIps; } diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/IdRequest.java b/src/main/java/com/cisd/tms/modules/cert/dto/IdRequest.java index 9ab5ffd..c8e7193 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/IdRequest.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/IdRequest.java @@ -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; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/ImportCertificateResponse.java b/src/main/java/com/cisd/tms/modules/cert/dto/ImportCertificateResponse.java index 1bc2407..90a2bd6 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/ImportCertificateResponse.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/ImportCertificateResponse.java @@ -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; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/P10Response.java b/src/main/java/com/cisd/tms/modules/cert/dto/P10Response.java index 8e0de1d..cb45b29 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/P10Response.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/P10Response.java @@ -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; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/Subject.java b/src/main/java/com/cisd/tms/modules/cert/dto/Subject.java index 09788a5..628ae10 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/Subject.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/Subject.java @@ -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; } } diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertDeleteRequest.java b/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertDeleteRequest.java index 09d92b8..be67776 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertDeleteRequest.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertDeleteRequest.java @@ -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; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertDetailResponse.java b/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertDetailResponse.java index 62fbf16..825f31b 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertDetailResponse.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertDetailResponse.java @@ -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; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertItemResponse.java b/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertItemResponse.java index 290de4a..b84274a 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertItemResponse.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertItemResponse.java @@ -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; } - diff --git a/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertListRequest.java b/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertListRequest.java index a6c5c0d..027cfa6 100644 --- a/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertListRequest.java +++ b/src/main/java/com/cisd/tms/modules/cert/dto/TrustedCertListRequest.java @@ -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; +} diff --git a/src/main/java/com/cisd/tms/modules/cert/entity/CertificateCrlEntity.java b/src/main/java/com/cisd/tms/modules/cert/entity/CertificateCrlEntity.java new file mode 100644 index 0000000..0f1e433 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/entity/CertificateCrlEntity.java @@ -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; +} diff --git a/src/main/java/com/cisd/tms/modules/cert/entity/CertificateCrlRevokedEntity.java b/src/main/java/com/cisd/tms/modules/cert/entity/CertificateCrlRevokedEntity.java new file mode 100644 index 0000000..4c7b02b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/entity/CertificateCrlRevokedEntity.java @@ -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; +} diff --git a/src/main/java/com/cisd/tms/modules/cert/enums/EntityCertMode.java b/src/main/java/com/cisd/tms/modules/cert/enums/EntityCertMode.java index e612fe0..3068653 100644 --- a/src/main/java/com/cisd/tms/modules/cert/enums/EntityCertMode.java +++ b/src/main/java/com/cisd/tms/modules/cert/enums/EntityCertMode.java @@ -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(), "实体证书模式无效")); } } diff --git a/src/main/java/com/cisd/tms/modules/cert/enums/EntityStatus.java b/src/main/java/com/cisd/tms/modules/cert/enums/EntityStatus.java index 695c880..e470847 100644 --- a/src/main/java/com/cisd/tms/modules/cert/enums/EntityStatus.java +++ b/src/main/java/com/cisd/tms/modules/cert/enums/EntityStatus.java @@ -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(), "实体状态无效")); } } diff --git a/src/main/java/com/cisd/tms/modules/cert/mapper/CertificateCrlEntityMapper.java b/src/main/java/com/cisd/tms/modules/cert/mapper/CertificateCrlEntityMapper.java new file mode 100644 index 0000000..9766568 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/mapper/CertificateCrlEntityMapper.java @@ -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 { +} diff --git a/src/main/java/com/cisd/tms/modules/cert/mapper/CertificateCrlRevokedEntityMapper.java b/src/main/java/com/cisd/tms/modules/cert/mapper/CertificateCrlRevokedEntityMapper.java new file mode 100644 index 0000000..7cd269b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/mapper/CertificateCrlRevokedEntityMapper.java @@ -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 { +} diff --git a/src/main/java/com/cisd/tms/modules/cert/repository/CertificateRepository.java b/src/main/java/com/cisd/tms/modules/cert/repository/CertificateRepository.java index b529fdd..501a4cb 100644 --- a/src/main/java/com/cisd/tms/modules/cert/repository/CertificateRepository.java +++ b/src/main/java/com/cisd/tms/modules/cert/repository/CertificateRepository.java @@ -10,8 +10,8 @@ public interface CertificateRepository { void deleteById(Long id); Optional findById(Long id); Optional findByFingerprint(String fingerprint); + Optional findByIssuerDnAndSerialNumber(String issuerDn, String serialNumber); boolean existsEntityCertByEntityId(Long entityId); long countByIssuerDn(String issuerDn); Page page(CertificateListRequest request); } - diff --git a/src/main/java/com/cisd/tms/modules/cert/repository/CrlRepository.java b/src/main/java/com/cisd/tms/modules/cert/repository/CrlRepository.java new file mode 100644 index 0000000..02ce78a --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/repository/CrlRepository.java @@ -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 findCrlById(Long id); + Optional findCrlByFingerprint(String fingerprint); + void deleteCrlById(Long id); + void deleteRevokedByCrlId(Long crlId); + Page pageRevoked(CrlListRequest request); + boolean existsRevoked(String issuerDn, String serialNumber); + Optional findRevocation(String issuerDn, String serialNumber); + void updateRevokedSubjectIfMissing(String issuerDn, String serialNumber, String subjectDn, String algoType); +} diff --git a/src/main/java/com/cisd/tms/modules/cert/repository/KeyEntityRepository.java b/src/main/java/com/cisd/tms/modules/cert/repository/KeyEntityRepository.java index cb9ccd2..660b3d1 100644 --- a/src/main/java/com/cisd/tms/modules/cert/repository/KeyEntityRepository.java +++ b/src/main/java/com/cisd/tms/modules/cert/repository/KeyEntityRepository.java @@ -11,6 +11,7 @@ public interface KeyEntityRepository { void deleteById(Long id); Optional findById(Long id); Optional findByKeyIdx(Integer keyIdx); + List findAll(); List findUsedKeyIdx(); Page page(EntityListRequest request); void updateStatus(Long id, String status); diff --git a/src/main/java/com/cisd/tms/modules/cert/repository/TrustedCertRepository.java b/src/main/java/com/cisd/tms/modules/cert/repository/TrustedCertRepository.java index 7ea2ac9..8acf10c 100644 --- a/src/main/java/com/cisd/tms/modules/cert/repository/TrustedCertRepository.java +++ b/src/main/java/com/cisd/tms/modules/cert/repository/TrustedCertRepository.java @@ -13,6 +13,6 @@ public interface TrustedCertRepository { Optional findByFingerprint(String fingerprint); List findBySubjectDn(String subjectDn); long countByIssuerDn(String issuerDn); + long countByIssuerDnExcludingId(String issuerDn, Long excludedId); Page page(TrustedCertListRequest request); } - diff --git a/src/main/java/com/cisd/tms/modules/cert/repository/impl/CertificateRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/cert/repository/impl/CertificateRepositoryImpl.java index d65ca7d..47ad9ae 100644 --- a/src/main/java/com/cisd/tms/modules/cert/repository/impl/CertificateRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/cert/repository/impl/CertificateRepositoryImpl.java @@ -49,6 +49,15 @@ public class CertificateRepositoryImpl implements CertificateRepository { return Optional.ofNullable(mapper.selectOne(wrapper)); } + @Override + public Optional findByIssuerDnAndSerialNumber(String issuerDn, String serialNumber) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(CertificateEntity::getIssuerDn, issuerDn) + .eq(CertificateEntity::getSerialNumber, serialNumber) + .last("LIMIT 1"); + return Optional.ofNullable(mapper.selectOne(wrapper)); + } + @Override public boolean existsEntityCertByEntityId(Long entityId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() @@ -66,12 +75,20 @@ public class CertificateRepositoryImpl implements CertificateRepository { @Override public Page page(CertificateListRequest request) { LambdaQueryWrapper 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; } } - diff --git a/src/main/java/com/cisd/tms/modules/cert/repository/impl/CrlRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/cert/repository/impl/CrlRepositoryImpl.java new file mode 100644 index 0000000..8d6fd8f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/repository/impl/CrlRepositoryImpl.java @@ -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 findCrlById(Long id) { + return Optional.ofNullable(crlMapper.selectById(id)); + } + + @Override + public Optional findCrlByFingerprint(String fingerprint) { + return Optional.ofNullable(crlMapper.selectOne(new LambdaQueryWrapper() + .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() + .eq(CertificateCrlRevokedEntity::getCrlId, crlId)); + } + + @Override + public Page pageRevoked(CrlListRequest request) { + LambdaQueryWrapper 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 records = revokedMapper.selectList(wrapper); + Page 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 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() + .eq(CertificateCrlRevokedEntity::getIssuerDn, issuerDn) + .eq(CertificateCrlRevokedEntity::getSerialNumber, serialNumber) + .isNull(CertificateCrlRevokedEntity::getSubjectDn) + .set(CertificateCrlRevokedEntity::getSubjectDn, subjectDn) + .set(CertificateCrlRevokedEntity::getAlgoType, algoType)); + } + + private LambdaQueryWrapper revocationWrapper(String issuerDn, String serialNumber) { + return new LambdaQueryWrapper() + .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()))"); + } +} diff --git a/src/main/java/com/cisd/tms/modules/cert/repository/impl/KeyEntityRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/cert/repository/impl/KeyEntityRepositoryImpl.java index 0cb87a4..08536f5 100644 --- a/src/main/java/com/cisd/tms/modules/cert/repository/impl/KeyEntityRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/cert/repository/impl/KeyEntityRepositoryImpl.java @@ -51,6 +51,12 @@ public class KeyEntityRepositoryImpl implements KeyEntityRepository { return Optional.ofNullable(mapper.selectOne(wrapper)); } + @Override + public List findAll() { + return mapper.selectList(new LambdaQueryWrapper() + .orderByDesc(KeyEntity::getCreateTime)); + } + @Override public List findUsedKeyIdx() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() diff --git a/src/main/java/com/cisd/tms/modules/cert/repository/impl/TrustedCertRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/cert/repository/impl/TrustedCertRepositoryImpl.java index 666fe33..8ecca74 100644 --- a/src/main/java/com/cisd/tms/modules/cert/repository/impl/TrustedCertRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/cert/repository/impl/TrustedCertRepositoryImpl.java @@ -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() + .eq(TrustedCertEntity::getIssuerDn, issuerDn) + .ne(excludedId != null, TrustedCertEntity::getId, excludedId)); + } + @Override public Page page(TrustedCertListRequest request) { LambdaQueryWrapper 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; } } - diff --git a/src/main/java/com/cisd/tms/modules/cert/service/CertificateService.java b/src/main/java/com/cisd/tms/modules/cert/service/CertificateService.java index 977f747..0c42735 100644 --- a/src/main/java/com/cisd/tms/modules/cert/service/CertificateService.java +++ b/src/main/java/com/cisd/tms/modules/cert/service/CertificateService.java @@ -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 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 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(), "实体/用户证书必须允许数字签名"); } } } diff --git a/src/main/java/com/cisd/tms/modules/cert/service/CrlRevocationService.java b/src/main/java/com/cisd/tms/modules/cert/service/CrlRevocationService.java new file mode 100644 index 0000000..0b15a4a --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/service/CrlRevocationService.java @@ -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 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; + } +} diff --git a/src/main/java/com/cisd/tms/modules/cert/service/CrlService.java b/src/main/java/com/cisd/tms/modules/cert/service/CrlService.java new file mode 100644 index 0000000..d54d972 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/service/CrlService.java @@ -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 list(CrlListRequest request) { + Page page = crlRepository.pageRevoked(request); + Page 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 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()); + } +} diff --git a/src/main/java/com/cisd/tms/modules/cert/service/EntityService.java b/src/main/java/com/cisd/tms/modules/cert/service/EntityService.java index 59ba0d0..c5f12e0 100644 --- a/src/main/java/com/cisd/tms/modules/cert/service/EntityService.java +++ b/src/main/java/com/cisd/tms/modules/cert/service/EntityService.java @@ -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 findByCertificatePublicKey(byte[] certificatePublicKey) { + return keyEntityRepository.findAll().stream() + .filter(entity -> certificatePublicKeyMatches(entity, certificatePublicKey)) + .map(KeyEntity::getId) + .findFirst(); + } + + public Optional 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 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 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请求编码失败"); } } diff --git a/src/main/java/com/cisd/tms/modules/cert/service/TrustedCertService.java b/src/main/java/com/cisd/tms/modules/cert/service/TrustedCertService.java index c0642d2..d208141 100644 --- a/src/main/java/com/cisd/tms/modules/cert/service/TrustedCertService.java +++ b/src/main/java/com/cisd/tms/modules/cert/service/TrustedCertService.java @@ -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 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 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 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"); } } diff --git a/src/main/java/com/cisd/tms/modules/cert/support/CertPemSupport.java b/src/main/java/com/cisd/tms/modules/cert/support/CertPemSupport.java index 903b142..4c56d5d 100644 --- a/src/main/java/com/cisd/tms/modules/cert/support/CertPemSupport.java +++ b/src/main/java/com/cisd/tms/modules/cert/support/CertPemSupport.java @@ -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(); } } - diff --git a/src/main/java/com/cisd/tms/modules/cert/support/Sm2KeySupport.java b/src/main/java/com/cisd/tms/modules/cert/support/Sm2KeySupport.java index 3568bf0..3c83e53 100644 --- a/src/main/java/com/cisd/tms/modules/cert/support/Sm2KeySupport.java +++ b/src/main/java/com/cisd/tms/modules/cert/support/Sm2KeySupport.java @@ -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; } } - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 825c31f..59f1f25 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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。 diff --git a/src/main/resources/db/migration/V1__tms_schema_full.sql b/src/main/resources/db/migration/V1__tms_schema_full.sql index 763e68a..70bdf6c 100644 --- a/src/main/resources/db/migration/V1__tms_schema_full.sql +++ b/src/main/resources/db/migration/V1__tms_schema_full.sql @@ -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) +); diff --git a/src/main/resources/db/migration/V7__add_cert_module_tables.sql b/src/main/resources/db/migration/V7__add_cert_module_tables.sql index 2e3ec06..d062d1e 100644 --- a/src/main/resources/db/migration/V7__add_cert_module_tables.sql +++ b/src/main/resources/db/migration/V7__add_cert_module_tables.sql @@ -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) +); diff --git a/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java b/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java index caa6614..c7a5358 100644 --- a/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java @@ -64,6 +64,21 @@ class InternalAuthorizationInterceptorTest { Mockito.verifyNoInteractions(operationAuditService); } + @Test + void shouldAllowFullAuthLevelToAccessLimitedEndpoint() throws Exception { + OperationAuditService operationAuditService = Mockito.mock(OperationAuditService.class); + InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper(), operationAuditService); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/trusted-certificates/list"); + request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN"); + request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, handler("keyAdminLimitedEndpoint")); + + Assertions.assertTrue(allowed); + Mockito.verifyNoInteractions(operationAuditService); + } + private static HandlerMethod handler(String methodName) throws NoSuchMethodException { DemoController controller = new DemoController(); return new HandlerMethod(controller, DemoController.class.getDeclaredMethod(methodName)); @@ -75,5 +90,9 @@ class InternalAuthorizationInterceptorTest { @RequireInternalAuth(role = com.cisd.tms.modules.auth.enums.RoleCode.KEY_ADMIN, authLevel = com.cisd.tms.modules.auth.enums.AuthLevel.FULL) public void keyAdminFullEndpoint() { } + + @RequireInternalAuth(role = com.cisd.tms.modules.auth.enums.RoleCode.KEY_ADMIN, authLevel = com.cisd.tms.modules.auth.enums.AuthLevel.LIMITED) + public void keyAdminLimitedEndpoint() { + } } } diff --git a/src/test/java/com/cisd/tms/modules/cert/CertChineseErrorMessageTest.java b/src/test/java/com/cisd/tms/modules/cert/CertChineseErrorMessageTest.java new file mode 100644 index 0000000..d8905f9 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/cert/CertChineseErrorMessageTest.java @@ -0,0 +1,54 @@ +package com.cisd.tms.modules.cert; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CertChineseErrorMessageTest { + + private static final Path CERT_MODULE = Path.of("src/main/java/com/cisd/tms/modules/cert"); + private static final Pattern EXCEPTION_MESSAGE = Pattern.compile( + "(?:new BizException\\([^\\n]*?,\\s*|new IllegalArgumentException\\(|Assert\\.(?:hasText|isTrue)\\([^\\n]*?,\\s*)\"([^\"]+)\"" + ); + private static final Pattern CHINESE = Pattern.compile("\\p{IsHan}"); + + @Test + void certModuleErrorsShouldReturnChineseMessages() throws IOException { + List violations = new ArrayList<>(); + try (var files = Files.walk(CERT_MODULE)) { + files.filter(path -> path.toString().endsWith(".java")) + .forEach(path -> collectViolations(path, violations)); + } + + assertTrue(violations.isEmpty(), String.join("\n", violations)); + } + + private static void collectViolations(Path path, List violations) { + try { + List lines = Files.readAllLines(path); + for (int index = 0; index < lines.size(); index++) { + String line = lines.get(index); + Matcher matcher = EXCEPTION_MESSAGE.matcher(line); + while (matcher.find()) { + String message = matcher.group(1); + if (!CHINESE.matcher(message).find()) { + violations.add(path + ":" + (index + 1) + " -> " + message); + } + } + if (line.matches(".*@(NotNull|NotBlank|Min)\\b(?!\\(.*message\\s*=).*")) { + violations.add(path + ":" + (index + 1) + " -> validation annotation missing Chinese message"); + } + } + } catch (IOException ex) { + throw new IllegalStateException("读取证书模块源码失败", ex); + } + } +} diff --git a/src/test/java/com/cisd/tms/modules/cert/controller/CertControllerSecurityAnnotationTest.java b/src/test/java/com/cisd/tms/modules/cert/controller/CertControllerSecurityAnnotationTest.java new file mode 100644 index 0000000..672c656 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/cert/controller/CertControllerSecurityAnnotationTest.java @@ -0,0 +1,69 @@ +package com.cisd.tms.modules.cert.controller; + +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.log.annotation.AuditedOperation; +import com.cisd.tms.security.internal.ReplayProtected; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class CertControllerSecurityAnnotationTest { + + @Test + void shouldProtectCertificateWriteEndpoints() throws Exception { + assertWriteProtected(CertificateController.class.getMethod("importCertificate", org.springframework.web.multipart.MultipartFile.class)); + assertWriteProtected(CertificateController.class.getMethod("delete", com.cisd.tms.modules.cert.dto.IdRequest.class)); + } + + @Test + void shouldProtectTrustedCertificateWriteEndpoints() throws Exception { + assertWriteProtected(TrustedCertController.class.getMethod("importTrusted", org.springframework.web.multipart.MultipartFile.class)); + assertWriteProtected(TrustedCertController.class.getMethod("delete", com.cisd.tms.modules.cert.dto.TrustedCertDeleteRequest.class)); + } + + @Test + void shouldProtectEntityWriteEndpoints() throws Exception { + assertWriteProtected(EntityController.class.getMethod("create", com.cisd.tms.modules.cert.dto.EntityCreateRequest.class)); + assertWriteProtected(EntityController.class.getMethod("update", com.cisd.tms.modules.cert.dto.EntityUpdateRequest.class)); + assertWriteProtected(EntityController.class.getMethod("delete", com.cisd.tms.modules.cert.dto.IdRequest.class)); + assertWriteProtected(EntityController.class.getMethod("genP10", com.cisd.tms.modules.cert.dto.EntityGenP10Request.class)); + } + + @Test + void shouldProtectCrlWriteEndpoints() throws Exception { + assertWriteProtected(CrlController.class.getMethod("importCrl", org.springframework.web.multipart.MultipartFile.class)); + assertWriteProtected(CrlController.class.getMethod("delete", com.cisd.tms.modules.cert.dto.IdRequest.class)); + } + + @Test + void shouldRequireLimitedKeyAdminForReadEndpoints() throws Exception { + assertReadProtected(CertificateController.class.getMethod("list", com.cisd.tms.modules.cert.dto.CertificateListRequest.class)); + assertReadProtected(CertificateController.class.getMethod("detail", com.cisd.tms.modules.cert.dto.IdRequest.class)); + assertReadProtected(TrustedCertController.class.getMethod("list", com.cisd.tms.modules.cert.dto.TrustedCertListRequest.class)); + assertReadProtected(TrustedCertController.class.getMethod("detail", com.cisd.tms.modules.cert.dto.IdRequest.class)); + assertReadProtected(EntityController.class.getMethod("list", com.cisd.tms.modules.cert.dto.EntityListRequest.class)); + assertReadProtected(EntityController.class.getMethod("p10", com.cisd.tms.modules.cert.dto.IdRequest.class)); + assertReadProtected(CrlController.class.getMethod("list", com.cisd.tms.modules.cert.dto.CrlListRequest.class)); + } + + private void assertWriteProtected(Method method) { + assertNotNull(method.getAnnotation(ReplayProtected.class), method.toString()); + assertNotNull(method.getAnnotation(AuditedOperation.class), method.toString()); + RequireInternalAuth auth = method.getAnnotation(RequireInternalAuth.class); + assertNotNull(auth, method.toString()); + assertEquals(RoleCode.KEY_ADMIN, auth.role(), method.toString()); + assertEquals(AuthLevel.FULL, auth.authLevel(), method.toString()); + } + + private void assertReadProtected(Method method) { + RequireInternalAuth auth = method.getAnnotation(RequireInternalAuth.class); + assertNotNull(auth, method.toString()); + assertEquals(RoleCode.KEY_ADMIN, auth.role(), method.toString()); + assertEquals(AuthLevel.LIMITED, auth.authLevel(), method.toString()); + } +} diff --git a/src/test/java/com/cisd/tms/modules/cert/controller/CertSwaggerAnnotationTest.java b/src/test/java/com/cisd/tms/modules/cert/controller/CertSwaggerAnnotationTest.java new file mode 100644 index 0000000..9901c41 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/cert/controller/CertSwaggerAnnotationTest.java @@ -0,0 +1,107 @@ +package com.cisd.tms.modules.cert.controller; + +import com.cisd.tms.common.api.ApiResponse; +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.CommonPageRequest; +import com.cisd.tms.modules.cert.dto.CrlListRequest; +import com.cisd.tms.modules.cert.dto.CrlRevocationInfo; +import com.cisd.tms.modules.cert.dto.CrlRevokedItemResponse; +import com.cisd.tms.modules.cert.dto.DeleteDependencyResponse; +import com.cisd.tms.modules.cert.dto.EntityCreateRequest; +import com.cisd.tms.modules.cert.dto.EntityCreateResponse; +import com.cisd.tms.modules.cert.dto.EntityGenP10Request; +import com.cisd.tms.modules.cert.dto.EntityItemResponse; +import com.cisd.tms.modules.cert.dto.EntityListRequest; +import com.cisd.tms.modules.cert.dto.EntityUpdateRequest; +import com.cisd.tms.modules.cert.dto.IdRequest; +import com.cisd.tms.modules.cert.dto.ImportCertificateResponse; +import com.cisd.tms.modules.cert.dto.P10Response; +import com.cisd.tms.modules.cert.dto.Subject; +import com.cisd.tms.modules.cert.dto.TrustedCertDeleteRequest; +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 io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.web.bind.annotation.PostMapping; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class CertSwaggerAnnotationTest { + + private static final List> CONTROLLERS = List.of( + CertificateController.class, + TrustedCertController.class, + EntityController.class, + CrlController.class + ); + + private static final List> DTOS = List.of( + CertificateDetailResponse.class, + CertificateItemResponse.class, + CertificateListRequest.class, + CommonPageRequest.class, + CrlListRequest.class, + CrlRevocationInfo.class, + CrlRevokedItemResponse.class, + DeleteDependencyResponse.class, + EntityCreateRequest.class, + EntityCreateResponse.class, + EntityGenP10Request.class, + EntityItemResponse.class, + EntityListRequest.class, + EntityUpdateRequest.class, + IdRequest.class, + ImportCertificateResponse.class, + P10Response.class, + Subject.class, + TrustedCertDeleteRequest.class, + TrustedCertDetailResponse.class, + TrustedCertItemResponse.class, + TrustedCertListRequest.class + ); + + @Test + void certControllersShouldHaveSwaggerOperationDescriptions() { + for (Class controller : CONTROLLERS) { + Tag tag = controller.getAnnotation(Tag.class); + assertNotNull(tag, controller.getSimpleName() + " missing @Tag"); + assertFalse(tag.description().isBlank(), controller.getSimpleName() + " missing @Tag description"); + for (Method method : controller.getDeclaredMethods()) { + if (method.isAnnotationPresent(PostMapping.class)) { + Operation operation = method.getAnnotation(Operation.class); + assertNotNull(operation, controller.getSimpleName() + "#" + method.getName() + " missing @Operation"); + assertFalse(operation.summary().isBlank(), controller.getSimpleName() + "#" + method.getName() + " missing summary"); + assertFalse(operation.description().isBlank(), controller.getSimpleName() + "#" + method.getName() + " missing description"); + } + } + } + } + + @Test + void certDtosShouldHaveSwaggerSchemaDescriptions() { + for (Class dto : DTOS) { + Schema schema = dto.getAnnotation(Schema.class); + assertNotNull(schema, dto.getSimpleName() + " missing @Schema"); + assertFalse(schema.description().isBlank(), dto.getSimpleName() + " missing class description"); + for (Field field : dto.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + Schema fieldSchema = field.getAnnotation(Schema.class); + assertNotNull(fieldSchema, dto.getSimpleName() + "." + field.getName() + " missing @Schema"); + assertFalse(fieldSchema.description().isBlank(), dto.getSimpleName() + "." + field.getName() + " missing description"); + } + } + } +} diff --git a/src/test/java/com/cisd/tms/modules/cert/repository/impl/CertificateRepositoryImplTest.java b/src/test/java/com/cisd/tms/modules/cert/repository/impl/CertificateRepositoryImplTest.java new file mode 100644 index 0000000..7b30bf0 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/cert/repository/impl/CertificateRepositoryImplTest.java @@ -0,0 +1,47 @@ +package com.cisd.tms.modules.cert.repository.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.cisd.tms.modules.cert.dto.CertificateListRequest; +import com.cisd.tms.modules.cert.entity.CertificateEntity; +import com.cisd.tms.modules.cert.mapper.CertificateEntityMapper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.apache.ibatis.session.Configuration; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CertificateRepositoryImplTest { + + @Test + void shouldFilterCertificateListByUiCriteria() { + TableInfoHelper.initTableInfo( + new MapperBuilderAssistant(new Configuration(), ""), + CertificateEntity.class + ); + CertificateEntityMapper mapper = Mockito.mock(CertificateEntityMapper.class); + CertificateRepositoryImpl repository = new CertificateRepositoryImpl(mapper); + Mockito.when(mapper.selectCount(Mockito.any())).thenReturn(0L); + Mockito.when(mapper.selectList(Mockito.any())).thenReturn(List.of()); + CertificateListRequest request = new CertificateListRequest(); + request.setOrgCode("00000001"); + request.setAlgoType("SM2"); + request.setCertType("ENTITY"); + request.setSubject("CN=entity"); + + repository.page(request); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); + Mockito.verify(mapper).selectCount(captor.capture()); + String sqlSegment = captor.getValue().getSqlSegment(); + assertTrue(sqlSegment.contains("t_key_entity"), sqlSegment); + assertTrue(sqlSegment.contains("org_code"), sqlSegment); + assertTrue(sqlSegment.contains("algo"), sqlSegment); + assertTrue(sqlSegment.contains("cert"), sqlSegment); + assertTrue(sqlSegment.contains("subject"), sqlSegment); + } +} diff --git a/src/test/java/com/cisd/tms/modules/cert/repository/impl/CrlRepositoryImplTest.java b/src/test/java/com/cisd/tms/modules/cert/repository/impl/CrlRepositoryImplTest.java new file mode 100644 index 0000000..60537a3 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/cert/repository/impl/CrlRepositoryImplTest.java @@ -0,0 +1,77 @@ +package com.cisd.tms.modules.cert.repository.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.cisd.tms.modules.cert.dto.CrlListRequest; +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.apache.ibatis.builder.MapperBuilderAssistant; +import org.apache.ibatis.session.Configuration; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CrlRepositoryImplTest { + + @Test + void shouldFilterRevokedListByAlgoTypeAndSubject() { + TableInfoHelper.initTableInfo( + new MapperBuilderAssistant(new Configuration(), ""), + CertificateCrlRevokedEntity.class + ); + CertificateCrlRevokedEntityMapper revokedMapper = Mockito.mock(CertificateCrlRevokedEntityMapper.class); + CrlRepository repository = new CrlRepositoryImpl(Mockito.mock(CertificateCrlEntityMapper.class), revokedMapper); + Mockito.when(revokedMapper.selectCount(Mockito.any())).thenReturn(0L); + Mockito.when(revokedMapper.selectList(Mockito.any())).thenReturn(List.of()); + CrlListRequest request = new CrlListRequest(); + request.setAlgoType("sm2"); + request.setSubject("CN=entity"); + + repository.pageRevoked(request); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); + Mockito.verify(revokedMapper).selectCount(captor.capture()); + String sqlSegment = captor.getValue().getSqlSegment(); + assertTrue(sqlSegment.contains("algo"), sqlSegment); + assertTrue(sqlSegment.contains("subject"), sqlSegment); + } + + @Test + void shouldOnlyTreatRevocationAsEffectiveWhenCrlIsNotExpired() { + TableInfoHelper.initTableInfo( + new MapperBuilderAssistant(new Configuration(), ""), + CertificateCrlRevokedEntity.class + ); + CertificateCrlRevokedEntityMapper revokedMapper = Mockito.mock(CertificateCrlRevokedEntityMapper.class); + CrlRepository repository = new CrlRepositoryImpl(Mockito.mock(CertificateCrlEntityMapper.class), revokedMapper); + Mockito.when(revokedMapper.selectCount(Mockito.any())).thenReturn(0L); + + repository.existsRevoked("CN=Root", "A"); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); + Mockito.verify(revokedMapper).selectCount(captor.capture()); + String sqlSegment = captor.getValue().getSqlSegment(); + assertTrue(sqlSegment.contains("t_certificate_crl"), sqlSegment); + assertTrue(sqlSegment.contains("next_update"), sqlSegment); + } + + @Test + void shouldUpdateRevokedSubjectByIssuerAndSerial() { + TableInfoHelper.initTableInfo( + new MapperBuilderAssistant(new Configuration(), ""), + CertificateCrlRevokedEntity.class + ); + CertificateCrlRevokedEntityMapper revokedMapper = Mockito.mock(CertificateCrlRevokedEntityMapper.class); + CrlRepository repository = new CrlRepositoryImpl(Mockito.mock(CertificateCrlEntityMapper.class), revokedMapper); + + repository.updateRevokedSubjectIfMissing("CN=Root", "A", "CN=User", "SM2"); + + Mockito.verify(revokedMapper).update(Mockito.any(), Mockito.any()); + } +} diff --git a/src/test/java/com/cisd/tms/modules/cert/repository/impl/TrustedCertRepositoryImplTest.java b/src/test/java/com/cisd/tms/modules/cert/repository/impl/TrustedCertRepositoryImplTest.java new file mode 100644 index 0000000..8d0f9d3 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/cert/repository/impl/TrustedCertRepositoryImplTest.java @@ -0,0 +1,42 @@ +package com.cisd.tms.modules.cert.repository.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.cisd.tms.modules.cert.dto.TrustedCertListRequest; +import com.cisd.tms.modules.cert.entity.TrustedCertEntity; +import com.cisd.tms.modules.cert.mapper.TrustedCertEntityMapper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.apache.ibatis.session.Configuration; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TrustedCertRepositoryImplTest { + + @Test + void shouldFilterTrustedCertificateListByUiCriteria() { + TableInfoHelper.initTableInfo( + new MapperBuilderAssistant(new Configuration(), ""), + TrustedCertEntity.class + ); + TrustedCertEntityMapper mapper = Mockito.mock(TrustedCertEntityMapper.class); + TrustedCertRepositoryImpl repository = new TrustedCertRepositoryImpl(mapper); + Mockito.when(mapper.selectCount(Mockito.any())).thenReturn(0L); + Mockito.when(mapper.selectList(Mockito.any())).thenReturn(List.of()); + TrustedCertListRequest request = new TrustedCertListRequest(); + request.setAlgoType("SM2"); + request.setSubject("CN=Root"); + + repository.page(request); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); + Mockito.verify(mapper).selectCount(captor.capture()); + String sqlSegment = captor.getValue().getSqlSegment(); + assertTrue(sqlSegment.contains("algo"), sqlSegment); + assertTrue(sqlSegment.contains("subject"), sqlSegment); + } +} diff --git a/src/test/java/com/cisd/tms/modules/cert/service/CertificateServiceTest.java b/src/test/java/com/cisd/tms/modules/cert/service/CertificateServiceTest.java new file mode 100644 index 0000000..753a023 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/cert/service/CertificateServiceTest.java @@ -0,0 +1,425 @@ +package com.cisd.tms.modules.cert.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.cisd.tms.modules.cert.dto.CertificateItemResponse; +import com.cisd.tms.modules.cert.dto.CertificateListRequest; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.cert.dto.CrlRevocationInfo; +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.support.CertPemSupport; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.mock.web.MockMultipartFile; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.time.LocalDateTime; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; + +class CertificateServiceTest { + + static { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + @Test + void shouldListCertificateWithUiFields() { + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + EntityService entityService = Mockito.mock(EntityService.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CertificateService service = new CertificateService(certificateRepository, entityService, trustedCertService); + LocalDateTime validFrom = LocalDateTime.of(2026, 1, 27, 0, 0); + LocalDateTime validTo = LocalDateTime.of(2029, 1, 27, 0, 0); + LocalDateTime importTime = LocalDateTime.of(2026, 1, 27, 11, 0); + CertificateEntity certificate = new CertificateEntity() + .setEntityId(10L) + .setCertType("ENTITY") + .setSubjectDn("CN=entity,O=CISD,C=CN") + .setIssuerDn("CN=Root CA,O=CISD,C=CN") + .setSerialNumber("1234567890") + .setAlgoType("SM2") + .setFingerprint("ABCDEF") + .setValidFrom(validFrom) + .setValidTo(validTo) + .setImportTime(importTime); + certificate.setId(20L); + Page page = new Page<>(1, 10, 1); + page.setRecords(List.of(certificate)); + CertificateListRequest request = new CertificateListRequest(); + request.setOrgCode("00000001"); + request.setAlgoType("SM2"); + request.setCertType("ENTITY"); + request.setSubject("entity"); + Mockito.when(certificateRepository.page(request)).thenReturn(page); + Mockito.when(entityService.findOrgCodeById(10L)).thenReturn(Optional.of("00000001")); + + Page result = service.list(request); + + assertEquals(1, result.getRecords().size()); + CertificateItemResponse item = result.getRecords().get(0); + assertEquals("00000001", item.getOrgCode()); + assertEquals("ENTITY", item.getCertType()); + assertEquals("实体证书", item.getCertTypeName()); + assertEquals("SM2", item.getAlgoType()); + assertEquals("CN=entity,O=CISD,C=CN", item.getSubjectDn()); + assertEquals("CN=Root CA,O=CISD,C=CN", item.getIssuerDn()); + assertEquals("1234567890", item.getSerialNumber()); + assertEquals(validFrom, item.getValidFrom()); + assertEquals(validTo, item.getValidTo()); + assertEquals(importTime, item.getImportTime()); + } + + @Test + void shouldImportEntityCertificateWhenPublicKeyMatchesEntity() throws Exception { + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + EntityService entityService = Mockito.mock(EntityService.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CertificateService service = new CertificateService(certificateRepository, entityService, trustedCertService); + KeyPair caKeyPair = sm2KeyPair(); + KeyPair entityKeyPair = sm2KeyPair(); + X509Certificate caCert = certificate(caKeyPair, caKeyPair, "CN=Root CA", "CN=Root CA", true); + X509Certificate entityCert = certificate(entityKeyPair, caKeyPair, "CN=Entity", "CN=Root CA", false); + TrustedCertEntity issuer = trustedIssuer(caCert); + Mockito.when(certificateRepository.findByFingerprint(any())).thenReturn(Optional.empty()); + Mockito.when(certificateRepository.save(any())).thenAnswer(invocation -> { + CertificateEntity saved = invocation.getArgument(0); + saved.setId(200L); + return saved; + }); + Mockito.when(trustedCertService.resolveIssuer(entityCert)) + .thenReturn(issuer); + Mockito.when(entityService.findByCertificatePublicKey(entityCert.getPublicKey().getEncoded())) + .thenReturn(Optional.of(10L)); + + service.importCertificate(multipart(entityCert)); + + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(CertificateEntity.class); + Mockito.verify(certificateRepository).save(entityCaptor.capture()); + assertEquals("ENTITY", entityCaptor.getValue().getCertType()); + assertEquals(10L, entityCaptor.getValue().getEntityId()); + Mockito.verify(entityService).markCertificateImported(10L); + } + + @Test + void shouldImportUserCertificateWhenPublicKeyDoesNotMatchEntity() throws Exception { + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + EntityService entityService = Mockito.mock(EntityService.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CertificateService service = new CertificateService(certificateRepository, entityService, trustedCertService); + KeyPair caKeyPair = sm2KeyPair(); + KeyPair userKeyPair = sm2KeyPair(); + X509Certificate caCert = certificate(caKeyPair, caKeyPair, "CN=Root CA", "CN=Root CA", true); + X509Certificate userCert = certificate(userKeyPair, caKeyPair, "CN=User", "CN=Root CA", false); + TrustedCertEntity issuer = trustedIssuer(caCert); + Mockito.when(certificateRepository.findByFingerprint(any())).thenReturn(Optional.empty()); + Mockito.when(certificateRepository.save(any())).thenAnswer(invocation -> { + CertificateEntity saved = invocation.getArgument(0); + saved.setId(201L); + return saved; + }); + Mockito.when(trustedCertService.resolveIssuer(userCert)) + .thenReturn(issuer); + Mockito.when(entityService.findByCertificatePublicKey(userCert.getPublicKey().getEncoded())) + .thenReturn(Optional.empty()); + + service.importCertificate(multipart(userCert)); + + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(CertificateEntity.class); + Mockito.verify(certificateRepository).save(entityCaptor.capture()); + assertEquals("USER", entityCaptor.getValue().getCertType()); + assertNull(entityCaptor.getValue().getEntityId()); + Mockito.verify(entityService, Mockito.never()).markCertificateImported(any()); + } + + @Test + void shouldRejectCertificateImportWhenListedInEffectiveCrl() throws Exception { + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + EntityService entityService = Mockito.mock(EntityService.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CrlRevocationService crlRevocationService = Mockito.mock(CrlRevocationService.class); + CertificateService service = new CertificateService( + certificateRepository, + entityService, + trustedCertService, + crlRevocationService + ); + KeyPair caKeyPair = sm2KeyPair(); + KeyPair userKeyPair = sm2KeyPair(); + X509Certificate caCert = certificate(caKeyPair, caKeyPair, "CN=Root CA", "CN=Root CA", true); + X509Certificate userCert = certificate(userKeyPair, caKeyPair, "CN=User", "CN=Root CA", false); + Mockito.when(certificateRepository.findByFingerprint(any())).thenReturn(Optional.empty()); + Mockito.when(trustedCertService.resolveIssuer(userCert)).thenReturn(trustedIssuer(caCert)); + Mockito.when(crlRevocationService.isRevoked( + userCert.getIssuerX500Principal().getName(), + userCert.getSerialNumber().toString(16).toUpperCase() + )).thenReturn(true); + + assertThrows(BizException.class, () -> service.importCertificate(multipart(userCert))); + + Mockito.verify(certificateRepository, Mockito.never()).save(any()); + } + + @Test + void shouldBackfillRevokedSubjectBeforeRejectingCertificateImport() throws Exception { + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + EntityService entityService = Mockito.mock(EntityService.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CrlRevocationService crlRevocationService = Mockito.mock(CrlRevocationService.class); + CertificateService service = new CertificateService(certificateRepository, entityService, trustedCertService, crlRevocationService); + KeyPair caKeyPair = sm2KeyPair(); + KeyPair userKeyPair = sm2KeyPair(); + X509Certificate caCert = certificate(caKeyPair, caKeyPair, "CN=Root CA", "CN=Root CA", true); + X509Certificate userCert = certificate(userKeyPair, caKeyPair, "CN=User", "CN=Root CA", false); + String issuerDn = userCert.getIssuerX500Principal().getName(); + String serialNumber = userCert.getSerialNumber().toString(16).toUpperCase(); + Mockito.when(certificateRepository.findByFingerprint(any())).thenReturn(Optional.empty()); + Mockito.when(trustedCertService.resolveIssuer(userCert)).thenReturn(trustedIssuer(caCert)); + Mockito.when(crlRevocationService.isRevoked(issuerDn, serialNumber)).thenReturn(true); + Mockito.when(crlRevocationService.findRevocation(issuerDn, serialNumber)).thenReturn(Optional.of(new CrlRevocationInfo())); + + assertThrows(BizException.class, () -> service.importCertificate(multipart(userCert))); + + Mockito.verify(crlRevocationService).fillSubjectIfMissing(issuerDn, serialNumber, userCert.getSubjectX500Principal().getName(), "SM2"); + } + + @Test + void shouldRejectImportedCaCertificateAsEndEntityCertificate() throws Exception { + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + EntityService entityService = Mockito.mock(EntityService.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CertificateService service = new CertificateService(certificateRepository, entityService, trustedCertService); + KeyPair caKeyPair = sm2KeyPair(); + KeyPair intermediateKeyPair = sm2KeyPair(); + X509Certificate root = certificate(caKeyPair, caKeyPair, "CN=Root CA", "CN=Root CA", true); + X509Certificate intermediate = certificate(intermediateKeyPair, caKeyPair, "CN=Intermediate CA", "CN=Root CA", true); + Mockito.when(certificateRepository.findByFingerprint(any())).thenReturn(Optional.empty()); + Mockito.when(trustedCertService.resolveIssuer(intermediate)).thenReturn(trustedIssuer(root)); + + assertThrows(BizException.class, () -> service.importCertificate(multipart(intermediate))); + + Mockito.verify(certificateRepository, Mockito.never()).save(any()); + } + + @Test + void shouldRejectEndEntityCertificateWithoutDigitalSignatureUsage() throws Exception { + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + EntityService entityService = Mockito.mock(EntityService.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CertificateService service = new CertificateService(certificateRepository, entityService, trustedCertService); + KeyPair caKeyPair = sm2KeyPair(); + KeyPair userKeyPair = sm2KeyPair(); + X509Certificate caCert = certificate(caKeyPair, caKeyPair, "CN=Root CA", "CN=Root CA", true); + X509Certificate userCert = certificate(userKeyPair, caKeyPair, "CN=User", "CN=Root CA", false, KeyUsage.keyEncipherment); + Mockito.when(certificateRepository.findByFingerprint(any())).thenReturn(Optional.empty()); + Mockito.when(trustedCertService.resolveIssuer(userCert)).thenReturn(trustedIssuer(caCert)); + + assertThrows(BizException.class, () -> service.importCertificate(multipart(userCert))); + + Mockito.verify(certificateRepository, Mockito.never()).save(any()); + } + + @Test + void shouldPersistRsaAlgorithmWhenImportingRsaCertificate() throws Exception { + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + EntityService entityService = Mockito.mock(EntityService.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CertificateService service = new CertificateService(certificateRepository, entityService, trustedCertService); + KeyPair caKeyPair = rsaKeyPair(); + KeyPair userKeyPair = rsaKeyPair(); + X509Certificate caCert = rsaCertificate(caKeyPair, caKeyPair, "CN=RSA Root", "CN=RSA Root", true, KeyUsage.keyCertSign | KeyUsage.cRLSign); + X509Certificate userCert = rsaCertificate(userKeyPair, caKeyPair, "CN=RSA User", "CN=RSA Root", false, KeyUsage.digitalSignature); + Mockito.when(certificateRepository.findByFingerprint(any())).thenReturn(Optional.empty()); + Mockito.when(certificateRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + Mockito.when(trustedCertService.resolveIssuer(userCert)).thenReturn(trustedIssuer(caCert)); + + service.importCertificate(multipart(userCert)); + + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(CertificateEntity.class); + Mockito.verify(certificateRepository).save(entityCaptor.capture()); + assertEquals("RSA", entityCaptor.getValue().getAlgoType()); + } + + @Test + void shouldReturnRevokedRuntimeStatusInListWhenCrlMatches() { + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + EntityService entityService = Mockito.mock(EntityService.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CrlRevocationService crlRevocationService = Mockito.mock(CrlRevocationService.class); + CertificateService service = new CertificateService( + certificateRepository, + entityService, + trustedCertService, + crlRevocationService + ); + LocalDateTime validFrom = LocalDateTime.now().minusDays(1); + LocalDateTime validTo = LocalDateTime.now().plusDays(30); + CertificateEntity certificate = new CertificateEntity() + .setSubjectDn("CN=entity") + .setIssuerDn("CN=Root") + .setSerialNumber("A") + .setAlgoType("SM2") + .setValidFrom(validFrom) + .setValidTo(validTo) + .setImportTime(LocalDateTime.now()); + certificate.setId(1L); + Page page = new Page<>(1, 10, 1); + page.setRecords(List.of(certificate)); + CertificateListRequest request = new CertificateListRequest(); + Mockito.when(certificateRepository.page(request)).thenReturn(page); + Mockito.when(crlRevocationService.isRevoked("CN=Root", "A")).thenReturn(true); + + Page result = service.list(request); + + assertEquals("REVOKED", result.getRecords().get(0).getRuntimeStatus()); + } + + @Test + void shouldReturnRevokedRuntimeStatusInDetailWhenCrlMatches() { + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + EntityService entityService = Mockito.mock(EntityService.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CrlRevocationService crlRevocationService = Mockito.mock(CrlRevocationService.class); + CertificateService service = new CertificateService( + certificateRepository, + entityService, + trustedCertService, + crlRevocationService + ); + CertificateEntity certificate = new CertificateEntity() + .setSubjectDn("CN=entity") + .setIssuerDn("CN=Root") + .setSerialNumber("A") + .setAlgoType("SM2") + .setFingerprint("ABC") + .setCertData("CERT") + .setValidFrom(LocalDateTime.now().minusDays(1)) + .setValidTo(LocalDateTime.now().plusDays(30)); + certificate.setId(1L); + Mockito.when(certificateRepository.findById(1L)).thenReturn(Optional.of(certificate)); + Mockito.when(crlRevocationService.isRevoked("CN=Root", "A")).thenReturn(true); + + assertEquals("REVOKED", service.detail(1L).getRuntimeStatus()); + } + + private KeyPair sm2KeyPair() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", "BC"); + generator.initialize(new ECGenParameterSpec("sm2p256v1")); + return generator.generateKeyPair(); + } + + private X509Certificate certificate( + KeyPair subjectKeyPair, + KeyPair issuerKeyPair, + String subject, + String issuer, + boolean ca + ) throws Exception { + return certificate(subjectKeyPair, issuerKeyPair, subject, issuer, ca, ca + ? KeyUsage.keyCertSign | KeyUsage.cRLSign + : KeyUsage.digitalSignature); + } + + private X509Certificate certificate( + KeyPair subjectKeyPair, + KeyPair issuerKeyPair, + String subject, + String issuer, + boolean ca, + int keyUsage + ) throws Exception { + Instant now = Instant.now(); + JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( + new X500Name(issuer), + BigInteger.valueOf(System.nanoTime()).abs(), + Date.from(now.minus(1, ChronoUnit.DAYS)), + Date.from(now.plus(30, ChronoUnit.DAYS)), + new X500Name(subject), + subjectKeyPair.getPublic() + ); + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(ca)); + builder.addExtension(Extension.keyUsage, true, new KeyUsage(keyUsage)); + X509CertificateHolder holder = builder.build( + new JcaContentSignerBuilder("SM3withSM2") + .setProvider("BC") + .build(issuerKeyPair.getPrivate()) + ); + return new JcaX509CertificateConverter() + .setProvider("BC") + .getCertificate(holder); + } + + private KeyPair rsaKeyPair() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + return generator.generateKeyPair(); + } + + private X509Certificate rsaCertificate( + KeyPair subjectKeyPair, + KeyPair issuerKeyPair, + String subject, + String issuer, + boolean ca, + int keyUsage + ) throws Exception { + Instant now = Instant.now(); + JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( + new X500Name(issuer), + BigInteger.valueOf(System.nanoTime()).abs(), + Date.from(now.minus(1, ChronoUnit.DAYS)), + Date.from(now.plus(30, ChronoUnit.DAYS)), + new X500Name(subject), + subjectKeyPair.getPublic() + ); + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(ca)); + builder.addExtension(Extension.keyUsage, true, new KeyUsage(keyUsage)); + X509CertificateHolder holder = builder.build( + new JcaContentSignerBuilder("SHA256withRSA") + .build(issuerKeyPair.getPrivate()) + ); + return new JcaX509CertificateConverter().getCertificate(holder); + } + + private TrustedCertEntity trustedIssuer(X509Certificate certificate) { + return new TrustedCertEntity() + .setSubjectDn(certificate.getSubjectX500Principal().getName()) + .setIssuerDn(certificate.getIssuerX500Principal().getName()) + .setCertData(CertPemSupport.toPem(certificate)); + } + + private MockMultipartFile multipart(X509Certificate certificate) throws Exception { + return new MockMultipartFile( + "file", + "cert.pem", + "application/x-pem-file", + CertPemSupport.toPem(certificate).getBytes() + ); + } +} diff --git a/src/test/java/com/cisd/tms/modules/cert/service/CrlServiceTest.java b/src/test/java/com/cisd/tms/modules/cert/service/CrlServiceTest.java new file mode 100644 index 0000000..17e9c40 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/cert/service/CrlServiceTest.java @@ -0,0 +1,391 @@ +package com.cisd.tms.modules.cert.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.CRLNumber; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.X509CRLHolder; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v2CRLBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CRLConverter; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockMultipartFile; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.Date; +import java.util.Optional; +import java.util.List; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CrlServiceTest { + + static { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + @Test + void shouldImportValidSm2CrlAndSaveMetadataAndRevokedRows() throws Exception { + CrlRepository crlRepository = Mockito.mock(CrlRepository.class); + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CrlService service = new CrlService(crlRepository, certificateRepository, trustedCertService); + KeyPair caKeyPair = sm2KeyPair(); + KeyPair leafKeyPair = sm2KeyPair(); + X509Certificate caCert = certificate( + caKeyPair, + caKeyPair, + "CN=Root", + "CN=Root", + true, + KeyUsage.keyCertSign | KeyUsage.cRLSign, + BigInteger.ONE + ); + X509Certificate leafCert = certificate( + leafKeyPair, + caKeyPair, + "CN=entity", + "CN=Root", + false, + KeyUsage.digitalSignature, + BigInteger.TEN + ); + X509CRL crl = crl(caCert, caKeyPair, leafCert.getSerialNumber(), org.bouncycastle.asn1.x509.CRLReason.keyCompromise); + Mockito.when(crlRepository.findCrlByFingerprint(any())).thenReturn(Optional.empty()); + Mockito.when(trustedCertService.resolveCrlIssuer(crl)).thenReturn(trusted(caCert)); + Mockito.when(certificateRepository.findByIssuerDnAndSerialNumber(caCert.getSubjectX500Principal().getName(), "A")) + .thenReturn(Optional.of(localCertificate(leafCert))); + Mockito.when(crlRepository.saveCrl(any())).thenAnswer(invocation -> { + CertificateCrlEntity saved = invocation.getArgument(0); + saved.setId(100L); + return saved; + }); + + service.importCrl(multipart(crl)); + + Mockito.verify(crlRepository).saveCrl(Mockito.argThat(saved -> + saved.getRevokedCount() == 1 + && "SM2".equals(saved.getAlgoType()) + && "CN=Root".equals(saved.getIssuerDn()) + )); + Mockito.verify(crlRepository).saveRevoked(Mockito.argThat(saved -> + "CN=entity".equals(saved.getSubjectDn()) + && "A".equals(saved.getSerialNumber()) + && "keyCompromise".equals(saved.getRevocationReason()) + )); + } + + @Test + void shouldUseIssuerAlgorithmWhenSavingCrlMetadataAndRevokedRows() throws Exception { + CrlRepository crlRepository = Mockito.mock(CrlRepository.class); + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CrlService service = new CrlService(crlRepository, certificateRepository, trustedCertService); + KeyPair caKeyPair = sm2KeyPair(); + X509Certificate caCert = certificate( + caKeyPair, + caKeyPair, + "CN=Root", + "CN=Root", + true, + KeyUsage.keyCertSign | KeyUsage.cRLSign, + BigInteger.ONE + ); + X509CRL crl = crl(caCert, caKeyPair, BigInteger.TEN, org.bouncycastle.asn1.x509.CRLReason.keyCompromise); + TrustedCertEntity issuer = trusted(caCert); + issuer.setAlgoType("RSA"); + Mockito.when(crlRepository.findCrlByFingerprint(any())).thenReturn(Optional.empty()); + Mockito.when(trustedCertService.resolveCrlIssuer(crl)).thenReturn(issuer); + Mockito.when(crlRepository.saveCrl(any())).thenAnswer(invocation -> { + CertificateCrlEntity saved = invocation.getArgument(0); + saved.setId(101L); + return saved; + }); + + service.importCrl(multipart(crl)); + + Mockito.verify(crlRepository).saveCrl(Mockito.argThat(saved -> "RSA".equals(saved.getAlgoType()))); + Mockito.verify(crlRepository).saveRevoked(Mockito.argThat(saved -> "RSA".equals(saved.getAlgoType()))); + } + + @Test + void shouldRejectDuplicateCrlFingerprint() throws Exception { + CrlRepository crlRepository = Mockito.mock(CrlRepository.class); + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CrlService service = new CrlService(crlRepository, certificateRepository, trustedCertService); + KeyPair caKeyPair = sm2KeyPair(); + X509Certificate caCert = certificate( + caKeyPair, + caKeyPair, + "CN=Root", + "CN=Root", + true, + KeyUsage.keyCertSign | KeyUsage.cRLSign, + BigInteger.ONE + ); + X509CRL crl = crl(caCert, caKeyPair, BigInteger.TEN, org.bouncycastle.asn1.x509.CRLReason.keyCompromise); + Mockito.when(crlRepository.findCrlByFingerprint(any())).thenReturn(Optional.of(new CertificateCrlEntity())); + + assertThrows(RuntimeException.class, () -> service.importCrl(multipart(crl))); + + Mockito.verify(crlRepository, Mockito.never()).saveCrl(any()); + } + + @Test + void shouldRejectCrlWhenIssuerCannotBeResolved() throws Exception { + CrlRepository crlRepository = Mockito.mock(CrlRepository.class); + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + TrustedCertService trustedCertService = Mockito.mock(TrustedCertService.class); + CrlService service = new CrlService(crlRepository, certificateRepository, trustedCertService); + KeyPair caKeyPair = sm2KeyPair(); + X509Certificate caCert = certificate( + caKeyPair, + caKeyPair, + "CN=Root", + "CN=Root", + true, + KeyUsage.keyCertSign | KeyUsage.cRLSign, + BigInteger.ONE + ); + X509CRL crl = crl(caCert, caKeyPair, BigInteger.TEN, org.bouncycastle.asn1.x509.CRLReason.keyCompromise); + Mockito.when(crlRepository.findCrlByFingerprint(any())).thenReturn(Optional.empty()); + Mockito.when(trustedCertService.resolveCrlIssuer(crl)).thenThrow(new RuntimeException("issuer missing")); + + assertThrows(RuntimeException.class, () -> service.importCrl(multipart(crl))); + + Mockito.verify(crlRepository, Mockito.never()).saveCrl(any()); + } + + @Test + void shouldRejectCrlWhenThisUpdateIsInFuture() throws Exception { + CrlRepository crlRepository = Mockito.mock(CrlRepository.class); + CrlService service = new CrlService(crlRepository, Mockito.mock(CertificateRepository.class), Mockito.mock(TrustedCertService.class)); + KeyPair caKeyPair = sm2KeyPair(); + X509Certificate caCert = certificate( + caKeyPair, + caKeyPair, + "CN=Root", + "CN=Root", + true, + KeyUsage.keyCertSign | KeyUsage.cRLSign, + BigInteger.ONE + ); + Instant now = Instant.now(); + X509CRL crl = crl( + caCert, + caKeyPair, + BigInteger.TEN, + org.bouncycastle.asn1.x509.CRLReason.keyCompromise, + now.plus(1, ChronoUnit.DAYS), + now.plus(2, ChronoUnit.DAYS) + ); + + assertThrows(RuntimeException.class, () -> service.importCrl(multipart(crl))); + + Mockito.verify(crlRepository, Mockito.never()).saveCrl(any()); + } + + @Test + void shouldRejectCrlWhenNextUpdateIsExpired() throws Exception { + CrlRepository crlRepository = Mockito.mock(CrlRepository.class); + CrlService service = new CrlService(crlRepository, Mockito.mock(CertificateRepository.class), Mockito.mock(TrustedCertService.class)); + KeyPair caKeyPair = sm2KeyPair(); + X509Certificate caCert = certificate( + caKeyPair, + caKeyPair, + "CN=Root", + "CN=Root", + true, + KeyUsage.keyCertSign | KeyUsage.cRLSign, + BigInteger.ONE + ); + Instant now = Instant.now(); + X509CRL crl = crl( + caCert, + caKeyPair, + BigInteger.TEN, + org.bouncycastle.asn1.x509.CRLReason.keyCompromise, + now.minus(3, ChronoUnit.DAYS), + now.minus(1, ChronoUnit.DAYS) + ); + + assertThrows(RuntimeException.class, () -> service.importCrl(multipart(crl))); + + Mockito.verify(crlRepository, Mockito.never()).saveCrl(any()); + } + + @Test + void shouldListRevokedCertificateRows() { + CrlRepository crlRepository = Mockito.mock(CrlRepository.class); + CrlService service = new CrlService(crlRepository, Mockito.mock(CertificateRepository.class), Mockito.mock(TrustedCertService.class)); + LocalDateTime revocationTime = LocalDateTime.of(2026, 1, 27, 11, 0); + CertificateCrlRevokedEntity revoked = new CertificateCrlRevokedEntity() + .setSubjectDn("CN=entity") + .setIssuerDn("CN=Root") + .setSerialNumber("A") + .setAlgoType("SM2") + .setCrlNumber("123") + .setRevocationReason("keyCompromise") + .setRevocationTime(revocationTime); + revoked.setId(1L); + Page page = new Page<>(1, 10, 1); + page.setRecords(List.of(revoked)); + CrlListRequest request = new CrlListRequest(); + Mockito.when(crlRepository.pageRevoked(request)).thenReturn(page); + + Page result = service.list(request); + + assertEquals(1, result.getRecords().size()); + CrlRevokedItemResponse item = result.getRecords().get(0); + assertEquals(1L, item.getId()); + assertEquals("CN=entity", item.getSubjectDn()); + assertEquals("CN=Root", item.getIssuerDn()); + assertEquals("A", item.getSerialNumber()); + assertEquals("SM2", item.getAlgoType()); + assertEquals("123", item.getCrlNumber()); + assertEquals("keyCompromise", item.getRevocationReason()); + assertEquals(revocationTime, item.getRevocationTime()); + } + + @Test + void shouldDeleteCrlMetadataAndRevokedRows() { + CrlRepository crlRepository = Mockito.mock(CrlRepository.class); + CrlService service = new CrlService(crlRepository, Mockito.mock(CertificateRepository.class), Mockito.mock(TrustedCertService.class)); + CertificateCrlEntity crl = new CertificateCrlEntity(); + crl.setId(99L); + Mockito.when(crlRepository.findCrlById(99L)).thenReturn(Optional.of(crl)); + + service.delete(99L); + + Mockito.verify(crlRepository).deleteRevokedByCrlId(99L); + Mockito.verify(crlRepository).deleteCrlById(99L); + } + + private KeyPair sm2KeyPair() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", "BC"); + generator.initialize(new ECGenParameterSpec("sm2p256v1")); + return generator.generateKeyPair(); + } + + private X509Certificate certificate( + KeyPair subjectKeyPair, + KeyPair issuerKeyPair, + String subject, + String issuer, + boolean ca, + int keyUsage, + BigInteger serialNumber + ) throws Exception { + Instant now = Instant.now(); + JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( + new X500Name(issuer), + serialNumber, + Date.from(now.minus(1, ChronoUnit.DAYS)), + Date.from(now.plus(30, ChronoUnit.DAYS)), + new X500Name(subject), + subjectKeyPair.getPublic() + ); + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(ca)); + builder.addExtension(Extension.keyUsage, true, new KeyUsage(keyUsage)); + X509CertificateHolder holder = builder.build( + new JcaContentSignerBuilder("SM3withSM2") + .setProvider("BC") + .build(issuerKeyPair.getPrivate()) + ); + return new JcaX509CertificateConverter() + .setProvider("BC") + .getCertificate(holder); + } + + private X509CRL crl(X509Certificate issuer, KeyPair issuerKeyPair, BigInteger revokedSerial, int reason) throws Exception { + Instant now = Instant.now(); + return crl( + issuer, + issuerKeyPair, + revokedSerial, + reason, + now.minus(1, ChronoUnit.HOURS), + now.plus(7, ChronoUnit.DAYS) + ); + } + + private X509CRL crl( + X509Certificate issuer, + KeyPair issuerKeyPair, + BigInteger revokedSerial, + int reason, + Instant thisUpdate, + Instant nextUpdate + ) throws Exception { + X509v2CRLBuilder builder = new X509v2CRLBuilder( + new X500Name(issuer.getSubjectX500Principal().getName()), + Date.from(thisUpdate) + ); + builder.setNextUpdate(Date.from(nextUpdate)); + builder.addCRLEntry(revokedSerial, Date.from(thisUpdate), reason); + builder.addExtension(Extension.cRLNumber, false, new CRLNumber(BigInteger.valueOf(123))); + X509CRLHolder holder = builder.build( + new JcaContentSignerBuilder("SM3withSM2") + .setProvider("BC") + .build(issuerKeyPair.getPrivate()) + ); + return new JcaX509CRLConverter() + .setProvider("BC") + .getCRL(holder); + } + + private TrustedCertEntity trusted(X509Certificate certificate) { + return new TrustedCertEntity() + .setSubjectDn(certificate.getSubjectX500Principal().getName()) + .setIssuerDn(certificate.getIssuerX500Principal().getName()) + .setCertData(CertPemSupport.toPem(certificate)) + .setAlgoType("SM2"); + } + + private CertificateEntity localCertificate(X509Certificate certificate) { + return new CertificateEntity() + .setSubjectDn(certificate.getSubjectX500Principal().getName()) + .setIssuerDn(certificate.getIssuerX500Principal().getName()) + .setSerialNumber(certificate.getSerialNumber().toString(16).toUpperCase()) + .setAlgoType("SM2"); + } + + private MockMultipartFile multipart(X509CRL crl) throws Exception { + String base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8)) + .encodeToString(crl.getEncoded()); + String pem = "-----BEGIN X509 CRL-----\n" + base64 + "\n-----END X509 CRL-----"; + return new MockMultipartFile("file", "revoked.crl", "application/pkix-crl", pem.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/test/java/com/cisd/tms/modules/cert/service/EntityServiceTest.java b/src/test/java/com/cisd/tms/modules/cert/service/EntityServiceTest.java index f814d65..2973a8d 100644 --- a/src/test/java/com/cisd/tms/modules/cert/service/EntityServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/cert/service/EntityServiceTest.java @@ -27,6 +27,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import java.io.StringReader; +import java.lang.reflect.Modifier; import java.security.KeyPairGenerator; import java.security.Security; import java.security.interfaces.ECPublicKey; @@ -38,10 +39,18 @@ import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; class EntityServiceTest { + @Test + void shouldSerializeEntityCreationInsideSingleTmsProcess() throws Exception { + assertTrue(Modifier.isSynchronized(EntityService.class + .getMethod("create", EntityCreateRequest.class) + .getModifiers())); + } + @Test void shouldListEntityWithUiFieldsAndStatusName() { KeyEntityRepository keyEntityRepository = Mockito.mock(KeyEntityRepository.class); diff --git a/src/test/java/com/cisd/tms/modules/cert/service/TrustedCertServiceTest.java b/src/test/java/com/cisd/tms/modules/cert/service/TrustedCertServiceTest.java new file mode 100644 index 0000000..55cd164 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/cert/service/TrustedCertServiceTest.java @@ -0,0 +1,304 @@ +package com.cisd.tms.modules.cert.service; + +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.cert.dto.TrustedCertDeleteRequest; +import com.cisd.tms.modules.cert.entity.TrustedCertEntity; +import com.cisd.tms.modules.cert.repository.CertificateRepository; +import com.cisd.tms.modules.cert.repository.TrustedCertRepository; +import com.cisd.tms.modules.cert.support.CertPemSupport; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockMultipartFile; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import javax.security.auth.x500.X500Principal; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TrustedCertServiceTest { + + static { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + @Test + void shouldRejectTrustedImportWhenCertificateIsNotCa() throws Exception { + TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class); + TrustedCertService service = new TrustedCertService(trustedCertRepository, Mockito.mock(CertificateRepository.class)); + KeyPair leafKeyPair = sm2KeyPair(); + X509Certificate certificate = certificate( + leafKeyPair, + leafKeyPair, + "CN=Leaf", + "CN=Leaf", + false, + KeyUsage.digitalSignature + ); + Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty()); + + assertThrows(BizException.class, () -> service.importTrusted(multipart(certificate), "leaf")); + } + + @Test + void shouldRejectTrustedImportWhenCaCannotSignCertificates() throws Exception { + TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class); + TrustedCertService service = new TrustedCertService(trustedCertRepository, Mockito.mock(CertificateRepository.class)); + KeyPair rootKeyPair = sm2KeyPair(); + KeyPair intermediateKeyPair = sm2KeyPair(); + X509Certificate root = certificate( + rootKeyPair, + rootKeyPair, + "CN=Root", + "CN=Root", + true, + KeyUsage.keyCertSign | KeyUsage.cRLSign + ); + X509Certificate intermediate = certificate( + intermediateKeyPair, + rootKeyPair, + "CN=Intermediate", + "CN=Root", + true, + KeyUsage.digitalSignature + ); + Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty()); + Mockito.when(trustedCertRepository.findBySubjectDn(intermediate.getIssuerX500Principal().getName())) + .thenReturn(List.of(trusted(root, 0))); + + assertThrows(BizException.class, () -> service.importTrusted(multipart(intermediate), "intermediate")); + } + + @Test + void shouldRejectTrustedImportWhenCaKeyUsageIsMissing() throws Exception { + TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class); + TrustedCertService service = new TrustedCertService(trustedCertRepository, Mockito.mock(CertificateRepository.class)); + KeyPair rootKeyPair = sm2KeyPair(); + X509Certificate root = certificate( + rootKeyPair, + rootKeyPair, + "CN=Root", + "CN=Root", + true, + null + ); + Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty()); + + assertThrows(BizException.class, () -> service.importTrusted(multipart(root), "root")); + } + + @Test + void shouldResolveIssuerBySignatureWhenSubjectDnHasMultipleCandidates() throws Exception { + TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class); + TrustedCertService service = new TrustedCertService(trustedCertRepository, Mockito.mock(CertificateRepository.class)); + KeyPair wrongRootKeyPair = sm2KeyPair(); + KeyPair rightRootKeyPair = sm2KeyPair(); + KeyPair intermediateKeyPair = sm2KeyPair(); + X509Certificate wrongRoot = certificate( + wrongRootKeyPair, + wrongRootKeyPair, + "CN=Root", + "CN=Root", + true, + KeyUsage.keyCertSign | KeyUsage.cRLSign + ); + X509Certificate rightRoot = certificate( + rightRootKeyPair, + rightRootKeyPair, + "CN=Root", + "CN=Root", + true, + KeyUsage.keyCertSign | KeyUsage.cRLSign + ); + X509Certificate intermediate = certificate( + intermediateKeyPair, + rightRootKeyPair, + "CN=Intermediate", + "CN=Root", + true, + KeyUsage.keyCertSign | KeyUsage.cRLSign + ); + Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty()); + Mockito.when(trustedCertRepository.findBySubjectDn(intermediate.getIssuerX500Principal().getName())) + .thenReturn(List.of(trusted(wrongRoot, 0), trusted(rightRoot, 0))); + Mockito.when(trustedCertRepository.save(Mockito.any())).thenAnswer(invocation -> invocation.getArgument(0)); + + service.importTrusted(multipart(intermediate), "intermediate"); + + Mockito.verify(trustedCertRepository).save(Mockito.any()); + } + + @Test + void shouldIgnoreSelfWhenCheckingRootDeleteDependencies() throws Exception { + TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class); + CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class); + TrustedCertService service = new TrustedCertService(trustedCertRepository, certificateRepository); + TrustedCertEntity root = new TrustedCertEntity() + .setSubjectDn("CN=Root") + .setIssuerDn("CN=Root"); + root.setId(10L); + Mockito.when(trustedCertRepository.findById(10L)).thenReturn(Optional.of(root)); + Mockito.when(trustedCertRepository.countByIssuerDnExcludingId("CN=Root", 10L)).thenReturn(0L); + Mockito.when(certificateRepository.countByIssuerDn("CN=Root")).thenReturn(0L); + TrustedCertDeleteRequest request = new TrustedCertDeleteRequest(); + request.setId(10L); + + service.delete(request); + + Mockito.verify(trustedCertRepository).deleteById(10L); + } + + @Test + void shouldRejectCrlIssuerWhenCaCannotSignCrls() throws Exception { + TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class); + TrustedCertService service = new TrustedCertService(trustedCertRepository, Mockito.mock(CertificateRepository.class)); + KeyPair rootKeyPair = sm2KeyPair(); + X509Certificate root = certificate( + rootKeyPair, + rootKeyPair, + "CN=Root", + "CN=Root", + true, + KeyUsage.keyCertSign + ); + X509CRL crl = Mockito.mock(X509CRL.class); + Mockito.when(crl.getIssuerX500Principal()).thenReturn(new X500Principal(root.getSubjectX500Principal().getName())); + Mockito.when(trustedCertRepository.findBySubjectDn(root.getSubjectX500Principal().getName())) + .thenReturn(List.of(trusted(root, 0))); + + assertThrows(BizException.class, () -> service.resolveCrlIssuer(crl)); + } + + @Test + void shouldPersistRsaAlgorithmWhenImportingRsaTrustedCa() throws Exception { + TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class); + TrustedCertService service = new TrustedCertService(trustedCertRepository, Mockito.mock(CertificateRepository.class)); + KeyPair rootKeyPair = rsaKeyPair(); + X509Certificate root = rsaCertificate( + rootKeyPair, + rootKeyPair, + "CN=RSA Root", + "CN=RSA Root", + true, + KeyUsage.keyCertSign | KeyUsage.cRLSign + ); + Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty()); + Mockito.when(trustedCertRepository.save(Mockito.any())).thenAnswer(invocation -> invocation.getArgument(0)); + + service.importTrusted(multipart(root), "rsa-root"); + + org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(TrustedCertEntity.class); + Mockito.verify(trustedCertRepository).save(captor.capture()); + assertEquals("RSA", captor.getValue().getAlgoType()); + } + + private KeyPair sm2KeyPair() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", "BC"); + generator.initialize(new ECGenParameterSpec("sm2p256v1")); + return generator.generateKeyPair(); + } + + private KeyPair rsaKeyPair() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + return generator.generateKeyPair(); + } + + private X509Certificate certificate( + KeyPair subjectKeyPair, + KeyPair issuerKeyPair, + String subject, + String issuer, + boolean ca, + Integer keyUsage + ) throws Exception { + Instant now = Instant.now(); + JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( + new X500Name(issuer), + BigInteger.valueOf(System.nanoTime()).abs(), + Date.from(now.minus(1, ChronoUnit.DAYS)), + Date.from(now.plus(30, ChronoUnit.DAYS)), + new X500Name(subject), + subjectKeyPair.getPublic() + ); + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(ca)); + if (keyUsage != null) { + builder.addExtension(Extension.keyUsage, true, new KeyUsage(keyUsage)); + } + X509CertificateHolder holder = builder.build( + new JcaContentSignerBuilder("SM3withSM2") + .setProvider("BC") + .build(issuerKeyPair.getPrivate()) + ); + return new JcaX509CertificateConverter() + .setProvider("BC") + .getCertificate(holder); + } + + private X509Certificate rsaCertificate( + KeyPair subjectKeyPair, + KeyPair issuerKeyPair, + String subject, + String issuer, + boolean ca, + Integer keyUsage + ) throws Exception { + Instant now = Instant.now(); + JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( + new X500Name(issuer), + BigInteger.valueOf(System.nanoTime()).abs(), + Date.from(now.minus(1, ChronoUnit.DAYS)), + Date.from(now.plus(30, ChronoUnit.DAYS)), + new X500Name(subject), + subjectKeyPair.getPublic() + ); + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(ca)); + if (keyUsage != null) { + builder.addExtension(Extension.keyUsage, true, new KeyUsage(keyUsage)); + } + X509CertificateHolder holder = builder.build( + new JcaContentSignerBuilder("SHA256withRSA") + .build(issuerKeyPair.getPrivate()) + ); + return new JcaX509CertificateConverter().getCertificate(holder); + } + + private TrustedCertEntity trusted(X509Certificate certificate, int caLevel) { + return new TrustedCertEntity() + .setSubjectDn(certificate.getSubjectX500Principal().getName()) + .setIssuerDn(certificate.getIssuerX500Principal().getName()) + .setCertData(CertPemSupport.toPem(certificate)) + .setCaLevel(caLevel); + } + + private MockMultipartFile multipart(X509Certificate certificate) throws Exception { + return new MockMultipartFile( + "file", + "cert.pem", + "application/x-pem-file", + CertPemSupport.toPem(certificate).getBytes() + ); + } +} diff --git a/src/test/java/com/cisd/tms/modules/cert/support/CertPemSupportTest.java b/src/test/java/com/cisd/tms/modules/cert/support/CertPemSupportTest.java new file mode 100644 index 0000000..0889e3d --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/cert/support/CertPemSupportTest.java @@ -0,0 +1,60 @@ +package com.cisd.tms.modules.cert.support; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509CRLHolder; +import org.bouncycastle.cert.X509v2CRLBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CRLConverter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.cert.X509CRL; +import java.security.spec.ECGenParameterSpec; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CertPemSupportTest { + + static { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + @Test + void shouldParseCommonBeginCrlPemLabel() throws Exception { + KeyPair keyPair = sm2KeyPair(); + X509CRL crl = crl(keyPair); + String base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8)) + .encodeToString(crl.getEncoded()); + String pem = "-----BEGIN CRL-----\n" + base64 + "\n-----END CRL-----"; + + X509CRL parsed = CertPemSupport.parseCrl(pem.getBytes(StandardCharsets.UTF_8)); + + assertEquals(crl.getIssuerX500Principal(), parsed.getIssuerX500Principal()); + } + + private KeyPair sm2KeyPair() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", "BC"); + generator.initialize(new ECGenParameterSpec("sm2p256v1")); + return generator.generateKeyPair(); + } + + private X509CRL crl(KeyPair issuerKeyPair) throws Exception { + Instant now = Instant.now(); + X509v2CRLBuilder builder = new X509v2CRLBuilder(new X500Name("CN=Root"), Date.from(now.minus(1, ChronoUnit.HOURS))); + builder.setNextUpdate(Date.from(now.plus(1, ChronoUnit.DAYS))); + builder.addCRLEntry(BigInteger.TEN, Date.from(now.minus(30, ChronoUnit.MINUTES)), org.bouncycastle.asn1.x509.CRLReason.keyCompromise); + X509CRLHolder holder = builder.build(new JcaContentSignerBuilder("SM3withSM2").setProvider("BC").build(issuerKeyPair.getPrivate())); + return new JcaX509CRLConverter().setProvider("BC").getCRL(holder); + } +}