From 26aa9d985e2f9ba6ce3895e7e2625226e2008bfb Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Thu, 29 Dec 2022 11:48:57 +0100 Subject: [PATCH] expand DB API with AML functions, fix purse refund calculations in libtalerexchange --- src/exchangedb/0003-aml_history.sql | 2 +- src/exchangedb/0003-aml_staff.sql | 3 + src/exchangedb/0003-aml_status.sql | 2 +- .../pg_lookup_kyc_process_by_account.c | 3 +- src/exchangedb/pg_select_purse.c | 28 +- src/include/taler_crypto_lib.h | 40 +++ src/include/taler_exchangedb_plugin.h | 307 ++++++++++++++++++ src/lib/exchange_api_common.c | 17 +- src/testing/test_exchange_p2p.c | 14 +- src/util/offline_signatures.c | 8 +- 10 files changed, 382 insertions(+), 42 deletions(-) diff --git a/src/exchangedb/0003-aml_history.sql b/src/exchangedb/0003-aml_history.sql index 009f79d89..d3650f6c6 100644 --- a/src/exchangedb/0003-aml_history.sql +++ b/src/exchangedb/0003-aml_history.sql @@ -57,7 +57,7 @@ BEGIN ,partition_suffix ); PERFORM comment_partitioned_column( - '0 for AML decision required, 1 for AML is OK, -1 for account is frozen (prevents further transactions)' + '0 for all OK, 1 for AML decision required, 2 for account is frozen (prevents further transactions)' ,'new_status' ,table_name ,partition_suffix diff --git a/src/exchangedb/0003-aml_staff.sql b/src/exchangedb/0003-aml_staff.sql index dca0fc8d5..00f60985a 100644 --- a/src/exchangedb/0003-aml_staff.sql +++ b/src/exchangedb/0003-aml_staff.sql @@ -21,6 +21,7 @@ CREATE TABLE aml_staff ,master_sig BYTEA CHECK (LENGTH(master_sig)=64) ,decider_name VARCHAR NOT NULL ,is_active BOOLEAN NOT NULL + ,read_only BOOLEAN NOT NULL ,last_change INT8 NOT NULL ); COMMENT ON TABLE aml_staff @@ -33,5 +34,7 @@ COMMENT ON COLUMN aml_staff.decider_name IS 'Name of the staff member.'; COMMENT ON COLUMN aml_staff.is_active IS 'true if we are currently supporting the use of this AML staff member.'; +COMMENT ON COLUMN aml_staff.is_active + IS 'true if the member has read-only access.'; COMMENT ON COLUMN aml_staff.last_change IS 'Latest time when active status changed. Used to detect replays of old messages.'; diff --git a/src/exchangedb/0003-aml_status.sql b/src/exchangedb/0003-aml_status.sql index 1e676bc1c..c0683c0d8 100644 --- a/src/exchangedb/0003-aml_status.sql +++ b/src/exchangedb/0003-aml_status.sql @@ -53,7 +53,7 @@ BEGIN ,partition_suffix ); PERFORM comment_partitioned_column( - '0 for AML decision required, 1 for AML is OK, -1 for account is frozen (prevents further transactions)' + '0 for all OK, 1 for AML decision required, 2 for account is frozen (prevents further transactions)' ,'status' ,table_name ,partition_suffix diff --git a/src/exchangedb/pg_lookup_kyc_process_by_account.c b/src/exchangedb/pg_lookup_kyc_process_by_account.c index 6183ae7af..79a9d6c8f 100644 --- a/src/exchangedb/pg_lookup_kyc_process_by_account.c +++ b/src/exchangedb/pg_lookup_kyc_process_by_account.c @@ -25,6 +25,7 @@ #include "pg_lookup_kyc_process_by_account.h" #include "pg_helper.h" + enum GNUNET_DB_QueryStatus TEH_PG_lookup_kyc_process_by_account ( void *cls, @@ -59,7 +60,7 @@ TEH_PG_lookup_kyc_process_by_account ( *provider_account_id = NULL; *provider_legitimization_id = NULL; - /* Used in #postgres_lookup_kyc_process_by_account() */ + /* Used in #postgres_lookup_kyc_process_by_account() */ PREPARE (pg, "lookup_process_by_account", "SELECT " diff --git a/src/exchangedb/pg_select_purse.c b/src/exchangedb/pg_select_purse.c index 9143e8721..6496d4a28 100644 --- a/src/exchangedb/pg_select_purse.c +++ b/src/exchangedb/pg_select_purse.c @@ -66,20 +66,20 @@ TEH_PG_select_purse ( PREPARE (pg, "select_purse", "SELECT " - " merge_pub" - ",purse_creation" - ",purse_expiration" - ",h_contract_terms" - ",amount_with_fee_val" - ",amount_with_fee_frac" - ",balance_val" - ",balance_frac" - ",merge_timestamp" - ",purse_sig IS NOT NULL AS purse_deleted" - " FROM purse_requests" - " LEFT JOIN purse_merges USING (purse_pub)" - " LEFT JOIN purse_deletion USING (purse_pub)" - " WHERE purse_pub=$1;"); + " pr.merge_pub" + ",pr.purse_creation" + ",pr.purse_expiration" + ",pr.h_contract_terms" + ",pr.amount_with_fee_val" + ",pr.amount_with_fee_frac" + ",pr.balance_val" + ",pr.balance_frac" + ",pm.merge_timestamp" + ",pd.purse_sig IS NOT NULL AS purse_deleted" + " FROM purse_requests pr" + " LEFT JOIN purse_merges pm ON (pm.purse_pub = pr.purse_pub)" + " LEFT JOIN purse_deletion pd ON (pd.purse_pub = pr.purse_pub)" + " WHERE pr.purse_pub=$1;"); *merge_timestamp = GNUNET_TIME_UNIT_FOREVER_TS; return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "select_purse", diff --git a/src/include/taler_crypto_lib.h b/src/include/taler_crypto_lib.h index d63fd7ccd..5e7ea6059 100644 --- a/src/include/taler_crypto_lib.h +++ b/src/include/taler_crypto_lib.h @@ -146,6 +146,18 @@ struct TALER_ReserveSignatureP }; +/** + * (Symmetric) key used to encrypt KYC attribute data in the database. + */ +struct TALER_AttributeKeyP +{ + /** + * Actual key material. + */ + struct GNUNET_HashCode key; +}; + + /** * @brief Type of public keys to for merchant authorizations. * Merchants can issue refunds using the corresponding @@ -536,6 +548,30 @@ struct TALER_AmlOfficerSignatureP }; +/** + * Bitmask with possible AML decision states. + */ +enum TALER_AmlDecisionState +{ + + /** + * All AML requirements are currently satisfied. + */ + TALER_AML_NONE = 0, + + /** + * An AML investigation is pending. + */ + TALER_AML_PENDING = 1, + + /** + * An AML decision has concluded that the funds must be frozen. + */ + TALER_AML_FROZEN = 2 + +}; + + /** * @brief Type of blinding keys for Taler. * must be 32 bytes (DB) @@ -4597,6 +4633,7 @@ TALER_exchange_online_purse_status_verify ( * @param officer_name name of the officer * @param change_date when to affect the status change * @param is_active true to enable the officer + * @param read_only true to only allow read-only access * @param master_priv private key to sign with * @param[out] master_sig where to write the signature */ @@ -4606,6 +4643,7 @@ TALER_exchange_offline_aml_officer_status_sign ( const char *officer_name, struct GNUNET_TIME_Timestamp change_date, bool is_active, + bool read_only, const struct TALER_MasterPrivateKeyP *master_priv, struct TALER_MasterSignatureP *master_sig); @@ -4617,6 +4655,7 @@ TALER_exchange_offline_aml_officer_status_sign ( * @param officer_name name of the officer * @param change_date when to affect the status change * @param is_active true to enable the officer + * @param read_only true to only allow read-only access * @param master_pub public key to verify against * @param master_sig the signature the signature * @return #GNUNET_OK if the signature is valid @@ -4627,6 +4666,7 @@ TALER_exchange_offline_aml_officer_status_verify ( const char *officer_name, struct GNUNET_TIME_Timestamp change_date, bool is_active, + bool read_only, const struct TALER_MasterPublicKeyP *master_pub, const struct TALER_MasterSignatureP *master_sig); diff --git a/src/include/taler_exchangedb_plugin.h b/src/include/taler_exchangedb_plugin.h index da28262a8..bba593553 100644 --- a/src/include/taler_exchangedb_plugin.h +++ b/src/include/taler_exchangedb_plugin.h @@ -2244,6 +2244,31 @@ typedef void size_t buf_size); +/** + * Callback with KYC attributes about a particular user. + * + * @param cls closure + * @param h_payto account for which the attribute data is stored + * @param provider_section provider that must be checked + * @param birthdate birthdate of user, in format YYYY-MM-DD; can be NULL; + * digits can be 0 if exact day, month or year are unknown + * @param collection_time when was the data collected + * @param expiration_time when does the data expire + * @param enc_attributes_size number of bytes in @a enc_attributes + * @param enc_attributes encrypted attribute data + */ +typedef void +(*TALER_EXCHANGEDB_AttributeCallback)( + void *cls, + const struct TALER_PaytoHashP *h_payto, + const char *provider_section, + const char *birthdate, + struct GNUNET_TIME_Timestamp collection_time, + struct GNUNET_TIME_Timestamp expiration_time, + size_t enc_attributes_size, + const void *enc_attributes); + + /** * Function called with details about deposits that have been made, * with the goal of auditing the deposit's execution. @@ -3100,6 +3125,46 @@ typedef void const struct TALER_EXCHANGEDB_DenominationKeyInformation *issue); +/** + * Return AML status. + * + * @param cls closure + * @param row_id current row in AML status table + * @param h_payto account for which the attribute data is stored + * @param threshold currently monthly threshold that would trigger an AML check + * @param decision_time when was the last decision made + */ +typedef void +(*TALER_EXCHANGEDB_AmlStatusCallback)( + void *cls, + uint64_t row_id, + const struct TALER_PaytoHashP *h_payto, + const struct TALER_Amount *threshold, + enum TALER_AmlDecisionState status); + + +/** + * Return historic AML decision. + * + * @param cls closure + * @param new_threshold new monthly threshold that would trigger an AML check + * @param new_status AML decision status + * @param decision_time when was the decision made + * @param justification human-readable text justifying the decision + * @param decider_pub public key of the staff member + * @param decider_sig signature of the staff member + */ +typedef void +(*TALER_EXCHANGEDB_AmlHistoryCallback)( + void *cls, + const struct TALER_Amount *new_threshold, + enum TALER_AmlDecisionState new_status, + struct GNUNET_TIME_Absolute decision_time, + const char *justification, + const struct TALER_AmlOfficerPublicKeyP *decider_pub, + const struct TALER_AmlOfficerSignatureP *decider_sig); + + /** * @brief The plugin API, returned from the plugin's "init" function. * The argument given to "init" is simply a configuration handle. @@ -6435,6 +6500,248 @@ struct TALER_EXCHANGEDB_Plugin void *kac_cls); + // FIXME: functions below here not yet implemented! + + /** + * Store KYC attribute data. + * + * @param cls closure + * @param h_payto account for which the attribute data is stored + * @param kyc_prox key for similarity search + * @param provider_section provider that must be checked + * @param birthdate birthdate of user, in format YYYY-MM-DD; can be NULL; + * digits can be 0 if exact day, month or year are unknown + * @param collection_time when was the data collected + * @param expiration_time when does the data expire + * @param enc_attributes_size number of bytes in @a enc_attributes + * @param enc_attributes encrypted attribute data + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*insert_kyc_attributes)( + void *cls, + const struct TALER_PaytoHashP *h_payto, + const struct GNUNET_ShortHashCode *kyc_prox, + const char *provider_section, + const char *birthdate, + struct GNUNET_TIME_Timestamp collection_time, + struct GNUNET_TIME_Timestamp expiration_time, + size_t enc_attributes_size, + const void *enc_attributes); + + + /** + * Update KYC attribute data. + * + * @param cls closure + * @param h_payto account for which the attribute data is stored + * @param kyc_prox key for similarity search + * @param provider_section provider that must be checked + * @param birthdate birthdate of user, in format YYYY-MM-DD; can be NULL; + * digits can be 0 if exact day, month or year are unknown + * @param collection_time when was the data collected + * @param expiration_time when does the data expire + * @param enc_attributes_size number of bytes in @a enc_attributes + * @param enc_attributes encrypted attribute data + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*update_kyc_attributes)( + void *cls, + const struct TALER_PaytoHashP *h_payto, + const struct GNUNET_ShortHashCode *kyc_prox, + const char *provider_section, + const char *birthdate, + struct GNUNET_TIME_Timestamp collection_time, + struct GNUNET_TIME_Timestamp expiration_time, + size_t enc_attributes_size, + const void *enc_attributes); + + + /** + * Lookup similar KYC attribute data. + * + * @param cls closure + * @param h_payto account for which the attribute data is stored + * @param kyc_prox key for similarity search + * @param cb callback to invoke on each match + * @param cb_cls closure for @a cb + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*select_similar_kyc_attributes)( + void *cls, + const struct GNUNET_ShortHashCode *kyc_prox, + TALER_EXCHANGEDB_AttributeCallback cb, + void *cb_cls); + + + /** + * Lookup KYC attribute data for a specific account. + * + * @param cls closure + * @param h_payto account for which the attribute data is stored + * @param cb callback to invoke on each match + * @param cb_cls closure for @a cb + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*select_kyc_attributes)( + void *cls, + const struct TALER_PaytoHashP *h_payto, + TALER_EXCHANGEDB_AttributeCallback cb, + void *cb_cls); + + + /** + * Insert AML staff record. + * + * @param cls closure + * @param decider_pub public key of the staff member + * @param master_sig offline signature affirming the AML officer + * @param decider_name full name of the staff member + * @param is_active true to enable, false to set as inactive + * @param read_only true to set read-only access + * @param last_change when was the change made effective + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*insert_aml_officer)( + void *cls, + const struct TALER_AmlOfficerPublicKeyP *decider_pub, + const struct TALER_MasterSignatureP *master_sig, + const char *decider_name, + bool is_active, + bool read_only, + struct GNUNET_TIME_Absolute last_change); + + + /** + * Update AML staff record. + * + * @param cls closure + * @param decider_pub public key of the staff member + * @param master_sig offline signature affirming the AML officer + * @param decider_name full name of the staff member + * @param is_active true to enable, false to set as inactive + * @param read_only true to set read-only access + * @param last_change when was the change made effective + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*update_aml_officer)( + void *cls, + const struct TALER_AmlOfficerPublicKeyP *decider_pub, + const struct TALER_MasterSignatureP *master_sig, + const char *decider_name, + bool is_active, + bool read_only, + struct GNUNET_TIME_Absolute last_change); + + + /** + * Fetch AML staff record. + * + * @param cls closure + * @param decider_pub public key of the staff member + * @param[out] master_sig offline signature affirming the AML officer + * @param[out] decider_name full name of the staff member + * @param[out] is_active true to enable, false to set as inactive + * @param[out] read_only true to set read-only access + * @param[out] last_change when was the change made effective + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*lookup_aml_officer)( + void *cls, + const struct TALER_AmlOfficerPublicKeyP *decider_pub, + struct TALER_MasterSignatureP *master_sig, + char **decider_name, + bool *is_active, + bool *read_only, + struct GNUNET_TIME_Absolute *last_change); + + + /** + * Trigger AML process, an account has crossed the threshold. Inserts or + * updates the AML status. + * + * @param cls closure + * @param h_payto account for which the attribute data is stored + * @param threshold_crossed existing threshold that was crossed + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*trigger_aml_process)( + void *cls, + const struct TALER_PaytoHashP *h_payto, + const struct TALER_Amount *threshold_crossed); + + + /** + * Lookup AML decisions that have a particular state. + * + * @param cls closure + * @param decision which decision states to filter by + * @param row_off offset to start from + * @param forward true to go forward in time, false to go backwards + * @param cb callback to invoke on each match + * @param cb_cls closure for @a cb + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*select_aml_processes)( + void *cls, + enum TALER_AmlDecisionState decision, + uint64_t row_off, + bool forward, + TALER_EXCHANGEDB_AmlStatusCallback cb, + void *cb_cls); + + + /** + * Lookup AML decision history for a particular account. + * + * @param cls closure + * @param h_payto which account should we return the AML decision history for + * @param cb callback to invoke on each match + * @param cb_cls closure for @a cb + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*select_aml_history)( + void *cls, + const struct TALER_PaytoHashP *h_payto, + TALER_EXCHANGEDB_AmlHistoryCallback cb, + void *cb_cls); + + + /** + * Insert an AML decision. Inserts into AML history and insert or updates AML + * status. + * + * @param cls closure + * @param h_payto account for which the attribute data is stored + * @param new_threshold new monthly threshold that would trigger an AML check + * @param new_status AML decision status + * @param decision_time when was the decision made + * @param justification human-readable text justifying the decision + * @param decider_pub public key of the staff member + * @param decider_sig signature of the staff member + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*insert_aml_decision)( + void *cls, + const struct TALER_PaytoHashP *h_payto, + const struct TALER_Amount *new_threshold, + enum TALER_AmlDecisionState new_status, + struct GNUNET_TIME_Absolute decision_time, + const char *justification, + const struct TALER_AmlOfficerPublicKeyP *decider_pub, + const struct TALER_AmlOfficerSignatureP *decider_sig); + + }; #endif /* _TALER_EXCHANGE_DB_H */ diff --git a/src/lib/exchange_api_common.c b/src/lib/exchange_api_common.c index 3807b997f..b895bf9a8 100644 --- a/src/lib/exchange_api_common.c +++ b/src/lib/exchange_api_common.c @@ -1336,15 +1336,11 @@ help_purse_deposit (struct CoinHistoryParseContext *pc, } if (refunded) { - /* We add the amount to refunds here, the original - deposit will be added to the balance later because - we still return GNUNET_YES, thus effectively - cancelling out this operation with respect to - the final balance. */ + /* We wave the deposit fee. */ if (0 > TALER_amount_add (&pc->rtotal, &pc->rtotal, - amount)) + &pc->dk->fees.deposit)) { /* overflow in refund history? inconceivable! Bad exchange! */ GNUNET_break_op (0); @@ -1415,15 +1411,6 @@ help_purse_refund (struct CoinHistoryParseContext *pc, GNUNET_break_op (0); return GNUNET_SYSERR; } - if (0 > - TALER_amount_add (&pc->rtotal, - &pc->rtotal, - amount)) - { - /* overflow in refund history? inconceivable! Bad exchange! */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } return GNUNET_NO; } diff --git a/src/testing/test_exchange_p2p.c b/src/testing/test_exchange_p2p.c index ad95bf63c..7c3bdd57e 100644 --- a/src/testing/test_exchange_p2p.c +++ b/src/testing/test_exchange_p2p.c @@ -174,17 +174,17 @@ run (void *cls, TALER_TESTING_cmd_purse_create_with_deposit ( "purse-with-deposit", MHD_HTTP_OK, - "{\"amount\":\"EUR:1\",\"summary\":\"ice cream\"}", + "{\"amount\":\"EUR:0.99\",\"summary\":\"ice cream\"}", true, /* upload contract */ GNUNET_TIME_UNIT_MINUTES, /* expiration */ "withdraw-coin-1", - "EUR:1.01", + "EUR:1.00", NULL), TALER_TESTING_cmd_purse_poll ( "push-poll-purse-before-merge", MHD_HTTP_OK, "purse-with-deposit", - "EUR:1", + "EUR:0.99", true, GNUNET_TIME_UNIT_MINUTES), TALER_TESTING_cmd_contract_get ( @@ -206,13 +206,13 @@ run (void *cls, TALER_TESTING_cmd_status ( "push-check-post-merge-reserve-balance-get", "create-reserve-1", - "EUR:1.03", + "EUR:1.02", MHD_HTTP_OK), /* POST history doesn't yet support P2P transfers */ TALER_TESTING_cmd_reserve_status ( "push-check-post-merge-reserve-balance-post", "create-reserve-1", - "EUR:1.03", + "EUR:1.02", MHD_HTTP_OK), /* Test conflicting merge */ TALER_TESTING_cmd_purse_merge ( @@ -261,12 +261,12 @@ run (void *cls, TALER_TESTING_cmd_status ( "pull-check-post-merge-reserve-balance-get", "create-reserve-1", - "EUR:2.02", + "EUR:2.01", MHD_HTTP_OK), TALER_TESTING_cmd_reserve_status ( "push-check-post-merge-reserve-balance-post", "create-reserve-1", - "EUR:2.02", + "EUR:2.01", MHD_HTTP_OK), /* create 2nd purse for a deposit conflict */ TALER_TESTING_cmd_purse_create_with_reserve ( diff --git a/src/util/offline_signatures.c b/src/util/offline_signatures.c index d6638998b..b1e3b93a3 100644 --- a/src/util/offline_signatures.c +++ b/src/util/offline_signatures.c @@ -54,7 +54,7 @@ struct TALER_MasterAmlOfficerStatusPS struct GNUNET_HashCode h_officer_name GNUNET_PACKED; /** - * 1 if enabled, 0 if disabled, in NBO. + * Bitmask: 1 if enabled; 2 for read-only access. in NBO. */ uint32_t is_active GNUNET_PACKED; }; @@ -67,6 +67,7 @@ TALER_exchange_offline_aml_officer_status_sign ( const char *officer_name, struct GNUNET_TIME_Timestamp change_date, bool is_active, + bool read_only, const struct TALER_MasterPrivateKeyP *master_priv, struct TALER_MasterSignatureP *master_sig) { @@ -75,7 +76,7 @@ TALER_exchange_offline_aml_officer_status_sign ( .purpose.size = htonl (sizeof (as)), .change_date = GNUNET_TIME_timestamp_hton (change_date), .officer_pub = *officer_pub, - .is_active = htonl (is_active ? 1 : 0) + .is_active = htonl ((is_active ? 1 : 0) + (read_only ? 2 : 0)) }; GNUNET_CRYPTO_hash (officer_name, @@ -93,6 +94,7 @@ TALER_exchange_offline_aml_officer_status_verify ( const char *officer_name, struct GNUNET_TIME_Timestamp change_date, bool is_active, + bool read_only, const struct TALER_MasterPublicKeyP *master_pub, const struct TALER_MasterSignatureP *master_sig) { @@ -101,7 +103,7 @@ TALER_exchange_offline_aml_officer_status_verify ( .purpose.size = htonl (sizeof (as)), .change_date = GNUNET_TIME_timestamp_hton (change_date), .officer_pub = *officer_pub, - .is_active = htonl (is_active ? 1 : 0) + .is_active = htonl ((is_active ? 1 : 0) + (read_only ? 2 : 0)) }; GNUNET_CRYPTO_hash (officer_name,