From f06c675f9b0961cf4d8d516496f0ce094a3de434 Mon Sep 17 00:00:00 2001 From: waner Date: Thu, 28 May 2026 10:11:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=A0=B9=E8=AF=81=E4=B9=A6=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E5=A2=9E=E5=8A=A0=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../modules/cert/config/CertProperties.java | 49 +++++-- .../service/FileRootCertSourceVerifier.java | 121 +++++++++++++++++ .../cert/service/RootCertSourceVerifier.java | 6 + .../cert/service/TrustedCertService.java | 56 ++------ .../db/migration/V1__tms_schema_full.sql | 3 + .../FileRootCertSourceVerifierTest.java | 126 ++++++++++++++++++ .../cert/service/TrustedCertServiceTest.java | 32 +++-- 8 files changed, 324 insertions(+), 71 deletions(-) create mode 100644 src/main/java/com/cisd/tms/modules/cert/service/FileRootCertSourceVerifier.java create mode 100644 src/main/java/com/cisd/tms/modules/cert/service/RootCertSourceVerifier.java create mode 100644 src/test/java/com/cisd/tms/modules/cert/service/FileRootCertSourceVerifierTest.java diff --git a/README.md b/README.md index dee0a05..aee7739 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ Open: - `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 metadata 和 CRL 内每条 revoked certificate 明细;原始 CRL 文件只作为临时文件参与解析,任务结束后删除,不作为业务数据长期保存。 - CRL issuer 通过可信 CA 的 issuer DN 候选和实际 CRL 签名验证解析。 diff --git a/src/main/java/com/cisd/tms/modules/cert/config/CertProperties.java b/src/main/java/com/cisd/tms/modules/cert/config/CertProperties.java index e19a8ae..80ce7cb 100644 --- a/src/main/java/com/cisd/tms/modules/cert/config/CertProperties.java +++ b/src/main/java/com/cisd/tms/modules/cert/config/CertProperties.java @@ -2,22 +2,49 @@ package com.cisd.tms.modules.cert.config; import org.springframework.boot.context.properties.ConfigurationProperties; -import java.util.ArrayList; -import java.util.List; - @ConfigurationProperties(prefix = "tms.cert") public class CertProperties { - /** - * 允许导入为信任锚的根 CA 证书 SHA-256 指纹。为空时保持兼容,不限制根 CA 导入。 - */ - private List trustedRootFingerprints = new ArrayList<>(); + private RootCertSource rootCertSource = new RootCertSource(); - public List getTrustedRootFingerprints() { - return trustedRootFingerprints; + public RootCertSource getRootCertSource() { + return rootCertSource; } - public void setTrustedRootFingerprints(List trustedRootFingerprints) { - this.trustedRootFingerprints = trustedRootFingerprints == null ? new ArrayList<>() : trustedRootFingerprints; + public void setRootCertSource(RootCertSource rootCertSource) { + 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; + } } } diff --git a/src/main/java/com/cisd/tms/modules/cert/service/FileRootCertSourceVerifier.java b/src/main/java/com/cisd/tms/modules/cert/service/FileRootCertSourceVerifier.java new file mode 100644 index 0000000..08680b1 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/service/FileRootCertSourceVerifier.java @@ -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); + } +} diff --git a/src/main/java/com/cisd/tms/modules/cert/service/RootCertSourceVerifier.java b/src/main/java/com/cisd/tms/modules/cert/service/RootCertSourceVerifier.java new file mode 100644 index 0000000..eb7d7dc --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/cert/service/RootCertSourceVerifier.java @@ -0,0 +1,6 @@ +package com.cisd.tms.modules.cert.service; + +public interface RootCertSourceVerifier { + + void verifyTrustedSource(byte[] certData); +} diff --git a/src/main/java/com/cisd/tms/modules/cert/service/TrustedCertService.java b/src/main/java/com/cisd/tms/modules/cert/service/TrustedCertService.java index b9385a0..5446c00 100644 --- a/src/main/java/com/cisd/tms/modules/cert/service/TrustedCertService.java +++ b/src/main/java/com/cisd/tms/modules/cert/service/TrustedCertService.java @@ -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.TrustedCertItemResponse; 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.repository.CertificateRepository; 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.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @@ -28,10 +26,7 @@ import java.security.cert.X509CRL; import java.security.cert.X509Certificate; import java.time.LocalDateTime; import java.time.ZoneId; -import java.util.Locale; import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; @Service public class TrustedCertService { @@ -47,32 +42,25 @@ public class TrustedCertService { private final TrustedCertRepository trustedCertRepository; private final CertificateRepository certificateRepository; - private final Set trustedRootFingerprints; - - public TrustedCertService( - TrustedCertRepository trustedCertRepository, - CertificateRepository certificateRepository - ) { - this(trustedCertRepository, certificateRepository, List.of()); - } + private final RootCertSourceVerifier rootCertSourceVerifier; @Autowired public TrustedCertService( TrustedCertRepository trustedCertRepository, CertificateRepository certificateRepository, - CertProperties certProperties + RootCertSourceVerifier rootCertSourceVerifier ) { - this(trustedCertRepository, certificateRepository, certProperties.getTrustedRootFingerprints()); + this.trustedCertRepository = trustedCertRepository; + this.certificateRepository = certificateRepository; + this.rootCertSourceVerifier = rootCertSourceVerifier; } TrustedCertService( TrustedCertRepository trustedCertRepository, - CertificateRepository certificateRepository, - List trustedRootFingerprints + CertificateRepository certificateRepository ) { - this.trustedCertRepository = trustedCertRepository; - this.certificateRepository = certificateRepository; - this.trustedRootFingerprints = normalizeFingerprints(trustedRootFingerprints); + this(trustedCertRepository, certificateRepository, certData -> { + }); } public Page list(TrustedCertListRequest request) { @@ -111,7 +99,7 @@ public class TrustedCertService { } int caLevel = isSelfSigned(certificate) ? 0 : 1; if (caLevel == 0) { - assertRootFingerprintAllowed(fingerprint); + rootCertSourceVerifier.verifyTrustedSource(fileData); // 根 CA 必须能自签自验,防止把 subject=issuer 的伪造证书放入信任锚。 certificate.verify(certificate.getPublicKey(), "BC"); } else { @@ -312,30 +300,4 @@ public class TrustedCertService { private boolean isSelfSigned(X509Certificate certificate) { 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 normalizeFingerprints(List 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); - } } diff --git a/src/main/resources/db/migration/V1__tms_schema_full.sql b/src/main/resources/db/migration/V1__tms_schema_full.sql index aa107ff..33c0890 100644 --- a/src/main/resources/db/migration/V1__tms_schema_full.sql +++ b/src/main/resources/db/migration/V1__tms_schema_full.sql @@ -77,6 +77,9 @@ SET password_salt = 'a6bea7601138e90a540b39c73d53df81', WHERE role_code = 'OPS_ADMIN' AND uid = 1; +ALTER TABLE tms_auth_full_account +DROP COLUMN password_hash; + -- 如果这里失败,说明旧库中存在不属于上述固定席位的认证账号,需要先明确其迁移策略。 ALTER TABLE tms_auth_full_account MODIFY COLUMN password_digest VARCHAR(64) NOT NULL COMMENT '口令静态摘要:SM3(password_salt + 原始口令)'; diff --git a/src/test/java/com/cisd/tms/modules/cert/service/FileRootCertSourceVerifierTest.java b/src/test/java/com/cisd/tms/modules/cert/service/FileRootCertSourceVerifierTest.java new file mode 100644 index 0000000..7e10caa --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/cert/service/FileRootCertSourceVerifierTest.java @@ -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); + } +} diff --git a/src/test/java/com/cisd/tms/modules/cert/service/TrustedCertServiceTest.java b/src/test/java/com/cisd/tms/modules/cert/service/TrustedCertServiceTest.java index a9a377e..265e00d 100644 --- a/src/test/java/com/cisd/tms/modules/cert/service/TrustedCertServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/cert/service/TrustedCertServiceTest.java @@ -20,6 +20,7 @@ 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; @@ -30,7 +31,6 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.List; -import java.util.Locale; import java.util.Optional; import javax.security.auth.x500.X500Principal; @@ -195,7 +195,12 @@ class TrustedCertServiceTest { @Test void shouldPersistRsaAlgorithmWhenImportingRsaTrustedCa() throws Exception { 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(); X509Certificate root = rsaCertificate( rootKeyPair, @@ -210,14 +215,16 @@ class TrustedCertServiceTest { service.importTrusted(multipart(root), "rsa-root"); + Mockito.verify(rootCertSourceVerifier).verifyTrustedSource(CertPemSupport.toPem(root).getBytes(StandardCharsets.UTF_8)); org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(TrustedCertEntity.class); Mockito.verify(trustedCertRepository).save(captor.capture()); assertEquals("RSA", captor.getValue().getAlgoType()); } @Test - void shouldAllowRootCaWhenFingerprintMatchesWhitelist() throws Exception { + void shouldAllowRootCaWhenSourceVerifierAccepts() throws Exception { TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class); + RootCertSourceVerifier rootCertSourceVerifier = Mockito.mock(RootCertSourceVerifier.class); KeyPair rootKeyPair = sm2KeyPair(); X509Certificate root = certificate( rootKeyPair, @@ -230,19 +237,21 @@ class TrustedCertServiceTest { TrustedCertService service = new TrustedCertService( trustedCertRepository, 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.save(Mockito.any())).thenAnswer(invocation -> invocation.getArgument(0)); service.importTrusted(multipart(root), "root"); + Mockito.verify(rootCertSourceVerifier).verifyTrustedSource(CertPemSupport.toPem(root).getBytes(StandardCharsets.UTF_8)); Mockito.verify(trustedCertRepository).save(Mockito.any()); } @Test - void shouldRejectRootCaWhenFingerprintDoesNotMatchWhitelist() throws Exception { + void shouldRejectRootCaWhenSourceVerifierRejects() throws Exception { TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class); + RootCertSourceVerifier rootCertSourceVerifier = Mockito.mock(RootCertSourceVerifier.class); KeyPair rootKeyPair = sm2KeyPair(); X509Certificate root = certificate( rootKeyPair, @@ -255,13 +264,16 @@ class TrustedCertServiceTest { TrustedCertService service = new TrustedCertService( trustedCertRepository, Mockito.mock(CertificateRepository.class), - List.of("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + rootCertSourceVerifier ); 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")); - assertEquals("根CA证书指纹不在白名单中", exception.getMessage()); + assertEquals("根证书来源不可信", exception.getMessage()); Mockito.verify(trustedCertRepository, Mockito.never()).save(Mockito.any()); } @@ -349,11 +361,7 @@ class TrustedCertServiceTest { "file", "cert.pem", "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:"); - } }