draft-guetschow-taler-protocol.md (40612B)
1 --- 2 v: 3 3 4 title: "The GNU Taler Protocol" 5 docname: draft-guetschow-taler-protocol 6 category: info 7 8 ipr: trust200902 9 workgroup: independent 10 stream: independent 11 keyword: 12 - taler 13 - cryptography 14 - ecash 15 - payments 16 17 #venue: 18 # repo: https://git.gnunet.org/lsd0009.git/ 19 # latest: https://lsd.gnunet.org/lsd0009/ 20 21 author: 22 - 23 name: Mikolai Gütschow 24 org: TUD Dresden University of Technology 25 abbrev: TU Dresden 26 street: Helmholtzstr. 10 27 city: Dresden 28 code: D-01069 29 country: Germany 30 email: mikolai.guetschow@tu-dresden.de 31 32 normative: 33 RFC20: 34 RFC2104: 35 RFC5869: 36 RFC6234: 37 RFC7748: 38 RFC8032: 39 HKDF: DOI.10.1007/978-3-642-14623-7_34 40 SHS: DOI.10.6028/NIST.FIPS.180-4 41 42 informative: 43 44 45 --- abstract 46 47 \[ TBW \] 48 49 --- middle 50 51 # Introduction 52 53 \[ TBW \] 54 55 Beware that this document is still work-in-progress and may contain errors. 56 Use at your own risk! 57 58 # Notation 59 60 - `"abc"` denotes the literal string `abc` encoded as ASCII [RFC20] 61 - `a | b` denotes the concatenation of a with b 62 - `len(a)` denotes the length in bytes of the byte string a 63 - `padZero(y, a)` denotes the byte string a, zero-padded to the length of y bytes 64 - `bits(x)`/`bytes(x)` denotes the minimal number of bits/bytes necessary to represent the multiple precision integer x 65 - `uint(y, x)` denotes the `y` least significant bits of the integer `x`, zero-padded and encoded in network byte order (big endian) 66 - `uintY(x)` where `Y` is a positive integer number is equivalent to `uint(Y, x)` 67 - `random(y)` denotes a randomly generated sequence of y bits 68 - `a * b (mod N)` / `a ** b (mod N)` denotes the multiplication / exponentiation of multiple precision integers a and b, modulo N 69 - `for`, `if`, variable assignment `=`, and conditional operators are to be interpreted like their Python/Julia equivalents 70 - `data.key` denotes the property `key` on the object `data` 71 - `0..n` denotes the exclusive range of integer numbers from `0` to `n-1` 72 - `⟨dataᵢ⟩` within a context of `i = 0..n` denotes `n` objects `dataᵢ`, represented in memory as a continuous array 73 - `⟨dataᵢ.key⟩` within a context of `i = 0..n` denotes an array of the `n` properties `key` of all `n` objects `dataᵢ` 74 75 # Cryptographic Primitives 76 77 // todo: maybe change this description to something more similar to protocol functions (Julia-inspired syntax) 78 79 ## Cryptographic Hash Functions 80 81 ### SHA-256 {#sha256} 82 83 ~~~ 84 SHA-256(msg) -> hash 85 86 Input: 87 msg input message of length L < 2^61 octets 88 89 Output: 90 hash message digest of fixed length HashLen = 32 octets 91 ~~~ 92 93 `hash` is the output of SHA-256 as per Sections 4.1, 5.1, 6.1, and 6.2 of [RFC6234]. 94 95 ### SHA-512 {#sha512} 96 97 ~~~ 98 SHA-512(msg) -> hash 99 100 Input: 101 msg input message of length L < 2^125 octets 102 103 Output: 104 hash message digest of fixed length HashLen = 64 octets 105 ~~~ 106 107 `hash` is the output of SHA-512 as per Sections 4.2, 5.2, 6.3, and 6.4 of [RFC6234]. 108 109 ### SHA-512-256 (truncated SHA-512) {#sha512-trunc} 110 111 ~~~ 112 SHA-512-256(msg) -> hash 113 114 Input: 115 msg input message of length L < 2^125 octets 116 117 Output: 118 hash message digest of fixed length HashLen = 32 octets 119 ~~~ 120 121 The output `hash` corresponds to the first 32 octets of the output of SHA-512 defined in {{sha512}}: 122 123 ~~~ 124 temp = SHA-512(msg) 125 hash = temp[0:31] 126 ~~~ 127 128 Note that this operation differs from SHA-512/256 as defined in [SHS] in the initial hash value. 129 130 131 ## Message Authentication Codes 132 133 ### HMAC {#hmac} 134 135 ~~~ 136 HMAC-Hash(key, text) -> out 137 138 Option: 139 Hash cryptographic hash function with output length HashLen 140 141 Input: 142 key secret key of length at least HashLen 143 text input data of arbitary length 144 145 Output: 146 out output of length HashLen 147 ~~~ 148 149 `out` is calculated as defined in [RFC2104]. 150 151 152 ## Key Derivation Functions 153 154 ### HKDF {#hkdf} 155 156 The Hashed Key Derivation Function (HKDF) used in Taler is an instantiation of [RFC5869] 157 with two different hash functions for the Extract and Expand step as suggested in [HKDF]: 158 `HKDF-Extract` uses `HMAC-SHA512`, while `HKDF-Expand` uses `HMAC-SHA256` (cf. {{hmac}}). 159 160 ~~~ 161 HKDF(salt, IKM, info, L) -> OKM 162 163 Inputs: 164 salt optional salt value (a non-secret random value); 165 if not provided, it is set to a string of 64 zeros. 166 IKM input keying material 167 info optional context and application specific information 168 (can be a zero-length string) 169 L length of output keying material in octets 170 (<= 255*32 = 8160) 171 172 Output: 173 OKM output keying material (of L octets) 174 ~~~ 175 176 The output OKM is calculated as follows: 177 178 ~~~ 179 PRK = HKDF-Extract(salt, IKM) with Hash = SHA-512 (HashLen = 64) 180 OKM = HKDF-Expand(PRK, info, L) with Hash = SHA-256 (HashLen = 32) 181 ~~~ 182 183 ### HKDF-Mod 184 185 Based on the HKDF defined in {{hkdf}}, this function returns an OKM that is smaller than a given multiple precision integer N. 186 187 ~~~ 188 HKDF-Mod(N, salt, IKM, info) -> OKM 189 190 Inputs: 191 N multiple precision integer 192 salt optional salt value (a non-secret random value); 193 if not provided, it is set to a string of 64 zeros. 194 IKM input keying material 195 info optional context and application specific information 196 (can be a zero-length string) 197 198 Output: 199 OKM output keying material (smaller than N) 200 ~~~ 201 202 The final output `OKM` is determined deterministically based on a counter initialized at zero. 203 204 ~~~ 205 counter = 0 206 do until OKM < N: 207 x = HKDF(salt, IKM, info | uint16(counter), bytes(N)) 208 OKM = uint(bits(N), x) 209 counter += 1 210 ~~~ 211 212 ## Non-Blind Signatures 213 214 ### Ed25519 215 216 Taler uses EdDSA instantiated with curve25519 as Ed25519, 217 as defined in Section 5.1 of [RFC8032]. 218 In particular, Taler does _not_ make use of Ed25519ph or Ed25519ctx 219 as defined in that document. 220 221 #### Key generation 222 223 ~~~ 224 Ed25519-GetPub(priv) -> pub 225 226 Input: 227 priv private Ed25519 key 228 229 Output: 230 pub public Ed25519 key 231 ~~~ 232 233 `pub` is calculated as described in Section 5.1.5 of [RFC8032]. 234 235 ~~~ 236 Ed25519-Keygen() -> (priv, pub) 237 238 Output: 239 priv private Ed25519 key 240 pub public Ed25519 key 241 ~~~ 242 243 `priv` and `pub` are calculated as described in Section 5.1.5 of [RFC8032], 244 which is equivalent to the following: 245 246 ~~~ 247 priv = random(256) 248 pub = Ed25519-GetPub(priv) 249 ~~~ 250 251 #### Signing 252 253 ~~~ 254 Ed25519-Sign(priv, msg) -> sig 255 256 Inputs: 257 priv Ed25519 private key 258 msg message to be signed 259 260 Output: 261 sig signature on the message by the given private key 262 ~~~ 263 264 `sig` is calculated as described in Section 5.1.6 of [RFC8032]. 265 266 #### Verifying 267 268 ~~~ 269 Ed25519-Verify(pub, msg, sig) -> out 270 271 Inputs: 272 pub Ed25519 public key 273 msg signed message 274 sig signature on msg 275 276 Output: 277 out true, if sig is a valid signature for msg 278 ~~~ 279 280 `out` is the outcome of the last check of Section 5.1.7 of [RFC8032]. 281 282 ## Key Agreement 283 284 ### X25519 285 286 Taler uses Elliptic Curve Diffie-Hellman (ECDH) on curve25519 as defined in Section 6.1 of [RFC7748], 287 but reuses Ed25519 keypairs for one side of the agreement instead of random bytes. 288 Depending on whether the private or public part is from Ed25519, two different functions are used. 289 290 {::comment} 291 see https://libsodium.gitbook.io/doc/advanced/scalar_multiplication 292 see https://libsodium.gitbook.io/doc/advanced/ed25519-curve25519 293 {:/} 294 295 ~~~ 296 ECDH-Ed25519-Priv(priv, pub) -> shared 297 298 Input: 299 priv private Ed25519 key 300 pub public X25519 key 301 302 Output: 303 shared shared secret based on the given keys 304 ~~~ 305 306 `shared` is calculated as follows, using the function X25519 defined in Section 5 of [RFC7748]: 307 308 ~~~ 309 priv' = SHA-512-256(priv) 310 // todo: missing bit clamping from https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_sign/ed25519/ref10/keypair.c#L71 311 shared' = X25519(priv', pub) 312 shared = SHA-512(shared') 313 ~~~ 314 315 {::comment} 316 see GNUNET_CRYPTO_eddsa_ecdh 317 {:/} 318 319 ~~~ 320 ECDH-Ed25519-Pub(priv, pub) -> shared 321 322 Input: 323 priv private X25519 key 324 pub public Ed25519 key 325 326 Output: 327 shared shared secret based on the given keys 328 ~~~ 329 330 `shared` is calculated as follows, using the function X25519 defined in Section 5 of [RFC7748], 331 and `Convert-Point-Ed25519-Curve25519(p)` which implements the birational map of Section 4.1 of [RFC7748]: 332 333 ~~~ 334 pub' = Convert-Point-Ed25519-Curve25519(pub) 335 shared' = X25519(priv, pub') 336 shared = SHA-512(shared') 337 338 {::comment} 339 see GNUNET_CRYPTO_eddsa_ecdh 340 {:/} 341 ~~~ 342 343 ~~~ 344 ECDH-GetPub(priv) -> pub 345 346 Input: 347 priv private X25519 key 348 349 Output: 350 pub public X25519 key 351 ~~~ 352 353 `pub` is calculated according to Section 6.1 of [RFC7748]: 354 355 ~~~ 356 pub = X25519(priv, 9) 357 ~~~ 358 359 {::comment} 360 see GNUNET_CRYPTO_ecdhe_key_get_public 361 {:/} 362 363 ## Blind Signatures 364 365 ### RSA-FDH {#rsa-fdh} 366 367 #### Supporting Functions 368 369 ~~~ 370 RSA-FDH(msg, pubkey) -> fdh 371 372 Inputs: 373 msg message 374 pubkey RSA public key consisting of modulus N and public exponent e 375 376 Output: 377 fdh full-domain hash of msg over pubkey.N 378 ~~~ 379 380 `fdh` is calculated based on HKDF-Mod from {{hkdf-mod}} as follows: 381 382 ~~~ 383 info = "RSA-FDA FTpsW!" 384 salt = uint16(bytes(pubkey.N)) | uint16(bytes(pubkey.e)) 385 | pubkey.N | pubkey.e 386 fdh = HKDF-Mod(pubkey.N, salt, msg, info) 387 ~~~ 388 389 The resulting `fdh` can be used to test against a malicious RSA pubkey 390 by verifying that the greatest common denominator (gcd) of `fdh` and `pubkey.N` is 1. 391 392 ~~~ 393 RSA-FDH-Derive(bks, pubkey) -> out 394 395 Inputs: 396 bks blinding key secret of length L = 32 octets 397 pubkey RSA public key consisting of modulus N and public exponent e 398 399 Output: 400 out full-domain hash of bks over pubkey.N 401 ~~~ 402 403 `out` is calculated based on HKDF-Mod from {{hkdf-mod}} as follows: 404 405 ~~~ 406 info = "Blinding KDF" 407 salt = "Blinding KDF extractor HMAC key" 408 fdh = HKDF-Mod(pubkey.N, salt, bks, info) 409 ~~~ 410 411 #### Blinding 412 413 ~~~ 414 RSA-FDH-Blind(msg, bks, pubkey) -> out 415 416 Inputs: 417 msg message 418 bks blinding key secret of length L = 32 octets 419 pubkey RSA public key consisting of modulus N and public exponent e 420 421 Output: 422 out message blinded for pubkey 423 ~~~ 424 425 `out` is calculated based on RSA-FDH from {{rsa-fdh}} as follows: 426 427 ~~~ 428 data = RSA-FDH(msg, pubkey) 429 r = RSA-FDH-Derive(bks, pubkey) 430 r_e = r ** pubkey.e (mod pubkey.N) 431 out = r_e * data (mod pubkey.N) 432 ~~~ 433 434 #### Signing 435 436 ~~~ 437 RSA-FDH-Sign(data, privkey) -> sig 438 439 Inputs: 440 data data to be signed, an integer smaller than privkey.N 441 privkey RSA private key consisting of modulus N and private exponent d 442 443 Output: 444 sig signature on data by privkey 445 ~~~ 446 447 `sig` is calculated as follows: 448 449 ~~~ 450 sig = data ** privkey.d (mod privkey.N) 451 ~~~ 452 453 #### Unblinding 454 455 ~~~ 456 RSA-FDH-Unblind(sig, bks, pubkey) -> out 457 458 Inputs: 459 sig blind signature 460 bks blinding key secret of length L = 32 octets 461 pubkey RSA public key consisting of modulus N and public exponent e 462 463 Output: 464 out unblinded signature 465 ~~~ 466 467 `out` is calculated as follows: 468 469 ~~~ 470 r = RSA-FDH-Derive(bks, pubkey) 471 r_inv = inverse of r (mod pubkey.N) 472 out = sig * r_inv (mod pubkey.N) 473 ~~~ 474 475 #### Verifying 476 477 ~~~ 478 RSA-FDH-Verify(msg, sig, pubkey) -> out 479 480 Inputs: 481 msg message 482 sig signature of pubkey over msg 483 pubkey RSA public key consisting of modulus N and public exponent e 484 485 Output: 486 out true, if sig is a valid signature 487 ~~~ 488 489 `out` is calculated based on RSA-FDH from {{rsa-fdh}} as follows: 490 491 ~~~ 492 data = RSA-FDH(msg, pubkey) 493 exp = sig ** pubkey.e (mod pubkey.N) 494 out = (data == exp) 495 ~~~ 496 497 ### Clause-Schnorr 498 499 # Datatypes and Notation 500 501 ## Amounts {#amounts} 502 503 Amounts are represented in Taler as positive fixed-point values 504 consisting of `value` as the non-negative integer part of the base currency, 505 the `fraction` given in units of one hundred millionth (1e-8) of the base currency, 506 and `currency` as the 3-11 ASCII characters identifying the currency. 507 508 Whenever used in the protocol, the binary representation of an `amount` is 509 `uint64(amount.value) | uint32(amount.fraction) | padZero(12, amount.currency)`. 510 511 ## Timestamps 512 513 Absolute timestamps are represented as `uint64(x)` where `x` corresponds to 514 the microseconds since `1970-01-01 00:00 CEST` (the UNIX epoch). 515 The special value `0xFFFFFFFFFFFFFFFF` represents "never". 516 <!-- 517 // todo: check if needed and correct 518 Relative timestamps are represented as `uint64(x)` where `x` is given in microseconds. 519 The special value `0xFFFFFFFFFFFFFFFF` represents "forever". 520 --> 521 522 ## Signatures 523 524 All messages to be signed in Taler start with a header containing their size and 525 a fixed signing context (purpose) as registered by GANA in the 526 [GNUnet Signature Purposes](https://gana.gnunet.org/gnunet-signatures/gnunet_signatures.html) 527 registry. Taler-related purposes start at 1000. 528 529 ~~~ 530 Gen-Msg(purpose, msg) -> out 531 532 Inputs: 533 purpose signature purpose as registered at GANA 534 msg message content (excl. header) to be signed 535 536 Output: 537 out complete message (incl. header) to be signed 538 ~~~ 539 540 `out` is formed as follows: 541 542 ~~~ 543 out = uint32(len(msg)) | uint32(purpose) | msg 544 ~~~ 545 546 ## Helper Functions 547 548 There are a certain number of single-argument functions which are often needed, 549 and therefore omit the parentheses of the typical function syntax: 550 551 - `Knows data` specifies `data` that is known a priori at the start of the protocol operation 552 - `Check cond` verifies that the boolean condition or variable `cond` is true, 553 or aborts the protocol operation otherwise 554 - `Persist data` persists the given `data` to the local database 555 - `data = Lookup by key` retrieves previously persisted `data` by the given `key` 556 - `Sum ⟨dataᵢ⟩` is valid for numerical objects `dataᵢ` including amounts (cf. {{amounts}}), 557 and denotes the numerical sum of these objects 558 559 Some more functions that are commonly used throughout {{protocol}}: 560 561 ~~~ 562 Hash-Denom(denom) = 563 SHA-512(uint32(0) | uint32(1) | denom.pub) 564 565 Hash-Planchet(planchet, denom) = 566 SHA-512( SHA-512( denom.pub ) | uint32(0x1) | planchet ) 567 568 Check-Subtract(value, subtrahend) = 569 Check value >= subtrahend 570 Persist value -= subtrahend 571 ~~~ 572 573 # The Taler Crypto Protocol {#protocol} 574 575 // todo: briefly introduce the three components wallet, exchange, merchant; maybe with ASCII diagram version 576 577 // todo: capitalize wallet, exchange, merchant? 578 579 ## Withdrawal {#withdrawal} 580 581 The wallet generates `n > 0` coins `⟨coinᵢ⟩` and requests `n` signatures `⟨blind_sigᵢ⟩` from the exchange, 582 attributing value to the coins according to `n` chosen denominations `⟨denomᵢ⟩`. 583 The total value and withdrawal fee (defined by the exchange per denomination) 584 must be smaller or equal to the amount stored in the single reserve used for withdrawal. 585 586 // todo: document TALER_MAX_COINS = 64 per operation (due to CS-encoding) 587 588 // todo: extend with extra roundtrip for CBS 589 590 ~~~ 591 wallet exchange 592 Knows ⟨denomᵢ⟩ Knows ⟨denomᵢ.priv⟩ 593 | | 594 +-----------------------------+ | 595 | (W1) reserve key generation | | 596 +-----------------------------+ | 597 | | 598 |----------- (bank transfer) ----------->| 599 | (subject: reserve.pub, amount: value) | 600 | | 601 | +------------------------------+ 602 | | Persist (reserve.pub, value) | 603 | +------------------------------+ 604 | | 605 +-----------------------------------+ | 606 | (W2) coin generation and blinding | | 607 +-----------------------------------+ | 608 | | 609 |-------------- /withdraw -------------->| 610 | (reserve.pub, planchets, sig) | 611 | | 612 | +--------------------------------+ 613 | | (E1) coin issuance and signing | 614 | +--------------------------------+ 615 | | 616 |<---------- (⟨blind_sigᵢ⟩) -------------| 617 | | 618 +----------------------+ | 619 | (W3) coin unblinding | | 620 +----------------------+ | 621 | | 622 ~~~ 623 624 where (for RSA, without age-restriction) 625 626 ~~~ 627 (W1) reserve key generation (wallet) 628 629 reserve = Ed25519-Keygen() 630 Persist (reserve, value) 631 ~~~ 632 633 The wallet derives coins and blinding secrets using a HKDF from a single seed per withdrawal operation, 634 together with an integer index. 635 This is strictly speaking an implementation detail since the seed is never revealed to any other party, 636 and might be chosen to be implemented differently. 637 638 // todo: blind_secret/coin.priv differently generated in TALER_EXCHANGE_post_withdraw_start/prepare_coins, double check with wallet-core (probably implementation detail here) 639 640 ~~~ 641 (W2) coin generation and blinding (wallet) 642 643 batch_seed = random(256) 644 Persist batch_seed 645 for i in 0..n: 646 coin_seedᵢ = HKDF(uint32(i), batch_seed, "taler-withdrawal-coin-derivation", 64) 647 blind_secretᵢ = coin_seedᵢ[32:] 648 coinᵢ.priv = coin_seedᵢ[:32] 649 coinᵢ.pub = Ed25519-GetPub(coinᵢ.priv) 650 h_denomᵢ = Hash-Denom(denomᵢ) 651 planchetᵢ = RSA-FDH-Blind(SHA-512(coinᵢ.pub), blind_secretᵢ, denomᵢ.pub) 652 h_planchetᵢ = Hash-Planchet(planchetᵢ, denomᵢ) 653 planchets = (⟨h_denomᵢ⟩, ⟨planchetᵢ⟩) 654 msg = Gen-Msg(WALLET_RESERVE_WITHDRAW, 655 ( Sum ⟨denomᵢ.value⟩ | Sum ⟨denomᵢ.fee_withdraw⟩ 656 | SHA-512( ⟨h_planchetᵢ⟩ ) | uint256(0x0) | uint32(0x0) | uint32(0x0) )) 657 sig = Ed25519-Sign(reserve.priv, msg) 658 ~~~ 659 660 ~~~ 661 (E1) coin issuance and signing (exchange) 662 663 (⟨h_denomᵢ⟩, ⟨planchetᵢ⟩) = planchets 664 for i in 0..n: 665 denomᵢ = Lookup by h_denomᵢ 666 Check denomᵢ known and not withdraw-expired 667 h_planchetᵢ = Hash-Planchet(planchetᵢ, denomᵢ) 668 msg = Gen-Msg(WALLET_RESERVE_WITHDRAW, 669 ( Sum ⟨denomᵢ.value⟩ | Sum ⟨denomᵢ.fee_withdraw⟩ 670 | SHA-512( ⟨h_planchetᵢ⟩ ) | uint256(0x0) | uint32(0x0) | uint32(0x0) )) 671 Check Ed25519-Verify(reserve.pub, msg, sig) 672 Check reserve KYC status ok or not needed 673 total = Sum ⟨denomᵢ.value⟩ + Sum ⟨denomᵢ.fee_withdraw⟩ 674 Check-Subtract(reserve.balance, total) 675 for i in 0..n: 676 blind_sigᵢ = RSA-FDH-Sign(planchetᵢ, denomᵢ.priv) 677 Persist withdrawal // todo: what exactly? should be checked first for replay? 678 ~~~ 679 680 ~~~ 681 (W3) coin unblinding (wallet) 682 683 for i in 0..n: 684 coinᵢ.sig = RSA-FDH-Unblind(blind_sigᵢ, blind_secretᵢ, denomᵢ.pub) 685 Check RSA-FDH-Verify(SHA-512(coinᵢ.pub), coinᵢ.sig, denomᵢ.pub) 686 coinᵢ.h_denom = h_denomᵢ 687 coinᵢ.blind_secret = blind_secretᵢ // todo: why save blind_secret, if batch_seed already persisted? 688 Persist ⟨coinᵢ⟩ 689 ~~~ 690 691 ## Payment {#payment} 692 693 The wallet obtains `contract` information for an `order` from the merchant 694 after claiming it with a `nonce`. 695 Payment of the order is prepared by signing (partial) deposit authorizations `⟨depositᵢ⟩` with coins `⟨coinᵢ⟩` of certain denominations `⟨denomᵢ⟩`, 696 where the sum of all contributions (`contributionᵢ <= denomᵢ.value`) must match the `contract.price` plus potential deposit fees. 697 The payment is complete as soon as the merchant successfully redeems the deposit authorizations at the exchange (cf. {{deposit}}). 698 699 ~~~ 700 wallet merchant 701 Knows ⟨coinᵢ⟩ Knows merchant.priv 702 Knows exchange, payto 703 | | 704 | +-----------------------+ 705 | | (M1) order generation | 706 | +-----------------------+ 707 | | 708 |<------- (QR-Code / NFC / URI) ---------| 709 | (order.{id,token?}) | 710 | | 711 +-----------------------+ | 712 | (W1) nonce generation | | 713 +-----------------------+ | 714 | | 715 |------- /orders/{order.id}/claim ------>| 716 | (nonce.pub, order.token?) | 717 | | 718 | +--------------------------+ 719 | | (M2) contract generation | 720 | +--------------------------+ 721 | | 722 |<---- (contract, merchant.pub, sig) ----| 723 | | 724 +--------------------------+ | 725 | (W2) payment preparation | | 726 +--------------------------+ | 727 | | 728 |------- /orders/{order.id}/pay -------->| 729 | (⟨depositᵢ⟩) | 730 | | 731 | +--------------------+ 732 | | (M3) deposit check | 733 | +--------------------+ 734 | | 735 |<--------------- (sig) -----------------| 736 | | 737 +---------------------------+ | 738 | (W3) payment verification | | 739 +---------------------------+ | 740 | | 741 ~~~ 742 743 where (without age restriction, policy and wallet data hash) 744 745 ~~~ 746 (M1) order generation (merchant) 747 748 wire_salt = random(128) 749 determine id, price, info, token? 750 Persist order = (id, price, info, token?, wire_salt) 751 ~~~ 752 753 ~~~ 754 (W1) nonce generation (wallet) 755 756 nonce = Ed25519-Keygen() 757 Persist nonce.priv 758 ~~~ 759 760 Note that the private key of `nonce` is currently not used anywhere in the protocol. 761 However, it could be used in the future to prove ownership of an order transaction, 762 enabling use-cases such as "unclaiming" or transferring an order to another person, 763 or proving the payment without resorting to the individual coins. 764 765 ~~~ 766 (M2) contract generation (merchant) 767 768 Check order.token? == token? 769 h_wire = HKDF(wire_salt, payto, "merchant-wire-signature", 64) 770 determine timestamp, refund_deadline, wire_deadline 771 contract = (order.{id,price,info,token?}, exchange, h_wire, timestamp, refund_deadline, wire_deadline) 772 contract.nonce = nonce.pub 773 Persist contract 774 h_contract = SHA-512(canonicalJSON(contract)) 775 msg = Gen-Msg(MERCHANT_CONTRACT, h_contract) 776 sig = Ed25519-Sign(merchant.priv, msg) 777 ~~~ 778 779 ~~~ 780 (W2) payment preparation (wallet) 781 782 h_contract = SHA-512(canonicalJSON(contract)) 783 msg = Gen-Msg(MERCHANT_CONTRACT, h_contract) 784 Check Ed25519-Verify(merchant.pub, msg, sig) 785 Check contract.nonce == nonce 786 // TODO: double-check extra hash check? 787 // todo: maybe get rid of CoinSelection altogether by claiming we already know coinᵢ and contributionᵢ 788 ⟨selectionᵢ⟩ = CoinSelection(contract.{exchange,price}) TODO: include MarkDirty here 789 for i in 0..n: 790 (coinᵢ, denomᵢ, contributionᵢ) = selectionᵢ 791 msgᵢ = Gen-Msg(WALLET_COIN_DEPOSIT, 792 ( h_contract | uint256(0x0) 793 | uint512(0x0) | contract.h_wire | coinᵢ.h_denom 794 | contract.timestamp | contract.refund_deadline 795 | contributionᵢ + denomᵢ.fee_deposit 796 | denomᵢ.fee_deposit | merchant.pub | uint512(0x0) )) 797 sigᵢ = Ed25519-Sign(coinᵢ.priv, msgᵢ) 798 depositᵢ = (coinᵢ.{pub,sig,h_denom}, contributionᵢ, sigᵢ) 799 Persist (contract, ⟨sigᵢ⟩, ⟨depositᵢ⟩) 800 ~~~ 801 802 // TODO: explain CoinSelection 803 804 // TODO: maybe introduce symbol for pub/priv 805 806 ~~~ 807 (M3) deposit check (merchant) 808 809 Check Sum ⟨depositᵢ.contribution⟩ == contract.price 810 Check Deposit(⟨depositᵢ⟩) 811 msg = Gen-Msg(MERCHANT_PAYMENT_OK, h_contract) 812 sig = Ed25519-Sign(merchant.priv, msg) 813 ~~~ 814 815 ~~~ 816 (W3) payment verification (wallet) 817 818 msg = Gen-Msg(MERCHANT_PAYMENT_OK, h_contract) 819 Check Ed25519-Verify(merchant.pub, msg, sig) 820 ~~~ 821 822 ## Deposit {#deposit} 823 824 // todo: add introductory text 825 826 Deposit could also be used directly by a wallet with its own payto and a minimal contract. 827 828 ~~~ 829 merchant exchange 830 Knows exchange.pub Knows exchange.priv 831 Knows merchant.priv Knows ⟨denomᵢ⟩ 832 Knows payto, wire_salt | 833 Knows contract, ⟨depositᵢ⟩ | 834 | | 835 +--------------------------+ | 836 | (M1) deposit preparation | | 837 +--------------------------+ | 838 | | 839 |----------- /batch-deposit ------------>| 840 | (info, h_contract, ⟨depositᵢ⟩ | 841 | merchant.pub, sig) | 842 | | 843 | +-------------------------+ 844 | | (E1) deposit validation | 845 | +-------------------------+ 846 | | 847 |<--- (timestamp, exchange.pub, sig) ----| 848 | | 849 +---------------------------+ | 850 | (M2) deposit verification | | 851 +---------------------------+ | 852 | | 853 ~~~ 854 855 where (without age restriction, policy and wallet data hash) 856 857 ~~~ 858 (M1) Deposit preparation (merchant) 859 860 info.time = contract.{timestamp, wire_deadline, refund_deadline} 861 info.wire = (payto, wire_salt) 862 h_contract = SHA-512(canonicalJSON(contract)) 863 msg = Gen-Msg(MERCHANT_CONTRACT, h_contract) 864 sig = Ed25519-Sign(merchant.priv, msg) 865 ~~~ 866 867 ~~~ 868 (E1) Deposit validation (exchange) 869 870 h_wire = HKDF(info.wire.wire_salt, info.wire.payto, "merchant-wire-signature", 64) 871 for i in 0..n: 872 coinᵢ = depositᵢ.coin 873 denomᵢ = Lookup by coinᵢ.h_denom 874 Check denomᵢ known and not deposit-expired 875 totalᵢ = depositᵢ.contribution + denomᵢ.fee_deposit 876 msgᵢ = Gen-Msg(WALLET_COIN_DEPOSIT, 877 ( h_contract | uint256(0x0) 878 | uint512(0x0) | h_wire | coinᵢ.h_denom 879 | info.time.timestamp | info.time.refund_deadline 880 | totalᵢ 881 | denomᵢ.fee_deposit | merchant.pub | uint512(0x0) )) 882 Check Ed25519-Verify(coinᵢ.pub, msgᵢ, depositᵢ.sig) 883 Check RSA-FDH-Verify(SHA-512(coinᵢ.pub), coinᵢ.sig, denomᵢ.pub) 884 Check-Subtract(coinᵢ.value, total) 885 Persist deposit-record 886 schedule bank transfer to payto 887 timestamp = now() 888 msg = Gen-Msg(EXCHANGE_CONFIRM_DEPOSIT, 889 ( h_contract | h_wire | uint512(0x0) 890 | timestamp | info.time.wire_deadline 891 | info.time.refund_deadline 892 | Sum ⟨depositᵢ.contribution⟩ 893 | SHA-512( ⟨depositᵢ.sig⟩ ) | merchant.pub )) 894 sig = Ed25519-Sign(exchange.priv, msg) 895 ~~~ 896 897 ~~~ 898 (M2) Deposit verification (merchant) 899 900 h_wire = HKDF(wire_salt, payto, "merchant-wire-signature", 64) 901 msg = Gen-Msg(EXCHANGE_CONFIRM_DEPOSIT, 902 ( h_contract | h_wire | uint512(0x0) 903 | timestamp | contract.wire_deadline 904 | contract.refund_deadline 905 | Sum ⟨depositᵢ.contribution⟩ 906 | SHA-512( ⟨depositᵢ.sig⟩ ) | merchant.pub )) 907 Check Ed25519-Verify(exchange.pub, msg, sig) 908 ~~~ 909 910 ## Refresh {#refresh} 911 912 The wallet obtains `n` new coins `⟨coinᵢ⟩` of denominations `⟨denomᵢ⟩` 913 in exchange for one old `coin` of denomination `denom` from the exchange. 914 There are two reasons why a wallet needs to do this: 915 916 1. Obtaining unlinkable change after using only a part of the coin's value during a payment (cf. {{payment}}) or deposit (cf. {{deposit}}), 917 i.e. where `contribution <= denom.value` 918 2. Renewing a coin before it deposit-expires. 919 920 The sum of the refresh fee of `denom` and the new denominations' values and withdrawal fees (defined by the exchange) 921 must be smaller or equal to the residual value of the old `coin`. 922 923 The private key of each new coin candidate `⟨coinₖᵢ.priv⟩` is transitively derived from the old coin's private key `coin.priv` 924 via a 512-bit secret `⟨sharedₖᵢ⟩` according to `Refresh-Derive`. 925 The secret is regeneratable with the knowledge of `coin.priv` via the link protocol (cf. {{link}}). 926 The derivation ensures that ownership of coins (knowledge of the private key) is correctly transferred, 927 and thereby that value transfer among untrusted parties can only happen via payment and deposit, not via refresh. 928 929 ~~~ 930 Refresh-Derive(shared, denom) = 931 planchet_seed = HKDF(uint32(i), shared, "taler-coin-derivation", 64) 932 blind_secret = HKDF("bks", planchet_seed, "", 32) 933 coin.priv = HKDF("coin", planchet_seed, "", 32) 934 coin.pub = Ed25519-GetPub(coin.priv) 935 planchet = RSA-FDH-Blind(SHA-512(coin.pub), blind_secret, denomᵢ.pub) 936 h_planchet = Hash-Planchet(planchet, denomᵢ) 937 return (coin, blind_secret, planchet, h_planchet) 938 ~~~ 939 940 Taler uses a cut-and-choose protocol with the fixed parameter `κ=3` to enforce correct derivation 941 of `⟨sharedₖᵢ⟩` from a single seed per batch of planchets `⟨batch_seedₖ⟩` 942 (in (κ-1)/κ of the cases, making income concealment for tax evasion purposes unpractical). 943 944 Refreshing consists of two parts: 945 946 1. Melting of the old coin and commiting to κ batches of blinded planchet candidates 947 2. Revelation of κ-1 secrets `⟨revealed_seedₖ⟩` to prove the proper construction of the (revealed) batches of blinded planchet candidates. 948 949 ~~~ 950 wallet exchange 951 Knows ⟨denomᵢ⟩ Knows ⟨denomᵢ.priv⟩ 952 Knows coin | 953 | | 954 +-------------------+ | 955 | (W1) coin melting | | 956 +-------------------+ | 957 | | 958 |---------------- /melt ---------------->| 959 | (coin.{pub,sig,h_denom}, value, | 960 | refresh_seed, planchets, sig) | 961 | | 962 | +---------------------------------------+ 963 | | (E1) gamma selection and coin signing | 964 | +---------------------------------------+ 965 | | 966 |<------ (ɣ, exchange.pub, sig) ---------| 967 | | 968 +------------------------+ | 969 | (W2) secret revelation | | 970 +------------------------+ | 971 | | 972 |------------ /reveal-melt ------------->| 973 | (commitment, ⟨revealed_seedₖ⟩) | 974 | | 975 | +----------------------------+ 976 | | (E2) commitment validation | 977 | +----------------------------+ 978 | | 979 |<---------- (⟨blind_sigᵢ⟩) -------------| 980 | | 981 +----------------------+ | 982 | (W3) coin unblinding | | 983 +----------------------+ | 984 | | 985 ~~~ 986 987 where (for RSA, without age-restriction) 988 989 {::comment} 990 // see TALER_EXCHANGE_get_melt_data 991 ⟨batch_seedₖ⟩ // see TALER_refresh_expand_seed_to_kappa_batch_seeds 992 ⟨transferₖᵢ.priv⟩ // see TALER_refresh_expand_batch_seed_to_transfer_data 993 // todo: pretty sure ECDH-GetPub and Ed25519-GetPub are equivalent, if not, transferₖᵢ.pub needs to change 994 h_planchetₖᵢ // see TALER_coin_ev_hash 995 h_planchetsₖ // see TALER_wallet_blinded_planchet_details_hash 996 commitment // see TALER_refresh_get_commitment 997 998 ⟨ᵧₖᵢ⟩ 999 {:/} 1000 1001 ~~~ 1002 (W1) coin melting (wallet) 1003 1004 refresh_seed = random(256) 1005 ⟨batch_seedₖ⟩ = HKDF("refresh-batch-seeds", refresh_seed, coin.priv, k*64) 1006 for k in 0..κ: 1007 ⟨transferₖᵢ.priv⟩ = HKDF("refresh-transfer-private-keys", batch_seedₖ, "", n*32) 1008 for i in 0..n: 1009 transferₖᵢ.pub = ECDH-GetPub(transferₖᵢ.priv) 1010 sharedₖᵢ = ECDH-Ed25519-Pub(transferₖᵢ.priv, coin.pub) 1011 (coinₖᵢ, blind_secretₖᵢ, planchetₖᵢ, h_planchetₖᵢ) = Refresh-Derive(sharedₖᵢ, denomᵢ) 1012 h_planchetsₖ = SHA-512( ⟨h_planchetₖᵢ⟩ ) 1013 value = coin.denom.fee_refresh + Sum ⟨denomᵢ.value⟩ + Sum ⟨denomᵢ.fee_withdraw⟩ 1014 commitment = SHA-512( refresh_seed | uint256(0x0) | coin.pub | value 1015 | SHA-512( ⟨h_planchetsₖ⟩ ) ) 1016 for i in 0..n: 1017 h_denomᵢ = Hash-Denom(denomᵢ) 1018 planchets = (⟨h_denomᵢ⟩, ⟨planchetₖᵢ⟩, ⟨transferₖᵢ.pub⟩)) 1019 msg = Gen-Msg(WALLET_COIN_MELT, 1020 ( commitment | coin.h_denom | uint256(0x0) 1021 | value | denom.fee_refresh )) 1022 sig = Ed25519-Sign(coin.priv, msg) 1023 Persist (coin.denom.pub, ...) // todo: double-check 1024 ~~~ 1025 1026 {::comment} 1027 1028 see TEH_handler_melt 1029 1030 ⟨ᵧₖᵢ⟩ 1031 {:/} 1032 1033 ~~~ 1034 (E1) gamma selection and coin signing (exchange) 1035 1036 denom = Lookup by coin.h_denom 1037 Check denom known and not deposit-expired 1038 Check RSA-FDH-Verify(SHA-512(coin.pub), coin.sig, denom.pub) 1039 Check coin.pub known and dirty 1040 (⟨h_denomᵢ⟩, ⟨planchetₖᵢ⟩, ⟨transferₖᵢ.pub⟩)) = planchets 1041 for i in 0..n: 1042 denomᵢ = Lookup by h_denomᵢ 1043 Check denomᵢ known and not withdraw-expired 1044 value' = coin.denom.fee_refresh + Sum ⟨denomᵢ.value⟩ + Sum ⟨denomᵢ.fee_withdraw⟩ 1045 Check value' == value 1046 Check-Subtract(coin.value, value) 1047 for k in 0..κ: 1048 for i in 0..n: 1049 h_planchetₖᵢ = Hash-Planchet(planchetₖᵢ, denomᵢ) 1050 h_planchetsₖ = SHA-512( ⟨h_planchetₖᵢ⟩ ) 1051 commitment = SHA-512( refresh_seed | uint256(0x0) | coin.pub | value 1052 | SHA-512( ⟨h_planchetsₖ⟩ ) ) 1053 msg = Gen-Msg(WALLET_COIN_MELT, 1054 ( commitment | coin.h_denom | uint256(0x0) 1055 | value | denom.fee_refresh )) 1056 Check Ed25519-Verify(coin.pub, msg, sig) 1057 refresh_record = Lookup by commitment 1058 (ɣ, _, _, done, _) = refresh_record 1059 if refresh_record not found: 1060 ɣ = 0..κ at random 1061 for i in 0..n: 1062 blind_sigᵢ = RSA-FDH-Sign(planchetᵧᵢ, denomᵧᵢ.priv) 1063 link_info = (refresh_seed, ⟨transferₖᵢ.pub⟩, ⟨h_denomᵢ⟩, coin_sig) 1064 Persist refresh_record = (commitment, ɣ, ⟨blind_sigᵢ⟩, h_planchetsᵧ, false, link_info) 1065 msg = Gen-Msg(EXCHANGE_CONFIRM_MELT, 1066 ( commitment | uint32(ɣ) )) 1067 sig = Ed25519-Sign(exchange.priv, msg) 1068 ~~~ 1069 1070 {::comment} 1071 1072 // see src/lib/exchange_api_post-melt.c: handle_melt_finished 1073 // see src/lib/exchange_api_post-reveal-melt.c: perform_protocol 1074 1075 ⟨ᵧₖᵢ⟩ 1076 {:/} 1077 1078 ~~~ 1079 (W2) secret revelation (wallet) 1080 1081 Check exchange.pub known 1082 msg = Gen-Msg(EXCHANGE_CONFIRM_MELT, 1083 ( commitment | ɣ )) 1084 Check Ed25519-Verify(exchange.pub, msg, sig) 1085 Persist refresh-challenge // what exactly? 1086 for k in 0..κ and k != ɣ: 1087 revealed_seedₖ = batch_seedₖ 1088 ~~~ 1089 1090 {::comment} 1091 1092 // see TEH_handler_reveal_melt 1093 1094 ⟨ᵧₖᵢ⟩ 1095 {:/} 1096 1097 ~~~ 1098 (E2) commitment validation (exchange) 1099 1100 refresh_record = Lookup by commitment 1101 (ɣ, ⟨blind_sigᵢ⟩, h_planchetsᵧ, done, _) = refresh_record 1102 Check not done // todo: sure? 1103 for k in 0..κ and k != ɣ: 1104 ⟨transferₖᵢ.priv⟩ = HKDF("refresh-transfer-private-keys", batch_seedₖ, "", n*32) 1105 for i in 0..n: 1106 transferₖᵢ.pub = ECDH-GetPub(transferₖᵢ.priv) 1107 sharedₖᵢ = ECDH-Ed25519-Pub(transferₖᵢ.priv, coin.pub) 1108 (_, _, _, h_planchetₖᵢ) = Refresh-Derive(sharedₖᵢ, denomᵢ) 1109 h_planchetsₖ = SHA-512( ⟨h_planchetₖᵢ⟩ ) 1110 value = coin.denom.fee_refresh + Sum ⟨denomᵢ.value⟩ + Sum ⟨denomᵢ.fee_withdraw⟩ 1111 commitment' = SHA-512( refresh_seed | uint256(0x0) | coin.pub | value 1112 | SHA-512( ⟨h_planchetsₖ⟩ ) ) 1113 Check commitment == commitment' 1114 Persist refresh_record = (_, _, _, true, _) 1115 ~~~ 1116 1117 {::comment} 1118 1119 // see src/lib/exchange_api_post-reveal-melt.c: reveal_melt_ok 1120 1121 ⟨ᵧₖᵢ⟩ 1122 {:/} 1123 1124 ~~~ 1125 (W3) coin unblinding (wallet) 1126 1127 for i in 0..n: 1128 coinᵧᵢ.sig = RSA-FDH-Unblind(blind_sigᵧᵢ, blind_secretᵧᵢ, denomᵢ.pub) 1129 Check RSA-FDH-Verify(SHA-512(coinᵧᵢ.pub), coinᵧᵢ.sig, denomᵢ.pub) 1130 coinᵧᵢ.h_denom = h_denomᵢ 1131 Persist ⟨coinᵧᵢ⟩ 1132 ~~~ 1133 1134 ### Link {#link} 1135 1136 // todo: add introductory text 1137 1138 ~~~ 1139 wallet exchange 1140 Knows coin Knows refresh_record for coin 1141 | | 1142 +----------------------+ | 1143 | (W1) history request | | 1144 +----------------------+ | 1145 | | 1146 |------ /coins/{coin.pub}/history ------>| 1147 | (sig) | 1148 | | 1149 | +----------------------------+ 1150 | | (E1) refresh secret lookup | 1151 | +----------------------------+ 1152 | | 1153 |<------------- (melt_info) -------------| 1154 | | 1155 +-----------------------+ | 1156 | (W2) coin acquisition | | 1157 +-----------------------+ | 1158 | | 1159 ~~~ 1160 1161 where (for RSA, without age-restriction) 1162 1163 1164 {::comment} 1165 1166 ⟨ᵧₖᵢ⟩ 1167 {:/} 1168 1169 ~~~ 1170 (W1) history request (wallet) 1171 1172 msg = Gen-Msg(COIN_HISTORY_REQUEST, uint64(0x0)) 1173 sig = Ed25519-Sign(coin.priv, msg) 1174 ~~~ 1175 1176 {::comment} 1177 1178 ⟨ᵧₖᵢ⟩ 1179 {:/} 1180 1181 ~~~ 1182 (E1) refresh secret lookup (exchange) 1183 1184 refresh_record = Lookup by coin.pub 1185 (ɣ, ⟨blind_sigᵢ⟩, _, done, link_info) = refresh_record 1186 if done: 1187 melt_info = (ɣ, link_info, ⟨blind_sigᵢ⟩) 1188 else: 1189 melt_info = (ɣ, link_info) 1190 ~~~ 1191 1192 {::comment} 1193 1194 ⟨ᵧₖᵢ⟩ 1195 {:/} 1196 1197 ~~~ 1198 (W2) coin acquisition (wallet) 1199 1200 (ɣ, link_info, ⟨blind_sigᵢ⟩?) = melt_info 1201 (refresh_seed, ⟨transferₖᵢ.pub⟩, ⟨h_denomᵢ⟩, coin_sig) = link_info 1202 1203 for i in 0..n: 1204 denomᵢ = Lookup by h_denomᵢ 1205 for k in 0..κ: 1206 for i in 0..n: 1207 sharedₖᵢ = ECDH-Ed25519-Priv(coin.priv, transferₖᵢ.pub) 1208 (coinₖᵢ, blind_secretₖᵢ _, h_planchetₖᵢ) = Refresh-Derive(sharedₖᵢ, denomᵢ) 1209 h_planchetsₖ = SHA-512( ⟨h_planchetₖᵢ⟩ ) 1210 value = coin.denom.fee_refresh + Sum ⟨denomᵢ.value⟩ + Sum ⟨denomᵢ.fee_withdraw⟩ 1211 commitment = SHA-512( refresh_seed | uint256(0x0) | coin.pub | value 1212 | SHA-512( ⟨h_planchetsₖ⟩ ) ) 1213 msg = Gen-Msg(WALLET_COIN_MELT, 1214 ( commitment | coin.h_denom | uint256(0x0) 1215 | value | denom.fee_refresh )) 1216 Check Ed25519-Verify(coin.pub, msg, sig) 1217 1218 if ⟨blind_sigᵢ⟩ returned: 1219 for i in 0..n: 1220 coinᵧᵢ.sig = RSA-FDH-Unblind(blind_sigᵧᵢ, blind_secretᵧᵢ, denomᵢ.pub) 1221 Check RSA-FDH-Verify(SHA-512(coinᵧᵢ.pub), coinᵧᵢ.sig, denomᵢ.pub) 1222 coinᵧᵢ.h_denom = h_denomᵢ 1223 Persist ⟨coinᵧᵢ⟩ 1224 ~~~ 1225 1226 ## Refund {#refund} 1227 1228 // todo 1229 1230 ## Recoup {#recoup} 1231 1232 // todo 1233 1234 ## Wallet-to-Wallet Push Payment {#w2w-push} 1235 1236 // todo 1237 1238 ## Wallet-to-Wallet Pull Payment {#w2w-pull} 1239 1240 // todo 1241 1242 # Security Considerations 1243 1244 \[ TBD \] 1245 1246 # IANA Considerations 1247 1248 None. 1249 1250 --- back 1251 1252 # Change log 1253 1254 # Acknowledgments 1255 {:numbered="false"} 1256 1257 \[ TBD \] 1258 1259 This work was supported in part by the German Federal Ministry of 1260 Education and Research (BMBF) within the project Concrete Contracts.