097-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.