证书相关功能

This commit is contained in:
liulu 2026-04-24 11:18:21 +08:00
parent 0e94269539
commit f47775692f
41 changed files with 1773 additions and 0 deletions

View File

@ -0,0 +1,57 @@
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.cert.dto.CertificateDetailResponse;
import com.cisd.tms.modules.cert.dto.CertificateItemResponse;
import com.cisd.tms.modules.cert.dto.CertificateListRequest;
import com.cisd.tms.modules.cert.dto.IdRequest;
import com.cisd.tms.modules.cert.dto.ImportCertificateResponse;
import com.cisd.tms.modules.cert.service.CertificateService;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/v1/certificates")
@Tag(name = "Certificates")
public class CertificateController {
private final CertificateService certificateService;
public CertificateController(CertificateService certificateService) {
this.certificateService = certificateService;
}
@PostMapping("/list")
public ApiResponse<Page<CertificateItemResponse>> list(@Valid @RequestBody CertificateListRequest request) {
return ApiResponse.success(certificateService.list(request));
}
@PostMapping("/import")
public ApiResponse<ImportCertificateResponse> importCertificate(
@RequestPart("file") MultipartFile file,
@RequestParam("certType") String certType,
@RequestParam(value = "entityId", required = false) Long entityId
) {
return ApiResponse.success(certificateService.importCertificate(file, certType, entityId));
}
@PostMapping("/detail")
public ApiResponse<CertificateDetailResponse> detail(@Valid @RequestBody IdRequest request) {
return ApiResponse.success(certificateService.detail(request.getId()));
}
@PostMapping("/delete")
public ApiResponse<Void> delete(@Valid @RequestBody IdRequest request) {
certificateService.delete(request.getId());
return ApiResponse.success();
}
}

View File

@ -0,0 +1,64 @@
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.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.P10Response;
import com.cisd.tms.modules.cert.service.EntityService;
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.RestController;
@RestController
@RequestMapping("/api/v1/entities")
@Tag(name = "Cert Entities")
public class EntityController {
private final EntityService entityService;
public EntityController(EntityService entityService) {
this.entityService = entityService;
}
@PostMapping("/list")
public ApiResponse<Page<EntityItemResponse>> list(@Valid @RequestBody EntityListRequest request) {
return ApiResponse.success(entityService.list(request));
}
@PostMapping("/create")
public ApiResponse<EntityCreateResponse> create(@Valid @RequestBody EntityCreateRequest request) {
return ApiResponse.success(entityService.create(request));
}
@PostMapping("/update")
public ApiResponse<Void> update(@Valid @RequestBody EntityUpdateRequest request) {
entityService.update(request);
return ApiResponse.success();
}
@PostMapping("/delete")
public ApiResponse<Void> delete(@Valid @RequestBody IdRequest request) {
entityService.delete(request.getId());
return ApiResponse.success();
}
@PostMapping("/gen-p10")
public ApiResponse<P10Response> genP10(@Valid @RequestBody EntityGenP10Request request) {
return ApiResponse.success(new P10Response(entityService.generateP10(request)));
}
@PostMapping("/p10")
public ApiResponse<P10Response> p10(@Valid @RequestBody IdRequest request) {
return ApiResponse.success(new P10Response(entityService.getP10(request.getId())));
}
}

View File

@ -0,0 +1,57 @@
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.cert.dto.DeleteDependencyResponse;
import com.cisd.tms.modules.cert.dto.IdRequest;
import com.cisd.tms.modules.cert.dto.ImportCertificateResponse;
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 com.cisd.tms.modules.cert.service.TrustedCertService;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/v1/trusted-certificates")
@Tag(name = "Trusted Certificates")
public class TrustedCertController {
private final TrustedCertService trustedCertService;
public TrustedCertController(TrustedCertService trustedCertService) {
this.trustedCertService = trustedCertService;
}
@PostMapping("/list")
public ApiResponse<Page<TrustedCertItemResponse>> list(@Valid @RequestBody TrustedCertListRequest request) {
return ApiResponse.success(trustedCertService.list(request));
}
@PostMapping("/import")
public ApiResponse<ImportCertificateResponse> importTrusted(
@RequestPart("file") MultipartFile file,
@RequestParam("alias") String alias
) {
return ApiResponse.success(trustedCertService.importTrusted(file, alias));
}
@PostMapping("/detail")
public ApiResponse<TrustedCertDetailResponse> detail(@Valid @RequestBody IdRequest request) {
return ApiResponse.success(trustedCertService.detail(request.getId()));
}
@PostMapping("/delete")
public ApiResponse<DeleteDependencyResponse> delete(@Valid @RequestBody TrustedCertDeleteRequest request) {
return ApiResponse.success(trustedCertService.delete(request));
}
}

View File

@ -0,0 +1,22 @@
package com.cisd.tms.modules.cert.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class CertificateDetailResponse {
private Long id;
private Long entityId;
private String certType;
private String subjectDn;
private String issuerDn;
private String serialNumber;
private String algoType;
private String fingerprint;
private String certData;
private LocalDateTime validFrom;
private LocalDateTime validTo;
private String runtimeStatus;
}

View File

@ -0,0 +1,21 @@
package com.cisd.tms.modules.cert.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class CertificateItemResponse {
private Long id;
private Long entityId;
private String certType;
private String subjectDn;
private String issuerDn;
private String serialNumber;
private String algoType;
private String fingerprint;
private LocalDateTime validFrom;
private LocalDateTime validTo;
private String runtimeStatus;
}

View File

@ -0,0 +1,12 @@
package com.cisd.tms.modules.cert.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class CertificateListRequest extends CommonPageRequest {
private String certType;
private String keyword;
}

View File

@ -0,0 +1,13 @@
package com.cisd.tms.modules.cert.dto;
import jakarta.validation.constraints.Min;
import lombok.Data;
@Data
public class CommonPageRequest {
@Min(1)
private int pageNum = 1;
@Min(1)
private int pageSize = 10;
}

View File

@ -0,0 +1,12 @@
package com.cisd.tms.modules.cert.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class DeleteDependencyResponse {
private long childCaCount;
private long issuedCertCount;
}

View File

@ -0,0 +1,16 @@
package com.cisd.tms.modules.cert.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.List;
@Data
public class EntityCreateRequest {
@NotBlank
private String entityName;
@NotBlank
private String algoType;
private List<String> allowIps;
}

View File

@ -0,0 +1,11 @@
package com.cisd.tms.modules.cert.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class EntityCreateResponse {
private Long entityId;
}

View File

@ -0,0 +1,15 @@
package com.cisd.tms.modules.cert.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class EntityGenP10Request {
@NotNull
private Long id;
@Valid
@NotNull
private Subject subject;
}

View File

@ -0,0 +1,19 @@
package com.cisd.tms.modules.cert.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class EntityItemResponse {
private Long id;
private String entityName;
private String algoType;
private Integer keyLen;
private Integer keyIdx;
private String subject;
private List<String> allowIps;
private LocalDateTime createTime;
}

View File

@ -0,0 +1,12 @@
package com.cisd.tms.modules.cert.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class EntityListRequest extends CommonPageRequest {
private String keyword;
private String algoType;
}

View File

@ -0,0 +1,15 @@
package com.cisd.tms.modules.cert.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Data
public class EntityUpdateRequest {
@NotNull
private Long id;
private String entityName;
private List<String> allowIps;
}

View File

@ -0,0 +1,11 @@
package com.cisd.tms.modules.cert.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class IdRequest {
@NotNull
private Long id;
}

View File

@ -0,0 +1,15 @@
package com.cisd.tms.modules.cert.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
public class ImportCertificateResponse {
private Long certId;
private String subjectDn;
private LocalDateTime validTo;
}

View File

@ -0,0 +1,11 @@
package com.cisd.tms.modules.cert.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class P10Response {
private String p10;
}

View File

@ -0,0 +1,72 @@
package com.cisd.tms.modules.cert.dto;
import lombok.Data;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@Data
public class Subject {
private static final String COMMA = ",";
private String commonName;
private String country;
private String province;
private String city;
private String org;
private String orgUnit;
public String getDN() {
Assert.hasText(commonName, "commonName must not be blank");
StringBuilder builder = new StringBuilder();
if (StringUtils.hasText(commonName)) {
builder.append("CN=").append(commonName).append(COMMA);
}
if (StringUtils.hasText(orgUnit)) {
builder.append("OU=").append(orgUnit).append(COMMA);
}
if (StringUtils.hasText(org)) {
builder.append("O=").append(org).append(COMMA);
}
if (StringUtils.hasText(city)) {
builder.append("L=").append(city).append(COMMA);
}
if (StringUtils.hasText(province)) {
builder.append("ST=").append(province).append(COMMA);
}
if (StringUtils.hasText(country)) {
builder.append("C=").append(country).append(COMMA);
}
builder.deleteCharAt(builder.length() - 1);
return builder.toString();
}
public static Subject fromDN(String dn) {
Assert.hasText(dn, "dn must not be blank");
Subject subject = new Subject();
String[] splits = dn.trim().split(",");
for (String split : splits) {
String trim = split.trim();
if (trim.startsWith("C=")) {
subject.setCountry(trim.substring(2));
}
if (trim.startsWith("ST=")) {
subject.setProvince(trim.substring(3));
}
if (trim.startsWith("L=")) {
subject.setCity(trim.substring(2));
}
if (trim.startsWith("O=")) {
subject.setOrg(trim.substring(2));
}
if (trim.startsWith("OU=")) {
subject.setOrgUnit(trim.substring(3));
}
if (trim.startsWith("CN=")) {
subject.setCommonName(trim.substring(3));
}
}
Assert.hasText(subject.commonName, "commonName must not be blank");
return subject;
}
}

View File

@ -0,0 +1,12 @@
package com.cisd.tms.modules.cert.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class TrustedCertDeleteRequest {
@NotNull
private Long id;
private Boolean force = Boolean.FALSE;
}

View File

@ -0,0 +1,22 @@
package com.cisd.tms.modules.cert.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class TrustedCertDetailResponse {
private Long id;
private String alias;
private String subjectDn;
private String issuerDn;
private String serialNumber;
private String algoType;
private String fingerprint;
private String certData;
private Integer caLevel;
private LocalDateTime validFrom;
private LocalDateTime validTo;
private LocalDateTime importTime;
}

View File

@ -0,0 +1,21 @@
package com.cisd.tms.modules.cert.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class TrustedCertItemResponse {
private Long id;
private String alias;
private String subjectDn;
private String issuerDn;
private String serialNumber;
private String algoType;
private String fingerprint;
private Integer caLevel;
private LocalDateTime validFrom;
private LocalDateTime validTo;
private LocalDateTime importTime;
}

View File

@ -0,0 +1,11 @@
package com.cisd.tms.modules.cert.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class TrustedCertListRequest extends CommonPageRequest {
private String keyword;
}

View File

@ -0,0 +1,31 @@
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")
public class CertificateEntity extends BaseEntity {
private Long entityId;
private String certType;
private String subjectDn;
private String issuerDn;
private String serialNumber;
private String algoType;
private String fingerprint;
private String certData;
private LocalDateTime validFrom;
private LocalDateTime validTo;
private LocalDateTime importTime;
private String signData;
private String createBy;
}

View File

@ -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;
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@TableName("t_key_entity")
public class KeyEntity extends BaseEntity {
private String entityName;
private String algoType;
private Integer keyLen;
private Integer keyIdx;
private String backupData;
private String publicKey;
private String p10Data;
private String subjectDn;
private String allowIps;
private String signData;
private String createBy;
}

View File

@ -0,0 +1,31 @@
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_trusted_cert")
public class TrustedCertEntity extends BaseEntity {
private String alias;
private String subjectDn;
private String issuerDn;
private String serialNumber;
private String algoType;
private String fingerprint;
private String certData;
private Integer caLevel;
private LocalDateTime validFrom;
private LocalDateTime validTo;
private LocalDateTime importTime;
private String signData;
private String createBy;
}

View File

@ -0,0 +1,10 @@
package com.cisd.tms.modules.cert.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.cert.entity.CertificateEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CertificateEntityMapper extends BaseMapperX<CertificateEntity> {
}

View File

@ -0,0 +1,10 @@
package com.cisd.tms.modules.cert.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.cert.entity.KeyEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface KeyEntityMapper extends BaseMapperX<KeyEntity> {
}

View File

@ -0,0 +1,10 @@
package com.cisd.tms.modules.cert.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.cert.entity.TrustedCertEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TrustedCertEntityMapper extends BaseMapperX<TrustedCertEntity> {
}

View File

@ -0,0 +1,17 @@
package com.cisd.tms.modules.cert.repository;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.modules.cert.dto.CertificateListRequest;
import com.cisd.tms.modules.cert.entity.CertificateEntity;
import java.util.Optional;
public interface CertificateRepository {
CertificateEntity save(CertificateEntity entity);
void deleteById(Long id);
Optional<CertificateEntity> findById(Long id);
Optional<CertificateEntity> findByFingerprint(String fingerprint);
boolean existsEntityCertByEntityId(Long entityId);
long countByIssuerDn(String issuerDn);
Page<CertificateEntity> page(CertificateListRequest request);
}

View File

@ -0,0 +1,17 @@
package com.cisd.tms.modules.cert.repository;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.modules.cert.dto.EntityListRequest;
import com.cisd.tms.modules.cert.entity.KeyEntity;
import java.util.List;
import java.util.Optional;
public interface KeyEntityRepository {
KeyEntity save(KeyEntity entity);
void deleteById(Long id);
Optional<KeyEntity> findById(Long id);
Optional<KeyEntity> findByKeyIdx(Integer keyIdx);
List<Integer> findUsedKeyIdx();
Page<KeyEntity> page(EntityListRequest request);
}

View File

@ -0,0 +1,18 @@
package com.cisd.tms.modules.cert.repository;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.modules.cert.dto.TrustedCertListRequest;
import com.cisd.tms.modules.cert.entity.TrustedCertEntity;
import java.util.List;
import java.util.Optional;
public interface TrustedCertRepository {
TrustedCertEntity save(TrustedCertEntity entity);
void deleteById(Long id);
Optional<TrustedCertEntity> findById(Long id);
Optional<TrustedCertEntity> findByFingerprint(String fingerprint);
List<TrustedCertEntity> findBySubjectDn(String subjectDn);
long countByIssuerDn(String issuerDn);
Page<TrustedCertEntity> page(TrustedCertListRequest request);
}

View File

@ -0,0 +1,86 @@
package com.cisd.tms.modules.cert.repository.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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 com.cisd.tms.modules.cert.repository.CertificateRepository;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Optional;
@Repository
public class CertificateRepositoryImpl implements CertificateRepository {
private final CertificateEntityMapper mapper;
public CertificateRepositoryImpl(CertificateEntityMapper mapper) {
this.mapper = mapper;
}
@Override
public CertificateEntity save(CertificateEntity entity) {
if (entity.getId() == null) {
mapper.insert(entity);
} else {
mapper.updateById(entity);
}
return entity;
}
@Override
public void deleteById(Long id) {
mapper.deleteById(id);
}
@Override
public Optional<CertificateEntity> findById(Long id) {
return Optional.ofNullable(mapper.selectById(id));
}
@Override
public Optional<CertificateEntity> findByFingerprint(String fingerprint) {
LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<CertificateEntity>()
.eq(CertificateEntity::getFingerprint, fingerprint)
.last("LIMIT 1");
return Optional.ofNullable(mapper.selectOne(wrapper));
}
@Override
public boolean existsEntityCertByEntityId(Long entityId) {
LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<CertificateEntity>()
.eq(CertificateEntity::getEntityId, entityId)
.eq(CertificateEntity::getCertType, "ENTITY");
return mapper.selectCount(wrapper) > 0;
}
@Override
public long countByIssuerDn(String issuerDn) {
return mapper.selectCount(new LambdaQueryWrapper<CertificateEntity>()
.eq(CertificateEntity::getIssuerDn, issuerDn));
}
@Override
public Page<CertificateEntity> page(CertificateListRequest request) {
LambdaQueryWrapper<CertificateEntity> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(request.getCertType())) {
wrapper.eq(CertificateEntity::getCertType, request.getCertType());
}
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(CertificateEntity::getSubjectDn, request.getKeyword())
.or().like(CertificateEntity::getIssuerDn, request.getKeyword()));
}
wrapper.orderByDesc(CertificateEntity::getImportTime);
long total = mapper.selectCount(wrapper);
long offset = (long) (request.getPageNum() - 1) * request.getPageSize();
wrapper.last("LIMIT " + offset + "," + request.getPageSize());
List<CertificateEntity> records = mapper.selectList(wrapper);
Page<CertificateEntity> page = new Page<>(request.getPageNum(), request.getPageSize(), total);
page.setRecords(records);
return page;
}
}

View File

@ -0,0 +1,82 @@
package com.cisd.tms.modules.cert.repository.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.modules.cert.dto.EntityListRequest;
import com.cisd.tms.modules.cert.entity.KeyEntity;
import com.cisd.tms.modules.cert.mapper.KeyEntityMapper;
import com.cisd.tms.modules.cert.repository.KeyEntityRepository;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Repository
public class KeyEntityRepositoryImpl implements KeyEntityRepository {
private final KeyEntityMapper mapper;
public KeyEntityRepositoryImpl(KeyEntityMapper mapper) {
this.mapper = mapper;
}
@Override
public KeyEntity save(KeyEntity entity) {
if (entity.getId() == null) {
mapper.insert(entity);
} else {
mapper.updateById(entity);
}
return entity;
}
@Override
public void deleteById(Long id) {
mapper.deleteById(id);
}
@Override
public Optional<KeyEntity> findById(Long id) {
return Optional.ofNullable(mapper.selectById(id));
}
@Override
public Optional<KeyEntity> findByKeyIdx(Integer keyIdx) {
LambdaQueryWrapper<KeyEntity> wrapper = new LambdaQueryWrapper<KeyEntity>()
.eq(KeyEntity::getKeyIdx, keyIdx)
.last("LIMIT 1");
return Optional.ofNullable(mapper.selectOne(wrapper));
}
@Override
public List<Integer> findUsedKeyIdx() {
LambdaQueryWrapper<KeyEntity> wrapper = new LambdaQueryWrapper<KeyEntity>()
.select(KeyEntity::getKeyIdx);
return mapper.selectList(wrapper).stream()
.map(KeyEntity::getKeyIdx)
.collect(Collectors.toList());
}
@Override
public Page<KeyEntity> page(EntityListRequest request) {
LambdaQueryWrapper<KeyEntity> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(KeyEntity::getEntityName, request.getKeyword())
.or().like(KeyEntity::getSubjectDn, request.getKeyword()));
}
if (StringUtils.hasText(request.getAlgoType())) {
wrapper.eq(KeyEntity::getAlgoType, request.getAlgoType());
}
wrapper.orderByDesc(KeyEntity::getCreateTime);
long total = mapper.selectCount(wrapper);
long offset = (long) (request.getPageNum() - 1) * request.getPageSize();
wrapper.last("LIMIT " + offset + "," + request.getPageSize());
List<KeyEntity> records = mapper.selectList(wrapper);
Page<KeyEntity> page = new Page<>(request.getPageNum(), request.getPageSize(), total);
page.setRecords(records);
return page;
}
}

View File

@ -0,0 +1,83 @@
package com.cisd.tms.modules.cert.repository.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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 com.cisd.tms.modules.cert.repository.TrustedCertRepository;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Optional;
@Repository
public class TrustedCertRepositoryImpl implements TrustedCertRepository {
private final TrustedCertEntityMapper mapper;
public TrustedCertRepositoryImpl(TrustedCertEntityMapper mapper) {
this.mapper = mapper;
}
@Override
public TrustedCertEntity save(TrustedCertEntity entity) {
if (entity.getId() == null) {
mapper.insert(entity);
} else {
mapper.updateById(entity);
}
return entity;
}
@Override
public void deleteById(Long id) {
mapper.deleteById(id);
}
@Override
public Optional<TrustedCertEntity> findById(Long id) {
return Optional.ofNullable(mapper.selectById(id));
}
@Override
public Optional<TrustedCertEntity> findByFingerprint(String fingerprint) {
LambdaQueryWrapper<TrustedCertEntity> wrapper = new LambdaQueryWrapper<TrustedCertEntity>()
.eq(TrustedCertEntity::getFingerprint, fingerprint)
.last("LIMIT 1");
return Optional.ofNullable(mapper.selectOne(wrapper));
}
@Override
public List<TrustedCertEntity> findBySubjectDn(String subjectDn) {
return mapper.selectList(new LambdaQueryWrapper<TrustedCertEntity>()
.eq(TrustedCertEntity::getSubjectDn, subjectDn)
.orderByAsc(TrustedCertEntity::getCaLevel));
}
@Override
public long countByIssuerDn(String issuerDn) {
return mapper.selectCount(new LambdaQueryWrapper<TrustedCertEntity>()
.eq(TrustedCertEntity::getIssuerDn, issuerDn));
}
@Override
public Page<TrustedCertEntity> page(TrustedCertListRequest request) {
LambdaQueryWrapper<TrustedCertEntity> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(request.getKeyword())) {
wrapper.and(w -> w.like(TrustedCertEntity::getAlias, request.getKeyword())
.or().like(TrustedCertEntity::getSubjectDn, request.getKeyword())
.or().like(TrustedCertEntity::getIssuerDn, request.getKeyword()));
}
wrapper.orderByDesc(TrustedCertEntity::getImportTime);
long total = mapper.selectCount(wrapper);
long offset = (long) (request.getPageNum() - 1) * request.getPageSize();
wrapper.last("LIMIT " + offset + "," + request.getPageSize());
List<TrustedCertEntity> records = mapper.selectList(wrapper);
Page<TrustedCertEntity> page = new Page<>(request.getPageNum(), request.getPageSize(), total);
page.setRecords(records);
return page;
}
}

View File

@ -0,0 +1,156 @@
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.CertificateDetailResponse;
import com.cisd.tms.modules.cert.dto.CertificateItemResponse;
import com.cisd.tms.modules.cert.dto.CertificateListRequest;
import com.cisd.tms.modules.cert.dto.ImportCertificateResponse;
import com.cisd.tms.modules.cert.entity.CertificateEntity;
import com.cisd.tms.modules.cert.entity.KeyEntity;
import com.cisd.tms.modules.cert.repository.CertificateRepository;
import com.cisd.tms.modules.cert.support.CertPemSupport;
import com.cisd.tms.modules.cert.support.CertRuntimeStatusResolver;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
@Service
public class CertificateService {
private final CertificateRepository certificateRepository;
private final EntityService entityService;
private final TrustedCertService trustedCertService;
public CertificateService(
CertificateRepository certificateRepository,
EntityService entityService,
TrustedCertService trustedCertService
) {
this.certificateRepository = certificateRepository;
this.entityService = entityService;
this.trustedCertService = trustedCertService;
}
public Page<CertificateItemResponse> list(CertificateListRequest request) {
Page<CertificateEntity> page = certificateRepository.page(request);
Page<CertificateItemResponse> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
result.setRecords(page.getRecords().stream().map(this::toItem).toList());
return result;
}
@Transactional
public ImportCertificateResponse importCertificate(MultipartFile file, String certType, Long entityId) {
try {
X509Certificate certificate = CertPemSupport.parseCertificate(file.getBytes());
assertValidTime(certificate);
String fingerprint = CertPemSupport.sha256Fingerprint(certificate);
if (certificateRepository.findByFingerprint(fingerprint).isPresent()) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "certificate already exists");
}
Long finalEntityId = null;
if ("ENTITY".equalsIgnoreCase(certType)) {
if (entityId == null) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "entityId is required for ENTITY cert");
}
KeyEntity entity = entityService.getById(entityId);
byte[] certKey = certificate.getPublicKey().getEncoded();
byte[] entityKey = Base64.getDecoder().decode(entity.getPublicKey());
byte[] entityKeyX509 = com.cisd.tms.modules.cert.support.Sm2KeySupport.toSm2PublicKey(entityKey).getEncoded();
if (!java.util.Arrays.equals(certKey, entityKeyX509)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "certificate public key does not match entity");
}
finalEntityId = entityId;
}
trustedCertService.validateParentChain(trustedCertService.resolveIssuer(certificate.getIssuerX500Principal().getName()));
certificate.verify(
CertPemSupport.parseCertificate(
trustedCertService.resolveIssuer(certificate.getIssuerX500Principal().getName())
.getCertData().getBytes()
).getPublicKey(),
"BC"
);
CertificateEntity entity = new CertificateEntity()
.setEntityId(finalEntityId)
.setCertType(certType)
.setSubjectDn(certificate.getSubjectX500Principal().getName())
.setIssuerDn(certificate.getIssuerX500Principal().getName())
.setSerialNumber(certificate.getSerialNumber().toString(16).toUpperCase())
.setAlgoType("SM2")
.setFingerprint(fingerprint)
.setCertData(CertPemSupport.toPem(certificate))
.setValidFrom(LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault()))
.setValidTo(LocalDateTime.ofInstant(certificate.getNotAfter().toInstant(), ZoneId.systemDefault()))
.setImportTime(LocalDateTime.now())
.setCreateBy("system");
entity.setCreateTime(LocalDateTime.now());
entity.setUpdateTime(LocalDateTime.now());
certificateRepository.save(entity);
return new ImportCertificateResponse(entity.getId(), entity.getSubjectDn(), entity.getValidTo());
} catch (BizException ex) {
throw ex;
} catch (Exception ex) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "certificate import failed: " + ex.getMessage());
}
}
public CertificateDetailResponse detail(Long id) {
CertificateEntity entity = getById(id);
CertificateDetailResponse response = new CertificateDetailResponse();
response.setId(entity.getId());
response.setEntityId(entity.getEntityId());
response.setCertType(entity.getCertType());
response.setSubjectDn(entity.getSubjectDn());
response.setIssuerDn(entity.getIssuerDn());
response.setSerialNumber(entity.getSerialNumber());
response.setAlgoType(entity.getAlgoType());
response.setFingerprint(entity.getFingerprint());
response.setCertData(entity.getCertData());
response.setValidFrom(entity.getValidFrom());
response.setValidTo(entity.getValidTo());
response.setRuntimeStatus(CertRuntimeStatusResolver.resolve(entity.getValidFrom(), entity.getValidTo()));
return response;
}
@Transactional
public void delete(Long id) {
getById(id);
certificateRepository.deleteById(id);
}
private CertificateItemResponse toItem(CertificateEntity entity) {
CertificateItemResponse response = new CertificateItemResponse();
response.setId(entity.getId());
response.setEntityId(entity.getEntityId());
response.setCertType(entity.getCertType());
response.setSubjectDn(entity.getSubjectDn());
response.setIssuerDn(entity.getIssuerDn());
response.setSerialNumber(entity.getSerialNumber());
response.setAlgoType(entity.getAlgoType());
response.setFingerprint(entity.getFingerprint());
response.setValidFrom(entity.getValidFrom());
response.setValidTo(entity.getValidTo());
response.setRuntimeStatus(CertRuntimeStatusResolver.resolve(entity.getValidFrom(), entity.getValidTo()));
return response;
}
private CertificateEntity getById(Long id) {
return certificateRepository.findById(id)
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "certificate not found"));
}
private void assertValidTime(X509Certificate certificate) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime notBefore = LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault());
LocalDateTime notAfter = LocalDateTime.ofInstant(certificate.getNotAfter().toInstant(), ZoneId.systemDefault());
if (now.isBefore(notBefore) || now.isAfter(notAfter)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "certificate is not in valid time window");
}
}
}

View File

@ -0,0 +1,238 @@
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.integration.crypto.pcie.PcieUserKeyType;
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
import com.cisd.tms.integration.crypto.pcie.model.GenerateKeyPairRequest;
import com.cisd.tms.integration.crypto.pcie.model.UserKeySignRequest;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
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.Subject;
import com.cisd.tms.modules.cert.entity.KeyEntity;
import com.cisd.tms.modules.cert.repository.CertificateRepository;
import com.cisd.tms.modules.cert.repository.KeyEntityRepository;
import com.cisd.tms.modules.cert.support.Sm2KeySupport;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.bouncycastle.asn1.gm.GMObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
@Service
public class EntityService {
private static final String SM2 = "SM2";
private final KeyEntityRepository keyEntityRepository;
private final CertificateRepository certificateRepository;
private final PcieCryptoService pcieCryptoService;
private final ObjectMapper objectMapper;
public EntityService(
KeyEntityRepository keyEntityRepository,
CertificateRepository certificateRepository,
PcieCryptoService pcieCryptoService,
ObjectMapper objectMapper
) {
this.keyEntityRepository = keyEntityRepository;
this.certificateRepository = certificateRepository;
this.pcieCryptoService = pcieCryptoService;
this.objectMapper = objectMapper;
}
public Page<EntityItemResponse> list(EntityListRequest request) {
Page<KeyEntity> page = keyEntityRepository.page(request);
Page<EntityItemResponse> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
result.setRecords(page.getRecords().stream().map(this::toItem).toList());
return result;
}
@Transactional
public EntityCreateResponse create(EntityCreateRequest request) {
if (!SM2.equalsIgnoreCase(request.getAlgoType())) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "only SM2 is supported");
}
int keyIdx = resolveNextKeyIdx();
GenerateKeyPairRequest generateRequest = new GenerateKeyPairRequest();
generateRequest.setKeyBits(256);
generateRequest.setKeyIndex(keyIdx);
generateRequest.setKeyType(PcieUserKeyType.SIGN.getCode());
pcieCryptoService.generateKeyPairEcc(generateRequest);
try {
BackupDataResult backup = pcieCryptoService.backupUserKey(keyIdx, PcieUserKeyType.SIGN.getCode(), 4096);
BackupDataResult publicKey = pcieCryptoService.exportSignPublicKeyEcc(keyIdx);
KeyEntity entity = new KeyEntity()
.setEntityName(request.getEntityName())
.setAlgoType(SM2)
.setKeyLen(256)
.setKeyIdx(keyIdx)
.setBackupData(Base64.getEncoder().encodeToString(backup.getData()))
.setPublicKey(Base64.getEncoder().encodeToString(publicKey.getData()))
.setAllowIps(writeAllowIps(request.getAllowIps()))
.setCreateBy("system");
entity.setCreateTime(LocalDateTime.now());
entity.setUpdateTime(LocalDateTime.now());
keyEntityRepository.save(entity);
return new EntityCreateResponse(entity.getId());
} catch (RuntimeException ex) {
try {
pcieCryptoService.deleteUserKey(keyIdx);
} catch (Exception ignored) {
}
throw ex;
}
}
@Transactional
public void update(EntityUpdateRequest request) {
KeyEntity entity = getById(request.getId());
if (StringUtils.hasText(request.getEntityName())) {
entity.setEntityName(request.getEntityName());
}
if (request.getAllowIps() != null) {
entity.setAllowIps(writeAllowIps(request.getAllowIps()));
}
keyEntityRepository.save(entity);
}
@Transactional
public void delete(Long id) {
KeyEntity entity = getById(id);
if (certificateRepository.existsEntityCertByEntityId(id)) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "entity certificate exists, delete certificate first");
}
pcieCryptoService.deleteUserKey(entity.getKeyIdx());
keyEntityRepository.deleteById(id);
}
@Transactional
public String generateP10(EntityGenP10Request request) {
KeyEntity entity = getById(request.getId());
Subject subject = request.getSubject();
String subjectDn = subject.getDN();
PublicKey publicKey = Sm2KeySupport.toSm2PublicKey(Base64.getDecoder().decode(entity.getPublicKey()));
PKCS10CertificationRequestBuilder builder =
new JcaPKCS10CertificationRequestBuilder(new X500Name(subjectDn), publicKey);
PKCS10CertificationRequest csr = builder.build(new CardContentSigner(entity.getKeyIdx(), pcieCryptoService));
String pem = encodePkcs10Pem(csr);
entity.setSubjectDn(subjectDn);
entity.setP10Data(pem);
keyEntityRepository.save(entity);
return pem;
}
public String getP10(Long id) {
KeyEntity entity = getById(id);
return entity.getP10Data();
}
public KeyEntity getById(Long id) {
return keyEntityRepository.findById(id)
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "entity not found"));
}
private EntityItemResponse toItem(KeyEntity entity) {
EntityItemResponse response = new EntityItemResponse();
response.setId(entity.getId());
response.setEntityName(entity.getEntityName());
response.setAlgoType(entity.getAlgoType());
response.setKeyLen(entity.getKeyLen());
response.setKeyIdx(entity.getKeyIdx());
response.setSubject(entity.getSubjectDn());
response.setAllowIps(readAllowIps(entity.getAllowIps()));
response.setCreateTime(entity.getCreateTime());
return response;
}
private int resolveNextKeyIdx() {
List<Integer> used = keyEntityRepository.findUsedKeyIdx();
for (int idx = 2; idx <= 8; idx++) {
if (!used.contains(idx)) {
return idx;
}
}
throw new BizException(ErrorCode.CONFLICT.getCode(), "no available key index in [2..8]");
}
private String writeAllowIps(List<String> allowIps) {
try {
return allowIps == null ? "[]" : objectMapper.writeValueAsString(allowIps);
} catch (JsonProcessingException e) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "invalid allowIps");
}
}
private List<String> readAllowIps(String allowIps) {
if (!StringUtils.hasText(allowIps)) {
return List.of();
}
try {
return objectMapper.readValue(allowIps, new TypeReference<List<String>>() {});
} catch (Exception e) {
return new ArrayList<>();
}
}
private String encodePkcs10Pem(PKCS10CertificationRequest csr) {
try {
String base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8))
.encodeToString(csr.getEncoded());
return "-----BEGIN CERTIFICATE REQUEST-----\n" + base64 + "\n-----END CERTIFICATE REQUEST-----";
} catch (Exception e) {
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to encode csr");
}
}
private static final class CardContentSigner implements ContentSigner {
private final java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream();
private final int keyIdx;
private final PcieCryptoService pcieCryptoService;
private CardContentSigner(int keyIdx, PcieCryptoService pcieCryptoService) {
this.keyIdx = keyIdx;
this.pcieCryptoService = pcieCryptoService;
}
@Override
public org.bouncycastle.asn1.x509.AlgorithmIdentifier getAlgorithmIdentifier() {
return new org.bouncycastle.asn1.x509.AlgorithmIdentifier(GMObjectIdentifiers.sm2sign_with_sm3);
}
@Override
public OutputStream getOutputStream() {
return output;
}
@Override
public byte[] getSignature() {
UserKeySignRequest request = new UserKeySignRequest();
request.setKeyIndex(keyIdx);
request.setData(output.toByteArray());
return Sm2KeySupport.rawSignatureToDer(pcieCryptoService.userKeySignWithSm3(request).getData());
}
}
}

View File

@ -0,0 +1,184 @@
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.DeleteDependencyResponse;
import com.cisd.tms.modules.cert.dto.ImportCertificateResponse;
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 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.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
@Service
public class TrustedCertService {
static {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private final TrustedCertRepository trustedCertRepository;
private final CertificateRepository certificateRepository;
public TrustedCertService(
TrustedCertRepository trustedCertRepository,
CertificateRepository certificateRepository
) {
this.trustedCertRepository = trustedCertRepository;
this.certificateRepository = certificateRepository;
}
public Page<TrustedCertItemResponse> list(TrustedCertListRequest request) {
Page<TrustedCertEntity> page = trustedCertRepository.page(request);
Page<TrustedCertItemResponse> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
result.setRecords(page.getRecords().stream().map(this::toItem).toList());
return result;
}
@Transactional
public ImportCertificateResponse importTrusted(MultipartFile file, String alias) {
try {
X509Certificate certificate = CertPemSupport.parseCertificate(file.getBytes());
assertValidTime(certificate);
String fingerprint = CertPemSupport.sha256Fingerprint(certificate);
if (trustedCertRepository.findByFingerprint(fingerprint).isPresent()) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "trusted certificate already exists");
}
int caLevel = isSelfSigned(certificate) ? 0 : 1;
if (caLevel == 0) {
certificate.verify(certificate.getPublicKey(), "BC");
} else {
TrustedCertEntity issuer = resolveIssuer(certificate.getIssuerX500Principal().getName());
X509Certificate issuerCert = CertPemSupport.parseCertificate(issuer.getCertData().getBytes());
certificate.verify(issuerCert.getPublicKey(), "BC");
validateParentChain(issuer);
}
TrustedCertEntity entity = new TrustedCertEntity()
.setAlias(alias)
.setSubjectDn(certificate.getSubjectX500Principal().getName())
.setIssuerDn(certificate.getIssuerX500Principal().getName())
.setSerialNumber(certificate.getSerialNumber().toString(16).toUpperCase())
.setAlgoType("SM2")
.setFingerprint(fingerprint)
.setCertData(CertPemSupport.toPem(certificate))
.setCaLevel(caLevel)
.setValidFrom(LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault()))
.setValidTo(LocalDateTime.ofInstant(certificate.getNotAfter().toInstant(), ZoneId.systemDefault()))
.setImportTime(LocalDateTime.now())
.setCreateBy("system");
entity.setCreateTime(LocalDateTime.now());
entity.setUpdateTime(LocalDateTime.now());
trustedCertRepository.save(entity);
return new ImportCertificateResponse(entity.getId(), entity.getSubjectDn(), entity.getValidTo());
} catch (BizException ex) {
throw ex;
} catch (Exception ex) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "trusted certificate import failed: " + ex.getMessage());
}
}
public TrustedCertDetailResponse detail(Long id) {
TrustedCertEntity entity = getById(id);
TrustedCertDetailResponse response = new TrustedCertDetailResponse();
response.setId(entity.getId());
response.setAlias(entity.getAlias());
response.setSubjectDn(entity.getSubjectDn());
response.setIssuerDn(entity.getIssuerDn());
response.setSerialNumber(entity.getSerialNumber());
response.setAlgoType(entity.getAlgoType());
response.setFingerprint(entity.getFingerprint());
response.setCertData(entity.getCertData());
response.setCaLevel(entity.getCaLevel());
response.setValidFrom(entity.getValidFrom());
response.setValidTo(entity.getValidTo());
response.setImportTime(entity.getImportTime());
return response;
}
@Transactional
public DeleteDependencyResponse delete(TrustedCertDeleteRequest request) {
TrustedCertEntity entity = getById(request.getId());
long childCaCount = trustedCertRepository.countByIssuerDn(entity.getSubjectDn());
long issuedCertCount = certificateRepository.countByIssuerDn(entity.getSubjectDn());
if ((childCaCount > 0 || issuedCertCount > 0) && !Boolean.TRUE.equals(request.getForce())) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "trusted certificate has dependent certificates");
}
trustedCertRepository.deleteById(request.getId());
return new DeleteDependencyResponse(childCaCount, issuedCertCount);
}
public TrustedCertEntity resolveIssuer(String subjectDn) {
List<TrustedCertEntity> matched = trustedCertRepository.findBySubjectDn(subjectDn);
if (matched.isEmpty()) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "issuer certificate not found");
}
return matched.get(0);
}
public void validateParentChain(TrustedCertEntity entity) {
if (entity.getSubjectDn().equals(entity.getIssuerDn())) {
return;
}
try {
X509Certificate certificate = CertPemSupport.parseCertificate(entity.getCertData().getBytes());
TrustedCertEntity issuer = resolveIssuer(entity.getIssuerDn());
X509Certificate issuerCert = CertPemSupport.parseCertificate(issuer.getCertData().getBytes());
certificate.verify(issuerCert.getPublicKey(), "BC");
validateParentChain(issuer);
} catch (BizException ex) {
throw ex;
} catch (Exception ex) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "certificate chain validation failed");
}
}
private TrustedCertItemResponse toItem(TrustedCertEntity entity) {
TrustedCertItemResponse response = new TrustedCertItemResponse();
response.setId(entity.getId());
response.setAlias(entity.getAlias());
response.setSubjectDn(entity.getSubjectDn());
response.setIssuerDn(entity.getIssuerDn());
response.setSerialNumber(entity.getSerialNumber());
response.setAlgoType(entity.getAlgoType());
response.setFingerprint(entity.getFingerprint());
response.setCaLevel(entity.getCaLevel());
response.setValidFrom(entity.getValidFrom());
response.setValidTo(entity.getValidTo());
response.setImportTime(entity.getImportTime());
return response;
}
private TrustedCertEntity getById(Long id) {
return trustedCertRepository.findById(id)
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "trusted certificate not found"));
}
private void assertValidTime(X509Certificate certificate) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime notBefore = LocalDateTime.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault());
LocalDateTime notAfter = LocalDateTime.ofInstant(certificate.getNotAfter().toInstant(), ZoneId.systemDefault());
if (now.isBefore(notBefore) || now.isAfter(notAfter)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "certificate is not in valid time window");
}
}
private boolean isSelfSigned(X509Certificate certificate) {
return certificate.getSubjectX500Principal().equals(certificate.getIssuerX500Principal());
}
}

View File

@ -0,0 +1,75 @@
package com.cisd.tms.modules.cert.support;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.Security;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
public final class CertPemSupport {
private static final String PROVIDER = "BC";
static {
if (Security.getProvider(PROVIDER) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private CertPemSupport() {
}
public static X509Certificate parseCertificate(byte[] bytes) {
try {
byte[] normalized = toDer(bytes);
CertificateFactory factory = CertificateFactory.getInstance("X.509", PROVIDER);
return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(normalized));
} catch (Exception e) {
throw new IllegalArgumentException("invalid certificate file", e);
}
}
public static String toPem(X509Certificate certificate) {
try {
String base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8))
.encodeToString(certificate.getEncoded());
return "-----BEGIN CERTIFICATE-----\n" + base64 + "\n-----END CERTIFICATE-----";
} catch (Exception e) {
throw new IllegalArgumentException("failed to encode certificate pem", e);
}
}
public static String sha256Fingerprint(X509Certificate certificate) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return hex(digest.digest(certificate.getEncoded()));
} catch (Exception e) {
throw new IllegalArgumentException("failed to calculate fingerprint", e);
}
}
private static byte[] toDer(byte[] bytes) {
String text = new String(bytes, StandardCharsets.UTF_8);
if (!text.contains("BEGIN CERTIFICATE")) {
return bytes;
}
String normalized = text
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replaceAll("\\s+", "");
return Base64.getDecoder().decode(normalized);
}
private static String hex(byte[] bytes) {
StringBuilder builder = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
builder.append(String.format("%02X", b));
}
return builder.toString();
}
}

View File

@ -0,0 +1,21 @@
package com.cisd.tms.modules.cert.support;
import java.time.LocalDateTime;
public final class CertRuntimeStatusResolver {
private CertRuntimeStatusResolver() {
}
public static String resolve(LocalDateTime validFrom, LocalDateTime validTo) {
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(validFrom)) {
return "NOT_YET_VALID";
}
if (now.isAfter(validTo)) {
return "EXPIRED";
}
return "VALID";
}
}

View File

@ -0,0 +1,84 @@
package com.cisd.tms.modules.cert.support;
import com.cisd.tms.integration.crypto.pcie.jna.EccRefPublicKey;
import com.cisd.tms.integration.crypto.pcie.jna.EccSignature;
import com.sun.jna.Pointer;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Security;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.util.Arrays;
public final class Sm2KeySupport {
private static final String PROVIDER = "BC";
static {
if (Security.getProvider(PROVIDER) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private Sm2KeySupport() {
}
public static PublicKey toSm2PublicKey(byte[] blob) {
try {
EccRefPublicKey key = new EccRefPublicKey();
Pointer pointer = key.getPointer();
pointer.clear(key.size());
pointer.write(0, blob, 0, blob.length);
key.read();
byte[] x = trimCoordinate(key.x);
byte[] y = trimCoordinate(key.y);
ECNamedCurveParameterSpec bcSpec = ECNamedCurveTable.getParameterSpec("sm2p256v1");
ECNamedCurveSpec spec = new ECNamedCurveSpec("sm2p256v1", bcSpec.getCurve(), bcSpec.getG(), bcSpec.getN());
ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(
new ECPoint(new BigInteger(1, x), new BigInteger(1, y)),
spec
);
return KeyFactory.getInstance("EC", PROVIDER).generatePublic(publicKeySpec);
} catch (Exception e) {
throw new IllegalArgumentException("failed to parse sm2 public key", e);
}
}
public static byte[] rawSignatureToDer(byte[] rawSignature) {
EccSignature signature = new EccSignature();
Pointer pointer = signature.getPointer();
pointer.clear(signature.size());
pointer.write(0, rawSignature, 0, rawSignature.length);
signature.read();
ASN1EncodableVector vector = new ASN1EncodableVector();
vector.add(new ASN1Integer(new BigInteger(1, trimCoordinate(signature.r))));
vector.add(new ASN1Integer(new BigInteger(1, trimCoordinate(signature.s))));
try {
return new DERSequence(vector).getEncoded();
} catch (Exception e) {
throw new IllegalArgumentException("failed to encode sm2 signature", e);
}
}
private static byte[] trimCoordinate(byte[] source) {
int start = 0;
while (start < source.length - 1 && source[start] == 0) {
start++;
}
byte[] trimmed = Arrays.copyOfRange(source, start, source.length);
if (trimmed.length > 32) {
return Arrays.copyOfRange(trimmed, trimmed.length - 32, trimmed.length);
}
return trimmed;
}
}

View File

@ -0,0 +1,72 @@
-- -----------------------------------------------------------------------------
-- Cert module tables
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS t_key_entity (
id BIGINT PRIMARY KEY,
entity_name VARCHAR(255) NOT NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2',
key_len INT NOT NULL COMMENT 'SM2 fixed 256 in V1',
key_idx INT NOT NULL COMMENT 'card key index, allowed range [2..8]',
backup_data VARCHAR(500) NOT NULL COMMENT 'SDFE_BackupUserKey output',
public_key VARCHAR(500) NOT NULL COMMENT 'public key, Base64/PEM',
p10_data VARCHAR(2000) NULL COMMENT 'PKCS#10 PEM content',
subject_dn VARCHAR(500) NULL COMMENT 'DN used when generating P10',
allow_ips VARCHAR(500) NULL COMMENT 'allowed source IP list',
sign_data VARCHAR(512) NULL COMMENT 'reserved, not used in V1',
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_t_key_entity_key_idx (key_idx),
KEY idx_t_key_entity_name (entity_name),
KEY idx_t_key_entity_algo (algo_type),
KEY idx_t_key_entity_create_time (create_time)
);
CREATE TABLE IF NOT EXISTS t_certificate (
id BIGINT PRIMARY KEY,
entity_id BIGINT NULL COMMENT 'required when cert_type=ENTITY',
cert_type VARCHAR(16) NOT NULL COMMENT 'ENTITY / USER',
subject_dn VARCHAR(500) NOT NULL,
issuer_dn VARCHAR(1024) NOT NULL,
serial_number VARCHAR(128) NOT NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
fingerprint VARCHAR(128) NOT NULL COMMENT 'SHA-256 fingerprint',
cert_data VARCHAR(2000) NOT NULL COMMENT 'certificate PEM content',
valid_from DATETIME(3) NOT NULL,
valid_to DATETIME(3) NOT NULL,
import_time DATETIME(3) NOT NULL,
sign_data VARCHAR(512) NULL COMMENT 'reserved, not used in V1',
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_t_certificate_fingerprint (fingerprint),
KEY idx_t_certificate_entity_id (entity_id),
KEY idx_t_certificate_cert_type (cert_type),
KEY idx_t_certificate_issuer_dn (issuer_dn),
KEY idx_t_certificate_valid_to (valid_to),
KEY idx_t_certificate_import_time (import_time)
);
CREATE TABLE IF NOT EXISTS t_trusted_cert (
id BIGINT PRIMARY KEY,
alias VARCHAR(128) NOT NULL,
subject_dn VARCHAR(1024) NOT NULL,
issuer_dn VARCHAR(1024) NOT NULL,
serial_number VARCHAR(128) NOT NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
fingerprint VARCHAR(128) NOT NULL COMMENT 'SHA-256 fingerprint',
cert_data VARCHAR(2000) NOT NULL COMMENT 'certificate PEM content',
ca_level TINYINT NOT NULL COMMENT '0:Root, 1:Intermediate',
valid_from DATETIME(3) NOT NULL,
valid_to DATETIME(3) NOT NULL,
import_time DATETIME(3) NOT NULL,
sign_data VARCHAR(512) NULL COMMENT 'reserved, not used in V1',
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_t_trusted_cert_fingerprint (fingerprint),
KEY idx_t_trusted_cert_subject_dn (subject_dn),
KEY idx_t_trusted_cert_issuer_dn (issuer_dn),
KEY idx_t_trusted_cert_valid_to (valid_to)
);