diff --git a/README.en.md b/README.en.md index f5291a9..75badc3 100644 --- a/README.en.md +++ b/README.en.md @@ -57,7 +57,7 @@ Supported suites: `WOP-RSA2048-SHA256` / `WOP-RSA3072-SHA256` / `WOP-RSA4096-SHA ```java WopClient client = WopClient.builder() .appKey("app_001") - .suite("WOP-RSA3072-SHA256") // or WOP-RSA2048/4096-SHA256 / WOP-SM2-SM3 + .suite("WOP-RSA2048-SHA256") // or WOP-RSA2048/4096-SHA256 / WOP-SM2-SM3 .merchantPrivateKey(merchantPrivateKey) // PEM or single-line Base64 .platformPublicKey(platformPublicKey) .build(); diff --git a/README.md b/README.md index 6ffd03b..680a411 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ VerifyResult result = WopClient.defaultClient().execute( ```java WopClient client = WopClient.builder() - .appKey("app_001").suite("WOP-RSA3072-SHA256") + .appKey("app_001").suite("WOP-RSA2048-SHA256") .merchantPrivateKey(merchantPrivateKey).platformPublicKey(platformPublicKey) .serverRoot("https://gw.example.com/gateway") .build(); @@ -107,7 +107,7 @@ WopClient client = WopClient.builder() ```java WopClient client = WopClient.builder() .appKey("app_001") - .suite("WOP-RSA3072-SHA256") // 或 WOP-RSA2048/4096-SHA256 / WOP-SM2-SM3 + .suite("WOP-RSA2048-SHA256") // 或 WOP-RSA3072/4096-SHA256 / WOP-SM2-SM3 .merchantPrivateKey(merchantPrivateKey) // PEM 或 Base64 单行 .platformPublicKey(platformPublicKey) .build(); @@ -166,7 +166,7 @@ mvn verify ``` 向量覆盖面(字节级断言 + 全负向量):SHA-256/SM3 摘要与 digest 头、AES-256-GCM/SM4-GCM -固定 key/IV 密文、RSA3072/4096 确定性签名、SM2 固定 k 签名(r‖s 64B)、OAEP 解包与 +固定 key/IV 密文、RSA2048/3072/4096 确定性签名、SM2 固定 k 签名(r‖s 64B)、OAEP 解包与 MGF1-SHA-1 陷阱、SM2 C1C3C2 解密与 C1C2C3 拒收、DEK 载荷、digest 头格式规则(双空格/ 大写 hex/长度/跨族)、严格无填充 base64url。 diff --git a/config/wopSdkConfigDefault.json b/config/wopSdkConfigDefault.json index d505f20..4fc8124 100644 --- a/config/wopSdkConfigDefault.json +++ b/config/wopSdkConfigDefault.json @@ -1,6 +1,6 @@ { "appKey": "YOUR_APP_KEY", - "suite": "WOP-RSA3072-SHA256", + "suite": "WOP-RSA2048-SHA256", "merchantPrivateKey": "YOUR_MERCHANT_PRIVATE_KEY", "platformPublicKey": "YOUR_PLATFORM_PUBLIC_KEY", "serverRoot": "https://gw.example.com/gateway", diff --git a/wop-sdk-core/pom.xml b/wop-sdk-core/pom.xml index 183b549..30303db 100644 --- a/wop-sdk-core/pom.xml +++ b/wop-sdk-core/pom.xml @@ -19,8 +19,8 @@ antrun 在 generate-test-resources 阶段按 commit SHA 从 raw.githubusercontent.com 拉取并逐字节 sha256 校验,不一致即构建失败(fail-closed)。升级流程见 CONTRIBUTING §4:先合 wop-specs PR,再以下面三项同步新钉。 --> - 4a50824b3b9008fbda7a6c675209ccebc713f245 - 1cbb1d3c494109d58e80cfd450c4aaf4159f991029abb875514517a232fd8001 + a1f7feb5b4af5c9052149e2adfd2063caa102677 + c7dd65740a1da9a5cfb804bca0c8b1bd7d4ab967adeb075297677a4bef148736 c920ca1a93ccb3899a659f59fed6ec4652cf9e1b3b58bbdac23c45ac3ed2353e diff --git a/wop-sdk-core/src/main/resources/config/wopSdkConfigDefault.json b/wop-sdk-core/src/main/resources/config/wopSdkConfigDefault.json index d505f20..4fc8124 100644 --- a/wop-sdk-core/src/main/resources/config/wopSdkConfigDefault.json +++ b/wop-sdk-core/src/main/resources/config/wopSdkConfigDefault.json @@ -1,6 +1,6 @@ { "appKey": "YOUR_APP_KEY", - "suite": "WOP-RSA3072-SHA256", + "suite": "WOP-RSA2048-SHA256", "merchantPrivateKey": "YOUR_MERCHANT_PRIVATE_KEY", "platformPublicKey": "YOUR_PLATFORM_PUBLIC_KEY", "serverRoot": "https://gw.example.com/gateway", diff --git a/wop-sdk-core/src/test/java/com/wanlianyida/wop/WopClientConfigSpecTest.java b/wop-sdk-core/src/test/java/com/wanlianyida/wop/WopClientConfigSpecTest.java index 0c26472..fe7a5e8 100644 --- a/wop-sdk-core/src/test/java/com/wanlianyida/wop/WopClientConfigSpecTest.java +++ b/wop-sdk-core/src/test/java/com/wanlianyida/wop/WopClientConfigSpecTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -21,6 +22,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotSame; @@ -104,6 +106,56 @@ void defaultClientConcurrentInitCreatesSingleInstance() throws Exception { } } + @Test + void defaultClientLateContenderReusesInstanceInsideLock() throws Exception { + // 二次检查 false 弧(先持锁者已初始化实例、后进锁线程直接复用):纯竞速调度不保证命中 + // ——后醒线程快路径即可返回。反射持锁制造确定性交错补齐该弧(JaCoCo 分支门禁 100%)。 + WopSdkConfig config = new WopSdkConfig.Builder() + .appKey("app_late") + .suite("WOP-RSA3072-SHA256") + .merchantPrivateKey(RSA_PRIV) + .platformPublicKey(RSA_PUB) + .serverRoot("https://gw.example.com/gateway") + .transport(draft -> new TransportResponse(200, Collections.emptyMap(), new byte[0])) + .build(); + WopClient holder = WopClient.fromConfig(config); + WopClient.resetDefault(); + + Field lockField = WopClient.class.getDeclaredField("DEFAULT_CLIENT_LOCK"); + lockField.setAccessible(true); + Object lock = lockField.get(null); + Field instanceField = WopClient.class.getDeclaredField("defaultClientInstance"); + instanceField.setAccessible(true); + + ExecutorService pool = Executors.newSingleThreadExecutor(); + Future worker; + AtomicReference workerThread = new AtomicReference<>(); + CountDownLatch submitted = new CountDownLatch(1); + try { + synchronized (lock) { + worker = pool.submit(() -> { + workerThread.set(Thread.currentThread()); + submitted.countDown(); + return WopClient.defaultClient(); // 快路径读到 null,阻塞在锁入口 + }); + assertTrue(submitted.await(5, TimeUnit.SECONDS)); + awaitThreadState(workerThread.get(), Thread.State.BLOCKED); + instanceField.set(null, holder); // 模拟"他人持锁期间已完成初始化" + } // 释放锁:worker 进入二次检查,instance 已非空 → 复用 holder + assertSame(holder, worker.get(5, TimeUnit.SECONDS)); + } finally { + pool.shutdownNow(); + } + } + + private static void awaitThreadState(Thread thread, Thread.State expected) throws InterruptedException { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + while (thread.getState() != expected) { + assertTrue(System.nanoTime() < deadline, "worker 未进入 " + expected + " 状态"); + Thread.sleep(10); + } + } + @Test void resetDefaultReloadsNewConfigConcurrently() throws Exception { Path file = writeConfig("app_v1"); diff --git a/wop-sdk-core/src/test/java/com/wanlianyida/wop/crypto/KeyEncryptVectorTest.java b/wop-sdk-core/src/test/java/com/wanlianyida/wop/crypto/KeyEncryptVectorTest.java index a3e1f89..d249fd8 100644 --- a/wop-sdk-core/src/test/java/com/wanlianyida/wop/crypto/KeyEncryptVectorTest.java +++ b/wop-sdk-core/src/test/java/com/wanlianyida/wop/crypto/KeyEncryptVectorTest.java @@ -22,10 +22,21 @@ */ class KeyEncryptVectorTest { + private static final AlgorithmSuite RSA2048 = AlgorithmSuite.parse("WOP-RSA2048-SHA256"); private static final AlgorithmSuite RSA3072 = AlgorithmSuite.parse("WOP-RSA3072-SHA256"); private static final AlgorithmSuite RSA4096 = AlgorithmSuite.parse("WOP-RSA4096-SHA256"); private static final AlgorithmSuite SM2 = AlgorithmSuite.parse("WOP-SM2-SM3"); + @Test + void oaep2048UnwrapMatchesGoldenVector() { + JsonNode vector = TestVectors.firstById("keyEncrypt", "oaep2048-unwrap"); + PrivateKey priv = KeyCodec.parsePrivateKey(TestVectors.keys("rsa2048").path("privatePkcs8B64").asText(), RSA2048); + byte[] cipher = Codec.b64UrlDecode(vector.path("cipherB64u").asText()); + byte[] plain = RsaOaepKeyEncryptStrategy.INSTANCE.decrypt(cipher, priv); + // 字节级:解包明文 = DEK 载荷 + assertEquals(vector.path("expectedPlaintext").asText(), new String(plain, StandardCharsets.UTF_8)); + } + @Test void oaep3072UnwrapMatchesGoldenVector() { JsonNode vector = TestVectors.firstById("keyEncrypt", "oaep3072-unwrap"); diff --git a/wop-sdk-core/src/test/java/com/wanlianyida/wop/crypto/SignatureStrategyVectorTest.java b/wop-sdk-core/src/test/java/com/wanlianyida/wop/crypto/SignatureStrategyVectorTest.java index 429d447..28c7609 100644 --- a/wop-sdk-core/src/test/java/com/wanlianyida/wop/crypto/SignatureStrategyVectorTest.java +++ b/wop-sdk-core/src/test/java/com/wanlianyida/wop/crypto/SignatureStrategyVectorTest.java @@ -30,6 +30,7 @@ */ class SignatureStrategyVectorTest { + private static final AlgorithmSuite RSA2048 = AlgorithmSuite.parse("WOP-RSA2048-SHA256"); private static final AlgorithmSuite RSA3072 = AlgorithmSuite.parse("WOP-RSA3072-SHA256"); private static final AlgorithmSuite RSA4096 = AlgorithmSuite.parse("WOP-RSA4096-SHA256"); private static final AlgorithmSuite SM2 = AlgorithmSuite.parse("WOP-SM2-SM3"); @@ -49,6 +50,21 @@ void rsa3072SignMatchesGoldenVector() { assertTrue(RsaPkcs1SignatureStrategy.INSTANCE.verify(msg, sig, pub, Codec.utf8("x"))); } + @Test + void rsa2048SignMatchesGoldenVector() { + JsonNode vector = TestVectors.firstById("signature", "rsa2048-sign"); + byte[] msg = Codec.utf8(vector.path("message").asText()); + PrivateKey priv = KeyCodec.parsePrivateKey(TestVectors.keys("rsa2048").path("privatePkcs8B64").asText(), RSA2048); + PublicKey pub = KeyCodec.parsePublicKey(TestVectors.keys("rsa2048").path("publicSpkiB64").asText(), RSA2048); + + byte[] sig = RsaPkcs1SignatureStrategy.INSTANCE.sign(msg, priv, Codec.utf8("x")); + // 字节级:签名 = 向量;b64url 长度恒 342(2048 位) + assertEquals(vector.path("expectedSigB64u").asText(), Codec.b64UrlEncode(sig)); + assertEquals(vector.path("sigLenBytes").asInt(), sig.length); + assertEquals(vector.path("b64uLen").asInt(), Codec.b64UrlEncode(sig).length()); + assertTrue(RsaPkcs1SignatureStrategy.INSTANCE.verify(msg, sig, pub, Codec.utf8("x"))); + } + @Test void rsa4096SignMatchesGoldenVector() { JsonNode vector = TestVectors.firstById("signature", "rsa4096-sign");