fix: verify exchange coin control signatures

chore/issuance-comments-zh
chenyuan 2 weeks ago
parent b7000539ec
commit 2f0ae18116

@ -6,14 +6,24 @@ public final class ReservedCoin {
private final String currencyId; private final String currencyId;
private final BigDecimal denomination; private final BigDecimal denomination;
private final String status; private final String status;
private final String completeCurrency;
private final String cbCtrlSignature;
private final String cbFinalSignature;
public ReservedCoin(String currencyId, BigDecimal denomination, String status) { public ReservedCoin(String currencyId, BigDecimal denomination, String status,
String completeCurrency, String cbCtrlSignature, String cbFinalSignature) {
this.currencyId = currencyId; this.currencyId = currencyId;
this.denomination = denomination; this.denomination = denomination;
this.status = status; this.status = status;
this.completeCurrency = completeCurrency;
this.cbCtrlSignature = cbCtrlSignature;
this.cbFinalSignature = cbFinalSignature;
} }
public String getCurrencyId() { return currencyId; } public String getCurrencyId() { return currencyId; }
public BigDecimal getDenomination() { return denomination; } public BigDecimal getDenomination() { return denomination; }
public String getStatus() { return status; } public String getStatus() { return status; }
public String getCompleteCurrency() { return completeCurrency; }
public String getCbCtrlSignature() { return cbCtrlSignature; }
public String getCbFinalSignature() { return cbFinalSignature; }
} }

@ -290,10 +290,17 @@ public class JdbcExchangeResourceRepository implements ExchangeResourceRepositor
@Override @Override
public List<ReservedCoin> findReservedCoins(ExchangeOrder order) { public List<ReservedCoin> findReservedCoins(ExchangeOrder order) {
return jdbc.query("SELECT currency_id,denomination,status FROM exchange_coin_reservation WHERE exchange_id=? " + return jdbc.query("SELECT r.currency_id,r.denomination,r.status,c.complete_currency," +
"ORDER BY denomination DESC,currency_id", "d.control_system_signature AS cb_ctrl_signature," +
"c.issuance_ownership_signature AS cb_final_signature " +
"FROM exchange_coin_reservation r " +
"JOIN commercial_bank_currency c ON c.currency_id=r.currency_id " +
"LEFT JOIN draft_digital_currency d ON d.coin_id=c.currency_id " +
"WHERE r.exchange_id=? ORDER BY r.denomination DESC,r.currency_id",
(rs, row) -> new ReservedCoin(rs.getString("currency_id"), rs.getBigDecimal("denomination"), (rs, row) -> new ReservedCoin(rs.getString("currency_id"), rs.getBigDecimal("denomination"),
rs.getString("status")), order.getId().toString()); rs.getString("status"), rs.getString("complete_currency"),
rs.getString("cb_ctrl_signature"), rs.getString("cb_final_signature")),
order.getId().toString());
} }
@Override @Override
@ -369,12 +376,23 @@ public class JdbcExchangeResourceRepository implements ExchangeResourceRepositor
if (valid == null || valid != 1) { if (valid == null || valid != 1) {
throw validation("币串真实性、当前权属或状态校验失败:" + coin.getCurrencyId()); throw validation("币串真实性、当前权属或状态校验失败:" + coin.getCurrencyId());
} }
List<CoinProof> proofs = jdbc.query("SELECT issuance_ownership_original_text,issuance_ownership_signature " + List<CoinProof> proofs = jdbc.query("SELECT s.digest,d.control_system_signature," +
"FROM commercial_bank_currency WHERE currency_id=?", "c.issuance_ownership_original_text,c.issuance_ownership_signature " +
(rs, row) -> new CoinProof(rs.getString(1), rs.getString(2)), coin.getCurrencyId()); "FROM commercial_bank_currency c " +
"JOIN draft_digital_currency d ON d.coin_id=c.currency_id " +
"JOIN digital_currency_production_batch b ON b.batch_id=d.batch_id " +
"JOIN control_system_signature s ON s.id=b.control_system_signature_id " +
"WHERE c.currency_id=? AND d.control_system_signature=b.control_system_signature " +
"AND d.control_system_signature=s.control_signature",
(rs, row) -> new CoinProof(rs.getString(1), rs.getString(2),
rs.getString(3), rs.getString(4)), coin.getCurrencyId());
InstitutionKeySubject subject = new InstitutionKeySubject(actor.getUserId(), actor.getSchoolId(), actor.getClassId()); InstitutionKeySubject subject = new InstitutionKeySubject(actor.getUserId(), actor.getSchoolId(), actor.getClassId());
if (proofs.isEmpty() || !keyService.verifyCentralBank(subject, proofs.get(0).originalText, if (proofs.isEmpty() || !keyService.verifyCentralBank(subject, proofs.get(0).controlDigest,
proofs.get(0).signature)) { proofs.get(0).controlSignature)) {
throw validation("币串中的央行控制系统签名验证失败:" + coin.getCurrencyId());
}
if (!keyService.verifyCentralBank(subject, proofs.get(0).ownershipOriginalText,
proofs.get(0).ownershipSignature)) {
throw validation("币串中的央行发行确权签名验证失败:" + coin.getCurrencyId()); throw validation("币串中的央行发行确权签名验证失败:" + coin.getCurrencyId());
} }
total = total.add(coin.getDenomination()).setScale(2); total = total.add(coin.getDenomination()).setScale(2);
@ -510,11 +528,16 @@ public class JdbcExchangeResourceRepository implements ExchangeResourceRepositor
} }
private static final class CoinProof { private static final class CoinProof {
private final String originalText; private final String controlDigest;
private final String signature; private final String controlSignature;
private CoinProof(String originalText, String signature) { private final String ownershipOriginalText;
this.originalText = originalText; private final String ownershipSignature;
this.signature = signature; private CoinProof(String controlDigest, String controlSignature,
String ownershipOriginalText, String ownershipSignature) {
this.controlDigest = controlDigest;
this.controlSignature = controlSignature;
this.ownershipOriginalText = ownershipOriginalText;
this.ownershipSignature = ownershipSignature;
} }
} }
} }

@ -33,6 +33,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@ActiveProfiles("test") @ActiveProfiles("test")
class ExchangeControllerTest { class ExchangeControllerTest {
private static final String USER_ID = "00000000-0000-0000-0000-000000000999"; private static final String USER_ID = "00000000-0000-0000-0000-000000000999";
private static final String ISSUANCE_REQUEST_ID = "99999999-9999-9999-9999-999999999999";
private static final String DEDUCTION_TRANSACTION_ID = "EXCHANGE_TEST_DEDUCTION";
private static final String PRODUCTION_BATCH_ID = "EXCHANGE_TEST_BATCH";
private static final long CONTROL_SIGNATURE_ID = 990002L;
@Autowired private MockMvc mockMvc; @Autowired private MockMvc mockMvc;
@Autowired private JdbcTemplate jdbc; @Autowired private JdbcTemplate jdbc;
@Autowired private InstitutionIdentityCryptography cryptography; @Autowired private InstitutionIdentityCryptography cryptography;
@ -62,7 +66,7 @@ class ExchangeControllerTest {
jdbc.update("DELETE FROM digital_wallet"); jdbc.update("DELETE FROM digital_wallet");
WalletOpeningTestData.deleteWalletFacts(jdbc, USER_ID); WalletOpeningTestData.deleteWalletFacts(jdbc, USER_ID);
jdbc.update("DELETE FROM institution_identifier_application WHERE user_id=?", USER_ID); jdbc.update("DELETE FROM institution_identifier_application WHERE user_id=?", USER_ID);
jdbc.update("DELETE FROM commercial_bank_currency WHERE issuance_request_id='EXCHANGE_TEST_ISSUANCE'"); deleteIssuanceProofData();
jdbc.update("INSERT INTO institution_identifier_application (id,bank_code,user_id,school_id,class_id," + jdbc.update("INSERT INTO institution_identifier_application (id,bank_code,user_id,school_id,class_id," +
"request_timestamp,original_text,status,institution_identifier,training_round,scoring_criteria," + "request_timestamp,original_text,status,institution_identifier,training_round,scoring_criteria," +
"created_at,updated_at,created_by,updated_by,deleted) VALUES " + "created_at,updated_at,created_by,updated_by,deleted) VALUES " +
@ -72,6 +76,10 @@ class ExchangeControllerTest {
keyService.commercialBankKey(subject, "test"); keyService.commercialBankKey(subject, "test");
keyService.centralBankKey(subject, "test"); keyService.centralBankKey(subject, "test");
String issuanceSignature = keyService.signCentralBank(subject, "ISSUANCE_ORIGINAL"); String issuanceSignature = keyService.signCentralBank(subject, "ISSUANCE_ORIGINAL");
String controlOriginalText = "200.00|ORG_EXCHANGE_TEST|TXN_EXCHANGE_TEST";
String controlDigest = cryptography.sm3(controlOriginalText);
String controlSignature = keyService.signCentralBank(subject, controlDigest);
seedIssuanceProofData(controlOriginalText, controlDigest, controlSignature, issuanceSignature);
InstitutionSm2KeyPair walletKey = cryptography.generateSm2KeyPair(); InstitutionSm2KeyPair walletKey = cryptography.generateSm2KeyPair();
jdbc.update("INSERT INTO simulated_bank_account (account_id,user_id,bank_code,bank_name,card_number,card_last4," + jdbc.update("INSERT INTO simulated_bank_account (account_id,user_id,bank_code,bank_name,card_number,card_last4," +
"balance,frozen_amount,status,created_at,updated_at) VALUES " + "balance,frozen_amount,status,created_at,updated_at) VALUES " +
@ -93,7 +101,7 @@ class ExchangeControllerTest {
+ "received_at,received_by_user_id,received_by) VALUES (?,?,?,?,?,?,100,'DC',?," + "received_at,received_by_user_id,received_by) VALUES (?,?,?,?,?,?,100,'DC',?,"
+ "'ISSUANCE_ORIGINAL',?,'AVAILABLE'," + "'ISSUANCE_ORIGINAL',?,'AVAILABLE',"
+ "CURRENT_TIMESTAMP,?,'test')", + "CURRENT_TIMESTAMP,?,'test')",
"DC_EXCHANGE_TEST_" + i, 9000 + i, 9000, "EXCHANGE_TEST_ISSUANCE", "BKCHCNBJ00001", "DC_EXCHANGE_TEST_" + i, 9000 + i, 9000, ISSUANCE_REQUEST_ID, "BKCHCNBJ00001",
"ORG_EXCHANGE_TEST", "DC_TEST_COMPLETE_" + i, issuanceSignature, USER_ID); "ORG_EXCHANGE_TEST", "DC_TEST_COMPLETE_" + i, issuanceSignature, USER_ID);
jdbc.update("INSERT INTO central_bank_currency_ownership (currency_id,owner_type,owner_id,status," + jdbc.update("INSERT INTO central_bank_currency_ownership (currency_id,owner_type,owner_id,status," +
"last_transaction_id,updated_at) VALUES (?,'BANK','BKCHCNBJ00001','AVAILABLE',NULL,CURRENT_TIMESTAMP)", "last_transaction_id,updated_at) VALUES (?,'BANK','BKCHCNBJ00001','AVAILABLE',NULL,CURRENT_TIMESTAMP)",
@ -119,7 +127,7 @@ class ExchangeControllerTest {
jdbc.update("DELETE FROM simulated_bank_account"); jdbc.update("DELETE FROM simulated_bank_account");
jdbc.update("DELETE FROM digital_wallet"); jdbc.update("DELETE FROM digital_wallet");
WalletOpeningTestData.deleteWalletFacts(jdbc, USER_ID); WalletOpeningTestData.deleteWalletFacts(jdbc, USER_ID);
jdbc.update("DELETE FROM commercial_bank_currency WHERE issuance_request_id='EXCHANGE_TEST_ISSUANCE'"); deleteIssuanceProofData();
jdbc.update("DELETE FROM institution_sm2_key_audit WHERE user_id=?", USER_ID); jdbc.update("DELETE FROM institution_sm2_key_audit WHERE user_id=?", USER_ID);
jdbc.update("DELETE FROM institution_sm2_key WHERE user_id=?", USER_ID); jdbc.update("DELETE FROM institution_sm2_key WHERE user_id=?", USER_ID);
jdbc.update("DELETE FROM institution_identifier_application WHERE user_id=?", USER_ID); jdbc.update("DELETE FROM institution_identifier_application WHERE user_id=?", USER_ID);
@ -170,9 +178,18 @@ class ExchangeControllerTest {
java.math.BigDecimal.class, accountId)).isEqualByComparingTo("200.00"); java.math.BigDecimal.class, accountId)).isEqualByComparingTo("200.00");
assertThat(jdbc.queryForObject("SELECT status FROM exchange_debit_record WHERE exchange_id=?", assertThat(jdbc.queryForObject("SELECT status FROM exchange_debit_record WHERE exchange_id=?",
String.class, id)).isEqualTo("FROZEN"); String.class, id)).isEqualTo("FROZEN");
String expectedCbFinalSignature = jdbc.queryForObject(
"SELECT issuance_ownership_signature FROM commercial_bank_currency WHERE currency_id=?",
String.class, "DC_EXCHANGE_TEST_1");
String expectedCbCtrlSignature = jdbc.queryForObject(
"SELECT control_system_signature FROM draft_digital_currency WHERE coin_id=?",
String.class, "DC_EXCHANGE_TEST_1");
mockMvc.perform(post("/api/v1/exchanges/{id}/reserve-coins", id).with(user())) mockMvc.perform(post("/api/v1/exchanges/{id}/reserve-coins", id).with(user()))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("COINS_RESERVED")) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("COINS_RESERVED"))
.andExpect(jsonPath("$.data.coinCount").value(2)); .andExpect(jsonPath("$.data.coinCount").value(2))
.andExpect(jsonPath("$.data.coins[0].completeCurrency").value("DC_TEST_COMPLETE_1"))
.andExpect(jsonPath("$.data.coins[0].cbCtrlSignature").value(expectedCbCtrlSignature))
.andExpect(jsonPath("$.data.coins[0].cbFinalSignature").value(expectedCbFinalSignature));
mockMvc.perform(post("/api/v1/exchanges/{id}/confirm-ownership", id).with(user())) mockMvc.perform(post("/api/v1/exchanges/{id}/confirm-ownership", id).with(user()))
.andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SUCCESS")) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SUCCESS"))
.andExpect(jsonPath("$.data.walletBalanceAfter").value(200.00)) .andExpect(jsonPath("$.data.walletBalanceAfter").value(200.00))
@ -316,6 +333,26 @@ class ExchangeControllerTest {
Integer.class, exchangeId)).isEqualTo(1); Integer.class, exchangeId)).isEqualTo(1);
} }
@Test
void rejectsCoinWhenCentralBankControlSignatureIsTampered() throws Exception {
String[] values = createHeldExchange();
String attemptId = values[0];
executeAction(attemptId, "05", "receive-debit-voucher", "COMPLETED");
executeAction(attemptId, "05", "reserve-coins", "COMPLETED");
String[] actions = {"adjust-bank-ledger", "generate-transfer-request", "receive-transfer-request",
"reply-transfer-request", "verify-bank-signature", "recalculate-transfer-digest",
"compare-transfer-digest"};
for (String action : actions) executeAction(attemptId, "05", action, "COMPLETED");
jdbc.update("UPDATE draft_digital_currency SET control_system_signature=? WHERE coin_id=?",
"TAMPERED_CONTROL_SIGNATURE", "DC_EXCHANGE_TEST_1");
mockMvc.perform(post("/api/v1/exchange/attempts/{id}/steps/05/actions/verify-coin-ownership", attemptId)
.with(user()).contentType(MediaType.APPLICATION_JSON).content("{}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
"央行控制系统签名验证失败")));
}
@Test @Test
void rejectsCentralConfirmationWhenItsOriginalTextNoLongerMatchesTheSignedDigest() throws Exception { void rejectsCentralConfirmationWhenItsOriginalTextNoLongerMatchesTheSignedDigest() throws Exception {
String[] values = createHeldExchange(); String[] values = createHeldExchange();
@ -426,6 +463,58 @@ class ExchangeControllerTest {
.andExpect(jsonPath("$.data.actionStatus").value(expectedStatus)); .andExpect(jsonPath("$.data.actionStatus").value(expectedStatus));
} }
private void seedIssuanceProofData(String controlOriginalText, String controlDigest, String controlSignature,
String issuanceSignature) {
jdbc.update("INSERT INTO control_system_signature (id,transaction_identifier_id,control_id,amount," +
"institution_identifier,transaction_identifier,original_text,digest,signing_key_id,signature," +
"control_signature,response_message,status,user_id,school_id,class_id,created_at,updated_at," +
"created_by,updated_by,deleted) VALUES (?,?,?,200,?,?,?,?,?,?,?,?,'SENT',?,1999,2999," +
"CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
CONTROL_SIGNATURE_ID, 990003L, "CTRL_EXCHANGE_TEST", "ORG_EXCHANGE_TEST", "TXN_EXCHANGE_TEST",
controlOriginalText, controlDigest, InstitutionKeyService.CENTRAL_FIRST_KEY, controlSignature,
controlSignature, "{}", USER_ID);
jdbc.update("INSERT INTO central_bank_issuance_receipt (request_id,request_no,payload_json,received_at," +
"received_by_user_id,received_by) VALUES (?,?,?,CURRENT_TIMESTAMP,?,'test')",
ISSUANCE_REQUEST_ID, "REQ_EXCHANGE_TEST", "{}", USER_ID);
jdbc.update("INSERT INTO reserve_deduction_notification (request_id,transaction_id,bank_code,organization_id," +
"reserve_account_no,reserve_account_name,currency,deduction_amount,status,sent_at," +
"sent_by_user_id,sent_by) VALUES (?,?,?,?,?,?,?,200,'SUBMITTED',CURRENT_TIMESTAMP,?,'test')",
ISSUANCE_REQUEST_ID, DEDUCTION_TRANSACTION_ID, "BKCHCNBJ00001", "ORG_EXCHANGE_TEST",
"RESERVE_EXCHANGE_TEST", "兑换测试准备金账户", "DC", USER_ID);
jdbc.update("INSERT INTO reserve_deduction_execution (transaction_id,request_id,before_balance," +
"deduction_amount,after_balance,status,executed_at,executed_by_user_id,executed_by) " +
"VALUES (?,?,1000,200,800,'DEDUCTED',CURRENT_TIMESTAMP,?,'test')",
DEDUCTION_TRANSACTION_ID, ISSUANCE_REQUEST_ID, USER_ID);
jdbc.update("INSERT INTO digital_currency_production_batch (batch_id,request_id,deduction_transaction_id," +
"organization_id,currency,total_amount,status,created_at,created_by_user_id,created_by," +
"control_system_signature_id,complete_control_bit,control_system_signature) " +
"VALUES (?,?,?,?,?,200,'OWNERSHIP_CONFIRMED',CURRENT_TIMESTAMP,?,'test',?,?,?)",
PRODUCTION_BATCH_ID, ISSUANCE_REQUEST_ID, DEDUCTION_TRANSACTION_ID, "ORG_EXCHANGE_TEST", "DC",
USER_ID, CONTROL_SIGNATURE_ID, "CONTROL_BIT_EXCHANGE_TEST", controlSignature);
for (int i = 1; i <= 2; i++) {
jdbc.update("INSERT INTO draft_digital_currency (coin_id,batch_id,sequence_no,denomination," +
"organization_id,currency,status,created_at,created_by_user_id,created_by," +
"complete_control_bit,control_system_signature,ownership_original_text," +
"ownership_signature,complete_currency,confirmed_at,confirmed_by_user_id,confirmed_by) " +
"VALUES (?,?,?,100,'ORG_EXCHANGE_TEST','DC','AVAILABLE',CURRENT_TIMESTAMP,?,'test'," +
"?,?,'ISSUANCE_ORIGINAL',? ,?,CURRENT_TIMESTAMP,?,'test')",
"DC_EXCHANGE_TEST_" + i, PRODUCTION_BATCH_ID, i, USER_ID,
"CONTROL_BIT_EXCHANGE_TEST", controlSignature,
issuanceSignature, "DC_TEST_COMPLETE_" + i, USER_ID);
}
}
private void deleteIssuanceProofData() {
jdbc.update("DELETE FROM commercial_bank_currency WHERE issuance_request_id IN (?,?)",
ISSUANCE_REQUEST_ID, "EXCHANGE_TEST_ISSUANCE");
jdbc.update("DELETE FROM draft_digital_currency WHERE batch_id=?", PRODUCTION_BATCH_ID);
jdbc.update("DELETE FROM digital_currency_production_batch WHERE batch_id=?", PRODUCTION_BATCH_ID);
jdbc.update("DELETE FROM reserve_deduction_execution WHERE transaction_id=?", DEDUCTION_TRANSACTION_ID);
jdbc.update("DELETE FROM reserve_deduction_notification WHERE request_id=?", ISSUANCE_REQUEST_ID);
jdbc.update("DELETE FROM central_bank_issuance_receipt WHERE request_id=?", ISSUANCE_REQUEST_ID);
jdbc.update("DELETE FROM control_system_signature WHERE id=?", CONTROL_SIGNATURE_ID);
}
private String repeat(char value, int count) { private String repeat(char value, int count) {
StringBuilder text = new StringBuilder(count); StringBuilder text = new StringBuilder(count);
for (int i = 0; i < count; i++) text.append(value); for (int i = 0; i < count; i++) text.append(value);

Loading…
Cancel
Save