commit 1115588a771ce842472ddbdb1ff13ece93237f20
parent beccb8dc7add5c843adf1b2c2fd2c2bb86e01068
Author: Antoine A <>
Date: Wed, 14 Jan 2026 14:23:50 +0100
dd80: progress on logic and API
Diffstat:
1 file changed, 228 insertions(+), 0 deletions(-)
diff --git a/design-documents/080-short-wire-subject.rst b/design-documents/080-short-wire-subject.rst
@@ -137,6 +137,234 @@ for the registration public key, the exchange is informed. Otherwise,
the wire transfer is put in a "hold" state until either it passes some
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.
+
+Short subject are derived from a SHA-512 hash of the key and metadata.
+
+SHA-512("RESERVE" + "ECDSA" + raw key bytes)
+
+SHA-512("KYC" + "SLH-DSA" + raw key bytes)
+
+Then the hash is truncated to the number of bytes we can fill in the subject and encoded using a supported alphabet.
+
+TODO: define derivation with precision
+
+- SIMPLE: BASE32 encode and trunc
+- BTC: trunc and encode in segwit address
+- NZ-SBI: BASE32 encode and trunc 36 char
+
+TODO: do we add a prefix ?
+
+.. ts:def:: TransferSubject
+
+ // Union discriminated by the "type" field.
+ type TransferSubject =
+ | SimpleSubject
+ | BitcoinSubject
+ | NzSbiSubject;
+
+.. ts:def:: SimpleSubject
+
+ interface SimpleSubject {
+ // Subject for system accepting large subjects
+ type: "SIMPLE";
+
+ // Encoded string containing either the full key and transfer type or a derived short subject
+ subject: string;
+ }
+
+ interface BitcoinSubject {
+ // Subject for Bitcoin networks
+ type: "BTC";
+
+ // Encoded bitcoin address to add as recipent whith minimum amount.
+ address: string;
+ }
+
+ interface NzSbiSubject {
+ // Subject for New Zealand Settlement Before Interchange system
+ type: "NZ-SBI";
+
+ // 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 ?
+ // BTC: bitcoin address
+ // NZ-SBI: New Zealand special three fields
+ type SubjectFormat = "SIMPLE" | "CH-YUH" | "BTC" | "NZ-SBI"
+
+The backend will expose the supported formats in its ``/config`` endpoint.
+
+- "SIMPLE" & "CH-YUH" -> SimpleSubject
+- "BTC" -> BitcoinSubject
+- "NZ-SBI" -> NzSbiSubject
+
+Proof of Work
+-------------
+
+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.
+
+The algorithm and the difficulty can change over time and the client needs to handle those changes.
+
+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.
+
+TODO: what is a good value for no POW: null or none or identify
+
+.. ts:def:: PowChallenge
+ interface PowChallenge {
+ // Algorithm used to perform PoW, new ones will be added in the future
+ // PBKDF2-HMAC-SHA256: PBKDF2 using SHA-512
+ // none: no PoW is required
+ alg: "PBKDF2-HMAC-SHA256" | "none";
+
+ // How many iterations to run, used by PBKDF2-HMAC-SHA256
+ // Set to zero for none
+ iterations: Integer;
+
+ // Number of lead bits that must be zero for the challenge to be accepted
+ difficulty: Integer;
+ }
+
+Auditor
+-------
+
+Keep an history of all generated subject with their creation/expiration date for the auditor to match subject to keys.
+
+API
+---
+
+
+.. http:get:: /config
+
+ Only new fields are listed there:
+
+ **Response:**
+
+ :http:statuscode:`200 OK`:
+ The exchange responds with a `WireConfig` object. This request should
+ virtually always be successful.
+
+ **Details:**
+
+ .. ts:def:: WireConfig
+
+ interface WireConfig {
+ // Current pow challenge being used, can change over time
+ pow: PowChallenge;
+
+ // Supported formats for registration, there must at least one.
+ supported_formats: SubjectFormat[];
+ }
+
+
+.. http:post:: /subject
+
+ Get an appropriate subject to link a transfer to a public key.
+
+ **Request:**
+
+ .. ts:def:: SubjectRequest {
+ // Public key algorithm;
+ alg: "ECDSA";
+
+ // Encoded public key
+ key: EddsaPublicKey;
+
+ // Transfer types
+ type: "reserve" | "kyc" | "link";
+
+ // Pow salt used
+ pow: String;
+
+ // Subject format requested
+ format: SubjectFormat;
+
+ // Optional expiration date, null or never will use a default expiration
+ expiration?: Timestamp;
+ }
+
+ **Response:**
+
+ :http:statuscode:`200 Ok`:
+ Response is a `SubjectResult`.
+ :http:statuscode:`400 Bad request`:
+ Input data was invalid.
+ :http:statuscode:`409 Conflict`:
+ * ``TALER_EC_BANK_UNSUPPORTED_FORMAT``: format is not supported.
+ * ``TALER_EC_BANK_POW_FAILURE``: PoW checks failed, the PoW configuration might have changed.
+ * ``TALER_EC_DERIVATION_REUSE``: derived short subject is already used, you should retry using another key.
+
+ **Details:**
+
+ .. ts:def:: SubjectResult
+
+ interface SubjectResult {
+ // Subject to use
+ subject: SimpleSubject;
+
+ // Expiration date after which this subject can be reused.
+ expiration: Timestamp;
+ }
+
+.. http:post:: /registration
+
+ Link a registration public key to a transfer public key.
+
+ TODO: How is this endpoint used ?
+
+ **Request:**
+
+ .. ts:def:: RegistrationRequest {
+ // Mapping public key algorithm;
+ link_alg: "ECDSA";
+
+ // Encoded registration key
+ link_key: EddsaPublicKey;
+
+ // Public key algorithm;
+ alg: "ECDSA";
+
+ // Encoded public key
+ key: EddsaPublicKey;
+
+ // Transfer types
+ type: "reserve" | "kyc";
+
+ // Optional expiration date, null or never will use a default expiration
+ expiration?: Timestamp;
+ }
+
+ **Response:**
+
+ :http:statuscode:`200 Ok`:
+ Response is a `RegistrationResult`.
+ :http:statuscode:`400 Bad request`:
+ Input data was invalid.
+
+ **Details:**
+
+ .. ts:def:: RegistrationResult
+
+ interface RegistrationResult {
+ // Expiration date after which this mapping will expired.
+ expiration: Timestamp;
+ }
Test Plan
=========