Internet-Draft The GNU Taler Protocol June 2025
Gütschow Expires 15 December 2025 [Page]
Workgroup:
independent
Internet-Draft:
draft-guetschow-taler-protocol
Published:
Intended Status:
Informational
Expires:
Author:
M. Gütschow
TU Dresden

The GNU Taler Protocol

Abstract

[ TBW ]

Status of This Memo

This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.

Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.

Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."

This Internet-Draft will expire on 15 December 2025.

Table of Contents

1. Introduction

[ TBW ]

Beware that this document is still work-in-progress and may contain errors. Use at your own risk!

2. Notation

3. Cryptographic Primitives

3.1. Cryptographic Hash Functions

3.1.1. SHA-256

SHA-256(msg) -> hash

Input:
    msg     input message of length L < 2^61 octets

Output:
    hash    message digest of fixed length HashLen = 32 octets

hash is the output of SHA-256 as per Sections 4.1, 5.1, 6.1, and 6.2 of [RFC6234].

3.1.2. SHA-512

SHA-512(msg) -> hash

Input:
    msg     input message of length L < 2^125 octets

Output:
    hash    message digest of fixed length HashLen = 64 octets

hash is the output of SHA-512 as per Sections 4.2, 5.2, 6.3, and 6.4 of [RFC6234].

3.1.3. SHA-512-256 (truncated SHA-512)

SHA-512(msg) -> hash

Input:
    msg     input message of length L < 2^125 octets

Output:
    hash    message digest of fixed length HashLen = 32 octets

The output hash corresponds to the first 32 octets of the output of SHA-512 defined in Section 3.1.2:

temp = SHA-512(msg)
hash = temp[0:31]

Note that this operation differs from SHA-512/256 as defined in [SHS] in the initial hash value.

3.2. Message Authentication Codes

3.2.1. HMAC

HMAC-Hash(key, text) -> out

Option:
    Hash    cryptographic hash function with output length HashLen

Input:
    key     secret key of length at least HashLen
    text    input data of arbitary length

Output:
    out     output of length HashLen

out is calculated as defined in [RFC2104].

3.3. Key Derivation Functions

3.3.1. HKDF

The Hashed Key Derivation Function (HKDF) used in Taler is an instantiation of [RFC5869] with two different hash functions for the Extract and Expand step as suggested in [HKDF]: HKDF-Extract uses HMAC-SHA512, while HKDF-Expand uses HMAC-SHA256 (cf. Section 3.2.1).

HKDF(salt, IKM, info, L) -> OKM

Inputs:
    salt    optional salt value (a non-secret random value);
              if not provided, it is set to a string of 64 zeros.
    IKM     input keying material
    info    optional context and application specific information
              (can be a zero-length string)
    L       length of output keying material in octets
              (<= 255*32 = 8160)

Output:
    OKM      output keying material (of L octets)

The output OKM is calculated as follows:

PRK = HKDF-Extract(salt, IKM) with Hash = SHA-512 (HashLen = 64)
OKM = HKDF-Expand(PRK, info, L) with Hash = SHA-256 (HashLen = 32)

3.3.2. HKDF-Mod

Based on the HKDF defined in Section 3.3.1, this function returns an OKM that is smaller than a given multiple precision integer N.

HKDF-Mod(N, salt, IKM, info) -> OKM

Inputs:
    N        multiple precision integer
    salt     optional salt value (a non-secret random value);
              if not provided, it is set to a string of 64 zeros.
    IKM      input keying material
    info     optional context and application specific information
              (can be a zero-length string)

Output:
    OKM      output keying material (smaller than N)

The final output OKM is determined deterministically based on a counter initialized at zero.

counter = 0
do until OKM < N:
    x = HKDF(salt, IKM, info | bigEndian(16, counter), bytes(N))
    OKM = bigEndian(bits(N), x)
    counter += 1

3.5. Blind Signatures

3.5.1. RSA-FDH

3.5.1.1. Supporting Functions
RSA-FDH(msg, pubkey) -> fdh

Inputs:
    msg     message
    pubkey  RSA public key consisting of modulus N and public exponent e

Output:
    fdh     full-domain hash of msg over pubkey.N

fdh is calculated based on HKDF-Mod from Section 3.3.2 as follows:

info = "RSA-FDA FTpsW!"
salt = bigEndian(16, bytes(pubkey.N)) | bigEndian(16, bytes(pubkey.e))
     | pubkey.N | pubkey.e
fdh = HKDF-Mod(pubkey.N, salt, msg, info)

The resulting fdh can be used to test against a malicious RSA pubkey by verifying that the greatest common denominator (gcd) of fdh and pubkey.N is 1.

RSA-FDH-Derive(bks, pubkey) -> out

Inputs:
    bks     blinding key secret of length L = 32 octets
    pubkey  RSA public key consisting of modulus N and public exponent e

Output:
    out     full-domain hash of bks over pubkey.N

out is calculated based on HKDF-Mod from Section 3.3.2 as follows:

info = "Blinding KDF"
salt = "Blinding KDF extractor HMAC key"
fdh = HKDF-Mod(pubkey.N, salt, bks, info)
3.5.1.2. Blinding
RSA-FDH-Blind(msg, bks, pubkey) -> out

Inputs:
    msg     message
    bks     blinding key secret of length L = 32 octets
    pubkey  RSA public key consisting of modulus N and public exponent e

Output:
    out     message blinded for pubkey

out is calculated based on RSA-FDH from Section 3.5.1 as follows:

data = RSA-FDH(msg, pubkey)
r = RSA-FDH-Derive(bks, pubkey)
r_e = r ** pubkey.e (mod pubkey.N)
out = r_e * data (mod pubkey.N)
3.5.1.3. Signing
RSA-FDH-Sign(data, privkey) -> sig

Inputs:
    data    data to be signed, an integer smaller than privkey.N
    privkey RSA private key consisting of modulus N and private exponent d

Output:
    sig     signature on data by privkey

sig is calculated as follows:

sig = data ** privkey.d (mod privkey.N)
3.5.1.4. Unblinding
RSA-FDH-Unblind(sig, bks, pubkey) -> out

Inputs:
    sig     blind signature
    bks     blinding key secret of length L = 32 octets
    pubkey  RSA public key consisting of modulus N and public exponent e

Output:
    out     unblinded signature

out is calculated as follows:

r = RSA-FDH-Derive(bks, pubkey)
r_inv = inverse of r (mod pubkey.N)
out = sig * r_inv (mod pubkey.N)
3.5.1.5. Verifying
RSA-FDH-Verify(msg, sig, pubkey) -> out

Inputs:
    msg     message
    sig     signature of pubkey over msg
    pubkey  RSA public key consisting of modulus N and public exponent e

Output:
    out     true, if sig is a valid signature

out is calculated based on RSA-FDH from Section 3.5.1 as follows:

data = RSA-FDH(msg, pubkey)
exp = sig ** pubkey.e (mod pubkey.N)
out = (data == exp)

4. The Taler Crypto Protocol

// todo: explain persist, check

4.1. Withdrawal

The wallet creates n > 0 coins and requests n signatures from the exchange, attributing value to the coins according to n chosen denominations. The total value and withdrawal fee (defined by the exchange per denomination) must be smaller or equal to the amount stored in the single reserve used for withdrawal. The symbol * in front of a certain part means that it is repeated n times for the n coins, where ?* denotes the index number 0 <= ?* < n.

            wallet                                  exchange
knows *denom.pub                        knows *denom.priv
               |                                        |
reserve = EdDSA-Keygen()                                |
persist (reserve, value)                                |
               |                                        |
               |----------- (bank transfer) ----------->|
               | (subject: reserve.pub, amount: value)  |
               |                                        |
               |                        persist (reserve.pub, value)
               |                                        |
master_secret = random(256)                             |
persist master_secret                                   |
*(coin, blind_secret) = GenerateCoin(master_secret, ?*) |
*coin.h_denom = SHA-512(bigEndian(32, 0) | bigEndian(32, 1) | denom.pub)
*blind_coin = RSA-FDH-Blind(SHA-512(coin.pub), blind_secret, denom.pub)
sig0 = EdDSA-Sign(reserve.priv, msg0)                   |
               |                                        |
               |-------------- /withdraw -------------->|
               |      (reserve.pub, *coin.h_denom,      |
               |            *blind_coin, sig)           |
               |                                        |
               |                        *denom = Denom-Lookup(coin.h_denom)
               |                        check *denom.pub known and not withdrawal-expired
               |                        check EdDSA-Verify(reserve.pub, msg, sig)
               |                        check reserve KYC status ok or not needed
               |                        check reserve.balance >= sum(*denom.value + *denom.fee_withdraw)
               |                        reserve.balance -= sum(*denom.value + *denom.fee_withdraw)
               |                        *blind_sig = RSA-FDH-Sign(blind_coin, denom.priv)
               |                        persist withdrawal
               |                                        |
               |<------------ *blind_sig ---------------|
               |                                        |
*coin.sig = RSA-FDH-Unblind(blind_sig, blind_secret, denom.pub)
check *RSA-FDH-Verify(SHA-512(coin.pub), coin.sig, denom.pub)
persist *(coin, blind_secret)

where msg0 is formed as follows:

msg0 = bigEndian(32, 160) | bigEndian(32, 1200) /* TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW */
     | bigEndianAmount(sum(*denom.value)) | bigEndianAmount(sum(*denom.fee_withdraw))
     | SHA-512( *(SHA-512(denom.pub) | bigEndian(32, 0x1) | blind_coin) )
     | bigEndian(256, 0x0)
     | bigEndian(32, 0x0) | bigEndian(32, 0x0)

The wallet derives coins and blinding secrets using GenerateCoin from a master secret and an integer index. This is strictly speaking an implementation detail since the master secret is never revealed to any other party, and might be chosen to be implemented differently.

// todo: discuss with Florian: use reserve.priv as master_secret?

GenerateCoin(secret, idx) -> (coin, bks)

Inputs:
    secret  secret to derive coin from
    idx     coin index

Output:
    coin    private-public keypair of the coin
    bks     random blinding key secret of length 32 bytes

coin and blind_secret are calculated as follows:

tmp = HKDF(bigEndian(32, idx), master_secret, "taler-withdrawal-coin-derivation", 64)
(coin.priv, bks) = (tmp[:32], tmp[32:])
coin.pub = EdDSA-GetPub(coin.priv)

(for RSA, without age-restriction)

4.2. Payment

            wallet                                  merchant
knows merchant.pub                      knows merchant.priv
knows valid *coin                       knows exchange, payto
               |                                        |
               |                        wire_salt = random(128)
               |                        persist order = (id, price, info, token?, wire_salt)
               |                                        |
               |<-------- (order.{id,token?}) ----------|
               |                                        |
nonce = EdDSA-Keygen()                                  |
persist nonce.priv                                      |
               |                                        |
               |------- /orders/{order.id}/claim ------>|
               |            (nonce, token?)             |
               |                                        |
               |                        h_wire = HKDF(wire_salt, payto,
               |                                      "merchant-wire-signature", 64)
               |                        determine timestamp, refund_deadline, wire_deadline
               |                        contract = (order.{id,price,info,token?},
               |                                    exchange, h_wire, timestamp,
               |                                    refund_deadline, wire_deadline)
               |                        check contract.token = token?
               |                        contract.nonce = nonce
               |                        persist contract
               |                        sig0 = EdDSA-Sign(merchant.priv, msg0)
               |                                        |
               |<----------- contract, sig0 ------------|
               |                                        |
check EdDSA-Verify(merchant.pub, msg0, sig0)            |
check contract.nonce = nonce                            |
TODO: double-check extra hash check?                    |
*(coin, fraction) = CoinSelection(contract.{exchange,price}) TODO: include MarkDirty here
*sig1 = EdDSA-Sign(*coin.priv, *msg1)                   |
*deposit = *(coin.{pub,sig,h_denom}, fraction, sig1)    |
persist (contract, sig, *deposit)                       |
               |                                        |
               |------- /orders/{order.id}/pay -------->|
               |              (*deposit)                |
               |                                        |
               |                        check sum(*deposit.fraction) = contract.price
               |                        *check Deposit(deposit)
               |                        sig2 = EdDSA-Sign(merchant.priv, msg2)
               |                                        |
               |<---------------- sig2 -----------------|
               |                                        |
check EdDSA-Verify(merchant.pub, msg2, sig2)            |

TODO: discuss - nonce doesn't strictly need to be EdDSA keypair?

where msg0, *msg1, and msg2 are formed as follows:

h_contract = SHA-512(canonicalJSON(contract))

msg0 = bigEndian(32, 72) | bigEndian(32, 1101) /* TALER_SIGNATURE_MERCHANT_CONTRACT */
     | h_contract
*msg1 = bigEndian(32, 456) | bigEndian(32, 1201) /* TALER_SIGNATURE_WALLET_COIN_DEPOSIT */
      | h_contract | bigEndian(256, 0x0)
      | bigEndian(512, 0x0) | contract.h_wire | *coin.h_denom
      | bigEndianTime(contract.timestamp) | bigEndianTime(contract.refund_deadline)
      | bigEndianAmount(*fraction + *denom.fee_deposit)
      | bigEndianAmount(*denom.fee_deposit) | merchant.pub | bigEndian(512, 0x0)
msg2 = bigEndian(32, 72) | bigEndian(32, 1104) /* TALER_SIGNATURE_MERCHANT_PAYMENT_OK */
     | h_contract

(without age restriction, policy and wallet data hash)

4.3. Deposit

       merchant/wallet                              exchange
knows exchange.pub                      knows exchange.priv
knows merchant.pub                      knows *denom.pub
knows payto, wire_salt, sig2, h_contract
knows contract, *deposit from Payment
               |                                        |
               |----------- /batch-deposit ------------>|
               | (contract.{timestamp,wire_deadline,refund_deadline}
               |  h_contract, merchant.pub, sig2,       |
               |  payto, *deposit, wire_salt)           |
               |                                        |
               |                        *denom = Denom-Lookup(*deposit.coin.h_denom)
               |                        check *denom.pub known and not deposit-expired
               |                        contract.h_wire = HKDF(wire_salt, payto,
               |                                               "merchant-wire-signature", 64)
               |                        check *EdDSA-Verify(deposit.coin.pub, msg1, sig1)
               |                        check *RSA-FDH-Verify(SHA-512(deposit.coin.pub),
               |                                              deposit.coin.sig, denom.pub)
               |                        check not overspending
               |                        persist deposit-record, mark-spent
               |                        schedule bank transfer
               |                        exchange_timestamp = now()
               |                        sig3 = EdDSA-Sign(exchange.priv, msg3)
               |                                        |
               |<----- sig3, exchange_timestamp --------|
               |                                        |
check EdDSA-Verify(exchange.pub, msg3, sig3)            |

where msg3 is formed as follows:

msg3 = bigEndian(32, 344) | bigEndian(32, 1033) /* TALER_SIGNATURE_EXCHANGE_CONFIRM_DEPOSIT */
     | h_contract | contract.h_wire | bigEndian(512, 0x0)
     | bigEndianTime(exchange_timestamp) | bigEndianTime(contract.wire_deadline)
     | bigEndianTime(contract.refund_deadline)
     | bigEndianAmount(sum(*deposit.fraction - *denom.fee_deposit))
     | SHA-512(*deposit.sig1) | merchant.pub

5. Security Considerations

[ TBD ]

6. IANA Considerations

None.

7. Normative References

[HKDF]
Krawczyk, H., "Cryptographic Extraction and Key Derivation: The HKDF Scheme", Springer Berlin Heidelberg, Lecture Notes in Computer Science pp. 631-648, DOI 10.1007/978-3-642-14623-7_34, ISBN ["9783642146220", "9783642146237"], , <https://doi.org/10.1007/978-3-642-14623-7_34>.
[RFC20]
Cerf, V., "ASCII format for network interchange", STD 80, RFC 20, DOI 10.17487/RFC0020, , <https://www.rfc-editor.org/rfc/rfc20>.
[RFC2104]
Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed-Hashing for Message Authentication", RFC 2104, DOI 10.17487/RFC2104, , <https://www.rfc-editor.org/rfc/rfc2104>.
[RFC5869]
Krawczyk, H. and P. Eronen, "HMAC-based Extract-and-Expand Key Derivation Function (HKDF)", RFC 5869, DOI 10.17487/RFC5869, , <https://www.rfc-editor.org/rfc/rfc5869>.
[RFC6234]
Eastlake 3rd, D. and T. Hansen, "US Secure Hash Algorithms (SHA and SHA-based HMAC and HKDF)", RFC 6234, DOI 10.17487/RFC6234, , <https://www.rfc-editor.org/rfc/rfc6234>.
[SHS]
"Secure hash standard", National Institute of Standards and Technology (U.S.), DOI 10.6028/nist.fips.180-4, , <https://doi.org/10.6028/nist.fips.180-4>.

Appendix A. Change log

Acknowledgments

[ TBD ]

This work was supported in part by the German Federal Ministry of Education and Research (BMBF) within the project Concrete Contracts.

Author's Address

Mikolai Gütschow
TUD Dresden University of Technology
Helmholtzstr. 10
D-01069 Dresden
Germany