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

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