feat:吊销列表、证书管理功能
This commit is contained in:
@@ -92,9 +92,28 @@ Open:
|
|||||||
- Upgrade create task API: `POST /api/v1/upgrades`
|
- Upgrade create task API: `POST /api/v1/upgrades`
|
||||||
- Upgrade execute API: `POST /api/v1/upgrades/{taskId}/execute`
|
- Upgrade execute API: `POST /api/v1/upgrades/{taskId}/execute`
|
||||||
- Upgrade list API: `GET /api/v1/upgrades`
|
- 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 count API: `GET /api/v1/device/crypto/device-count`
|
||||||
- PCIe crypto HMAC API: `POST /api/v1/device/crypto/hmac`
|
- 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
|
## CISD Reset
|
||||||
|
|
||||||
最小调用顺序:
|
最小调用顺序:
|
||||||
|
|||||||
+14
-1
@@ -3,6 +3,7 @@ package com.cisd.tms.modules.auth.security;
|
|||||||
import com.cisd.tms.common.api.ApiResponse;
|
import com.cisd.tms.common.api.ApiResponse;
|
||||||
import com.cisd.tms.common.enums.ErrorCode;
|
import com.cisd.tms.common.enums.ErrorCode;
|
||||||
import com.cisd.tms.common.util.HttpResponseUtil;
|
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.modules.log.service.OperationAuditService;
|
||||||
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
|
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -41,7 +42,7 @@ public class InternalAuthorizationInterceptor implements HandlerInterceptor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String currentAuthLevel = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL);
|
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");
|
writeForbidden(response, "auth level not allowed");
|
||||||
operationAuditService.recordDeniedLog(request, handlerMethod, "auth level not allowed");
|
operationAuditService.recordDeniedLog(request, handlerMethod, "auth level not allowed");
|
||||||
return false;
|
return false;
|
||||||
@@ -51,6 +52,18 @@ public class InternalAuthorizationInterceptor implements HandlerInterceptor {
|
|||||||
return true;
|
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) {
|
private <T extends java.lang.annotation.Annotation> T findAnnotation(HandlerMethod handlerMethod, Class<T> annotationType) {
|
||||||
T methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getMethod(), annotationType);
|
T methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getMethod(), annotationType);
|
||||||
if (methodAnnotation != null) {
|
if (methodAnnotation != null) {
|
||||||
|
|||||||
@@ -2,25 +2,33 @@ package com.cisd.tms.modules.cert.controller;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.cisd.tms.common.api.ApiResponse;
|
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.CertificateDetailResponse;
|
||||||
import com.cisd.tms.modules.cert.dto.CertificateItemResponse;
|
import com.cisd.tms.modules.cert.dto.CertificateItemResponse;
|
||||||
import com.cisd.tms.modules.cert.dto.CertificateListRequest;
|
import com.cisd.tms.modules.cert.dto.CertificateListRequest;
|
||||||
import com.cisd.tms.modules.cert.dto.IdRequest;
|
import com.cisd.tms.modules.cert.dto.IdRequest;
|
||||||
import com.cisd.tms.modules.cert.dto.ImportCertificateResponse;
|
import com.cisd.tms.modules.cert.dto.ImportCertificateResponse;
|
||||||
import com.cisd.tms.modules.cert.service.CertificateService;
|
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 io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
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.RequestPart;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/certificates")
|
@RequestMapping("/api/v1/certificates")
|
||||||
@Tag(name = "Certificates")
|
@Tag(name = "证书管理", description = "实体证书和用户证书的导入、查询、详情和删除接口")
|
||||||
public class CertificateController {
|
public class CertificateController {
|
||||||
|
|
||||||
private final CertificateService certificateService;
|
private final CertificateService certificateService;
|
||||||
@@ -30,28 +38,38 @@ public class CertificateController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/list")
|
@PostMapping("/list")
|
||||||
|
@Operation(summary = "查询证书列表", description = "按机构号、算法类型、证书类型和证书主题分页查询实体/用户证书,返回证书有效期、运行状态和指纹信息。")
|
||||||
|
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.LIMITED)
|
||||||
public ApiResponse<Page<CertificateItemResponse>> list(@Valid @RequestBody CertificateListRequest request) {
|
public ApiResponse<Page<CertificateItemResponse>> list(@Valid @RequestBody CertificateListRequest request) {
|
||||||
return ApiResponse.success(certificateService.list(request));
|
return ApiResponse.success(certificateService.list(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/import")
|
@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(
|
public ApiResponse<ImportCertificateResponse> importCertificate(
|
||||||
@RequestPart("file") MultipartFile file,
|
@Parameter(description = "证书文件,支持 PEM/DER X.509 证书")
|
||||||
@RequestParam("certType") String certType,
|
@RequestPart("file") MultipartFile file
|
||||||
@RequestParam(value = "entityId", required = false) Long entityId
|
|
||||||
) {
|
) {
|
||||||
return ApiResponse.success(certificateService.importCertificate(file, certType, entityId));
|
return ApiResponse.success(certificateService.importCertificate(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/detail")
|
@PostMapping("/detail")
|
||||||
|
@Operation(summary = "查询证书详情", description = "根据证书 ID 查询证书详情,包含证书 PEM 内容、DN、序列号、算法、有效期和运行状态。")
|
||||||
|
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.LIMITED)
|
||||||
public ApiResponse<CertificateDetailResponse> detail(@Valid @RequestBody IdRequest request) {
|
public ApiResponse<CertificateDetailResponse> detail(@Valid @RequestBody IdRequest request) {
|
||||||
return ApiResponse.success(certificateService.detail(request.getId()));
|
return ApiResponse.success(certificateService.detail(request.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/delete")
|
@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) {
|
public ApiResponse<Void> delete(@Valid @RequestBody IdRequest request) {
|
||||||
certificateService.delete(request.getId());
|
certificateService.delete(request.getId());
|
||||||
return ApiResponse.success();
|
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.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.cisd.tms.common.api.ApiResponse;
|
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.EntityCreateRequest;
|
||||||
import com.cisd.tms.modules.cert.dto.EntityCreateResponse;
|
import com.cisd.tms.modules.cert.dto.EntityCreateResponse;
|
||||||
import com.cisd.tms.modules.cert.dto.EntityGenP10Request;
|
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.IdRequest;
|
||||||
import com.cisd.tms.modules.cert.dto.P10Response;
|
import com.cisd.tms.modules.cert.dto.P10Response;
|
||||||
import com.cisd.tms.modules.cert.service.EntityService;
|
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 io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -20,7 +28,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/entities")
|
@RequestMapping("/api/v1/entities")
|
||||||
@Tag(name = "Cert Entities")
|
@Tag(name = "证书实体管理", description = "证书实体密钥生成、机构信息维护、P10 生成和 P10 查询接口")
|
||||||
public class EntityController {
|
public class EntityController {
|
||||||
|
|
||||||
private final EntityService entityService;
|
private final EntityService entityService;
|
||||||
@@ -30,35 +38,54 @@ public class EntityController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/list")
|
@PostMapping("/list")
|
||||||
|
@Operation(summary = "查询证书实体列表", description = "按机构号、算法类型和实体状态分页查询证书实体,返回密钥索引、证书主题、允许访问 IP 和状态信息。")
|
||||||
|
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.LIMITED)
|
||||||
public ApiResponse<Page<EntityItemResponse>> list(@Valid @RequestBody EntityListRequest request) {
|
public ApiResponse<Page<EntityItemResponse>> list(@Valid @RequestBody EntityListRequest request) {
|
||||||
return ApiResponse.success(entityService.list(request));
|
return ApiResponse.success(entityService.list(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/create")
|
@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) {
|
public ApiResponse<EntityCreateResponse> create(@Valid @RequestBody EntityCreateRequest request) {
|
||||||
return ApiResponse.success(entityService.create(request));
|
return ApiResponse.success(entityService.create(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/update")
|
@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) {
|
public ApiResponse<Void> update(@Valid @RequestBody EntityUpdateRequest request) {
|
||||||
entityService.update(request);
|
entityService.update(request);
|
||||||
return ApiResponse.success();
|
return ApiResponse.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/delete")
|
@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) {
|
public ApiResponse<Void> delete(@Valid @RequestBody IdRequest request) {
|
||||||
entityService.delete(request.getId());
|
entityService.delete(request.getId());
|
||||||
return ApiResponse.success();
|
return ApiResponse.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/gen-p10")
|
@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) {
|
public ApiResponse<P10Response> genP10(@Valid @RequestBody EntityGenP10Request request) {
|
||||||
return ApiResponse.success(new P10Response(entityService.generateP10(request)));
|
return ApiResponse.success(new P10Response(entityService.generateP10(request)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/p10")
|
@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) {
|
public ApiResponse<P10Response> p10(@Valid @RequestBody IdRequest request) {
|
||||||
return ApiResponse.success(new P10Response(entityService.getP10(request.getId())));
|
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.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.cisd.tms.common.api.ApiResponse;
|
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.DeleteDependencyResponse;
|
||||||
import com.cisd.tms.modules.cert.dto.IdRequest;
|
import com.cisd.tms.modules.cert.dto.IdRequest;
|
||||||
import com.cisd.tms.modules.cert.dto.ImportCertificateResponse;
|
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.TrustedCertItemResponse;
|
||||||
import com.cisd.tms.modules.cert.dto.TrustedCertListRequest;
|
import com.cisd.tms.modules.cert.dto.TrustedCertListRequest;
|
||||||
import com.cisd.tms.modules.cert.service.TrustedCertService;
|
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 io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
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.RequestPart;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/trusted-certificates")
|
@RequestMapping("/api/v1/trusted-certificates")
|
||||||
@Tag(name = "Trusted Certificates")
|
@Tag(name = "可信CA证书管理", description = "可信根 CA 和中间 CA 证书的导入、查询、详情和删除接口")
|
||||||
public class TrustedCertController {
|
public class TrustedCertController {
|
||||||
|
|
||||||
private final TrustedCertService trustedCertService;
|
private final TrustedCertService trustedCertService;
|
||||||
@@ -32,26 +40,37 @@ public class TrustedCertController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/list")
|
@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) {
|
public ApiResponse<Page<TrustedCertItemResponse>> list(@Valid @RequestBody TrustedCertListRequest request) {
|
||||||
return ApiResponse.success(trustedCertService.list(request));
|
return ApiResponse.success(trustedCertService.list(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/import")
|
@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(
|
public ApiResponse<ImportCertificateResponse> importTrusted(
|
||||||
@RequestPart("file") MultipartFile file,
|
@Parameter(description = "可信 CA 证书文件,支持 PEM/DER X.509 证书")
|
||||||
@RequestParam("alias") String alias
|
@RequestPart("file") MultipartFile file
|
||||||
) {
|
) {
|
||||||
return ApiResponse.success(trustedCertService.importTrusted(file, alias));
|
return ApiResponse.success(trustedCertService.importTrusted(file, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/detail")
|
@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) {
|
public ApiResponse<TrustedCertDetailResponse> detail(@Valid @RequestBody IdRequest request) {
|
||||||
return ApiResponse.success(trustedCertService.detail(request.getId()));
|
return ApiResponse.success(trustedCertService.detail(request.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/delete")
|
@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) {
|
public ApiResponse<DeleteDependencyResponse> delete(@Valid @RequestBody TrustedCertDeleteRequest request) {
|
||||||
return ApiResponse.success(trustedCertService.delete(request));
|
return ApiResponse.success(trustedCertService.delete(request));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,46 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "证书详情响应")
|
||||||
public class CertificateDetailResponse {
|
public class CertificateDetailResponse {
|
||||||
|
@Schema(description = "证书 ID", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "关联证书实体 ID;用户证书为空", example = "10")
|
||||||
private Long entityId;
|
private Long entityId;
|
||||||
|
|
||||||
|
@Schema(description = "证书类型,ENTITY 表示实体证书,USER 表示用户证书", example = "ENTITY")
|
||||||
private String certType;
|
private String certType;
|
||||||
|
|
||||||
|
@Schema(description = "证书主题 DN", example = "C=CN,O=CISD,CN=entity")
|
||||||
private String subjectDn;
|
private String subjectDn;
|
||||||
|
|
||||||
|
@Schema(description = "签发者 DN", example = "C=CN,O=GMSSL,CN=MiddleCA for Test")
|
||||||
private String issuerDn;
|
private String issuerDn;
|
||||||
|
|
||||||
|
@Schema(description = "证书序列号,十六进制大写", example = "19DDD25B8D0")
|
||||||
private String serialNumber;
|
private String serialNumber;
|
||||||
|
|
||||||
|
@Schema(description = "算法类型", example = "SM2")
|
||||||
private String algoType;
|
private String algoType;
|
||||||
|
|
||||||
|
@Schema(description = "证书 SHA-256 指纹,十六进制大写", example = "083C476C26DFD8876E0580601EA66726DF06BB20989DFD862BB34B9DD8B68094")
|
||||||
private String fingerprint;
|
private String fingerprint;
|
||||||
|
|
||||||
|
@Schema(description = "证书 PEM 内容")
|
||||||
private String certData;
|
private String certData;
|
||||||
|
|
||||||
|
@Schema(description = "证书生效时间", example = "2026-04-30T00:00:00")
|
||||||
private LocalDateTime validFrom;
|
private LocalDateTime validFrom;
|
||||||
|
|
||||||
|
@Schema(description = "证书失效时间", example = "2027-04-30T00:00:00")
|
||||||
private LocalDateTime validTo;
|
private LocalDateTime validTo;
|
||||||
|
|
||||||
|
@Schema(description = "运行状态,VALID/NOT_YET_VALID/EXPIRED/REVOKED", example = "VALID")
|
||||||
private String runtimeStatus;
|
private String runtimeStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,52 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "证书列表项")
|
||||||
public class CertificateItemResponse {
|
public class CertificateItemResponse {
|
||||||
|
@Schema(description = "证书 ID", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "关联证书实体 ID;用户证书为空", example = "10")
|
||||||
private Long entityId;
|
private Long entityId;
|
||||||
|
|
||||||
|
@Schema(description = "关联证书实体机构号;用户证书为空", example = "00000001")
|
||||||
|
private String orgCode;
|
||||||
|
|
||||||
|
@Schema(description = "证书类型,ENTITY 表示实体证书,USER 表示用户证书", example = "ENTITY")
|
||||||
private String certType;
|
private String certType;
|
||||||
|
|
||||||
|
@Schema(description = "证书类型中文名称", example = "实体证书")
|
||||||
|
private String certTypeName;
|
||||||
|
|
||||||
|
@Schema(description = "证书主题 DN", example = "C=CN,O=CISD,CN=entity")
|
||||||
private String subjectDn;
|
private String subjectDn;
|
||||||
|
|
||||||
|
@Schema(description = "签发者 DN", example = "C=CN,O=GMSSL,CN=MiddleCA for Test")
|
||||||
private String issuerDn;
|
private String issuerDn;
|
||||||
|
|
||||||
|
@Schema(description = "证书序列号,十六进制大写", example = "19DDD25B8D0")
|
||||||
private String serialNumber;
|
private String serialNumber;
|
||||||
|
|
||||||
|
@Schema(description = "算法类型", example = "SM2")
|
||||||
private String algoType;
|
private String algoType;
|
||||||
|
|
||||||
|
@Schema(description = "证书 SHA-256 指纹,十六进制大写", example = "083C476C26DFD8876E0580601EA66726DF06BB20989DFD862BB34B9DD8B68094")
|
||||||
private String fingerprint;
|
private String fingerprint;
|
||||||
|
|
||||||
|
@Schema(description = "证书生效时间", example = "2026-04-30T00:00:00")
|
||||||
private LocalDateTime validFrom;
|
private LocalDateTime validFrom;
|
||||||
|
|
||||||
|
@Schema(description = "证书失效时间", example = "2027-04-30T00:00:00")
|
||||||
private LocalDateTime validTo;
|
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;
|
private String runtimeStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Schema(description = "证书列表查询请求")
|
||||||
public class CertificateListRequest extends CommonPageRequest {
|
public class CertificateListRequest extends CommonPageRequest {
|
||||||
private String certType;
|
@Schema(description = "机构号,按证书实体机构号模糊匹配;仅实体证书有关联机构号", example = "00000001")
|
||||||
private String keyword;
|
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;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.Min;
|
import jakarta.validation.constraints.Min;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "通用分页请求")
|
||||||
public class CommonPageRequest {
|
public class CommonPageRequest {
|
||||||
@Min(1)
|
@Schema(description = "页码,从 1 开始", example = "1")
|
||||||
|
@Min(value = 1, message = "页码必须大于等于1")
|
||||||
private int pageNum = 1;
|
private int pageNum = 1;
|
||||||
@Min(1)
|
|
||||||
|
@Schema(description = "每页数量", example = "10")
|
||||||
|
@Min(value = 1, message = "每页数量必须大于等于1")
|
||||||
private int pageSize = 10;
|
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;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "删除依赖统计响应")
|
||||||
public class DeleteDependencyResponse {
|
public class DeleteDependencyResponse {
|
||||||
|
@Schema(description = "依赖该 CA 的下级 CA 数量", example = "0")
|
||||||
private long childCaCount;
|
private long childCaCount;
|
||||||
|
|
||||||
|
@Schema(description = "该 CA 已签发的本地证书数量", example = "1")
|
||||||
private long issuedCertCount;
|
private long issuedCertCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "创建证书实体请求")
|
||||||
public class EntityCreateRequest {
|
public class EntityCreateRequest {
|
||||||
@NotBlank
|
@Schema(description = "机构代码", example = "00000001")
|
||||||
|
@NotBlank(message = "机构代码不能为空")
|
||||||
private String orgCode;
|
private String orgCode;
|
||||||
@NotBlank
|
|
||||||
|
@Schema(description = "机构名称", example = "测试机构")
|
||||||
|
@NotBlank(message = "机构名称不能为空")
|
||||||
private String orgName;
|
private String orgName;
|
||||||
@NotBlank
|
|
||||||
|
@Schema(description = "算法类型,支持 SM2/RSA", example = "SM2")
|
||||||
|
@NotBlank(message = "算法类型不能为空")
|
||||||
private String algoType;
|
private String algoType;
|
||||||
|
|
||||||
|
@Schema(description = "允许访问该实体证书能力的来源 IP 列表", example = "[\"127.0.0.1\"]")
|
||||||
private List<String> allowIps;
|
private List<String> allowIps;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "创建证书实体响应")
|
||||||
public class EntityCreateResponse {
|
public class EntityCreateResponse {
|
||||||
|
@Schema(description = "创建后的证书实体 ID", example = "1")
|
||||||
private Long entityId;
|
private Long entityId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "生成 P10 证书请求")
|
||||||
public class EntityGenP10Request {
|
public class EntityGenP10Request {
|
||||||
@NotNull
|
@Schema(description = "证书实体 ID", example = "1")
|
||||||
|
@NotNull(message = "实体ID不能为空")
|
||||||
private Long id;
|
private Long id;
|
||||||
@NotNull
|
|
||||||
|
@Schema(description = "证书模式,SINGLE 表示单证,DOUBLE 表示双证", example = "SINGLE")
|
||||||
|
@NotNull(message = "证书模式不能为空")
|
||||||
private String certMode;
|
private String certMode;
|
||||||
|
|
||||||
|
@Schema(description = "证书主题信息")
|
||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull(message = "证书主题不能为空")
|
||||||
private Subject subject;
|
private Subject subject;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,44 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "证书实体列表项")
|
||||||
public class EntityItemResponse {
|
public class EntityItemResponse {
|
||||||
|
@Schema(description = "证书实体 ID", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "机构代码", example = "00000001")
|
||||||
private String orgCode;
|
private String orgCode;
|
||||||
|
|
||||||
|
@Schema(description = "机构名称", example = "测试机构")
|
||||||
private String orgName;
|
private String orgName;
|
||||||
|
|
||||||
|
@Schema(description = "算法类型", example = "SM2")
|
||||||
private String algoType;
|
private String algoType;
|
||||||
|
|
||||||
|
@Schema(description = "密钥长度,SM2 为 256,RSA 通常为 2048", example = "256")
|
||||||
private Integer keyLen;
|
private Integer keyLen;
|
||||||
|
|
||||||
|
@Schema(description = "密码卡用户密钥索引", example = "2")
|
||||||
private Integer keyIdx;
|
private Integer keyIdx;
|
||||||
|
|
||||||
|
@Schema(description = "已生成 P10 的证书主题 DN", example = "C=CN,O=CISD,CN=entity")
|
||||||
private String subject;
|
private String subject;
|
||||||
|
|
||||||
|
@Schema(description = "允许访问该实体证书能力的来源 IP 列表", example = "[\"127.0.0.1\"]")
|
||||||
private List<String> allowIps;
|
private List<String> allowIps;
|
||||||
|
|
||||||
|
@Schema(description = "实体状态,CREATED/P10_GENERATED/CERT_IMPORTED", example = "CREATED")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "实体状态中文名称", example = "已创建")
|
||||||
private String statusName;
|
private String statusName;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", example = "2026-04-30T15:30:00")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Schema(description = "证书实体列表查询请求")
|
||||||
public class EntityListRequest extends CommonPageRequest {
|
public class EntityListRequest extends CommonPageRequest {
|
||||||
|
@Schema(description = "机构代码,模糊匹配", example = "00000001")
|
||||||
private String orgCode;
|
private String orgCode;
|
||||||
|
|
||||||
|
@Schema(description = "算法类型,支持 SM2/RSA", example = "SM2")
|
||||||
private String algoType;
|
private String algoType;
|
||||||
|
|
||||||
|
@Schema(description = "实体状态,CREATED/P10_GENERATED/CERT_IMPORTED", example = "CREATED")
|
||||||
private String status;
|
private String status;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "更新证书实体请求")
|
||||||
public class EntityUpdateRequest {
|
public class EntityUpdateRequest {
|
||||||
@NotNull
|
@Schema(description = "证书实体 ID", example = "1")
|
||||||
|
@NotNull(message = "实体ID不能为空")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "机构代码", example = "00000001")
|
||||||
private String orgCode;
|
private String orgCode;
|
||||||
|
|
||||||
|
@Schema(description = "机构名称", example = "测试机构")
|
||||||
private String orgName;
|
private String orgName;
|
||||||
|
|
||||||
|
@Schema(description = "允许访问该实体证书能力的来源 IP 列表", example = "[\"127.0.0.1\"]")
|
||||||
private List<String> allowIps;
|
private List<String> allowIps;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "通用 ID 请求")
|
||||||
public class IdRequest {
|
public class IdRequest {
|
||||||
@NotNull
|
@Schema(description = "记录 ID", example = "1")
|
||||||
|
@NotNull(message = "ID不能为空")
|
||||||
private Long id;
|
private Long id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -7,9 +8,14 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "证书导入响应")
|
||||||
public class ImportCertificateResponse {
|
public class ImportCertificateResponse {
|
||||||
|
@Schema(description = "导入后的证书 ID", example = "1")
|
||||||
private Long certId;
|
private Long certId;
|
||||||
|
|
||||||
|
@Schema(description = "证书主题 DN", example = "C=CN,O=CISD,CN=entity")
|
||||||
private String subjectDn;
|
private String subjectDn;
|
||||||
|
|
||||||
|
@Schema(description = "证书失效时间", example = "2027-04-30T00:00:00")
|
||||||
private LocalDateTime validTo;
|
private LocalDateTime validTo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "P10 证书请求响应")
|
||||||
public class P10Response {
|
public class P10Response {
|
||||||
|
@Schema(description = "PKCS#10 PEM 内容")
|
||||||
private String p10;
|
private String p10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.bouncycastle.asn1.x500.X500Name;
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
import org.bouncycastle.asn1.x500.X500NameBuilder;
|
import org.bouncycastle.asn1.x500.X500NameBuilder;
|
||||||
@@ -8,13 +9,25 @@ import org.springframework.util.Assert;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "证书主题信息")
|
||||||
public class Subject {
|
public class Subject {
|
||||||
|
|
||||||
|
@Schema(description = "通用名称 CN", example = "entity")
|
||||||
private String commonName;
|
private String commonName;
|
||||||
|
|
||||||
|
@Schema(description = "国家代码 C,必须为 2 位", example = "CN")
|
||||||
private String country;
|
private String country;
|
||||||
|
|
||||||
|
@Schema(description = "省份 ST", example = "浙江省")
|
||||||
private String province;
|
private String province;
|
||||||
|
|
||||||
|
@Schema(description = "城市 L", example = "杭州市")
|
||||||
private String city;
|
private String city;
|
||||||
|
|
||||||
|
@Schema(description = "组织 O", example = "CISD")
|
||||||
private String org;
|
private String org;
|
||||||
|
|
||||||
|
@Schema(description = "组织单元 OU", example = "Cert")
|
||||||
private String orgUnit;
|
private String orgUnit;
|
||||||
|
|
||||||
public String getDN() {
|
public String getDN() {
|
||||||
@@ -22,7 +35,7 @@ public class Subject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public X500Name toX500Name() {
|
public X500Name toX500Name() {
|
||||||
Assert.hasText(commonName, "commonName must not be blank");
|
Assert.hasText(commonName, "通用名称不能为空");
|
||||||
X500NameBuilder builder = new X500NameBuilder(BCStyle.INSTANCE);
|
X500NameBuilder builder = new X500NameBuilder(BCStyle.INSTANCE);
|
||||||
if (StringUtils.hasText(commonName)) {
|
if (StringUtils.hasText(commonName)) {
|
||||||
builder.addRDN(BCStyle.CN, commonName);
|
builder.addRDN(BCStyle.CN, commonName);
|
||||||
@@ -40,14 +53,14 @@ public class Subject {
|
|||||||
builder.addRDN(BCStyle.ST, province);
|
builder.addRDN(BCStyle.ST, province);
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(country)) {
|
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);
|
builder.addRDN(BCStyle.C, country);
|
||||||
}
|
}
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Subject fromDN(String dn) {
|
public static Subject fromDN(String dn) {
|
||||||
Assert.hasText(dn, "dn must not be blank");
|
Assert.hasText(dn, "DN不能为空");
|
||||||
Subject subject = new Subject();
|
Subject subject = new Subject();
|
||||||
String[] splits = dn.trim().split(",");
|
String[] splits = dn.trim().split(",");
|
||||||
for (String split : splits) {
|
for (String split : splits) {
|
||||||
@@ -71,7 +84,7 @@ public class Subject {
|
|||||||
subject.setCommonName(trim.substring(3));
|
subject.setCommonName(trim.substring(3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Assert.hasText(subject.commonName, "commonName must not be blank");
|
Assert.hasText(subject.commonName, "通用名称不能为空");
|
||||||
return subject;
|
return subject;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "删除可信CA证书请求")
|
||||||
public class TrustedCertDeleteRequest {
|
public class TrustedCertDeleteRequest {
|
||||||
@NotNull
|
@Schema(description = "可信 CA 证书 ID", example = "1")
|
||||||
|
@NotNull(message = "可信CA证书ID不能为空")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "是否强制删除;存在下级 CA 或已签发证书时需要显式传 true", example = "false")
|
||||||
private Boolean force = Boolean.FALSE;
|
private Boolean force = Boolean.FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,46 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "可信CA证书详情响应")
|
||||||
public class TrustedCertDetailResponse {
|
public class TrustedCertDetailResponse {
|
||||||
|
@Schema(description = "可信 CA 证书 ID", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "证书别名", example = "middle-ca")
|
||||||
private String alias;
|
private String alias;
|
||||||
|
|
||||||
|
@Schema(description = "证书主题 DN", example = "C=CN,O=GMSSL,CN=MiddleCA for Test")
|
||||||
private String subjectDn;
|
private String subjectDn;
|
||||||
|
|
||||||
|
@Schema(description = "签发者 DN", example = "C=CN,O=GMSSL,CN=RootCA for Test")
|
||||||
private String issuerDn;
|
private String issuerDn;
|
||||||
|
|
||||||
|
@Schema(description = "证书序列号,十六进制大写", example = "1001")
|
||||||
private String serialNumber;
|
private String serialNumber;
|
||||||
|
|
||||||
|
@Schema(description = "算法类型", example = "SM2")
|
||||||
private String algoType;
|
private String algoType;
|
||||||
|
|
||||||
|
@Schema(description = "证书 SHA-256 指纹,十六进制大写")
|
||||||
private String fingerprint;
|
private String fingerprint;
|
||||||
|
|
||||||
|
@Schema(description = "证书 PEM 内容")
|
||||||
private String certData;
|
private String certData;
|
||||||
|
|
||||||
|
@Schema(description = "CA 层级,0 表示根 CA,1 表示中间 CA", example = "1")
|
||||||
private Integer caLevel;
|
private Integer caLevel;
|
||||||
|
|
||||||
|
@Schema(description = "证书生效时间", example = "2026-04-30T00:00:00")
|
||||||
private LocalDateTime validFrom;
|
private LocalDateTime validFrom;
|
||||||
|
|
||||||
|
@Schema(description = "证书失效时间", example = "2036-04-30T00:00:00")
|
||||||
private LocalDateTime validTo;
|
private LocalDateTime validTo;
|
||||||
|
|
||||||
|
@Schema(description = "导入时间", example = "2026-04-30T15:30:00")
|
||||||
private LocalDateTime importTime;
|
private LocalDateTime importTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,43 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@Schema(description = "可信CA证书列表项")
|
||||||
public class TrustedCertItemResponse {
|
public class TrustedCertItemResponse {
|
||||||
|
@Schema(description = "可信 CA 证书 ID", example = "1")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "证书别名", example = "middle-ca")
|
||||||
private String alias;
|
private String alias;
|
||||||
|
|
||||||
|
@Schema(description = "证书主题 DN", example = "C=CN,O=GMSSL,CN=MiddleCA for Test")
|
||||||
private String subjectDn;
|
private String subjectDn;
|
||||||
|
|
||||||
|
@Schema(description = "签发者 DN", example = "C=CN,O=GMSSL,CN=RootCA for Test")
|
||||||
private String issuerDn;
|
private String issuerDn;
|
||||||
|
|
||||||
|
@Schema(description = "证书序列号,十六进制大写", example = "1001")
|
||||||
private String serialNumber;
|
private String serialNumber;
|
||||||
|
|
||||||
|
@Schema(description = "算法类型", example = "SM2")
|
||||||
private String algoType;
|
private String algoType;
|
||||||
|
|
||||||
|
@Schema(description = "证书 SHA-256 指纹,十六进制大写")
|
||||||
private String fingerprint;
|
private String fingerprint;
|
||||||
|
|
||||||
|
@Schema(description = "CA 层级,0 表示根 CA,1 表示中间 CA", example = "1")
|
||||||
private Integer caLevel;
|
private Integer caLevel;
|
||||||
|
|
||||||
|
@Schema(description = "证书生效时间", example = "2026-04-30T00:00:00")
|
||||||
private LocalDateTime validFrom;
|
private LocalDateTime validFrom;
|
||||||
|
|
||||||
|
@Schema(description = "证书失效时间", example = "2036-04-30T00:00:00")
|
||||||
private LocalDateTime validTo;
|
private LocalDateTime validTo;
|
||||||
|
|
||||||
|
@Schema(description = "导入时间", example = "2026-04-30T15:30:00")
|
||||||
private LocalDateTime importTime;
|
private LocalDateTime importTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
package com.cisd.tms.modules.cert.dto;
|
package com.cisd.tms.modules.cert.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Schema(description = "可信CA证书列表查询请求")
|
||||||
public class TrustedCertListRequest extends CommonPageRequest {
|
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())
|
return Arrays.stream(values())
|
||||||
.filter(value -> value.code.equalsIgnoreCase(code))
|
.filter(value -> value.code.equalsIgnoreCase(code))
|
||||||
.findFirst()
|
.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())
|
return Arrays.stream(values())
|
||||||
.filter(value -> value.code.equalsIgnoreCase(code))
|
.filter(value -> value.code.equalsIgnoreCase(code))
|
||||||
.findFirst()
|
.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);
|
void deleteById(Long id);
|
||||||
Optional<CertificateEntity> findById(Long id);
|
Optional<CertificateEntity> findById(Long id);
|
||||||
Optional<CertificateEntity> findByFingerprint(String fingerprint);
|
Optional<CertificateEntity> findByFingerprint(String fingerprint);
|
||||||
|
Optional<CertificateEntity> findByIssuerDnAndSerialNumber(String issuerDn, String serialNumber);
|
||||||
boolean existsEntityCertByEntityId(Long entityId);
|
boolean existsEntityCertByEntityId(Long entityId);
|
||||||
long countByIssuerDn(String issuerDn);
|
long countByIssuerDn(String issuerDn);
|
||||||
Page<CertificateEntity> page(CertificateListRequest request);
|
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);
|
void deleteById(Long id);
|
||||||
Optional<KeyEntity> findById(Long id);
|
Optional<KeyEntity> findById(Long id);
|
||||||
Optional<KeyEntity> findByKeyIdx(Integer keyIdx);
|
Optional<KeyEntity> findByKeyIdx(Integer keyIdx);
|
||||||
|
List<KeyEntity> findAll();
|
||||||
List<Integer> findUsedKeyIdx();
|
List<Integer> findUsedKeyIdx();
|
||||||
Page<KeyEntity> page(EntityListRequest request);
|
Page<KeyEntity> page(EntityListRequest request);
|
||||||
void updateStatus(Long id, String status);
|
void updateStatus(Long id, String status);
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ public interface TrustedCertRepository {
|
|||||||
Optional<TrustedCertEntity> findByFingerprint(String fingerprint);
|
Optional<TrustedCertEntity> findByFingerprint(String fingerprint);
|
||||||
List<TrustedCertEntity> findBySubjectDn(String subjectDn);
|
List<TrustedCertEntity> findBySubjectDn(String subjectDn);
|
||||||
long countByIssuerDn(String issuerDn);
|
long countByIssuerDn(String issuerDn);
|
||||||
|
long countByIssuerDnExcludingId(String issuerDn, Long excludedId);
|
||||||
Page<TrustedCertEntity> page(TrustedCertListRequest request);
|
Page<TrustedCertEntity> page(TrustedCertListRequest request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-6
@@ -49,6 +49,15 @@ public class CertificateRepositoryImpl implements CertificateRepository {
|
|||||||
return Optional.ofNullable(mapper.selectOne(wrapper));
|
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
|
@Override
|
||||||
public boolean existsEntityCertByEntityId(Long entityId) {
|
public boolean existsEntityCertByEntityId(Long entityId) {
|
||||||
LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<CertificateEntity>()
|
LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<CertificateEntity>()
|
||||||
@@ -66,12 +75,20 @@ public class CertificateRepositoryImpl implements CertificateRepository {
|
|||||||
@Override
|
@Override
|
||||||
public Page<CertificateEntity> page(CertificateListRequest request) {
|
public Page<CertificateEntity> page(CertificateListRequest request) {
|
||||||
LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
if (StringUtils.hasText(request.getCertType())) {
|
if (StringUtils.hasText(request.getOrgCode())) {
|
||||||
wrapper.eq(CertificateEntity::getCertType, request.getCertType());
|
wrapper.apply(
|
||||||
|
"entity_id IN (SELECT id FROM t_key_entity WHERE org_code LIKE CONCAT('%', {0}, '%'))",
|
||||||
|
request.getOrgCode()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(request.getKeyword())) {
|
if (StringUtils.hasText(request.getAlgoType())) {
|
||||||
wrapper.and(w -> w.like(CertificateEntity::getSubjectDn, request.getKeyword())
|
wrapper.eq(CertificateEntity::getAlgoType, request.getAlgoType().toUpperCase());
|
||||||
.or().like(CertificateEntity::getIssuerDn, request.getKeyword()));
|
}
|
||||||
|
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);
|
wrapper.orderByDesc(CertificateEntity::getImportTime);
|
||||||
long total = mapper.selectCount(wrapper);
|
long total = mapper.selectCount(wrapper);
|
||||||
@@ -83,4 +100,3 @@ public class CertificateRepositoryImpl implements CertificateRepository {
|
|||||||
return page;
|
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));
|
return Optional.ofNullable(mapper.selectOne(wrapper));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<KeyEntity> findAll() {
|
||||||
|
return mapper.selectList(new LambdaQueryWrapper<KeyEntity>()
|
||||||
|
.orderByDesc(KeyEntity::getCreateTime));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Integer> findUsedKeyIdx() {
|
public List<Integer> findUsedKeyIdx() {
|
||||||
LambdaQueryWrapper<KeyEntity> wrapper = new LambdaQueryWrapper<KeyEntity>()
|
LambdaQueryWrapper<KeyEntity> wrapper = new LambdaQueryWrapper<KeyEntity>()
|
||||||
|
|||||||
+12
-5
@@ -62,13 +62,21 @@ public class TrustedCertRepositoryImpl implements TrustedCertRepository {
|
|||||||
.eq(TrustedCertEntity::getIssuerDn, issuerDn));
|
.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
|
@Override
|
||||||
public Page<TrustedCertEntity> page(TrustedCertListRequest request) {
|
public Page<TrustedCertEntity> page(TrustedCertListRequest request) {
|
||||||
LambdaQueryWrapper<TrustedCertEntity> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<TrustedCertEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
if (StringUtils.hasText(request.getKeyword())) {
|
if (StringUtils.hasText(request.getAlgoType())) {
|
||||||
wrapper.and(w -> w.like(TrustedCertEntity::getAlias, request.getKeyword())
|
wrapper.eq(TrustedCertEntity::getAlgoType, request.getAlgoType().toUpperCase());
|
||||||
.or().like(TrustedCertEntity::getSubjectDn, request.getKeyword())
|
}
|
||||||
.or().like(TrustedCertEntity::getIssuerDn, request.getKeyword()));
|
if (StringUtils.hasText(request.getSubject())) {
|
||||||
|
wrapper.like(TrustedCertEntity::getSubjectDn, request.getSubject());
|
||||||
}
|
}
|
||||||
wrapper.orderByDesc(TrustedCertEntity::getImportTime);
|
wrapper.orderByDesc(TrustedCertEntity::getImportTime);
|
||||||
long total = mapper.selectCount(wrapper);
|
long total = mapper.selectCount(wrapper);
|
||||||
@@ -80,4 +88,3 @@ public class TrustedCertRepositoryImpl implements TrustedCertRepository {
|
|||||||
return page;
|
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.CertificateListRequest;
|
||||||
import com.cisd.tms.modules.cert.dto.ImportCertificateResponse;
|
import com.cisd.tms.modules.cert.dto.ImportCertificateResponse;
|
||||||
import com.cisd.tms.modules.cert.entity.CertificateEntity;
|
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.repository.CertificateRepository;
|
||||||
import com.cisd.tms.modules.cert.support.CertPemSupport;
|
import com.cisd.tms.modules.cert.support.CertPemSupport;
|
||||||
import com.cisd.tms.modules.cert.support.CertRuntimeStatusResolver;
|
import com.cisd.tms.modules.cert.support.CertRuntimeStatusResolver;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -19,7 +19,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.util.Base64;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class CertificateService {
|
public class CertificateService {
|
||||||
@@ -27,15 +27,27 @@ public class CertificateService {
|
|||||||
private final CertificateRepository certificateRepository;
|
private final CertificateRepository certificateRepository;
|
||||||
private final EntityService entityService;
|
private final EntityService entityService;
|
||||||
private final TrustedCertService trustedCertService;
|
private final TrustedCertService trustedCertService;
|
||||||
|
private final CrlRevocationService crlRevocationService;
|
||||||
|
|
||||||
public CertificateService(
|
public CertificateService(
|
||||||
CertificateRepository certificateRepository,
|
CertificateRepository certificateRepository,
|
||||||
EntityService entityService,
|
EntityService entityService,
|
||||||
TrustedCertService trustedCertService
|
TrustedCertService trustedCertService
|
||||||
|
) {
|
||||||
|
this(certificateRepository, entityService, trustedCertService, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CertificateService(
|
||||||
|
CertificateRepository certificateRepository,
|
||||||
|
EntityService entityService,
|
||||||
|
TrustedCertService trustedCertService,
|
||||||
|
CrlRevocationService crlRevocationService
|
||||||
) {
|
) {
|
||||||
this.certificateRepository = certificateRepository;
|
this.certificateRepository = certificateRepository;
|
||||||
this.entityService = entityService;
|
this.entityService = entityService;
|
||||||
this.trustedCertService = trustedCertService;
|
this.trustedCertService = trustedCertService;
|
||||||
|
this.crlRevocationService = crlRevocationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page<CertificateItemResponse> list(CertificateListRequest request) {
|
public Page<CertificateItemResponse> list(CertificateListRequest request) {
|
||||||
@@ -46,43 +58,36 @@ public class CertificateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public ImportCertificateResponse importCertificate(MultipartFile file, String certType, Long entityId) {
|
public ImportCertificateResponse importCertificate(MultipartFile file) {
|
||||||
try {
|
try {
|
||||||
|
//todo file空值判断
|
||||||
|
|
||||||
X509Certificate certificate = CertPemSupport.parseCertificate(file.getBytes());
|
X509Certificate certificate = CertPemSupport.parseCertificate(file.getBytes());
|
||||||
assertValidTime(certificate);
|
assertValidTime(certificate);
|
||||||
|
assertEndEntityCertificate(certificate);
|
||||||
|
String algoType = CertPemSupport.detectAlgoType(certificate);
|
||||||
String fingerprint = CertPemSupport.sha256Fingerprint(certificate);
|
String fingerprint = CertPemSupport.sha256Fingerprint(certificate);
|
||||||
if (certificateRepository.findByFingerprint(fingerprint).isPresent()) {
|
if (certificateRepository.findByFingerprint(fingerprint).isPresent()) {
|
||||||
throw new BizException(ErrorCode.CONFLICT.getCode(), "certificate already exists");
|
throw new BizException(ErrorCode.CONFLICT.getCode(), "证书已存在");
|
||||||
}
|
}
|
||||||
Long finalEntityId = null;
|
Optional<Long> matchedEntityId = entityService.findByCertificatePublicKey(certificate.getPublicKey().getEncoded());
|
||||||
if ("ENTITY".equalsIgnoreCase(certType)) {
|
Long finalEntityId = matchedEntityId.orElse(null);
|
||||||
if (entityId == null) {
|
String certType = matchedEntityId.isPresent() ? "ENTITY" : "USER";
|
||||||
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "entityId is required for ENTITY cert");
|
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吊销");
|
||||||
}
|
}
|
||||||
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;
|
|
||||||
}
|
|
||||||
trustedCertService.validateParentChain(trustedCertService.resolveIssuer(certificate.getIssuerX500Principal().getName()));
|
|
||||||
certificate.verify(
|
|
||||||
CertPemSupport.parseCertificate(
|
|
||||||
trustedCertService.resolveIssuer(certificate.getIssuerX500Principal().getName())
|
|
||||||
.getCertData().getBytes()
|
|
||||||
).getPublicKey(),
|
|
||||||
"BC"
|
|
||||||
);
|
|
||||||
CertificateEntity entity = new CertificateEntity()
|
CertificateEntity entity = new CertificateEntity()
|
||||||
.setEntityId(finalEntityId)
|
.setEntityId(finalEntityId)
|
||||||
.setCertType(certType)
|
.setCertType(certType)
|
||||||
.setSubjectDn(certificate.getSubjectX500Principal().getName())
|
.setSubjectDn(certificate.getSubjectX500Principal().getName())
|
||||||
.setIssuerDn(certificate.getIssuerX500Principal().getName())
|
.setIssuerDn(issuerDn)
|
||||||
.setSerialNumber(certificate.getSerialNumber().toString(16).toUpperCase())
|
.setSerialNumber(serialNumber)
|
||||||
.setAlgoType("SM2")
|
.setAlgoType(algoType)
|
||||||
.setFingerprint(fingerprint)
|
.setFingerprint(fingerprint)
|
||||||
.setCertData(CertPemSupport.toPem(certificate))
|
.setCertData(CertPemSupport.toPem(certificate))
|
||||||
.setValidFrom(LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault()))
|
.setValidFrom(LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault()))
|
||||||
@@ -99,7 +104,7 @@ public class CertificateService {
|
|||||||
} catch (BizException ex) {
|
} catch (BizException ex) {
|
||||||
throw ex;
|
throw ex;
|
||||||
} catch (Exception 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.setCertData(entity.getCertData());
|
||||||
response.setValidFrom(entity.getValidFrom());
|
response.setValidFrom(entity.getValidFrom());
|
||||||
response.setValidTo(entity.getValidTo());
|
response.setValidTo(entity.getValidTo());
|
||||||
response.setRuntimeStatus(CertRuntimeStatusResolver.resolve(entity.getValidFrom(), entity.getValidTo()));
|
response.setRuntimeStatus(resolveRuntimeStatus(entity));
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +136,11 @@ public class CertificateService {
|
|||||||
CertificateItemResponse response = new CertificateItemResponse();
|
CertificateItemResponse response = new CertificateItemResponse();
|
||||||
response.setId(entity.getId());
|
response.setId(entity.getId());
|
||||||
response.setEntityId(entity.getEntityId());
|
response.setEntityId(entity.getEntityId());
|
||||||
|
if (entity.getEntityId() != null) {
|
||||||
|
entityService.findOrgCodeById(entity.getEntityId()).ifPresent(response::setOrgCode);
|
||||||
|
}
|
||||||
response.setCertType(entity.getCertType());
|
response.setCertType(entity.getCertType());
|
||||||
|
response.setCertTypeName(certTypeName(entity.getCertType()));
|
||||||
response.setSubjectDn(entity.getSubjectDn());
|
response.setSubjectDn(entity.getSubjectDn());
|
||||||
response.setIssuerDn(entity.getIssuerDn());
|
response.setIssuerDn(entity.getIssuerDn());
|
||||||
response.setSerialNumber(entity.getSerialNumber());
|
response.setSerialNumber(entity.getSerialNumber());
|
||||||
@@ -139,13 +148,41 @@ public class CertificateService {
|
|||||||
response.setFingerprint(entity.getFingerprint());
|
response.setFingerprint(entity.getFingerprint());
|
||||||
response.setValidFrom(entity.getValidFrom());
|
response.setValidFrom(entity.getValidFrom());
|
||||||
response.setValidTo(entity.getValidTo());
|
response.setValidTo(entity.getValidTo());
|
||||||
response.setRuntimeStatus(CertRuntimeStatusResolver.resolve(entity.getValidFrom(), entity.getValidTo()));
|
response.setImportTime(entity.getImportTime());
|
||||||
|
response.setRuntimeStatus(resolveRuntimeStatus(entity));
|
||||||
return response;
|
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) {
|
private CertificateEntity getById(Long id) {
|
||||||
return certificateRepository.findById(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) {
|
private void assertValidTime(X509Certificate certificate) {
|
||||||
@@ -153,7 +190,17 @@ public class CertificateService {
|
|||||||
LocalDateTime notBefore = LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault());
|
LocalDateTime notBefore = LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault());
|
||||||
LocalDateTime notAfter = LocalDateTime.ofInstant(certificate.getNotAfter().toInstant(), ZoneId.systemDefault());
|
LocalDateTime notAfter = LocalDateTime.ofInstant(certificate.getNotAfter().toInstant(), ZoneId.systemDefault());
|
||||||
if (now.isBefore(notBefore) || now.isAfter(notAfter)) {
|
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.security.PublicKey;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class EntityService {
|
public class EntityService {
|
||||||
@@ -75,7 +77,7 @@ public class EntityService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public EntityCreateResponse create(EntityCreateRequest request) {
|
public synchronized EntityCreateResponse create(EntityCreateRequest request) {
|
||||||
String algoType = normalizeAlgoType(request.getAlgoType());
|
String algoType = normalizeAlgoType(request.getAlgoType());
|
||||||
int keyIdx = resolveNextKeyIdx();
|
int keyIdx = resolveNextKeyIdx();
|
||||||
int keyLen = SM2.equals(algoType) ? 256 : 2048;
|
int keyLen = SM2.equals(algoType) ? 256 : 2048;
|
||||||
@@ -137,7 +139,7 @@ public class EntityService {
|
|||||||
public void delete(Long id) {
|
public void delete(Long id) {
|
||||||
KeyEntity entity = getById(id);
|
KeyEntity entity = getById(id);
|
||||||
if (certificateRepository.existsEntityCertByEntityId(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());
|
pcieCryptoService.deleteUserKey(entity.getKeyIdx());
|
||||||
keyEntityRepository.deleteById(id);
|
keyEntityRepository.deleteById(id);
|
||||||
@@ -147,7 +149,7 @@ public class EntityService {
|
|||||||
public String generateP10(EntityGenP10Request request) {
|
public String generateP10(EntityGenP10Request request) {
|
||||||
KeyEntity entity = getById(request.getId());
|
KeyEntity entity = getById(request.getId());
|
||||||
if (!SM2.equalsIgnoreCase(entity.getAlgoType())) {
|
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());
|
EntityCertMode certMode = EntityCertMode.requireValid(request.getCertMode());
|
||||||
Subject subject = request.getSubject();
|
Subject subject = request.getSubject();
|
||||||
@@ -173,7 +175,19 @@ public class EntityService {
|
|||||||
|
|
||||||
public KeyEntity getById(Long id) {
|
public KeyEntity getById(Long id) {
|
||||||
return keyEntityRepository.findById(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
|
@Transactional
|
||||||
@@ -201,15 +215,28 @@ public class EntityService {
|
|||||||
|
|
||||||
private String normalizeAlgoType(String algoType) {
|
private String normalizeAlgoType(String algoType) {
|
||||||
if (!StringUtils.hasText(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);
|
String normalized = algoType.toUpperCase(Locale.ROOT);
|
||||||
if (!SM2.equals(normalized) && !RSA.equals(normalized)) {
|
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;
|
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() {
|
private int resolveNextKeyIdx() {
|
||||||
List<Integer> used = keyEntityRepository.findUsedKeyIdx();
|
List<Integer> used = keyEntityRepository.findUsedKeyIdx();
|
||||||
for (int idx = 2; idx <= 8; idx++) {
|
for (int idx = 2; idx <= 8; idx++) {
|
||||||
@@ -217,14 +244,14 @@ public class EntityService {
|
|||||||
return idx;
|
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) {
|
private String writeAllowIps(List<String> allowIps) {
|
||||||
try {
|
try {
|
||||||
return allowIps == null ? "[]" : objectMapper.writeValueAsString(allowIps);
|
return allowIps == null ? "[]" : objectMapper.writeValueAsString(allowIps);
|
||||||
} catch (JsonProcessingException e) {
|
} 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());
|
.encodeToString(csr.getEncoded());
|
||||||
return "-----BEGIN CERTIFICATE REQUEST-----\n" + base64 + "\n-----END CERTIFICATE REQUEST-----";
|
return "-----BEGIN CERTIFICATE REQUEST-----\n" + base64 + "\n-----END CERTIFICATE REQUEST-----";
|
||||||
} catch (Exception e) {
|
} 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 org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
|
import java.security.cert.X509CRL;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
@@ -27,6 +28,9 @@ import java.util.List;
|
|||||||
@Service
|
@Service
|
||||||
public class TrustedCertService {
|
public class TrustedCertService {
|
||||||
|
|
||||||
|
private static final int KEY_USAGE_KEY_CERT_SIGN = 5;
|
||||||
|
private static final int KEY_USAGE_CRL_SIGN = 6;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
if (Security.getProvider("BC") == null) {
|
if (Security.getProvider("BC") == null) {
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
@@ -56,17 +60,16 @@ public class TrustedCertService {
|
|||||||
try {
|
try {
|
||||||
X509Certificate certificate = CertPemSupport.parseCertificate(file.getBytes());
|
X509Certificate certificate = CertPemSupport.parseCertificate(file.getBytes());
|
||||||
assertValidTime(certificate);
|
assertValidTime(certificate);
|
||||||
|
assertTrustedCaCertificate(certificate);
|
||||||
String fingerprint = CertPemSupport.sha256Fingerprint(certificate);
|
String fingerprint = CertPemSupport.sha256Fingerprint(certificate);
|
||||||
if (trustedCertRepository.findByFingerprint(fingerprint).isPresent()) {
|
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;
|
int caLevel = isSelfSigned(certificate) ? 0 : 1;
|
||||||
if (caLevel == 0) {
|
if (caLevel == 0) {
|
||||||
certificate.verify(certificate.getPublicKey(), "BC");
|
certificate.verify(certificate.getPublicKey(), "BC");
|
||||||
} else {
|
} else {
|
||||||
TrustedCertEntity issuer = resolveIssuer(certificate.getIssuerX500Principal().getName());
|
TrustedCertEntity issuer = resolveIssuer(certificate);
|
||||||
X509Certificate issuerCert = CertPemSupport.parseCertificate(issuer.getCertData().getBytes());
|
|
||||||
certificate.verify(issuerCert.getPublicKey(), "BC");
|
|
||||||
validateParentChain(issuer);
|
validateParentChain(issuer);
|
||||||
}
|
}
|
||||||
TrustedCertEntity entity = new TrustedCertEntity()
|
TrustedCertEntity entity = new TrustedCertEntity()
|
||||||
@@ -74,7 +77,7 @@ public class TrustedCertService {
|
|||||||
.setSubjectDn(certificate.getSubjectX500Principal().getName())
|
.setSubjectDn(certificate.getSubjectX500Principal().getName())
|
||||||
.setIssuerDn(certificate.getIssuerX500Principal().getName())
|
.setIssuerDn(certificate.getIssuerX500Principal().getName())
|
||||||
.setSerialNumber(certificate.getSerialNumber().toString(16).toUpperCase())
|
.setSerialNumber(certificate.getSerialNumber().toString(16).toUpperCase())
|
||||||
.setAlgoType("SM2")
|
.setAlgoType(CertPemSupport.detectAlgoType(certificate))
|
||||||
.setFingerprint(fingerprint)
|
.setFingerprint(fingerprint)
|
||||||
.setCertData(CertPemSupport.toPem(certificate))
|
.setCertData(CertPemSupport.toPem(certificate))
|
||||||
.setCaLevel(caLevel)
|
.setCaLevel(caLevel)
|
||||||
@@ -89,7 +92,7 @@ public class TrustedCertService {
|
|||||||
} catch (BizException ex) {
|
} catch (BizException ex) {
|
||||||
throw ex;
|
throw ex;
|
||||||
} catch (Exception 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
|
@Transactional
|
||||||
public DeleteDependencyResponse delete(TrustedCertDeleteRequest request) {
|
public DeleteDependencyResponse delete(TrustedCertDeleteRequest request) {
|
||||||
TrustedCertEntity entity = getById(request.getId());
|
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());
|
long issuedCertCount = certificateRepository.countByIssuerDn(entity.getSubjectDn());
|
||||||
if ((childCaCount > 0 || issuedCertCount > 0) && !Boolean.TRUE.equals(request.getForce())) {
|
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());
|
trustedCertRepository.deleteById(request.getId());
|
||||||
return new DeleteDependencyResponse(childCaCount, issuedCertCount);
|
return new DeleteDependencyResponse(childCaCount, issuedCertCount);
|
||||||
@@ -126,25 +129,64 @@ public class TrustedCertService {
|
|||||||
public TrustedCertEntity resolveIssuer(String subjectDn) {
|
public TrustedCertEntity resolveIssuer(String subjectDn) {
|
||||||
List<TrustedCertEntity> matched = trustedCertRepository.findBySubjectDn(subjectDn);
|
List<TrustedCertEntity> matched = trustedCertRepository.findBySubjectDn(subjectDn);
|
||||||
if (matched.isEmpty()) {
|
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);
|
return matched.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateParentChain(TrustedCertEntity entity) {
|
public TrustedCertEntity resolveIssuer(X509Certificate certificate) {
|
||||||
if (entity.getSubjectDn().equals(entity.getIssuerDn())) {
|
List<TrustedCertEntity> matched = trustedCertRepository.findBySubjectDn(
|
||||||
return;
|
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 {
|
try {
|
||||||
X509Certificate certificate = CertPemSupport.parseCertificate(entity.getCertData().getBytes());
|
X509Certificate certificate = CertPemSupport.parseCertificate(entity.getCertData().getBytes());
|
||||||
TrustedCertEntity issuer = resolveIssuer(entity.getIssuerDn());
|
assertValidTime(certificate);
|
||||||
X509Certificate issuerCert = CertPemSupport.parseCertificate(issuer.getCertData().getBytes());
|
assertTrustedCaCertificate(certificate);
|
||||||
certificate.verify(issuerCert.getPublicKey(), "BC");
|
if (entity.getSubjectDn().equals(entity.getIssuerDn())) {
|
||||||
|
certificate.verify(certificate.getPublicKey(), "BC");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TrustedCertEntity issuer = resolveIssuer(certificate);
|
||||||
validateParentChain(issuer);
|
validateParentChain(issuer);
|
||||||
} catch (BizException ex) {
|
} catch (BizException ex) {
|
||||||
throw ex;
|
throw ex;
|
||||||
} catch (Exception 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) {
|
private TrustedCertEntity getById(Long id) {
|
||||||
return trustedCertRepository.findById(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) {
|
private void assertValidTime(X509Certificate certificate) {
|
||||||
@@ -174,7 +216,27 @@ public class TrustedCertService {
|
|||||||
LocalDateTime notBefore = LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault());
|
LocalDateTime notBefore = LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault());
|
||||||
LocalDateTime notAfter = LocalDateTime.ofInstant(certificate.getNotAfter().toInstant(), ZoneId.systemDefault());
|
LocalDateTime notAfter = LocalDateTime.ofInstant(certificate.getNotAfter().toInstant(), ZoneId.systemDefault());
|
||||||
if (now.isBefore(notBefore) || now.isAfter(notAfter)) {
|
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.io.ByteArrayInputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
import java.security.PublicKey;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509CRL;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public final class CertPemSupport {
|
public final class CertPemSupport {
|
||||||
|
|
||||||
@@ -25,11 +28,21 @@ public final class CertPemSupport {
|
|||||||
|
|
||||||
public static X509Certificate parseCertificate(byte[] bytes) {
|
public static X509Certificate parseCertificate(byte[] bytes) {
|
||||||
try {
|
try {
|
||||||
byte[] normalized = toDer(bytes);
|
byte[] normalized = toDer(bytes, "CERTIFICATE");
|
||||||
CertificateFactory factory = CertificateFactory.getInstance("X.509", PROVIDER);
|
CertificateFactory factory = CertificateFactory.getInstance("X.509", PROVIDER);
|
||||||
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(normalized));
|
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(normalized));
|
||||||
} catch (Exception e) {
|
} 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());
|
.encodeToString(certificate.getEncoded());
|
||||||
return "-----BEGIN CERTIFICATE-----\n" + base64 + "\n-----END CERTIFICATE-----";
|
return "-----BEGIN CERTIFICATE-----\n" + base64 + "\n-----END CERTIFICATE-----";
|
||||||
} catch (Exception e) {
|
} 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,21 +71,49 @@ public final class CertPemSupport {
|
|||||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
return hex(digest.digest(certificate.getEncoded()));
|
return hex(digest.digest(certificate.getEncoded()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalArgumentException("failed to calculate fingerprint", e);
|
throw new IllegalArgumentException("证书指纹计算失败", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] toDer(byte[] bytes) {
|
public static String sha256Fingerprint(X509CRL crl) {
|
||||||
String text = new String(bytes, StandardCharsets.UTF_8);
|
try {
|
||||||
if (!text.contains("BEGIN CERTIFICATE")) {
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
return bytes;
|
return hex(digest.digest(crl.getEncoded()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("CRL指纹计算失败", e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
String normalized = text
|
||||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
.replace("-----BEGIN " + label + "-----", "")
|
||||||
.replace("-----END CERTIFICATE-----", "")
|
.replace("-----END " + label + "-----", "")
|
||||||
.replaceAll("\\s+", "");
|
.replaceAll("\\s+", "");
|
||||||
return Base64.getDecoder().decode(normalized);
|
return Base64.getDecoder().decode(normalized);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
private static String hex(byte[] bytes) {
|
private static String hex(byte[] bytes) {
|
||||||
StringBuilder builder = new StringBuilder(bytes.length * 2);
|
StringBuilder builder = new StringBuilder(bytes.length * 2);
|
||||||
@@ -72,4 +123,3 @@ public final class CertPemSupport {
|
|||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public final class Sm2KeySupport {
|
|||||||
);
|
);
|
||||||
return KeyFactory.getInstance("EC", PROVIDER).generatePublic(publicKeySpec);
|
return KeyFactory.getInstance("EC", PROVIDER).generatePublic(publicKeySpec);
|
||||||
} catch (Exception e) {
|
} 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 {
|
try {
|
||||||
return new DERSequence(vector).getEncoded();
|
return new DERSequence(vector).getEncoded();
|
||||||
} catch (Exception e) {
|
} 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;
|
return trimmed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ tms:
|
|||||||
# 是否启用 /api/** 的内部登录鉴权;开发联调时可临时关闭。
|
# 是否启用 /api/** 的内部登录鉴权;开发联调时可临时关闭。
|
||||||
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:false}
|
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}
|
debug-auth-level: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_AUTH_LEVEL:FULL}
|
||||||
# 关闭内部鉴权后,注入请求上下文的调试 session token。
|
# 关闭内部鉴权后,注入请求上下文的调试 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_issuer_dn (issuer_dn(512)),
|
||||||
KEY idx_t_trusted_cert_valid_to (valid_to)
|
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_issuer_dn (issuer_dn(512)),
|
||||||
KEY idx_t_trusted_cert_valid_to (valid_to)
|
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)
|
||||||
|
);
|
||||||
|
|||||||
+19
@@ -64,6 +64,21 @@ class InternalAuthorizationInterceptorTest {
|
|||||||
Mockito.verifyNoInteractions(operationAuditService);
|
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 {
|
private static HandlerMethod handler(String methodName) throws NoSuchMethodException {
|
||||||
DemoController controller = new DemoController();
|
DemoController controller = new DemoController();
|
||||||
return new HandlerMethod(controller, DemoController.class.getDeclaredMethod(methodName));
|
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)
|
@RequireInternalAuth(role = com.cisd.tms.modules.auth.enums.RoleCode.KEY_ADMIN, authLevel = com.cisd.tms.modules.auth.enums.AuthLevel.FULL)
|
||||||
public void keyAdminFullEndpoint() {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -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 org.mockito.Mockito;
|
||||||
|
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.security.interfaces.ECPublicKey;
|
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.assertArrayEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
|
||||||
class EntityServiceTest {
|
class EntityServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSerializeEntityCreationInsideSingleTmsProcess() throws Exception {
|
||||||
|
assertTrue(Modifier.isSynchronized(EntityService.class
|
||||||
|
.getMethod("create", EntityCreateRequest.class)
|
||||||
|
.getModifiers()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldListEntityWithUiFieldsAndStatusName() {
|
void shouldListEntityWithUiFieldsAndStatusName() {
|
||||||
KeyEntityRepository keyEntityRepository = Mockito.mock(KeyEntityRepository.class);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user