taler-docs

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

098-token-fountains.rst (11683B)


      1 .. _dd-98:
      2 
      3 DD 98: Token Fountains for Promotions
      4 #####################################
      5 
      6 :Design status: Draft
      7 :Implementation status: Partial
      8 :DD shepherd: TBD
      9 :Historical contributors: Bohdan Potuzhnyi
     10 :First published: 2026-07-23
     11 :Last substantive change: 2026-09-06
     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 * zero 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 An empty grants array means no grants are currently available, not that the
    166 credential is revoked.  Wallets continue polling for updates.
    167 
    168 Protocol Flow
    169 -------------
    170 
    171 1. The institution configures suitable ``discount`` or ``subscription`` token
    172    families on the merchant backend.
    173 
    174 2. It creates a fountain with grants for those token families.
    175 
    176 3. The backend returns ``fountain_id`` and ``fountain_secret`` once.  The
    177    institution forms the Taler fountain URI and gives it to the intended
    178    recipients.
    179 
    180 4. A wallet imports the URI and calls ``GET /fountain/info`` using the bearer
    181    credential.
    182 
    183 5. The wallet prepares blinded token envelopes for any grants it needs to
    184    replenish and submits them to ``POST /fountain/withdraw``.
    185 
    186 6. The backend checks the grant, issue-key window and withdrawal limit, then
    187    blind-signs the accepted envelopes.
    188 
    189 7. The wallet unblinds and stores the tokens.  Their later use follows the
    190    existing merchant token protocol.
    191 
    192 Merchant API
    193 ------------
    194 
    195 Private Management Operations
    196 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    197 
    198 The merchant API adds private operations to create, inspect, update, list and
    199 delete fountains:
    200 
    201 * ``POST /private/fountains``
    202 
    203 * ``GET /private/fountains``
    204 
    205 * ``GET /private/fountains/$FOUNTAIN_ID``
    206 
    207 * ``PATCH /private/fountains/$FOUNTAIN_ID``
    208 
    209 * ``DELETE /private/fountains/$FOUNTAIN_ID``
    210 
    211 Creation returns:
    212 
    213 .. ts:def:: FountainCreateResponse
    214 
    215   interface FountainCreateResponse {
    216     // Public identifier included in the wallet onboarding URI.
    217     fountain_id: Slug;
    218 
    219     // 256-bit bearer credential, Crockford Base32 encoded.
    220     // Returned only at creation; the backend stores only its hash.
    221     fountain_secret: string;
    222   }
    223 
    224 Wallet Operations
    225 ~~~~~~~~~~~~~~~~~
    226 
    227 ``GET /fountain/info`` returns the current grants, token-family metadata and
    228 issue public keys needed to prepare blinded envelopes.
    229 
    230 ``POST /fountain/withdraw`` accepts the bearer credential and blinded
    231 envelopes.  The request is grouped by token family and issue-key slot so that
    232 the backend can enforce each grant's limits.
    233 
    234 Each grant result returns ``token_sigs`` using the same
    235 :ts:type:`SignedTokenEnvelope` format as the order payment response.
    236 
    237 The backend records completed withdrawals by fountain and canonical request
    238 hash. Retrying with the same ``grants`` array returns the original result
    239 without consuming quota or signing again, even if the grants or key window
    240 have changed. Quota consumption, issued-token records and replay data are
    241 committed in one transaction so a failed operation does not consume quota.
    242 Replay data is retained as a complete response until the latest issue-key
    243 expiry in the withdrawal, even if some entries expire earlier.  Deleting a
    244 fountain drops its replay data at once.
    245 
    246 Deletion invalidates future requests using the credential.  It does not
    247 invalidate tokens that the wallet already withdrew.
    248 
    249 Security Considerations
    250 =======================
    251 
    252 Anyone possessing a fountain URI can withdraw under its grants.  Distribution
    253 systems must therefore protect the URI like any bearer credential.  Wallets
    254 must avoid exposing it in history, telemetry, screenshots or backups that are
    255 not intended to contain secrets.
    256 
    257 The backend stores only a hash of ``fountain_secret``.  Rate limiting of failed
    258 authentication may be handled by the deployment's reverse proxy.  The backend
    259 must enforce withdrawal limits atomically to prevent concurrent requests from
    260 exceeding a grant.
    261 
    262 Fountains are not proof of student identity.  Eligibility is decided by the
    263 institution when it distributes the link.  A shared campaign link can be
    264 forwarded, while a per-recipient link can only be individually disabled after
    265 the institution learns that it was compromised.
    266 
    267 Privacy Considerations
    268 ======================
    269 
    270 Fountain descriptions should contain opaque administrative references rather
    271 than names or student identifiers.  The merchant backend does not need a
    272 recipient's identity to issue blind-signed tokens.
    273 
    274 Network metadata can still correlate a wallet's requests.  Deployments that
    275 require stronger network-level privacy need an appropriate transport or proxy;
    276 blind signatures alone do not hide IP addresses or request timing.
    277 
    278 Drawbacks
    279 =========
    280 
    281 The fountain API adds persistent bearer credentials, polling and quota state
    282 to the merchant backend.
    283 
    284 Shared campaign fountains trade administrative simplicity for coarse
    285 revocation and shared quotas.  Per-recipient fountains provide better control
    286 but require the institution to create and distribute many distinct links.
    287 
    288 Test Plan
    289 =========
    290 
    291 * Test creation, inspection, update, listing and deletion of fountains.
    292 
    293 * Test importing a URI containing both ``fountain_id`` and
    294   ``fountain_secret``.
    295 
    296 * Test withdrawals for both ``discount`` and ``subscription`` token families.
    297 
    298 * Test atomic enforcement of per-period limits under concurrent requests.
    299 
    300 * Test rejection of unknown grants, unsupported issue-key slots and invalid
    301   credentials.
    302 
    303 * Test that deleting a fountain prevents new withdrawals without invalidating
    304   tokens already held by a wallet.
    305 
    306 * Test that token use cannot be linked to its fountain withdrawal from the
    307   blind-signature transcript.
    308 
    309 Definition of Done
    310 ==================
    311 
    312 * The merchant backend implements fountain management, information and
    313   withdrawal operations.
    314 
    315 * The onboarding URI includes the merchant location, ``fountain_id`` and
    316   ``fountain_secret``.
    317 
    318 * Wallet Core can import a fountain, poll its grants, replenish tokens within
    319   the configured limits and stop when the credential is revoked.
    320 
    321 * Merchant applications can create and distribute promotional fountains
    322   without supplying recipient identities to the merchant backend.
    323 
    324 * Discount and subscription token families retain their existing consumption
    325   and replacement semantics.