taler-docs

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

097-project-sesame.rst (40543B)


      1 DD 97: Project Sesame — Anonymous Access Control
      2 ################################################
      3 
      4 *Status*: draft (2026-07-17)
      5 
      6 Summary
      7 =======
      8 
      9 This design document describes how the Taler eco-system can be used
     10 to enable anonymous access to different physical access control systems.
     11 A wallet obtains blind-signed
     12 tokens (one per *access group*) from the merchant backend via a new
     13 *fountain* credential. To open a door, the wallet reads a challenge
     14 from the (offline) lock, instantiates an order from the door's
     15 template, pays with its group token (consuming it and receiving a
     16 replacement via blind signature), and relays the resulting signed
     17 confirmation (``pos_confirmation``) back to the lock, which verifies
     18 it without any network connection. The merchant backend learns *that*
     19 someone from a given access group opened a given door, but not *who*.
     20 
     21 The design reuses existing merchant features — token families,
     22 templates, OTP devices, v1 contract choices and blind signatures —
     23 and adds one new concept (the fountain) plus a small number of
     24 protocol extensions. All protocol changes proposed here are
     25 annotated with "Since protocol **vOpenSesame**"; the placeholder is
     26 to be replaced with the concrete merchant protocol version number
     27 once the changes are implemented.
     28 
     29 Motivation
     30 ==========
     31 
     32 Conventional electronic access control systems identify the user on
     33 every door opening, producing detailed movement profiles. At the
     34 same time, purely offline mechanical or RFID solutions lack
     35 revocability, usage statistics and flexible group management.
     36 
     37 The Taler merchant backend already contains almost all building
     38 blocks for a privacy-preserving alternative:
     39 
     40 * *Token families* provide blind-signed, single-use, time-sliced
     41   tokens (see `DD 46 <046-mumimo-contracts.html>`__ and
     42   `DD 68 <068-tokens-roadmap.html>`__).
     43 * *Templates* allow order creation without merchant-side frontend
     44   logic.
     45 * *OTP devices* allow generation of confirmation
     46   (``pos_confirmation``) for successful payments, which an offline
     47   device (door) can verify.
     48 
     49 Beyond pure access control, the same infrastructure supports *paying*
     50 to open a door (parking garage, laundry room): a template choice with
     51 a non-zero amount is an ordinary Taler payment.
     52 
     53 Requirements
     54 ============
     55 
     56 * **Good UX**: the user taps the door with a phone; the door opens
     57   within well under a second.
     58 * **Anonymity within the group**: the merchant backend learns
     59   "someone from group X opened door Y at time T", but cannot identify
     60   the individual user, nor link accesses across doors to the same
     61   user.
     62 * **Offline locks**: the lock verifies a confirmation without any
     63   network connection. Only the wallet and the merchant backend need
     64   Internet connectivity.
     65 * **Per-group tokens**: one blind-signed token per access group, not
     66   per door. A user in two groups holds two tokens regardless of the
     67   number of doors.
     68 * **Revocability**: revocation latency is configurable per access
     69   group (e.g. 15 minutes for a server room, 30 days for a co-working
     70   space). Revocation means: stop issuing tokens.
     71 * **Usage statistics**: every door opening is recorded at the
     72   merchant backend (door, group, timestamp) for maintenance and
     73   reporting, without identifying users.
     74 * **Payment support**: doors may additionally (or exclusively) be
     75   opened against payment in digital cash.
     76 * **IAM decoupling**: identity management stays outside the merchant
     77   backend. Any identity/access management system can provision
     78   fountains via the private API.
     79 * **Minimal Taler changes**: reuse token families, templates, OTP
     80   devices and the pay protocol; add only the fountain and small
     81   extensions to existing endpoints.
     82 
     83 Proposed Solution
     84 =================
     85 
     86 Business view
     87 -------------
     88 
     89 This section is the non-technical summary; the protocol description
     90 starts at `Architecture`_.
     91 
     92 Employees or members carry a digital key in the Taler Wallet app on
     93 their phone. Opening a door means tapping the phone on the reader:
     94 the phone contacts the Taler service over the Internet
     95 and shows the door a signed permit. The doors themselves need no
     96 network connection and no per-user configuration.
     97 
     98 .. figure:: images/097/sesame-business-overview.svg
     99    :align: center
    100    :alt: Opening a door, business view: tap the phone, the phone
    101          trades an anonymous group pass for a signed permit, the door
    102          checks the permit offline and opens.
    103 
    104 
    105    Opening a door (business view)
    106 
    107 The key is anonymous within a group: access management (the door
    108 owner) sees that some member of, say, the staff group opened a door,
    109 which is enough for maintenance planning and billing, but cannot
    110 tell which member it was and cannot build movement profiles.
    111 
    112 Getting the key is a one-time sign-up with the company account.
    113 Access management registers a digital key for the person's access
    114 groups and the phone receives it as a QR code or link. From then on the wallet
    115 app picks up its passes by itself in the background. The sign-up is
    116 personal, but the passes obtained with the key are not: the service
    117 cannot link a pass to the sign-up it came from, which is what makes
    118 door openings anonymous.
    119 
    120 .. figure:: images/097/sesame-business-signup.svg
    121    :align: center
    122    :alt: Sign-up, business view: sign in with the company account,
    123          access management registers a digital key, the phone scans a
    124          QR code and from then on picks up anonymous passes
    125          automatically.
    126 
    127 
    128    Sign-up and pass pick-up (business view)
    129 
    130 Access is managed per group, not per door. Group membership comes
    131 from the existing IAM or HR system. When a person leaves, access
    132 management stops their key with one API call; passes already on the
    133 phone expire on their own within a period configured per area
    134 (15 minutes for a server room, days for a co-working space).
    135 
    136 .. figure:: images/097/sesame-business-lifecycle.svg
    137    :align: center
    138    :alt: Access lifecycle, business view: person joins, phone receives
    139          the digital key, doors open during membership, key is stopped
    140          on leaving and remaining passes expire on their own.
    141 
    142 
    143    Access lifecycle (business view)
    144 
    145 The same setup can also charge for opening a door (parking garage,
    146 laundry room): a door can accept payment in digital cash instead of,
    147 or in addition to, a group pass.
    148 
    149 Architecture
    150 ------------
    151 
    152 .. list-table::
    153    :header-rows: 1
    154    :widths: 20 65 15
    155 
    156    * - Component
    157      - Role
    158      - Network
    159    * - IAM / identity provider (e.g. OIDC)
    160      - Authenticates users towards the access control operator.
    161      - online
    162    * - Access control system (ACS)
    163      - Manages doors, access groups and user→group mappings; provisions
    164        token families, templates, OTP devices and fountains at the
    165        merchant backend via the private API.
    166      - online
    167    * - Merchant backend
    168      - Issues blind-signed tokens, verifies token payments, computes
    169        ``pos_confirmation`` for locks.
    170      - online
    171    * - Taler Wallet
    172      - Holds fountain secret and tokens; bridges lock and merchant
    173        backend.
    174      - online
    175    * - Lock
    176      - Generates challenges, verifies ``pos_confirmation``, opens the
    177        door.
    178      - offline
    179 
    180 .. figure:: images/097/sesame-deployment.svg
    181    :align: center
    182    :alt: Deployment view: wallet bridges the offline lock and the
    183          merchant backend; the ACS provisions the merchant backend and
    184          configures locks out-of-band.
    185 
    186 
    187    Deployment view
    188 
    189 Cryptographic building blocks
    190 -----------------------------
    191 
    192 .. list-table::
    193    :header-rows: 1
    194    :widths: 30 40 30
    195 
    196    * - Primitive
    197      - Purpose
    198      - Instantiation
    199    * - Blind signature
    200      - Token issuance (withdrawal and re-issue)
    201      - existing Taler token family code (RSA/CS)
    202    * - Signature over challenge (``pos_confirmation``)
    203      - Merchant confirmation to the lock
    204      - ECDSA (P-256) or EdDSA, **new**
    205    * - TOTP
    206      - Door challenge / replay prevention
    207      - existing OTP devices
    208 
    209 Data model mapping
    210 ------------------
    211 
    212 .. figure:: images/097/sesame-data-model.svg
    213    :align: center
    214    :alt: Data model mapping: access group to token family (1:1), door
    215          to template plus OTP device (1:1:1), user to fountain (1:1);
    216          template choices and fountain grants reference token
    217          families.
    218 
    219 
    220    Data model mapping
    221 
    222 A door with a distinct access policy needs its own door id (its own
    223 OTP device and template). Doors MAY share one door id if their
    224 access policy is identical, at the cost of losing per-door usage
    225 statistics.
    226 
    227 Access groups as token families
    228 -------------------------------
    229 
    230 Each access group is an ordinary token family created via
    231 :http:post:`[/instances/$INSTANCE]/private/tokenfamilies` with
    232 ``kind: "subscription"`` — no new token family kind is introduced.
    233 The merchant backend already auto-generates one issue key per
    234 ``validity_granularity`` time slot; the hash of the issue key
    235 (``h_issue``) thus identifies group and time slot. Different groups
    236 can use different ``duration`` values:
    237 
    238 * server room: 15 minutes,
    239 * office building: 1 day,
    240 * co-working space: 30 days.
    241 
    242 The token ``duration`` bounds the revocation latency (see
    243 `Revocation`_).
    244 
    245 Doors as OTP devices and templates
    246 ----------------------------------
    247 
    248 Each door is registered as an OTP device
    249 (:http:post:`[/instances/$INSTANCE]/private/otp-devices`) and gets a
    250 template (:http:post:`[/instances/$INSTANCE]/private/templates`)
    251 whose ``otp_id`` references the device. The ``otp_algorithm``
    252 determines how the merchant backend computes the
    253 ``pos_confirmation`` that the lock verifies:
    254 
    255 ===== ========================= ======================================================
    256 Value Name                      ``pos_confirmation``
    257 ===== ========================= ======================================================
    258 0     ``NONE``                  none (existing)
    259 1     ``TOTP_WITHOUT_PRICE``    TOTP code, shared-secret verification (existing)
    260 2     ``TOTP_WITH_PRICE``       TOTP code including price (existing)
    261 3     ``ECDSA_CHALLENGE``       ECDSA signature over the order's challenge
    262                                 (**new**, since protocol **vOpenSesame**)
    263 4     ``EDDSA_CHALLENGE``       EdDSA signature over the order's challenge
    264                                 (**new**, since protocol **vOpenSesame**)
    265 ===== ========================= ======================================================
    266 
    267 For the challenge-signature algorithms (3 and 4) the key pair is
    268 generated *by the merchant backend*; the private key is never
    269 exposed. The public key is returned on device creation and the ACS
    270 configures it into the lock. For TOTP algorithms the lock shares the
    271 ``otp_key`` secret as today.
    272 
    273 The lock itself is configured out-of-band (not stored at the
    274 merchant) with: door id, challenge mode (TOTP or nonce), and either
    275 the TOTP secret or the device public key. Challenge handling at the
    276 lock:
    277 
    278 ========= ==================== ====================================================
    279 Mode      Lock generates       Lock verifies
    280 ========= ==================== ====================================================
    281 TOTP      nothing              recomputes TOTP code with its own clock
    282 Nonce     random 32 bytes      signature verification of ``pos_confirmation``
    283                                over its own nonce with the device public key
    284 ========= ==================== ====================================================
    285 
    286 The door template ties the device to the access groups allowed to
    287 open it. Since protocol **vOpenSesame**, fixed-order templates can
    288 carry v1 contract ``choices`` (see `Extended: templates with token
    289 choices`_). Example (informal):
    290 
    291 .. code-block:: none
    292 
    293    TemplateCreateRequest {
    294      template_id:          "door-201",
    295      template_description: "Office building room 201",
    296      otp_id:               "door-201",
    297      template_contract: {
    298        choices: [
    299          { // staff members: consume one token, get a replacement
    300            amount:  "KUDOS:0",
    301            inputs:  [ { type: "token", token_family_slug: "staff" } ],
    302            outputs: [ { type: "token", token_family_slug: "staff",
    303                         reissue_with_input_key: true } ]
    304          },
    305          { // facilities team: different group, same door
    306            amount:  "KUDOS:0",
    307            inputs:  [ { type: "token", token_family_slug: "facilities" } ],
    308            outputs: [ { type: "token", token_family_slug: "facilities",
    309                         reissue_with_input_key: true } ]
    310          },
    311          { // visitors: pay to open
    312            amount:  "EUR:5"
    313          }
    314        ]
    315      }
    316    }
    317 
    318 Multiple choices mean multiple access groups (or payment options) for
    319 the same door; the wallet picks the choice matching a token family it
    320 holds. Each token choice consumes one token (input) and re-issues
    321 one (output) — the single-use replacement mechanism. A one-time
    322 access pass (visitor) is simply a choice *without* a token output,
    323 paired with a ``discount``-kind token family; no separate mechanism
    324 is needed.
    325 
    326 Same-key constraint
    327 ~~~~~~~~~~~~~~~~~~~
    328 
    329 When a choice consumes and re-issues a token of the *same* family,
    330 the replacement token must be signed with the *same* issue key as the
    331 consumed token — not with the key of the current time slot. This is
    332 what the new ``reissue_with_input_key`` flag on `OrderOutputToken`
    333 expresses.
    334 
    335 Without this constraint, a revoked user could keep obtaining
    336 replacement tokens under ever-newer keys through door accesses alone,
    337 bypassing the fountain (where revocation is enforced) indefinitely.
    338 With the constraint, the replacement expires together with the
    339 consumed token, forcing every wallet back to the fountain once per
    340 key slot.
    341 
    342 Users as fountains
    343 ------------------
    344 
    345 A *fountain* is a per-user bearer credential at the merchant backend
    346 that entitles a wallet to withdraw blind-signed tokens for a set of
    347 token families. It is the only new concept introduced by this
    348 design.
    349 
    350 * One fountain per user, created by the ACS at user onboarding.
    351 * Each fountain holds a 256-bit ``fountain_secret`` (given to the
    352   user's wallet) and a list of *grants*, one per access group.
    353 * Adding/removing group memberships: ``PATCH`` the fountain's grants;
    354   the secret stays valid.
    355 * Revoking the user entirely: ``DELETE`` the fountain.
    356 
    357 The wallet receives the credential as a new URI (an extension of
    358 `LSD 0006 <https://lsd.gnunet.org/lsd0006/>`__ is required):
    359 
    360 .. code-block:: none
    361 
    362    taler://fountain/$MERCHANT_HOST[/$INSTANCE_PATH]/$FOUNTAIN_SECRET
    363 
    364 Each grant carries three parameters:
    365 
    366 * ``tokens_per_period_limit``: how many tokens the fountain will
    367   blind-sign per key validity period (rate limit; relevant for
    368   non-subscription families such as one-time passes).
    369 * ``tokens_per_period_stash``: how many tokens the wallet *should*
    370   hold per period (client-side target).
    371 * ``key_window_size``: how many key slots ahead of the current one
    372   the wallet may withdraw for. With window size 3 and a duration of
    373   one day, the wallet can prefetch tokens for today, tomorrow and the
    374   day after, reducing fountain round trips — at the cost of
    375   revocation latency (see `Revocation`_).
    376 
    377 Protocol flows
    378 --------------
    379 
    380 System setup
    381 ~~~~~~~~~~~~
    382 
    383 Performed by the ACS through the private merchant API:
    384 
    385 1. For each access group:
    386    :http:post:`[/instances/$INSTANCE]/private/tokenfamilies`
    387    (``kind: "subscription"``, per-group ``duration`` and
    388    ``validity_granularity``).
    389 2. For each door:
    390    :http:post:`[/instances/$INSTANCE]/private/otp-devices` with
    391    ``otp_algorithm`` 1 (TOTP lock) or 3/4 (challenge-signature lock).
    392    For 3/4 the response contains the generated device public key.
    393 3. For each door:
    394    :http:post:`[/instances/$INSTANCE]/private/templates` with
    395    ``otp_id`` = device id and one contract choice per authorized
    396    group.
    397 4. The ACS configures the physical lock with door id, challenge mode
    398    and the TOTP secret or device public key.
    399 
    400 User onboarding
    401 ~~~~~~~~~~~~~~~
    402 
    403 1. The user authenticates towards the ACS via the IAM (e.g. OIDC).
    404 2. The ACS resolves the user's roles to access groups.
    405 3. The ACS creates the fountain:
    406    ``POST /private/fountains`` with one grant per group.
    407 4. The ACS hands the resulting ``taler://fountain/...`` URI to the
    408    user's wallet (QR code, deep link, ...).
    409 5. The wallet stores the fountain URI, fetches the fountain info and
    410    withdraws its initial tokens (next section).
    411 
    412 .. figure:: images/097/sesame-onboarding.svg
    413    :align: center
    414    :alt: Onboarding sequence: IAM login, ACS resolves groups, creates
    415          the fountain at the merchant backend, hands the fountain URI
    416          to the wallet.
    417 
    418 
    419    User onboarding
    420 
    421 Token withdrawal via fountain
    422 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    423 
    424 The fountain secret is never placed in a URL (it would leak into
    425 server logs and proxies): it travels in an ``Authorization: Bearer``
    426 header for GET and in the request body for POST.
    427 
    428 .. figure:: images/097/sesame-withdraw.svg
    429    :align: center
    430    :alt: Withdrawal sequence: wallet fetches grants and issue keys via
    431          GET /fountain/info, then submits blinded envelopes via
    432          POST /fountain/withdraw and unblinds the returned signatures.
    433 
    434 
    435    Token withdrawal via fountain
    436 
    437 The wallet re-polls ``GET /fountain/info`` at the advertised
    438 ``poll_freq`` to learn about grant changes (added or removed groups).
    439 
    440 Offline door access
    441 ~~~~~~~~~~~~~~~~~~~
    442 
    443 .. figure:: images/097/sesame-door-access.svg
    444    :align: center
    445    :alt: Door access sequence: lock hands the challenge to the wallet
    446          over NFC, the wallet creates and pays an order at the merchant
    447          backend with its group token, and presents the resulting
    448          pos_confirmation to the lock.
    449 
    450 
    451    Offline door access
    452 
    453 The two HTTP round trips (order creation and payment) dominate the
    454 door opening latency; the cryptography (ECDSA verification on the
    455 lock MCU with hardware acceleration) is on the order of 15 ms.
    456 
    457 Token renewal
    458 ~~~~~~~~~~~~~
    459 
    460 Tokens are replaced inline on every door access, but all tokens of a
    461 slot expire together with the slot's issue key (enforced by the
    462 same-key constraint). Before expiry — with a grace period of, e.g.,
    463 a quarter of the validity period — the wallet performs a regular
    464 fountain withdrawal (previous section) using background scheduling
    465 (Android WorkManager / iOS BGTaskScheduler). If background execution
    466 is unavailable, renewal happens on next app start, possibly making
    467 the user wait briefly at the door.
    468 
    469 Revocation
    470 ~~~~~~~~~~
    471 
    472 Because tokens are blind-signed, the merchant backend cannot
    473 selectively invalidate a token it has never seen. Revocation
    474 therefore means *stop issuing*:
    475 
    476 * **Full revocation**: ``DELETE /private/fountains/$FOUNTAIN_ID``.
    477   The wallet's next fountain request fails with 401; existing tokens
    478   die with their issue key.
    479 * **Partial revocation** (user leaves one group):
    480   ``PATCH /private/fountains/$FOUNTAIN_ID`` with the remaining
    481   grants. The secret stays valid; the wallet learns about the
    482   reduced grants on its next ``GET /fountain/info`` poll and drops
    483   the affected tokens.
    484 
    485 The maximum revocation delay is ``duration`` × ``key_window_size``:
    486 prefetched tokens for future slots remain usable until their keys
    487 expire. Operators choose these parameters per group accordingly.
    488 Other users of the group are unaffected. The same-key constraint is
    489 what makes this bound hold (see `Same-key constraint`_).
    490 
    491 Usage statistics
    492 ~~~~~~~~~~~~~~~~
    493 
    494 No new mechanism: the existing ``pay`` webhook
    495 (`WebhookEventType`) fires on every successful door opening and
    496 carries the paid contract terms and order id, from which the ACS
    497 derives door id, token family and timestamp.
    498 
    499 Protocol changes (vOpenSesame)
    500 ------------------------------
    501 
    502 Endpoints used unchanged
    503 ~~~~~~~~~~~~~~~~~~~~~~~~
    504 
    505 * :http:post:`[/instances/$INSTANCE]/private/tokenfamilies` —
    506   create access group.
    507 * :http:get:`[/instances/$INSTANCE]/templates/$TEMPLATE_ID` —
    508   wallet fetches template details.
    509 * :http:post:`[/instances/$INSTANCE]/orders/$ORDER_ID/claim` and
    510   :http:post:`[/instances/$INSTANCE]/orders/$ORDER_ID/pay` — token
    511   spend/re-issue via the existing `PayRequest` / `PaymentResponse`
    512   structures (``tokens``, ``wallet_data.tokens_evs``,
    513   ``token_sigs``, ``pos_confirmation``).
    514 * Webhooks (``pay`` event) for usage statistics.
    515 
    516 Extended: challenge-signature OTP devices
    517 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    518 
    519 :http:post:`[/instances/$INSTANCE]/private/otp-devices` accepts two
    520 new ``otp_algorithm`` values and, for these, generates the key pair
    521 itself:
    522 
    523 .. ts:def:: OtpDeviceAddDetails
    524 
    525   interface OtpDeviceAddDetails {
    526 
    527     // Device ID to use.
    528     otp_device_id: string;
    529 
    530     // Human-readable description for the device.
    531     otp_device_description: string;
    532 
    533     // A key encoded with RFC 3548 Base32.
    534     // IMPORTANT: This is not using the typical
    535     // Taler base32-crockford encoding.
    536     // Instead it uses the RFC 3548 encoding to
    537     // be compatible with the TOTP standard.
    538     // Since protocol **vOpenSesame** this field is optional:
    539     // it MUST be omitted for the challenge-signature
    540     // algorithms (3 and 4), where the backend generates
    541     // the key pair and never exposes the private key.
    542     otp_key?: string;
    543 
    544     // Algorithm for computing the POS confirmation.
    545     // "NONE" or 0: No algorithm (no pos confirmation will be generated)
    546     // "TOTP_WITHOUT_PRICE" or 1: Without amounts (typical OTP device)
    547     // "TOTP_WITH_PRICE" or 2: With amounts (special-purpose OTP device)
    548     // "ECDSA_CHALLENGE" or 3: ECDSA signature over the order's
    549     //   challenge. Since protocol **vOpenSesame**.
    550     // "EDDSA_CHALLENGE" or 4: EdDSA signature over the order's
    551     //   challenge. Since protocol **vOpenSesame**.
    552     otp_algorithm: Integer | string;
    553 
    554     // Counter for counter-based OTP devices.
    555     otp_ctr?: Integer;
    556   }
    557 
    558 For algorithms 3 and 4 the response changes from ``204 No content``
    559 to ``200 OK`` with:
    560 
    561 .. ts:def:: OtpDeviceCreateResponse
    562 
    563   interface OtpDeviceCreateResponse {
    564 
    565     // Public key of the key pair generated by the backend
    566     // for this device, to be configured into the offline
    567     // verifier (lock). For "ECDSA_CHALLENGE" a compressed
    568     // NIST P-256 point, for "EDDSA_CHALLENGE" an Ed25519
    569     // public key. Crockford Base32 encoded.
    570     // Since protocol **vOpenSesame**.
    571     otp_device_pub: string;
    572   }
    573 
    574 `OtpDeviceDetails` (returned by
    575 :http:get:`[/instances/$INSTANCE]/private/otp-devices/$DEVICE_ID`)
    576 gains the same field:
    577 
    578 .. code-block:: none
    579 
    580    // Public key of the device key pair, present only for
    581    // challenge-signature algorithms (3 and 4).
    582    // Since protocol **vOpenSesame**.
    583    otp_device_pub?: string;
    584 
    585 The corresponding C enum ``TALER_MerchantConfirmationAlgorithm``
    586 (today ``TALER_MCA_NONE`` = 0, ``TALER_MCA_WITHOUT_PRICE`` = 1,
    587 ``TALER_MCA_WITH_PRICE`` = 2) is extended with
    588 ``TALER_MCA_ECDSA_CHALLENGE`` = 3 and
    589 ``TALER_MCA_EDDSA_CHALLENGE`` = 4, and
    590 ``TALER_build_pos_confirmation()`` learns to sign the hash of the
    591 order's challenge with the device private key. In the
    592 `PaymentResponse`, ``pos_confirmation`` then carries the Crockford
    593 Base32 encoded signature; the JSON schema is unchanged.
    594 
    595 Extended: templates with token choices
    596 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    597 
    598 Fixed-order templates learn to describe v1 contracts.
    599 `TemplateContractFixedOrder` is extended:
    600 
    601 .. ts:def:: TemplateContractFixedOrder
    602 
    603   interface TemplateContractFixedOrder {
    604 
    605     // The price is imposed by the merchant and cannot be
    606     // changed by the customer.
    607     // This parameter is optional.
    608     amount?: Amount;
    609 
    610     // Contract choices for orders created from this
    611     // template. If present, instantiated orders are
    612     // version 1 contracts with exactly these choices
    613     // (token inputs/outputs, see `OrderChoice`), enabling
    614     // token-gated and token-re-issuing templates.
    615     // Must not be combined with ``amount``.
    616     // Since protocol **vOpenSesame**.
    617     choices?: OrderChoice[];
    618   }
    619 
    620 Extended: same-key re-issue constraint
    621 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    622 
    623 `OrderOutputToken` gains a flag that binds the issue key of an output
    624 token to the input token consumed in the same choice:
    625 
    626 .. ts:def:: OrderOutputToken
    627 
    628   interface OrderOutputToken {
    629 
    630     // Token output.
    631     type: "token";
    632 
    633     // Token family slug as configured in the merchant backend.
    634     token_family_slug: string;
    635 
    636     // How many units of the output are issued by the merchant.
    637     // Defaults to 1 if not specified.
    638     count?: Integer;
    639 
    640     // When should the output token be valid.
    641     valid_at?: Timestamp;
    642 
    643     // If true, the output tokens are blind-signed with the
    644     // same issue key (same ``h_issue``) as the input token
    645     // of the same token family consumed by this choice,
    646     // instead of the key of the current time slot. The
    647     // choice MUST then also contain an `OrderInputToken`
    648     // for the same ``token_family_slug``, and this field
    649     // must not be combined with ``valid_at``.
    650     // Since protocol **vOpenSesame**.
    651     reissue_with_input_key?: boolean;
    652   }
    653 
    654 At pay time, :http:post:`[/instances/$INSTANCE]/orders/$ORDER_ID/pay`
    655 selects the issue key matching the ``h_issue`` of the presented input
    656 token for such outputs. The effective key MUST be one of the keys
    657 listed for the family in the contract's `ContractTokenFamily`
    658 ``keys`` array; the ``key_index`` in `ContractOutputToken` designates
    659 the key for the current slot and is overridden by this rule (see
    660 `Discussion / Q&A`_).
    661 
    662 Extended: challenge pass-through on template use
    663 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    664 
    665 :http:post:`[/instances/$INSTANCE]/templates/$TEMPLATE_ID` accepts a
    666 client-supplied challenge that the backend stores with the order and
    667 later signs when computing the ``pos_confirmation``:
    668 
    669 .. ts:def:: UsingTemplateCommonRequest
    670 
    671   interface UsingTemplateCommonRequest {
    672 
    673     // Type of the template being instantiated.
    674     template_type: string;
    675 
    676     // Summary to use in the contract. Only if
    677     // not already specified by the template.
    678     summary?: string;
    679 
    680     // The amount to be paid, including tip.
    681     amount?: Amount;
    682 
    683     // Optional tip amount.
    684     tip?: Amount;
    685 
    686     // Challenge obtained out-of-band (e.g. read from an
    687     // offline lock over NFC): 32 bytes, Crockford Base32
    688     // encoded. Only meaningful — and then mandatory — if
    689     // the template's OTP device uses a challenge-signature
    690     // algorithm (3 or 4); rejected otherwise.
    691     // Since protocol **vOpenSesame**.
    692     challenge?: string;
    693   }
    694 
    695 New: fountain management (private API)
    696 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    697 
    698 All fountain endpoints are new since protocol **vOpenSesame**.
    699 
    700 ``POST [/instances/$INSTANCE]/private/fountains``
    701 
    702 Creates a fountain. Request:
    703 
    704 .. ts:def:: FountainCreateRequest
    705 
    706   interface FountainCreateRequest {
    707 
    708     // Human-readable description; typically an opaque user
    709     // identifier of the provisioning access control system.
    710     // Note that the merchant backend does not need — and
    711     // should not receive — the user's real identity.
    712     description: string;
    713 
    714     // How often wallets holding this fountain's secret
    715     // should re-poll ``GET /fountain/info`` for updated
    716     // grants.
    717     poll_freq: RelativeTime;
    718 
    719     // One grant per token family (access group) the user
    720     // may withdraw tokens for.
    721     grants: FountainGrant[];
    722   }
    723 
    724 .. ts:def:: FountainGrant
    725 
    726   interface FountainGrant {
    727 
    728     // Slug of the token family this grant refers to.
    729     token_family_slug: string;
    730 
    731     // Maximum number of tokens the fountain will
    732     // blind-sign per key validity period for this family.
    733     tokens_per_period_limit: Integer;
    734 
    735     // Number of tokens the wallet should aim to hold per
    736     // period (client-side stocking target; must not
    737     // exceed ``tokens_per_period_limit``).
    738     tokens_per_period_stash: Integer;
    739 
    740     // Number of key slots ahead of the current one the
    741     // wallet may withdraw tokens for. Bounds prefetching
    742     // and thereby the revocation latency.
    743     key_window_size: Integer;
    744   }
    745 
    746 Response (``200 OK``):
    747 
    748 .. ts:def:: FountainCreateResponse
    749 
    750   interface FountainCreateResponse {
    751 
    752     // Identifier of the fountain, used by the access
    753     // control system for subsequent CRUD operations.
    754     fountain_id: string;
    755 
    756     // 256-bit bearer credential for the wallet,
    757     // Crockford Base32 encoded. Returned exactly once;
    758     // the backend stores only a hash of it.
    759     fountain_secret: string;
    760   }
    761 
    762 ``GET [/instances/$INSTANCE]/private/fountains``
    763 
    764 Lists all fountains of the instance (``fountain_id`` and
    765 ``description`` per entry, following the conventions of the other
    766 private list endpoints).
    767 
    768 ``GET [/instances/$INSTANCE]/private/fountains/$FOUNTAIN_ID``
    769 
    770 Returns the fountain's ``description``, ``poll_freq`` and ``grants``
    771 (never the secret).
    772 
    773 ``PATCH [/instances/$INSTANCE]/private/fountains/$FOUNTAIN_ID``
    774 
    775 Replaces ``description``, ``poll_freq`` and/or ``grants``. Used for
    776 group membership changes and partial revocation; the fountain secret
    777 remains valid.
    778 
    779 ``DELETE [/instances/$INSTANCE]/private/fountains/$FOUNTAIN_ID``
    780 
    781 Deletes the fountain: full revocation of the user. Subsequent wallet
    782 requests with the secret fail with ``401 Unauthorized``.
    783 
    784 New: fountain wallet API (public)
    785 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    786 
    787 ``GET [/instances/$INSTANCE]/fountain/info``
    788 
    789 Authenticated with ``Authorization: Bearer $FOUNTAIN_SECRET`` (the
    790 secret is deliberately kept out of URLs to avoid leakage into logs
    791 and proxies). Response (``200 OK``):
    792 
    793 .. ts:def:: FountainInfoResponse
    794 
    795   interface FountainInfoResponse {
    796 
    797     // How often the wallet should re-poll this endpoint.
    798     poll_freq: RelativeTime;
    799 
    800     // Grants of this fountain, enriched with the token
    801     // family metadata and issue keys the wallet needs to
    802     // prepare blinded envelopes.
    803     grants: FountainWalletGrant[];
    804   }
    805 
    806 .. ts:def:: FountainWalletGrant
    807 
    808   interface FountainWalletGrant {
    809 
    810     // Slug of the token family.
    811     token_family_slug: string;
    812 
    813     // Maximum withdrawals per key validity period.
    814     tokens_per_period_limit: Integer;
    815 
    816     // Suggested number of tokens to hold per period.
    817     tokens_per_period_stash: Integer;
    818 
    819     // Number of key slots ahead the wallet may withdraw for.
    820     key_window_size: Integer;
    821 
    822     // Token family metadata including the currently valid
    823     // (and, within ``key_window_size``, upcoming) issue
    824     // public keys.
    825     token_family: ContractTokenFamily;
    826   }
    827 
    828 Error responses: ``401 Unauthorized`` if the secret is unknown
    829 (including deleted fountains — the wallet then reports the access as
    830 revoked).
    831 
    832 ``POST [/instances/$INSTANCE]/fountain/withdraw``
    833 
    834 Withdraws blind-signed tokens for any subset of the fountain's
    835 grants in a single request. The request is structured per grant:
    836 each entry addresses one granted token family and one key slot, and
    837 carries the envelopes to sign against that grant's limits:
    838 
    839 .. ts:def:: FountainWithdrawRequest
    840 
    841   interface FountainWithdrawRequest {
    842 
    843     // The fountain's bearer credential.
    844     fountain_secret: string;
    845 
    846     // One entry per (granted token family, key slot)
    847     // being withdrawn from. At most one entry per such
    848     // pair.
    849     grants: FountainGrantWithdrawal[];
    850   }
    851 
    852 .. ts:def:: FountainGrantWithdrawal
    853 
    854   interface FountainGrantWithdrawal {
    855 
    856     // Token family to withdraw from. Must match one of
    857     // the fountain's grants.
    858     token_family_slug: string;
    859 
    860     // Desired validity time of the tokens; selects the
    861     // issue key slot. Must lie within the grant's
    862     // ``key_window_size``. Defaults to "now".
    863     valid_at?: Timestamp;
    864 
    865     // Blinded envelopes to sign with the selected slot's
    866     // issue key, as in the pay protocol. The number of
    867     // envelopes is counted against the grant's
    868     // ``tokens_per_period_limit`` for that slot.
    869     envelopes: TokenEnvelope[];
    870   }
    871 
    872 Response (``200 OK``):
    873 
    874 .. ts:def:: FountainWithdrawResponse
    875 
    876   interface FountainWithdrawResponse {
    877 
    878     // One entry per entry in the request's ``grants``
    879     // array, in the same order.
    880     grants: FountainGrantWithdrawalResult[];
    881   }
    882 
    883 .. ts:def:: FountainGrantWithdrawalResult
    884 
    885   interface FountainGrantWithdrawalResult {
    886 
    887     // Token family these signatures belong to.
    888     token_family_slug: string;
    889 
    890     // Hash of the issue public key that was used, so the
    891     // wallet can associate the tokens with the right slot.
    892     h_issue: HashCode;
    893 
    894     // Blind signatures, in the same order as the
    895     // ``envelopes`` of the request entry, as in the pay
    896     // protocol (`TokenIssueBlindSig`).
    897     blind_sigs: TokenIssueBlindSig[];
    898   }
    899 
    900 Error responses: ``401 Unauthorized`` (unknown secret),
    901 ``409 Conflict`` (entry for a family without grant, or ``valid_at``
    902 outside the grant's key window),
    903 ``429 Too Many Requests`` (``tokens_per_period_limit`` exhausted for
    904 a grant and slot).
    905 
    906 New URI scheme
    907 ~~~~~~~~~~~~~~
    908 
    909 ``taler://fountain/$MERCHANT_HOST[/$INSTANCE_PATH]/$FOUNTAIN_SECRET``
    910 tells a wallet where to find the fountain and how to authenticate.
    911 This requires an extension of LSD 0006 and corresponding
    912 wallet-core support (new transaction/subscription type holding the
    913 fountain state, background renewal scheduling).
    914 
    915 Security considerations
    916 -----------------------
    917 
    918 Threat model
    919 ~~~~~~~~~~~~
    920 
    921 .. list-table::
    922    :header-rows: 1
    923    :widths: 22 25 53
    924 
    925    * - Attacker
    926      - Goal
    927      - Prevented by
    928    * - NFC eavesdropper
    929      - replay an access response
    930      - single-use challenge (nonce discarded, TOTP expires)
    931    * - wallet of wrong group
    932      - open unauthorized door
    933      - template choices name specific token families; pay fails for
    934        tokens of other families
    935    * - revoked wallet
    936      - keep access after revocation
    937      - same-key constraint: re-issued tokens die with the original
    938        key; re-withdrawal requires the fountain, which is gone
    939    * - token thief
    940      - use a stolen token
    941      - tokens are single-use; the legitimate user's next access
    942        fails, exposing the theft
    943    * - colluding doors
    944      - track a user across doors
    945      - lock only sees ``pos_confirmation`` for its own challenge;
    946        blind signatures unlink accesses
    947    * - compromised lock
    948      - forge confirmations
    949      - lock holds only the public key (or TOTP secret for
    950        low-security doors); device private key stays at the backend
    951    * - ACS + merchant collusion
    952      - deanonymize a user
    953      - blind signatures unlink issuance from use; the merchant knows
    954        only "someone from group X"
    955    * - fountain secret thief
    956      - withdraw tokens
    957      - equivalent to a stolen physical key; fountain can be deleted
    958        and re-created with a new secret
    959 
    960 Privacy properties
    961 ~~~~~~~~~~~~~~~~~~
    962 
    963 * **Lock** sees only ``pos_confirmation`` and time — no user or
    964   group information.
    965 * **Merchant backend** sees at withdrawal only the fountain secret
    966   (no identity); at door access only door id, token family and time.
    967   Blind signatures prevent linking a door access to any withdrawal
    968   event or to other accesses by the same user.
    969 * **Anonymity set**: all users holding valid tokens of the same
    970   family (per time slot).
    971 * **ACS** knows the identity→fountain mapping but never sees door
    972   accesses of individual users.
    973 * **Residual risk — timing correlation**: withdrawal events
    974   (authenticated by fountain secret) and door accesses could be
    975   correlated by timing in backend logs. Inline replacement tokens
    976   are not withdrawals, so the correlation window is limited to the
    977   periodic per-slot renewal; wallets SHOULD randomize renewal times
    978   within the grace period.
    979 
    980 Test Plan
    981 =========
    982 
    983 * Merchant backend integration tests covering: challenge-signature
    984   OTP device lifecycle (create → key returned → pay →
    985   ``pos_confirmation`` verifies against the returned public key);
    986   templates with token choices; same-key re-issue (replacement token
    987   verifiably signed by the input token's key, including across slot
    988   boundaries); fountain CRUD, grant limits, window enforcement, and
    989   revocation semantics (401 after DELETE, reduced grants after
    990   PATCH).
    991 * Wallet-core integration test: full cycle fountain URI → info →
    992   withdraw → template order with challenge → pay with token →
    993   unblind replacement → renewal after key expiry.
    994 * Lock-side verification tested against a software lock simulator
    995   (nonce and TOTP modes) before hardware integration.
    996 
    997 Definition of Done
    998 ==================
    999 
   1000 * Protocol changes ratified and merged into the merchant API
   1001   reference (``core/api-merchant.rst``) with the final protocol
   1002   version number replacing **vOpenSesame**.
   1003 * Merchant backend implements the extended and new endpoints,
   1004   guarded by the protocol version bump.
   1005 * LSD 0006 extension for ``taler://fountain`` published.
   1006 * Wallet-core implements fountain handling and background renewal.
   1007 * End-to-end demo with at least one nonce-mode (challenge-signature)
   1008   and one TOTP-mode door.
   1009 
   1010 Alternatives
   1011 ============
   1012 
   1013 * **Per-door token families** instead of per-group: simpler template
   1014   choices, but token count and fountain traffic grow with the number
   1015   of doors, and the anonymity set shrinks per door.
   1016 * **Online locks**: the merchant backend (or ACS) opens the door
   1017   directly over the network, removing one NFC round trip. Rejected
   1018   as baseline: requires connected locks; may be revisited as an
   1019   optimization (see Discussion).
   1020 * **Selling access tokens as ordinary orders** (existing
   1021   subscription purchase flow) instead of fountains: works without new
   1022   endpoints, but provides no per-user revocable issuance channel, no
   1023   per-period limits, and requires a payment interaction for what is
   1024   an entitlement, complicating IAM integration.
   1025 * **Exchange coins instead of merchant tokens**: would provide
   1026   anonymity but has no notion of merchant-scoped groups, per-slot
   1027   keys or free re-issue, and would entangle access control with real
   1028   money flows.
   1029 
   1030 Drawbacks
   1031 =========
   1032 
   1033 * **Revocation latency** is bounded, not zero: up to ``duration`` ×
   1034   ``key_window_size``. Short slots (server room) mean more frequent
   1035   wallet renewals and more issue keys.
   1036 * **Two HTTP round trips** per door opening put the latency budget
   1037   at the mercy of the phone's network conditions (concrete/metal
   1038   buildings, Wi-Fi↔mobile handover); see Discussion.
   1039 * **Background renewal** depends on OS task scheduling; users who
   1040   block background activity may wait at the door.
   1041 * **New credential class** (fountain secret) at the merchant backend
   1042   increases the authentication surface of the public API.
   1043 * The merchant backend accumulates per-group, per-door usage data;
   1044   operators must treat these logs as personal-data-adjacent even
   1045   though they name no user.
   1046 
   1047 Discussion / Q&A
   1048 ================
   1049 
   1050 * **Same-key constraint mechanics.** Binding the output key to the
   1051   input token (``reissue_with_input_key``) was chosen over "output
   1052   key must equal current slot" because a payment near a slot
   1053   boundary may legitimately consume a token of the previous slot.
   1054   Open question: how to represent the overridden ``key_index`` in
   1055   the contract terms cleanly — the current proposal keeps
   1056   ``key_index`` pointing at the current-slot key and overrides it at
   1057   pay time, which is implicit; an explicit marker value may be
   1058   preferable.
   1059 * **Latency budget.** Estimated door opening time is ~250–550 ms,
   1060   dominated by the two HTTP round trips (order creation + pay), not
   1061   by cryptography (P-256 verification on an MCU with hardware
   1062   acceleration is ~15 ms). These numbers need real measurements.
   1063   If they disappoint, candidate optimizations: pre-created orders
   1064   per door, combining order creation and payment into one request,
   1065   or the online-lock mode below.
   1066 * **Wallet template prefetch.** The wallet only learns which token
   1067   families a door accepts from the (cacheable) template. A reverse
   1068   lookup ("which door ids accept family X") would let the wallet
   1069   skip order creation for doors it cannot open, but requires
   1070   distributing door lists to wallets and keeping them fresh;
   1071   deferred for marginal UX gain.
   1072 * **Future work (v2):**
   1073 
   1074   * *HOTP counter mode* for clockless offline doors: the merchant
   1075     maintains a monotonic counter per device and returns
   1076     ``{ ctr, HOTP(secret, ctr) }``; the lock accepts iff
   1077     ``ctr > last_seen_ctr``. Note that ``otp_ctr`` is already
   1078     stored by the backend today but unused by
   1079     ``TALER_build_pos_confirmation()``, so this needs new
   1080     generation logic.
   1081   * *Forced key rotation* (e.g.
   1082     ``POST /private/tokenfamilies/$SLUG/rotate-keys``) for immediate
   1083     revocation, at the cost of forcing all wallets of the group to
   1084     re-withdraw.
   1085   * *Online door mode*: backend-initiated opening via a callback to
   1086     the operator's system, trading offline capability for lower
   1087     interaction latency.