commit f5cd9e43a524132b8fc5717fd5401459c2732a65
parent aeaca65e39c0c55864ac77dd965fe8035304b4f8
Author: Christian Grothoff <christian@grothoff.org>
Date: Sun, 4 Jan 2026 21:31:43 +0100
work on #10806
Diffstat:
2 files changed, 135 insertions(+), 23 deletions(-)
diff --git a/core/api-merchant.rst b/core/api-merchant.rst
@@ -46,6 +46,7 @@ Android PoS app is currently targeting **v20**.
**Upcoming versions:**
* ``vTAXES``: adds features to manage taxes
+* ``vPAIVANA``: adds features for templates to support session-based payments
**Ideas for future version:**
@@ -1015,6 +1016,59 @@ Querying payment status
}
+.. http:get:: [/instances/$INSTANCE]/sessions/$SESSION_ID
+
+ Query the payment status for a session. This endpoint is for Web shops,
+ Paivana and other integrations (like JavaScript from inside the customer's
+ browser) that need an unauthenticated way to query a payment status where
+ they do not know the order ID because they *only* know the session ID they
+ passed into the template mechanism. Here, the "$SESSION_ID" is an arbitrary
+ string, the semantics of which are left to the application. However, it must
+ match the "$SESSION_ID" passed to the template mechanism.
+
+ Since protocol **vPAIVANA**.
+
+ **Request:**
+
+ :query timeout_ms=NUMBER: *Optional.* If specified, the merchant backend will
+ wait up to ``timeout_ms`` milliseconds for completion of the payment before
+ sending the HTTP response. A client must never rely on this behavior, as the
+ merchant backend may return a response immediately.
+ :query claimed=BOOLEAN: *Optional*. If set to true, long-polling should
+ return as soon as a respective order was claimed.
+
+ **Response:**
+
+ :http:statuscode:`200 OK`:
+ The response is a `GetSessionStatusPaidResponse`. Returned if an
+ order that paid for the respective session exists and was paid.
+ :http:statuscode:`202 Accepted`:
+ The response is a `GetSessionStatusClaimedResponse`. Returned if an
+ order that paid for the respective session exists and was claimed,
+ but not yet paid.
+ :http:statuscode:`404 Not found`:
+ The merchant backend is unaware of an order matching the given session.
+
+ **Details:**
+
+ .. ts:def:: GetSessionStatusPaidResponse
+
+ interface GetSessionStatusPaidResponse {
+
+ // Order ID of the paid order.
+ order_id: String;
+
+ }
+
+ .. ts:def:: GetSessionStatusClaimedResponse
+
+ interface GetSessionStatusClaimedResponse {
+
+ // Order ID of the claimed order.
+ order_id: String;
+
+ }
+
Demonstrating payment
^^^^^^^^^^^^^^^^^^^^^
@@ -1452,7 +1506,7 @@ Setting up instances
The administrator must use the command-line tool if they forgot
their password.
- Since protocol **v21**
+ Since protocol **v21**.
**Request** the request must be an `InstanceAuthConfigurationMessage`.
diff --git a/design-documents/076-paywall-proxy.rst b/design-documents/076-paywall-proxy.rst
@@ -20,11 +20,16 @@ Requirements
* Must withstand high traffic from bots, requests before a payment happened
must be *very* cheap, both in terms of response generation and database
interaction.
+* Should work not just for our paivana-httpd but also for Turnstile-style
+ paywalls that need to work with purely static paywall pages without
+ PHP sessions.
+
Proposed Solution
=================
Architecture
+------------
* paivana-httpd is a reverse proxy that sits between ingress HTTP(S) traffic
and the protected upstream service.
@@ -34,28 +39,69 @@ Architecture
Steps:
-* Browser visits git.taler.net
-* paivana-httpd checks for a Paivana cookie
-
- * If cookie is set and valid, the request is reverse-proxied to upstream. *Stop.*
- * Otherwise, a paywall page is rendered, continue.
-
-* The browser (rendering the paywall page) generates a random paivana ID via JS.
-* Based on this paivana ID, a ``taler://pay-template/{paivana_backend}/.well-known/paivana/{template_id}?paivana_id={paivana_id}``
- URI is generated and rendered as a QR code and link.
-* The browser long-polls on a ``{paivana_backend}/.well-known/pavivana/paivanas/{paivana_id}``
- endpoint that returns when an order with the given paivana ID has been paid
+* Browser visits ``{website}``
+ (for example, ``https://git.taler.net``) where
+ ``{domain}`` is the domain name of ``{website}``.
+* paivana-httpd working as a reverse-proxy for
+ ``{website}``. Whenever called for a non-whitelisted
+ URL, it checks for a the presence of a Paivana cookie valid for
+ this client IP address and ``{website}`` at this time.
+ The *Paivana Cookie* is computed as:
+
+ ``cur_time || '-' || H(website || client_ip || paivana_server_secret || cur_time)``.
+
+ * If such a cookie is set and valid, the request is
+ reverse-proxied to upstream. *Stop.*
+ * Otherwise, a static non-cachable paywall page is returned,
+ including a machine-readable ``Paivana`` HTTP header with
+ the ``taler://pay-template/`` URL minus the client-computed
+ ``{paivana_id}`` and fullfillment URL (see below).
+ *Continue.*
+
+* The browser (rendering the paywall page) generates a random
+ *paivana ID* via JS using the current time (``cur_time``) in seconds
+ since the Epoch and the current URL (``{website}``) plus some
+ freshly generated entropy (``{nonce}``):
+
+ ``paivana_id := cur_time || '-' || H(nonce || website || cur_time)``.
+
+ The same computation could also easily be done by a non-JS client
+ that processes the ``Paivana`` HTTP header (or a GNU Taler wallet
+ running as a Web extension).
+
+* Based on this paivana ID, a
+ ``taler://pay-template/{merchant_backend}/{template_id}?session_id={paivana_id}&fulfillment_url={website}``
+ URI is generated and rendered as a QR code and link, prompting
+ the user to pay for access to the ``{website}`` using GNU Taler.
+
+* The JavaScript in the paywall page running in the browser
+ (or the non-JS client) long-polls
+ on a new ``https://{merchant_backend}/sessions/{paivana_id}``
+ endpoint that returns when an order with the given session ID has been paid
for (regardless of the order ID, which is not known to the browser).
-* A wallet now needs to instantiate the pay template and pay for the resulting order
- by talking to the Paivana backend which proxies the requests to the merchant backend
- and in the process learns the order ID and the payment status change.
- paivana-httpd may also implement the required subset of the merchant backend itself in the future.
+* A wallet now needs to instantiate the pay template, passing the
+ ``session_id`` and the ``website`` as an additional inputs
+ to the order creation (the session ID here will work just like
+ existing use of ``session_ids`` in session-bound payments).
+ Similarly, the ``website`` works as the fulfillment URL as usual.
+* The wallet then must pay for the resulting order
+ by talking to the Merchant backend.
* When the long-poller returns and the payment has succeeded, the
- HTTP response sets the Paivana cookie. The browser reloads the page.
+ browser (still rendering the paywall page) also learns the order ID.
+* The JavaScript of the paywall page (or the non-JS client
+ processing the ``Paivana`` HTTP header) then POSTs the order ID,
+ ``nonce``, ``cur_time``
+ and ``website`` to ``{domain}/.well-known/pavivana``.
+* paivana-httpd computes the paivana ID and checks if the given
+ order ID was indeed paid recently for the computed paivana ID.
+ If so, it generates an HTTP response which the Paivana cookie
+ and redirects to the fulfillment URL (which is the original {website}).
+* The browser reloads the page with the correct
+ Paivana cookie (see first step).
-The Paivana Cookie is computed as ``exp_timestamp || '-' || H(client_ip || paivana_server_secret || exp_timestamp)``.
Problems:
+---------
* A smart attacker might still create a lot of orders via the pay-template.
@@ -63,10 +109,15 @@ Problems:
* Solution B: Rate-limit template instantiation on a per-IP basis.
Implementation:
+---------------
-* Paivana needs to support extended template instantiation with a ``paivana_id``.
-* Paivana component needs to be specified / implemented
-* Wallet-core needs support for a ``paivana_id`` in pay templates.
+* Merchant backend needs way to lookup order IDs under a ``session_id``
+* Merchant backend needs way to instantiate templates with
+ a given ``session_id`` and ``fulfillment_url``. This also
+ requires extending the allowed responses for templates in general.
+* Paivana component needs to be implemented
+* Wallet-core needs support for a ``session_id`` and
+ ``fulfillment_url`` in pay templates.
Test Plan
@@ -82,12 +133,19 @@ N/A
Alternatives
============
+* Do not re-use the session ID mechanism but introduce some new concept.
+ This has the drawback of us needing additional tables and indicies,
+ and also the existing use of the session ID is very parallel to this one.
+
Drawbacks
=========
-* Requires JavaScript
+* This exposes an order ID to anyone who knows the session ID. This is
+ clearly not an issue in this context, and for the existing uses of
+ the session ID it also seems clear that knowledge of the session ID
+ requires an attacker to have access that would easily also already
+ give them any order ID, so this seems harmless.
- * Could be made to work without JS by returning some ``Paivana: ...`` header.
Discussion / Q&A
================