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");