feat: add issuance sm3 sm2 teaching service

agent/payment-training-progress
chenyuan 4 weeks ago
parent e5e897ce7a
commit c17cc29db2

@ -66,6 +66,11 @@
<artifactId>springdoc-openapi-ui</artifactId> <artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc.version}</version> <version>${springdoc.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.84</version>
</dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>

@ -0,0 +1,56 @@
package com.yau.digitalrmb.issuance.domain.service;
import com.yau.digitalrmb.issuance.domain.model.DenominationItem;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
public final class IssuanceMessageComposer {
public String compose(String bankCode, String organizationId, BigDecimal totalAmount,
List<DenominationItem> denominations, String currency, String timestamp) {
requireNonNull(bankCode, "bankCode");
requireNonNull(organizationId, "organizationId");
requireNonNull(totalAmount, "totalAmount");
requireNonNull(denominations, "denominations");
requireNonNull(currency, "currency");
requireNonNull(timestamp, "timestamp");
List<DenominationItem> orderedDenominations = new ArrayList<DenominationItem>(denominations);
for (DenominationItem denomination : orderedDenominations) {
requireNonNull(denomination, "denomination item");
}
Collections.sort(orderedDenominations, new Comparator<DenominationItem>() {
@Override
public int compare(DenominationItem left, DenominationItem right) {
return Integer.compare(right.getDenomination(), left.getDenomination());
}
});
StringBuilder denominationText = new StringBuilder();
for (DenominationItem denomination : orderedDenominations) {
if (denominationText.length() > 0) {
denominationText.append(',');
}
denominationText.append(denomination.getDenomination())
.append(':')
.append(denomination.getQuantity());
}
return "ISSUE|" + bankCode
+ "|" + organizationId
+ "|" + totalAmount.setScale(2, RoundingMode.UNNECESSARY).toPlainString()
+ "|" + denominationText
+ "|" + currency
+ "|" + timestamp;
}
private static void requireNonNull(Object value, String name) {
Objects.requireNonNull(value, name + " must not be null");
}
}

@ -0,0 +1,8 @@
package com.yau.digitalrmb.issuance.domain.service;
public interface IssuanceSignatureService {
SignedIssuancePayload sign(String keyRef, String plainText);
boolean verify(String keyRef, String plainText, SignedIssuancePayload payload);
}

@ -0,0 +1,28 @@
package com.yau.digitalrmb.issuance.domain.service;
import java.util.Objects;
public final class SignedIssuancePayload {
private final String digest;
private final String signature;
private final String signingKeyRef;
public SignedIssuancePayload(String digest, String signature, String signingKeyRef) {
this.digest = Objects.requireNonNull(digest, "digest must not be null");
this.signature = Objects.requireNonNull(signature, "signature must not be null");
this.signingKeyRef = Objects.requireNonNull(signingKeyRef, "signingKeyRef must not be null");
}
public String getDigest() {
return digest;
}
public String getSignature() {
return signature;
}
public String getSigningKeyRef() {
return signingKeyRef;
}
}

@ -0,0 +1,79 @@
package com.yau.digitalrmb.issuance.infrastructure.crypto;
import com.yau.digitalrmb.issuance.domain.service.IssuanceSignatureService;
import com.yau.digitalrmb.issuance.domain.service.SignedIssuancePayload;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;
import java.util.Objects;
public final class BouncyCastleIssuanceSignatureService implements IssuanceSignatureService {
private static final char[] HEX = "0123456789ABCDEF".toCharArray();
private final InMemorySm2SigningKeyProvider keyProvider;
public BouncyCastleIssuanceSignatureService() {
this(new InMemorySm2SigningKeyProvider());
}
public BouncyCastleIssuanceSignatureService(InMemorySm2SigningKeyProvider keyProvider) {
this.keyProvider = Objects.requireNonNull(keyProvider, "keyProvider must not be null");
InMemorySm2SigningKeyProvider.ensureBouncyCastleProvider();
}
@Override
public SignedIssuancePayload sign(String keyRef, String plainText) {
requireNonNull(keyRef, "keyRef");
requireNonNull(plainText, "plainText");
byte[] textBytes = plainText.getBytes(StandardCharsets.UTF_8);
String digest = digest(textBytes);
String signature = Base64.getEncoder().encodeToString(keyProvider.sign(keyRef, textBytes));
return new SignedIssuancePayload(digest, signature, keyRef);
}
@Override
public boolean verify(String keyRef, String plainText, SignedIssuancePayload payload) {
requireNonNull(keyRef, "keyRef");
requireNonNull(plainText, "plainText");
requireNonNull(payload, "payload");
PublicKey publicKey = keyProvider.publicKeyFor(keyRef);
if (!keyRef.equals(payload.getSigningKeyRef())) {
return false;
}
try {
Signature signature = Signature.getInstance("SM3withSM2", "BC");
signature.initVerify(publicKey);
signature.update(plainText.getBytes(StandardCharsets.UTF_8));
return signature.verify(Base64.getDecoder().decode(payload.getSignature()));
} catch (GeneralSecurityException exception) {
throw new IllegalStateException("Unable to verify the SM2 signature", exception);
}
}
private static String digest(byte[] textBytes) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SM3", "BC");
return toUpperCaseHex(messageDigest.digest(textBytes));
} catch (GeneralSecurityException exception) {
throw new IllegalStateException("Unable to calculate the SM3 digest", exception);
}
}
private static String toUpperCaseHex(byte[] bytes) {
char[] characters = new char[bytes.length * 2];
for (int index = 0; index < bytes.length; index++) {
int value = bytes[index] & 0xFF;
characters[index * 2] = HEX[value >>> 4];
characters[index * 2 + 1] = HEX[value & 0x0F];
}
return new String(characters);
}
private static void requireNonNull(Object value, String name) {
Objects.requireNonNull(value, name + " must not be null");
}
}

@ -0,0 +1,71 @@
package com.yau.digitalrmb.issuance.infrastructure.crypto;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.security.spec.ECGenParameterSpec;
import java.util.Objects;
public final class InMemorySm2SigningKeyProvider {
public static final String TEACHING_KEY_REF = "sm2-key-02";
private static final String PROVIDER_NAME = BouncyCastleProvider.PROVIDER_NAME;
private final KeyPair keyPair;
public InMemorySm2SigningKeyProvider() {
ensureBouncyCastleProvider();
this.keyPair = generateKeyPair();
}
public byte[] sign(String keyRef, byte[] plainText) {
requireKnownKey(keyRef);
Objects.requireNonNull(plainText, "plainText must not be null");
try {
Signature signature = Signature.getInstance("SM3withSM2", PROVIDER_NAME);
signature.initSign(keyPair.getPrivate());
signature.update(plainText);
return signature.sign();
} catch (GeneralSecurityException exception) {
throw new IllegalStateException("Unable to sign with the in-memory SM2 key", exception);
}
}
public PublicKey publicKeyFor(String keyRef) {
requireKnownKey(keyRef);
return keyPair.getPublic();
}
static void ensureBouncyCastleProvider() {
if (Security.getProvider(PROVIDER_NAME) != null) {
return;
}
synchronized (InMemorySm2SigningKeyProvider.class) {
if (Security.getProvider(PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
}
private KeyPair generateKeyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", PROVIDER_NAME);
keyPairGenerator.initialize(new ECGenParameterSpec("sm2p256v1"), new SecureRandom());
return keyPairGenerator.generateKeyPair();
} catch (GeneralSecurityException exception) {
throw new IllegalStateException("Unable to generate the in-memory SM2 key", exception);
}
}
private static void requireKnownKey(String keyRef) {
Objects.requireNonNull(keyRef, "keyRef must not be null");
if (!TEACHING_KEY_REF.equals(keyRef)) {
throw new IllegalArgumentException("Unknown SM2 key reference: " + keyRef);
}
}
}

@ -0,0 +1,62 @@
package com.yau.digitalrmb.issuance.infrastructure.crypto;
import com.yau.digitalrmb.issuance.domain.model.DenominationItem;
import com.yau.digitalrmb.issuance.domain.service.IssuanceMessageComposer;
import com.yau.digitalrmb.issuance.domain.service.SignedIssuancePayload;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class BouncyCastleIssuanceSignatureServiceTest {
private static final String KEY_REF = "sm2-key-02";
private static final String PLAIN_TEXT =
"ISSUE|BKCHCNBJ00001|ORG_3A4B5C6D7E8F|50000.00|100:400,50:100|DC|20260801103218";
private final IssuanceMessageComposer composer = new IssuanceMessageComposer();
private final BouncyCastleIssuanceSignatureService service =
new BouncyCastleIssuanceSignatureService(new InMemorySm2SigningKeyProvider());
@Test
void composesPrototypeCompatiblePlainTextInDescendingDenominationOrder() {
assertThat(composer.compose(
"BKCHCNBJ00001",
"ORG_3A4B5C6D7E8F",
new BigDecimal("50000.00"),
Arrays.asList(new DenominationItem(50, 100), new DenominationItem(100, 400)),
"DC",
"20260801103218"))
.isEqualTo(PLAIN_TEXT);
}
@Test
void signsWithSm3AndSm2UsingTheTeachingKey() {
SignedIssuancePayload payload = service.sign(KEY_REF, PLAIN_TEXT);
assertThat(payload.getDigest()).matches("[0-9A-F]{64}");
assertThat(payload.getSignature()).isNotBlank();
assertThat(payload.getSigningKeyRef()).isEqualTo(KEY_REF);
assertThat(service.verify(KEY_REF, PLAIN_TEXT, payload)).isTrue();
assertThat(service.verify(KEY_REF, PLAIN_TEXT + "0", payload)).isFalse();
}
@Test
void rejectsUnknownKeyReferences() {
assertThatThrownBy(() -> service.sign("sm2-key-unknown", PLAIN_TEXT))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Unknown SM2 key reference");
}
@Test
void rejectsUnknownKeyReferencesDuringVerification() {
SignedIssuancePayload payload = service.sign(KEY_REF, PLAIN_TEXT);
assertThatThrownBy(() -> service.verify("sm2-key-unknown", PLAIN_TEXT, payload))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Unknown SM2 key reference");
}
}
Loading…
Cancel
Save