feat:根证书导入增加校验
This commit is contained in:
parent
cae94149cb
commit
f06c675f9b
@ -177,7 +177,7 @@ Open:
|
|||||||
- `POST /api/v1/crls/delete`
|
- `POST /api/v1/crls/delete`
|
||||||
|
|
||||||
当前实现边界:
|
当前实现边界:
|
||||||
- 可通过 `tms.cert.trusted-root-fingerprints` 配置根 CA 证书 SHA-256 指纹白名单;配置后根 CA 导入必须命中白名单,中间 CA 仍按当前可信链校验。
|
- 根 CA 导入必须完成来源校验:读取配置项 `tms.cert.root-cert-source.public-key-sm3-path`、`tms.cert.root-cert-source.public-key-path`、`tms.cert.root-cert-source.signature-path` 指向的文件,默认分别为 `/home/tms/device/dev.mac.pub.sm3`、`/home/tms/device/mac_sm2_public_key.pemy` 和 `/home/tms/device/rootcert.sign`;中间 CA 仍按当前可信链校验。
|
||||||
- CRL 文件支持 PEM / DER 格式,导入接口会先创建后台任务并返回 `taskId`,前端通过 `POST /api/v1/crls/import-tasks/detail` 轮询 `PENDING/RUNNING/SUCCESS/FAILED` 状态。
|
- CRL 文件支持 PEM / DER 格式,导入接口会先创建后台任务并返回 `taskId`,前端通过 `POST /api/v1/crls/import-tasks/detail` 轮询 `PENDING/RUNNING/SUCCESS/FAILED` 状态。
|
||||||
- 后台导入完成后保存 CRL metadata 和 CRL 内每条 revoked certificate 明细;原始 CRL 文件只作为临时文件参与解析,任务结束后删除,不作为业务数据长期保存。
|
- 后台导入完成后保存 CRL metadata 和 CRL 内每条 revoked certificate 明细;原始 CRL 文件只作为临时文件参与解析,任务结束后删除,不作为业务数据长期保存。
|
||||||
- CRL issuer 通过可信 CA 的 issuer DN 候选和实际 CRL 签名验证解析。
|
- CRL issuer 通过可信 CA 的 issuer DN 候选和实际 CRL 签名验证解析。
|
||||||
|
|||||||
@ -2,22 +2,49 @@ package com.cisd.tms.modules.cert.config;
|
|||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@ConfigurationProperties(prefix = "tms.cert")
|
@ConfigurationProperties(prefix = "tms.cert")
|
||||||
public class CertProperties {
|
public class CertProperties {
|
||||||
|
|
||||||
/**
|
private RootCertSource rootCertSource = new RootCertSource();
|
||||||
* 允许导入为信任锚的根 CA 证书 SHA-256 指纹。为空时保持兼容,不限制根 CA 导入。
|
|
||||||
*/
|
|
||||||
private List<String> trustedRootFingerprints = new ArrayList<>();
|
|
||||||
|
|
||||||
public List<String> getTrustedRootFingerprints() {
|
public RootCertSource getRootCertSource() {
|
||||||
return trustedRootFingerprints;
|
return rootCertSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTrustedRootFingerprints(List<String> trustedRootFingerprints) {
|
public void setRootCertSource(RootCertSource rootCertSource) {
|
||||||
this.trustedRootFingerprints = trustedRootFingerprints == null ? new ArrayList<>() : trustedRootFingerprints;
|
this.rootCertSource = rootCertSource == null ? new RootCertSource() : rootCertSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RootCertSource {
|
||||||
|
|
||||||
|
private String publicKeySm3Path = "/home/tms/device/dev.mac.pub.sm3";
|
||||||
|
|
||||||
|
private String publicKeyPath = "/home/tms/device/mac_sm2_public_key.pemy";
|
||||||
|
|
||||||
|
private String signaturePath = "/home/tms/device/rootcert.sign";
|
||||||
|
|
||||||
|
public String getPublicKeySm3Path() {
|
||||||
|
return publicKeySm3Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublicKeySm3Path(String publicKeySm3Path) {
|
||||||
|
this.publicKeySm3Path = publicKeySm3Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPublicKeyPath() {
|
||||||
|
return publicKeyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublicKeyPath(String publicKeyPath) {
|
||||||
|
this.publicKeyPath = publicKeyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSignaturePath() {
|
||||||
|
return signaturePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSignaturePath(String signaturePath) {
|
||||||
|
this.signaturePath = signaturePath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,121 @@
|
|||||||
|
package com.cisd.tms.modules.cert.service;
|
||||||
|
|
||||||
|
import com.cisd.tms.common.enums.ErrorCode;
|
||||||
|
import com.cisd.tms.common.exception.BizException;
|
||||||
|
import com.cisd.tms.modules.cert.config.CertProperties;
|
||||||
|
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.util.encoders.Hex;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class FileRootCertSourceVerifier implements RootCertSourceVerifier {
|
||||||
|
|
||||||
|
private static final String UNTRUSTED_ROOT_CERT = "根证书来源不可信";
|
||||||
|
|
||||||
|
static {
|
||||||
|
if (Security.getProvider("BC") == null) {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Path publicKeySm3Path;
|
||||||
|
private final Path publicKeyPath;
|
||||||
|
private final Path signaturePath;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public FileRootCertSourceVerifier(CertProperties certProperties) {
|
||||||
|
this(
|
||||||
|
Path.of(certProperties.getRootCertSource().getPublicKeySm3Path()),
|
||||||
|
Path.of(certProperties.getRootCertSource().getPublicKeyPath()),
|
||||||
|
Path.of(certProperties.getRootCertSource().getSignaturePath())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileRootCertSourceVerifier(Path deviceDir) {
|
||||||
|
this(
|
||||||
|
deviceDir.resolve("dev.mac.pub.sm3"),
|
||||||
|
deviceDir.resolve("mac_sm2_public_key.pemy"),
|
||||||
|
deviceDir.resolve("rootcert.sign")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileRootCertSourceVerifier(Path publicKeySm3Path, Path publicKeyPath, Path signaturePath) {
|
||||||
|
this.publicKeySm3Path = publicKeySm3Path;
|
||||||
|
this.publicKeyPath = publicKeyPath;
|
||||||
|
this.signaturePath = signaturePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verifyTrustedSource(byte[] certData) {
|
||||||
|
try {
|
||||||
|
PublicKey publicKey = loadPublicKey(publicKeyPath);
|
||||||
|
if (!isPublicKeySm3Trusted(publicKey)) {
|
||||||
|
throw untrusted();
|
||||||
|
}
|
||||||
|
byte[] signature = Files.readAllBytes(signaturePath);
|
||||||
|
if (!verifySignature(certData, signature, publicKey)) {
|
||||||
|
throw untrusted();
|
||||||
|
}
|
||||||
|
} catch (BizException ex) {
|
||||||
|
throw ex;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw untrusted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPublicKeySm3Trusted(PublicKey publicKey) throws Exception {
|
||||||
|
String savedSm3 = Files.readString(publicKeySm3Path, StandardCharsets.UTF_8);
|
||||||
|
return normalizeHex(savedSm3).equals(normalizeHex(sm3Hex(publicKey.getEncoded())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean verifySignature(byte[] certData, byte[] sign, PublicKey publicKey) throws Exception {
|
||||||
|
Signature signature = Signature.getInstance("SM3withSM2", "BC");
|
||||||
|
signature.initVerify(publicKey);
|
||||||
|
signature.update(certData);
|
||||||
|
return signature.verify(sign);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PublicKey loadPublicKey(Path publicKeyPath) throws Exception {
|
||||||
|
String pem = Files.readString(publicKeyPath, StandardCharsets.UTF_8);
|
||||||
|
String keyBase64 = pem.lines()
|
||||||
|
.filter(line -> !line.startsWith("-----"))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(StringUtils::hasText)
|
||||||
|
.reduce("", String::concat);
|
||||||
|
byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
|
||||||
|
return KeyFactory.getInstance("EC", "BC").generatePublic(new X509EncodedKeySpec(keyBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sm3Hex(byte[] data) {
|
||||||
|
SM3Digest digest = new SM3Digest();
|
||||||
|
digest.update(data, 0, data.length);
|
||||||
|
byte[] result = new byte[digest.getDigestSize()];
|
||||||
|
digest.doFinal(result, 0);
|
||||||
|
return Hex.toHexString(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeHex(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return value.replaceAll("[^0-9A-Fa-f]", "").toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BizException untrusted() {
|
||||||
|
return new BizException(ErrorCode.BAD_REQUEST.getCode(), UNTRUSTED_ROOT_CERT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.cisd.tms.modules.cert.service;
|
||||||
|
|
||||||
|
public interface RootCertSourceVerifier {
|
||||||
|
|
||||||
|
void verifyTrustedSource(byte[] certData);
|
||||||
|
}
|
||||||
@ -9,7 +9,6 @@ import com.cisd.tms.modules.cert.dto.TrustedCertDeleteRequest;
|
|||||||
import com.cisd.tms.modules.cert.dto.TrustedCertDetailResponse;
|
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.config.CertProperties;
|
|
||||||
import com.cisd.tms.modules.cert.entity.TrustedCertEntity;
|
import com.cisd.tms.modules.cert.entity.TrustedCertEntity;
|
||||||
import com.cisd.tms.modules.cert.repository.CertificateRepository;
|
import com.cisd.tms.modules.cert.repository.CertificateRepository;
|
||||||
import com.cisd.tms.modules.cert.repository.TrustedCertRepository;
|
import com.cisd.tms.modules.cert.repository.TrustedCertRepository;
|
||||||
@ -18,7 +17,6 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.util.StringUtils;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -28,10 +26,7 @@ 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;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class TrustedCertService {
|
public class TrustedCertService {
|
||||||
@ -47,32 +42,25 @@ public class TrustedCertService {
|
|||||||
|
|
||||||
private final TrustedCertRepository trustedCertRepository;
|
private final TrustedCertRepository trustedCertRepository;
|
||||||
private final CertificateRepository certificateRepository;
|
private final CertificateRepository certificateRepository;
|
||||||
private final Set<String> trustedRootFingerprints;
|
private final RootCertSourceVerifier rootCertSourceVerifier;
|
||||||
|
|
||||||
public TrustedCertService(
|
|
||||||
TrustedCertRepository trustedCertRepository,
|
|
||||||
CertificateRepository certificateRepository
|
|
||||||
) {
|
|
||||||
this(trustedCertRepository, certificateRepository, List.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public TrustedCertService(
|
public TrustedCertService(
|
||||||
TrustedCertRepository trustedCertRepository,
|
TrustedCertRepository trustedCertRepository,
|
||||||
CertificateRepository certificateRepository,
|
CertificateRepository certificateRepository,
|
||||||
CertProperties certProperties
|
RootCertSourceVerifier rootCertSourceVerifier
|
||||||
) {
|
) {
|
||||||
this(trustedCertRepository, certificateRepository, certProperties.getTrustedRootFingerprints());
|
this.trustedCertRepository = trustedCertRepository;
|
||||||
|
this.certificateRepository = certificateRepository;
|
||||||
|
this.rootCertSourceVerifier = rootCertSourceVerifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
TrustedCertService(
|
TrustedCertService(
|
||||||
TrustedCertRepository trustedCertRepository,
|
TrustedCertRepository trustedCertRepository,
|
||||||
CertificateRepository certificateRepository,
|
CertificateRepository certificateRepository
|
||||||
List<String> trustedRootFingerprints
|
|
||||||
) {
|
) {
|
||||||
this.trustedCertRepository = trustedCertRepository;
|
this(trustedCertRepository, certificateRepository, certData -> {
|
||||||
this.certificateRepository = certificateRepository;
|
});
|
||||||
this.trustedRootFingerprints = normalizeFingerprints(trustedRootFingerprints);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page<TrustedCertItemResponse> list(TrustedCertListRequest request) {
|
public Page<TrustedCertItemResponse> list(TrustedCertListRequest request) {
|
||||||
@ -111,7 +99,7 @@ public class TrustedCertService {
|
|||||||
}
|
}
|
||||||
int caLevel = isSelfSigned(certificate) ? 0 : 1;
|
int caLevel = isSelfSigned(certificate) ? 0 : 1;
|
||||||
if (caLevel == 0) {
|
if (caLevel == 0) {
|
||||||
assertRootFingerprintAllowed(fingerprint);
|
rootCertSourceVerifier.verifyTrustedSource(fileData);
|
||||||
// 根 CA 必须能自签自验,防止把 subject=issuer 的伪造证书放入信任锚。
|
// 根 CA 必须能自签自验,防止把 subject=issuer 的伪造证书放入信任锚。
|
||||||
certificate.verify(certificate.getPublicKey(), "BC");
|
certificate.verify(certificate.getPublicKey(), "BC");
|
||||||
} else {
|
} else {
|
||||||
@ -312,30 +300,4 @@ public class TrustedCertService {
|
|||||||
private boolean isSelfSigned(X509Certificate certificate) {
|
private boolean isSelfSigned(X509Certificate certificate) {
|
||||||
return certificate.getSubjectX500Principal().equals(certificate.getIssuerX500Principal());
|
return certificate.getSubjectX500Principal().equals(certificate.getIssuerX500Principal());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertRootFingerprintAllowed(String fingerprint) {
|
|
||||||
if (trustedRootFingerprints.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!trustedRootFingerprints.contains(normalizeFingerprint(fingerprint))) {
|
|
||||||
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "根CA证书指纹不在白名单中");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> normalizeFingerprints(List<String> fingerprints) {
|
|
||||||
if (fingerprints == null) {
|
|
||||||
return Set.of();
|
|
||||||
}
|
|
||||||
return fingerprints.stream()
|
|
||||||
.map(this::normalizeFingerprint)
|
|
||||||
.filter(StringUtils::hasText)
|
|
||||||
.collect(Collectors.toUnmodifiableSet());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalizeFingerprint(String fingerprint) {
|
|
||||||
if (!StringUtils.hasText(fingerprint)) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return fingerprint.replaceAll("[^0-9A-Fa-f]", "").toUpperCase(Locale.ROOT);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,6 +77,9 @@ SET password_salt = 'a6bea7601138e90a540b39c73d53df81',
|
|||||||
WHERE role_code = 'OPS_ADMIN'
|
WHERE role_code = 'OPS_ADMIN'
|
||||||
AND uid = 1;
|
AND uid = 1;
|
||||||
|
|
||||||
|
ALTER TABLE tms_auth_full_account
|
||||||
|
DROP COLUMN password_hash;
|
||||||
|
|
||||||
-- 如果这里失败,说明旧库中存在不属于上述固定席位的认证账号,需要先明确其迁移策略。
|
-- 如果这里失败,说明旧库中存在不属于上述固定席位的认证账号,需要先明确其迁移策略。
|
||||||
ALTER TABLE tms_auth_full_account
|
ALTER TABLE tms_auth_full_account
|
||||||
MODIFY COLUMN password_digest VARCHAR(64) NOT NULL COMMENT '口令静态摘要:SM3(password_salt + 原始口令)';
|
MODIFY COLUMN password_digest VARCHAR(64) NOT NULL COMMENT '口令静态摘要:SM3(password_salt + 原始口令)';
|
||||||
|
|||||||
@ -0,0 +1,126 @@
|
|||||||
|
package com.cisd.tms.modules.cert.service;
|
||||||
|
|
||||||
|
import com.cisd.tms.common.exception.BizException;
|
||||||
|
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.util.encoders.Hex;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.spec.ECGenParameterSpec;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class FileRootCertSourceVerifierTest {
|
||||||
|
|
||||||
|
static {
|
||||||
|
if (Security.getProvider("BC") == null) {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path deviceDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAcceptRootCertWhenPublicKeySm3AndSignatureAreValid() throws Exception {
|
||||||
|
KeyPair keyPair = sm2KeyPair();
|
||||||
|
byte[] certData = "trusted-root-cert".getBytes(StandardCharsets.UTF_8);
|
||||||
|
writeTrustFiles(keyPair, certData);
|
||||||
|
|
||||||
|
FileRootCertSourceVerifier verifier = new FileRootCertSourceVerifier(deviceDir);
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> verifier.verifyTrustedSource(certData));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReadTrustFilesFromConfiguredPaths() throws Exception {
|
||||||
|
KeyPair keyPair = sm2KeyPair();
|
||||||
|
byte[] certData = "trusted-root-cert".getBytes(StandardCharsets.UTF_8);
|
||||||
|
Path configuredSm3Path = deviceDir.resolve("custom-dev.mac.pub.sm3");
|
||||||
|
Path configuredPublicKeyPath = deviceDir.resolve("custom-mac_sm2_public_key.pemy");
|
||||||
|
Path configuredSignPath = deviceDir.resolve("custom-rootcert.sign");
|
||||||
|
Files.writeString(configuredPublicKeyPath, toPublicKeyPem(keyPair), StandardCharsets.UTF_8);
|
||||||
|
Files.writeString(configuredSm3Path, sm3Hex(keyPair.getPublic().getEncoded()), StandardCharsets.UTF_8);
|
||||||
|
Files.write(configuredSignPath, sign(certData, keyPair));
|
||||||
|
|
||||||
|
FileRootCertSourceVerifier verifier = new FileRootCertSourceVerifier(
|
||||||
|
configuredSm3Path,
|
||||||
|
configuredPublicKeyPath,
|
||||||
|
configuredSignPath
|
||||||
|
);
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> verifier.verifyTrustedSource(certData));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectRootCertWhenPublicKeySm3DoesNotMatch() throws Exception {
|
||||||
|
KeyPair keyPair = sm2KeyPair();
|
||||||
|
byte[] certData = "trusted-root-cert".getBytes(StandardCharsets.UTF_8);
|
||||||
|
writeTrustFiles(keyPair, certData);
|
||||||
|
Files.writeString(deviceDir.resolve("dev.mac.pub.sm3"), "00", StandardCharsets.UTF_8);
|
||||||
|
FileRootCertSourceVerifier verifier = new FileRootCertSourceVerifier(deviceDir);
|
||||||
|
|
||||||
|
BizException exception = assertThrows(BizException.class, () -> verifier.verifyTrustedSource(certData));
|
||||||
|
|
||||||
|
assertEquals("根证书来源不可信", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectRootCertWhenSignatureDoesNotMatch() throws Exception {
|
||||||
|
KeyPair keyPair = sm2KeyPair();
|
||||||
|
byte[] certData = "trusted-root-cert".getBytes(StandardCharsets.UTF_8);
|
||||||
|
writeTrustFiles(keyPair, certData);
|
||||||
|
FileRootCertSourceVerifier verifier = new FileRootCertSourceVerifier(deviceDir);
|
||||||
|
|
||||||
|
BizException exception = assertThrows(
|
||||||
|
BizException.class,
|
||||||
|
() -> verifier.verifyTrustedSource("tampered".getBytes(StandardCharsets.UTF_8))
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("根证书来源不可信", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeTrustFiles(KeyPair keyPair, byte[] certData) throws Exception {
|
||||||
|
Files.writeString(deviceDir.resolve("mac_sm2_public_key.pemy"), toPublicKeyPem(keyPair), StandardCharsets.UTF_8);
|
||||||
|
Files.writeString(deviceDir.resolve("dev.mac.pub.sm3"), sm3Hex(keyPair.getPublic().getEncoded()), StandardCharsets.UTF_8);
|
||||||
|
Files.write(deviceDir.resolve("rootcert.sign"), sign(certData, keyPair));
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyPair sm2KeyPair() throws Exception {
|
||||||
|
KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", "BC");
|
||||||
|
generator.initialize(new ECGenParameterSpec("sm2p256v1"));
|
||||||
|
return generator.generateKeyPair();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] sign(byte[] data, KeyPair keyPair) throws Exception {
|
||||||
|
Signature signature = Signature.getInstance("SM3withSM2", "BC");
|
||||||
|
signature.initSign(keyPair.getPrivate());
|
||||||
|
signature.update(data);
|
||||||
|
return signature.sign();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toPublicKeyPem(KeyPair keyPair) {
|
||||||
|
String base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8))
|
||||||
|
.encodeToString(keyPair.getPublic().getEncoded());
|
||||||
|
return "-----BEGIN PUBLIC KEY-----\n" + base64 + "\n-----END PUBLIC KEY-----\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sm3Hex(byte[] data) {
|
||||||
|
SM3Digest digest = new SM3Digest();
|
||||||
|
digest.update(data, 0, data.length);
|
||||||
|
byte[] result = new byte[digest.getDigestSize()];
|
||||||
|
digest.doFinal(result, 0);
|
||||||
|
return Hex.toHexString(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ import org.mockito.Mockito;
|
|||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
@ -30,7 +31,6 @@ import java.time.Instant;
|
|||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import javax.security.auth.x500.X500Principal;
|
import javax.security.auth.x500.X500Principal;
|
||||||
|
|
||||||
@ -195,7 +195,12 @@ class TrustedCertServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldPersistRsaAlgorithmWhenImportingRsaTrustedCa() throws Exception {
|
void shouldPersistRsaAlgorithmWhenImportingRsaTrustedCa() throws Exception {
|
||||||
TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class);
|
TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class);
|
||||||
TrustedCertService service = new TrustedCertService(trustedCertRepository, Mockito.mock(CertificateRepository.class));
|
RootCertSourceVerifier rootCertSourceVerifier = Mockito.mock(RootCertSourceVerifier.class);
|
||||||
|
TrustedCertService service = new TrustedCertService(
|
||||||
|
trustedCertRepository,
|
||||||
|
Mockito.mock(CertificateRepository.class),
|
||||||
|
rootCertSourceVerifier
|
||||||
|
);
|
||||||
KeyPair rootKeyPair = rsaKeyPair();
|
KeyPair rootKeyPair = rsaKeyPair();
|
||||||
X509Certificate root = rsaCertificate(
|
X509Certificate root = rsaCertificate(
|
||||||
rootKeyPair,
|
rootKeyPair,
|
||||||
@ -210,14 +215,16 @@ class TrustedCertServiceTest {
|
|||||||
|
|
||||||
service.importTrusted(multipart(root), "rsa-root");
|
service.importTrusted(multipart(root), "rsa-root");
|
||||||
|
|
||||||
|
Mockito.verify(rootCertSourceVerifier).verifyTrustedSource(CertPemSupport.toPem(root).getBytes(StandardCharsets.UTF_8));
|
||||||
org.mockito.ArgumentCaptor<TrustedCertEntity> captor = org.mockito.ArgumentCaptor.forClass(TrustedCertEntity.class);
|
org.mockito.ArgumentCaptor<TrustedCertEntity> captor = org.mockito.ArgumentCaptor.forClass(TrustedCertEntity.class);
|
||||||
Mockito.verify(trustedCertRepository).save(captor.capture());
|
Mockito.verify(trustedCertRepository).save(captor.capture());
|
||||||
assertEquals("RSA", captor.getValue().getAlgoType());
|
assertEquals("RSA", captor.getValue().getAlgoType());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldAllowRootCaWhenFingerprintMatchesWhitelist() throws Exception {
|
void shouldAllowRootCaWhenSourceVerifierAccepts() throws Exception {
|
||||||
TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class);
|
TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class);
|
||||||
|
RootCertSourceVerifier rootCertSourceVerifier = Mockito.mock(RootCertSourceVerifier.class);
|
||||||
KeyPair rootKeyPair = sm2KeyPair();
|
KeyPair rootKeyPair = sm2KeyPair();
|
||||||
X509Certificate root = certificate(
|
X509Certificate root = certificate(
|
||||||
rootKeyPair,
|
rootKeyPair,
|
||||||
@ -230,19 +237,21 @@ class TrustedCertServiceTest {
|
|||||||
TrustedCertService service = new TrustedCertService(
|
TrustedCertService service = new TrustedCertService(
|
||||||
trustedCertRepository,
|
trustedCertRepository,
|
||||||
Mockito.mock(CertificateRepository.class),
|
Mockito.mock(CertificateRepository.class),
|
||||||
List.of(colonFingerprint(CertPemSupport.sha256Fingerprint(root)).toLowerCase(Locale.ROOT))
|
rootCertSourceVerifier
|
||||||
);
|
);
|
||||||
Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty());
|
Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty());
|
||||||
Mockito.when(trustedCertRepository.save(Mockito.any())).thenAnswer(invocation -> invocation.getArgument(0));
|
Mockito.when(trustedCertRepository.save(Mockito.any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
service.importTrusted(multipart(root), "root");
|
service.importTrusted(multipart(root), "root");
|
||||||
|
|
||||||
|
Mockito.verify(rootCertSourceVerifier).verifyTrustedSource(CertPemSupport.toPem(root).getBytes(StandardCharsets.UTF_8));
|
||||||
Mockito.verify(trustedCertRepository).save(Mockito.any());
|
Mockito.verify(trustedCertRepository).save(Mockito.any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectRootCaWhenFingerprintDoesNotMatchWhitelist() throws Exception {
|
void shouldRejectRootCaWhenSourceVerifierRejects() throws Exception {
|
||||||
TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class);
|
TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class);
|
||||||
|
RootCertSourceVerifier rootCertSourceVerifier = Mockito.mock(RootCertSourceVerifier.class);
|
||||||
KeyPair rootKeyPair = sm2KeyPair();
|
KeyPair rootKeyPair = sm2KeyPair();
|
||||||
X509Certificate root = certificate(
|
X509Certificate root = certificate(
|
||||||
rootKeyPair,
|
rootKeyPair,
|
||||||
@ -255,13 +264,16 @@ class TrustedCertServiceTest {
|
|||||||
TrustedCertService service = new TrustedCertService(
|
TrustedCertService service = new TrustedCertService(
|
||||||
trustedCertRepository,
|
trustedCertRepository,
|
||||||
Mockito.mock(CertificateRepository.class),
|
Mockito.mock(CertificateRepository.class),
|
||||||
List.of("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
rootCertSourceVerifier
|
||||||
);
|
);
|
||||||
Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty());
|
Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty());
|
||||||
|
Mockito.doThrow(new BizException(400, "根证书来源不可信"))
|
||||||
|
.when(rootCertSourceVerifier)
|
||||||
|
.verifyTrustedSource(Mockito.any());
|
||||||
|
|
||||||
BizException exception = assertThrows(BizException.class, () -> service.importTrusted(multipart(root), "root"));
|
BizException exception = assertThrows(BizException.class, () -> service.importTrusted(multipart(root), "root"));
|
||||||
|
|
||||||
assertEquals("根CA证书指纹不在白名单中", exception.getMessage());
|
assertEquals("根证书来源不可信", exception.getMessage());
|
||||||
Mockito.verify(trustedCertRepository, Mockito.never()).save(Mockito.any());
|
Mockito.verify(trustedCertRepository, Mockito.never()).save(Mockito.any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,11 +361,7 @@ class TrustedCertServiceTest {
|
|||||||
"file",
|
"file",
|
||||||
"cert.pem",
|
"cert.pem",
|
||||||
"application/x-pem-file",
|
"application/x-pem-file",
|
||||||
CertPemSupport.toPem(certificate).getBytes()
|
CertPemSupport.toPem(certificate).getBytes(StandardCharsets.UTF_8)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String colonFingerprint(String fingerprint) {
|
|
||||||
return fingerprint.replaceAll("(.{2})(?=.)", "$1:");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user