taler-docs

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

commit 38b8831a3ad9bcfe6d8b83b049c4505efcf9131e
parent 7f1f4d97f1058ba34aed01f49c163b306abb78bd
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Thu, 23 Jul 2026 12:54:10 +0200

api updates

Diffstat:
Mcore/api-merchant.rst | 56++++++++++++++++++--------------------------------------
Mcore/merchant/delete-private-fountains-FOUNTAIN_ID.rst | 4++--
Mcore/merchant/get-fountain-info.rst | 6+++---
Mcore/merchant/get-private-fountains-FOUNTAIN_ID.rst | 2+-
Mcore/merchant/get-private-fountains.rst | 2+-
Mcore/merchant/get-private-otp-devices-DEVICE_ID.rst | 6+++---
Mcore/merchant/patch-private-fountains-FOUNTAIN_ID.rst | 6+++---
Mcore/merchant/post-fountain-withdraw.rst | 2+-
Mcore/merchant/post-orders-ORDER_ID-pay.rst | 2+-
Mcore/merchant/post-private-fountains.rst | 21+++++++++++----------
Mcore/merchant/post-private-otp-devices.rst | 10+++++-----
Mcore/merchant/post-templates-TEMPLATE_ID.rst | 7++++---
Adesign-documents/097-challenge-confirmations.rst | 249+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ddesign-documents/097-sesame.rst | 1087-------------------------------------------------------------------------------
Adesign-documents/098-token-fountains.rst | 288+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign-documents/099-programmable-templates.rst | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rdesign-documents/images/097/Makefile -> design-documents/images/999/Makefile | 0
Rdesign-documents/images/097/sesame-business-lifecycle.puml -> design-documents/images/999/sesame-business-lifecycle.puml | 0
Rdesign-documents/images/097/sesame-business-lifecycle.svg -> design-documents/images/999/sesame-business-lifecycle.svg | 0
Rdesign-documents/images/097/sesame-business-overview.puml -> design-documents/images/999/sesame-business-overview.puml | 0
Rdesign-documents/images/097/sesame-business-overview.svg -> design-documents/images/999/sesame-business-overview.svg | 0
Rdesign-documents/images/097/sesame-business-signup.puml -> design-documents/images/999/sesame-business-signup.puml | 0
Rdesign-documents/images/097/sesame-business-signup.svg -> design-documents/images/999/sesame-business-signup.svg | 0
Rdesign-documents/images/097/sesame-data-model.puml -> design-documents/images/999/sesame-data-model.puml | 0
Rdesign-documents/images/097/sesame-data-model.svg -> design-documents/images/999/sesame-data-model.svg | 0
Rdesign-documents/images/097/sesame-deployment.puml -> design-documents/images/999/sesame-deployment.puml | 0
Rdesign-documents/images/097/sesame-deployment.svg -> design-documents/images/999/sesame-deployment.svg | 0
Rdesign-documents/images/097/sesame-door-access.puml -> design-documents/images/999/sesame-door-access.puml | 0
Rdesign-documents/images/097/sesame-door-access.svg -> design-documents/images/999/sesame-door-access.svg | 0
Rdesign-documents/images/097/sesame-onboarding.puml -> design-documents/images/999/sesame-onboarding.puml | 0
Rdesign-documents/images/097/sesame-onboarding.svg -> design-documents/images/999/sesame-onboarding.svg | 0
Rdesign-documents/images/097/sesame-withdraw.puml -> design-documents/images/999/sesame-withdraw.puml | 0
Rdesign-documents/images/097/sesame-withdraw.svg -> design-documents/images/999/sesame-withdraw.svg | 0
Rdesign-documents/images/097/taler-style.iuml -> design-documents/images/999/taler-style.iuml | 0
Mdesign-documents/index.rst | 3+++
35 files changed, 777 insertions(+), 1158 deletions(-)

diff --git a/core/api-merchant.rst b/core/api-merchant.rst @@ -63,10 +63,12 @@ The currently implemented protocol version is **v32**. **Upcoming versions:** * ``vTAXES``: adds features to manage taxes -* ``vOpenSesame``: adds fountains for continuous token issuance, - challenge-signature OTP algorithms, token choices in fixed-order - templates and same-key token re-issue for anonymous access control - (see :doc:`DD 97 </design-documents/097-project-sesame>`) +* ``vChallengeConfirmation``: adds challenge-signature OTP algorithms for + payment confirmations verified by offline appliances and tags + (see :doc:`DD 97 </design-documents/097-challenge-confirmations>`) +* ``vTokenFountains``: adds fountains for distributing promotional + merchant tokens + (see :doc:`DD 98 </design-documents/098-token-fountains>`) * ``vMixedPayments``: adds the additive ``amount_external`` field for orders settled partly outside of Taler, the ``/private/orders/$ORDER_ID/collect`` endpoint for completing zero-Taler orders without a wallet, the @@ -661,6 +663,8 @@ if they were expected. .. include:: merchant/delete-private-transfers-TID.rst +.. _merchant-otp-device-api: + ----------- OTP Devices ----------- @@ -996,22 +1000,20 @@ Deleting token families .. _merchant-fountain-api: -------------------------------------- -Fountains: Continuous Token Issuance -------------------------------------- +----------------------------------------- +Fountains: Promotional Token Distribution +----------------------------------------- -A fountain is a per-user bearer credential that entitles a wallet to -withdraw blind-signed tokens for a set of token families, without -creating an order per withdrawal. Fountains are the issuance channel -for anonymous access control -(see :doc:`DD 97 </design-documents/097-project-sesame>`): revoking -a user means deleting their fountain (or patching away a grant), -after which the affected tokens expire with their issue keys. +A fountain is a bearer credential that entitles a wallet to withdraw +blind-signed promotional tokens for a set of token families without +creating an order per withdrawal. An institution can distribute a fountain +to one recipient or share it for a campaign +(see :doc:`DD 98 </design-documents/098-token-fountains>`). Wallets receive the credential as a -``taler://fountain/$MERCHANT_HOST[/$INSTANCE_PATH]/$FOUNTAIN_SECRET`` +``taler://fountain/$MERCHANT_HOST[/$INSTANCE_PATH]/$FOUNTAIN_ID/$FOUNTAIN_SECRET`` URI. All fountain endpoints are available since protocol -**vOpenSesame**. +**vTokenFountains**. Managing fountains ^^^^^^^^^^^^^^^^^^ @@ -1136,13 +1138,6 @@ open or deferred. // This parameter is optional. amount?: Amount; - // Contract choices for orders created from this template. - // If present, instantiated orders are version 1 contracts - // with exactly these choices (token inputs/outputs). - // Must not be combined with ``amount``. - // Since protocol **vOpenSesame**. - choices?: OrderChoice[]; - } .. ts:def:: TemplateContractInventoryCart @@ -1276,16 +1271,6 @@ Template Choices // the validity is supposed to be "now" (time of order creation). valid_at?: Timestamp; - // If true, the output tokens are blind-signed with the same - // issue key (same ``h_issue``) as the input token of the same - // token family consumed by the selected choice, instead of the - // key of the current time slot. The choice MUST then also - // contain an `OrderInputToken` for the same - // ``token_family_slug``, and this field must not be combined - // with ``valid_at``. - // Since protocol **vOpenSesame**. - reissue_with_input_key?: boolean; - } .. ts:def:: OrderOutputTaxReceipt @@ -1876,11 +1861,6 @@ and is thus not yet buyer-specific. // Index of the public key for this output token // in the `ContractTokenFamily` ``keys`` array. - // If the corresponding `OrderOutputToken` set - // ``reissue_with_input_key``, this designates the key of the - // current time slot and is overridden at pay time by the key - // matching the ``h_issue`` of the presented input token. - // Since protocol **vOpenSesame**. key_index: Integer; } diff --git a/core/merchant/delete-private-fountains-FOUNTAIN_ID.rst b/core/merchant/delete-private-fountains-FOUNTAIN_ID.rst @@ -1,10 +1,10 @@ .. http:delete:: [/instances/$INSTANCE]/private/fountains/$FOUNTAIN_ID - Deletes a fountain: full revocation of the associated user. + Deletes a fountain and disables future withdrawals with its credential. Subsequent wallet requests with the fountain secret fail with ``401 Unauthorized``; tokens already withdrawn remain valid until their issue key expires. - This endpoint is available since protocol **vOpenSesame**. + This endpoint is available since protocol **vTokenFountains**. **Required permission:** ``fountains-write`` diff --git a/core/merchant/get-fountain-info.rst b/core/merchant/get-fountain-info.rst @@ -3,9 +3,9 @@ Used by wallets to learn which token families they can withdraw from and to obtain the token family metadata and issue public keys needed to prepare blinded envelopes. Wallets re-poll this - endpoint at the advertised ``poll_freq`` to learn about grant - changes (added or removed groups, partial revocation). - This endpoint is available since protocol **vOpenSesame**. + endpoint at the advertised ``poll_freq`` to learn about changed + promotional grants. + This endpoint is available since protocol **vTokenFountains**. The fountain secret authenticates the request via the ``Authorization: Bearer $FOUNTAIN_SECRET`` header; it is diff --git a/core/merchant/get-private-fountains-FOUNTAIN_ID.rst b/core/merchant/get-private-fountains-FOUNTAIN_ID.rst @@ -2,7 +2,7 @@ Returns detailed information about a specific fountain. The fountain secret is never returned. - This endpoint is available since protocol **vOpenSesame**. + This endpoint is available since protocol **vTokenFountains**. **Required permission:** ``fountains-read`` diff --git a/core/merchant/get-private-fountains.rst b/core/merchant/get-private-fountains.rst @@ -1,7 +1,7 @@ .. http:get:: [/instances/$INSTANCE]/private/fountains Lists all fountains of the instance. - This endpoint is available since protocol **vOpenSesame**. + This endpoint is available since protocol **vTokenFountains**. **Required permission:** ``fountains-read`` diff --git a/core/merchant/get-private-otp-devices-DEVICE_ID.rst b/core/merchant/get-private-otp-devices-DEVICE_ID.rst @@ -46,14 +46,14 @@ // 1: TOTP without price // 2: TOTP with price // 3: ECDSA signature over the order's challenge - // (since protocol **vOpenSesame**) + // (since protocol **vChallengeConfirmation**) // 4: EdDSA signature over the order's challenge - // (since protocol **vOpenSesame**) + // (since protocol **vChallengeConfirmation**) otp_algorithm: Integer; // Public key of the device key pair, only present for // challenge-signature algorithms (3 and 4). - // Since protocol **vOpenSesame**. + // Since protocol **vChallengeConfirmation**. otp_device_pub?: string; // Counter for counter-based OTP devices. diff --git a/core/merchant/patch-private-fountains-FOUNTAIN_ID.rst b/core/merchant/patch-private-fountains-FOUNTAIN_ID.rst @@ -1,10 +1,10 @@ .. http:patch:: [/instances/$INSTANCE]/private/fountains/$FOUNTAIN_ID - Modifies a fountain. Used for group membership changes and - partial revocation: replacing the grants does not invalidate + Modifies a fountain. Used for campaign or eligibility changes: + replacing the grants does not invalidate the fountain secret; wallets learn about the changed grants on their next ``GET /fountain/info`` poll. - This endpoint is available since protocol **vOpenSesame**. + This endpoint is available since protocol **vTokenFountains**. **Required permission:** ``fountains-write`` diff --git a/core/merchant/post-fountain-withdraw.rst b/core/merchant/post-fountain-withdraw.rst @@ -5,7 +5,7 @@ structured per grant: each entry addresses one granted token family and one key slot, and carries the envelopes to sign against that grant's limits. - This endpoint is available since protocol **vOpenSesame**. + This endpoint is available since protocol **vTokenFountains**. The fountain secret travels in the request body, never in the URL. diff --git a/core/merchant/post-orders-ORDER_ID-pay.rst b/core/merchant/post-orders-ORDER_ID-pay.rst @@ -201,7 +201,7 @@ // ("ECDSA_CHALLENGE" or "EDDSA_CHALLENGE"), contains the // Crockford Base32 encoded signature over the hash of the // order's challenge, made with the device private key. - // Since protocol **vOpenSesame**. + // Since protocol **vChallengeConfirmation**. pos_confirmation?: string; // Signed tokens. Returned in the same order as the diff --git a/core/merchant/post-private-fountains.rst b/core/merchant/post-private-fountains.rst @@ -1,9 +1,10 @@ .. http:post:: [/instances/$INSTANCE]/private/fountains - This is used to create a fountain: a per-user bearer credential - that entitles a wallet to withdraw blind-signed tokens for a set - of token families (see :doc:`DD 97 </design-documents/097-project-sesame>`). - This endpoint is available since protocol **vOpenSesame**. + This is used to create a fountain: a bearer credential that + entitles a wallet to withdraw blind-signed promotional tokens for + a set of token families + (see :doc:`DD 98 </design-documents/098-token-fountains>`). + This endpoint is available since protocol **vTokenFountains**. **Required permission:** ``fountains-write`` @@ -31,8 +32,8 @@ interface FountainCreateRequest { - // Human-readable description; typically an opaque user - // identifier of the provisioning access management system. + // Human-readable description; preferably an opaque campaign + // or recipient identifier from the institution's system. // The merchant backend does not need -- and should not // receive -- the user's real identity. description: string; @@ -63,8 +64,8 @@ tokens_per_period_stash: Integer; // Number of key slots ahead of the current one the wallet - // may withdraw tokens for. Bounds prefetching and thereby - // the revocation latency. + // may withdraw tokens for. Bounds prefetching and the time + // for which already-withdrawn promotional tokens remain usable. key_window_size: Integer; } @@ -72,8 +73,8 @@ interface FountainCreateResponse { - // Identifier of the fountain, used for subsequent - // CRUD operations. + // Identifier of the fountain, used for subsequent CRUD + // operations and included in the wallet onboarding URI. fountain_id: string; // 256-bit bearer credential for the wallet, Crockford diff --git a/core/merchant/post-private-otp-devices.rst b/core/merchant/post-private-otp-devices.rst @@ -17,7 +17,7 @@ ``otp_algorithm`` ("ECDSA_CHALLENGE" or "EDDSA_CHALLENGE"): the backend generated the device key pair and returns a `OtpDeviceCreateResponse` with the public key. - Since protocol **vOpenSesame**. + Since protocol **vChallengeConfirmation**. :http:statuscode:`400 Bad Request`: The request body is malformed. :http:statuscode:`404 Not found`: @@ -53,7 +53,7 @@ // Taler base32-crockford encoding. // Instead it uses the RFC 3548 encoding to // be compatible with the TOTP standard. - // Since protocol **vOpenSesame** this field is optional: + // Since protocol **vChallengeConfirmation** this field is optional: // it MUST be omitted for the challenge-signature algorithms // (3 and 4), where the backend generates the key pair itself // and never exposes the private key. @@ -64,9 +64,9 @@ // "TOTP_WITHOUT_PRICE" or 1: Without amounts (typical OTP device) // "TOTP_WITH_PRICE" or 2: With amounts (special-purpose OTP device) // "ECDSA_CHALLENGE" or 3: ECDSA signature over the order's - // challenge, since protocol **vOpenSesame**. + // challenge, since protocol **vChallengeConfirmation**. // "EDDSA_CHALLENGE" or 4: EdDSA signature over the order's - // challenge, since protocol **vOpenSesame**. + // challenge, since protocol **vChallengeConfirmation**. // The "string" variants are supported @since protocol **v7**. otp_algorithm: Integer | string; @@ -83,6 +83,6 @@ // For "ECDSA_CHALLENGE" a compressed NIST P-256 point, for // "EDDSA_CHALLENGE" an Ed25519 public key. // Crockford Base32 encoded. - // Since protocol **vOpenSesame**. + // Since protocol **vChallengeConfirmation**. otp_device_pub: string; } diff --git a/core/merchant/post-templates-TEMPLATE_ID.rst b/core/merchant/post-templates-TEMPLATE_ID.rst @@ -67,14 +67,15 @@ // Since protocol **v25**. tip?: Amount; - // Challenge obtained out-of-band (e.g. read from an offline - // lock over NFC): 32 bytes, Crockford Base32 encoded. Stored + // Challenge obtained from an offline verifier, such as an + // appliance or electronic tag: 32 bytes, Crockford Base32 + // encoded. Stored // with the order and signed by the OTP device associated with // the template when the POS confirmation is computed. Only // meaningful -- and then mandatory -- if the template's OTP // device uses a challenge-signature algorithm ("ECDSA_CHALLENGE" // or "EDDSA_CHALLENGE"); rejected otherwise. - // Since protocol **vOpenSesame**. + // Since protocol **vChallengeConfirmation**. challenge?: string; } diff --git a/design-documents/097-challenge-confirmations.rst b/design-documents/097-challenge-confirmations.rst @@ -0,0 +1,249 @@ +.. _dd-97: + +DD 97: Challenge-Signature Payment Confirmations +################################################ + +Summary +======= + +This document proposes challenge-signature payment confirmations for +unattended devices that cannot contact the merchant backend themselves. +The device generates a fresh challenge, the merchant backend signs it after +payment, and the device verifies the signature before performing its local +action. + +.. note:: + + This design document proposes an extension for discussion. The protocol + version name ``vChallengeConfirmation`` is a placeholder. + +Motivation +========== + +Some unattended devices must verify that a Taler payment succeeded without +being online themselves. Existing point-of-sale confirmations use a +time-based one-time password (TOTP). A TOTP proves knowledge of a shared +secret, but it does not bind the confirmation to a challenge freshly chosen +by the device. + +Two examples motivate a challenge-bound confirmation: + +* A coffee machine generates a fresh challenge and displays or transmits it + to the customer's wallet. After payment, the machine dispenses the + selected product only if it receives a valid signature for that challenge. + +* An electronic farmer tag attached to an unattended farm stand or collection + point generates a fresh challenge before a local action. It can verify the + payment confirmation even when the tag itself has no network connection. + +The merchant backend remains online and processes the payment normally. The +offline device only needs a public key and a source of unpredictable +challenges. + +Requirements +============ + +* The confirmation must be bound to a fresh challenge generated by the + offline device. + +* The merchant backend must generate and retain the signing private key. + +* The offline device must only require the corresponding public key. + +* The wallet must be able to transport the challenge and confirmation without + interpreting device-specific behavior. + +* Replaying an accepted confirmation must not authorize another local action. + +* Existing TOTP confirmation algorithms must remain unchanged. + +Proposed Solution +================= + +The merchant backend's OTP algorithm enumeration is extended with two +challenge-signature algorithms: + +* ``ECDSA_CHALLENGE`` (numeric value 3) uses ECDSA with NIST P-256. + +* ``EDDSA_CHALLENGE`` (numeric value 4) uses EdDSA with Ed25519. + +When a challenge-signature OTP device is created, the merchant backend +generates the signing key pair. It stores the private key and returns the +public key to the merchant application. The public key is then configured +in the offline verifier. The private key is never exposed through the API. + +For these algorithms, the client supplies a fresh 32-byte challenge while +instantiating a template. The challenge is encoded using Crockford Base32 +and becomes part of the order. After successful payment, the backend signs +the challenge with the key associated with the template's OTP device. The +signature is returned in ``PaymentResponse.pos_confirmation``. + +The wallet only transports the challenge to the merchant and the resulting +signature back to the device. It does not possess the signing key and does +not decide whether the local action is allowed. + +Payment Flow +============ + +1. The offline device generates a cryptographically unpredictable challenge. + +2. The wallet obtains the challenge from the device and includes it as + ``challenge`` when instantiating the merchant template. + +3. The merchant backend creates the order and the wallet pays it using the + normal Taler payment protocol. + +4. After accepting the payment, the backend signs the order's challenge and + returns the encoded signature as ``pos_confirmation``. + +5. The wallet relays the confirmation to the device. + +6. The device verifies the signature using its configured public key and + performs the local action only after successful verification. + +The device must reject a challenge after it has accepted a confirmation for +that challenge. This prevents replaying a previous payment confirmation to +obtain another product or action. + +Protocol Changes +================ + +OTP Device Creation +------------------- + +The existing :ref:`merchant OTP device API <merchant-otp-device-api>` accepts +values 3 and 4 for ``OtpDeviceAddDetails.otp_algorithm`` when creating a +device. For these values, ``OtpDeviceAddDetails.otp_key`` must be omitted +because the backend generates the key pair. + +Successful creation returns: + +.. ts:def:: OtpDeviceCreateResponse + + interface OtpDeviceCreateResponse { + // Public key generated for the challenge-signature device. + // For ECDSA this is a compressed NIST P-256 point. + // For EdDSA this is an Ed25519 public key. + // Crockford Base32 encoded. + otp_device_pub: string; + } + +The response for reading an OTP device exposes the same public key as the +optional ``OtpDeviceDetails.otp_device_pub`` field. + +Template Instantiation +---------------------- + +The common template request gains: + +.. ts:def:: UsingTemplateCommonRequestChallenge + + interface UsingTemplateCommonRequestChallenge { + // Fresh challenge generated by the offline verifier. + // Exactly 32 bytes, Crockford Base32 encoded. + challenge?: string; + } + +The field is required when the template references an OTP device using +``ECDSA_CHALLENGE`` or ``EDDSA_CHALLENGE`` and must otherwise be rejected. + +Payment Response +---------------- + +For a challenge-signature OTP device, the existing +``PaymentResponse.pos_confirmation`` field contains the Crockford Base32 +encoded signature over the order's challenge. + +Error Handling +============== + +Template instantiation fails if: + +* the challenge is absent for a challenge-signature OTP device; + +* the challenge is present for an OTP algorithm that does not use it; + +* the challenge is not valid Crockford Base32; or + +* the decoded challenge does not have the required length. + +Payment processing fails closed if the backend cannot load the device's +private key or cannot produce the signature. It must not return a successful +confirmation with an empty or malformed ``pos_confirmation``. + +Security Considerations +======================= + +Challenges must be generated with a cryptographically secure random number +generator. Predictable challenges would permit an attacker to prepare a +confirmation before the device requests it. + +An offline verifier must remember challenges that have already been accepted, +at least for as long as replaying them could cause a second local action. A +device with volatile state should generate challenges from a sufficiently +large random space and invalidate the current challenge immediately after +successful verification. + +The verifier's configured public key is security-sensitive configuration. +Replacing it changes which merchant backend can authorize the device. + +The signature construction must use domain separation defined by the merchant +protocol. It should bind the signature to the challenge and the relevant +device or order context so that signatures cannot be reused across protocol +purposes. + +Privacy Considerations +====================== + +The challenge is random and carries no customer identity. The offline device +does not learn the customer's Taler payment credentials. The wallet learns +that the payment is associated with a local challenge, which is inherent in +relaying the confirmation. + +Drawbacks +========= + +The device must implement public-key signature verification and secure replay +handling, which is more demanding than displaying or checking a short TOTP. + +If the payment succeeds but the wallet cannot relay the confirmation, the +merchant has received the payment while the local action has not happened. +Applications need a recovery or refund procedure appropriate to the product. + +Test Plan +========= + +* Test OTP device creation for both challenge-signature algorithms and verify + that the private key is never returned. + +* Test rejection of supplied ``otp_key`` values for challenge-signature + devices. + +* Test template instantiation with missing, malformed, short and unexpected + challenges. + +* Test that a successful payment returns a signature accepted by the + corresponding public key and rejected by another key. + +* Test that modifying the challenge invalidates the confirmation. + +* Test coffee-machine and farmer-tag integrations with repeated and replayed + challenges. + +Definition of Done +================== + +* The merchant backend supports OTP algorithms 3 and 4, generates their key + pairs and exposes only their public keys. + +* Template instantiation validates and stores a 32-byte challenge for these + algorithms. + +* Successful payment returns a challenge-bound signature in + ``pos_confirmation``. + +* Wallet Core can transport the challenge and relay the confirmation without + interpreting device-specific behavior. + +* At least one offline verifier implementation validates confirmations and + rejects replayed challenges. diff --git a/design-documents/097-sesame.rst b/design-documents/097-sesame.rst @@ -1,1087 +0,0 @@ -DD 97: Project Sesame — Anonymous Access Control -################################################ - -*Status*: draft (2026-07-17) - -Summary -======= - -This design document describes how the Taler eco-system can be used -to enable anonymous access to different physical access control systems. -A wallet obtains blind-signed -tokens (one per *access group*) from the merchant backend via a new -*fountain* credential. To open a door, the wallet reads a challenge -from the (offline) lock, instantiates an order from the door's -template, pays with its group token (consuming it and receiving a -replacement via blind signature), and relays the resulting signed -confirmation (``pos_confirmation``) back to the lock, which verifies -it without any network connection. The merchant backend learns *that* -someone from a given access group opened a given door, but not *who*. - -The design reuses existing merchant features — token families, -templates, OTP devices, v1 contract choices and blind signatures — -and adds one new concept (the fountain) plus a small number of -protocol extensions. All protocol changes proposed here are -annotated with "Since protocol **vOpenSesame**"; the placeholder is -to be replaced with the concrete merchant protocol version number -once the changes are implemented. - -Motivation -========== - -Conventional electronic access control systems identify the user on -every door opening, producing detailed movement profiles. At the -same time, purely offline mechanical or RFID solutions lack -revocability, usage statistics and flexible group management. - -The Taler merchant backend already contains almost all building -blocks for a privacy-preserving alternative: - -* *Token families* provide blind-signed, single-use, time-sliced - tokens (see `DD 46 <046-mumimo-contracts.html>`__ and - `DD 68 <068-tokens-roadmap.html>`__). -* *Templates* allow order creation without merchant-side frontend - logic. -* *OTP devices* allow generation of confirmation - (``pos_confirmation``) for successful payments, which an offline - device (door) can verify. - -Beyond pure access control, the same infrastructure supports *paying* -to open a door (parking garage, laundry room): a template choice with -a non-zero amount is an ordinary Taler payment. - -Requirements -============ - -* **Good UX**: the user taps the door with a phone; the door opens - within well under a second. -* **Anonymity within the group**: the merchant backend learns - "someone from group X opened door Y at time T", but cannot identify - the individual user, nor link accesses across doors to the same - user. -* **Offline locks**: the lock verifies a confirmation without any - network connection. Only the wallet and the merchant backend need - Internet connectivity. -* **Per-group tokens**: one blind-signed token per access group, not - per door. A user in two groups holds two tokens regardless of the - number of doors. -* **Revocability**: revocation latency is configurable per access - group (e.g. 15 minutes for a server room, 30 days for a co-working - space). Revocation means: stop issuing tokens. -* **Usage statistics**: every door opening is recorded at the - merchant backend (door, group, timestamp) for maintenance and - reporting, without identifying users. -* **Payment support**: doors may additionally (or exclusively) be - opened against payment in digital cash. -* **IAM decoupling**: identity management stays outside the merchant - backend. Any identity/access management system can provision - fountains via the private API. -* **Minimal Taler changes**: reuse token families, templates, OTP - devices and the pay protocol; add only the fountain and small - extensions to existing endpoints. - -Proposed Solution -================= - -Business view -------------- - -This section is the non-technical summary; the protocol description -starts at `Architecture`_. - -Employees or members carry a digital key in the Taler Wallet app on -their phone. Opening a door means tapping the phone on the reader: -the phone contacts the Taler service over the Internet -and shows the door a signed permit. The doors themselves need no -network connection and no per-user configuration. - -.. figure:: images/097/sesame-business-overview.svg - :align: center - :alt: Opening a door, business view: tap the phone, the phone - trades an anonymous group pass for a signed permit, the door - checks the permit offline and opens. - - - Opening a door (business view) - -The key is anonymous within a group: access management (the door -owner) sees that some member of, say, the staff group opened a door, -which is enough for maintenance planning and billing, but cannot -tell which member it was and cannot build movement profiles. - -Getting the key is a one-time sign-up with the company account. -Access management registers a digital key for the person's access -groups and the phone receives it as a QR code or link. From then on the wallet -app picks up its passes by itself in the background. The sign-up is -personal, but the passes obtained with the key are not: the service -cannot link a pass to the sign-up it came from, which is what makes -door openings anonymous. - -.. figure:: images/097/sesame-business-signup.svg - :align: center - :alt: Sign-up, business view: sign in with the company account, - access management registers a digital key, the phone scans a - QR code and from then on picks up anonymous passes - automatically. - - - Sign-up and pass pick-up (business view) - -Access is managed per group, not per door. Group membership comes -from the existing IAM or HR system. When a person leaves, access -management stops their key with one API call; passes already on the -phone expire on their own within a period configured per area -(15 minutes for a server room, days for a co-working space). - -.. figure:: images/097/sesame-business-lifecycle.svg - :align: center - :alt: Access lifecycle, business view: person joins, phone receives - the digital key, doors open during membership, key is stopped - on leaving and remaining passes expire on their own. - - - Access lifecycle (business view) - -The same setup can also charge for opening a door (parking garage, -laundry room): a door can accept payment in digital cash instead of, -or in addition to, a group pass. - -Architecture ------------- - -.. list-table:: - :header-rows: 1 - :widths: 20 65 15 - - * - Component - - Role - - Network - * - IAM / identity provider (e.g. OIDC) - - Authenticates users towards the access control operator. - - online - * - Access control system (ACS) - - Manages doors, access groups and user→group mappings; provisions - token families, templates, OTP devices and fountains at the - merchant backend via the private API. - - online - * - Merchant backend - - Issues blind-signed tokens, verifies token payments, computes - ``pos_confirmation`` for locks. - - online - * - Taler Wallet - - Holds fountain secret and tokens; bridges lock and merchant - backend. - - online - * - Lock - - Generates challenges, verifies ``pos_confirmation``, opens the - door. - - offline - -.. figure:: images/097/sesame-deployment.svg - :align: center - :alt: Deployment view: wallet bridges the offline lock and the - merchant backend; the ACS provisions the merchant backend and - configures locks out-of-band. - - - Deployment view - -Cryptographic building blocks ------------------------------ - -.. list-table:: - :header-rows: 1 - :widths: 30 40 30 - - * - Primitive - - Purpose - - Instantiation - * - Blind signature - - Token issuance (withdrawal and re-issue) - - existing Taler token family code (RSA/CS) - * - Signature over challenge (``pos_confirmation``) - - Merchant confirmation to the lock - - ECDSA (P-256) or EdDSA, **new** - * - TOTP - - Door challenge / replay prevention - - existing OTP devices - -Data model mapping ------------------- - -.. figure:: images/097/sesame-data-model.svg - :align: center - :alt: Data model mapping: access group to token family (1:1), door - to template plus OTP device (1:1:1), user to fountain (1:1); - template choices and fountain grants reference token - families. - - - Data model mapping - -A door with a distinct access policy needs its own door id (its own -OTP device and template). Doors MAY share one door id if their -access policy is identical, at the cost of losing per-door usage -statistics. - -Access groups as token families -------------------------------- - -Each access group is an ordinary token family created via -:http:post:`[/instances/$INSTANCE]/private/tokenfamilies` with -``kind: "subscription"`` — no new token family kind is introduced. -The merchant backend already auto-generates one issue key per -``validity_granularity`` time slot; the hash of the issue key -(``h_issue``) thus identifies group and time slot. Different groups -can use different ``duration`` values: - -* server room: 15 minutes, -* office building: 1 day, -* co-working space: 30 days. - -The token ``duration`` bounds the revocation latency (see -`Revocation`_). - -Doors as OTP devices and templates ----------------------------------- - -Each door is registered as an OTP device -(:http:post:`[/instances/$INSTANCE]/private/otp-devices`) and gets a -template (:http:post:`[/instances/$INSTANCE]/private/templates`) -whose ``otp_id`` references the device. The ``otp_algorithm`` -determines how the merchant backend computes the -``pos_confirmation`` that the lock verifies: - -===== ========================= ====================================================== -Value Name ``pos_confirmation`` -===== ========================= ====================================================== -0 ``NONE`` none (existing) -1 ``TOTP_WITHOUT_PRICE`` TOTP code, shared-secret verification (existing) -2 ``TOTP_WITH_PRICE`` TOTP code including price (existing) -3 ``ECDSA_CHALLENGE`` ECDSA signature over the order's challenge - (**new**, since protocol **vOpenSesame**) -4 ``EDDSA_CHALLENGE`` EdDSA signature over the order's challenge - (**new**, since protocol **vOpenSesame**) -===== ========================= ====================================================== - -For the challenge-signature algorithms (3 and 4) the key pair is -generated *by the merchant backend*; the private key is never -exposed. The public key is returned on device creation and the ACS -configures it into the lock. For TOTP algorithms the lock shares the -``otp_key`` secret as today. - -The lock itself is configured out-of-band (not stored at the -merchant) with: door id, challenge mode (TOTP or nonce), and either -the TOTP secret or the device public key. Challenge handling at the -lock: - -========= ==================== ==================================================== -Mode Lock generates Lock verifies -========= ==================== ==================================================== -TOTP nothing recomputes TOTP code with its own clock -Nonce random 32 bytes signature verification of ``pos_confirmation`` - over its own nonce with the device public key -========= ==================== ==================================================== - -The door template ties the device to the access groups allowed to -open it. Since protocol **vOpenSesame**, fixed-order templates can -carry v1 contract ``choices`` (see `Extended: templates with token -choices`_). Example (informal): - -.. code-block:: none - - TemplateCreateRequest { - template_id: "door-201", - template_description: "Office building room 201", - otp_id: "door-201", - template_contract: { - choices: [ - { // staff members: consume one token, get a replacement - amount: "KUDOS:0", - inputs: [ { type: "token", token_family_slug: "staff" } ], - outputs: [ { type: "token", token_family_slug: "staff", - reissue_with_input_key: true } ] - }, - { // facilities team: different group, same door - amount: "KUDOS:0", - inputs: [ { type: "token", token_family_slug: "facilities" } ], - outputs: [ { type: "token", token_family_slug: "facilities", - reissue_with_input_key: true } ] - }, - { // visitors: pay to open - amount: "EUR:5" - } - ] - } - } - -Multiple choices mean multiple access groups (or payment options) for -the same door; the wallet picks the choice matching a token family it -holds. Each token choice consumes one token (input) and re-issues -one (output) — the single-use replacement mechanism. A one-time -access pass (visitor) is simply a choice *without* a token output, -paired with a ``discount``-kind token family; no separate mechanism -is needed. - -Same-key constraint -~~~~~~~~~~~~~~~~~~~ - -When a choice consumes and re-issues a token of the *same* family, -the replacement token must be signed with the *same* issue key as the -consumed token — not with the key of the current time slot. This is -what the new ``reissue_with_input_key`` flag on `OrderOutputToken` -expresses. - -Without this constraint, a revoked user could keep obtaining -replacement tokens under ever-newer keys through door accesses alone, -bypassing the fountain (where revocation is enforced) indefinitely. -With the constraint, the replacement expires together with the -consumed token, forcing every wallet back to the fountain once per -key slot. - -Users as fountains ------------------- - -A *fountain* is a per-user bearer credential at the merchant backend -that entitles a wallet to withdraw blind-signed tokens for a set of -token families. It is the only new concept introduced by this -design. - -* One fountain per user, created by the ACS at user onboarding. -* Each fountain holds a 256-bit ``fountain_secret`` (given to the - user's wallet) and a list of *grants*, one per access group. -* Adding/removing group memberships: ``PATCH`` the fountain's grants; - the secret stays valid. -* Revoking the user entirely: ``DELETE`` the fountain. - -The wallet receives the credential as a new URI (an extension of -`LSD 0006 <https://lsd.gnunet.org/lsd0006/>`__ is required): - -.. code-block:: none - - taler://fountain/$MERCHANT_HOST[/$INSTANCE_PATH]/$FOUNTAIN_SECRET - -Each grant carries three parameters: - -* ``tokens_per_period_limit``: how many tokens the fountain will - blind-sign per key validity period (rate limit; relevant for - non-subscription families such as one-time passes). -* ``tokens_per_period_stash``: how many tokens the wallet *should* - hold per period (client-side target). -* ``key_window_size``: how many key slots ahead of the current one - the wallet may withdraw for. With window size 3 and a duration of - one day, the wallet can prefetch tokens for today, tomorrow and the - day after, reducing fountain round trips — at the cost of - revocation latency (see `Revocation`_). - -Protocol flows --------------- - -System setup -~~~~~~~~~~~~ - -Performed by the ACS through the private merchant API: - -1. For each access group: - :http:post:`[/instances/$INSTANCE]/private/tokenfamilies` - (``kind: "subscription"``, per-group ``duration`` and - ``validity_granularity``). -2. For each door: - :http:post:`[/instances/$INSTANCE]/private/otp-devices` with - ``otp_algorithm`` 1 (TOTP lock) or 3/4 (challenge-signature lock). - For 3/4 the response contains the generated device public key. -3. For each door: - :http:post:`[/instances/$INSTANCE]/private/templates` with - ``otp_id`` = device id and one contract choice per authorized - group. -4. The ACS configures the physical lock with door id, challenge mode - and the TOTP secret or device public key. - -User onboarding -~~~~~~~~~~~~~~~ - -1. The user authenticates towards the ACS via the IAM (e.g. OIDC). -2. The ACS resolves the user's roles to access groups. -3. The ACS creates the fountain: - ``POST /private/fountains`` with one grant per group. -4. The ACS hands the resulting ``taler://fountain/...`` URI to the - user's wallet (QR code, deep link, ...). -5. The wallet stores the fountain URI, fetches the fountain info and - withdraws its initial tokens (next section). - -.. figure:: images/097/sesame-onboarding.svg - :align: center - :alt: Onboarding sequence: IAM login, ACS resolves groups, creates - the fountain at the merchant backend, hands the fountain URI - to the wallet. - - - User onboarding - -Token withdrawal via fountain -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The fountain secret is never placed in a URL (it would leak into -server logs and proxies): it travels in an ``Authorization: Bearer`` -header for GET and in the request body for POST. - -.. figure:: images/097/sesame-withdraw.svg - :align: center - :alt: Withdrawal sequence: wallet fetches grants and issue keys via - GET /fountain/info, then submits blinded envelopes via - POST /fountain/withdraw and unblinds the returned signatures. - - - Token withdrawal via fountain - -The wallet re-polls ``GET /fountain/info`` at the advertised -``poll_freq`` to learn about grant changes (added or removed groups). - -Offline door access -~~~~~~~~~~~~~~~~~~~ - -.. figure:: images/097/sesame-door-access.svg - :align: center - :alt: Door access sequence: lock hands the challenge to the wallet - over NFC, the wallet creates and pays an order at the merchant - backend with its group token, and presents the resulting - pos_confirmation to the lock. - - - Offline door access - -The two HTTP round trips (order creation and payment) dominate the -door opening latency; the cryptography (ECDSA verification on the -lock MCU with hardware acceleration) is on the order of 15 ms. - -Token renewal -~~~~~~~~~~~~~ - -Tokens are replaced inline on every door access, but all tokens of a -slot expire together with the slot's issue key (enforced by the -same-key constraint). Before expiry — with a grace period of, e.g., -a quarter of the validity period — the wallet performs a regular -fountain withdrawal (previous section) using background scheduling -(Android WorkManager / iOS BGTaskScheduler). If background execution -is unavailable, renewal happens on next app start, possibly making -the user wait briefly at the door. - -Revocation -~~~~~~~~~~ - -Because tokens are blind-signed, the merchant backend cannot -selectively invalidate a token it has never seen. Revocation -therefore means *stop issuing*: - -* **Full revocation**: ``DELETE /private/fountains/$FOUNTAIN_ID``. - The wallet's next fountain request fails with 401; existing tokens - die with their issue key. -* **Partial revocation** (user leaves one group): - ``PATCH /private/fountains/$FOUNTAIN_ID`` with the remaining - grants. The secret stays valid; the wallet learns about the - reduced grants on its next ``GET /fountain/info`` poll and drops - the affected tokens. - -The maximum revocation delay is ``duration`` × ``key_window_size``: -prefetched tokens for future slots remain usable until their keys -expire. Operators choose these parameters per group accordingly. -Other users of the group are unaffected. The same-key constraint is -what makes this bound hold (see `Same-key constraint`_). - -Usage statistics -~~~~~~~~~~~~~~~~ - -No new mechanism: the existing ``pay`` webhook -(`WebhookEventType`) fires on every successful door opening and -carries the paid contract terms and order id, from which the ACS -derives door id, token family and timestamp. - -Protocol changes (vOpenSesame) ------------------------------- - -Endpoints used unchanged -~~~~~~~~~~~~~~~~~~~~~~~~ - -* :http:post:`[/instances/$INSTANCE]/private/tokenfamilies` — - create access group. -* :http:get:`[/instances/$INSTANCE]/templates/$TEMPLATE_ID` — - wallet fetches template details. -* :http:post:`[/instances/$INSTANCE]/orders/$ORDER_ID/claim` and - :http:post:`[/instances/$INSTANCE]/orders/$ORDER_ID/pay` — token - spend/re-issue via the existing `PayRequest` / `PaymentResponse` - structures (``tokens``, ``wallet_data.tokens_evs``, - ``token_sigs``, ``pos_confirmation``). -* Webhooks (``pay`` event) for usage statistics. - -Extended: challenge-signature OTP devices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:http:post:`[/instances/$INSTANCE]/private/otp-devices` accepts two -new ``otp_algorithm`` values and, for these, generates the key pair -itself: - -.. ts:def:: OtpDeviceAddDetails - - interface OtpDeviceAddDetails { - - // Device ID to use. - otp_device_id: string; - - // Human-readable description for the device. - otp_device_description: string; - - // A key encoded with RFC 3548 Base32. - // IMPORTANT: This is not using the typical - // Taler base32-crockford encoding. - // Instead it uses the RFC 3548 encoding to - // be compatible with the TOTP standard. - // Since protocol **vOpenSesame** this field is optional: - // it MUST be omitted for the challenge-signature - // algorithms (3 and 4), where the backend generates - // the key pair and never exposes the private key. - otp_key?: string; - - // Algorithm for computing the POS confirmation. - // "NONE" or 0: No algorithm (no pos confirmation will be generated) - // "TOTP_WITHOUT_PRICE" or 1: Without amounts (typical OTP device) - // "TOTP_WITH_PRICE" or 2: With amounts (special-purpose OTP device) - // "ECDSA_CHALLENGE" or 3: ECDSA signature over the order's - // challenge. Since protocol **vOpenSesame**. - // "EDDSA_CHALLENGE" or 4: EdDSA signature over the order's - // challenge. Since protocol **vOpenSesame**. - otp_algorithm: Integer | string; - - // Counter for counter-based OTP devices. - otp_ctr?: Integer; - } - -For algorithms 3 and 4 the response changes from ``204 No content`` -to ``200 OK`` with: - -.. ts:def:: OtpDeviceCreateResponse - - interface OtpDeviceCreateResponse { - - // Public key of the key pair generated by the backend - // for this device, to be configured into the offline - // verifier (lock). For "ECDSA_CHALLENGE" a compressed - // NIST P-256 point, for "EDDSA_CHALLENGE" an Ed25519 - // public key. Crockford Base32 encoded. - // Since protocol **vOpenSesame**. - otp_device_pub: string; - } - -`OtpDeviceDetails` (returned by -:http:get:`[/instances/$INSTANCE]/private/otp-devices/$DEVICE_ID`) -gains the same field: - -.. code-block:: none - - // Public key of the device key pair, present only for - // challenge-signature algorithms (3 and 4). - // Since protocol **vOpenSesame**. - otp_device_pub?: string; - -The corresponding C enum ``TALER_MerchantConfirmationAlgorithm`` -(today ``TALER_MCA_NONE`` = 0, ``TALER_MCA_WITHOUT_PRICE`` = 1, -``TALER_MCA_WITH_PRICE`` = 2) is extended with -``TALER_MCA_ECDSA_CHALLENGE`` = 3 and -``TALER_MCA_EDDSA_CHALLENGE`` = 4, and -``TALER_build_pos_confirmation()`` learns to sign the hash of the -order's challenge with the device private key. In the -`PaymentResponse`, ``pos_confirmation`` then carries the Crockford -Base32 encoded signature; the JSON schema is unchanged. - -Extended: templates with token choices -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Fixed-order templates learn to describe v1 contracts. -`TemplateContractFixedOrder` is extended: - -.. ts:def:: TemplateContractFixedOrder - - interface TemplateContractFixedOrder { - - // The price is imposed by the merchant and cannot be - // changed by the customer. - // This parameter is optional. - amount?: Amount; - - // Contract choices for orders created from this - // template. If present, instantiated orders are - // version 1 contracts with exactly these choices - // (token inputs/outputs, see `OrderChoice`), enabling - // token-gated and token-re-issuing templates. - // Must not be combined with ``amount``. - // Since protocol **vOpenSesame**. - choices?: OrderChoice[]; - } - -Extended: same-key re-issue constraint -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -`OrderOutputToken` gains a flag that binds the issue key of an output -token to the input token consumed in the same choice: - -.. ts:def:: OrderOutputToken - - interface OrderOutputToken { - - // Token output. - type: "token"; - - // Token family slug as configured in the merchant backend. - token_family_slug: string; - - // How many units of the output are issued by the merchant. - // Defaults to 1 if not specified. - count?: Integer; - - // When should the output token be valid. - valid_at?: Timestamp; - - // If true, the output tokens are blind-signed with the - // same issue key (same ``h_issue``) as the input token - // of the same token family consumed by this choice, - // instead of the key of the current time slot. The - // choice MUST then also contain an `OrderInputToken` - // for the same ``token_family_slug``, and this field - // must not be combined with ``valid_at``. - // Since protocol **vOpenSesame**. - reissue_with_input_key?: boolean; - } - -At pay time, :http:post:`[/instances/$INSTANCE]/orders/$ORDER_ID/pay` -selects the issue key matching the ``h_issue`` of the presented input -token for such outputs. The effective key MUST be one of the keys -listed for the family in the contract's `ContractTokenFamily` -``keys`` array; the ``key_index`` in `ContractOutputToken` designates -the key for the current slot and is overridden by this rule (see -`Discussion / Q&A`_). - -Extended: challenge pass-through on template use -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:http:post:`[/instances/$INSTANCE]/templates/$TEMPLATE_ID` accepts a -client-supplied challenge that the backend stores with the order and -later signs when computing the ``pos_confirmation``: - -.. ts:def:: UsingTemplateCommonRequest - - interface UsingTemplateCommonRequest { - - // Type of the template being instantiated. - template_type: string; - - // Summary to use in the contract. Only if - // not already specified by the template. - summary?: string; - - // The amount to be paid, including tip. - amount?: Amount; - - // Optional tip amount. - tip?: Amount; - - // Challenge obtained out-of-band (e.g. read from an - // offline lock over NFC): 32 bytes, Crockford Base32 - // encoded. Only meaningful — and then mandatory — if - // the template's OTP device uses a challenge-signature - // algorithm (3 or 4); rejected otherwise. - // Since protocol **vOpenSesame**. - challenge?: string; - } - -New: fountain management (private API) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -All fountain endpoints are new since protocol **vOpenSesame**. - -``POST [/instances/$INSTANCE]/private/fountains`` - -Creates a fountain. Request: - -.. ts:def:: FountainCreateRequest - - interface FountainCreateRequest { - - // Human-readable description; typically an opaque user - // identifier of the provisioning access control system. - // Note that the merchant backend does not need — and - // should not receive — the user's real identity. - description: string; - - // How often wallets holding this fountain's secret - // should re-poll ``GET /fountain/info`` for updated - // grants. - poll_freq: RelativeTime; - - // One grant per token family (access group) the user - // may withdraw tokens for. - grants: FountainGrant[]; - } - -.. ts:def:: FountainGrant - - interface FountainGrant { - - // Slug of the token family this grant refers to. - token_family_slug: string; - - // Maximum number of tokens the fountain will - // blind-sign per key validity period for this family. - tokens_per_period_limit: Integer; - - // Number of tokens the wallet should aim to hold per - // period (client-side stocking target; must not - // exceed ``tokens_per_period_limit``). - tokens_per_period_stash: Integer; - - // Number of key slots ahead of the current one the - // wallet may withdraw tokens for. Bounds prefetching - // and thereby the revocation latency. - key_window_size: Integer; - } - -Response (``200 OK``): - -.. ts:def:: FountainCreateResponse - - interface FountainCreateResponse { - - // Identifier of the fountain, used by the access - // control system for subsequent CRUD operations. - fountain_id: string; - - // 256-bit bearer credential for the wallet, - // Crockford Base32 encoded. Returned exactly once; - // the backend stores only a hash of it. - fountain_secret: string; - } - -``GET [/instances/$INSTANCE]/private/fountains`` - -Lists all fountains of the instance (``fountain_id`` and -``description`` per entry, following the conventions of the other -private list endpoints). - -``GET [/instances/$INSTANCE]/private/fountains/$FOUNTAIN_ID`` - -Returns the fountain's ``description``, ``poll_freq`` and ``grants`` -(never the secret). - -``PATCH [/instances/$INSTANCE]/private/fountains/$FOUNTAIN_ID`` - -Replaces ``description``, ``poll_freq`` and/or ``grants``. Used for -group membership changes and partial revocation; the fountain secret -remains valid. - -``DELETE [/instances/$INSTANCE]/private/fountains/$FOUNTAIN_ID`` - -Deletes the fountain: full revocation of the user. Subsequent wallet -requests with the secret fail with ``401 Unauthorized``. - -New: fountain wallet API (public) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``GET [/instances/$INSTANCE]/fountain/info`` - -Authenticated with ``Authorization: Bearer $FOUNTAIN_SECRET`` (the -secret is deliberately kept out of URLs to avoid leakage into logs -and proxies). Response (``200 OK``): - -.. ts:def:: FountainInfoResponse - - interface FountainInfoResponse { - - // How often the wallet should re-poll this endpoint. - poll_freq: RelativeTime; - - // Grants of this fountain, enriched with the token - // family metadata and issue keys the wallet needs to - // prepare blinded envelopes. - grants: FountainWalletGrant[]; - } - -.. ts:def:: FountainWalletGrant - - interface FountainWalletGrant { - - // Slug of the token family. - token_family_slug: string; - - // Maximum withdrawals per key validity period. - tokens_per_period_limit: Integer; - - // Suggested number of tokens to hold per period. - tokens_per_period_stash: Integer; - - // Number of key slots ahead the wallet may withdraw for. - key_window_size: Integer; - - // Token family metadata including the currently valid - // (and, within ``key_window_size``, upcoming) issue - // public keys. - token_family: ContractTokenFamily; - } - -Error responses: ``401 Unauthorized`` if the secret is unknown -(including deleted fountains — the wallet then reports the access as -revoked). - -``POST [/instances/$INSTANCE]/fountain/withdraw`` - -Withdraws blind-signed tokens for any subset of the fountain's -grants in a single request. The request is structured per grant: -each entry addresses one granted token family and one key slot, and -carries the envelopes to sign against that grant's limits: - -.. ts:def:: FountainWithdrawRequest - - interface FountainWithdrawRequest { - - // The fountain's bearer credential. - fountain_secret: string; - - // One entry per (granted token family, key slot) - // being withdrawn from. At most one entry per such - // pair. - grants: FountainGrantWithdrawal[]; - } - -.. ts:def:: FountainGrantWithdrawal - - interface FountainGrantWithdrawal { - - // Token family to withdraw from. Must match one of - // the fountain's grants. - token_family_slug: string; - - // Desired validity time of the tokens; selects the - // issue key slot. Must lie within the grant's - // ``key_window_size``. Defaults to "now". - valid_at?: Timestamp; - - // Blinded envelopes to sign with the selected slot's - // issue key, as in the pay protocol. The number of - // envelopes is counted against the grant's - // ``tokens_per_period_limit`` for that slot. - envelopes: TokenEnvelope[]; - } - -Response (``200 OK``): - -.. ts:def:: FountainWithdrawResponse - - interface FountainWithdrawResponse { - - // One entry per entry in the request's ``grants`` - // array, in the same order. - grants: FountainGrantWithdrawalResult[]; - } - -.. ts:def:: FountainGrantWithdrawalResult - - interface FountainGrantWithdrawalResult { - - // Token family these signatures belong to. - token_family_slug: string; - - // Hash of the issue public key that was used, so the - // wallet can associate the tokens with the right slot. - h_issue: HashCode; - - // Blind signatures, in the same order as the - // ``envelopes`` of the request entry, as in the pay - // protocol (`TokenIssueBlindSig`). - blind_sigs: TokenIssueBlindSig[]; - } - -Error responses: ``401 Unauthorized`` (unknown secret), -``409 Conflict`` (entry for a family without grant, or ``valid_at`` -outside the grant's key window), -``429 Too Many Requests`` (``tokens_per_period_limit`` exhausted for -a grant and slot). - -New URI scheme -~~~~~~~~~~~~~~ - -``taler://fountain/$MERCHANT_HOST[/$INSTANCE_PATH]/$FOUNTAIN_SECRET`` -tells a wallet where to find the fountain and how to authenticate. -This requires an extension of LSD 0006 and corresponding -wallet-core support (new transaction/subscription type holding the -fountain state, background renewal scheduling). - -Security considerations ------------------------ - -Threat model -~~~~~~~~~~~~ - -.. list-table:: - :header-rows: 1 - :widths: 22 25 53 - - * - Attacker - - Goal - - Prevented by - * - NFC eavesdropper - - replay an access response - - single-use challenge (nonce discarded, TOTP expires) - * - wallet of wrong group - - open unauthorized door - - template choices name specific token families; pay fails for - tokens of other families - * - revoked wallet - - keep access after revocation - - same-key constraint: re-issued tokens die with the original - key; re-withdrawal requires the fountain, which is gone - * - token thief - - use a stolen token - - tokens are single-use; the legitimate user's next access - fails, exposing the theft - * - colluding doors - - track a user across doors - - lock only sees ``pos_confirmation`` for its own challenge; - blind signatures unlink accesses - * - compromised lock - - forge confirmations - - lock holds only the public key (or TOTP secret for - low-security doors); device private key stays at the backend - * - ACS + merchant collusion - - deanonymize a user - - blind signatures unlink issuance from use; the merchant knows - only "someone from group X" - * - fountain secret thief - - withdraw tokens - - equivalent to a stolen physical key; fountain can be deleted - and re-created with a new secret - -Privacy properties -~~~~~~~~~~~~~~~~~~ - -* **Lock** sees only ``pos_confirmation`` and time — no user or - group information. -* **Merchant backend** sees at withdrawal only the fountain secret - (no identity); at door access only door id, token family and time. - Blind signatures prevent linking a door access to any withdrawal - event or to other accesses by the same user. -* **Anonymity set**: all users holding valid tokens of the same - family (per time slot). -* **ACS** knows the identity→fountain mapping but never sees door - accesses of individual users. -* **Residual risk — timing correlation**: withdrawal events - (authenticated by fountain secret) and door accesses could be - correlated by timing in backend logs. Inline replacement tokens - are not withdrawals, so the correlation window is limited to the - periodic per-slot renewal; wallets SHOULD randomize renewal times - within the grace period. - -Test Plan -========= - -* Merchant backend integration tests covering: challenge-signature - OTP device lifecycle (create → key returned → pay → - ``pos_confirmation`` verifies against the returned public key); - templates with token choices; same-key re-issue (replacement token - verifiably signed by the input token's key, including across slot - boundaries); fountain CRUD, grant limits, window enforcement, and - revocation semantics (401 after DELETE, reduced grants after - PATCH). -* Wallet-core integration test: full cycle fountain URI → info → - withdraw → template order with challenge → pay with token → - unblind replacement → renewal after key expiry. -* Lock-side verification tested against a software lock simulator - (nonce and TOTP modes) before hardware integration. - -Definition of Done -================== - -* Protocol changes ratified and merged into the merchant API - reference (``core/api-merchant.rst``) with the final protocol - version number replacing **vOpenSesame**. -* Merchant backend implements the extended and new endpoints, - guarded by the protocol version bump. -* LSD 0006 extension for ``taler://fountain`` published. -* Wallet-core implements fountain handling and background renewal. -* End-to-end demo with at least one nonce-mode (challenge-signature) - and one TOTP-mode door. - -Alternatives -============ - -* **Per-door token families** instead of per-group: simpler template - choices, but token count and fountain traffic grow with the number - of doors, and the anonymity set shrinks per door. -* **Online locks**: the merchant backend (or ACS) opens the door - directly over the network, removing one NFC round trip. Rejected - as baseline: requires connected locks; may be revisited as an - optimization (see Discussion). -* **Selling access tokens as ordinary orders** (existing - subscription purchase flow) instead of fountains: works without new - endpoints, but provides no per-user revocable issuance channel, no - per-period limits, and requires a payment interaction for what is - an entitlement, complicating IAM integration. -* **Exchange coins instead of merchant tokens**: would provide - anonymity but has no notion of merchant-scoped groups, per-slot - keys or free re-issue, and would entangle access control with real - money flows. - -Drawbacks -========= - -* **Revocation latency** is bounded, not zero: up to ``duration`` × - ``key_window_size``. Short slots (server room) mean more frequent - wallet renewals and more issue keys. -* **Two HTTP round trips** per door opening put the latency budget - at the mercy of the phone's network conditions (concrete/metal - buildings, Wi-Fi↔mobile handover); see Discussion. -* **Background renewal** depends on OS task scheduling; users who - block background activity may wait at the door. -* **New credential class** (fountain secret) at the merchant backend - increases the authentication surface of the public API. -* The merchant backend accumulates per-group, per-door usage data; - operators must treat these logs as personal-data-adjacent even - though they name no user. - -Discussion / Q&A -================ - -* **Same-key constraint mechanics.** Binding the output key to the - input token (``reissue_with_input_key``) was chosen over "output - key must equal current slot" because a payment near a slot - boundary may legitimately consume a token of the previous slot. - Open question: how to represent the overridden ``key_index`` in - the contract terms cleanly — the current proposal keeps - ``key_index`` pointing at the current-slot key and overrides it at - pay time, which is implicit; an explicit marker value may be - preferable. -* **Latency budget.** Estimated door opening time is ~250–550 ms, - dominated by the two HTTP round trips (order creation + pay), not - by cryptography (P-256 verification on an MCU with hardware - acceleration is ~15 ms). These numbers need real measurements. - If they disappoint, candidate optimizations: pre-created orders - per door, combining order creation and payment into one request, - or the online-lock mode below. -* **Wallet template prefetch.** The wallet only learns which token - families a door accepts from the (cacheable) template. A reverse - lookup ("which door ids accept family X") would let the wallet - skip order creation for doors it cannot open, but requires - distributing door lists to wallets and keeping them fresh; - deferred for marginal UX gain. -* **Future work (v2):** - - * *HOTP counter mode* for clockless offline doors: the merchant - maintains a monotonic counter per device and returns - ``{ ctr, HOTP(secret, ctr) }``; the lock accepts iff - ``ctr > last_seen_ctr``. Note that ``otp_ctr`` is already - stored by the backend today but unused by - ``TALER_build_pos_confirmation()``, so this needs new - generation logic. - * *Forced key rotation* (e.g. - ``POST /private/tokenfamilies/$SLUG/rotate-keys``) for immediate - revocation, at the cost of forcing all wallets of the group to - re-withdraw. - * *Online door mode*: backend-initiated opening via a callback to - the operator's system, trading offline capability for lower - interaction latency. diff --git a/design-documents/098-token-fountains.rst b/design-documents/098-token-fountains.rst @@ -0,0 +1,288 @@ +.. _dd-98: + +DD 98: Token Fountains for Promotions +##################################### + +Summary +======= + +This document proposes token fountains as a simple way for institutions to +distribute promotional ``discount`` and ``subscription`` tokens. A wallet +imports a bearer credential, periodically retrieves the available grants and +withdraws blind-signed tokens within limits configured by the institution. + +.. note:: + + This design document proposes an extension for discussion. The protocol + version name ``vTokenFountains`` is a placeholder. + +Motivation +========== + +Institutions sometimes want to distribute promotional benefits to a group +without creating and paying a separate order for every recipient. For +example, a university may give students discount tokens accepted by a campus +merchant, or periodically provide subscription tokens for a continuing +promotion. + +GNU Taler already defines blind-signed merchant tokens. What is missing is a +simple distribution channel through which a wallet can obtain an authorized +set of those tokens over time. A **token fountain** is a bearer credential +that authorizes such withdrawals. + +The institution can distribute a fountain as a QR code or deep link. The +wallet imports the link, learns which token families are available and +withdraws blind-signed tokens subject to the configured limits. Blind +signatures keep a later use of a token unlinkable from its withdrawal. + +Requirements +============ + +* Institutions must be able to distribute promotional tokens without + creating a separate order for every withdrawal. + +* A fountain must support grants for existing ``discount`` and + ``subscription`` token families. + +* Token withdrawals must use blind signatures so that later token use is not + linkable to withdrawal. + +* The merchant backend must enforce per-period withdrawal limits atomically. + +* Wallets must be able to import a fountain using a QR code or Taler URI. + +* The bearer secret must not appear in HTTP request URLs after onboarding. + +* Deployments must be able to update or revoke future withdrawals without + invalidating tokens that were already issued. + +Proposed Solution +================= + +Terminology +----------- + +In protocol descriptions, this document uses the existing token-family +``kind`` values: + +* A ``discount`` token is useful for a one-time or otherwise limited + promotion. It may be consumed without the merchant issuing a replacement. + +* A ``subscription`` token represents a continuing promotion. When an order + consumes it, the existing merchant token mechanism can return a replacement + token envelope as an output of the selected choice. + +User-facing applications may call either benefit a **pass**. The protocol +continues to distinguish them by token-family kind. + +Fountain Credential +------------------- + +A fountain has: + +* a public ``fountain_id`` identifying the fountain within a merchant + instance; + +* a random ``fountain_secret`` used as its bearer credential; + +* a polling frequency; and + +* one or more grants describing which token families may be withdrawn and + at what rate. + +The onboarding URI is: + +:: + + taler://fountain/$MERCHANT_HOST[/$INSTANCE_PATH]/$FOUNTAIN_ID/$FOUNTAIN_SECRET + +The URI may be displayed as a QR code or opened as a wallet deep link. +``fountain_id`` tells the wallet which fountain it is importing, while +``fountain_secret`` authenticates the import. The wallet must treat the full +URI as sensitive. + +After import, the secret must not be placed in subsequent HTTP URLs. It is +sent in the ``Authorization`` header when reading fountain information and in +the request body when withdrawing tokens, as specified by the merchant API. + +Deployment Models +----------------- + +An institution may use one fountain for an entire campaign. This is simple +to distribute, but every recipient shares one bearer credential and the same +withdrawal limits. A leaked link cannot be revoked for only one recipient. + +Alternatively, the institution may create one fountain per recipient and +distribute each link through its existing student communication system. This +allows independent limits and revocation without requiring the merchant +backend to store the student's real identity. The fountain description can +contain an opaque reference meaningful only to the institution. + +The choice is operational rather than cryptographic. Blind token issuance +prevents the merchant from linking a withdrawn token to its later use in +either model. + +Grant Model +----------- + +Each :ts:type:`FountainGrant` names a token family and defines: + +* ``tokens_per_period_limit``, the maximum number of tokens signed for one + issue-key validity period; + +* ``tokens_per_period_stash``, the number of tokens the wallet should try to + keep available for that period; and + +* ``key_window_size``, the number of issue-key slots into the future for + which the wallet may prepare tokens. + +The merchant may update grants without replacing the bearer credential. +Wallets periodically retrieve the current grants according to ``poll_freq``. +Removing a grant prevents future withdrawals from that family, but tokens +already withdrawn remain valid according to the token family's issue keys. + +Protocol Flow +------------- + +1. The institution configures suitable ``discount`` or ``subscription`` token + families on the merchant backend. + +2. It creates a fountain with grants for those token families. + +3. The backend returns ``fountain_id`` and ``fountain_secret`` once. The + institution forms the Taler fountain URI and gives it to the intended + recipients. + +4. A wallet imports the URI and calls ``GET /fountain/info`` using the bearer + credential. + +5. The wallet prepares blinded token envelopes for any grants it needs to + replenish and submits them to ``POST /fountain/withdraw``. + +6. The backend checks the grant, issue-key window and withdrawal limit, then + blind-signs the accepted envelopes. + +7. The wallet unblinds and stores the tokens. Their later use follows the + existing merchant token protocol. + +Merchant API +------------ + +Private Management Operations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The merchant API adds private operations to create, inspect, update, list and +delete fountains: + +* ``POST /private/fountains`` + +* ``GET /private/fountains`` + +* ``GET /private/fountains/$FOUNTAIN_ID`` + +* ``PATCH /private/fountains/$FOUNTAIN_ID`` + +* ``DELETE /private/fountains/$FOUNTAIN_ID`` + +Creation returns: + +.. ts:def:: FountainCreateResponse + + interface FountainCreateResponse { + // Public identifier included in the wallet onboarding URI. + fountain_id: string; + + // 256-bit bearer credential, Crockford Base32 encoded. + // Returned only at creation; the backend stores only its hash. + fountain_secret: string; + } + +Wallet Operations +~~~~~~~~~~~~~~~~~ + +``GET /fountain/info`` returns the current grants, token-family metadata and +issue public keys needed to prepare blinded envelopes. + +``POST /fountain/withdraw`` accepts the bearer credential and blinded +envelopes. The request is grouped by token family and issue-key slot so that +the backend can enforce each grant's limits. + +Deletion invalidates future requests using the credential. It does not +invalidate tokens that the wallet already withdrew. + +Security Considerations +======================= + +Anyone possessing a fountain URI can withdraw under its grants. Distribution +systems must therefore protect the URI like any bearer credential. Wallets +must avoid exposing it in history, telemetry, screenshots or backups that are +not intended to contain secrets. + +The backend stores only a hash of ``fountain_secret``. It must rate-limit +failed authentication and enforce withdrawal limits atomically to prevent +concurrent requests from exceeding a grant. + +Fountains are not proof of student identity. Eligibility is decided by the +institution when it distributes the link. A shared campaign link can be +forwarded, while a per-recipient link can only be individually disabled after +the institution learns that it was compromised. + +Privacy Considerations +====================== + +Fountain descriptions should contain opaque administrative references rather +than names or student identifiers. The merchant backend does not need a +recipient's identity to issue blind-signed tokens. + +Network metadata can still correlate a wallet's requests. Deployments that +require stronger network-level privacy need an appropriate transport or proxy; +blind signatures alone do not hide IP addresses or request timing. + +Drawbacks +========= + +The fountain API adds persistent bearer credentials, polling and quota state +to the merchant backend. + +Shared campaign fountains trade administrative simplicity for coarse +revocation and shared quotas. Per-recipient fountains provide better control +but require the institution to create and distribute many distinct links. + +Test Plan +========= + +* Test creation, inspection, update, listing and deletion of fountains. + +* Test importing a URI containing both ``fountain_id`` and + ``fountain_secret``. + +* Test withdrawals for both ``discount`` and ``subscription`` token families. + +* Test atomic enforcement of per-period limits under concurrent requests. + +* Test rejection of unknown grants, unsupported issue-key slots and invalid + credentials. + +* Test that deleting a fountain prevents new withdrawals without invalidating + tokens already held by a wallet. + +* Test that token use cannot be linked to its fountain withdrawal from the + blind-signature transcript. + +Definition of Done +================== + +* The merchant backend implements fountain management, information and + withdrawal operations. + +* The onboarding URI includes the merchant location, ``fountain_id`` and + ``fountain_secret``. + +* Wallet Core can import a fountain, poll its grants, replenish tokens within + the configured limits and stop when the credential is revoked. + +* Merchant applications can create and distribute promotional fountains + without supplying recipient identities to the merchant backend. + +* Discount and subscription token families retain their existing consumption + and replacement semantics. diff --git a/design-documents/099-programmable-templates.rst b/design-documents/099-programmable-templates.rst @@ -0,0 +1,184 @@ +.. _dd-99: + +DD 99: Programmable Template Validation +####################################### + +Summary +======= + +This document sketches programmable validation for merchant templates. +A locally configured Bash script examines a proposed order after normal +template processing and either permits or rejects order creation. + +.. warning:: + + This is an early design sketch. It does not yet define a protocol version, + complete input schema or stable configuration API. + +Motivation +========== + +Merchant templates cover common ways of turning client input into an order, +but some deployments have local conditions that are too specific for the +generic merchant protocol. Examples include checking an institution's local +campaign rules, validating a site-specific reference or applying a temporary +business restriction. + +Adding every local rule to the merchant backend would make the protocol and +implementation increasingly specialized. Programmable template validation +would instead let an administrator associate a local Bash script with a +template. The script examines a proposed order and either allows or rejects +its creation. + +Requirements +============ + +* Normal merchant template validation must run before a custom validator. + +* A validator must be a Bash script configured locally by the merchant + operator. + +* The script must initially be limited to allowing or rejecting an order. + +* Script input must use a structured, versioned JSON interface. + +* Failure, timeout or malformed output must reject order creation. + +* Scripts must run with strict time and resource limits. + +* Scripts must not receive merchant secrets or unrelated customer data. + +* Installing or changing scripts must require an administrative + permission and produce an audit record. + +Proposed Solution +================= + +The merchant backend performs its normal template processing first: + +1. It parses and validates the template instantiation request. + +2. It resolves products and constructs the proposed contract terms. + +3. If the template has a configured validation script, the backend invokes + it with the proposed order as versioned JSON on standard input. + +4. An exit status of zero allows the order. Any other exit status rejects + it. + +5. Only an allowed proposal is persisted as an order. + +The script is validation-only. Allowing scripts to rewrite +contract terms would make it harder to reason about signatures, totals and +which component owns protocol validation. A later design may introduce +carefully constrained transformations if concrete use cases require them. + +Script Configuration and Invocation +----------------------------------- + +The merchant operator associates an absolute Bash script path with a +template. This is server-side configuration and must never be supplied by a +client while instantiating the template. The backend invokes the configured +file using a fixed command equivalent to: + +.. code-block:: console + + /bin/bash /configured/path/validate-template.sh + +The backend must not construct a shell command from template request values. +The script receives no request-derived command-line arguments. A versioned +JSON object is written to its standard input and may contain: + +* the merchant instance and template identifiers; + +* the normalized template request; + +* the proposed contract terms; + +* the current time and a small, explicitly defined execution context; and + +* a version number for the validator input schema. + +It must not contain merchant private keys, database credentials or unrelated +customer data. + +The script communicates its decision through its exit status: + +* Exit status 0 permits creation of the proposed order. + +* Any non-zero exit status rejects order creation. + +Standard output is ignored in the initial design. A bounded amount of +standard error may be recorded in merchant logs for administrators, but must +not automatically be returned to an untrusted client. + +Execution Requirements +---------------------- + +Script execution must be bounded by a short timeout and strict resource +limits. Failure, timeout and non-zero process termination must deny order +creation. + +Network and filesystem access should be disabled by default. If a deployment +needs external state, access should be explicitly configured and documented +rather than inherited from the merchant backend process. + +The script runs with a minimal environment and a dedicated unprivileged +service account. The backend must not pass authentication tokens, database +credentials or other ambient secrets through environment variables. + +Configuration and operation of scripts require an administrative +permission separate from ordinary template use. Logs should identify the +template, script and outcome without recording complete contract terms or +sensitive request values by default. + +Open Design Work +================ + +The following points require concrete use cases and implementation +experiments: + +* the exact versioned input schema; + +* how scripts are installed, selected and updated; + +* which merchant API error represents rejection by a script; + +* whether controlled read-only access to local data is necessary; + +* how deterministic behavior and reproducible debugging are achieved; and + +* whether any future version should permit constrained modifications to the + proposed contract. + +Security Considerations +======================= + +A script runs on the order-creation path and is therefore both a security +boundary and a denial-of-service risk. It must not execute with the merchant +backend's full authority. Resource limits and fail-closed behavior are +mandatory. Bash is not itself a sandbox, so the operating system must enforce +the configured process, filesystem and network restrictions. + +Script updates can change which orders a merchant accepts. Configuration +changes therefore need authentication, audit records and an explicit rollback +mechanism. + +Test Plan +========= + +An eventual implementation should test exit status zero and non-zero, +timeouts, missing or unreadable scripts, process failures, resource +exhaustion and attempts to use forbidden network or filesystem access. It +should also verify the JSON input, confirm that rejected orders are not +partially persisted and ensure that diagnostics do not leak sensitive +contract data. + +Definition of Done +================== + +This design is ready for implementation only after the versioned input +schema, script configuration, permission model, error mapping and operating +system restrictions are specified precisely. At least one real deployment +rule must be implemented as a Bash script to validate the interface before it +becomes a stable merchant feature. diff --git a/design-documents/images/097/Makefile b/design-documents/images/999/Makefile diff --git a/design-documents/images/097/sesame-business-lifecycle.puml b/design-documents/images/999/sesame-business-lifecycle.puml diff --git a/design-documents/images/097/sesame-business-lifecycle.svg b/design-documents/images/999/sesame-business-lifecycle.svg diff --git a/design-documents/images/097/sesame-business-overview.puml b/design-documents/images/999/sesame-business-overview.puml diff --git a/design-documents/images/097/sesame-business-overview.svg b/design-documents/images/999/sesame-business-overview.svg diff --git a/design-documents/images/097/sesame-business-signup.puml b/design-documents/images/999/sesame-business-signup.puml diff --git a/design-documents/images/097/sesame-business-signup.svg b/design-documents/images/999/sesame-business-signup.svg diff --git a/design-documents/images/097/sesame-data-model.puml b/design-documents/images/999/sesame-data-model.puml diff --git a/design-documents/images/097/sesame-data-model.svg b/design-documents/images/999/sesame-data-model.svg diff --git a/design-documents/images/097/sesame-deployment.puml b/design-documents/images/999/sesame-deployment.puml diff --git a/design-documents/images/097/sesame-deployment.svg b/design-documents/images/999/sesame-deployment.svg diff --git a/design-documents/images/097/sesame-door-access.puml b/design-documents/images/999/sesame-door-access.puml diff --git a/design-documents/images/097/sesame-door-access.svg b/design-documents/images/999/sesame-door-access.svg diff --git a/design-documents/images/097/sesame-onboarding.puml b/design-documents/images/999/sesame-onboarding.puml diff --git a/design-documents/images/097/sesame-onboarding.svg b/design-documents/images/999/sesame-onboarding.svg diff --git a/design-documents/images/097/sesame-withdraw.puml b/design-documents/images/999/sesame-withdraw.puml diff --git a/design-documents/images/097/sesame-withdraw.svg b/design-documents/images/999/sesame-withdraw.svg diff --git a/design-documents/images/097/taler-style.iuml b/design-documents/images/999/taler-style.iuml diff --git a/design-documents/index.rst b/design-documents/index.rst @@ -108,4 +108,7 @@ Design documents that start with "XX" are considered deprecated. 094-discounts-passes-wallet 095-captcha-100 096-partial-payments + 097-challenge-confirmations + 098-token-fountains + 099-programmable-templates 999-template