taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

commit 11b087e82244f171b386a3d53facee7a43727983
parent c336901cbce62b7d2833fa5a930f9ca443d146da
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu,  8 Aug 2024 12:41:38 +0200

reorganize docs structure

Diffstat:
Mcore/api-exchange.rst | 2990+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcore/api-merchant.rst | 3---
Mcore/api-terminal.rst | 6++----
Mindex.rst | 5+++--
Mtaler-exchange-manual.rst | 973-------------------------------------------------------------------------------
Ataler-kyc-manual.rst | 1020+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 2518 insertions(+), 2479 deletions(-)

diff --git a/core/api-exchange.rst b/core/api-exchange.rst @@ -24,9 +24,6 @@ for all details not specified in the individual requests. The `glossary <https://docs.taler.net/taler-developer-manual.html#developer-glossary>`_ defines all specific terms used in this section. -.. contents:: Table of Contents - :local: - .. include:: tos.rst .. _keys: @@ -1314,1106 +1311,935 @@ Management operations authorized by master key } --------------- -AML operations --------------- +--------------- +Auditor actions +--------------- -This API is only for designated AML officers. It is used -to allow exchange staff to monitor suspicious transactions -and freeze or unfreeze accounts suspected of money laundering. +.. _auditor_action: +This part of the API is for the use by auditors interacting with the exchange. -.. http:get:: /aml/$OFFICER_PUB/measures - To enable the AML staff SPA to give AML staff a choice of possible measures, a - new endpoint ``/aml/$OFFICER_PUB/measures`` is added that allows the AML SPA - to dynamically GET the list of available measures. It returns a list of known - KYC checks (by name) with their descriptions and a list of AML programs with - information about the required context. +.. http:post:: /auditors/$AUDITOR_PUB/$H_DENOM_PUB - This endpoint was introduced in protocol **v20**. + This is used to add an auditor signature to the ``/keys`` response. It + affirms to wallets and merchants that this auditor is indeed auditing + the coins issued by the respective denomination. There is no "delete" + operation for this, as auditors can only stop auditing a denomination + when it expires. **Request:** - *Taler-AML-Officer-Signature*: - The client must provide Base-32 encoded EdDSA signature with - ``$OFFICER_PRIV``, affirming the desire to obtain AML data. Note that - this is merely a simple authentication mechanism, the details of the - request are not protected by the signature. + The request must be a `AuditorSignatureAddMessage`. **Response:** - :http:statuscode:`200 Ok`: - Information about possible measures is returned in a - `AvailableMeasureSummary` object. + :http:statuscode:`204 No content`: + The backend has successfully stored the auditor signature. + :http:statuscode:`403 Forbidden`: + The auditor signature is invalid. + :http:statuscode:`404 Not found`: + The denomination key for which the auditor is providing a signature is unknown. + The response will be a `DenominationUnknownMessage`. + :http:statuscode:`410 Gone`: + This auditor is no longer supported by the exchange. + :http:statuscode:`412 Precondition failed`: + This auditor is not yet known to the exchange. **Details:** - .. ts:def:: AvailableMeasureSummary - - interface AvailableMeasureSummary { - - // Available original measures that can be - // triggered directly by default rules. - roots: { "$measure_name" : MeasureInformation; }; - - // Available AML programs. - programs: { "$prog_name" : AmlProgramRequirement; }; - - // Available KYC checks. - checks: { "$check_name" : KycCheckInformation; }; + .. ts:def:: DenominationUnknownMessage - } + interface DenominationUnknownMessage { - .. ts:def:: MeasureInformation + // Taler error code. + code: number; - interface MeasureInformation { + // Signature by the exchange over a + // `TALER_DenominationUnknownAffirmationPS`. + // Must have purpose ``TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_UNKNOWN``. + exchange_sig: EddsaSignature; - // Name of a KYC check. - check_name: string; + // Public key of the exchange used to create + // the 'exchange_sig. + exchange_pub: EddsaPublicKey; - // Name of an AML program. - prog_name: string; + // Hash of the denomination public key that is unknown. + h_denom_pub: HashCode; - // Context for the check. Optional. - context?: Object; + // When was the signature created. + timestamp: Timestamp; } - .. ts:def:: AmlProgramRequirement - - interface AmlProgramRequirement { - - // Description of what the AML program does. - description: string; + .. ts:def:: AuditorSignatureAddMessage - // List of required field names in the context to run this - // AML program. SPA must check that the AML staff is providing - // adequate CONTEXT when defining a measure using this program. - context: string[]; + interface AuditorSignatureAddMessage { - // List of required attribute names in the - // input of this AML program. These attributes - // are the minimum that the check must produce - // (it may produce more). - inputs: string[]; + // Signature by the auditor over a + // `TALER_ExchangeKeyValidityPS`. + // Must have purpose ``TALER_SIGNATURE_AUDITOR_EXCHANGE_KEYS``. + auditor_sig: EddsaSignature; } - .. ts:def:: KycCheckInformation - - interface KycCheckInformation { +.. _exchange-withdrawal: - // Description of the KYC check. Should be shown - // to the AML staff but will also be shown to the - // client when they initiate the check in the KYC SPA. - description: string; +---------- +Withdrawal +---------- - // Map from IETF BCP 47 language tags to localized - // description texts. - description_i18n ?: { [lang_tag: string]: string}; +This API is used by the wallet to obtain digital coins. - // Names of the fields that the CONTEXT must provide - // as inputs to this check. - // SPA must check that the AML staff is providing - // adequate CONTEXT when defining a measure using - // this check. - requires: string[]; +When transferring money to the exchange such as via SEPA transfers, the exchange creates +a *reserve*, which keeps the money from the customer. The customer must +specify an EdDSA reserve public key as part of the transfer, and can then +withdraw digital coins using the corresponding private key. All incoming and +outgoing transactions are recorded under the corresponding public key by the +exchange. - // Names of the attributes the check will output. - // SPA must check that the outputs match the - // required inputs when combining a KYC check - // with an AML program into a measure. - outputs: string[]; +.. note:: - // Name of a root measure taken when this check fails. - fallback: string; - } + Eventually the exchange will need to advertise a policy for how long it will + keep transaction histories for inactive or even fully drained reserves. We + will therefore need some additional handler similar to ``/keys`` to + advertise those terms of service. -.. http:get:: /aml/$OFFICER_PUB/kyc-statistics/$NAME - Returns the number of KYC events matching the given event type ``$NAME`` in - the specified time range. Note that this query can be slow as the - statistics are computed on-demand. (This is OK as such requests should be - rare.) +.. http:get:: /reserves/$RESERVE_PUB - This endpoint was introduced in protocol **v20**. + Request summary information about a reserve. **Request:** - *Taler-AML-Officer-Signature*: - The client must provide Base-32 encoded EdDSA signature with - ``$OFFICER_PRIV``, affirming the desire to obtain AML data. Note that this - is merely a simple authentication mechanism, the details of the request are - not protected by the signature. - - :query start_date=TIMESTAMP: - *Optional*. Specifies the date when to - start looking (inclusive). If not given, the start time of the - exchange operation is used. The TIMESTAMP is given - in seconds since the UNIX epoch. - :query end_date=TIMESTAMP: - *Optional*. Specifies the date when to - stop looking (exclusive). If not given, the current date is used. The TIMESTAMP is given - in seconds since the UNIX epoch. + :query timeout_ms=MILLISECONDS: *Optional.* If specified, the exchange will wait up to MILLISECONDS for incoming funds before returning a 404 if the reserve does not yet exist. **Response:** :http:statuscode:`200 OK`: - The responds will be an `EventCounter` message. + The exchange responds with a `ReserveSummary` object; the reserve was known to the exchange. + :http:statuscode:`404 Not found`: + The reserve key does not belong to a reserve known to the exchange. **Details:** - .. ts:def:: EventCounter + .. ts:def:: ReserveSummary - interface EventCounter { - // Number of events of the specified type in - // the given range. - counter: Integer; - } + interface ReserveSummary { + // Balance left in the reserve. + balance: Amount; + // If set, age restriction is required to be set for each coin to this + // value during the withdrawal from this reserve. The client then MUST + // use a denomination with support for age restriction enabled for the + // withdrawal. + // The value represents a valid age group from the list of permissible + // age groups as defined by the exchange's output to /keys. + maximum_age_group?: number; + } -.. http:get:: /aml/$OFFICER_PUB/decisions - **Request:** +Withdraw +~~~~~~~~ - *Taler-AML-Officer-Signature*: - The client must provide Base-32 encoded EdDSA signature with - ``$OFFICER_PRIV``, affirming the desire to obtain AML data. Note that - this is merely a simple authentication mechanism, the details of the - request are not protected by the signature. +.. http:post:: /csr-withdraw - This endpoint was introduced in this form in protocol **v20**. + Obtain exchange-side input values in preparation for a + withdraw step for certain denomination cipher types, + specifically at this point for Clause-Schnorr blind + signatures. - :query limit: - *Optional*. takes value of the form ``N (-N)``, so that at - most ``N`` values strictly older (younger) than ``start`` are returned. - Defaults to ``-20`` to return the last 20 entries (before ``start``). - :query offset: - *Optional*. Row number threshold, see ``delta`` for its - interpretation. Defaults to ``INT64_MAX``, namely the biggest row id - possible in the database. - :query h_payto: - *Optional*. Account selector. All matching accounts are returned if this - filter is absent, otherwise only decisions for this account. - :query active: - *Optional*. If set to yes, only return active decisions, if no only - decisions that have been superceeded. Do not give (or use "all") to - see all decisions regardless of activity status. - :query investigation: - *Optional*. If set to yes, only return accounts that are under - AML investigation, if no only accounts that are not under investigation. - Do not give (or use "all") to see all accounts regardless of - investigation status. + **Request:** The request body must be a `WithdrawPrepareRequest` object. **Response:** :http:statuscode:`200 OK`: - The response will be an `AmlDecisionsResponse` message. - :http:statuscode:`204 No content`: - There are no matching AML records. - :http:statuscode:`403 Forbidden`: - The signature is invalid. + The request was successful, and the response is a `WithdrawPrepareResponse`. Note that repeating exactly the same request + will again yield the same response (assuming none of the denomination is expired). :http:statuscode:`404 Not found`: - The designated AML account is not known. - :http:statuscode:`409 Conflict`: - The designated AML account is not enabled. + The denomination key is not known to the exchange. + :http:statuscode:`410 Gone`: + The requested denomination key is not yet or no longer valid. + It either before the validity start, past the expiration or was revoked. The response is a + `DenominationExpiredMessage`. Clients must evaluate + the error code provided to understand which of the + cases this is and handle it accordingly. **Details:** - .. ts:def:: AmlDecisionsResponse - - interface AmlDecisionsResponse { + .. ts:def:: WithdrawPrepareRequest - // Array of AML decisions matching the query. - records: AmlDecision[]; - } + interface WithdrawPrepareRequest { - .. ts:def:: AmlDecision + // Nonce to be used by the exchange to derive + // its private inputs from. Must not have ever + // been used before. + nonce: CSNonce; - interface AmlDecision { + // Hash of the public key of the denomination the + // request relates to. + denom_pub_hash: HashCode; - // Which payto-address is this record about. - // Identifies a GNU Taler wallet or an affected bank account. - h_payto: PaytoHash; + } - // Row ID of the record. Used to filter by offset. - rowid: Integer; + .. ts:def:: WithdrawPrepareResponse - // Justification for the decision. NULL if none - // is available. - justification?: string; + type WithdrawPrepareResponse = + | ExchangeWithdrawValue; - // When was the decision made? - decision_time: Timestamp; + .. ts:def:: ExchangeWithdrawValue - // Free-form properties about the account. - // Can be used to store properties such as PEP, - // risk category, type of business, hits on - // sanctions lists, etc. - properties?: AccountProperties; + type ExchangeWithdrawValue = + | ExchangeRsaWithdrawValue + | ExchangeCsWithdrawValue; - // What are the new rules? - limits: LegitimizationRuleSet; + .. ts:def:: ExchangeRsaWithdrawValue - // True if the account is under investigation by AML staff - // after this decision. - to_investigate: boolean; + interface ExchangeRsaWithdrawValue { + cipher: "RSA"; + } - // True if this is the active decision for the - // account. - is_active: boolean; + .. ts:def:: ExchangeCsWithdrawValue - } + interface ExchangeCsWithdrawValue { + cipher: "CS"; - .. ts:def:: AccountProperties + // CSR R0 value + r_pub_0: CsRPublic; - // All fields in this object are optional. The actual - // properties collected depend fully on the discretion - // of the exchange operator; - // however, some common fields are standardized - // and thus described here. - interface AccountProperties { + // CSR R1 value + r_pub_1: CsRPublic; + } - // True if this is a politically exposed account. - // Rules for classifying accounts as politically - // exposed are country-dependent. - pep?: boolean; - // True if this is a sanctioned account. - // Rules for classifying accounts as sanctioned - // are country-dependent. - sanctioned?: boolean; +Batch Withdraw +~~~~~~~~~~~~~~ - // True if this is a high-risk account. - // Rules for classifying accounts as at-risk - // are exchange operator-dependent. - high_risk?: boolean; +.. http:post:: /reserves/$RESERVE_PUB/batch-withdraw - // Business domain of the account owner. - // The list of possible business domains is - // operator- or country-dependent. - business_domain?: string; + Withdraw multiple coins from the same reserve. Note that the client should + commit all of the request details, including the private key of the coins and + the blinding factors, to disk *before* issuing this request, so that it can + recover the information if necessary in case of transient failures, like + power outage, network outage, etc. - // Is the client's account currently frozen? - is_frozen?: boolean; + **Request:** The request body must be a `BatchWithdrawRequest` object. - // Was the client's account reported to the authorities? - was_reported?: boolean; + **Response:** - } + :http:statuscode:`200 OK`: + The request was successful, and the response is a `BatchWithdrawResponse`. + Note that repeating exactly the same request will again yield the same + response, so if the network goes down during the transaction or before the + client can commit the coin signature to disk, the coin is not lost. + :http:statuscode:`403 Forbidden`: + A signature is invalid. + This response comes with a standard `ErrorDetail` response. + :http:statuscode:`404 Not found`: + A denomination key or the reserve are not known to the exchange. If the + denomination key is unknown, this suggests a bug in the wallet as the + wallet should have used current denomination keys from ``/keys``. + In this case, the response will be a `DenominationUnknownMessage`. + If the reserve is unknown, the wallet should not report a hard error yet, but + instead simply wait for up to a day, as the wire transaction might simply + not yet have completed and might be known to the exchange in the near future. + In this case, the wallet should repeat the exact same request later again + using exactly the same blinded coin. + :http:statuscode:`409 Conflict`: + One of the following reasons occured: - .. ts:def:: LegitimizationRuleSet + 1. The balance of the reserve is not sufficient to withdraw the coins of the + indicated denominations. The response is `WithdrawError` object. - interface LegitimizationRuleSet { + 2. The reserve has a birthday set and requires a request to ``/age-withdraw`` instead. + The response comes with a standard `ErrorDetail` response with error-code ``TALER_EC_EXCHANGE_RESERVES_AGE_RESTRICTION_REQUIRED`` and an additional field ``maximum_allowed_age`` for the maximum age (in years) that the client can commit to in the call to ``/age-withdraw`` + :http:statuscode:`410 Gone`: + A requested denomination key is not yet or no longer valid. + It either before the validity start, past the expiration or was revoked. + The response is a `DenominationExpiredMessage`. Clients must evaluate the + error code provided to understand which of the cases this is and handle it + accordingly. + :http:statuscode:`451 Unavailable for Legal Reasons`: + This reserve has received funds from a purse or the amount withdrawn + exceeds another legal threshold and thus the reserve must + be upgraded to an account (with KYC) before the withdraw can + complete. Note that this response does NOT affirm that the + withdraw will ultimately complete with the requested amount. + The user should be redirected to the provided location to perform + the required KYC checks to open the account before withdrawing. + Afterwards, the request should be repeated. + The response will be an `LegitimizationNeededResponse` object. - // When does this set of rules expire and - // we automatically transition to the successor - // measure? - expiration_time: Timestamp; + Implementation note: internally, we need to + distinguish between upgrading the reserve to an + account (due to P2P payment) and identifying the + owner of the origin bank account (due to exceeding + the withdraw amount threshold), as we need to create + a different payto://-URI for the KYC check depending + on the case. - // Name of the measure to apply when the expiration time is - // reached. If not set, we refer to the default - // set of rules (and the default account state). - successor_measure?: string; - // Legitimization rules that are to be applied - // to this account. - rules: KycRule[]; + **Details:** - // Custom measures that KYC rules and the - // ``successor_measure`` may refer to. - custom_measures: { "$measure_name" : MeasureInformation; }; + .. ts:def:: BatchWithdrawRequest - } + interface BatchWithdrawRequest { + // Array of requests for the individual coins to withdraw. + planchets: WithdrawRequest[]; - .. ts:def:: KycRule + } - interface KycRule { + .. ts:def:: WithdrawRequest - // Type of operation to which the rule applies. - operation_type: string; + interface WithdrawRequest { + // Hash of a denomination public key, specifying the type of coin the client + // would like the exchange to create. + denom_pub_hash: HashCode; - // The measures will be taken if the given - // threshold is crossed over the given timeframe. - threshold: Amount; + // Coin's blinded public key, should be (blindly) signed by the exchange's + // denomination private key. + coin_ev: CoinEnvelope; - // Over which duration should the ``threshold`` be - // computed. All amounts of the respective - // ``operation_type`` will be added up for this - // duration and the sum compared to the ``threshold``. - timeframe: RelativeTime; + // Signature of `TALER_WithdrawRequestPS` created with + // the `reserves's private key <reserve-priv>` + // using purpose ``TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW``. + reserve_sig: EddsaSignature; - // Array of names of measures to apply. - // Names listed can be original measures or - // custom measures from the `AmlOutcome`. - // A special measure "verboten" is used if the - // threshold may never be crossed. - measures: string[]; + } - // If multiple rules apply to the same account - // at the same time, the number with the highest - // rule determines which set of measures will - // be activated and thus become visible for the - // user. - display_priority: Integer; - // True if the rule (specifically, operation_type, - // threshold, timeframe) and the general nature of - // the measures (verboten or approval required) - // should be exposed to the client. - // Defaults to "false" if not set. - exposed?: boolean; + .. ts:def:: BatchWithdrawResponse - // True if all the measures will eventually need to - // be satisfied, false if any of the measures should - // do. Primarily used by the SPA to indicate how - // the measures apply when showing them to the user; - // in the end, AML programs will decide after each - // measure what to do next. - // Default (if missing) is false. - is_and_combinator?: boolean; + interface BatchWithdrawResponse { + // Array of blinded signatures, in the same order as was + // given in the request. + ev_sigs: WithdrawResponse[]; } -.. http:get:: /aml/$OFFICER_PUB/attributes/$H_PAYTO + .. ts:def:: WithdrawResponse - Obtain attributes obtained as part of AML/KYC processes for a - given account. + interface WithdrawResponse { + // The blinded signature over the 'coin_ev', affirms the coin's + // validity after unblinding. + ev_sig: BlindedDenominationSignature; - This endpoint was introduced in protocol **v20**. + } - **Request:** + .. ts:def:: BlindedDenominationSignature - *Taler-AML-Officer-Signature*: - The client must provide Base-32 encoded EdDSA signature with - ``$OFFICER_PRIV``, affirming the desire to obtain AML data. Note that - this is merely a simple authentication mechanism, the details of the - request are not protected by the signature. + type BlindedDenominationSignature = + | RsaBlindedDenominationSignature + | CSBlindedDenominationSignature; - :query limit: - *Optional*. takes value of the form ``N (-N)``, so that at - most ``N`` values strictly older (younger) than ``start`` are returned. - Defaults to ``-20`` to return the last 20 entries (before ``start``). - :query offset: - *Optional*. Row number threshold, see ``delta`` for its - interpretation. Defaults to ``INT64_MAX``, namely the biggest row id - possible in the database. + .. ts:def:: RsaBlindedDenominationSignature - **Response:** + interface RsaBlindedDenominationSignature { + cipher: "RSA"; - :http:statuscode:`200 OK`: - The responds will be an `KycAttributes` message. - :http:statuscode:`204 No content`: - There are no matching KYC attributes. - :http:statuscode:`403 Forbidden`: - The signature is invalid. - :http:statuscode:`404 Not found`: - The designated AML account is not known. - :http:statuscode:`409 Conflict`: - The designated AML account is not enabled. + // (blinded) RSA signature + blinded_rsa_signature: BlindedRsaSignature; + } - .. ts:def:: KycAttributes - - interface KycAttributes { - - // Matching KYC attribute history of the account. - details: KycAttributeCollectionEvent[]; - - } - - .. ts:def:: KycAttributeCollectionEvent - - interface KycAttributeCollectionEvent { - - // Row ID of the record. Used to filter by offset. - rowid: Integer; + .. ts:def:: CSBlindedDenominationSignature - // Name of the provider - // which was used to collect the attributes. NULL if they were - // just uploaded via a form by the account owner. - provider_name?: string; + interface CSBlindedDenominationSignature { + type: "CS"; - // The collected KYC data. NULL if the attribute data could not - // be decrypted (internal error of the exchange, likely the - // attribute key was changed). - attributes?: Object; + // Signer chosen bit value, 0 or 1, used + // in Clause Blind Schnorr to make the + // ROS problem harder. + b: Integer; - // Time when the KYC data was collected - collection_time: Timestamp; + // Blinded scalar calculated from c_b. + s: Cs25519Scalar; } + .. ts:def:: WithdrawError - .. http:post:: /aml/$OFFICER_PUB/decision + interface WithdrawError { + // Text describing the error. + hint: string; - Make an AML decision. Triggers the respective action and - records the justification. + // Detailed error code. + code: Integer; - **Request:** + // Amount left in the reserve. + balance: Amount; - The request must be an `AmlDecisionRequest` message. + // History of the reserve's activity, in the same format + // as returned by ``/reserve/$RID/history``. + history: TransactionHistoryItem[] + } - **Response** + .. ts:def:: DenominationExpiredMessage - :http:statuscode:`204 No content`: - The AML decision has been executed and recorded successfully. - :http:statuscode:`403 Forbidden`: - The signature is invalid (or the AML officer not known). - :http:statuscode:`404 Not found`: - The payto-address the decision was made for is unknown to the exchange. - :http:statuscode:`409 Conflict`: - The designated AML account is not enabled or a more recent - decision was already submitted. + interface DenominationExpiredMessage { - **Details:** + // Taler error code. Note that beyond + // expiration this message format is also + // used if the key is not yet valid, or + // has been revoked. + code: number; - .. ts:def:: AmlDecisionRequest + // Signature by the exchange over a + // `TALER_DenominationExpiredAffirmationPS`. + // Must have purpose ``TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_EXPIRED``. + exchange_sig: EddsaSignature; - interface AmlDecisionRequest { + // Public key of the exchange used to create + // the 'exchange_sig. + exchange_pub: EddsaPublicKey; - // Human-readable justification for the decision. - justification: string; + // Hash of the denomination public key that is unknown. + h_denom_pub: HashCode; - // Which payto-address is the decision about? - // Identifies a GNU Taler wallet or an affected bank account. - h_payto: PaytoHash; + // When was the signature created. + timestamp: Timestamp; - // What are the new rules? - // New since protocol **v20**. - new_rules: LegitimizationRuleSet; + // What kind of operation was requested that now + // failed? + oper: string; + } - // What are the new account properties? - // New since protocol **v20**. - properties: AccountProperties; - // New measure to apply immediately to the account. - // Should typically be used to give the user some - // information or request additional information. - // Use "verboten" to communicate to the customer - // that there is no KYC check that could be passed - // to modify the ``new_rules``. - // New since protocol **v20**. - new_measure?: string; - // True if the account should remain under investigation by AML staff. - // New since protocol **v20**. - keep_investigating: boolean; - // Signature by the AML officer over a `TALER_AmlDecisionPS`. - // Must have purpose ``TALER_SIGNATURE_MASTER_AML_KEY``. - officer_sig: EddsaSignature; - // When was the decision made? - decision_time: Timestamp; +Withdraw with Age Restriction +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - } +If the reserve was marked with a maximum age group, the client has to perform a +cut&choose protocol with the exchange. It first calls +``/reserves/$RESERVE_PUB/age-withdraw`` and commits to ``n*kappa`` coins. On +success, the exchange answers this request with an noreveal-index. The client +then has to call ``/age-withdraw/$ACH/reveal`` to reveal all ``n*(kappa - 1)`` +coins along with their age commitments to proof that they were appropriate. +If so, the exchange will blindly sign ``n`` undisclosed coins from the request. ---------------- -Auditor actions ---------------- +.. http:post:: /reserves/$RESERVE_PUB/age-withdraw -.. _auditor_action: + Withdraw multiple coins *with age restriction* from the same reserve. + Note that the client should commit all of the request details, including the + private key of the coins and the blinding factors, to disk *before* issuing + this request, so that it can recover the information if necessary in case of + transient failures, like power outage, network outage, etc. -This part of the API is for the use by auditors interacting with the exchange. + **Request:** The request body must be a `AgeWithdrawRequest` object. + **Response:** -.. http:post:: /auditors/$AUDITOR_PUB/$H_DENOM_PUB + :http:statuscode:`200 OK`: + The request was successful, and the response is a `AgeWithdrawResponse`. + Note that repeating exactly the same request will again yield the same + response, so if the network goes down during the transaction or before the + client can commit the coin signature to disk, the coin is not lost. + :http:statuscode:`403 Forbidden`: + A signature is invalid. + This response comes with a standard `ErrorDetail` response. + :http:statuscode:`409 Conflict`: + One of two reasons occured: - This is used to add an auditor signature to the ``/keys`` response. It - affirms to wallets and merchants that this auditor is indeed auditing - the coins issued by the respective denomination. There is no "delete" - operation for this, as auditors can only stop auditing a denomination - when it expires. + 1. The balance of the reserve is not sufficient to withdraw the coins of the + given amount. The response is a `WithdrawError` object. - **Request:** + 2. The provided value for ``max_age`` is higher than the allowed value according to the reserve's birthday. + The response comes with a standard `ErrorDetail` response with error-code ``TALER_EC_EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE`` and an additional field ``maximum_allowed_age`` for the maximum age (in years) that the client can commit to in a call to ``/age-withdraw`` + :http:statuscode:`410 Gone`: + A requested denomination key is not yet or no longer valid. + It either before the validity start, past the expiration or was revoked. + The response is a `DenominationExpiredMessage`. Clients must evaluate the + error code provided to understand which of the cases this is and handle it + accordingly. + :http:statuscode:`451 Unavailable for Legal Reasons`: + This reserve has received funds from a purse or the amount withdrawn + exceeds another legal threshold and thus the reserve must + be upgraded to an account (with KYC) before the withdraw can + complete. Note that this response does NOT affirm that the + withdraw will ultimately complete with the requested amount. + The user should be redirected to the provided location to perform + the required KYC checks to open the account before withdrawing. + Afterwards, the request should be repeated. + The response will be an `LegitimizationNeededResponse` object. - The request must be a `AuditorSignatureAddMessage`. + .. ts:def:: AgeWithdrawRequest - **Response:** + interface AgeWithdrawRequest { + // Array of ``n`` hash codes of denomination public keys to order. + // These denominations MUST support age restriction as defined in the + // output to /keys. + // The sum of all denomination's values and fees MUST be at most the + // balance of the reserve. The balance of the reserve will be + // immediatley reduced by that amount. + denoms_h: HashCode[]; - :http:statuscode:`204 No content`: - The backend has successfully stored the auditor signature. - :http:statuscode:`403 Forbidden`: - The auditor signature is invalid. - :http:statuscode:`404 Not found`: - The denomination key for which the auditor is providing a signature is unknown. - The response will be a `DenominationUnknownMessage`. - :http:statuscode:`410 Gone`: - This auditor is no longer supported by the exchange. - :http:statuscode:`412 Precondition failed`: - This auditor is not yet known to the exchange. + // ``n`` arrays of ``kappa`` entries with blinded coin envelopes. Each + // (toplevel) entry represents ``kappa`` canditates for a particular + // coin. The exchange will respond with an index ``gamma``, which is + // the index that shall remain undisclosed during the reveal phase. + // The SHA512 hash $ACH over the blinded coin envelopes is the commitment + // that is later used as the key to the reveal-URL. + blinded_coins_evs: CoinEnvelope[][]; - **Details:** + // The maximum age to commit to. MUST be the same as the maximum + // age in the reserve. + max_age: number; - .. ts:def:: DenominationUnknownMessage + // Signature of `TALER_AgeWithdrawRequestPS` created with + // the `reserves's private key <reserve-priv>` + // using purpose ``TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW``. + reserve_sig: EddsaSignature; + } - interface DenominationUnknownMessage { + .. ts:def:: AgeWithdrawResponse - // Taler error code. - code: number; + interface AgeWithdrawResponse { + // index of the commitments that the client doesn't + // have to disclose + noreveal_index: Integer; - // Signature by the exchange over a - // `TALER_DenominationUnknownAffirmationPS`. - // Must have purpose ``TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_UNKNOWN``. + // Signature of `TALER_AgeWithdrawConfirmationPS` whereby + // the exchange confirms the ``noreveal_index``. exchange_sig: EddsaSignature; - // Public key of the exchange used to create - // the 'exchange_sig. + // `Public EdDSA key <sign-key-pub>` of the exchange that was used to + // generate the signature. Should match one of the exchange's signing + // keys from ``/keys``. Again given explicitly as the client might + // otherwise be confused by clock skew as to which signing key was used. exchange_pub: EddsaPublicKey; + } - // Hash of the denomination public key that is unknown. - h_denom_pub: HashCode; - - // When was the signature created. - timestamp: Timestamp; - } - .. ts:def:: AuditorSignatureAddMessage +.. http:post:: /age-withdraw/$ACH/reveal - interface AuditorSignatureAddMessage { + The client has previously committed to multiple coins with age restriction + in a call to ``/reserve/$RESERVE_PUB/age-withdraw`` and got a + `AgeWithdrawResponse` from the exchange. By calling this + endpoint, the client has to reveal each coin and their ``kappa - 1`` + age commitments, except for the age commitments with index + ``noreveal_index``. The hash of all commitments from the former withdraw + request is given as the ``$ACH`` value in the URL to this endpoint. - // Signature by the auditor over a - // `TALER_ExchangeKeyValidityPS`. - // Must have purpose ``TALER_SIGNATURE_AUDITOR_EXCHANGE_KEYS``. - auditor_sig: EddsaSignature; - } + **Request:** The request body must be a `AgeWithdrawRevealRequest` object. -.. _exchange-withdrawal: + **Response:** ----------- -Withdrawal ----------- + :http:statuscode:`200 OK`: + The request was successful, and the response is a `AgeWithdrawRevealResponse`. + Note that repeating exactly the same request will again yield the same + response, so if the network goes down during the transaction or before the + client can commit the coin signature to disk, the coin is not lost. + :http:statuscode:`404 Not found`: + The provided commitment $ACH is unknown. + :http:statuscode:`409 Conflict`: + The reveal operation failed and the response is an `WithdrawError` object. + The error codes indicate one of two cases: -This API is used by the wallet to obtain digital coins. + 1. An age commitment for at least one of the coins did not fulfill the + required maximum age requirement of the corresponding reserve. + Error code: + ``TALER_EC_EXCHANGE_GENERIC_COIN_AGE_REQUIREMENT_FAILURE``. + 2. The computation of the hash of the commitment with provided input does + result in the value $ACH. + Error code: + ``TALER_EC_EXCHANGE_AGE_WITHDRAW_REVEAL_INVALID_HASH`` -When transferring money to the exchange such as via SEPA transfers, the exchange creates -a *reserve*, which keeps the money from the customer. The customer must -specify an EdDSA reserve public key as part of the transfer, and can then -withdraw digital coins using the corresponding private key. All incoming and -outgoing transactions are recorded under the corresponding public key by the -exchange. -.. note:: + .. ts:def:: AgeWithdrawRevealRequest - Eventually the exchange will need to advertise a policy for how long it will - keep transaction histories for inactive or even fully drained reserves. We - will therefore need some additional handler similar to ``/keys`` to - advertise those terms of service. + interface AgeWithdrawRevealRequest { + // Array of ``n`` of ``(kappa - 1)`` disclosed coin master secrets, from + // which the coins' private key, blinding, nonce (for Clause-Schnorr) and + // age-restriction is calculated. + // + // Given each coin's private key and age commitment, the exchange will + // calculate each coin's blinded hash value und use all those (disclosed) + // blinded hashes together with the non-disclosed envelopes ``coin_evs`` + // during the verification of the original age-withdraw-commitment. + disclosed_coin_secrets: AgeRestrictedCoinSecret[][]; + } + .. ts:def:: AgeRestrictedCoinSecret -.. http:get:: /reserves/$RESERVE_PUB + // The Master key material from which the coins' private key ``coin_priv``, + // blinding ``beta`` and nonce ``nonce`` (for Clause-Schnorr) itself are + // derived as usually in wallet-core. Given a coin's master key material, + // the age commitment for the coin MUST be derived from this private key as + // follows: + // + // Let m ∈ {1,...,M} be the maximum age group as defined in the reserve + // that the wallet can commit to. + // + // For age group $AG ∈ {1,...m}, set + // seed = HDKF(coin_secret, "age-commitment", $AG) + // p[$AG] = Edx25519_generate_private(seed) + // and calculate the corresponding Edx25519PublicKey as + // q[$AG] = Edx25519_public_from_private(p[$AG]) + // + // For age groups $AG ∈ {m,...,M}, set + // f[$AG] = HDKF(coin_secret, "age-factor", $AG) + // and calculate the corresponding Edx25519PublicKey as + // q[$AG] = Edx25519_derive_public(`PublishedAgeRestrictionBaseKey`, f[$AG]) + // + type AgeRestrictedCoinSecret = string; - Request summary information about a reserve. + .. ts:def:: PublishedAgeRestrictionBaseKey - **Request:** + // The value for ``PublishedAgeRestrictionBaseKey`` is a randomly chosen + // `Edx25519PublicKey` for which the private key is not known to the clients. It is + // used during the age-withdraw protocol so that clients can proof that they + // derived all public keys to age groups higher than their allowed maximum + // from this particular value. + const PublishedAgeRestrictionBaseKey = + new Edx25519PublicKey("CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG"); - :query timeout_ms=MILLISECONDS: *Optional.* If specified, the exchange will wait up to MILLISECONDS for incoming funds before returning a 404 if the reserve does not yet exist. + .. ts:def:: AgeWithdrawRevealResponse - **Response:** + interface AgeWithdrawRevealResponse { + // List of the exchange's blinded RSA signatures on the new coins. + ev_sigs : BlindedDenominationSignature[]; + } - :http:statuscode:`200 OK`: - The exchange responds with a `ReserveSummary` object; the reserve was known to the exchange. - :http:statuscode:`404 Not found`: - The reserve key does not belong to a reserve known to the exchange. - **Details:** +.. _reserve-history: - .. ts:def:: ReserveSummary +--------------- +Reserve History +--------------- - interface ReserveSummary { - // Balance left in the reserve. - balance: Amount; +.. http:get:: /reserves/$RESERVE_PUB/history - // If set, age restriction is required to be set for each coin to this - // value during the withdrawal from this reserve. The client then MUST - // use a denomination with support for age restriction enabled for the - // withdrawal. - // The value represents a valid age group from the list of permissible - // age groups as defined by the exchange's output to /keys. - maximum_age_group?: number; - } + Request information about the full history of + a reserve or an account. + **Request:** -Withdraw -~~~~~~~~ + The GET request should come with the following HTTP headers: -.. http:post:: /csr-withdraw + *If-None-Match*: The client MAY provide an ``If-None-Match`` header with an + Etag. In that case, the server MUST additionally respond with an ``304`` + status code in case the reserve history matches the provided Etag. - Obtain exchange-side input values in preparation for a - withdraw step for certain denomination cipher types, - specifically at this point for Clause-Schnorr blind - signatures. + *Taler-Reserve-History-Signature*: The client MUST provide Base-32 encoded + EdDSA signature over a TALER_SIGNATURE_RESERVE_HISTORY_REQUEST made with + the respective ``$RESERVE_PRIV``, affirming desire to download the current + reserve transaction history. - **Request:** The request body must be a `WithdrawPrepareRequest` object. + :query start=OFFSET: *Optional.* Only return reserve history entries with + offsets above the given OFFSET. Allows clients to not + retrieve history entries they already have. **Response:** :http:statuscode:`200 OK`: - The request was successful, and the response is a `WithdrawPrepareResponse`. Note that repeating exactly the same request - will again yield the same response (assuming none of the denomination is expired). + The exchange responds with a `ReserveHistory` object; the reserve was known to the exchange. + :http:statuscode:`204 No content`: + The reserve history is known, but at this point from the given starting point it is empty. Can only happen if OFFSET was positive. + :http:statuscode:`304 Not modified`: + The reserve history matches the one identified by the "If-none-match" HTTP header of the request. + :http:statuscode:`403 Forbidden`: + The *TALER_SIGNATURE_RESERVE_HISTORY_REQUEST* is invalid. + This response comes with a standard `ErrorDetail` response. :http:statuscode:`404 Not found`: - The denomination key is not known to the exchange. - :http:statuscode:`410 Gone`: - The requested denomination key is not yet or no longer valid. - It either before the validity start, past the expiration or was revoked. The response is a - `DenominationExpiredMessage`. Clients must evaluate - the error code provided to understand which of the - cases this is and handle it accordingly. + The reserve key does not belong to a reserve known to the exchange. **Details:** - .. ts:def:: WithdrawPrepareRequest - - interface WithdrawPrepareRequest { + .. ts:def:: ReserveHistory - // Nonce to be used by the exchange to derive - // its private inputs from. Must not have ever - // been used before. - nonce: CSNonce; + interface ReserveHistory { + // Balance left in the reserve. + balance: Amount; - // Hash of the public key of the denomination the - // request relates to. - denom_pub_hash: HashCode; + // If set, gives the maximum age group that the client is required to set + // during withdrawal. + maximum_age_group: number; + // Transaction history for this reserve. + // May be partial (!). + history: TransactionHistoryItem[]; } - .. ts:def:: WithdrawPrepareResponse + Objects in the transaction history have the following format: - type WithdrawPrepareResponse = - | ExchangeWithdrawValue; + .. ts:def:: TransactionHistoryItem - .. ts:def:: ExchangeWithdrawValue + // Union discriminated by the "type" field. + type TransactionHistoryItem = + | AccountSetupTransaction + | ReserveWithdrawTransaction + | ReserveAgeWithdrawTransaction + | ReserveCreditTransaction + | ReserveClosingTransaction + | ReserveOpenRequestTransaction + | ReserveCloseRequestTransaction + | PurseMergeTransaction; - type ExchangeWithdrawValue = - | ExchangeRsaWithdrawValue - | ExchangeCsWithdrawValue; + .. ts:def:: AccountSetupTransaction - .. ts:def:: ExchangeRsaWithdrawValue + interface AccountSetupTransaction { + type: "SETUP"; - interface ExchangeRsaWithdrawValue { - cipher: "RSA"; - } + // Offset of this entry in the reserve history. + // Useful to request incremental histories via + // the "start" query parameter. + history_offset: Integer; - .. ts:def:: ExchangeCsWithdrawValue + // KYC fee agreed to by the reserve owner. + kyc_fee: Amount; - interface ExchangeCsWithdrawValue { - cipher: "CS"; + // Time when the KYC was triggered. + kyc_timestamp: Timestamp; - // CSR R0 value - r_pub_0: CsRPublic; + // Hash of the wire details of the account. + // Note that this hash is unsalted and potentially + // private (as it could be inverted), hence access + // to this endpoint must be authorized using the + // private key of the reserve. + h_wire: HashCode; + + // Signature created with the reserve's private key. + // Must be of purpose ``TALER_SIGNATURE_ACCOUNT_SETUP_REQUEST`` over + // a ``TALER_AccountSetupRequestSignaturePS``. + reserve_sig: EddsaSignature; - // CSR R1 value - r_pub_1: CsRPublic; } + .. ts:def:: ReserveWithdrawTransaction -Batch Withdraw -~~~~~~~~~~~~~~ + interface ReserveWithdrawTransaction { + type: "WITHDRAW"; -.. http:post:: /reserves/$RESERVE_PUB/batch-withdraw + // Offset of this entry in the reserve history. + // Useful to request incremental histories via + // the "start" query parameter. + history_offset: Integer; - Withdraw multiple coins from the same reserve. Note that the client should - commit all of the request details, including the private key of the coins and - the blinding factors, to disk *before* issuing this request, so that it can - recover the information if necessary in case of transient failures, like - power outage, network outage, etc. + // Amount withdrawn. + amount: Amount; - **Request:** The request body must be a `BatchWithdrawRequest` object. + // Hash of the denomination public key of the coin. + h_denom_pub: HashCode; - **Response:** + // Hash of the blinded coin to be signed. + h_coin_envelope: HashCode; - :http:statuscode:`200 OK`: - The request was successful, and the response is a `BatchWithdrawResponse`. - Note that repeating exactly the same request will again yield the same - response, so if the network goes down during the transaction or before the - client can commit the coin signature to disk, the coin is not lost. - :http:statuscode:`403 Forbidden`: - A signature is invalid. - This response comes with a standard `ErrorDetail` response. - :http:statuscode:`404 Not found`: - A denomination key or the reserve are not known to the exchange. If the - denomination key is unknown, this suggests a bug in the wallet as the - wallet should have used current denomination keys from ``/keys``. - In this case, the response will be a `DenominationUnknownMessage`. - If the reserve is unknown, the wallet should not report a hard error yet, but - instead simply wait for up to a day, as the wire transaction might simply - not yet have completed and might be known to the exchange in the near future. - In this case, the wallet should repeat the exact same request later again - using exactly the same blinded coin. - :http:statuscode:`409 Conflict`: - One of the following reasons occured: - - 1. The balance of the reserve is not sufficient to withdraw the coins of the - indicated denominations. The response is `WithdrawError` object. + // Signature over a `TALER_WithdrawRequestPS` + // with purpose ``TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW`` + // created with the reserve's private key. + reserve_sig: EddsaSignature; - 2. The reserve has a birthday set and requires a request to ``/age-withdraw`` instead. - The response comes with a standard `ErrorDetail` response with error-code ``TALER_EC_EXCHANGE_RESERVES_AGE_RESTRICTION_REQUIRED`` and an additional field ``maximum_allowed_age`` for the maximum age (in years) that the client can commit to in the call to ``/age-withdraw`` - :http:statuscode:`410 Gone`: - A requested denomination key is not yet or no longer valid. - It either before the validity start, past the expiration or was revoked. - The response is a `DenominationExpiredMessage`. Clients must evaluate the - error code provided to understand which of the cases this is and handle it - accordingly. - :http:statuscode:`451 Unavailable for Legal Reasons`: - This reserve has received funds from a purse or the amount withdrawn - exceeds another legal threshold and thus the reserve must - be upgraded to an account (with KYC) before the withdraw can - complete. Note that this response does NOT affirm that the - withdraw will ultimately complete with the requested amount. - The user should be redirected to the provided location to perform - the required KYC checks to open the account before withdrawing. - Afterwards, the request should be repeated. - The response will be an `LegitimizationNeededResponse` object. + // Fee that is charged for withdraw. + withdraw_fee: Amount; + } - Implementation note: internally, we need to - distinguish between upgrading the reserve to an - account (due to P2P payment) and identifying the - owner of the origin bank account (due to exceeding - the withdraw amount threshold), as we need to create - a different payto://-URI for the KYC check depending - on the case. + .. ts:def:: ReserveAgeWithdrawTransaction + interface ReserveAgeWithdrawTransaction { + type: "AGEWITHDRAW"; - **Details:** + // Offset of this entry in the reserve history. + // Useful to request incremental histories via + // the "start" query parameter. + history_offset: Integer; - .. ts:def:: BatchWithdrawRequest + // Total Amount withdrawn. + amount: Amount; - interface BatchWithdrawRequest { - // Array of requests for the individual coins to withdraw. - planchets: WithdrawRequest[]; + // Commitment of all ``n*kappa`` blinded coins. + h_commitment: HashCode; - } + // Signature over a `TALER_AgeWithdrawRequestPS` + // with purpose ``TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW`` + // created with the reserve's private key. + reserve_sig: EddsaSignature; - .. ts:def:: WithdrawRequest + // Fee that is charged for withdraw. + withdraw_fee: Amount; + } - interface WithdrawRequest { - // Hash of a denomination public key, specifying the type of coin the client - // would like the exchange to create. - denom_pub_hash: HashCode; - // Coin's blinded public key, should be (blindly) signed by the exchange's - // denomination private key. - coin_ev: CoinEnvelope; + .. ts:def:: ReserveCreditTransaction - // Signature of `TALER_WithdrawRequestPS` created with - // the `reserves's private key <reserve-priv>` - // using purpose ``TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW``. - reserve_sig: EddsaSignature; + interface ReserveCreditTransaction { + type: "CREDIT"; - } + // Offset of this entry in the reserve history. + // Useful to request incremental histories via + // the "start" query parameter. + history_offset: Integer; + // Amount deposited. + amount: Amount; - .. ts:def:: BatchWithdrawResponse + // Sender account ``payto://`` URL. + sender_account_url: string; - interface BatchWithdrawResponse { - // Array of blinded signatures, in the same order as was - // given in the request. - ev_sigs: WithdrawResponse[]; + // Opaque identifier internal to the exchange that + // uniquely identifies the wire transfer that credited the reserve. + wire_reference: Integer; + // Timestamp of the incoming wire transfer. + timestamp: Timestamp; } - .. ts:def:: WithdrawResponse - - interface WithdrawResponse { - // The blinded signature over the 'coin_ev', affirms the coin's - // validity after unblinding. - ev_sig: BlindedDenominationSignature; - - } - .. ts:def:: BlindedDenominationSignature + .. ts:def:: ReserveClosingTransaction - type BlindedDenominationSignature = - | RsaBlindedDenominationSignature - | CSBlindedDenominationSignature; + interface ReserveClosingTransaction { + type: "CLOSING"; - .. ts:def:: RsaBlindedDenominationSignature + // Offset of this entry in the reserve history. + // Useful to request incremental histories via + // the "start" query parameter. + history_offset: Integer; - interface RsaBlindedDenominationSignature { - cipher: "RSA"; + // Closing balance. + amount: Amount; - // (blinded) RSA signature - blinded_rsa_signature: BlindedRsaSignature; - } + // Closing fee charged by the exchange. + closing_fee: Amount; - .. ts:def:: CSBlindedDenominationSignature + // Wire transfer subject. + wtid: Base32; - interface CSBlindedDenominationSignature { - type: "CS"; + // ``payto://`` URI of the wire account into which the funds were returned to. + receiver_account_details: string; - // Signer chosen bit value, 0 or 1, used - // in Clause Blind Schnorr to make the - // ROS problem harder. - b: Integer; + // This is a signature over a + // struct `TALER_ReserveCloseConfirmationPS` with purpose + // ``TALER_SIGNATURE_EXCHANGE_RESERVE_CLOSED``. + exchange_sig: EddsaSignature; - // Blinded scalar calculated from c_b. - s: Cs25519Scalar; + // Public key used to create 'exchange_sig'. + exchange_pub: EddsaPublicKey; + // Time when the reserve was closed. + timestamp: Timestamp; } - .. ts:def:: WithdrawError - interface WithdrawError { - // Text describing the error. - hint: string; + .. ts:def:: ReserveOpenRequestTransaction - // Detailed error code. - code: Integer; + interface ReserveOpenRequestTransaction { + type: "OPEN"; - // Amount left in the reserve. - balance: Amount; + // Offset of this entry in the reserve history. + // Useful to request incremental histories via + // the "start" query parameter. + history_offset: Integer; - // History of the reserve's activity, in the same format - // as returned by ``/reserve/$RID/history``. - history: TransactionHistoryItem[] - } + // Open fee paid from the reserve. + open_fee: Amount; - .. ts:def:: DenominationExpiredMessage + // This is a signature over + // a struct `TALER_ReserveOpenPS` with purpose + // ``TALER_SIGNATURE_WALLET_RESERVE_OPEN``. + reserve_sig: EddsaSignature; - interface DenominationExpiredMessage { + // Timestamp of the open request. + request_timestamp: Timestamp; - // Taler error code. Note that beyond - // expiration this message format is also - // used if the key is not yet valid, or - // has been revoked. - code: number; + // Requested expiration. + requested_expiration: Timestamp; - // Signature by the exchange over a - // `TALER_DenominationExpiredAffirmationPS`. - // Must have purpose ``TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_EXPIRED``. - exchange_sig: EddsaSignature; + // Requested number of free open purses. + requested_min_purses: Integer; - // Public key of the exchange used to create - // the 'exchange_sig. - exchange_pub: EddsaPublicKey; + } - // Hash of the denomination public key that is unknown. - h_denom_pub: HashCode; + .. ts:def:: ReserveCloseRequestTransaction - // When was the signature created. - timestamp: Timestamp; + interface ReserveCloseRequestTransaction { + type: "CLOSE"; - // What kind of operation was requested that now - // failed? - oper: string; - } + // Offset of this entry in the reserve history. + // Useful to request incremental histories via + // the "start" query parameter. + history_offset: Integer; + // This is a signature over + // a struct `TALER_ReserveClosePS` with purpose + // ``TALER_SIGNATURE_WALLET_RESERVE_CLOSE``. + reserve_sig: EddsaSignature; + // Target account ``payto://``, optional. + h_payto?: PaytoHash; + // Timestamp of the close request. + request_timestamp: Timestamp; + } + .. ts:def:: PurseMergeTransaction -Withdraw with Age Restriction -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + interface PurseMergeTransaction { + type: "MERGE"; -If the reserve was marked with a maximum age group, the client has to perform a -cut&choose protocol with the exchange. It first calls -``/reserves/$RESERVE_PUB/age-withdraw`` and commits to ``n*kappa`` coins. On -success, the exchange answers this request with an noreveal-index. The client -then has to call ``/age-withdraw/$ACH/reveal`` to reveal all ``n*(kappa - 1)`` -coins along with their age commitments to proof that they were appropriate. -If so, the exchange will blindly sign ``n`` undisclosed coins from the request. + // Offset of this entry in the reserve history. + // Useful to request incremental histories via + // the "start" query parameter. + history_offset: Integer; + // SHA-512 hash of the contact of the purse. + h_contract_terms: HashCode; -.. http:post:: /reserves/$RESERVE_PUB/age-withdraw + // EdDSA public key used to approve merges of this purse. + merge_pub: EddsaPublicKey; - Withdraw multiple coins *with age restriction* from the same reserve. - Note that the client should commit all of the request details, including the - private key of the coins and the blinding factors, to disk *before* issuing - this request, so that it can recover the information if necessary in case of - transient failures, like power outage, network outage, etc. + // Minimum age required for all coins deposited into the purse. + min_age: Integer; - **Request:** The request body must be a `AgeWithdrawRequest` object. + // Number that identifies who created the purse + // and how it was paid for. + flags: Integer; - **Response:** + // Purse public key. + purse_pub: EddsaPublicKey; - :http:statuscode:`200 OK`: - The request was successful, and the response is a `AgeWithdrawResponse`. - Note that repeating exactly the same request will again yield the same - response, so if the network goes down during the transaction or before the - client can commit the coin signature to disk, the coin is not lost. - :http:statuscode:`403 Forbidden`: - A signature is invalid. - This response comes with a standard `ErrorDetail` response. - :http:statuscode:`409 Conflict`: - One of two reasons occured: - - 1. The balance of the reserve is not sufficient to withdraw the coins of the - given amount. The response is a `WithdrawError` object. - - 2. The provided value for ``max_age`` is higher than the allowed value according to the reserve's birthday. - The response comes with a standard `ErrorDetail` response with error-code ``TALER_EC_EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE`` and an additional field ``maximum_allowed_age`` for the maximum age (in years) that the client can commit to in a call to ``/age-withdraw`` - :http:statuscode:`410 Gone`: - A requested denomination key is not yet or no longer valid. - It either before the validity start, past the expiration or was revoked. - The response is a `DenominationExpiredMessage`. Clients must evaluate the - error code provided to understand which of the cases this is and handle it - accordingly. - :http:statuscode:`451 Unavailable for Legal Reasons`: - This reserve has received funds from a purse or the amount withdrawn - exceeds another legal threshold and thus the reserve must - be upgraded to an account (with KYC) before the withdraw can - complete. Note that this response does NOT affirm that the - withdraw will ultimately complete with the requested amount. - The user should be redirected to the provided location to perform - the required KYC checks to open the account before withdrawing. - Afterwards, the request should be repeated. - The response will be an `LegitimizationNeededResponse` object. - - .. ts:def:: AgeWithdrawRequest - - interface AgeWithdrawRequest { - // Array of ``n`` hash codes of denomination public keys to order. - // These denominations MUST support age restriction as defined in the - // output to /keys. - // The sum of all denomination's values and fees MUST be at most the - // balance of the reserve. The balance of the reserve will be - // immediatley reduced by that amount. - denoms_h: HashCode[]; - - // ``n`` arrays of ``kappa`` entries with blinded coin envelopes. Each - // (toplevel) entry represents ``kappa`` canditates for a particular - // coin. The exchange will respond with an index ``gamma``, which is - // the index that shall remain undisclosed during the reveal phase. - // The SHA512 hash $ACH over the blinded coin envelopes is the commitment - // that is later used as the key to the reveal-URL. - blinded_coins_evs: CoinEnvelope[][]; - - // The maximum age to commit to. MUST be the same as the maximum - // age in the reserve. - max_age: number; - - // Signature of `TALER_AgeWithdrawRequestPS` created with - // the `reserves's private key <reserve-priv>` - // using purpose ``TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW``. + // EdDSA signature of the account/reserve affirming the merge + // over a `TALER_AccountMergeSignaturePS`. + // Must be of purpose ``TALER_SIGNATURE_ACCOUNT_MERGE`` reserve_sig: EddsaSignature; - } - - .. ts:def:: AgeWithdrawResponse - - interface AgeWithdrawResponse { - // index of the commitments that the client doesn't - // have to disclose - noreveal_index: Integer; - - // Signature of `TALER_AgeWithdrawConfirmationPS` whereby - // the exchange confirms the ``noreveal_index``. - exchange_sig: EddsaSignature; - - // `Public EdDSA key <sign-key-pub>` of the exchange that was used to - // generate the signature. Should match one of the exchange's signing - // keys from ``/keys``. Again given explicitly as the client might - // otherwise be confused by clock skew as to which signing key was used. - exchange_pub: EddsaPublicKey; - } - - - -.. http:post:: /age-withdraw/$ACH/reveal - - The client has previously committed to multiple coins with age restriction - in a call to ``/reserve/$RESERVE_PUB/age-withdraw`` and got a - `AgeWithdrawResponse` from the exchange. By calling this - endpoint, the client has to reveal each coin and their ``kappa - 1`` - age commitments, except for the age commitments with index - ``noreveal_index``. The hash of all commitments from the former withdraw - request is given as the ``$ACH`` value in the URL to this endpoint. - - - **Request:** The request body must be a `AgeWithdrawRevealRequest` object. - - **Response:** - - :http:statuscode:`200 OK`: - The request was successful, and the response is a `AgeWithdrawRevealResponse`. - Note that repeating exactly the same request will again yield the same - response, so if the network goes down during the transaction or before the - client can commit the coin signature to disk, the coin is not lost. - :http:statuscode:`404 Not found`: - The provided commitment $ACH is unknown. - :http:statuscode:`409 Conflict`: - The reveal operation failed and the response is an `WithdrawError` object. - The error codes indicate one of two cases: - - 1. An age commitment for at least one of the coins did not fulfill the - required maximum age requirement of the corresponding reserve. - Error code: - ``TALER_EC_EXCHANGE_GENERIC_COIN_AGE_REQUIREMENT_FAILURE``. - 2. The computation of the hash of the commitment with provided input does - result in the value $ACH. - Error code: - ``TALER_EC_EXCHANGE_AGE_WITHDRAW_REVEAL_INVALID_HASH`` - - - .. ts:def:: AgeWithdrawRevealRequest - - interface AgeWithdrawRevealRequest { - // Array of ``n`` of ``(kappa - 1)`` disclosed coin master secrets, from - // which the coins' private key, blinding, nonce (for Clause-Schnorr) and - // age-restriction is calculated. - // - // Given each coin's private key and age commitment, the exchange will - // calculate each coin's blinded hash value und use all those (disclosed) - // blinded hashes together with the non-disclosed envelopes ``coin_evs`` - // during the verification of the original age-withdraw-commitment. - disclosed_coin_secrets: AgeRestrictedCoinSecret[][]; - } - - .. ts:def:: AgeRestrictedCoinSecret - // The Master key material from which the coins' private key ``coin_priv``, - // blinding ``beta`` and nonce ``nonce`` (for Clause-Schnorr) itself are - // derived as usually in wallet-core. Given a coin's master key material, - // the age commitment for the coin MUST be derived from this private key as - // follows: - // - // Let m ∈ {1,...,M} be the maximum age group as defined in the reserve - // that the wallet can commit to. - // - // For age group $AG ∈ {1,...m}, set - // seed = HDKF(coin_secret, "age-commitment", $AG) - // p[$AG] = Edx25519_generate_private(seed) - // and calculate the corresponding Edx25519PublicKey as - // q[$AG] = Edx25519_public_from_private(p[$AG]) - // - // For age groups $AG ∈ {m,...,M}, set - // f[$AG] = HDKF(coin_secret, "age-factor", $AG) - // and calculate the corresponding Edx25519PublicKey as - // q[$AG] = Edx25519_derive_public(`PublishedAgeRestrictionBaseKey`, f[$AG]) - // - type AgeRestrictedCoinSecret = string; + // Client-side timestamp of when the merge request was made. + merge_timestamp: Timestamp; - .. ts:def:: PublishedAgeRestrictionBaseKey + // Indicative time by which the purse should expire + // if it has not been merged into an account. At this + // point, all of the deposits made should be + // auto-refunded. + purse_expiration: Timestamp; - // The value for ``PublishedAgeRestrictionBaseKey`` is a randomly chosen - // `Edx25519PublicKey` for which the private key is not known to the clients. It is - // used during the age-withdraw protocol so that clients can proof that they - // derived all public keys to age groups higher than their allowed maximum - // from this particular value. - const PublishedAgeRestrictionBaseKey = - new Edx25519PublicKey("CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG"); + // Purse fee the reserve owner paid for the purse creation. + purse_fee: Amount; - .. ts:def:: AgeWithdrawRevealResponse + // Total amount merged into the reserve. + // (excludes fees). + amount: Amount; - interface AgeWithdrawRevealResponse { - // List of the exchange's blinded RSA signatures on the new coins. - ev_sigs : BlindedDenominationSignature[]; + // True if the purse was actually merged. + // If false, only the purse_fee has an impact + // on the reserve balance! + merged: boolean; } -.. _reserve-history: +.. _coin-history: ---------------- -Reserve History ---------------- +------------ +Coin History +------------ -.. http:get:: /reserves/$RESERVE_PUB/history +.. http:get:: /coins/$COIN_PUB/history - Request information about the full history of - a reserve or an account. + Obtain the transaction history of a coin. Used only in special cases, like + when the exchange claims a double-spending error and the wallet does not + believe it. Usually, the wallet knows the transaction history of each coin + and thus has no need to inquire. **Request:** @@ -2421,461 +2247,332 @@ Reserve History *If-None-Match*: The client MAY provide an ``If-None-Match`` header with an Etag. In that case, the server MUST additionally respond with an ``304`` - status code in case the reserve history matches the provided Etag. + status code in case the coin history matches the provided Etag. - *Taler-Reserve-History-Signature*: The client MUST provide Base-32 encoded - EdDSA signature over a TALER_SIGNATURE_RESERVE_HISTORY_REQUEST made with + *Taler-Coin-History-Signature*: The client MUST provide Base-32 encoded + EdDSA signature over a TALER_SIGNATURE_COIN_HISTORY_REQUEST made with the respective ``$RESERVE_PRIV``, affirming desire to download the current - reserve transaction history. + coin transaction history. - :query start=OFFSET: *Optional.* Only return reserve history entries with + :query start=OFFSET: *Optional.* Only return coin history entries with offsets above the given OFFSET. Allows clients to not retrieve history entries they already have. + **Response:** :http:statuscode:`200 OK`: - The exchange responds with a `ReserveHistory` object; the reserve was known to the exchange. + The operation succeeded, the exchange confirms that no double-spending took + place. The response will include a `CoinHistoryResponse` object. :http:statuscode:`204 No content`: The reserve history is known, but at this point from the given starting point it is empty. Can only happen if OFFSET was positive. :http:statuscode:`304 Not modified`: - The reserve history matches the one identified by the "If-none-match" HTTP header of the request. + The coin history has not changed since the previous query (detected via Etag + in "If-none-match" header). :http:statuscode:`403 Forbidden`: - The *TALER_SIGNATURE_RESERVE_HISTORY_REQUEST* is invalid. + The *TALER_SIGNATURE_COIN_HISTORY_REQUEST* is invalid. This response comes with a standard `ErrorDetail` response. :http:statuscode:`404 Not found`: - The reserve key does not belong to a reserve known to the exchange. - - **Details:** + The coin public key is not (yet) known to the exchange. - .. ts:def:: ReserveHistory + .. ts:def:: CoinHistoryResponse - interface ReserveHistory { - // Balance left in the reserve. + interface CoinHistoryResponse { + // Current balance of the coin. balance: Amount; - // If set, gives the maximum age group that the client is required to set - // during withdrawal. - maximum_age_group: number; + // Hash of the coin's denomination. + h_denom_pub: HashCode; - // Transaction history for this reserve. - // May be partial (!). - history: TransactionHistoryItem[]; + // Transaction history for the coin. + history: CoinSpendHistoryItem[]; } - Objects in the transaction history have the following format: - - .. ts:def:: TransactionHistoryItem + .. ts:def:: CoinSpendHistoryItem // Union discriminated by the "type" field. - type TransactionHistoryItem = - | AccountSetupTransaction - | ReserveWithdrawTransaction - | ReserveAgeWithdrawTransaction - | ReserveCreditTransaction - | ReserveClosingTransaction - | ReserveOpenRequestTransaction - | ReserveCloseRequestTransaction - | PurseMergeTransaction; - - .. ts:def:: AccountSetupTransaction - - interface AccountSetupTransaction { - type: "SETUP"; - + type CoinSpendHistoryItem = + | CoinDepositTransaction + | CoinMeltTransaction + | CoinRefundTransaction + | CoinRecoupTransaction + | CoinOldCoinRecoupTransaction + | CoinRecoupRefreshTransaction + | CoinPurseDepositTransaction + | CoinPurseRefundTransaction + | CoinReserveOpenDepositTransaction; + + .. ts:def:: CoinDepositTransaction + + interface CoinDepositTransaction { + type: "DEPOSIT"; + // Offset of this entry in the reserve history. // Useful to request incremental histories via // the "start" query parameter. history_offset: Integer; - // KYC fee agreed to by the reserve owner. - kyc_fee: Amount; - - // Time when the KYC was triggered. - kyc_timestamp: Timestamp; - - // Hash of the wire details of the account. - // Note that this hash is unsalted and potentially - // private (as it could be inverted), hence access - // to this endpoint must be authorized using the - // private key of the reserve. - h_wire: HashCode; + // The total amount of the coin's value absorbed (or restored in the + // case of a refund) by this transaction. + // The amount given includes + // the deposit fee. The current coin value can thus be computed by + // subtracting this amount. + amount: Amount; - // Signature created with the reserve's private key. - // Must be of purpose ``TALER_SIGNATURE_ACCOUNT_SETUP_REQUEST`` over - // a ``TALER_AccountSetupRequestSignaturePS``. - reserve_sig: EddsaSignature; + // Deposit fee. + deposit_fee: Amount; - } + // Public key of the merchant. + merchant_pub: EddsaPublicKey; - .. ts:def:: ReserveWithdrawTransaction + // Date when the operation was made. + timestamp: Timestamp; - interface ReserveWithdrawTransaction { - type: "WITHDRAW"; + // Date until which the merchant can issue a refund to the customer via the + // exchange, possibly zero if refunds are not allowed. + refund_deadline?: Timestamp; - // Offset of this entry in the reserve history. - // Useful to request incremental histories via - // the "start" query parameter. - history_offset: Integer; + // Signature over `TALER_DepositRequestPS`, made by the customer with the + // `coin's private key <coin-priv>`. + coin_sig: EddsaSignature; - // Amount withdrawn. - amount: Amount; + // Hash of the bank account from where we received the funds. + h_wire: HashCode; - // Hash of the denomination public key of the coin. + // Hash of the public denomination key used to sign the coin. + // Needed because 'coin_sig' signs over this, and + // that is important to fix the coin's denomination. h_denom_pub: HashCode; - // Hash of the blinded coin to be signed. - h_coin_envelope: HashCode; - - // Signature over a `TALER_WithdrawRequestPS` - // with purpose ``TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW`` - // created with the reserve's private key. - reserve_sig: EddsaSignature; + // Hash over the proposal data of the contract that + // is being paid. + h_contract_terms: HashCode; - // Fee that is charged for withdraw. - withdraw_fee: Amount; - } + } - .. ts:def:: ReserveAgeWithdrawTransaction + .. ts:def:: CoinMeltTransaction - interface ReserveAgeWithdrawTransaction { - type: "AGEWITHDRAW"; + interface CoinMeltTransaction { + type: "MELT"; // Offset of this entry in the reserve history. // Useful to request incremental histories via // the "start" query parameter. history_offset: Integer; - // Total Amount withdrawn. + // The total amount of the coin's value absorbed by this transaction. + // Note that for melt this means the amount given includes + // the melt fee. The current coin value can thus be computed by + // subtracting the amounts. amount: Amount; - // Commitment of all ``n*kappa`` blinded coins. - h_commitment: HashCode; + // Signature by the coin over a + // `TALER_RefreshMeltCoinAffirmationPS` of + // purpose ``TALER_SIGNATURE_WALLET_COIN_MELT``. + coin_sig: EddsaSignature; - // Signature over a `TALER_AgeWithdrawRequestPS` - // with purpose ``TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW`` - // created with the reserve's private key. - reserve_sig: EddsaSignature; + // Melt fee. + melt_fee: Amount; - // Fee that is charged for withdraw. - withdraw_fee: Amount; - } + // Commitment from the melt operation. + rc: TALER_RefreshCommitmentP; + + // Hash of the public denomination key used to sign the coin. + // Needed because 'coin_sig' signs over this, and + // that is important to fix the coin's denomination. + h_denom_pub: HashCode; + } - .. ts:def:: ReserveCreditTransaction + .. ts:def:: CoinRefundTransaction - interface ReserveCreditTransaction { - type: "CREDIT"; + interface CoinRefundTransaction { + type: "REFUND"; // Offset of this entry in the reserve history. // Useful to request incremental histories via // the "start" query parameter. history_offset: Integer; - // Amount deposited. + // The total amount of the coin's value restored + // by this transaction. + // The amount given excludes the transaction fee. + // The current coin value can thus be computed by + // adding the amounts to the coin's denomination value. amount: Amount; - // Sender account ``payto://`` URL. - sender_account_url: string; + // Refund fee. + refund_fee: Amount; - // Opaque identifier internal to the exchange that - // uniquely identifies the wire transfer that credited the reserve. - wire_reference: Integer; + // Hash over the proposal data of the contract that + // is being refunded. + h_contract_terms: HashCode; - // Timestamp of the incoming wire transfer. - timestamp: Timestamp; - } + // Refund transaction ID. + rtransaction_id: Integer; + + // `EdDSA Signature <eddsa-sig>` authorizing the REFUND over a + // `TALER_MerchantRefundConfirmationPS` with + // purpose ``TALER_SIGNATURE_MERCHANT_REFUND_OK``. Made with + // the `public key of the merchant <merchant-pub>`. + merchant_sig: EddsaSignature; + } - .. ts:def:: ReserveClosingTransaction + .. ts:def:: CoinRecoupTransaction - interface ReserveClosingTransaction { - type: "CLOSING"; + interface CoinRecoupTransaction { + type: "RECOUP"; // Offset of this entry in the reserve history. // Useful to request incremental histories via // the "start" query parameter. history_offset: Integer; - // Closing balance. + // The total amount of the coin's value absorbed + // by this transaction. + // The current coin value can thus be computed by + // subtracting the amount from + // the coin's denomination value. amount: Amount; - // Closing fee charged by the exchange. - closing_fee: Amount; + // Date when the operation was made. + timestamp: Timestamp; - // Wire transfer subject. - wtid: Base32; + // Signature by the coin over a + // `TALER_RecoupRequestPS` with purpose + // ``TALER_SIGNATURE_WALLET_COIN_RECOUP``. + coin_sig: EddsaSignature; - // ``payto://`` URI of the wire account into which the funds were returned to. - receiver_account_details: string; + // Hash of the public denomination key used to sign the coin. + // Needed because 'coin_sig' signs over this, and + // that is important to fix the coin's denomination. + h_denom_pub: HashCode; - // This is a signature over a - // struct `TALER_ReserveCloseConfirmationPS` with purpose - // ``TALER_SIGNATURE_EXCHANGE_RESERVE_CLOSED``. + // Coin blinding key. + coin_blind: DenominationBlindingKeyP; + + // Reserve receiving the recoup. + reserve_pub: EddsaPublicKey; + + // Signature by the exchange over a + // `TALER_RecoupConfirmationPS`, must be + // of purpose ``TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP``. exchange_sig: EddsaSignature; - // Public key used to create 'exchange_sig'. + // Public key of the private key used to create 'exchange_sig'. exchange_pub: EddsaPublicKey; - // Time when the reserve was closed. - timestamp: Timestamp; } + .. ts:def:: CoinOldCoinRecoupTransaction - .. ts:def:: ReserveOpenRequestTransaction - - interface ReserveOpenRequestTransaction { - type: "OPEN"; + interface CoinOldCoinRecoupTransaction { + type: "OLD-COIN-RECOUP"; // Offset of this entry in the reserve history. // Useful to request incremental histories via // the "start" query parameter. history_offset: Integer; - // Open fee paid from the reserve. - open_fee: Amount; - - // This is a signature over - // a struct `TALER_ReserveOpenPS` with purpose - // ``TALER_SIGNATURE_WALLET_RESERVE_OPEN``. - reserve_sig: EddsaSignature; + // The total amount of the coin's value restored + // by this transaction. + // The current coin value can thus be computed by + // adding the amount to the coin's denomination value. + amount: Amount; - // Timestamp of the open request. - request_timestamp: Timestamp; + // Date when the operation was made. + timestamp: Timestamp; - // Requested expiration. - requested_expiration: Timestamp; + // Signature by the exchange over a + // `TALER_RecoupRefreshConfirmationPS` + // of purpose ``TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH``. + exchange_sig: EddsaSignature; - // Requested number of free open purses. - requested_min_purses: Integer; + // Public key of the private key used to create 'exchange_sig'. + exchange_pub: EddsaPublicKey; } - .. ts:def:: ReserveCloseRequestTransaction + .. ts:def:: CoinRecoupRefreshTransaction - interface ReserveCloseRequestTransaction { - type: "CLOSE"; + interface CoinRecoupRefreshTransaction { + type: "RECOUP-REFRESH"; // Offset of this entry in the reserve history. // Useful to request incremental histories via // the "start" query parameter. history_offset: Integer; - // This is a signature over - // a struct `TALER_ReserveClosePS` with purpose - // ``TALER_SIGNATURE_WALLET_RESERVE_CLOSE``. - reserve_sig: EddsaSignature; + // The total amount of the coin's value absorbed + // by this transaction. + // The current coin value can thus be computed by + // subtracting this amounts from + // the coin's denomination value. + amount: Amount; - // Target account ``payto://``, optional. - h_payto?: PaytoHash; + // Date when the operation was made. + timestamp: Timestamp; - // Timestamp of the close request. - request_timestamp: Timestamp; - } + // Signature by the coin over a `TALER_RecoupRequestPS` + // with purpose ``TALER_SIGNATURE_WALLET_COIN_RECOUP``. + coin_sig: EddsaSignature; - .. ts:def:: PurseMergeTransaction + // Hash of the public denomination key used to sign the coin. + // Needed because 'coin_sig' signs over this, and + // that is important to fix the coin's denomination. + h_denom_pub: HashCode; - interface PurseMergeTransaction { - type: "MERGE"; - - // Offset of this entry in the reserve history. - // Useful to request incremental histories via - // the "start" query parameter. - history_offset: Integer; - - // SHA-512 hash of the contact of the purse. - h_contract_terms: HashCode; - - // EdDSA public key used to approve merges of this purse. - merge_pub: EddsaPublicKey; - - // Minimum age required for all coins deposited into the purse. - min_age: Integer; - - // Number that identifies who created the purse - // and how it was paid for. - flags: Integer; - - // Purse public key. - purse_pub: EddsaPublicKey; - - // EdDSA signature of the account/reserve affirming the merge - // over a `TALER_AccountMergeSignaturePS`. - // Must be of purpose ``TALER_SIGNATURE_ACCOUNT_MERGE`` - reserve_sig: EddsaSignature; - - // Client-side timestamp of when the merge request was made. - merge_timestamp: Timestamp; - - // Indicative time by which the purse should expire - // if it has not been merged into an account. At this - // point, all of the deposits made should be - // auto-refunded. - purse_expiration: Timestamp; - - // Purse fee the reserve owner paid for the purse creation. - purse_fee: Amount; - - // Total amount merged into the reserve. - // (excludes fees). - amount: Amount; - - // True if the purse was actually merged. - // If false, only the purse_fee has an impact - // on the reserve balance! - merged: boolean; - } - - -.. _coin-history: - ------------- -Coin History ------------- - -.. http:get:: /coins/$COIN_PUB/history - - Obtain the transaction history of a coin. Used only in special cases, like - when the exchange claims a double-spending error and the wallet does not - believe it. Usually, the wallet knows the transaction history of each coin - and thus has no need to inquire. - - **Request:** - - The GET request should come with the following HTTP headers: - - *If-None-Match*: The client MAY provide an ``If-None-Match`` header with an - Etag. In that case, the server MUST additionally respond with an ``304`` - status code in case the coin history matches the provided Etag. - - *Taler-Coin-History-Signature*: The client MUST provide Base-32 encoded - EdDSA signature over a TALER_SIGNATURE_COIN_HISTORY_REQUEST made with - the respective ``$RESERVE_PRIV``, affirming desire to download the current - coin transaction history. - - :query start=OFFSET: *Optional.* Only return coin history entries with - offsets above the given OFFSET. Allows clients to not - retrieve history entries they already have. - - - **Response:** - - :http:statuscode:`200 OK`: - The operation succeeded, the exchange confirms that no double-spending took - place. The response will include a `CoinHistoryResponse` object. - :http:statuscode:`204 No content`: - The reserve history is known, but at this point from the given starting point it is empty. Can only happen if OFFSET was positive. - :http:statuscode:`304 Not modified`: - The coin history has not changed since the previous query (detected via Etag - in "If-none-match" header). - :http:statuscode:`403 Forbidden`: - The *TALER_SIGNATURE_COIN_HISTORY_REQUEST* is invalid. - This response comes with a standard `ErrorDetail` response. - :http:statuscode:`404 Not found`: - The coin public key is not (yet) known to the exchange. + // Coin blinding key. + coin_blind: DenominationBlindingKeyP; - .. ts:def:: CoinHistoryResponse + // Signature by the exchange over a + // `TALER_RecoupRefreshConfirmationPS` + // of purpose ``TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH``. + exchange_sig: EddsaSignature; - interface CoinHistoryResponse { - // Current balance of the coin. - balance: Amount; + // Public key used to sign 'exchange_sig'. + exchange_pub: EddsaPublicKey; - // Hash of the coin's denomination. - h_denom_pub: HashCode; + // Blinding factor of the revoked new coin. + new_coin_blinding_secret: DenominationBlindingKeySecret; - // Transaction history for the coin. - history: CoinSpendHistoryItem[]; + // Blinded public key of the revoked new coin. + new_coin_ev: DenominationBlindingKeySecret; } - .. ts:def:: CoinSpendHistoryItem - - // Union discriminated by the "type" field. - type CoinSpendHistoryItem = - | CoinDepositTransaction - | CoinMeltTransaction - | CoinRefundTransaction - | CoinRecoupTransaction - | CoinOldCoinRecoupTransaction - | CoinRecoupRefreshTransaction - | CoinPurseDepositTransaction - | CoinPurseRefundTransaction - | CoinReserveOpenDepositTransaction; - - .. ts:def:: CoinDepositTransaction + .. ts:def:: CoinPurseDepositTransaction - interface CoinDepositTransaction { - type: "DEPOSIT"; + interface CoinPurseDepositTransaction { + type: "PURSE-DEPOSIT"; // Offset of this entry in the reserve history. // Useful to request incremental histories via // the "start" query parameter. history_offset: Integer; - // The total amount of the coin's value absorbed (or restored in the - // case of a refund) by this transaction. - // The amount given includes + // The total amount of the coin's value absorbed + // by this transaction. + // Note that this means the amount given includes // the deposit fee. The current coin value can thus be computed by - // subtracting this amount. + // subtracting the amount from + // the coin's denomination value. amount: Amount; // Deposit fee. deposit_fee: Amount; - // Public key of the merchant. - merchant_pub: EddsaPublicKey; - - // Date when the operation was made. - timestamp: Timestamp; - - // Date until which the merchant can issue a refund to the customer via the - // exchange, possibly zero if refunds are not allowed. - refund_deadline?: Timestamp; - - // Signature over `TALER_DepositRequestPS`, made by the customer with the - // `coin's private key <coin-priv>`. - coin_sig: EddsaSignature; - - // Hash of the bank account from where we received the funds. - h_wire: HashCode; - - // Hash of the public denomination key used to sign the coin. - // Needed because 'coin_sig' signs over this, and - // that is important to fix the coin's denomination. - h_denom_pub: HashCode; - - // Hash over the proposal data of the contract that - // is being paid. - h_contract_terms: HashCode; - - } - - .. ts:def:: CoinMeltTransaction - - interface CoinMeltTransaction { - type: "MELT"; - - // Offset of this entry in the reserve history. - // Useful to request incremental histories via - // the "start" query parameter. - history_offset: Integer; + // Public key of the purse. + purse_pub: EddsaPublicKey; - // The total amount of the coin's value absorbed by this transaction. - // Note that for melt this means the amount given includes - // the melt fee. The current coin value can thus be computed by - // subtracting the amounts. - amount: Amount; + // Date when the purse was set to expire. + purse_expiration: Timestamp; // Signature by the coin over a - // `TALER_RefreshMeltCoinAffirmationPS` of - // purpose ``TALER_SIGNATURE_WALLET_COIN_MELT``. + // `TALER_PurseDepositSignaturePS` of + // purpose ``TALER_SIGNATURE_PURSE_DEPOSIT``. coin_sig: EddsaSignature; - // Melt fee. - melt_fee: Amount; - - // Commitment from the melt operation. - rc: TALER_RefreshCommitmentP; - // Hash of the public denomination key used to sign the coin. // Needed because 'coin_sig' signs over this, and // that is important to fix the coin's denomination. @@ -2883,10 +2580,10 @@ Coin History } - .. ts:def:: CoinRefundTransaction + .. ts:def:: CoinPurseRefundTransaction - interface CoinRefundTransaction { - type: "REFUND"; + interface CoinPurseRefundTransaction { + type: "PURSE-REFUND"; // Offset of this entry in the reserve history. // Useful to request incremental histories via @@ -2895,33 +2592,32 @@ Coin History // The total amount of the coin's value restored // by this transaction. - // The amount given excludes the transaction fee. + // The amount given excludes the refund fee. // The current coin value can thus be computed by - // adding the amounts to the coin's denomination value. + // adding the amount to the coin's denomination value. amount: Amount; - // Refund fee. + // Refund fee (of the coin's denomination). The deposit + // fee will be waived. refund_fee: Amount; - // Hash over the proposal data of the contract that - // is being refunded. - h_contract_terms: HashCode; + // Public key of the purse that expired. + purse_pub: EddsaPublicKey; - // Refund transaction ID. - rtransaction_id: Integer; + // Signature by the exchange over a + // ``TALER_CoinPurseRefundConfirmationPS`` + // of purpose ``TALER_SIGNATURE_EXCHANGE_CONFIRM_PURSE_REFUND``. + exchange_sig: EddsaSignature; - // `EdDSA Signature <eddsa-sig>` authorizing the REFUND over a - // `TALER_MerchantRefundConfirmationPS` with - // purpose ``TALER_SIGNATURE_MERCHANT_REFUND_OK``. Made with - // the `public key of the merchant <merchant-pub>`. - merchant_sig: EddsaSignature; + // Public key used to sign 'exchange_sig'. + exchange_pub: EddsaPublicKey; } - .. ts:def:: CoinRecoupTransaction + .. ts:def:: CoinReserveOpenDepositTransaction - interface CoinRecoupTransaction { - type: "RECOUP"; + interface CoinReserveOpenDepositTransaction { + type: "RESERVE-OPEN-DEPOSIT"; // Offset of this entry in the reserve history. // Useful to request incremental histories via @@ -2930,211 +2626,16 @@ Coin History // The total amount of the coin's value absorbed // by this transaction. - // The current coin value can thus be computed by - // subtracting the amount from - // the coin's denomination value. - amount: Amount; + // Note that this means the amount given includes + // the deposit fee. + coin_contribution: Amount; - // Date when the operation was made. - timestamp: Timestamp; + // Signature of the reserve open operation being paid for. + reserve_sig: EddsaSignature; // Signature by the coin over a - // `TALER_RecoupRequestPS` with purpose - // ``TALER_SIGNATURE_WALLET_COIN_RECOUP``. - coin_sig: EddsaSignature; - - // Hash of the public denomination key used to sign the coin. - // Needed because 'coin_sig' signs over this, and - // that is important to fix the coin's denomination. - h_denom_pub: HashCode; - - // Coin blinding key. - coin_blind: DenominationBlindingKeyP; - - // Reserve receiving the recoup. - reserve_pub: EddsaPublicKey; - - // Signature by the exchange over a - // `TALER_RecoupConfirmationPS`, must be - // of purpose ``TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP``. - exchange_sig: EddsaSignature; - - // Public key of the private key used to create 'exchange_sig'. - exchange_pub: EddsaPublicKey; - - } - - .. ts:def:: CoinOldCoinRecoupTransaction - - interface CoinOldCoinRecoupTransaction { - type: "OLD-COIN-RECOUP"; - - // Offset of this entry in the reserve history. - // Useful to request incremental histories via - // the "start" query parameter. - history_offset: Integer; - - // The total amount of the coin's value restored - // by this transaction. - // The current coin value can thus be computed by - // adding the amount to the coin's denomination value. - amount: Amount; - - // Date when the operation was made. - timestamp: Timestamp; - - // Signature by the exchange over a - // `TALER_RecoupRefreshConfirmationPS` - // of purpose ``TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH``. - exchange_sig: EddsaSignature; - - // Public key of the private key used to create 'exchange_sig'. - exchange_pub: EddsaPublicKey; - - } - - .. ts:def:: CoinRecoupRefreshTransaction - - interface CoinRecoupRefreshTransaction { - type: "RECOUP-REFRESH"; - - // Offset of this entry in the reserve history. - // Useful to request incremental histories via - // the "start" query parameter. - history_offset: Integer; - - // The total amount of the coin's value absorbed - // by this transaction. - // The current coin value can thus be computed by - // subtracting this amounts from - // the coin's denomination value. - amount: Amount; - - // Date when the operation was made. - timestamp: Timestamp; - - // Signature by the coin over a `TALER_RecoupRequestPS` - // with purpose ``TALER_SIGNATURE_WALLET_COIN_RECOUP``. - coin_sig: EddsaSignature; - - // Hash of the public denomination key used to sign the coin. - // Needed because 'coin_sig' signs over this, and - // that is important to fix the coin's denomination. - h_denom_pub: HashCode; - - // Coin blinding key. - coin_blind: DenominationBlindingKeyP; - - // Signature by the exchange over a - // `TALER_RecoupRefreshConfirmationPS` - // of purpose ``TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH``. - exchange_sig: EddsaSignature; - - // Public key used to sign 'exchange_sig'. - exchange_pub: EddsaPublicKey; - - // Blinding factor of the revoked new coin. - new_coin_blinding_secret: DenominationBlindingKeySecret; - - // Blinded public key of the revoked new coin. - new_coin_ev: DenominationBlindingKeySecret; - } - - .. ts:def:: CoinPurseDepositTransaction - - interface CoinPurseDepositTransaction { - type: "PURSE-DEPOSIT"; - - // Offset of this entry in the reserve history. - // Useful to request incremental histories via - // the "start" query parameter. - history_offset: Integer; - - // The total amount of the coin's value absorbed - // by this transaction. - // Note that this means the amount given includes - // the deposit fee. The current coin value can thus be computed by - // subtracting the amount from - // the coin's denomination value. - amount: Amount; - - // Deposit fee. - deposit_fee: Amount; - - // Public key of the purse. - purse_pub: EddsaPublicKey; - - // Date when the purse was set to expire. - purse_expiration: Timestamp; - - // Signature by the coin over a - // `TALER_PurseDepositSignaturePS` of - // purpose ``TALER_SIGNATURE_PURSE_DEPOSIT``. - coin_sig: EddsaSignature; - - // Hash of the public denomination key used to sign the coin. - // Needed because 'coin_sig' signs over this, and - // that is important to fix the coin's denomination. - h_denom_pub: HashCode; - - } - - .. ts:def:: CoinPurseRefundTransaction - - interface CoinPurseRefundTransaction { - type: "PURSE-REFUND"; - - // Offset of this entry in the reserve history. - // Useful to request incremental histories via - // the "start" query parameter. - history_offset: Integer; - - // The total amount of the coin's value restored - // by this transaction. - // The amount given excludes the refund fee. - // The current coin value can thus be computed by - // adding the amount to the coin's denomination value. - amount: Amount; - - // Refund fee (of the coin's denomination). The deposit - // fee will be waived. - refund_fee: Amount; - - // Public key of the purse that expired. - purse_pub: EddsaPublicKey; - - // Signature by the exchange over a - // ``TALER_CoinPurseRefundConfirmationPS`` - // of purpose ``TALER_SIGNATURE_EXCHANGE_CONFIRM_PURSE_REFUND``. - exchange_sig: EddsaSignature; - - // Public key used to sign 'exchange_sig'. - exchange_pub: EddsaPublicKey; - - } - - .. ts:def:: CoinReserveOpenDepositTransaction - - interface CoinReserveOpenDepositTransaction { - type: "RESERVE-OPEN-DEPOSIT"; - - // Offset of this entry in the reserve history. - // Useful to request incremental histories via - // the "start" query parameter. - history_offset: Integer; - - // The total amount of the coin's value absorbed - // by this transaction. - // Note that this means the amount given includes - // the deposit fee. - coin_contribution: Amount; - - // Signature of the reserve open operation being paid for. - reserve_sig: EddsaSignature; - - // Signature by the coin over a - // `TALER_ReserveOpenDepositSignaturePS` of - // purpose ``TALER_SIGNATURE_RESERVE_OPEN_DEPOSIT``. + // `TALER_ReserveOpenDepositSignaturePS` of + // purpose ``TALER_SIGNATURE_RESERVE_OPEN_DEPOSIT``. coin_sig: EddsaSignature; } @@ -5386,180 +4887,675 @@ regulatory compliance. // basically encode any URL path (and optional arguments). id?: string; - } + } + + .. ts:def:: KycCheckPublicInformation + + // Since **vATTEST**. + interface KycCheckPublicInformation { + + // English description of the check. + description: string; + + // Map from IETF BCP 47 language tags to localized + // description texts. + description_i18n ?: { [lang_tag: string]: string }; + + // FIXME: is the above in any way sufficient + // to begin the check? Do we not need at least + // something more??!? + } + + +.. http:post:: /kyc-upload/$ID + + The ``/kyc-upload/$ID`` POST endpoint allows the SPA to upload + client-provided evidence. The ``$ID`` will be provided as part of the + ``/kyc-info`` body. This is for checks of type ``FORM``. In practice, + ``$ID`` will encode the ``$ACCESS_TOKEN``, legitimization measure serial ID + (to disambiguate) and the index of the selected measure (but these details + should be irrelevant for the client). + + This endpoint was introduced in protocol **v20**. + + **Request:** + + Basically oriented along the possible formats of a HTTP form being + POSTed. Details will depend on the form. The server will try to decode the + uploaded body from whatever format it is provided in. + + **Response:** + + :http:statuscode:`204 No Content`: + The information was successfully uploaded. The SPA should fetch + an updated ``/kyc-info/``. + :http:statuscode:`404 Not Found`: + The ``$ID`` is unknown to the exchange. + :http:statuscode:`409 Conflict`: + The upload conflicts with a previous upload. + :http:statuscode:`413 Request Entity Too Large`: + The body is too large. + +.. http:post:: /kyc-start/$ID + + The ``/kyc-start/$ID`` POST endpoint allows the SPA to set up a new external + KYC process. It will return the URL that the client must GET to begin the + KYC process. The SPA should probably open this URL in a new window or tab. + The ``$ID`` will be provided as part of the ``/kyc-info`` body. In + practice, ``$ID`` will encode the ``$ACCESS_TOKEN``, legitimization measure + serial ID (to disambiguate) and the index of the selected measure (but these + details should be irrelevant for the client). + + **Request:** + + Use empty JSON body for now. + + **Response:** + + :http:statuscode:`200 Ok`: + The KYC process was successfully initiated. The URL is in a + `KycProcessStartInformation` object. + :http:statuscode:`404 Not Found`: + The ``$ID`` is unknown to the exchange. + + **Details:** + + .. ts:def:: KycProcessStartInformation + + interface KycProcessStartInformation { + + // URL to open. + redirect_url: string; + } + + .. note:: + + As this endpoint is involved in every KYC check at the beginning, this + is also the place where we could integrate the payment process for the KYC fee + in the future (since **vATTEST**). + +.. http:get:: /kyc-proof/$PROVIDER_NAME?state=$H_PAYTO + + Upon completion of the process at the external KYC provider, the provider + must redirect the client (browser) to trigger a GET request to a new + ``/kyc-proof/$H_PAYTO/$PROVIDER_NAME`` endpoint. Once this endpoint is + triggered, the exchange will pass the received arguments to the respective + logic plugin. The logic plugin will then (asynchronously) update the KYC + status of the user. The logic plugin should redirect the user to the KYC + SPA. This endpoint deliberately does not use the ``$ACCESS_TOKEN`` as the + external KYC provider should not learn that token. + + This endpoint is thus accessed from the user's browser at the *end* of a + KYC process, possibly providing the exchange with additional + credentials to obtain the results of the KYC process. + Specifically, the URL arguments should provide + information to the exchange that allows it to verify that the + user has completed the KYC process. The details depend on + the logic, which is selected by the "$PROVIDER_NAME". + + While this is a GET (and thus safe, and idempotent), the operation + may actually trigger significant changes in the exchange's state. + In particular, it may update the KYC status of a particular + payment target. + + **Request:** + + Details on the request depend on the specific KYC logic + that was used. + + If the KYC plugin logic is OAuth 2.0, the query parameters are: + + :query code=CODE: + OAuth 2.0 code argument. + :query state=STATE: + OAuth 2.0 state argument with the H_PAYTO. + + .. note:: + + Depending on the OAuth variant used, additional + query parameters may need to be passed here. + + **Response:** + + Given that the response is returned to a user using a browser and **not** to + a Taler wallet, the response format is in human-readable HTML and not in + machine-readable JSON. + + :http:statuscode:`302 Found`: + The KYC operation succeeded and the + payment target is now authorized to transact. + The browser is redirected to a human-readable + page configured by the exchange operator. + :http:statuscode:`401 Unauthorized`: + The provided authorization token is invalid. + :http:statuscode:`404 Not found`: + The payment target is unknown. + :http:statuscode:`502 Bad Gateway`: + The exchange received an invalid reply from the + legitimization service. + :http:statuscode:`504 Gateway Timeout`: + The exchange did not receive a reply from the legitimization + service within a reasonable time period. + + +.. http:get:: /kyc-webhook/$PROVIDER_NAME/* +.. http:post:: /kyc-webhook/$PROVIDER_NAME/* +.. http:get:: /kyc-webhook/$LOGIC/* +.. http:post:: /kyc-webhook/$LOGIC/* + + All of the above endpoints can be used to update KYC status of a particular + payment target. They provide information to the KYC logic of the exchange + that allows it to verify that the user has completed the KYC process. May + be a GET or a POST request, depending on the specific "$LOGIC" and/or the + "$PROVIDER_NAME". + + **Request:** + + Details on the request depend on the specific KYC logic + that was used. + + **Response:** + + :http:statuscode:`204 No content`: + The operation succeeded. + :http:statuscode:`404 Not found`: + The specified logic is unknown. + + +-------------- +AML operations +-------------- + +This API is only for designated AML officers. It is used +to allow exchange staff to monitor suspicious transactions +and freeze or unfreeze accounts suspected of money laundering. + + +.. http:get:: /aml/$OFFICER_PUB/measures + + To enable the AML staff SPA to give AML staff a choice of possible measures, a + new endpoint ``/aml/$OFFICER_PUB/measures`` is added that allows the AML SPA + to dynamically GET the list of available measures. It returns a list of known + KYC checks (by name) with their descriptions and a list of AML programs with + information about the required context. + + This endpoint was introduced in protocol **v20**. + + **Request:** + + *Taler-AML-Officer-Signature*: + The client must provide Base-32 encoded EdDSA signature with + ``$OFFICER_PRIV``, affirming the desire to obtain AML data. Note that + this is merely a simple authentication mechanism, the details of the + request are not protected by the signature. + + **Response:** + + :http:statuscode:`200 Ok`: + Information about possible measures is returned in a + `AvailableMeasureSummary` object. + + **Details:** + + .. ts:def:: AvailableMeasureSummary + + interface AvailableMeasureSummary { + + // Available original measures that can be + // triggered directly by default rules. + roots: { "$measure_name" : MeasureInformation; }; + + // Available AML programs. + programs: { "$prog_name" : AmlProgramRequirement; }; + + // Available KYC checks. + checks: { "$check_name" : KycCheckInformation; }; + + } + + .. ts:def:: MeasureInformation + + interface MeasureInformation { + + // Name of a KYC check. + check_name: string; + + // Name of an AML program. + prog_name: string; + + // Context for the check. Optional. + context?: Object; + + } + + .. ts:def:: AmlProgramRequirement + + interface AmlProgramRequirement { + + // Description of what the AML program does. + description: string; + + // List of required field names in the context to run this + // AML program. SPA must check that the AML staff is providing + // adequate CONTEXT when defining a measure using this program. + context: string[]; + + // List of required attribute names in the + // input of this AML program. These attributes + // are the minimum that the check must produce + // (it may produce more). + inputs: string[]; + + } + + .. ts:def:: KycCheckInformation + + interface KycCheckInformation { + + // Description of the KYC check. Should be shown + // to the AML staff but will also be shown to the + // client when they initiate the check in the KYC SPA. + description: string; + + // Map from IETF BCP 47 language tags to localized + // description texts. + description_i18n ?: { [lang_tag: string]: string}; + + // Names of the fields that the CONTEXT must provide + // as inputs to this check. + // SPA must check that the AML staff is providing + // adequate CONTEXT when defining a measure using + // this check. + requires: string[]; + + // Names of the attributes the check will output. + // SPA must check that the outputs match the + // required inputs when combining a KYC check + // with an AML program into a measure. + outputs: string[]; + + // Name of a root measure taken when this check fails. + fallback: string; + } + +.. http:get:: /aml/$OFFICER_PUB/kyc-statistics/$NAME + + Returns the number of KYC events matching the given event type ``$NAME`` in + the specified time range. Note that this query can be slow as the + statistics are computed on-demand. (This is OK as such requests should be + rare.) + + This endpoint was introduced in protocol **v20**. + + **Request:** + + *Taler-AML-Officer-Signature*: + The client must provide Base-32 encoded EdDSA signature with + ``$OFFICER_PRIV``, affirming the desire to obtain AML data. Note that this + is merely a simple authentication mechanism, the details of the request are + not protected by the signature. + + :query start_date=TIMESTAMP: + *Optional*. Specifies the date when to + start looking (inclusive). If not given, the start time of the + exchange operation is used. The TIMESTAMP is given + in seconds since the UNIX epoch. + :query end_date=TIMESTAMP: + *Optional*. Specifies the date when to + stop looking (exclusive). If not given, the current date is used. The TIMESTAMP is given + in seconds since the UNIX epoch. + + **Response:** + + :http:statuscode:`200 OK`: + The responds will be an `EventCounter` message. + + **Details:** + + .. ts:def:: EventCounter + + interface EventCounter { + // Number of events of the specified type in + // the given range. + counter: Integer; + } + + +.. http:get:: /aml/$OFFICER_PUB/decisions + + **Request:** + + *Taler-AML-Officer-Signature*: + The client must provide Base-32 encoded EdDSA signature with + ``$OFFICER_PRIV``, affirming the desire to obtain AML data. Note that + this is merely a simple authentication mechanism, the details of the + request are not protected by the signature. + + This endpoint was introduced in this form in protocol **v20**. + + :query limit: + *Optional*. takes value of the form ``N (-N)``, so that at + most ``N`` values strictly older (younger) than ``start`` are returned. + Defaults to ``-20`` to return the last 20 entries (before ``start``). + :query offset: + *Optional*. Row number threshold, see ``delta`` for its + interpretation. Defaults to ``INT64_MAX``, namely the biggest row id + possible in the database. + :query h_payto: + *Optional*. Account selector. All matching accounts are returned if this + filter is absent, otherwise only decisions for this account. + :query active: + *Optional*. If set to yes, only return active decisions, if no only + decisions that have been superceeded. Do not give (or use "all") to + see all decisions regardless of activity status. + :query investigation: + *Optional*. If set to yes, only return accounts that are under + AML investigation, if no only accounts that are not under investigation. + Do not give (or use "all") to see all accounts regardless of + investigation status. + + **Response:** + + :http:statuscode:`200 OK`: + The response will be an `AmlDecisionsResponse` message. + :http:statuscode:`204 No content`: + There are no matching AML records. + :http:statuscode:`403 Forbidden`: + The signature is invalid. + :http:statuscode:`404 Not found`: + The designated AML account is not known. + :http:statuscode:`409 Conflict`: + The designated AML account is not enabled. + + **Details:** + + .. ts:def:: AmlDecisionsResponse + + interface AmlDecisionsResponse { + + // Array of AML decisions matching the query. + records: AmlDecision[]; + } + + .. ts:def:: AmlDecision + + interface AmlDecision { + + // Which payto-address is this record about. + // Identifies a GNU Taler wallet or an affected bank account. + h_payto: PaytoHash; + + // Row ID of the record. Used to filter by offset. + rowid: Integer; + + // Justification for the decision. NULL if none + // is available. + justification?: string; + + // When was the decision made? + decision_time: Timestamp; + + // Free-form properties about the account. + // Can be used to store properties such as PEP, + // risk category, type of business, hits on + // sanctions lists, etc. + properties?: AccountProperties; + + // What are the new rules? + limits: LegitimizationRuleSet; + + // True if the account is under investigation by AML staff + // after this decision. + to_investigate: boolean; + + // True if this is the active decision for the + // account. + is_active: boolean; + + } + + .. ts:def:: AccountProperties + + // All fields in this object are optional. The actual + // properties collected depend fully on the discretion + // of the exchange operator; + // however, some common fields are standardized + // and thus described here. + interface AccountProperties { + + // True if this is a politically exposed account. + // Rules for classifying accounts as politically + // exposed are country-dependent. + pep?: boolean; + + // True if this is a sanctioned account. + // Rules for classifying accounts as sanctioned + // are country-dependent. + sanctioned?: boolean; + + // True if this is a high-risk account. + // Rules for classifying accounts as at-risk + // are exchange operator-dependent. + high_risk?: boolean; + + // Business domain of the account owner. + // The list of possible business domains is + // operator- or country-dependent. + business_domain?: string; + + // Is the client's account currently frozen? + is_frozen?: boolean; + + // Was the client's account reported to the authorities? + was_reported?: boolean; + + } + + .. ts:def:: LegitimizationRuleSet + + interface LegitimizationRuleSet { + + // When does this set of rules expire and + // we automatically transition to the successor + // measure? + expiration_time: Timestamp; + + // Name of the measure to apply when the expiration time is + // reached. If not set, we refer to the default + // set of rules (and the default account state). + successor_measure?: string; + + // Legitimization rules that are to be applied + // to this account. + rules: KycRule[]; + + // Custom measures that KYC rules and the + // ``successor_measure`` may refer to. + custom_measures: { "$measure_name" : MeasureInformation; }; + + } + + .. ts:def:: KycRule + + interface KycRule { + + // Type of operation to which the rule applies. + operation_type: string; - .. ts:def:: KycCheckPublicInformation + // The measures will be taken if the given + // threshold is crossed over the given timeframe. + threshold: Amount; - // Since **vATTEST**. - interface KycCheckPublicInformation { + // Over which duration should the ``threshold`` be + // computed. All amounts of the respective + // ``operation_type`` will be added up for this + // duration and the sum compared to the ``threshold``. + timeframe: RelativeTime; - // English description of the check. - description: string; + // Array of names of measures to apply. + // Names listed can be original measures or + // custom measures from the `AmlOutcome`. + // A special measure "verboten" is used if the + // threshold may never be crossed. + measures: string[]; - // Map from IETF BCP 47 language tags to localized - // description texts. - description_i18n ?: { [lang_tag: string]: string }; + // If multiple rules apply to the same account + // at the same time, the number with the highest + // rule determines which set of measures will + // be activated and thus become visible for the + // user. + display_priority: Integer; - // FIXME: is the above in any way sufficient - // to begin the check? Do we not need at least - // something more??!? - } + // True if the rule (specifically, operation_type, + // threshold, timeframe) and the general nature of + // the measures (verboten or approval required) + // should be exposed to the client. + // Defaults to "false" if not set. + exposed?: boolean; + + // True if all the measures will eventually need to + // be satisfied, false if any of the measures should + // do. Primarily used by the SPA to indicate how + // the measures apply when showing them to the user; + // in the end, AML programs will decide after each + // measure what to do next. + // Default (if missing) is false. + is_and_combinator?: boolean; + } -.. http:post:: /kyc-upload/$ID +.. http:get:: /aml/$OFFICER_PUB/attributes/$H_PAYTO - The ``/kyc-upload/$ID`` POST endpoint allows the SPA to upload - client-provided evidence. The ``$ID`` will be provided as part of the - ``/kyc-info`` body. This is for checks of type ``FORM``. In practice, - ``$ID`` will encode the ``$ACCESS_TOKEN``, legitimization measure serial ID - (to disambiguate) and the index of the selected measure (but these details - should be irrelevant for the client). + Obtain attributes obtained as part of AML/KYC processes for a + given account. This endpoint was introduced in protocol **v20**. **Request:** - Basically oriented along the possible formats of a HTTP form being - POSTed. Details will depend on the form. The server will try to decode the - uploaded body from whatever format it is provided in. + *Taler-AML-Officer-Signature*: + The client must provide Base-32 encoded EdDSA signature with + ``$OFFICER_PRIV``, affirming the desire to obtain AML data. Note that + this is merely a simple authentication mechanism, the details of the + request are not protected by the signature. + + :query limit: + *Optional*. takes value of the form ``N (-N)``, so that at + most ``N`` values strictly older (younger) than ``start`` are returned. + Defaults to ``-20`` to return the last 20 entries (before ``start``). + :query offset: + *Optional*. Row number threshold, see ``delta`` for its + interpretation. Defaults to ``INT64_MAX``, namely the biggest row id + possible in the database. **Response:** - :http:statuscode:`204 No Content`: - The information was successfully uploaded. The SPA should fetch - an updated ``/kyc-info/``. - :http:statuscode:`404 Not Found`: - The ``$ID`` is unknown to the exchange. + :http:statuscode:`200 OK`: + The responds will be an `KycAttributes` message. + :http:statuscode:`204 No content`: + There are no matching KYC attributes. + :http:statuscode:`403 Forbidden`: + The signature is invalid. + :http:statuscode:`404 Not found`: + The designated AML account is not known. :http:statuscode:`409 Conflict`: - The upload conflicts with a previous upload. - :http:statuscode:`413 Request Entity Too Large`: - The body is too large. - -.. http:post:: /kyc-start/$ID - - The ``/kyc-start/$ID`` POST endpoint allows the SPA to set up a new external - KYC process. It will return the URL that the client must GET to begin the - KYC process. The SPA should probably open this URL in a new window or tab. - The ``$ID`` will be provided as part of the ``/kyc-info`` body. In - practice, ``$ID`` will encode the ``$ACCESS_TOKEN``, legitimization measure - serial ID (to disambiguate) and the index of the selected measure (but these - details should be irrelevant for the client). + The designated AML account is not enabled. - **Request:** + .. ts:def:: KycAttributes - Use empty JSON body for now. + interface KycAttributes { - **Response:** + // Matching KYC attribute history of the account. + details: KycAttributeCollectionEvent[]; - :http:statuscode:`200 Ok`: - The KYC process was successfully initiated. The URL is in a - `KycProcessStartInformation` object. - :http:statuscode:`404 Not Found`: - The ``$ID`` is unknown to the exchange. + } - **Details:** + .. ts:def:: KycAttributeCollectionEvent - .. ts:def:: KycProcessStartInformation + interface KycAttributeCollectionEvent { - interface KycProcessStartInformation { + // Row ID of the record. Used to filter by offset. + rowid: Integer; - // URL to open. - redirect_url: string; - } + // Name of the provider + // which was used to collect the attributes. NULL if they were + // just uploaded via a form by the account owner. + provider_name?: string; - .. note:: + // The collected KYC data. NULL if the attribute data could not + // be decrypted (internal error of the exchange, likely the + // attribute key was changed). + attributes?: Object; - As this endpoint is involved in every KYC check at the beginning, this - is also the place where we could integrate the payment process for the KYC fee - in the future (since **vATTEST**). + // Time when the KYC data was collected + collection_time: Timestamp; -.. http:get:: /kyc-proof/$PROVIDER_NAME?state=$H_PAYTO + } - Upon completion of the process at the external KYC provider, the provider - must redirect the client (browser) to trigger a GET request to a new - ``/kyc-proof/$H_PAYTO/$PROVIDER_NAME`` endpoint. Once this endpoint is - triggered, the exchange will pass the received arguments to the respective - logic plugin. The logic plugin will then (asynchronously) update the KYC - status of the user. The logic plugin should redirect the user to the KYC - SPA. This endpoint deliberately does not use the ``$ACCESS_TOKEN`` as the - external KYC provider should not learn that token. - This endpoint is thus accessed from the user's browser at the *end* of a - KYC process, possibly providing the exchange with additional - credentials to obtain the results of the KYC process. - Specifically, the URL arguments should provide - information to the exchange that allows it to verify that the - user has completed the KYC process. The details depend on - the logic, which is selected by the "$PROVIDER_NAME". + .. http:post:: /aml/$OFFICER_PUB/decision - While this is a GET (and thus safe, and idempotent), the operation - may actually trigger significant changes in the exchange's state. - In particular, it may update the KYC status of a particular - payment target. + Make an AML decision. Triggers the respective action and + records the justification. **Request:** - Details on the request depend on the specific KYC logic - that was used. - - If the KYC plugin logic is OAuth 2.0, the query parameters are: + The request must be an `AmlDecisionRequest` message. - :query code=CODE: - OAuth 2.0 code argument. - :query state=STATE: - OAuth 2.0 state argument with the H_PAYTO. + **Response** - .. note:: + :http:statuscode:`204 No content`: + The AML decision has been executed and recorded successfully. + :http:statuscode:`403 Forbidden`: + The signature is invalid (or the AML officer not known). + :http:statuscode:`404 Not found`: + The payto-address the decision was made for is unknown to the exchange. + :http:statuscode:`409 Conflict`: + The designated AML account is not enabled or a more recent + decision was already submitted. - Depending on the OAuth variant used, additional - query parameters may need to be passed here. + **Details:** - **Response:** + .. ts:def:: AmlDecisionRequest - Given that the response is returned to a user using a browser and **not** to - a Taler wallet, the response format is in human-readable HTML and not in - machine-readable JSON. + interface AmlDecisionRequest { - :http:statuscode:`302 Found`: - The KYC operation succeeded and the - payment target is now authorized to transact. - The browser is redirected to a human-readable - page configured by the exchange operator. - :http:statuscode:`401 Unauthorized`: - The provided authorization token is invalid. - :http:statuscode:`404 Not found`: - The payment target is unknown. - :http:statuscode:`502 Bad Gateway`: - The exchange received an invalid reply from the - legitimization service. - :http:statuscode:`504 Gateway Timeout`: - The exchange did not receive a reply from the legitimization - service within a reasonable time period. + // Human-readable justification for the decision. + justification: string; + // Which payto-address is the decision about? + // Identifies a GNU Taler wallet or an affected bank account. + h_payto: PaytoHash; -.. http:get:: /kyc-webhook/$PROVIDER_NAME/* -.. http:post:: /kyc-webhook/$PROVIDER_NAME/* -.. http:get:: /kyc-webhook/$LOGIC/* -.. http:post:: /kyc-webhook/$LOGIC/* + // What are the new rules? + // New since protocol **v20**. + new_rules: LegitimizationRuleSet; - All of the above endpoints can be used to update KYC status of a particular - payment target. They provide information to the KYC logic of the exchange - that allows it to verify that the user has completed the KYC process. May - be a GET or a POST request, depending on the specific "$LOGIC" and/or the - "$PROVIDER_NAME". + // What are the new account properties? + // New since protocol **v20**. + properties: AccountProperties; - **Request:** + // New measure to apply immediately to the account. + // Should typically be used to give the user some + // information or request additional information. + // Use "verboten" to communicate to the customer + // that there is no KYC check that could be passed + // to modify the ``new_rules``. + // New since protocol **v20**. + new_measure?: string; - Details on the request depend on the specific KYC logic - that was used. + // True if the account should remain under investigation by AML staff. + // New since protocol **v20**. + keep_investigating: boolean; - **Response:** + // Signature by the AML officer over a `TALER_AmlDecisionPS`. + // Must have purpose ``TALER_SIGNATURE_MASTER_AML_KEY``. + officer_sig: EddsaSignature; - :http:statuscode:`204 No content`: - The operation succeeded. - :http:statuscode:`404 Not found`: - The specified logic is unknown. + // When was the decision made? + decision_time: Timestamp; + } --------------- Reserve control diff --git a/core/api-merchant.rst b/core/api-merchant.rst @@ -24,9 +24,6 @@ Merchant Backend RESTful API ============================ -.. contents:: Table of Contents - :local: - ----------------------- Base URLs and Instances ----------------------- diff --git a/core/api-terminal.rst b/core/api-terminal.rst @@ -22,8 +22,6 @@ Terminal API ============ -.. contents:: Table of Contents - :local: Introduction ------------ @@ -75,7 +73,7 @@ Config provider_name: string; // The currency supported by this Terminal-API - // must be the same as the currency specified + // must be the same as the currency specified // in the currency field of the wire gateway config currency: string; @@ -286,7 +284,7 @@ Config // backend to check that the credit was confirmed. provider_transaction_id?: string; - // The fees which the customer had to pay to the + // The fees which the customer had to pay to the // provider terminal_fees?: Amount; diff --git a/index.rst b/index.rst @@ -60,12 +60,13 @@ Documentation Overview taler-merchant-pos-terminal taler-wallet taler-exchange-manual + taler-kyc-manual taler-challenger-manual taler-auditor-manual - taler-developer-manual libeufin/index - system-administration/index design-documents/index + taler-developer-manual + system-administration/index global-licensing manindex genindex diff --git a/taler-exchange-manual.rst b/taler-exchange-manual.rst @@ -944,324 +944,6 @@ Such a wire gateway configuration can be tested with the following commands: On success, you will see some of your account's transaction history (or an empty history), while on failure you should see an error message. -.. _LegalSetup: - -Legal Setup -=========== - -This chapter describes how to setup certain legal aspects of a GNU Taler -exchange. Users that just want to set up an exchange as an experiment without -legal requirements can safely skip these steps. - - -Legal conditions for using the service --------------------------------------- - -.. include:: frags/legal.rst - - -KYC Configuration -================= - -To legally operate, Taler exchange operators may have to comply with KYC -regulation that requires financial institutions to identify parties involved -in transactions at certain points. - -Taler permits an exchange to require KYC data under the following circumstances: - - * Customer withdraws money over a threshold - * Wallet receives (via refunds) money resulting in a balance over a threshold - * Wallet receives money via P2P payments over a threshold - * Merchant receives money over a threshold - * Reserve is "opened" for invoicing (**planned feature**) - -Any of the above requests can trigger the KYC process, -which can be illustrated as follows: - -.. image:: images/kyc-process.png - -At the end of the KYC process, the wallet re-tries the -original request, and assuming KYC was successful, the -request should then succeed. - - -Taler KYC Terminology ---------------------- - -* **Check**: A check establishes a particular attribute of a user, such as - their name based on an ID document and lifeness, mailing address, phone - number, taxpayer identity, etc. - -* **Type of operation**: The operation type determines which Taler-specific - operation has triggered the KYC requirement. We support four types of - operation: withdraw (by customer), deposit (by merchant), P2P receive (by - wallet) and (high) wallet balance. - -* **Condition**: A condition specifies when KYC is required. Conditions - include the *type of operation*, a threshold amount (e.g. above EUR:1000) - and possibly a time period (e.g. over the last month). - -* **Cost**: Metric for the business expense for a KYC check at a certain - *provider*. Not in any currency, costs are simply relative and non-negative - values. Costs are considered when multiple choices are allowed by the - *configuration*. - -* **Expiration**: KYC legitimizations may be outdated. Expiration rules - determine when *checks* have to be performed again. - -* **Legitimization rules**: The legitimization rules determine under which - *conditions* which *checks* must be performend and the *expiration* time - period for the *checks*. - -* **Logic**: Logic refers to a specific bit of code (realized as an exchange - plugin) that enables the interaction with a specific *provider*. Logic - typically requires configuration for access control (such as an - authorization token) and possibly the endpoint of the specific *provider* - implementing the respective API. - -* **Provider**: A provider performs a specific set of *checks* at a certain - *cost*. Interaction with a provider is performed by provider-specific - *logic*. - -Configuration of possible KYC/AML checks -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The configuration specifies a set of possible KYC checks offered by external -providers. The names of the configuration sections must being with -``kyc-check-`` followed by an arbitrary ``$CHECK_NAME``. - -The list of possible FORM names is fixed in the SPA -for a particular exchange release. - -The outcome of *any* check should always be uploaded encrypted into the -``kyc_attributes`` table. It MUST include an ``expiration_time``. - - - -KYC Configuration Options -------------------------- - -The KYC configuration determines the *legitimization rules*, and specifies -which providers offer which *checks* at what *cost*. - -The configuration specifies a set of providers, one per configuration section. The names of the configuration -sections must being with ``kyc-proider-`` followed by -an arbitrary ``$PROVIDER_ID``: - -.. code-block:: ini - :caption: /etc/taler/conf.d/exchange-kyc-providers.conf - - [kyc-provider-$PROVIDER_ID] - # How expensive is it to use this provider? - # Used to pick the cheapest provider possible. - COST = 42 - # Which plugin is responsible for this provider? - # Choices include "oauth2", "kycaid" and "persona". - LOGIC = oauth2 - # Which type of user does this provider handle? - # Either INDIVIDUAL or BUSINESS. - USER_TYPE = INDIVIDUAL - # Which checks does this provider provide? - # List of strings, no specific semantics. - PROVIDED_CHECKS = SMS GOVID PHOTO - # Plus additional logic-specific options, e.g.: - AUTHORIZATION_TOKEN = superdupersecret - FORM_ID = business_legi_form - # How long is the check considered valid? - EXPIRATION = 3650d - -The configuration also must specify a set of legitimization requirements, again one -per configuration section: - -.. code-block:: ini - :caption: /etc/taler/conf.d/exchange-kyc-rules.conf - - [kyc-legitimization-$RULE_NAME] - # Operation that triggers this legitimization. - # Must be one of WITHDRAW, DEPOSIT, P2P-RECEIVE - # or WALLET-BALANCE. - OPERATION_TYPE = WITHDRAW - # Required checks to be performed. - # List of strings, must individually match the - # strings in one or more provider's PROVIDED_CHECKS. - REQUIRED_CHECKS = SMS GOVID - # Threshold amount above which the legitimization is - # triggered. The total must be exceeded in the given - # timeframe. - THRESHOLD = KUDOS:100 - # Timeframe over which the amount to be compared to - # the THRESHOLD is calculated. Can be 'forever'. - # Ignored for WALLET-BALANCE. - TIMEFRAME = 30d - - -OAuth 2.0 specifics -------------------- - -In terms of configuration, the OAuth 2.0 logic requires the respective client -credentials to be configured apriori to enable access to the legitimization -service. The OAuth 2.0 configuration options are: - -.. code-block:: ini - :caption: /etc/taler/conf.d/exchange-oauth2.conf - - [kyc-provider-example-oauth2] - LOGIC = oauth2 - # (generic options omitted) - # How long is the KYC check valid? - KYC_OAUTH2_VALIDITY = forever - - # URL to which we redirect the user for the login process - KYC_OAUTH2_AUTHORIZE_URL = "http://kyc.example.com/authorize" - # URL where we POST the user's authentication information - KYC_OAUTH2_TOKEN_URL = "http://kyc.example.com/token" - # URL of the user info access point. - KYC_OAUTH2_INFO_URL = "http://kyc.example.com/info" - - # Where does the client get redirected upon completion? - KYC_OAUTH2_POST_URL = "http://example.com/thank-you" - - # For authentication to the OAuth2.0 service - KYC_OAUTH2_CLIENT_ID = testcase - KYC_OAUTH2_CLIENT_SECRET = password - - # Mustach template that converts OAuth2.0 data about the user - # into GNU Taler standardized attribute data. - KYC_OAUTH2_CONVERTER_HELPER = taler-exchange-kyc-oauth2-challenger.sh - -The converter helper is expected to be customized to the selected OAuth2.0 -service: different services may return different details about the user or -business, hence there cannot be a universal converter for all purposes. The -default shell script uses the ``jq`` tool to convert the JSON returned by the -service into the KYC attributes (also in JSON) expected by the exchange. The -script will need to be adjusted based on the attributes collected by the -specific backend. - -The Challenger service for address validation supports OAuth2.0, but does not -have a static AUTHORIZE_URL. Instead, the AUTHORIZE_URL must be enabled by the client -using a special authenticated request to the Challenger's ``/setup`` endpoint. -The exchange supports this by appending ``#setup`` to the AUTHORIZE_URL (note -that fragments are illegal in OAuth2.0 URLs). Be careful to quote the URL, -as ``#`` is otherwise interpreted as the beginning of a comment by the -configuration file syntax. - -.. code-block:: ini - :caption: /etc/taler/conf.d/exchange-challenger-oauth2.conf - - [kyc-provider-challenger-oauth2] - LOGIC = oauth2 - KYC_OAUTH2_AUTHORIZE_URL = "http://challenger.example.com/authorize/#setup" - KYC_OAUTH2_TOKEN_URL = "http://challenger.example.com/token" - KYC_OAUTH2_INFO_URL = "http://challenger.example.com/info" - -When using OAuth 2.0, the *CLIENT REDIRECT URI* must be set to the -``/kyc-proof/$PROVIDER_SECTION`` endpoint. For example, given the -configuration above and an exchange running on the host -``exchange.example.com``, the redirect URI would be -``https://exchange.example.com/kyc-proof/kyc-provider-challenger-oauth2/``. - - - -Persona specifics ------------------ - -We use the hosted flow. The Persona endpoints return a ``request-id``, which -we log for diagnosis. - -Persona should be configured to use the ``/kyc-webhook/`` endpoint of the -exchange to notify the exchange about the completion of KYC processes. The -webhook is authenticated using a shared secret, which should be in the -configuration. To use the Persona webhook, you must set the webhook URL in -the Persona service to ``$EXCHANGE_BASE_URL/kyc-webhook/$SECTION_NAME/`` where -``$SECTION_NAME`` is the name of the configuration section. You should also -extract the authentication token for the webhook and put it into the -configuration as shown above. - - -.. code-block:: ini - :caption: /etc/taler/conf.d/exchange-persona.conf - - [kyclogic-persona] - # Webhook authorization token. Global for all uses - # of the persona provider! - WEBHOOK_AUTH_TOKEN = wbhsec_698b5a19-c790-47f6-b396-deb572ec82f9 - - [kyc-provider-example-persona] - LOGIC = persona - # (generic options omitted) - - # How long is the KYC check valid? - KYC_PERSONA_VALIDITY = 365d - - # Which subdomain is used for our API? - KYC_PERSONA_SUBDOMAIN = taler - - # Authentication token to use. - KYC_PERSONA_AUTH_TOKEN = persona_sandbox_42XXXX - - # Form to use. - KYC_PERSONA_TEMPLATE_ID = itempl_Uj6Xxxxx - - # Where do we redirect to after KYC finished successfully. - KYC_PERSONA_POST_URL = "https://taler.net/kyc-done" - - # Salt to give to requests for idempotency. - # Optional. - # KYC_PERSONA_SALT = salt - - # Helper to convert JSON with KYC data returned by Persona into GNU Taler - # internal format. Should probably always be set to some variant of - # "taler-exchange-kyc-persona-converter.sh". - KYC_PERSONA_CONVERTER_HELPER = "taler-exchange-kyc-persona-converter.sh" - -The converter helper is expected to be customized to the -selected template: different templates may return different details -about the user or business, hence there cannot be a universal converter -for all purposes. The default shell script uses the ``jq`` tool to -convert the JSON returned by Persona into the KYC attributes (also -in JSON) expected by the exchange. The script will need to be adjusted -based on the attributes collected by the specific template. - - -KYC AID specifics ------------------ - -We use the hosted flow. - -KYCAID must be configured to use the ``/kyc-webhook/$SECTION_NAME/`` endpoint -of the exchange to notify the exchange about the completion of KYC processes. - -.. code-block:: ini - :caption: /etc/taler/conf.d/exchange-kycaid.conf - - [kyc-provider-example-kycaid] - LOGIC = kycaid - # (generic options omitted) - - # How long is the KYC check valid? - KYC_KYCAID_VALIDITY = 365d - - # Authentication token to use. - KYC_KYCAID_AUTH_TOKEN = XXX - - # Form to use. - KYC_KYCAID_FORM_ID = XXX - - # URL to go to after the process is complete. - KYC_KYCAID_POST_URL = "https://taler.net/kyc-done" - - # Script to convert the KYCAID data into the Taler format. - KYC_KYCAID_CONVERTER_HELPER = taler-exchange-kyc-kycaid-converter.sh - - -The converter helper is expected to be customized to the selected template: -different templates may return different details about the user or business, -hence there cannot be a universal converter for all purposes. The default -shell script uses the ``jq`` tool to convert the JSON returned by Persona into -the KYC attributes (also in JSON) expected by the exchange. The script will -need to be adjusted based on the attributes collected by the specific -template. - .. _Deployment: @@ -1615,342 +1297,6 @@ of ``taler-exchange-offline``. under highly unusual (“emergency”) conditions and not in normal operation. -AML Configuration -================= - -The AML configuration steps are used to add or remove keys of exchange -operator staff that are responsible for anti-money laundering (AML) -compliance. These AML officers are shown suspicious transactions and are -granted access to the KYC data of an exchange. They can then investigate the -transaction and decide on freezing or permitting the transfer. They may also -request additional KYC data from the consumer and can change the threshold -amount above which a further AML review is triggered. - -AML Officer Setup ------------------ - -To begin the AML setup, AML staff should launch the GNU Taler -exchange AML SPA Web interface. (FIXME-Sebastian: how?). The -SPA will generate a public-private key pair and store it in the -local storage of the browser. The public key will be displayed -and must be securely transmitted to the offline system for -approval. Using the offline system, one can then configure -which staff has access to the AML operations: - -.. code-block:: shell-session - - [root@exchange-offline]# taler-exchange-offline \ - aml-enable $PUBLIC_KEY "Legal Name" rw > aml.json - [root@exchange-online]# taler-exchange-offline \ - upload < aml.json - -The above commands would add an AML officer with the given "Legal Name" with -read-write (rw) access to the AML officer database. Using "ro" instead of -"rw" would grant read-only access to the data, leaving out the ability to -actually make AML decisions. Once AML access has been granted, the AML -officer can use the SPA to review cases and (with "rw" access) take AML -decisions. - -Access rights can be revoked at any time using: - -.. code-block:: shell-session - - [root@exchange-offline]# taler-exchange-offline \ - aml-disable $PUBLIC_KEY "Legal Name" > aml-off.json - [root@exchange-online]# taler-exchange-offline \ - upload < aml-off.json - - -AML Triggers ------------- - -AML decision processes are automatically triggered under certain configurable -conditions. The primary condition that *must* be configured is the -``AML_THRESHOLD``: - -.. code-block:: ini - :caption: /etc/taler/conf.d/exchange-business.conf - - [exchange] - # Accounts or wallets with monthly transaction volumes above this threshold - # are considered suspicious and are automatically flagged for AML review - # and put on hold until an AML officer has reached a decision. - AML_THRESHOLD = "EUR:1000000" - -Additionally, certain KYC attributes (such as the user being a -politically exposed person) may lead to an account being -flagged for AML review. The specific logic is configured by -providing the exchange with an external helper program that -makes the decision given the KYC attributes: - -.. code-block:: ini - :caption: /etc/taler/conf.d/exchange-business.conf - - [exchange] - # Specifies a program to run on KYC attribute data to decide - # whether we should immediately flag an account for AML review. - KYC_AML_TRIGGER = taler-exchange-kyc-aml-pep-trigger.sh - -The given program will be given the KYC attributes in JSON format on standard -input, and must return 0 to continue without AML and non-zero to flag the -account for manual review. To disable this trigger, simply leave the option to -its default value of '[/usr/bin/]true'. To flag all new users for manual -review, simply set the program to '[/usr/bin/]false'. - -AML Forms ---------- - -AML forms are defined by the DD 54 dynamic forms. -The shipped implementation with of the exchange is installed in - -.. code-block:: shell-session - - ${INSTALL_PREFIX}/share/taler/exchange/spa/forms.js - - -The variable ``form`` contains the list of all form available. For -every entry in the list the next properties are expected to be present: - -``label``: used in the UI as the name of the form - -``id``: identification name, this will be saved in the exchange database -along with the values to correctly render the form again. -It should simple, short and without any character outside numbers, -letters and underscore. - -``version``: when editing a form, instead of just replacing fields -it will be better to create a new form with the same id and new version. -That way old forms in the database will used old definition of the form. -It should be a number. - -``impl`` : a function that returns the design and behavior of form. -See DD 54 dynamic forms. - -.. attention:: - - do not remove a form the list if it has been used. Otherwise you - won't be able to see the information save in the exchange database. - -To add a new one you can simply copy and paste one element, and edit it. - -It is much easier to download ``@gnu-taler/aml-backoffice-ui`` source -from ``https://git.taler.net/wallet-core.git/``, compile and copy the file -from the ``dist/prod``. - - -AML Programs ------------- - -AML programs are helper programs that can: - -* Generate a list of *required* context field names - for the helper (introspection!) using the "--required-context" - command-line switch. The output should use the same - syntax as the REQUIRES clause of ``[kyc-check-]`` - configuration sections, except that new lines - MUST be used to separate fields instead of ";". -* Generate a list of *required* attribute names - for the helper (introspection!) using the "--required-attributes" - command-line switch. The output should use the same - list of names as the ATTRIBUTES in the - ``[kyc-provider-]`` configuration section - (but may also include FORM field names). -* Process an input JSON object of type - `AmlProgramInput` into a JSON object of - type `AmlOutcome`. - This is the default behavior if no command-line switches - are provided. - -.. ts:def:: AmlProgramInput - - interface AmlProgramInput { - - // JSON object that was provided as - // part of the *measure*. This JSON object is - // provided under "context" in the main JSON object - // input to the AML program. This "context" should - // satify both the REQUIRES clause of the respective - // check and the output of "--requires" from the - // AML program's command-line option. - context?: Object; - - // JSON object that captures the - // output of a ``[kyc-provider-]`` or (HTML) FORM. - // The keys in the JSON object will be the attribute - // names and the values must be strings representing - // the data. In the case of file uploads, the data - // MUST be base64-encoded. - attributes: Object; - - // JSON array with the results of historic - // AML desisions about the account. - aml_history: AmlHistoryEntry[]; - - // JSON array with the results of historic - // KYC data about the account. - kyc_history: KycHistoryEntry[]; - - } - -.. ts:def:: AmlHistoryEntry - - interface AmlHistoryEntry { - // When was the AML decision taken. - decision_time : Timestamp; - - // What was the justification given for the decision. - justification : string; - - // Public key of the AML officer taking the decision. - decider_pub : AmlOfficerPublicKeyP; - - // Properties associated with the account by the decision. - properties : Object; - - // New set of legitimization rules that was put in place. - new_rules : LegitimizationRuleSet; - - // True if the account was flagged for (further) - // investigation. - to_investigate : boolean; - - // True if this is the currently active decision. - is_active : boolean; - } - -.. ts:def:: KycHistoryEntry - - interface KycHistoryEntry { - // Name of the provider - // which was used to collect the attributes. NULL if they were - // just uploaded via a form by the account owner. - provider_name?: string; - - // True if the KYC process completed. - finished: boolean; - - // Numeric `error code <error-codes>`, if the - // KYC process did not succeed; 0 on success. - code: number; - - // Human-readable description of ``code``. Optional. - hint?: string; - - // Optional detail given when the KYC process failed. - error_message?: string; - - // Identifier of the user at the KYC provider. Optional. - provider_user_id?: string; - - // Identifier of the KYC process at the KYC provider. Optional. - provider_legitimization_id? :string; - - // The collected KYC data. - // NULL if the attribute data could not - // be decrypted or was not yet collected. - attributes?: Object; - - // Time when the KYC data was collected - collection_time: Timestamp; - - // Time when the KYC data will expire. - expiration_time: Timestamp; - } - -.. ts:def:: AmlOutcome - - interface AmlOutcome { - - // Should the client's account be investigated - // by AML staff? - // Defaults to false. - to_investigate?: boolean; - - // Free-form properties about the account. - // Can be used to store properties such as PEP, - // risk category, type of business, hits on - // sanctions lists, etc. - properties?: AccountProperties; - - // Types of events to add to the KYC events table. - // (for statistics). - events?: string[]; - - // KYC rules to apply. Note that this - // overrides *all* of the default rules - // until the ``expiration_time`` and specifies - // the successor measure to apply after the - // expiration time. - new_rules: LegitimizationRuleSet; - - } - -If the AML program fails (exits with a failure code or -does not provide well-formed JSON output) the AML/KYC -process continues with the FALLBACK measure. This should -usually be one that asks AML staff to contact the -systems administrator. - -AML programs are listed in the configuration file, one program per section: - -.. code-block:: ini - - [aml-program-$PROG_NAME] - - # Program to run. - COMMAND = taler-helper-aml-pep - - # Human-readable description of what this - # AML helper program will do. Used to show - # to the AML staff. - DESCRIPTION = "check if the customer is a PEP" - - # True if this AML program is enabled (and thus can be - # used in measures and exposed to AML staff). - # Optional, default is NO. - ENABLED = YES - - # **original** measure to take if COMMAND fails - # Usually points to a measure that asks AML staff - # to contact the systems administrator. The fallback measure - # context always includes the reasons for the - # failure. - FALLBACK = MEASURE_NAME - -AML Measures ------------- - -The exchange configuration specifies a set of -**original** *measures* one per configuration section: - -.. code-block:: ini - - [kyc-measure-$MEASURE_NAME] - - # Possible check for this measure. Optional. - # If not given, PROGRAM should be run immediately - # (on an empty set of attributes). - CHECK_NAME = IB_FORM - - # Context for the check. The context can be - # just an empty JSON object if there is none. - CONTEXT = {"choices":["individual","business"]} - - # Program to run on the context and check data to - # determine the outcome and next measure. - PROGRAM = taler-aml-program - -If no ``CHECK_NAME`` is provided at all, the AML ``PROGRAM`` is to be run -immediately. This is useful if no client-interaction is required to arrive at -a decision. - -.. note:: - - The list of *measures* is not complete: AML staff may freely define new - measures dynamically, usually by selecting checks, an AML program, and - providing context. - - Setup Linting ============= @@ -2188,325 +1534,6 @@ exchange can then not detect double-spending. Hence this operation must not be performed in a production system. You still also need to then grant the permissions to the other exchange processes again. - - -.. _ExchangeTemplateCustomization: - -Template Customization -====================== - -The Exchange comes with various HTML templates that are shown to -guide users through the KYC process. The Exchange uses `C implementation of mustache -<https://gitlab.com/jobol/mustach>`__ as the templating engine. This section -describes the various templates. In general, the templates must be installed -to the ``share/taler/exchange/templates/`` directory. The file names must be of -the form ``$NAME.$LANG.must`` where ``$NAME`` is the name of the template and -``$LANG`` is the 2-letter language code of the template. English templates -must exist and will be used as a fallback. If the browser (user-agent) has -provided language preferences in the HTTP header and the respective language -exists, the correct language will be automatically served. - -The following subsections give details about each of the templates. Most -subsection titles are the ``$NAME`` of the respective template. - - -Generic Errors Templates ------------------------- - -A number of templates are used for generic errors. These are: - - * kyc-proof-already-done (KYC process already completed) - * kyc-bad-request (400 Bad Request) - * kyc-proof-endpoint-unknown (404 Not Found for KYC logic) - * kyc-proof-internal-error (500 Internal Server Error) - * kyc-proof-target-unknown (404 Not Found for KYC operation) - -All of these templates are instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: String; human-readable Taler error code, should be shown for the - user to understand the error - - * message: String; optional, extended human-readable text provided to elaborate - on the error, should be shown to provide additional context - - -kycaid-invalid-request ----------------------- - -The KYCaid plugin does not support requests to the -``/kyc-proof/`` endpoint (HTTP 400 bad request). - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: String; human-readable Taler error code, should be shown for the - user to understand the error - - * error: String; error code from the server - - * error_details: String; optional error description from the server - - * error_uri: optional URI with further details about the error from the server - - - -oauth2-authentication-failure ------------------------------ - -The OAuth2 server said that the request was not -properly authenticated (HTTP 403 Forbidden). - - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: String; human-readable Taler error code, should be shown for the - user to understand the error - - -oauth2-authorization-failure ----------------------------- - -The OAuth2 server refused to return the KYC data -because the authorization code provided was -invalid (HTTP 403 Forbidden). - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: String; human-readable Taler error code, should be shown for the - user to understand the error - - * error: String; error code from the server - - * error_message: String; error message from the server - - -oauth2-authorization-failure-malformed --------------------------------------- - -The server refused the authorization, but then provided -a malformed response (HTTP 502 Bad Gateway). - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: string; human-readable Taler error code, should be shown for the - user to understand the error - - * debug: boolean; true if we are running in debug mode and are allowed to return HTML with potentially sensitive information - - * server_response: Object; could be NULL; this includes the (malformed) OAuth2 server response, it should be shown to the use if "debug" is true - - -oauth2-bad-request ------------------- - -The client made an invalid request (HTTP 400 Bad Request). - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: String; human-readable Taler error code, should be shown for the - user to understand the error - - * message: String; additional error message elaborating on what was bad about the request - - -oauth2-conversion-failure -------------------------- - -Converting the KYC data into the exchange's internal -format failed (HTTP 502 Bad Gateway). - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: string; human-readable Taler error code, should be shown for the - user to understand the error - - * debug: boolean; true if we are running in debug mode and are allowed to return HTML with potentially sensitive information - - * converter: String; name of the conversion command that failed which was used by the Exchange - - * attributes: Object; attributes returned by the conversion command, often NULL (after all, conversion failed) - - * message: error message elaborating on the conversion failure - - -oauth2-provider-failure ------------------------ - -We did not get an acceptable response from the OAuth2 -provider (HTTP 502 Bad Gateway). - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: String; human-readable Taler error code, should be shown for the - user to understand the error - - * message: String; could be NULL; text elaborating on the details of the failure - - -persona-exchange-unauthorized ------------------------------ - -The Persona server refused our request (HTTP 403 Forbidden from Persona, returned as a HTTP 502 Bad Gateway). - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: String; human-readable Taler error code, should be shown for the - user to understand the error - - * data: Object; data returned from Persona service, optional - - * persona_http_status: Integer; HTTP status code returned by Persona - - -persona-load-failure --------------------- - -The Persona server refused our request (HTTP 429 Too Many Requests from Persona, returned as a HTTP 503 Service Unavailable). - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: String; human-readable Taler error code, should be shown for the - user to understand the error - - * data: Object; data returned from Persona service, optional - - * persona_http_status: Integer; HTTP status code returned by Persona - - -persona-exchange-unpaid ------------------------ - -The Persona server refused our request (HTTP 402 Payment REquired from Persona, returned as a HTTP 503 Service Unavailable). - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: String; human-readable Taler error code, should be shown for the - user to understand the error - - * data: Object; data returned from Persona service, optional - - * persona_http_status: Integer; HTTP status code returned by Persona - - - -persona-logic-failure ---------------------- - -The Persona server refused our request (HTTP 400, 403, 409, 422 from Persona, returned as a HTTP 502 Bad Gateway). - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: String; human-readable Taler error code, should be shown for the - user to understand the error - - * data: Object; data returned from Persona service, optional - - * persona_http_status: Integer; HTTP status code returned by Persona - - -persona-invalid-response ------------------------- - -The Persona server refused our request in an -unexpected way; returned as a HTTP 502 Bad Gateway. - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: string; human-readable Taler error code, should be shown for the - user to understand the error - - * debug: boolean; true if we are running in debug mode and are allowed to return HTML with potentially sensitive information - - * server_response: Object; could be NULL; this includes the (malformed) OAuth2 server response, it should be shown to the use if "debug" is true - - -persona-network-timeout ------------------------ - -The Persona server refused our request (HTTP 408 from Persona, returned as a HTTP 504 Gateway Timeout). - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: String; human-readable Taler error code, should be shown for the - user to understand the error - - * data: Object; data returned from Persona service, optional - - * persona_http_status: Integer; HTTP status code returned by Persona - - -persona-kyc-failed ------------------- - -The Persona server indicated a problem with the KYC process, saying it was not completed. - -This template is instantiated using the following information: - - * persona_inquiry_id: String; internal ID of the inquiry within Persona, useful for further diagnostics by staff - - * data: Object; could be NULL; this includes the server response, it contains extensive diagnostics, see Persona documentation on their ``/api/v1/inquiries/$ID``. - - * persona_http_status: Integer; HTTP status code returned by Persona - -persona-provider-failure ------------------------- - -The Persona server refused our request (HTTP 500 from Persona, returned as a HTTP 502 Bad Gateway). - -This template is instantiated using the following information: - - * ec: Integer; numeric Taler error code, should be shown to indicate the - error compactly for reporting to developers - - * hint: String; human-readable Taler error code, should be shown for the - user to understand the error - - * data: Object; data returned from Persona service, optional - - * persona_http_status: Integer; HTTP status code returned by Persona - - .. _ExchangeBenchmarking: Benchmarking diff --git a/taler-kyc-manual.rst b/taler-kyc-manual.rst @@ -0,0 +1,1020 @@ +.. + This file is part of GNU TALER. + + Copyright (C) 2014-2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 2.1, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + + @author Christian Grothoff + @author Florian Dold + +.. _KycOperatorManual: + +Exchange KYC/AML Operator Manual +################################ + +.. contents:: Table of Contents + :depth: 1 + :local: + + +Introduction +============ + +About GNU Taler +--------------- + +.. include:: frags/about-taler.rst + + +About this manual +----------------- + +This manual targets compliance experts working with system administrators to +configure legitimization (KYC/KYB) and anti-money laundering (AML) processes +for a GNU Taler exchange. + + + + +.. _LegalSetup: + +Legal Setup +=========== + +This chapter describes how to setup certain legal aspects of a GNU Taler +exchange. Users that just want to set up an exchange as an experiment without +legal requirements can safely skip these steps. + + +Legal conditions for using the service +-------------------------------------- + +.. include:: frags/legal.rst + + +KYC Configuration +================= + +To legally operate, Taler exchange operators may have to comply with KYC +regulation that requires financial institutions to identify parties involved +in transactions at certain points. + +Taler permits an exchange to require KYC data under the following circumstances: + + * Customer withdraws money over a threshold + * Wallet receives (via refunds) money resulting in a balance over a threshold + * Wallet receives money via P2P payments over a threshold + * Merchant receives money over a threshold + * Reserve is "opened" for invoicing (**planned feature**) + +Any of the above requests can trigger the KYC process, +which can be illustrated as follows: + +.. image:: images/kyc-process.png + +At the end of the KYC process, the wallet re-tries the +original request, and assuming KYC was successful, the +request should then succeed. + + +Taler KYC Terminology +--------------------- + +* **Check**: A check establishes a particular attribute of a user, such as + their name based on an ID document and lifeness, mailing address, phone + number, taxpayer identity, etc. + +* **Type of operation**: The operation type determines which Taler-specific + operation has triggered the KYC requirement. We support four types of + operation: withdraw (by customer), deposit (by merchant), P2P receive (by + wallet) and (high) wallet balance. + +* **Condition**: A condition specifies when KYC is required. Conditions + include the *type of operation*, a threshold amount (e.g. above EUR:1000) + and possibly a time period (e.g. over the last month). + +* **Cost**: Metric for the business expense for a KYC check at a certain + *provider*. Not in any currency, costs are simply relative and non-negative + values. Costs are considered when multiple choices are allowed by the + *configuration*. + +* **Expiration**: KYC legitimizations may be outdated. Expiration rules + determine when *checks* have to be performed again. + +* **Legitimization rules**: The legitimization rules determine under which + *conditions* which *checks* must be performend and the *expiration* time + period for the *checks*. + +* **Logic**: Logic refers to a specific bit of code (realized as an exchange + plugin) that enables the interaction with a specific *provider*. Logic + typically requires configuration for access control (such as an + authorization token) and possibly the endpoint of the specific *provider* + implementing the respective API. + +* **Provider**: A provider performs a specific set of *checks* at a certain + *cost*. Interaction with a provider is performed by provider-specific + *logic*. + +Configuration of possible KYC/AML checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The configuration specifies a set of possible KYC checks offered by external +providers. The names of the configuration sections must being with +``kyc-check-`` followed by an arbitrary ``$CHECK_NAME``. + +The list of possible FORM names is fixed in the SPA +for a particular exchange release. + +The outcome of *any* check should always be uploaded encrypted into the +``kyc_attributes`` table. It MUST include an ``expiration_time``. + + + +KYC Configuration Options +------------------------- + +The KYC configuration determines the *legitimization rules*, and specifies +which providers offer which *checks* at what *cost*. + +The configuration specifies a set of providers, one per configuration section. The names of the configuration +sections must being with ``kyc-proider-`` followed by +an arbitrary ``$PROVIDER_ID``: + +.. code-block:: ini + :caption: /etc/taler/conf.d/exchange-kyc-providers.conf + + [kyc-provider-$PROVIDER_ID] + # How expensive is it to use this provider? + # Used to pick the cheapest provider possible. + COST = 42 + # Which plugin is responsible for this provider? + # Choices include "oauth2", "kycaid" and "persona". + LOGIC = oauth2 + # Which type of user does this provider handle? + # Either INDIVIDUAL or BUSINESS. + USER_TYPE = INDIVIDUAL + # Which checks does this provider provide? + # List of strings, no specific semantics. + PROVIDED_CHECKS = SMS GOVID PHOTO + # Plus additional logic-specific options, e.g.: + AUTHORIZATION_TOKEN = superdupersecret + FORM_ID = business_legi_form + # How long is the check considered valid? + EXPIRATION = 3650d + +The configuration also must specify a set of legitimization requirements, again one +per configuration section: + +.. code-block:: ini + :caption: /etc/taler/conf.d/exchange-kyc-rules.conf + + [kyc-legitimization-$RULE_NAME] + # Operation that triggers this legitimization. + # Must be one of WITHDRAW, DEPOSIT, P2P-RECEIVE + # or WALLET-BALANCE. + OPERATION_TYPE = WITHDRAW + # Required checks to be performed. + # List of strings, must individually match the + # strings in one or more provider's PROVIDED_CHECKS. + REQUIRED_CHECKS = SMS GOVID + # Threshold amount above which the legitimization is + # triggered. The total must be exceeded in the given + # timeframe. + THRESHOLD = KUDOS:100 + # Timeframe over which the amount to be compared to + # the THRESHOLD is calculated. Can be 'forever'. + # Ignored for WALLET-BALANCE. + TIMEFRAME = 30d + + +OAuth 2.0 specifics +------------------- + +In terms of configuration, the OAuth 2.0 logic requires the respective client +credentials to be configured apriori to enable access to the legitimization +service. The OAuth 2.0 configuration options are: + +.. code-block:: ini + :caption: /etc/taler/conf.d/exchange-oauth2.conf + + [kyc-provider-example-oauth2] + LOGIC = oauth2 + # (generic options omitted) + # How long is the KYC check valid? + KYC_OAUTH2_VALIDITY = forever + + # URL to which we redirect the user for the login process + KYC_OAUTH2_AUTHORIZE_URL = "http://kyc.example.com/authorize" + # URL where we POST the user's authentication information + KYC_OAUTH2_TOKEN_URL = "http://kyc.example.com/token" + # URL of the user info access point. + KYC_OAUTH2_INFO_URL = "http://kyc.example.com/info" + + # Where does the client get redirected upon completion? + KYC_OAUTH2_POST_URL = "http://example.com/thank-you" + + # For authentication to the OAuth2.0 service + KYC_OAUTH2_CLIENT_ID = testcase + KYC_OAUTH2_CLIENT_SECRET = password + + # Mustach template that converts OAuth2.0 data about the user + # into GNU Taler standardized attribute data. + KYC_OAUTH2_CONVERTER_HELPER = taler-exchange-kyc-oauth2-challenger.sh + +The converter helper is expected to be customized to the selected OAuth2.0 +service: different services may return different details about the user or +business, hence there cannot be a universal converter for all purposes. The +default shell script uses the ``jq`` tool to convert the JSON returned by the +service into the KYC attributes (also in JSON) expected by the exchange. The +script will need to be adjusted based on the attributes collected by the +specific backend. + +The Challenger service for address validation supports OAuth2.0, but does not +have a static AUTHORIZE_URL. Instead, the AUTHORIZE_URL must be enabled by the client +using a special authenticated request to the Challenger's ``/setup`` endpoint. +The exchange supports this by appending ``#setup`` to the AUTHORIZE_URL (note +that fragments are illegal in OAuth2.0 URLs). Be careful to quote the URL, +as ``#`` is otherwise interpreted as the beginning of a comment by the +configuration file syntax. + +.. code-block:: ini + :caption: /etc/taler/conf.d/exchange-challenger-oauth2.conf + + [kyc-provider-challenger-oauth2] + LOGIC = oauth2 + KYC_OAUTH2_AUTHORIZE_URL = "http://challenger.example.com/authorize/#setup" + KYC_OAUTH2_TOKEN_URL = "http://challenger.example.com/token" + KYC_OAUTH2_INFO_URL = "http://challenger.example.com/info" + +When using OAuth 2.0, the *CLIENT REDIRECT URI* must be set to the +``/kyc-proof/$PROVIDER_SECTION`` endpoint. For example, given the +configuration above and an exchange running on the host +``exchange.example.com``, the redirect URI would be +``https://exchange.example.com/kyc-proof/kyc-provider-challenger-oauth2/``. + + + +Persona specifics +----------------- + +We use the hosted flow. The Persona endpoints return a ``request-id``, which +we log for diagnosis. + +Persona should be configured to use the ``/kyc-webhook/`` endpoint of the +exchange to notify the exchange about the completion of KYC processes. The +webhook is authenticated using a shared secret, which should be in the +configuration. To use the Persona webhook, you must set the webhook URL in +the Persona service to ``$EXCHANGE_BASE_URL/kyc-webhook/$SECTION_NAME/`` where +``$SECTION_NAME`` is the name of the configuration section. You should also +extract the authentication token for the webhook and put it into the +configuration as shown above. + + +.. code-block:: ini + :caption: /etc/taler/conf.d/exchange-persona.conf + + [kyclogic-persona] + # Webhook authorization token. Global for all uses + # of the persona provider! + WEBHOOK_AUTH_TOKEN = wbhsec_698b5a19-c790-47f6-b396-deb572ec82f9 + + [kyc-provider-example-persona] + LOGIC = persona + # (generic options omitted) + + # How long is the KYC check valid? + KYC_PERSONA_VALIDITY = 365d + + # Which subdomain is used for our API? + KYC_PERSONA_SUBDOMAIN = taler + + # Authentication token to use. + KYC_PERSONA_AUTH_TOKEN = persona_sandbox_42XXXX + + # Form to use. + KYC_PERSONA_TEMPLATE_ID = itempl_Uj6Xxxxx + + # Where do we redirect to after KYC finished successfully. + KYC_PERSONA_POST_URL = "https://taler.net/kyc-done" + + # Salt to give to requests for idempotency. + # Optional. + # KYC_PERSONA_SALT = salt + + # Helper to convert JSON with KYC data returned by Persona into GNU Taler + # internal format. Should probably always be set to some variant of + # "taler-exchange-kyc-persona-converter.sh". + KYC_PERSONA_CONVERTER_HELPER = "taler-exchange-kyc-persona-converter.sh" + +The converter helper is expected to be customized to the +selected template: different templates may return different details +about the user or business, hence there cannot be a universal converter +for all purposes. The default shell script uses the ``jq`` tool to +convert the JSON returned by Persona into the KYC attributes (also +in JSON) expected by the exchange. The script will need to be adjusted +based on the attributes collected by the specific template. + + +KYC AID specifics +----------------- + +We use the hosted flow. + +KYCAID must be configured to use the ``/kyc-webhook/$SECTION_NAME/`` endpoint +of the exchange to notify the exchange about the completion of KYC processes. + +.. code-block:: ini + :caption: /etc/taler/conf.d/exchange-kycaid.conf + + [kyc-provider-example-kycaid] + LOGIC = kycaid + # (generic options omitted) + + # How long is the KYC check valid? + KYC_KYCAID_VALIDITY = 365d + + # Authentication token to use. + KYC_KYCAID_AUTH_TOKEN = XXX + + # Form to use. + KYC_KYCAID_FORM_ID = XXX + + # URL to go to after the process is complete. + KYC_KYCAID_POST_URL = "https://taler.net/kyc-done" + + # Script to convert the KYCAID data into the Taler format. + KYC_KYCAID_CONVERTER_HELPER = taler-exchange-kyc-kycaid-converter.sh + + +The converter helper is expected to be customized to the selected template: +different templates may return different details about the user or business, +hence there cannot be a universal converter for all purposes. The default +shell script uses the ``jq`` tool to convert the JSON returned by Persona into +the KYC attributes (also in JSON) expected by the exchange. The script will +need to be adjusted based on the attributes collected by the specific +template. + + + +AML Configuration +================= + +The AML configuration steps are used to add or remove keys of exchange +operator staff that are responsible for anti-money laundering (AML) +compliance. These AML officers are shown suspicious transactions and are +granted access to the KYC data of an exchange. They can then investigate the +transaction and decide on freezing or permitting the transfer. They may also +request additional KYC data from the consumer and can change the threshold +amount above which a further AML review is triggered. + +AML Officer Setup +----------------- + +To begin the AML setup, AML staff should launch the GNU Taler +exchange AML SPA Web interface. (FIXME-Sebastian: how?). The +SPA will generate a public-private key pair and store it in the +local storage of the browser. The public key will be displayed +and must be securely transmitted to the offline system for +approval. Using the offline system, one can then configure +which staff has access to the AML operations: + +.. code-block:: shell-session + + [root@exchange-offline]# taler-exchange-offline \ + aml-enable $PUBLIC_KEY "Legal Name" rw > aml.json + [root@exchange-online]# taler-exchange-offline \ + upload < aml.json + +The above commands would add an AML officer with the given "Legal Name" with +read-write (rw) access to the AML officer database. Using "ro" instead of +"rw" would grant read-only access to the data, leaving out the ability to +actually make AML decisions. Once AML access has been granted, the AML +officer can use the SPA to review cases and (with "rw" access) take AML +decisions. + +Access rights can be revoked at any time using: + +.. code-block:: shell-session + + [root@exchange-offline]# taler-exchange-offline \ + aml-disable $PUBLIC_KEY "Legal Name" > aml-off.json + [root@exchange-online]# taler-exchange-offline \ + upload < aml-off.json + + +AML Triggers +------------ + +AML decision processes are automatically triggered under certain configurable +conditions. The primary condition that *must* be configured is the +``AML_THRESHOLD``: + +.. code-block:: ini + :caption: /etc/taler/conf.d/exchange-business.conf + + [exchange] + # Accounts or wallets with monthly transaction volumes above this threshold + # are considered suspicious and are automatically flagged for AML review + # and put on hold until an AML officer has reached a decision. + AML_THRESHOLD = "EUR:1000000" + +Additionally, certain KYC attributes (such as the user being a +politically exposed person) may lead to an account being +flagged for AML review. The specific logic is configured by +providing the exchange with an external helper program that +makes the decision given the KYC attributes: + +.. code-block:: ini + :caption: /etc/taler/conf.d/exchange-business.conf + + [exchange] + # Specifies a program to run on KYC attribute data to decide + # whether we should immediately flag an account for AML review. + KYC_AML_TRIGGER = taler-exchange-kyc-aml-pep-trigger.sh + +The given program will be given the KYC attributes in JSON format on standard +input, and must return 0 to continue without AML and non-zero to flag the +account for manual review. To disable this trigger, simply leave the option to +its default value of '[/usr/bin/]true'. To flag all new users for manual +review, simply set the program to '[/usr/bin/]false'. + +AML Forms +--------- + +AML forms are defined by the DD 54 dynamic forms. +The shipped implementation with of the exchange is installed in + +.. code-block:: shell-session + + ${INSTALL_PREFIX}/share/taler/exchange/spa/forms.js + + +The variable ``form`` contains the list of all form available. For +every entry in the list the next properties are expected to be present: + +``label``: used in the UI as the name of the form + +``id``: identification name, this will be saved in the exchange database +along with the values to correctly render the form again. +It should simple, short and without any character outside numbers, +letters and underscore. + +``version``: when editing a form, instead of just replacing fields +it will be better to create a new form with the same id and new version. +That way old forms in the database will used old definition of the form. +It should be a number. + +``impl`` : a function that returns the design and behavior of form. +See DD 54 dynamic forms. + +.. attention:: + + do not remove a form the list if it has been used. Otherwise you + won't be able to see the information save in the exchange database. + +To add a new one you can simply copy and paste one element, and edit it. + +It is much easier to download ``@gnu-taler/aml-backoffice-ui`` source +from ``https://git.taler.net/wallet-core.git/``, compile and copy the file +from the ``dist/prod``. + + +AML Programs +------------ + +AML programs are helper programs that can: + +* Generate a list of *required* context field names + for the helper (introspection!) using the "--required-context" + command-line switch. The output should use the same + syntax as the REQUIRES clause of ``[kyc-check-]`` + configuration sections, except that new lines + MUST be used to separate fields instead of ";". +* Generate a list of *required* attribute names + for the helper (introspection!) using the "--required-attributes" + command-line switch. The output should use the same + list of names as the ATTRIBUTES in the + ``[kyc-provider-]`` configuration section + (but may also include FORM field names). +* Process an input JSON object of type + `AmlProgramInput` into a JSON object of + type `AmlOutcome`. + This is the default behavior if no command-line switches + are provided. + +.. ts:def:: AmlProgramInput + + interface AmlProgramInput { + + // JSON object that was provided as + // part of the *measure*. This JSON object is + // provided under "context" in the main JSON object + // input to the AML program. This "context" should + // satify both the REQUIRES clause of the respective + // check and the output of "--requires" from the + // AML program's command-line option. + context?: Object; + + // JSON object that captures the + // output of a ``[kyc-provider-]`` or (HTML) FORM. + // The keys in the JSON object will be the attribute + // names and the values must be strings representing + // the data. In the case of file uploads, the data + // MUST be base64-encoded. + attributes: Object; + + // JSON array with the results of historic + // AML desisions about the account. + aml_history: AmlHistoryEntry[]; + + // JSON array with the results of historic + // KYC data about the account. + kyc_history: KycHistoryEntry[]; + + } + +.. ts:def:: AmlHistoryEntry + + interface AmlHistoryEntry { + // When was the AML decision taken. + decision_time : Timestamp; + + // What was the justification given for the decision. + justification : string; + + // Public key of the AML officer taking the decision. + decider_pub : AmlOfficerPublicKeyP; + + // Properties associated with the account by the decision. + properties : Object; + + // New set of legitimization rules that was put in place. + new_rules : LegitimizationRuleSet; + + // True if the account was flagged for (further) + // investigation. + to_investigate : boolean; + + // True if this is the currently active decision. + is_active : boolean; + } + +.. ts:def:: KycHistoryEntry + + interface KycHistoryEntry { + // Name of the provider + // which was used to collect the attributes. NULL if they were + // just uploaded via a form by the account owner. + provider_name?: string; + + // True if the KYC process completed. + finished: boolean; + + // Numeric `error code <error-codes>`, if the + // KYC process did not succeed; 0 on success. + code: number; + + // Human-readable description of ``code``. Optional. + hint?: string; + + // Optional detail given when the KYC process failed. + error_message?: string; + + // Identifier of the user at the KYC provider. Optional. + provider_user_id?: string; + + // Identifier of the KYC process at the KYC provider. Optional. + provider_legitimization_id? :string; + + // The collected KYC data. + // NULL if the attribute data could not + // be decrypted or was not yet collected. + attributes?: Object; + + // Time when the KYC data was collected + collection_time: Timestamp; + + // Time when the KYC data will expire. + expiration_time: Timestamp; + } + +.. ts:def:: AmlOutcome + + interface AmlOutcome { + + // Should the client's account be investigated + // by AML staff? + // Defaults to false. + to_investigate?: boolean; + + // Free-form properties about the account. + // Can be used to store properties such as PEP, + // risk category, type of business, hits on + // sanctions lists, etc. + properties?: AccountProperties; + + // Types of events to add to the KYC events table. + // (for statistics). + events?: string[]; + + // KYC rules to apply. Note that this + // overrides *all* of the default rules + // until the ``expiration_time`` and specifies + // the successor measure to apply after the + // expiration time. + new_rules: LegitimizationRuleSet; + + } + +If the AML program fails (exits with a failure code or +does not provide well-formed JSON output) the AML/KYC +process continues with the FALLBACK measure. This should +usually be one that asks AML staff to contact the +systems administrator. + +AML programs are listed in the configuration file, one program per section: + +.. code-block:: ini + + [aml-program-$PROG_NAME] + + # Program to run. + COMMAND = taler-helper-aml-pep + + # Human-readable description of what this + # AML helper program will do. Used to show + # to the AML staff. + DESCRIPTION = "check if the customer is a PEP" + + # True if this AML program is enabled (and thus can be + # used in measures and exposed to AML staff). + # Optional, default is NO. + ENABLED = YES + + # **original** measure to take if COMMAND fails + # Usually points to a measure that asks AML staff + # to contact the systems administrator. The fallback measure + # context always includes the reasons for the + # failure. + FALLBACK = MEASURE_NAME + +AML Measures +------------ + +The exchange configuration specifies a set of +**original** *measures* one per configuration section: + +.. code-block:: ini + + [kyc-measure-$MEASURE_NAME] + + # Possible check for this measure. Optional. + # If not given, PROGRAM should be run immediately + # (on an empty set of attributes). + CHECK_NAME = IB_FORM + + # Context for the check. The context can be + # just an empty JSON object if there is none. + CONTEXT = {"choices":["individual","business"]} + + # Program to run on the context and check data to + # determine the outcome and next measure. + PROGRAM = taler-aml-program + +If no ``CHECK_NAME`` is provided at all, the AML ``PROGRAM`` is to be run +immediately. This is useful if no client-interaction is required to arrive at +a decision. + +.. note:: + + The list of *measures* is not complete: AML staff may freely define new + measures dynamically, usually by selecting checks, an AML program, and + providing context. + + + +.. _ExchangeTemplateCustomization: + +Template Customization +====================== + +The Exchange comes with various HTML templates that are shown to +guide users through the KYC process. The Exchange uses `C implementation of mustache +<https://gitlab.com/jobol/mustach>`__ as the templating engine. This section +describes the various templates. In general, the templates must be installed +to the ``share/taler/exchange/templates/`` directory. The file names must be of +the form ``$NAME.$LANG.must`` where ``$NAME`` is the name of the template and +``$LANG`` is the 2-letter language code of the template. English templates +must exist and will be used as a fallback. If the browser (user-agent) has +provided language preferences in the HTTP header and the respective language +exists, the correct language will be automatically served. + +The following subsections give details about each of the templates. Most +subsection titles are the ``$NAME`` of the respective template. + + +Generic Errors Templates +------------------------ + +A number of templates are used for generic errors. These are: + + * kyc-proof-already-done (KYC process already completed) + * kyc-bad-request (400 Bad Request) + * kyc-proof-endpoint-unknown (404 Not Found for KYC logic) + * kyc-proof-internal-error (500 Internal Server Error) + * kyc-proof-target-unknown (404 Not Found for KYC operation) + +All of these templates are instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: String; human-readable Taler error code, should be shown for the + user to understand the error + + * message: String; optional, extended human-readable text provided to elaborate + on the error, should be shown to provide additional context + + +kycaid-invalid-request +---------------------- + +The KYCaid plugin does not support requests to the +``/kyc-proof/`` endpoint (HTTP 400 bad request). + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: String; human-readable Taler error code, should be shown for the + user to understand the error + + * error: String; error code from the server + + * error_details: String; optional error description from the server + + * error_uri: optional URI with further details about the error from the server + + + +oauth2-authentication-failure +----------------------------- + +The OAuth2 server said that the request was not +properly authenticated (HTTP 403 Forbidden). + + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: String; human-readable Taler error code, should be shown for the + user to understand the error + + +oauth2-authorization-failure +---------------------------- + +The OAuth2 server refused to return the KYC data +because the authorization code provided was +invalid (HTTP 403 Forbidden). + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: String; human-readable Taler error code, should be shown for the + user to understand the error + + * error: String; error code from the server + + * error_message: String; error message from the server + + +oauth2-authorization-failure-malformed +-------------------------------------- + +The server refused the authorization, but then provided +a malformed response (HTTP 502 Bad Gateway). + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: string; human-readable Taler error code, should be shown for the + user to understand the error + + * debug: boolean; true if we are running in debug mode and are allowed to return HTML with potentially sensitive information + + * server_response: Object; could be NULL; this includes the (malformed) OAuth2 server response, it should be shown to the use if "debug" is true + + +oauth2-bad-request +------------------ + +The client made an invalid request (HTTP 400 Bad Request). + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: String; human-readable Taler error code, should be shown for the + user to understand the error + + * message: String; additional error message elaborating on what was bad about the request + + +oauth2-conversion-failure +------------------------- + +Converting the KYC data into the exchange's internal +format failed (HTTP 502 Bad Gateway). + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: string; human-readable Taler error code, should be shown for the + user to understand the error + + * debug: boolean; true if we are running in debug mode and are allowed to return HTML with potentially sensitive information + + * converter: String; name of the conversion command that failed which was used by the Exchange + + * attributes: Object; attributes returned by the conversion command, often NULL (after all, conversion failed) + + * message: error message elaborating on the conversion failure + + +oauth2-provider-failure +----------------------- + +We did not get an acceptable response from the OAuth2 +provider (HTTP 502 Bad Gateway). + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: String; human-readable Taler error code, should be shown for the + user to understand the error + + * message: String; could be NULL; text elaborating on the details of the failure + + +persona-exchange-unauthorized +----------------------------- + +The Persona server refused our request (HTTP 403 Forbidden from Persona, returned as a HTTP 502 Bad Gateway). + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: String; human-readable Taler error code, should be shown for the + user to understand the error + + * data: Object; data returned from Persona service, optional + + * persona_http_status: Integer; HTTP status code returned by Persona + + +persona-load-failure +-------------------- + +The Persona server refused our request (HTTP 429 Too Many Requests from Persona, returned as a HTTP 503 Service Unavailable). + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: String; human-readable Taler error code, should be shown for the + user to understand the error + + * data: Object; data returned from Persona service, optional + + * persona_http_status: Integer; HTTP status code returned by Persona + + +persona-exchange-unpaid +----------------------- + +The Persona server refused our request (HTTP 402 Payment REquired from Persona, returned as a HTTP 503 Service Unavailable). + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: String; human-readable Taler error code, should be shown for the + user to understand the error + + * data: Object; data returned from Persona service, optional + + * persona_http_status: Integer; HTTP status code returned by Persona + + + +persona-logic-failure +--------------------- + +The Persona server refused our request (HTTP 400, 403, 409, 422 from Persona, returned as a HTTP 502 Bad Gateway). + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: String; human-readable Taler error code, should be shown for the + user to understand the error + + * data: Object; data returned from Persona service, optional + + * persona_http_status: Integer; HTTP status code returned by Persona + + +persona-invalid-response +------------------------ + +The Persona server refused our request in an +unexpected way; returned as a HTTP 502 Bad Gateway. + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: string; human-readable Taler error code, should be shown for the + user to understand the error + + * debug: boolean; true if we are running in debug mode and are allowed to return HTML with potentially sensitive information + + * server_response: Object; could be NULL; this includes the (malformed) OAuth2 server response, it should be shown to the use if "debug" is true + + +persona-network-timeout +----------------------- + +The Persona server refused our request (HTTP 408 from Persona, returned as a HTTP 504 Gateway Timeout). + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: String; human-readable Taler error code, should be shown for the + user to understand the error + + * data: Object; data returned from Persona service, optional + + * persona_http_status: Integer; HTTP status code returned by Persona + + +persona-kyc-failed +------------------ + +The Persona server indicated a problem with the KYC process, saying it was not completed. + +This template is instantiated using the following information: + + * persona_inquiry_id: String; internal ID of the inquiry within Persona, useful for further diagnostics by staff + + * data: Object; could be NULL; this includes the server response, it contains extensive diagnostics, see Persona documentation on their ``/api/v1/inquiries/$ID``. + + * persona_http_status: Integer; HTTP status code returned by Persona + +persona-provider-failure +------------------------ + +The Persona server refused our request (HTTP 500 from Persona, returned as a HTTP 502 Bad Gateway). + +This template is instantiated using the following information: + + * ec: Integer; numeric Taler error code, should be shown to indicate the + error compactly for reporting to developers + + * hint: String; human-readable Taler error code, should be shown for the + user to understand the error + + * data: Object; data returned from Persona service, optional + + * persona_http_status: Integer; HTTP status code returned by Persona