098-token-fountains.rst (10795B)
1 .. _dd-98: 2 3 DD 98: Token Fountains for Promotions 4 ##################################### 5 6 :Design status: Draft 7 :Implementation status: Not started 8 :DD shepherd: TBD 9 :Historical contributors: Bohdan Potuzhnyi 10 :First published: 2026-07-23 11 :Last substantive change: 2026-07-24 12 :Normative references: ``core/api-merchant.rst`` (upcoming token-fountain API) 13 14 Summary 15 ======= 16 17 This document proposes token fountains as a simple way for institutions to 18 distribute promotional ``discount`` and ``subscription`` tokens. A wallet 19 imports a bearer credential, periodically retrieves the available grants and 20 withdraws blind-signed tokens within limits configured by the institution. 21 22 .. note:: 23 24 This design document proposes an extension for discussion. The protocol 25 version name ``vTokenFountains`` is a placeholder. 26 27 Motivation 28 ========== 29 30 Institutions sometimes want to distribute promotional benefits to a group 31 without creating and paying a separate order for every recipient. For 32 example, a university may give students discount tokens accepted by a campus 33 merchant, or periodically provide subscription tokens for a continuing 34 promotion. 35 36 GNU Taler already defines blind-signed merchant tokens. What is missing is a 37 simple distribution channel through which a wallet can obtain an authorized 38 set of those tokens over time. A **token fountain** is a bearer credential 39 that authorizes such withdrawals. 40 41 The institution can distribute a fountain as a QR code or deep link. The 42 wallet imports the link, learns which token families are available and 43 withdraws blind-signed tokens subject to the configured limits. Blind 44 signatures keep a later use of a token unlinkable from its withdrawal. 45 46 User Flow 47 ========= 48 49 The recipient scans the promotion once. The wallet obtains promotional 50 tokens and offers an applicable token later at checkout. 51 52 .. uml:: images/098/promotions-user-flow.puml 53 :align: center 54 :width: 90% 55 :caption: Joining and using a token-fountain promotion 56 :alt: Scan the promotion, receive promotional tokens in the wallet, use a 57 token at checkout and receive the discount or benefit. 58 59 Requirements 60 ============ 61 62 * Institutions must be able to distribute promotional tokens without 63 creating a separate order for every withdrawal. 64 65 * A fountain must support grants for existing ``discount`` and 66 ``subscription`` token families. 67 68 * Token withdrawals must use blind signatures so that later token use is not 69 linkable to withdrawal. 70 71 * The merchant backend must enforce per-period withdrawal limits atomically. 72 73 * Wallets must be able to import a fountain using a QR code or Taler URI. 74 75 * The bearer secret must not appear in HTTP request URLs after onboarding. 76 77 * Deployments must be able to update or revoke future withdrawals without 78 invalidating tokens that were already issued. 79 80 Proposed Solution 81 ================= 82 83 Terminology 84 ----------- 85 86 In protocol descriptions, this document uses the existing token-family 87 ``kind`` values: 88 89 * A ``discount`` token is useful for a one-time or otherwise limited 90 promotion. It may be consumed without the merchant issuing a replacement. 91 92 * A ``subscription`` token represents a continuing promotion. When an order 93 consumes it, the existing merchant token mechanism can return a replacement 94 token envelope as an output of the selected choice. 95 96 User-facing applications may call either benefit a **pass**. The protocol 97 continues to distinguish them by token-family kind. 98 99 Fountain Credential 100 ------------------- 101 102 A fountain has: 103 104 * a public ``fountain_id`` identifying the fountain within a merchant 105 instance; 106 107 * a random ``fountain_secret`` used as its bearer credential; 108 109 * a polling frequency; and 110 111 * one or more grants describing which token families may be withdrawn and 112 at what rate. 113 114 The onboarding URI is: 115 116 :: 117 118 taler://fountain/$MERCHANT_HOST[/$INSTANCE_PATH]/$FOUNTAIN_ID/$FOUNTAIN_SECRET 119 120 The URI may be displayed as a QR code or opened as a wallet deep link. 121 ``fountain_id`` tells the wallet which fountain it is importing, while 122 ``fountain_secret`` authenticates the import. The wallet must treat the full 123 URI as sensitive. 124 125 After import, the secret must not be placed in subsequent HTTP URLs. It is 126 sent in the ``Authorization`` header when reading fountain information and in 127 the request body when withdrawing tokens, as specified by the merchant API. 128 129 Deployment Models 130 ----------------- 131 132 An institution may use one fountain for an entire campaign. This is simple 133 to distribute, but every recipient shares one bearer credential and the same 134 withdrawal limits. A leaked link cannot be revoked for only one recipient. 135 136 Alternatively, the institution may create one fountain per recipient and 137 distribute each link through its existing student communication system. This 138 allows independent limits and revocation without requiring the merchant 139 backend to store the student's real identity. The fountain description can 140 contain an opaque reference meaningful only to the institution. 141 142 The choice is operational rather than cryptographic. Blind token issuance 143 prevents the merchant from linking a withdrawn token to its later use in 144 either model. 145 146 Grant Model 147 ----------- 148 149 Each :ts:type:`FountainGrant` names a token family and defines: 150 151 * ``tokens_per_period_limit``, the maximum number of tokens signed for one 152 issue-key validity period; 153 154 * ``tokens_per_period_stash``, the number of tokens the wallet should try to 155 keep available for that period; and 156 157 * ``key_window_size``, the number of issue-key slots into the future for 158 which the wallet may prepare tokens. 159 160 The merchant may update grants without replacing the bearer credential. 161 Wallets periodically retrieve the current grants according to ``poll_freq``. 162 Removing a grant prevents future withdrawals from that family, but tokens 163 already withdrawn remain valid according to the token family's issue keys. 164 165 Protocol Flow 166 ------------- 167 168 1. The institution configures suitable ``discount`` or ``subscription`` token 169 families on the merchant backend. 170 171 2. It creates a fountain with grants for those token families. 172 173 3. The backend returns ``fountain_id`` and ``fountain_secret`` once. The 174 institution forms the Taler fountain URI and gives it to the intended 175 recipients. 176 177 4. A wallet imports the URI and calls ``GET /fountain/info`` using the bearer 178 credential. 179 180 5. The wallet prepares blinded token envelopes for any grants it needs to 181 replenish and submits them to ``POST /fountain/withdraw``. 182 183 6. The backend checks the grant, issue-key window and withdrawal limit, then 184 blind-signs the accepted envelopes. 185 186 7. The wallet unblinds and stores the tokens. Their later use follows the 187 existing merchant token protocol. 188 189 Merchant API 190 ------------ 191 192 Private Management Operations 193 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 194 195 The merchant API adds private operations to create, inspect, update, list and 196 delete fountains: 197 198 * ``POST /private/fountains`` 199 200 * ``GET /private/fountains`` 201 202 * ``GET /private/fountains/$FOUNTAIN_ID`` 203 204 * ``PATCH /private/fountains/$FOUNTAIN_ID`` 205 206 * ``DELETE /private/fountains/$FOUNTAIN_ID`` 207 208 Creation returns: 209 210 .. ts:def:: FountainCreateResponse 211 212 interface FountainCreateResponse { 213 // Public identifier included in the wallet onboarding URI. 214 fountain_id: Slug; 215 216 // 256-bit bearer credential, Crockford Base32 encoded. 217 // Returned only at creation; the backend stores only its hash. 218 fountain_secret: string; 219 } 220 221 Wallet Operations 222 ~~~~~~~~~~~~~~~~~ 223 224 ``GET /fountain/info`` returns the current grants, token-family metadata and 225 issue public keys needed to prepare blinded envelopes. 226 227 ``POST /fountain/withdraw`` accepts the bearer credential and blinded 228 envelopes. The request is grouped by token family and issue-key slot so that 229 the backend can enforce each grant's limits. 230 231 Deletion invalidates future requests using the credential. It does not 232 invalidate tokens that the wallet already withdrew. 233 234 Security Considerations 235 ======================= 236 237 Anyone possessing a fountain URI can withdraw under its grants. Distribution 238 systems must therefore protect the URI like any bearer credential. Wallets 239 must avoid exposing it in history, telemetry, screenshots or backups that are 240 not intended to contain secrets. 241 242 The backend stores only a hash of ``fountain_secret``. It must rate-limit 243 failed authentication and enforce withdrawal limits atomically to prevent 244 concurrent requests from exceeding a grant. 245 246 Fountains are not proof of student identity. Eligibility is decided by the 247 institution when it distributes the link. A shared campaign link can be 248 forwarded, while a per-recipient link can only be individually disabled after 249 the institution learns that it was compromised. 250 251 Privacy Considerations 252 ====================== 253 254 Fountain descriptions should contain opaque administrative references rather 255 than names or student identifiers. The merchant backend does not need a 256 recipient's identity to issue blind-signed tokens. 257 258 Network metadata can still correlate a wallet's requests. Deployments that 259 require stronger network-level privacy need an appropriate transport or proxy; 260 blind signatures alone do not hide IP addresses or request timing. 261 262 Drawbacks 263 ========= 264 265 The fountain API adds persistent bearer credentials, polling and quota state 266 to the merchant backend. 267 268 Shared campaign fountains trade administrative simplicity for coarse 269 revocation and shared quotas. Per-recipient fountains provide better control 270 but require the institution to create and distribute many distinct links. 271 272 Test Plan 273 ========= 274 275 * Test creation, inspection, update, listing and deletion of fountains. 276 277 * Test importing a URI containing both ``fountain_id`` and 278 ``fountain_secret``. 279 280 * Test withdrawals for both ``discount`` and ``subscription`` token families. 281 282 * Test atomic enforcement of per-period limits under concurrent requests. 283 284 * Test rejection of unknown grants, unsupported issue-key slots and invalid 285 credentials. 286 287 * Test that deleting a fountain prevents new withdrawals without invalidating 288 tokens already held by a wallet. 289 290 * Test that token use cannot be linked to its fountain withdrawal from the 291 blind-signature transcript. 292 293 Definition of Done 294 ================== 295 296 * The merchant backend implements fountain management, information and 297 withdrawal operations. 298 299 * The onboarding URI includes the merchant location, ``fountain_id`` and 300 ``fountain_secret``. 301 302 * Wallet Core can import a fountain, poll its grants, replenish tokens within 303 the configured limits and stop when the credential is revoked. 304 305 * Merchant applications can create and distribute promotional fountains 306 without supplying recipient identities to the merchant backend. 307 308 * Discount and subscription token families retain their existing consumption 309 and replacement semantics.