14.23. DD 023: Taler KYC

14.23.1. Summary

This document discusses the Know-your-customer (KYC) processes supported by Taler.

14.23.2. Motivation

To legally operate, Taler has to comply with KYC regulation that requires banks to identify parties involved in transactions at certain points.

14.23.3. Requirements

Taler needs to run KYC checks in the following circumstances:

  • Customer withdraws money over a monthly threshold
    • exchange triggers KYC
    • key: IBAN (encoded as payto:// URI)
  • Wallet receives (via refunds) money resulting in a balance over a threshold
    • this is a client-side restriction
    • key: reserve (=KYC account) long term public key per wallet (encoded as payto:// URI)
  • Wallet receives money via P2P payments
    • key: reserve (=KYC account) long term public key per wallet (encoded as payto:// URI)
  • Merchant receives money (Q: any money, or above a monthly threshold?)
    • key: IBAN (encoded as payto:// URI)

14.23.4. Proposed Solution

Exchange modifications

We introduce a new wire_targets table into the exchange database. This table is referenced as the source or destination of payments (regular deposits and also P2P payments). A positive side-effect is that we reduce duplication in the reserves_in, wire_out and deposits tables as they can reference this table. In this table, we additionally store information related to the KYC status of the underlying payto://-URI.

The new /kyc-check/ endpoint is based on the wire_targets serial number. Access is authenticated by also passing the hash of the payto://-URI (weak authentication is acceptable, as the KYC status or the ability to initiate a KYC process are not very sensitive). Given this pair, the /kyc-check/ endpoint returns either the (positive) KYC status or redirects the client (202) to the current stage of the KYC process. (The endpoint may have to create and store a nonce to be used during /kyc-proof/, depending on the OAuth variant used.) The redirection is offered using an HTTP-redirect for Web-based clients and a JSON body with information for triggering a browser-based KYC process using OAuth 2.0.

The OAuth 2.0 process is setup to end at a new /kyc-proof/ endpoint. This endpoint then updates the KYC table of the exchange with the legitimization status (which is checked using OAuth 2.0). The endpoint also wakes up any long-polling /kyc-check/ requests. Naturally, the exchange’s OAuth 2.0 client credentials must be configured apriori with the legitimization service.

When withdrawing, the exchange checks if the KYC status is acceptable. If no KYC was done and if either the amount withdrawn over the last X days exceeds the threshold or the reserve received received a P2P transfer, then a 202 Accepted is returned which redirects the consumer to the new /kyc-check/ handler.

When depositing, the exchange checks the KYC status and if negative, returns an additional information field that tells the merchant the wire_target_serial number needed to begin the KYC process (this is independent of the amount) at the new /kyc-check/ handler.

When tracking deposits, the exchange also adds the wire_target_serial to the reply if the KYC status is negative.

The aggregator is modified to only SELECT deposits where the wire_target has the KYC status set to positive (unless KYC is disabled in the exchange configuration).

To allow the wallet to do the KYC check if it is about to exceed a set balance threshold, we modify the /keys response to add an optional field wallet_balance_limit_without_kyc the wallet is allowed to hold in coins from this exchange without KYC. If this field is absent, there is no limit. If the field is provided, a correct wallet must create a long-term account-reserve key pair. This should be the same key that is also used to receive wallet-to-wallet payments. Then, before a wallet performs an operation that would cause it to exceed the balance threshold in terms of funds held from a particular exchange, it must first request the user to complete the KYC process.

For that, it should POST to the new /wallet-kyc endpoint, providing its long-term reserve-account public key and a signature requesting permission to exceed the account limit. The exchange will respond with a wire target UUID. The wallet can then use this UUID to being the KYC process at /kyc-check/. The wallet must only proceed to obtain funds exceeding the threshold after the KYC process has concluded. While wallets could be “hacked” to bypass this measure (we cannot cryptographically enforce this), such modifications are a terms of service violation which may have legal consequences for the user.


Unrelated: We may want to consider directly deleting prewire records
instead of setting them to ``finished`` in ``taler-exchange-transfer``.

Exchange database schema changes

Note that there is may be some slight complication in the migration as the h_wire in deposits is salted, while the h_payto in the new wire_targets is expected to be unsalted. So converting the existing information to create the wire_targets table will be tricky!

We can either not support a fully automatic migration, or do an “expensive” migration with C logic (so not just SQL statements).

Given the other database changes for protocol v9, it was decided to just not support any migration this time.

-- Everything in one big transaction
-- Check patch versioning is in place.
SELECT _v.register_patch('exchange-TBD', NULL, NULL);
(wire_target_serial_id BIGSERIAL UNIQUE
,h_payto BYTEA NOT NULL CHECK (LENGTH(h_payto)=64),
,payto_uri STRING NOT NULL
,oauth_username STRING NOT NULL
,PRIMARY KEY (h_wire)
COMMENT ON TABLE wire_targets
  IS 'All recipients of money via the exchange';
COMMENT ON COLUMN wire_targets.payto_uri
  IS 'Can be a regular bank account, or also be a URI identifying a reserve-account (for P2P payments)';
COMMENT ON COLUMN wire_targets.h_payto
  IS 'Unsalted hash of payto_uri';
COMMENT ON COLUMN wire_targets.kyc_ok
  IS 'true if the KYC check was passed successfully';
COMMENT ON COLUMN wire_targets.oauth_username
  IS 'Name of the user that was used for OAuth 2.0-based legitimization';
-- NOTE: logic to fill wire_target missing, so this
-- CANNOT work if the database contains any data!
ALTER TABLE wire_out
  ADD COLUMN wire_target_serial_id INT8 NOT NULL REFERENCES wire_targets (wire_target_serial_id),
  DROP COLUMN wire_target;
COMMENT ON COLUMN wire_out.wire_target_serial_id
  IS 'Identifies the target bank account and KYC status';
ALTER TABLE reserves_in
  ADD COLUMN wire_source_serial_id INT8 NOT NULL REFERENCES wire_targets (wire_target_serial_id),
  DROP COLUMN sender_account_details;
COMMENT ON COLUMN wire_out.wire_target_serial_id
  IS 'Identifies the target bank account and KYC status';
ALTER TABLE reserves_close
  ADD COLUMN wire_source_serial_id INT8 NOT NULL REFERENCES wire_targets (wire_target_serial_id),
  DROP COLUMN receiver_account;
COMMENT ON COLUMN reserves_close.wire_target_serial_id
  IS 'Identifies the target bank account and KYC status. Note that closing does not depend on KYC.';
ALTER TABLE deposits
  ADD COLUMN wire_target_serial_id INT8 NOT NULL,
  DROP COLUMN h_wire,
COMMENT ON COLUMN deposits.wire_target_serial_id
  IS 'Identifies the target bank account and KYC status';
-- Complete transaction
-- FIXME: 512-bit SALT is likely not specified/checked
-- anywhere in the code (salt==string), and we probably
-- should move to a 128-bit salt anyway!

Merchant modifications

We introduce new kyc_status, kyc_timestamp and kyc_serial fields into a new table with primary keys exchange_url and account. This status is updated whenever a deposit is created or tracked, or whenever the mechant backend receives a /kyc-check/ response from the exchange. Initially, kyc_serial is zero, indicating that the merchant has not yet made any deposits and thus does not have an account at the exchange.

A new private endpoint /kyc is introduced which allows frontends to request the /kyc status of any configured account (including with long polling). If the KYC status is negative or the kyc_timestamp not recent (say older than one month), the merchant backend will re-check the KYC status at the exchange (and update its cached status). The endpoint then returns either that the KYC is OK, or information (same as from the exchange endpoint) to begin the KYC process.

The merchant backend uses the new field to remember that a KYC is pending (after /deposit, or tracing deposits) and the SPA then shows a notification whenever the staff is logged in to the system. The notification can be hidden for the current day (remembered in local storage).

The notification links to a (new) KYC status page. When opened, the KYC status page first re-checks the KYC status with the exchange. If the KYC is still unfinished, that page contains another link to begin the KYC process (redirecting to the OAuth 2.0 login page of the legitimization resource server), otherwise it shows that the KYC process is done. If the KYC is unfinished, the SPA should use long-polling on the KYC status on this page to ensure it is always up-to-date, and change to KYC satisfied should the long-poller return with positive news.


Semi-related: The TMH_setup_wire_account() is changed to use
128-bit salt values (to keep ``deposits`` table small) and checks for salt
to be well-formed should be added "everywhere".

Bank requirements

The exchange primarily requires an OAuth 2.0 login page where the user can either login (and share an access token that grants access to only the username) or register to initiate the KYC process.

14.23.5. Alternatives

We may not need the oauth_username, but it seems saner to store it to provide a link to the legitimization resource server.

We could also store the access token, but that seems slightly more dangerous and given the close business relationship is unnecessary.

We may want to store some additional “permission level” obtained from the resource server to say for which of the operations (see requirements section) the legitimization is sufficient.

14.23.6. Drawbacks

14.23.7. Discussion / Q&A

(This should be filled in with results from discussions on mailing lists / personal communication.)