diff --git a/src/main/java/com/yau/digitalrmb/exchange/domain/model/ReservedCoin.java b/src/main/java/com/yau/digitalrmb/exchange/domain/model/ReservedCoin.java index dc5ba8e..8e623c6 100644 --- a/src/main/java/com/yau/digitalrmb/exchange/domain/model/ReservedCoin.java +++ b/src/main/java/com/yau/digitalrmb/exchange/domain/model/ReservedCoin.java @@ -6,14 +6,24 @@ public final class ReservedCoin { private final String currencyId; private final BigDecimal denomination; 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.denomination = denomination; this.status = status; + this.completeCurrency = completeCurrency; + this.cbCtrlSignature = cbCtrlSignature; + this.cbFinalSignature = cbFinalSignature; } public String getCurrencyId() { return currencyId; } public BigDecimal getDenomination() { return denomination; } public String getStatus() { return status; } + public String getCompleteCurrency() { return completeCurrency; } + public String getCbCtrlSignature() { return cbCtrlSignature; } + public String getCbFinalSignature() { return cbFinalSignature; } } diff --git a/src/main/java/com/yau/digitalrmb/exchange/infrastructure/persistence/JdbcExchangeResourceRepository.java b/src/main/java/com/yau/digitalrmb/exchange/infrastructure/persistence/JdbcExchangeResourceRepository.java index 2ba80f7..2b1dc42 100644 --- a/src/main/java/com/yau/digitalrmb/exchange/infrastructure/persistence/JdbcExchangeResourceRepository.java +++ b/src/main/java/com/yau/digitalrmb/exchange/infrastructure/persistence/JdbcExchangeResourceRepository.java @@ -290,10 +290,17 @@ public class JdbcExchangeResourceRepository implements ExchangeResourceRepositor @Override public List findReservedCoins(ExchangeOrder order) { - return jdbc.query("SELECT currency_id,denomination,status FROM exchange_coin_reservation WHERE exchange_id=? " + - "ORDER BY denomination DESC,currency_id", + return jdbc.query("SELECT r.currency_id,r.denomination,r.status,c.complete_currency," + + "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.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 @@ -369,12 +376,23 @@ public class JdbcExchangeResourceRepository implements ExchangeResourceRepositor if (valid == null || valid != 1) { throw validation("币串真实性、当前权属或状态校验失败:" + coin.getCurrencyId()); } - List proofs = jdbc.query("SELECT issuance_ownership_original_text,issuance_ownership_signature " + - "FROM commercial_bank_currency WHERE currency_id=?", - (rs, row) -> new CoinProof(rs.getString(1), rs.getString(2)), coin.getCurrencyId()); + List proofs = jdbc.query("SELECT s.digest,d.control_system_signature," + + "c.issuance_ownership_original_text,c.issuance_ownership_signature " + + "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()); - if (proofs.isEmpty() || !keyService.verifyCentralBank(subject, proofs.get(0).originalText, - proofs.get(0).signature)) { + if (proofs.isEmpty() || !keyService.verifyCentralBank(subject, proofs.get(0).controlDigest, + proofs.get(0).controlSignature)) { + throw validation("币串中的央行控制系统签名验证失败:" + coin.getCurrencyId()); + } + if (!keyService.verifyCentralBank(subject, proofs.get(0).ownershipOriginalText, + proofs.get(0).ownershipSignature)) { throw validation("币串中的央行发行确权签名验证失败:" + coin.getCurrencyId()); } total = total.add(coin.getDenomination()).setScale(2); @@ -510,11 +528,16 @@ public class JdbcExchangeResourceRepository implements ExchangeResourceRepositor } private static final class CoinProof { - private final String originalText; - private final String signature; - private CoinProof(String originalText, String signature) { - this.originalText = originalText; - this.signature = signature; + private final String controlDigest; + private final String controlSignature; + private final String ownershipOriginalText; + private final String ownershipSignature; + private CoinProof(String controlDigest, String controlSignature, + String ownershipOriginalText, String ownershipSignature) { + this.controlDigest = controlDigest; + this.controlSignature = controlSignature; + this.ownershipOriginalText = ownershipOriginalText; + this.ownershipSignature = ownershipSignature; } } } diff --git a/src/test/java/com/yau/digitalrmb/exchange/interfaces/rest/ExchangeControllerTest.java b/src/test/java/com/yau/digitalrmb/exchange/interfaces/rest/ExchangeControllerTest.java index d5f67f6..7d1ee70 100644 --- a/src/test/java/com/yau/digitalrmb/exchange/interfaces/rest/ExchangeControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/exchange/interfaces/rest/ExchangeControllerTest.java @@ -33,6 +33,10 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @ActiveProfiles("test") class ExchangeControllerTest { 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 JdbcTemplate jdbc; @Autowired private InstitutionIdentityCryptography cryptography; @@ -62,7 +66,7 @@ class ExchangeControllerTest { jdbc.update("DELETE FROM digital_wallet"); WalletOpeningTestData.deleteWalletFacts(jdbc, 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," + "request_timestamp,original_text,status,institution_identifier,training_round,scoring_criteria," + "created_at,updated_at,created_by,updated_by,deleted) VALUES " + @@ -72,6 +76,10 @@ class ExchangeControllerTest { keyService.commercialBankKey(subject, "test"); keyService.centralBankKey(subject, "test"); 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(); 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 " + @@ -93,7 +101,7 @@ class ExchangeControllerTest { + "received_at,received_by_user_id,received_by) VALUES (?,?,?,?,?,?,100,'DC',?," + "'ISSUANCE_ORIGINAL',?,'AVAILABLE'," + "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); 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)", @@ -119,7 +127,7 @@ class ExchangeControllerTest { jdbc.update("DELETE FROM simulated_bank_account"); jdbc.update("DELETE FROM digital_wallet"); 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 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"); assertThat(jdbc.queryForObject("SELECT status FROM exchange_debit_record WHERE exchange_id=?", 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())) .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())) .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SUCCESS")) .andExpect(jsonPath("$.data.walletBalanceAfter").value(200.00)) @@ -316,6 +333,26 @@ class ExchangeControllerTest { 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 void rejectsCentralConfirmationWhenItsOriginalTextNoLongerMatchesTheSignedDigest() throws Exception { String[] values = createHeldExchange(); @@ -426,6 +463,58 @@ class ExchangeControllerTest { .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) { StringBuilder text = new StringBuilder(count); for (int i = 0; i < count; i++) text.append(value);