commit 5ff0e48679195af77b6c7488dbcefcc5fd320555
parent 6b7277a012c35b1cb17c7fd835ec2d4aac6d0195
Author: Antoine A <>
Date: Thu, 19 Feb 2026 02:56:23 +0100
dd80: update proposal
Diffstat:
1 file changed, 130 insertions(+), 204 deletions(-)
diff --git a/design-documents/080-short-wire-subject.rst b/design-documents/080-short-wire-subject.rst
@@ -1,11 +1,13 @@
-DD 80: Short wire transfer subjects
-###################################
+DD 80: Alternative wire transfer subjects
+#########################################
Summary
=======
-Some mediums and clients do not support subjects big enough to fit an entire reserve public key.
-We need to add an endpoint to link a reserve public key to shorter sequences than can fit.
+Some mediums and clients do not support subjects large enough to contain an entire reserve public key,
+and filling in the subject manually is very error-prone.
+We need a way to generate new input methods for wire transfers that are linked to the metadata currenlty stored in the unstructured subject.
+We also need support for recurring transfers.
Problem
=======
@@ -140,189 +142,35 @@ configurable **hold delay** or such a registration is received.
Subject derivation
------------------
-When the encoding space is not limited or large enough like in SEPA transfers,
-no special derivation logic is used and we use the existing one.
-
-The manner in which subjects are derived is voluntarily not specified, clients
-should not expect anything about the derivation logic and treat it as a black box.
-
-As the API must idempotent and derivation must use the entire coding space I expect
-all implementation to converge on the same deterministic algorithm.
-
-All non trivial subject derivation would start by making a SHA-512 hash of the key and metadata:
-SHA-512("RESERVE" + "ECDSA" + raw key bytes) or SHA-512("KYC" + "SLH-DSA" + raw key bytes).
-
-For binary subject format like BTC we simply truncate the hash. For text subject format it's way
-harder as we need to choose the alphabet to use for encoding.
-We currently use Crockford's Base32 because it is case-insensitive and typo complacent.
-However it make a poor use of the available entropy.
-
-We will have some format that only accept numbers 0-9 (10 chars), some will only accept simple
-ASCII A-Za-z0-9 (62 chars) and most will also accept some special characters A-Za-z0-9-: (64 characters).
-
-When we have enough space we should simply use our current Crockford’s Base32 and truncate the subject.
-It's working well and is efficient. With 16 characters we can encode 80bits and that is enough entropy
-for our use case.
-
-If we have access to less characters or have very limited alpahbet we will have to use a more complex
-approach an Arbitrary-Precision Base Conversion algorithm. We treat the hash as a single unsigned integer in base 256
-that we convert into a base A (length of the alphabet). Then using successive euclidean division we can ensure
-mathematical fariness across any alphabet size.
-
-.. code-block:: kotlin
-
- import java.math.BigInteger
- import java.security.MessageDigest
-
- fun deterministicEncode(data: ByteArray, alphabet: String, n: Int): String {
- val hash = MessageDigest.getInstance("SHA-512").digest(data)
-
- var value = BigInteger(1, hash)
- val base = BigInteger.valueOf(alphabet.length.toLong())
- val result = StringBuilder()
-
- repeat(n) {
- val (quotient, remainder) = value.divideAndRemainder(base)
- result.append(alphabet[remainder.toInt()])
- value = quotient
- }
- return result.reverse().toString()
- }
-
-- SIMPLE: no special encoding use the current very verbose encoding
-- CH_YUH: BASE32 encode and trunc N chars
-- NZ_SBI: BASE32 encode and trunc 36 char split in 3 chunk of 12 char
-- BTC: trunc and encode in segwit address
-- URI: the subject derivation depends on the underlying system but is similar
-
-.. ts:def:: TransferSubject
-
- // Union discriminated by the "type" field.
- type TransferSubject =
- | SimpleSubject
- | UriSubject
- | BitcoinSubject
- | NzSbiSubject;
-
-.. ts:def:: SimpleSubject
-
- interface SimpleSubject {
- // Subject for system accepting large subjects
- type: "SIMPLE";
-
- // Amount to transfer
- credit_amount: Amount;
-
- // Encoded string containing either the full key and transfer type or a derived short subject
- subject: string;
- }
-
-.. ts:def:: UriSubject
-
- interface UrlSubject {
- // Subject for system accepting prepared payments
- type: "URI";
-
- // Withdrawal confirmation URI
- uri: string;
- }
-
-.. ts:def:: BitcoinSubject
-
- interface BitcoinSubject {
- // Subject for Bitcoin networks
- type: "BTC";
-
- // Amount to transfer
- credit_amount: Amount;
-
- // Encoded bitcoin address to add as recipent whith minimum amount.
- address: string;
- }
-
-.. ts:def:: NzSbiSubject
-
- interface NzSbiSubject {
- // Subject for New Zealand Settlement Before Interchange system
- type: "NZ_SBI";
-
- // Amount to transfer
- credit_amount: Amount;
-
- // 12 chars to set in the Particulars field
- particulars: string;
-
- // 12 chars to set in the Code field
- code: string;
-
- // 12 chars to set in the Reference field
- reference: string
- }
-
-Sometimes some clients will have more restriction than others. We will sometimes supports many subject format.
-
-.. ts:def:: SubjectFormat
-
- // Subject format supported by the client
- // SIMPLE: SEPA like format >= 140 chars
- // CH-YUH: N chars ?
- // NZ-SBI: New Zealand special three fields
- // BTC: bitcoin address
- type SubjectFormat =
- | "SIMPLE"
- | "CH_YUH"
- | "URI"
- | "NZ_SBI"
- | "BTC";
-
-The backend will expose the supported formats in its ``/config`` endpoint.
-
-- "SIMPLE" & "CH_YUH" -> SimpleSubject
-- "URI" -> UriSubject
-- "BTC" -> BitcoinSubject
-- "NZ_SBI" -> NzSbiSubject
+The subject derivation must be deterministic and and use the entire coding space.
-Proof of Work
--------------
+We will continue to support the current encoding for simple cases.
-To prevent bad actors from exhausting the encoding space we SHOULD add some PoW to each request. It's stays optional are in some cases the encoding space is so large than no special encoding is used.
+All subject derivation starts by making a hash of the key and metadata as follow:
-As the available entropy space goes down, the difficulty to register a new key increase automatically as new derived keys will collide with existing ones. The PoW difficulty is another layer of protection that should be based on request rate.
+- SHA-512("RESERVE:ECDSA" + raw key bytes)
+- SHA-512("KYC:SLH-DSA" + raw key bytes)
-.. ts:def:: PowChallenge
+TODO do we really nead SHA-512. Why not SHA-256 as we will never be able to fit all this entropy.
- interface PowChallenge {
- // Algorithm used to perform PoW, new ones will be added in the future
- // PBKDF2-HMAC-SHA512: PBKDF2 using SHA-512
- alg: "PBKDF2-HMAC-SHA512";
+Then the hash bits are encoded differently for each subject format:
- // How many iterations to run, must be > 0
- iterations: Integer;
+Swiss QR-bill
+^^^^^^^^^^^^^
- // Number of lead bits that must be zero for the challenge to be accepted,
- // must be > 0
- difficulty: Integer;
-
- // Unique salt to use when solving the challenge
- salt: string;
- }
-
-The backend provides a salt challenge encoded as a string and expects the nonce solution to be a string. This design will work with all clients on all platforms.
-The challenge can be linked to a request, a user, a session, etc. It can be reused for a certain number of requests or for a certain period of time. All of these options should be supported by the current API, allowing us to strengthen security in the future in a transparent manner.
+Treat the whole hash as a big integer then modulo by 10 power 26.
+Encode the remainder into a string and add the 27th checksum character according to QR-bill spec.
Auditor
-------
-Keep an history of all generated subject with their creation/expiration date for the auditor to match subject to keys.
-
-API
----
+By running the subject derivation logic itself, the auditor can match corresponding transfers.
+Taler Wire Transfer HTTP API
+----------------------------
.. http:get:: /config
- Only new fields are listed there:
-
**Response:**
:http:statuscode:`200 OK`:
@@ -331,9 +179,16 @@ API
**Details:**
- .. ts:def:: WireConfig
+ .. ts:def:: SubjectFormat
+
+ type SubjectFormat =
+ | "SIMPLE"
+ | "URI"
+ | "CH_QR_BILL";
- interface WireConfig {
+ .. ts:def:: WireTransferConfig
+
+ interface WireTransferConfig {
// Supported formats for registration, there must at least one.
supported_formats: SubjectFormat[];
}
@@ -346,51 +201,44 @@ API
This endpoint generate an appropriate subject to link a transfer to the
registered public key.
- A mapping public key can also be used for repeated wire transfers.
+ A mapping public key must be used to allow recurrent wire transfers.
Reusing a mapping public key replace previous mapping.
- As this endpoint is unauthenticated a PoW challenge may need to be
- solved. In this case the solution is included using the following HTTP
- headers: ``Taler-Challenge-PoW-Salt`` and ``Taler-Challenge-PoW-Nonce``.
- A challenge solution might be reused, clients should store and reuse
- previous solution.
-
- A successfully completed challenge may be followed by a new challenge, and several challenges may need to be successfully completed. The client must handle this correctly and try again with the new challenge.
-
**Request:**
.. ts:def:: SubjectRequest
interface SubjectRequest {
- // Public key algorithm;
- alg: "ECDSA";
+ // Subject format requested
+ format: SubjectFormat;
- // Encoded public key
- key: EddsaPublicKey;
+ // Amount to transfer
+ credit_amount: Amount;
// Transfer types
type: "reserve" | "kyc";
- // Subject format requested
- format: SubjectFormat;
-
- // Amount to transfer
- credit_amount: Amount;
+ // Public key algorithm
+ alg: "ECDSA";
+
+ // Account public key
+ account_pub: EddsaPublicKey;
+
+ // Public key encoded inside the subject
+ authorization_key: EddsaPublicKey;
- // Optional mapping public key that will be used in the encoded subject instead
- map?: EddsaPublicKey;
+ // Signature of the account_pub public key using the authorization_key private key
+ authorization_signature: EddsaSignature;
- // Optional signature of the raw public key using the mapping key,
- // required if map is not null
- signature?: string;
+ // Whether the authorization_key will be reused for recurrent transfers
+ // Disable bounces in case of authorization_key reuse
+ recurrent: boolean;
}
**Response:**
:http:statuscode:`200 Ok`:
Response is a `SubjectResult`.
- :http:statuscode:`202 Accepted`:
- PoW is required for this operation. This returns the `PowChallenge` response.
:http:statuscode:`400 Bad request`:
Input data was invalid.
:http:statuscode:`409 Conflict`:
@@ -401,17 +249,95 @@ API
**Details:**
+
+ .. ts:def:: TransferSubject
+
+ // Union discriminated by the "type" field.
+ type TransferSubject =
+ | SimpleSubject
+ | UriSubject
+ | SwissQrBillSubject;
+
+ .. ts:def:: SimpleSubject
+
+ interface SimpleSubject {
+ // Subject for system accepting large subjects
+ type: "SIMPLE";
+
+ // Amount to transfer
+ credit_amount: Amount;
+
+ // Encoded string containing either the full key and transfer type or a derived short subject
+ subject: string;
+ }
+
+ .. ts:def:: UriSubject
+
+ interface UriSubject {
+ // Subject for system accepting prepared payments
+ type: "URI";
+
+ // Prepared payments confirmation URI
+ uri: string;
+ }
+
+ .. ts:def:: SwissQrBillSubject
+
+ interface SwissQrBillSubject {
+ // Subject for Swiss QR Bill
+ type: "CH_QR_BILL";
+
+ // Amount to transfer
+ credit_amount: Amount;
+
+ // 27-digit QR Reference number
+ reference: string;
+ }
+
.. ts:def:: SubjectResult
interface SubjectResult {
// Subject to use
subject: TransferSubject;
- // Expiration date after which this subject can be reused and if mapping
- // is used when it expired.
+ // Expiration date after which this subject will be reused
expiration: Timestamp;
}
+.. http:delete:: /registration
+
+ Remove an existing registration.
+
+ Use this endpoint to free a derived subject or cancel a recurrent paiment.
+
+ **Request:**
+
+ .. ts:def:: Unregistration
+
+ interface Unregistration {
+ // Current timestamp in the ISO 8601
+ timestamp: string;
+
+ // Public key used for registration
+ authorization_key: EddsaPublicKey;
+
+ // Signature of the timestamp using the authorization_key private key
+ // Necessary to prevent replay attack
+ authorization_signature: EddsaSignature;
+ }
+
+ **Response:**
+
+ :http:statuscode:`204 No content`:
+ The registration have been deleted.
+ :http:statuscode:`400 Bad request`:
+ Input data was invalid.
+ :http:statuscode:`404 Not found`:
+ Unknown registration.
+ :http:statuscode:`409 Conflict`:
+ * ``TALER_EC_BANK_OLD_TIMESTAMP``: the timestamp is too old.
+ * ``TALER_EC_BANK_BAD_SIGNATURE``: signature is invalid.
+
Test Plan
=========
@@ -419,9 +345,9 @@ Test Plan
Definition of Done
==================
-* New endpoints supported by all wire gateways
-* Support for Swiss-specific wire transfer subjects
-* Exchange points wallets/merchants to wire gateway APIs
+* New API supported by all wire gateways
+* Support for Swiss QR Bill wire transfer subjects
+* Exchange points wallets/merchants to wire transfer API
* Wallets support registration
* Wallets have UI where the user can specify "periodic"
wire transfers where the wallets periodically try to