diff options
Diffstat (limited to 'design-documents/023-taler-kyc.rst')
-rw-r--r-- | design-documents/023-taler-kyc.rst | 2262 |
1 files changed, 2262 insertions, 0 deletions
diff --git a/design-documents/023-taler-kyc.rst b/design-documents/023-taler-kyc.rst new file mode 100644 index 00000000..c20844b3 --- /dev/null +++ b/design-documents/023-taler-kyc.rst @@ -0,0 +1,2262 @@ +DD 23: Taler KYC +################ + +Summary +======= + +This document discusses the Know-your-customer (KYC) and Anti-Money Laundering +(AML) processes supported by Taler. + + +Motivation +========== + +To legally operate, Taler has to comply with KYC/AML regulation that requires +banks to identify parties involved in transactions at certain points. + + +Requirements +============ + +Taler needs to take *measures* based on the following primary *triggers*: + +* Customer withdraws money over a monthly threshold + + * exchange triggers KYC + * key: IBAN (encoded as payto:// URI) + +* Wallet receives (via refunds) money resulting in a balance over a threshold + + * this is a client-side restriction + * key: reserve (=KYC account) long term public key per wallet (encoded as payto:// URI) + +* Wallet receives money via P2P payments + + * there are two sub-cases: PUSH and PULL payments + * key: reserve (=KYC account) long term public key per wallet (encoded as payto:// URI) + +* Merchant receives money (Q: any money, or above a monthly threshold?) + + * key: IBAN (encoded as payto:// URI) + +* Reserve is "opened" for invoicing. + + * key: reserve (=KYC account) long term public key per wallet (encoded as payto:// URI) + +* Import of new sanctions lists and triggering of measures against matches of existing + customer records against the list + +For the different operation types, there can be both soft and hard +limits. Soft limits are those that the customer may raise by providing data +and passing KYC checks. Hard limits cannot be lifted, for example because an +exchange forbids crossing those limits in its terms of service for all +customers. + + +Process requirements +^^^^^^^^^^^^^^^^^^^^ + +The key consideration here is *plausibilization*: staff needs to +check that the client-provided information is plausible. As this +is highly case-dependent, this cannot be automated. + +For the different *measures*, there are various different possible KYC/AML +*checks* that could happen: + +* In-person validation by AML staff +* Various forms to be filled by AML staff +* Validation involving local authorities and post-office +* Online validation, sometimes with multiple options (like KYC for multiple people): + + * Forms to be supplied by user (different types of ID) + * Interactive video + * Documents to be supplied (business register) + * Address validation (e-mail or phone or postal) + +Additionally, the process is dynamic and conditional upon various decisions: + +* Individual vs. business +* PEP or non-PEP +* Hit on sanctions list +* Type of business (trust, foundation, listed on stock market, etc.) +* Need for plausibilization (via documents by user or staff research) +* Periodic updates (of customer data, of sanction lists) and re-assessment + +There are also various *outcomes*: + +* normal operation (with expiration date) +* normal operation but with AML staff investigating (new measure) +* held, requesting customer documentation (new measure) +* held, AML staff reviewing evidence for plausibilization (new measure) +* automatically frozen until certain day (due to sanctions) +* institutionally frozen until certain day (due to order by state authority) +* operation is categorically not allowed (at least above certain limits) + +Outcomes may also be (partially) public, that is exposed to the client. For +example, we may want to tell a wallet that it has hit a hard withdraw limit, +but might succeed at withdrawing a smaller amount. + +The outcome of a *check* can set new rules or trigger another *measure* (the +latter is conditional on reaching the expiration time of the outcome). + +As a result, we largely end up in a large state machine where the AML staff has +serious flexibiltiy while the user needs guidance as to the possible next moves +and/or to the current state of their account (where some information must not be +disclosed). + + +Documentation requirements +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For each account we must: + +* define risk-profile (902.4, 905.1) +* document the specific setup, likely not just the INI file +* should have some key Anti-Money-Laundering Act (AMLA) + file attributes, such as: + + * File opened, file closed (keep data for X years afterwards!) + * low-risk or high-risk business relationship + * PEP status + * business domain + * authority notification dates (possibly multiple) with + voluntary or mandatory notification classification + +Finally, we need to produce statistics: + +* There must be a page with an overview of AMLA files with opening + and closing dates and an easy way to determine for any day the + number of open AMLA files +* Technically, we also need a list of at-risk transactions and of + frozen transactions, but given that we can really only freeze + on an account-basis, I think there is nothing to do here +* number of incidents reported (voluntarily, required) +* number of business relationships at any point in time +* number of risky business relationships (PEP, etc.) +* number of frozen transactions (authority vs. sanction) with start-date and end-date +* start-data and end-date of relationships (data retained for X years after end of relationship) + +For this high-level monitoring, we need certain designated critical events to +be tracked in the system statistics: + +* account opened +* set to high risk +* set to low risk +* suspicious activity report filed with authority +* account frozen +* account unfrozen +* account closed +* sanction list import / update + + +TODO: Sanction lists +^^^^^^^^^^^^^^^^^^^^ + +We need to be able to import new sanction lists (whenever they are published) +and then check existing AMLA files against those lists. Additionally, newly +created AMLA files must be checked against the current list and some "measure" +applied in case of a match. + +This will primarily require us to define an endpoint to upload a sanction list +and to define a new table to track the list of sanctioned entities. As it is +expected that sanction lists will not permit fully automated determinations in +all cases, an external "sanction check" program should be configured which +compares records against the current list and determines the correct measure, +such as no change, further manual review by AML staff, or even automatic +freeze (and report) depending on how well the records match. + +Basically, the "sanction check" program takes the sanction list and an +attribute set to compute the same kind of `AmlOutcome` that an AML program +outputs given a context and an attribute set. + + +Security requirements +^^^^^^^^^^^^^^^^^^^^^ + +IBANs are predictable. We (probably) do not want random people to be able to +initate KYC processes for other parties. Similarly, the attestation API +requires us to somehow *authenticate* the user to ensure we only give out +attestation data to the data subject themselves. For P2P payments and +withdrawals, we have the reserve public key that is only known to the data +subject and thus can be used to authenticate the client via a signature. Only +pure deposits (by merchants or directly from a wallet) are a problem as the +only thing we know about the receiver is the IBAN at that time, and literally +any user could just deposit money into some bank account, so knowledge of the +IBAN is insufficient to determine that we actually are communicating with the +owner of the bank account. + + +Further considerations +^^^^^^^^^^^^^^^^^^^^^^ + +On top of all of this, we need to plan some *diagnostics* to determine when +components fail (such as scripts or external services providing malformed +results). + +Optionally, in the future, the solution should support fees to be paid by the +user for *voluntary* KYC processes related to attestation (#7365). + + +Proposed Solution +================= + +The main state of an account is represented by a set of `KycRules` (the +`LegitimizationRuleSet`) which specify the current *rules* to apply to +transactions involving the account. Rules can *exposed* to the account owner, +or can be secret. Each *rule* specifies certain *conditions* which, if met, +*trigger* a single specific *measure*. After a *rule* was *triggered* and +before the *outcome* of the respective *measure* has been produced (say +because the user did not yet enter their data or the AML officer is still +reviewing the case), the existing rules remain in force. Rules have a display +priority, and if a second rule with a higher display priority is also +triggered, the *measures* of the higher-priority rule become the active +*measures*. Except for the default rule set, every legitimization rule set +also has an *expiration* time after which a successor *measure* (or the +default rule set) is automatically triggered. + +For any possible *measures*, we define: + +* Contextual input data to be provided (with dynamic inputs, + e.g. amount set dynamically based on the *trigger* could be + in the context) +* A *check* to be performed (checks can be user-interactive (LINK, FORM) + or staff-interactive (INFO)) +* A fallback *measure* to take on failure of a user-interactive check + (if the check fails, we cannot run the AML *program* as required inputs + might be missing!) +* An (AML) *program* that uses *attribtes* from the *check* as well as + *context* data to determine an *outcome* represented as the + `AmlOutcome`. + +"verboten" is the name of a special *measure*, which means that crossing the +respective transaction threshold is categorically not allowed (for this +account). "verboten" with a threshold of zero can be used to freeze funds. + +Possible *outcomes* of a measure include: + +* The next operational state (normal, AML investigation) of the account + (basically, whether to add it to the work list of AML staff). +* A new set of *rules* in the form of a `LegitimizationRuleSet` that + determines custom rules to apply to transactions involving the account; + such rules may be used to block certain transactions by using the + "verboten" measure. The `LegitimizationRuleSet` also must specify + an *expiration* time by which we fall back to a successor measure + *or* to the default rules. +* A (largely) free-form set of `AccountProperties` that AML staff can + use to tag accounts with. Some default properties are defined, but + the exchange does not do anything with these and AML SPAs are free to + use any properties they like. Account properties are only exposed + to AML staff and never to the customer. +* A set of *events* that are to be added to the timeline of the + operator for statistical purposes. + +For the user-interactive *checks* we need a KYC SPA that is given: + +* instructions to render (with either a form to fill or links to external checks); + here the context could provide an array of choices! +* possibly an external check that was set up (if any); for cost-reasons, we + should only do one at a time, and probably should then always redirect the + browser to that check. + +For the staff-interactive *checks* we need an AML SPA: + +* to file forms and upload documentation (without state transition) +* to decide on next measure (providing context); here, the exchange needs + to expose the list of available *measures* and required *context* for each + +We need some customer-driven interactivity in KYB/KYC process, for example the +user may need to be given choices (address vs. phone, individual vs. business, +order in which to provide KYC data of beneficiaries). As a result, the +exchange needs to serve some SPA for *measures* where the user is shown the +next step(s) or choices (which person to collect KYC data on, whether to run +challenger on phone number of physical address, etc.). The SPA should also +potentially contain a form to allow the customer to directly upload documents +to us (like business registration) instead of to some KYC provider. This is +because KYC providers may not be flexible enough. The SPA should also allow +the customer to perform KYC checks voluntarily. + +Similarly, the AML staff will need to be able to trigger rather complex +KYB/KYC processes, like "need KYC on X and Y and Z" or "phone number or +mailing address" or "please upload form A/T/S". Here in particular it +should be possible to request not only filled forms, but arbitrary +documents. + + +Terminology +^^^^^^^^^^^ + +* **Attributes**: Attributes are used to represent KYC data obtained about + an account holder. Attributes include passport images, address data, + business registration documents, and indeed arbitrary forms filed by + AML staff or the customer themselves. Attribute data is considered + sensitive private information and is thus stored encrypted within the + exchange database. + +* **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. Checks may be given *context* (such as + whether a customer is an individual or a business) to run correctly. Checks + can also be AML staff inserting information for plausibilization. Checks + result in *attributes* about the account's owner which are given to an + external AML *program* together with the *context* to determine an *outcome*. + KYC checks are always specified with a fallback *measure* to be taken if + the check fails. + +* **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). + +* **Configuration**: The configuration determines the *legitimization rules*, + and specifies which providers offer which *checks*. + +* **Context**: Context is information provided as input into a *check* and + *program* to customize their execution. The context is initially set by the + *measure* (possibly including data from the *trigger*). Naturally, the + *program* may use its `AmlProgramInput` which includes *context* and + *attribute* data to compute an update *context* for the next set of + *measures* that it specifies in the `LegitimizationRuleSet` as part + of the `AmlOutcome`. Thus, *context* is something that typically + evolves as the *account* undergoes *measures*. Context is lost if + an account transitions to default *legitimization rules* due to + *expiration*. + +* **Display priority**: Every rule has a *display priority*. If a second + *rule* is *triggered* before the *outcome* of a *rule* could be determined, + the *rule* with the larger *display priority* becomes the requirement that + the account owner has to satisfy (and that thus will be displayed by the + KYC SPA). + +* **Expiration**: Except for the default rules, any set of KYC rules is + subject to *expiration*. This can be because *attributes* become outdated or + because sanctions have a time limit. The expiration time thus determines + when a new *measure* is triggered in the absence of a transaction crossing + thresholds in the current set of *legtimization rules*. + +* **Legitimization rules**: The *legitimization rules* determine under which + *conditions* which *measures* will be taken. A `LegitimizationRuleSet` + always also includes an *expiration* time period for (custom, non-default) + *legitimization rules* after which a fallback measure* will automatically + apply. Legitimization rules may be *exposed* to the client (for example, + to allow a wallet to stay below hard withdraw thresholds) or could be secret. + +* **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. + +* **Measure**: Describes the possible outgoing edges from one state in the + state machine (including how to show the current state). Each edge is given + some *context* and a *check* to be performed as well as an AML *program* + which determines the *outcome*. We generally distinguish between + "original" measures (defined globally in the exchange configuration) and + "custom" measures (defined specifically for an account by AML staff). + +* **Outcome**: An `AmlOutcome` describes the account state that an account + ends up in due to either an AML staff action or an AML *program* doing some + computation over the attributes resulting from a *check*. Outcomes can be + that certain types of transactions are "verboten", that the account is (or + remains) under investigation by AML staff, that the account is given certain + properties, and/or that certain events are to be logged. Outcomes also + include a new set of *legitimization rules* to apply (and an *expiration* + time at which point a successor *measure* will be automatically taken). + +* **Provider**: A provider performs a specific set of *checks* at a certain + *cost*. Interaction with a provider is performed by provider-specific + *logic*. + +* **Program**: An AML helper *program* is given *context* about the current + state of an account and the attribute data from a *check* to compute the + *outcome*. For example, a *program* may look at the "PEP" field of a KYC + check and decide if the outcome is to put the account into ``normal`` or + ``held-for-manual-review`` state. AML programs are always specified + with a fallback *measure* to be taken if the program fails. + +* **Trigger**: A specific transaction that satisfies a **Condition**. + +* **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. + + +Account owner authentication +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Access to the KYC SPA (or rather, its account-specific state) is controlled by +a *target token* (which is effectively like a bearer token, except passed +inside the URL). The *target token* ensures that only the account owner has +access to the KYC processes. It can be obtained by authenticating using +either the merchant private key or reserve private key, depending on the type +of the account (IBAN or wallet-reserve respectively). + +When we need to authenticate a bank account owner, we will simply require them +to make an outgoing wire transfer into the exchange bank account with a public +key in the wire transfer subject (just like when withdrawing), but augmented +with the string "KYC" so we can distinguish the wire transfer from a regular +withdrawal. Typically, we would put the merchant public key into the wire +transfer subject; wallets MAY put their long-term reserve public key instead. +The amount to be transferred is the *KYC fee*. + +This has several advantages: + +* Only the account owner can provide us with the public key, so we already + have also one super-hard piece of KYC evidence. +* If the account owner looses their public key, it's not a problem: they + would just have to do the transfer again with a new key. No need for + us to do any kind of intervention for key management. +* We could theoretically get paid to do the KYC process, or just "charge" a + nominal amount. +* This also somewhat addresses the payment for voluntary KYC processes where + a merchant wants to do KYC to get us to attest their identity for their + customers even if we do not yet have a legal need. The only issue here + is that this does not work if voluntary KYC is invoiced while mandatory + KYC is gratis. But, that kind of configuration is a business decision + and there is no hard need to support it immediately. +* This definitively addresses the need for authentication to access the + attestation API, which so far was only available for P2P payments as + we could not authenticate merchants. +* The "KYC" string allows us to distinguish the authentication transfers from + withdrawal transfers; by keeping the KYC fee at or below the closing fee, + we can even deploy this without fully updating the logic everywhere to + distinguish KYC transfers + +TODO: update wire gateway specification, update/new tables for KYC wire +transfers, update API spec for attestation, update exchange API (below) to +signal need for auth-payment via wire transfer, update merchant logic to +expose merchant public key to SPA for wire transfer if needed for KYC. + + +451 Response +^^^^^^^^^^^^ + +When KYC operations are required, various endpoints may respond with a +``451 Unavailable for Legal Reasons`` status code and a `KycNeededRedirect` +body. + + .. ts:def:: KycNeededRedirect + + interface KycNeededRedirect { + + // Numeric `error code <error-codes>` unique to the condition. + // Should always be ``TALER_EC_EXCHANGE_GENERIC_KYC_REQUIRED``. + code: number; + + // Human-readable description of the error, i.e. "missing parameter", + // "commitment violation", ... Should give a human-readable hint + // about the error's nature. Optional, may change without notice! + hint?: string; + + // Hash of the payto:// account URI for which KYC + // is required. + h_payto: PaytoHash; + + // Public key associated with the account. The client must sign + // the initial request for the KYC status using the corresponding + // private key. Will be either a reserve public key or a merchant + // (instance) public key. + // + // Absent if no public key is currently associated + // with the account and the client MUST thus first + // credit the exchange via an inbound wire transfer + // to associate a public key with the debited account. + account_pub?: EddsaPublicKey; + + // Identifies a set of measures that were triggered and that are + // now preventing this operation from proceeding. Gives the + // account holder a starting point for understanding why the + // transaction was blocked and how to lift it. The account holder + // should use the number to check for the account's AML/KYC status + // using the ``/kyc-check/$REQUIREMENT_ROW`` endpoint. + requirement_row: Integer; + + } + + +New endpoints +^^^^^^^^^^^^^ + +.. http:get:: /kyc-check/$REQUIREMENT_ROW + + Checks the KYC status of a particular payment target and possibly begins a + KYC process by allowing the customer to choose the next KYC measure to + satisfy. This endpoint is typically used by wallets or merchants that + have been told that a transaction is not happening because it triggered + some KYC/AML measure and now want to check how the KYC/AML + requirement could be fulfilled (or whether it already has been + statisfied and the operation can now proceed). Long-polling may be used + to instantly observe a change in the KYC requirement status. + + The requirement row of the ``/kyc-check/`` endpoint encodes the + legitimization measure's serial number. It is returned in + `KycNeededRedirect` responses via the ``requirement_row`` field. + + Given a valid pair of requirement row and account owner signature, the + ``/kyc-check/`` endpoint returns either just the KYC status or redirects the + client (202) to the next required stage of the KYC process. The redirection + must be for an HTTP(S) endpoint to be triggered via a simple HTTP GET. It + must always be the same endpoint for the same client, as the wallet/merchant + backend are not required to check for changes to this endpoint. Clients + that received a 202 status code may repeat the request and use long-polling + to detect a change of the HTTP status. + + **Request:** + + *Account-Owner-Signature*: + + The client must provide Base-32 encoded EdDSA signature with + ``$ACCOUNT_PRIV``, affirming the desire to obtain KYC data. Note that + this is merely a simple authentication mechanism, the details of the + request are not protected by the signature. The ``$ACCOUNT_PRIV`` is + either the (wallet long-term) reserve private key or the merchant instance + private key. + + :query timeout_ms=NUMBER: *Optional.* If specified, the exchange will + wait up to ``timeout_ms`` milliseconds if the requirement continues + to be mandatory provisioning of KYC data by the client. + Ignored if the HTTP status code is already ``200 Ok``. Note that + clients cannot long-poll for AML staff actions, so status information + about an account being under AML review needs to be requested + periodically. + + **Response:** + + :http:statuscode:`200 Ok`: + No mandatory KYC actions are required by the client at this time. + The client *may* still visit the KYC URL to initiate voluntary checks. + The response will be an `AccountKycStatus` object which specifies + restrictions that currently apply to the account. If the + client attempts to exceed *soft* limits, the status may change + to a ``202 Accepted``. Hard limits cannot be lifted by passing KYC checks. + :http:statuscode:`202 Accepted`: + The account holder performed an operation that would have crossed + *soft* limits and must be redirected to the provided location to perform + the required KYC checks to satisfy the legal requirements. Afterwards, the + ``/kyc-check/`` request should be repeated to check whether the + user has completed the process. + The response will be an `AccountKycStatus` object. + :http:statuscode:`204 No content`: + The exchange is not configured to perform KYC and thus + the legal requirements are already satisfied. + :http:statuscode:`403 Forbidden`: + The provided signature is not acceptable for the requirement row. + :http:statuscode:`404 Not found`: + The requirement row is unknown. + + **Details:** + + .. ts:def:: AccountKycStatus + + interface AccountKycStatus { + + // Current AML state for the target account. True if + // operations are not happening due to staff processing + // paperwork *or* due to legal requirements (so the + // client cannot do anything but wait). + // + // Note that not every AML staff action may be legally + // exposed to the client, so this is merely a hint that + // a client should be told that AML staff is currently + // reviewing the account. AML staff *may* review + // accounts without this flag being set! + aml_review: boolean; + + // Access token needed to construct the ``/kyc-spa/`` + // URL that the user should open in a browser to + // proceed with the KYC process (optional if the status + // type is ``200 Ok``, mandatory if the HTTP status + // is ``202 Accepted``). + access_token: AccountAccessToken; + + // Array with limitations that currently apply to this + // account and that may be increased or lifted if the + // KYC check is passed. + // Note that additional limits *may* exist and not be + // communicated to the client. If such limits are + // reached, this *may* be indicated by the account + // going into ``aml_review`` state. However, it is + // also possible that the exchange may legally have + // to deny operations without being allowed to provide + // any justification. + // The limits should be used by the client to + // possibly structure their operations (e.g. withdraw + // what is possible below the limit, ask the user to + // pass KYC checks or withdraw the rest after the time + // limit is passed, warn the user to not withdraw too + // much or even prevent the user from generating a + // request that would cause it to exceed hard limits). + limits?: AccountLimit[]; + + } + + .. ts:def:: AccountLimit + + interface AccountLimit { + + // Operation that is limited. + // Must be one of "WITHDRAW", "DEPOSIT", "P2P-RECEIVE" + // or "WALLET-BALANCE". + operation_type: string; + + // Timeframe during which the limit applies. + timeframe: RelativeTime; + + // Maximum amount allowed during the given timeframe. + // Zero if the operation is simply forbidden. + threshold: Amount; + + // True if this is a soft limit that could be raised + // by passing KYC checks. Clients *may* deliberately + // try to cross limits and trigger measures resulting + // in 451 responses to begin KYC processes. + // Clients that are aware of hard limits *should* + // inform users about the hard limit and prevent flows + // in the UI that would cause violations of hard limits. + soft_limit: boolean; + } + + +.. http:get:: /kyc-spa/$ACCESS_TOKEN +.. http:get:: /kyc-spa/$FILENAME + + A set of ``/kyc-spa/$ACCESS_TOKEN`` GET endpoints is created per account + hash that serves the KYC SPA. This is where the ``/kyc-check/`` endpoint + will in principle redirect clients. The KYC SPA will use the + ``$ACCESS_TOKEN`` of its URL to initialize itself via the + ``/kyc-info/$ACCESS_TOKEN`` endpoint family. The KYC SPA may download + additional resources via ``/kyc-spa/$FILENAME``. The filenames must not + match base32-encoded 256-bit values. + +.. http:get:: /kyc-info/$ACCESS_TOKEN + + The ``/kyc-info/$ACCESS_TOKEN`` endpoints are created per client + account hash (but access controlled via a unique target token) + to return information about the state of the KYC or AML process + to the KYC SPA. The SPA uses this information to show the user an + appropriate dialog. The SPA should also long-poll this endpoint for changes + to the AML/KYC state. Note that this is a client-facing endpoint, so it will + only provide a restricted amount of information to the customer (as some + laws may forbid us to inform particular customers about their true status). + The endpoint will typically inform the SPA about possible choices to + proceed, such as directly uploading files, contacting AML staff, or + proceeding with a particular KYC process at an external provider (such as + Challenger). If the user chooses to initate a KYC process at an external + provider, the SPA must request the respective process to be set-up by the + exchange via the ``/kyc-start/`` endpoint. + + **Request:** + + *If-None-Match*: + The client MAY provide an ``If-None-Match`` header with an ETag. + + :query timeout_ms=MILLISECONDS: + *Optional.* If specified, the exchange will wait up to MILLISECONDS for + a change to a more recent legitimization measure before returning a 304 + Not Modified status. + + **Response:** + + :http:statuscode:`200 OK`: + The body is a `KycProcessClientInformation`. + + *Etag*: Will be set to the serial ID of the measure. Used for long-polling. + + .. ts:def:: KycProcessClientInformation + + interface KycProcessClientInformation { + + // List of requirements. + requirements?: { name : KycRequirementInformation}; + + // True if the client is expected to eventually satisfy all requirements. + // Default (if missing) is false. + is_and_combinator?: boolean + + // List of available voluntary checks the client could pay for. + // Since **vATTEST**. + voluntary_checks?: { name : KycCheckInformation}; + } + + .. ts:def:: KycRequirementInformation + + interface KycRequirementInformation { + + // Which form should be used? Common values include "INFO" + // (to just show the descriptions but allow no action), + // "LINK" (to enable the user to obtain a link via + // ``/kyc-start/``) or any build-in form name supported + // by the SPA. + form: string; + + // English description of the requirement. + description: string; + + // Map from IETF BCP 47 language tags to localized + // description texts. + description_i18n ?: { [lang_tag: string]: string }; + + // ID of the requirement, useful to construct the + // ``/kyc-upload/$ID`` or ``/kyc-start/$ID`` endpoint URLs. + // Present if and only if "form" is not "INFO". The + // ``$ID`` value may itself contain ``/`` or ``?`` and + // basically encode any URL path (and optional arguments). + id?: string; + + } + + .. ts:def:: KycCheckInformation + + // Since **vATTEST**. + interface KycCheckInformation { + + // English description of the check. + description: string; + + // Map from IETF BCP 47 language tags to localized + // description texts. + description_i18n ?: { [lang_tag: string]: string }; + + } + + :http:statuscode:`204 No Content`: + There are no open KYC requirements or possible voluntary checks + the client might perform. + + :http:statuscode:`304 Not Modified`: + The KYC requirements did not change. + + +.. 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 both the ``$ACCESS_TOKEN`` and the index of the selected + measure (but this should be irrelevant for the client). + + **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 Content 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 both the ``$ACCESS_TOKEN`` and the index of + the selected measure (but this 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. + + **Details:** + + .. ts:def:: KycProcessStartInformation + + interface KycProcessStartInformation { + + // URL to open. + redirect_url: string; + } + + :http:statuscode:`404 Not Found`: + The ``$ID`` is unknown to the exchange. + + .. 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_SECTION?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_SECTION`` 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_SECTION". + + 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_SECTION/* +.. http:post:: /kyc-webhook/$PROVIDER_SECTION/* +.. http:get:: /kyc-webhook/$LOGIC/* +.. http:post:: /kyc-webhook/$LOGIC/* + + Alternatively, the KYC confirmation may be triggered by a ``/kyc-webhook`` + request. As KYC **providers** do not necessarily support passing detailed + information in the URL arguments, the ``/kyc-webhook`` only needs to specify + either the ``PROVIDER_SECTION`` *or* the ``LOGIC`` (the name of the plugin + implementing the KYC API). The API-specific webhook logic must then figure + out what exactly the webhook is about on its own. The ``/kyc-webhook/`` + endpoint works for GET or POST, again as details depend on the KYC provider. + In contrast to ``kyc-proof``, the response does NOT go to the end-users' + browser and should thus only indicate success or failure. + + **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. + + +.. http:post:: /kyc-wallet + + The ``/wallet-kyc`` POST endpoint allows a wallet to notify an exchange if + it will cross a balance threshold. Here, the ``balance`` specified should be + the threshold (from the ``wallet_balance_limit_without_kyc`` array) that the + wallet would cross, and *not* the *exact* balance of the wallet. The exchange + will respond with a wire target UUID. The wallet can then use this UUID to + being the KYC process at ``/kyc-check/``. The wallet must only proceed to + obtain funds exceeding the threshold after the KYC process has concluded. + While wallets could be "hacked" to bypass this measure (we cannot + cryptographically enforce this), such modifications are a terms of service + violation which may have legal consequences for the user. + + Setup KYC identification for a wallet. Returns the KYC UUID. This endpoint + is used by compliant Taler wallets when they are about to hit the balance + threshold and thus need to have the customer provide their personal details + to the exchange. The wallet is identified by its long-lived reserve public + key (which is used for P2P payments, not for withdrawals). + + **Request:** + + The request body must be a `WalletKycRequest` object. + + **Response:** + + :http:statuscode:`204 No Content`: + KYC is disabled at this exchange, or the balance is below the + threshold that requires KYC, or this wallet already satisfied + the KYC check for the given balance. + :http:statuscode:`403 Forbidden`: + The provided signature is invalid. + This response comes with a standard `ErrorDetail` response. + :http:statuscode:`451 Unavailable for Legal Reasons`: + The wallet must undergo a KYC check. A KYC ID was created. + The response will be a `KycNeededRedirect` object. + + **Details:** + + .. ts:def:: WalletKycRequest + + interface WalletKycRequest { + + // Balance threshold (not necessarily exact balance) + // to be crossed by the wallet that (may) trigger + // additional KYC requirements. + balance: Amount; + + // EdDSA signature of the wallet affirming the + // request, must be of purpose + // ``TALER_SIGNATURE_WALLET_ACCOUNT_SETUP`` + reserve_sig: EddsaSignature; + + // long-term wallet reserve-account + // public key used to create the signature. + reserve_pub: EddsaPublicKey; + } + + +.. 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. + + **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; + description_i18n: {}; + + // 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.) + + **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. + :query end_date=TIMESTAMP: + *Optional*. Specifies the date when to + stop looking (exclusive). If not given, the current date is used. + + **Response:** + + .. 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. + + :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 responds will be an `AmlDecisions` 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:: AmlDecisions + + interface AmlDecisions { + + // Array of AML decisions matching the query. + records: AmlDecisions[]; + } + + .. ts:def:: AmlRecord + + interface AmlRecord { + + // 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; + + // FIXME: more fields here! + } + + +.. http:get:: /aml/$OFFICER_PUB/attributes/$H_PAYTO + + Obtain attributes obtained as part of AML/KYC processes for a + given account. + + **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 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:`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. + + .. ts:def:: KycAttributes + + interface KycAttributes { + + // Matching KYC attribute history of the account. + details: KycDetail[]; + + } + + .. ts:def:: KycDetail + + // FIXME: bad name? + interface KycDetail { + + // Row ID of the record. Used to filter by offset. + rowid: Integer; + + // Name of the configuration section that specifies the provider + // which was used to collect the attributes. NULL if they were + // just uploaded via a form by the account owner. + provider_section?: string; + + // 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; + + // Time when the KYC data was collected + collection_time: Timestamp; + + } + + + .. http:post:: /aml/$OFFICER_PUB/decision + + Make an AML decision. Triggers the respective action and + records the justification. + + **Request:** + + The request body must be an `AmlDecision` message. + + **Response:** + + :http:statuscode:`204 No content`: + The AML decision has been executed and recorded successfully. + :http:statuscode:`403 Forbidden`: + The signature is invalid. + :http:statuscode:`404 Not found`: + The address the decision was made upon is unknown to the exchange or + the designated AML account is not known. + :http:statuscode:`409 Conflict`: + The designated AML account is not enabled or a more recent + decision was already submitted. + + **Details:** + + .. ts:def:: AmlDecision + + interface AmlDecision { + + // 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; + + // What are the new rules? + new_rules: LegitimizationRuleSet; + + // True if the account should remain under investigation by AML staff. + bool keep_investigating; + + // When was the decision made? + decision_time: Timestamp; + + // Signature by the AML officer over a `TALER_AmlDecisionPS`. + // Must have purpose ``TALER_SIGNATURE_MASTER_AML_KEY``. + officer_sig: EddsaSignature; + + } + + +Modifications to existing endpoints +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When withdrawing, the exchange checks if the KYC status is acceptable. If no +KYC was done and if either the amount withdrawn over a particular timeframe +exceeds the threshold or the reserve received received a P2P transfer, then a +``451 Unavailable for Legal Reasons`` is returned which redirects the consumer +to the new ``/kyc-check/`` handler. + +When depositing, the exchange aggregator (!) checks the KYC status and if +negative, returns an additional information field via the +``aggregation_transient`` table which is returned via GET ``/deposts/`` to the +merchant. This way, the merchant learns the ``requirement_row`` needed to +begin the KYC process (this is independent of the amount) at the new +``/kyc-check/`` handler. + +When merging into a reserve, the KYC status is checked and again the +merge fails with ``451 Unavailable for Legal Reasons`` to trigger the +KYC process. + +To allow the wallet to do the KYC check if it is about to exceed a set balance +threshold, we modify the ``/keys`` response to add an optional array +``wallet_balance_limit_without_kyc`` of threshold amounts is returned. +Whenever the wallet crosses one of these thresholds for the first time, it +should trigger the KYC process. If this field is absent, there is no limit. +If the field is provided, a correct wallet must create a long-term +account-reserve key pair. This should be the same key that is also used to +receive wallet-to-wallet payments. Then, *before* a wallet performs an +operation that would cause it to exceed the balance threshold in terms of +funds held from a particular exchange, it *should* first request the user to +complete the KYC process. For that, the wallet should POST to the new +``/kyc-wallet`` endpoint, providing its long-term reserve-account public key +and a signature requesting permission to exceed the account limit. + + +Configuration of external KYC providers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For each KYC provider that could contribute to checks the configuration +specifies a ``$PROVIDER_SECTION`` for each authentication procedure. For each +(enabled) provider, the exchange has a logic plugin which (asynchronously) +determines the redirect URL for a given wire target. See below for a +description of the high-level process for different providers. + +.. code-block:: ini + + [kyc-provider-$PROVIDER_ID] + + # Which plugin is responsible for this provider? + LOGIC = PLUGIN_NAME + + # Optional cost, useful if clients want to voluntarily + # trigger authentication procedures for attestation. + # Since **vATTEST**. + COST = EUR:5 + + # Plus additional logic-specific options, e.g.: + AUTHORIZATION_TOKEN = superdupersecret + + # Other logic-specific internal options (example): + FORM_ID = business_legi_form + + # Name of a program to run on the output of the plugin + # to convert the result into the desired set of attributes. + # The converter must create a log for the system administrator + # if the provided inputs do not match expectations. + # Note that the converter will be expected to output the + # set of attributes listed under the respective ``[kyc-check-*]`` + # sections. Calling the converter with ``--list-outputs`` + # should generate a (newline-separated) list of attributes + # the converter promises to generate in its JSON output + # (when run regularly). + CONVERTER = taler-exchange-helper-$NAME + + +Configuration of possible KYC/AML checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The configuration specifies a set of possible KYC checks offered by external +providers, one per configuration section: + +.. code-block:: ini + + [kyc-check-$CHECK_NAME] + + # Which type of check is this? Also determines + # the SPA form to show to the user for this check. + # + # INFO: wait for staff or contact staff out-of band + # (only information shown, no SPA action) + # FORM: SPA should show an inline (HTML) form + # LINK: SPA may start external KYC process or upload + # + TYPE = INFO|LINK|FORM + + # Optional. Set to YES to allow this check be + # done voluntarily by a client (they may then + # still have to pay for it). Used to offer the + # SPA to display checks even if they are + # not required. Default is NO. + # Since **vATTEST**. + VOLUNTARY = YES/NO + + # Provider id, present only if type is LINK. + PROVIDER_ID = id + + # Name of the SPA form, if type is FORM + # "INFO" and "LINK" are reserved and must not be used. + # The exchange server and the SPA must agree on a list + # of supported forms and the resulting attributes. + # + # The SPA should include a JSON resource file + # "forms.json" mapping form names to arrays of + # attribute names each form provides. + FORM_NAME = name + + # Descriptions to use in the SPA to display the check. + DESCRIPTION = "Upload your passport picture" + DESCRIPTION_I18N = "{"en":"Upload scan of your passport"}" + + # ';'-separated list of fields that the CONTEXT must + # provided as inputs to this check. For example, + # for a FORM of type CHOICE, this might state + # ``choices: string[];``. The type after the ":" + # is for now purely for documentation and is + # not checked. However, it may be shown to AML staff + # when they configure measures. + REQUIRES = requirement; + + # Description of the outputs provided by the check. + # Basically, the check's output is expected to + # provide the following fields as inputs into + # a subsequent AML program. + OUTPUTS = business_name street city country registration + + # **original** measure to take if the check fails + # (for any reason, e.g. provider or form fail to + # satisfy constraints or provider signals user error) + # Usually should point to a measure that requests + # AML staff to investigate. The fallback measure + # context always includes the reasons for the + # failure. + FALLBACK = MEASURE_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``. + + +Configuration of legitimization requirement triggers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The configuration also specifies a set of legitimization rules including the +condition and the measure the condition triggers, one condition per +configuration section: + +.. code-block:: ini + + [kyc-rule-$RULE_NAME] + + # Operation that triggers this legitimization. + # Must be one of WITHDRAW, DEPOSIT, P2P-RECEIVE + # or WALLET-BALANCE. + OPERATION_TYPE = WITHDRAW + + # Space-separated list of next measures to be performed. + # The SPA should display *all* of these measures to the user. + # (They have a choice of either which ones, or in + # which order they are to be performed.) + # A special measure name "verboten" is used if the + # specified threshold may never be crossed + # (under this set of rules). + NEXT_MEASURES = SWISSNESS KYB + + # "yes" if all NEXT_MEASURES will eventually need + # to be satisfied, "no" if the user has a choice between + # them. Not actually enforced by the exchange, but + # primarily used to inform the user whether this is + # an "and" or "or". YES for "and". + IS_AND_COMBINATOR = YES + + # YES if the rule (specifically, operation type, + # threshold, timeframe) and the general nature of + # the next measure (verboten or approval required) + # should be exposed to the client. + # Defaults to NO if not set. + EXPOSED = YES + + # 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. + # Ignored for WALLET-BALANCE. Can be 'forever'. + TIMEFRAME = 30 days + + # Enabled (default is NO) + ENABLED = NO + + +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: AmlDecisionDetail[]; + + // JSON array with the results of historic + // KYC data about the account. + kyc_history: KycDetail[]; + + } + +.. 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; + + } + +.. ts:def:: KycRule + + interface KycRule { + + // Type of operation to which the rule applies. + operation_type: string; + + // The measures will be taken if the given + // threshold is crossed over the given timeframe. + threshold: Amount; + + // 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; + + // 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[]; + + // 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; + + // 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; + } + +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 + + +Configuration of measures +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Finally, the 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. + + +Sanity checking +^^^^^^^^^^^^^^^ + +On start-up, ``taler-exchange-httpd`` should sanity-check its +configuration. Specifically, it should validate that for all AML programs the +input requirements (attributes and context) are claimed to be satisfied by the +respective checks that may trigger those programs, and similarly that for all +checks the original measures satisfy the context requirements for their KYC +checks. + +As a result, any component (AML program, form or external check) is warranted +to be always called with the declared required inputs. Furthermore, we can +detect if a component fails to produce the required output and the +configuration contains (presumably safe) FALLBACKs to address this case. The +exchange *MUST* detect circular failures, like when a FALLBACK triggers a +measure that itself immediately triggers again the same FALLBACK. + + +Exchange database schema +^^^^^^^^^^^^^^^^^^^^^^^^ + +We introduce a new ``wire_targets`` table into the exchange database. This +table is referenced as the source or destination of payments (regular deposits +and also P2P payments). A positive side-effect is that we reduce duplication +in the ``reserves_in``, ``wire_out`` and ``deposits`` tables as they can +reference this table. + +We introduce a new ``legitimization_processes`` table that tracks the status +of a legitimization process at a provider, including the configuration section +name, the user/account name at the provider, and some legitimization +identifier for the process at the provider. In this table, we additionally +store information related to the KYC status of the underlying payto://-URI, in +particular when the KYC expires (0 if it was never done). + +Finally, we introduce a new ``legitimization_requirements`` table that +contains a list of checks required for a particular wire target. When KYC is +triggered (say when some endpoint returns an HTTP status code of 451) a +new requirement is first put into the requirements table. Then, when the +client identifies as business or individual the specific legitimization +process is started. When the taler-exchange-aggregator triggers a KYC check +the merchant can observe this when a 202 (Accepted) status code is returned +on GET ``/deposits/`` with the respective legitimization requirement row. + + +.. sourcecode:: sql + + CREATE TABLE wire_targets + (wire_target_serial_id BIGSERIAL UNIQUE + ,wire_target_h_payto BYTEA PRIMARY KEY CHECK (LENGTH(wire_target_h_payto)=32), + ,access_token BYTEA UNIQUE CHECK (LENGTH(access_token)=32) DEFAULT gen_random_bytes(32) + ,target_pub BYTEA CHECK (LENGTH(target_pub)=32) DEFAULT NULL + ,payto_uri STRING NOT NULL + ) + PARTITION BY HASH (wire_target_h_payto); + + COMMENT ON TABLE wire_targets + IS 'All recipients of money via the exchange'; + COMMENT ON COLUMN wire_targets.h_payto + IS 'Unsalted hash of payto_uri'; + COMMENT ON COLUMN wire_targets.access_token + IS 'high-entropy random value that is used as a token to authorize access to the KYC process (without requiring a signature by target_priv)'; + COMMENT ON COLUMN wire_targets.target_pub + IS 'Public key (reserve_pub or merchant_pub) associated with the account; NULL if KYC is not allowed for the account (if there was no incoming KYC wire transfer yet); updated, thus NOT available to the auditor'; + COMMENT ON COLUMN wire_targets.payto_uri + IS 'Can be a regular bank account, or also be a URI identifying a reserve-account (for P2P payments)'; + + CREATE TABLE IF NOT EXISTS legitimization_measures + (legitimization_measure_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY + ,access_token BYTEA NOT NULL UNIQUE CHECK (LENGTH(access_token)=32) + REFERENCES wire_targets (access_token) + ,start_time INT8 NOT NULL + ,jmeasures TEXT NOT NULL + ,display_priority INT4 NOT NULL + ,is_finished BOOL NOT NULL DEFAULT(FALSE) + ) + PARTITION BY HASH (access_token); + + COMMENT ON COLUMN legitimization_measures.access_token + IS 'Used to uniquely identify the account and as a symmetric access control mechanism for the SPA'; + COMMENT ON COLUMN legitimization_measures.start_time + IS 'Time when the measure was triggered (by decision or rule)'; + COMMENT ON COLUMN legitimization_measures.jmeasures + IS 'JSON object of type LegitimizationMeasures with KYC/AML measures for the account encoded'; + COMMENT ON COLUMN legitimization_measures.display_priority + IS 'Display priority of the rule that triggered this measure; if in the meantime another rule also triggers, the measure is only replaced if the new rule has a higher display priority'; + COMMENT ON COLUMN legitimization_measures.is_finished + IS 'Set to TRUE if this set of measures was processed; used to avoid indexing measures that are done'; + + CREATE INDEX ON legitimization_measures (access_token) + WHERE NOT is_finished; + + CREATE TABLE legitimization_outcomes + (outcome_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY + ,h_payto BYTEA CHECK (LENGTH(h_payto)=32) + REFERENCES wire_targets (wire_target_h_payto) + ,decision_time INT8 NOT NULL DEFAULT(0) + ,expiration_time INT8 NOT NULL DEFAULT(0) + ,jproperties TEXT, + ,to_investigate BOOL NOT NULL + ,is_active BOOL NOT NULL DEFAULT(TRUE) + ,jnew_rules TEXT NOT NULL + ) + PARTITION BY HASH (h_payto); + + COMMENT ON TABLE legitimization_outcomes + IS 'Outcomes can come from AML programs'; + COMMENT ON COLUMN legitimization_outcomes.h_payto + IS 'hash of the payto://-URI this outcome is about'; + COMMENT ON COLUMN legitimization_outcomes.decision_time + IS 'when was this outcome decided'; + COMMENT ON COLUMN legitimization_outcomes.expiration_time + IS 'time when the decision expires and the expiration jnew_rules should be applied'; + COMMENT ON COLUMN legitimization_outcomes.jproperties + IS 'JSON object of type AccountProperties, such as PEP status, business domain, risk assessment, etc.'; + COMMENT ON COLUMN legitimization_outcomes.to_investigate + IS 'AML staff should investigate the activity of this account'; + COMMENT ON COLUMN legitimization_outcomes.is_active + IS 'TRUE if this is the current authoritative legitimization outcome'; + COMMENT ON COLUMN legitimization_outcomes.jnew_rules + IS 'JSON object of type LegitimizationRuleSet with rules to apply to the various operation types for this account; all KYC checks should first check if active new rules for a given account exist in this table (and apply specified measures); if not, it should check the default rules to decide if a measure is required'; + + CREATE INDEX legitimization_outcomes_active + ON legitimization_outcomes(h_payto) + WHERE is_active; + + CREATE TABLE legitimization_processes + (legitimization_process_serial_id BIGSERIAL UNIQUE + ,h_payto BYTEA NOT NULL CHECK (LENGTH(h_payto)=64) + REFERENCES wire_targets (wire_target_h_payto) + ,start_time INT8 NOT NULL + ,expiration_time INT8 NOT NULL DEFAULT (0) + ,legitimization_measure_serial_id INT8 + REFERENCES legitimization_measures (legitimization_measure_serial_id) + ,measure_index INT4 + ,provider_section TEXT NOT NULL + ,provider_user_id TEXT DEFAULT NULL + ,provider_legitimization_id TEXT DEFAULT NULL + ,redirect_url TEXT DEFAULT NULL + ,finished BOOLEAN DEFAULT (FALSE) + ) + PARTITION BY HASH (h_payto); + + COMMENT ON TABLE legitimization_processes + IS 'here we track KYC processes we initiated with external providers; the main reason is so that we do not initiate a second process when an equivalent one is still active; note that h_payto, provider_section, jcontext must match and the process must not be finished or expired for an existing redirect_url to be re-used; given that clients may voluntarily initiate KYC processes, there may not always be a legitimization_measure that triggered the setup'; + COMMENT ON COLUMN legitimization_processes.h_payto + IS 'foreign key linking the entry to the wire_targets table, NOT a primary key (multiple KYC setups are possible per wire target)'; + COMMENT ON COLUMN legitimization_processes.start_time + IS 'when was the legitimization process initiated'; + COMMENT ON COLUMN legitimization_processes.expiration_time + IS 'when does the process expire (and needs to be manually set up again)'; + COMMENT ON COLUMN legitimization_processes.measure_index + IS 'index of the measure in legitimization_measures that was selected for this KYC setup; NULL if legitimization_measure_serial_id is NULL; enables determination of the context data provided to the external process'; + COMMENT ON COLUMN legitimization_processes.provider_section + IS 'Configuration file section with details about this provider'; + COMMENT ON COLUMN legitimization_processes.provider_user_id + IS 'Identifier for the user at the provider that was used for the legitimization. NULL if provider is unaware.'; + COMMENT ON COLUMN legitimization_processes.provider_legitimization_id + IS 'Identifier for the specific legitimization process at the provider. NULL if legitimization was not started.'; + COMMENT ON COLUMN legitimization_processes.legitimization_measure_serial_id + IS 'measure that enabled this setup, NULL if client voluntarily initiated the process'; + COMMENT ON COLUMN legitimization_processes.redirect_url + IS 'Where the user should be redirected for this external KYC process'; + COMMENT ON COLUMN legitimization_processes.finished + IS 'set to TRUE when the specific legitimization process is finished'; + + CREATE TABLE kyc_attributes + (kyc_attributes_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY + ,h_payto BYTEA PRIMARY KEY CHECK (LENGTH(h_payto)=32) + REFERENCES wire_targets (wire_target_h_payto) + ,legitimization_process_serial_id INT8 + REFERENCES legitimization_processes (legitimization_process_serial_id) + DEFAULT NULL + ,collection_time INT8 NOT NULL + ,expiration_time INT8 NOT NULL + ,trigger_outcome_serial INT8 NOT NULL + REFERENCES legitimization_outcomes(outcome_serial_id) + ,encrypted_attributes BYTEA NOT NULL + ) PARTITION BY HASH (h_payto); + + COMMENT ON COLUMN kyc_attributes.h_payto + IS 'identifies the account this is about'; + COMMENT ON COLUMN kyc_attributes.legitimization_process_serial_id + IS 'serial ID of the legitimization process that resulted in these attributes, NULL if the attributes are from a form directly supplied by the account owner via a form'; + COMMENT ON COLUMN kyc_attributes.collection_time + IS 'when were these attributes collected'; + COMMENT ON COLUMN kyc_attributes.expiration_time + IS 'when are these attributes expected to expire'; + COMMENT ON COLUMN kyc_attributes.trigger_outcome_serial + IS 'ID of the outcome that was returned by the AML program based on the KYC data collected'; + COMMENT ON COLUMN kyc_attributes.encrypted_attributes + IS 'encrypted JSON object with the attribute data the check provided'; + + CREATE TABLE aml_history + (aml_history_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY + ,h_payto BYTEA CHECK (LENGTH(h_payto)=32) + REFERENCES wire_targets (wire_target_h_payto) + ,outcome_serial_id INT8 NOT NULL + REFERENCES legitimization_outcomes (outcome_serial_id) + ,justification TEXT NOT NULL + ,decider_pub BYTEA CHECK (LENGTH(decider_pub)=32) + ,decider_sig BYTEA CHECK (LENGTH(decider_sig)=64); + + COMMENT ON TABLE aml_history + IS 'Records decisions by AML staff with the respective signature and free-form justification.'; + COMMENT ON COLUMN aml_history.outcome_serial_id + IS 'Actual outcome for the account (included in what decider_sig signs over)'; + COMMENT ON COLUMN aml_history.decider_sig + IS 'Signature key of the staff member affirming the AML decision; of type AML_DECISION'; + + CREATE TABLE kyc_events + (kyc_event_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY + ,event_timestamp INT8 NOT NULL + ,event_type TEXT NOT NULL); + + COMMENT ON TABLE kyc_events + IS 'Records of key events for statistics. Populated via triggers.'; + COMMENT ON COLUMN kyc_events.event_type + IS 'Name of the event, such as account-open or sar-filed'; + + CREATE INDEX kyc_event_index + ON kyc_events(event_type,event_timestamp); + + +The ``jmeasures`` JSON in the ``legitimization_measures`` +table has is of type `LegitimizationMeasures`: + +.. ts:def:: LegitimizationMeasures + + interface LegitimizationMeasures { + + // Array of legitimization measures that + // are to be applied. + measures: MeasureInformation[]; + + // True if the client is expected to eventually satisfy all requirements. + // Default (if missing) is false. + is_and_combinator?: boolean; + } + + +The ``jnew_rules`` JSON in the ``legitimization_outcomes`` +table has is of type `LegitimizationRuleSet`: + +.. 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; }; + } + + +The ``jproperties`` JSON in the ``legitimization_outcomes`` table has is of +type `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. + +.. ts:def:: AccountProperties + + 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; + + } + + + +KYC forms +^^^^^^^^^ + +The KYC SPA run by clients needs to support three TYPEs of checks. INFO is +only about displaying the provided information, LINK is about setting up an +exteral KYC check and redirecting there. FORM is about displaying a particular +(HTML) form to the user and POSTing the entered information directly with the +exchange. Here we describe the forms that must be supported: + +* **CHOICE**: Asks the client a multiple-choice question. The context must + include "choices: string[]" with a list of choices to show. Used, for + example, to ask a client if they are an individual or a business. The + resulting HTML FORM field name must be "choice" and it must be mapped to + strings from the choices list. + +* **UPLOAD**: Asks the client to upload a single file. + The context must include a ``validity_duration`` which + will be converted to the ``expiration_time`` for + the uploaded data. The context may furthermore include + ``extensions?: string[]`` with a list of allowed file extensions the client's + file must end with (e.g. "png", "pdf", "gif"). In the absence of this + context, any file may be uploaded. The context may also include a + ``size_limit?: Integer`` with the maximum file size in bytes that can be + uploaded. The resulting HTTP POST should provide at least two fields, "filename" and + "filedata". "filename" must be set to the basename of the original file (to + the extend that it is available), and "filedata" to the base64-encoding of + the uploaded data. + +As with other SPA checks, the KYC form should also show +the description of the check. + + +Merchant modifications +^^^^^^^^^^^^^^^^^^^^^^ + +A new setting is required where the merchant backend can be configured for a +business (default) or individual. + +We introduce new ``kyc_ok``, ``aml_decision``, ``kyc_timestamp`` and +``exchange_kyc_serial`` fields into a new table ``merchant_kyc`` with primary +keys ``exchange_url`` and ``account_serial``. This status is updated whenever +a deposit is created or tracked, or whenever the mechant backend receives a +``/kyc-check/`` response from the exchange. Initially, +``exchange_kyc_serial`` is zero, indicating that the merchant has not yet made +any deposits and thus does not have an account at the exchange. + +A new private endpoint ``/kyc`` is introduced which allows frontends to +request the ``/kyc`` status of any configured account (including with long +polling). If the KYC status is negative or the ``kyc_timestamp`` not recent +(say older than one month), the merchant backend will re-check the KYC status +at the exchange (and update its cached status). The endpoint then returns +either that the KYC is OK, or information (same as from the exchange endpoint) +to begin the KYC process. + +The merchant backend uses the new field to remember that a KYC is pending +(after detection in ``taler-merchant-depositcheck``) and the SPA then shows a +notification whenever the staff is logged in to the system. The notification +can be hidden for the current day (remembered in local storage). + +The notification links to a (new) KYC status page. When opened, the KYC SPA +first re-checks the KYC status with the exchange. If the KYC is still +unfinished, that SPA will show forms, links or contact information to begin +the KYC process (for example, redirecting to the OAuth 2.0 login page of the +legitimization resource server), otherwise it shows that the KYC process is +done. If the KYC is unfinished, the merchant SPA should use long-polling on +the KYC status on this page to ensure it is always up-to-date, and change to +``KYC satisfied`` should the long-poller return with positive news. + + ..note:: + + Semi-related: The TMH_setup_wire_account() is changed to use + 128-bit salt values (to keep ``deposits`` table small) and checks for salt + to be well-formed should be added "everywhere". + + + +Bank requirements +^^^^^^^^^^^^^^^^^ + +The exchange primarily requires a KYC provider to be operated by the +bank that offers an endpoint for with an API implemented by one of +the logic plugins (and the respective legitimization configuration). + + +Logic plugins +^^^^^^^^^^^^^ + +The ``$PROVIDER_SECTION`` is based on the name of the configuration section, +not on the name of the logic plugin (that we call ``$LOGIC``). Using the +configuration section, the exchange then determines the logic plugin to use. + +This section describes the general API for all of the supported KYC providers, +as well as some details of how this general API could be implemented by the +logic for different APIs. + + +General KYC Logic Plugin API +---------------------------- + +This section provides a sketch of the proposed API for the KYC logic plugins. + +* initiation of KYC check (``kyc-check``): + + - inputs: + + provider_section (for additional configuration) + + h_payto + - outputs: + + success/provider-failure + + redirect URL (or NULL) + + provider_user_id (or NULL) + + provider_legitimization_id (or NULL) + +* KYC status check (``kyc-proof``): + + - inputs: + + provider_section (for additional configuration) + + h_payto + + provider_user_id (or NULL) + + provider_legitimization_id (or NULL) + - outputs: + + success/pending/user-aborted/user-failure/provider-failure status code + + HTML response for end-user + +* Webhook notification handler (``kyc-webhook``): + + - inputs: + + HTTP method (GET/POST) + + rest of URL (after provider_section) + + HTTP body (if applicable!) + - outputs: + + success/pending/user-aborted/user-failure/provider-failure status code + + h_payto (for DB status update) + + HTTP response to be returned to KYC provider + +The plugins do not directly interact with the database, the caller sets the +expiration on ``success`` and also updates ``provider_user_id`` and +``provider_legitimization_id`` in the tables as required. + + +For the webhook, we need a way to lookup ``h_payto`` by other data, so the +KYC logic plugin API should be provided a method lookup with: + + - inputs: + + ``provider_section`` + + ``provider_legitimization_id`` + - outputs: + + ``h_payto`` + + ``legitimization_process_row`` + + +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. + +For the ``/kyc-check/`` endpoint, the OAuth 2.0 logic may need to create and +store a nonce to be used during ``/kyc-proof/``, depending on the OAuth +variant used. This may require another exchange table. The OAuth 2.0 process +must then be set up to end at the new ``/kyc-proof/$PROVIDER_ID/`` endpoint. + +This ``/kyc-proof/oauth2/`` endpoint must query the OAuth 2.0 server using the +``code`` argument provided as a query parameter. Based on the result, it then +updates the KYC table of the exchange with the legitimization status and +returns a human-readable KYC status page. + +The ``/kyc-webhook/`` is not applicable. + + +Persona specifics +----------------- + +We would use the hosted flow. Endpoints return a ``request-id``, which we should +log for diagnosis. + +For ``/kyc-check/``: + +* Post to ``/api/v1/accounts`` using ``reference-id`` set to our ``h_payto``. + Returns ``id`` (account_id). + +* Create ``/verify`` endpoint using ``template-id`` (from configuration), + and ``account_id`` (from previous step) and a ``reference-id`` (use + the ``legitimization_serial_id`` for the new process). Set + ``redirect-uri`` to ``/kyc-proof/$PROVIDER_ID/``. However, we cannot + rely on the user clicking this, so we must also configure a webhook. + The request returns a '``verification-id``. That we store under + the ``provider_legitimization_id`` in the database. + +For ``/kyc-proof/``: + +* Use the ``/api/v1/verifications`` endpoint to get the verification + status. Requires the ``verification-id`` from the previous step. + Results include: created/pending/completed/expired (aborted)/failed. + +For ``/kyc-webhook/``: + +* The webhook is authenticated using a shared secret, which should + be in the configuration. So all we should have to do is parse + the POSTed body to find the status and the ``verification-id`` to + lookup ``h_payto`` and return the result. + + +KYC AID specifics +----------------- + +For ``/kyc-check/``: + +* Post to ``/applicants`` with a type (person or company) to + obtain ``applicant_id``. Store that under ``provider_user_id``. + ISSUE: *we* need to get the company_name, business_activity_id + and registration_country before this somehow! + +* start with create form URL ``/forms/$FORM_ID/urls`` + providing our ``h_payto`` as the ``external_applicant_id``, + using the ``applicant_id`` from above, + and the ``/kyc-proof/$PROVIDER_ID`` for the ``redirect_url``. + +* redirect customer to the ``form_url``, + store the ``verification_id`` under ``provider_legitimization_id`` + in the database. + +For ``/kyc-proof/``: + +* Not needed, just return an error. + +For ``/kyc-webhook/``: + +* For security, we should probably simply trigger the GET on + ``/verifications/{verification_id}`` to not trust an unsigned POST + to tell us anything for sure. The result is then returned. + + +Types of KYC events +^^^^^^^^^^^^^^^^^^^ + +The ``/aml/$OFFICER_PUB/kyc-statistics`` endpoint exposes statistics for +various KYC event types. + +We will initially support the use of the following types of KYC events in the +SPA (and have a dialog to show the total number of any of these for any +specified time range): + +* account-open +* account-closed +* voluntary-sar +* mandatory-sar +* pep-started +* pep-ended +* risky-started +* risky-ended +* account-frozen +* account-unfrozen + +Based on these, the SPA should also be albe to show active +statistics (for any given timestamp) on the total number of: + +* open accounts +* frozen accounts +* high-risk accounts +* PEPs served + +.. note:: + + This can be done by simply running the queries with + a start time of zero and subtracting. + + +Alternatives +============ + +We could also store the access token (returned by OAuth 2.0), but that seems +slightly more dangerous and given the close business relationship is +unnecessary. Furthermore, not all APIs offer this. + +We could extend the KYC logic API to return key attributes about the user +(such as legal name, phone number, address, etc.) which we could then sign and +return to the user. This would be useful in P2P payments to identify the +origin of an invoice. However, we might want to be careful to not disclose +the key attributes via the API by accident. This could likely be done by +limiting access to the respective endpoint to messages with a signature by the +reserve private key (which is the only case where we care to certify things +anyway). + + +Drawbacks +========= + + +Discussion / Q&A +================ + +(This should be filled in with results from discussions on mailing lists / personal communication.) |