merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

merchant-0048.sql (9206B)


      1 --
      2 -- This file is part of TALER
      3 -- Copyright (C) 2026 Taler Systems SA
      4 --
      5 -- TALER is free software; you can redistribute it and/or modify it under the
      6 -- terms of the GNU General Public License as published by the Free Software
      7 -- Foundation; either version 3, or (at your option) any later version.
      8 --
      9 -- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10 -- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11 -- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 --
     13 -- You should have received a copy of the GNU General Public License along with
     14 -- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 --
     16 
     17 -- @file merchant-0048.sql
     18 -- @brief Add DD97 challenge confirmations and DD98 token fountains.
     19 -- @author Bohdan Potuzhnyi
     20 
     21 BEGIN;
     22 
     23 SELECT _v.register_patch('merchant-0048', NULL, NULL);
     24 
     25 SET search_path TO merchant;
     26 
     27 CREATE PROCEDURE merchant.merchant_0048_init(s TEXT)
     28   LANGUAGE plpgsql
     29   AS $OUTER$
     30 BEGIN
     31   EXECUTE format('SET LOCAL search_path TO %I', s);
     32 
     33   -- For the challenge-signature algorithms the backend generates the
     34   -- key pair itself: otp_key then holds the private key and only
     35   -- otp_device_pub is ever handed back to the merchant.
     36   ALTER TABLE merchant_otp_devices
     37     ADD COLUMN otp_device_pub TEXT DEFAULT NULL;
     38   COMMENT ON COLUMN merchant_otp_devices.otp_device_pub
     39     IS 'Crockford base32-encoded public key of a challenge-signature device, NULL for the TOTP algorithms. The matching private key is kept in otp_key and never leaves the backend.';
     40 
     41   -- The challenge is chosen by the offline verifier, travels through
     42   -- the wallet when the template is instantiated, and is signed once
     43   -- the order is paid.
     44   ALTER TABLE merchant_orders
     45     ADD COLUMN pos_challenge BYTEA CHECK(LENGTH(pos_challenge)=32) DEFAULT NULL;
     46   COMMENT ON COLUMN merchant_orders.pos_challenge
     47     IS 'Challenge to sign when pos_algorithm is a challenge-signature algorithm, NULL otherwise';
     48 
     49   ALTER TABLE merchant_contract_terms
     50     ADD COLUMN pos_challenge BYTEA CHECK(LENGTH(pos_challenge)=32) DEFAULT NULL;
     51   COMMENT ON COLUMN merchant_contract_terms.pos_challenge
     52     IS 'Challenge to sign when pos_algorithm is a challenge-signature algorithm, NULL otherwise';
     53 
     54 
     55   CREATE TABLE merchant_fountains (
     56     fountain_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
     57     fountain_id TEXT NOT NULL UNIQUE,
     58     h_fountain_secret BYTEA NOT NULL UNIQUE,
     59     description TEXT NOT NULL,
     60     poll_freq INT8 NOT NULL,
     61     CONSTRAINT merchant_fountains_h_fountain_secret_check
     62       CHECK ((LENGTH(h_fountain_secret) = 64))
     63   );
     64   COMMENT ON TABLE merchant_fountains IS
     65     'Token fountains: bearer credentials that entitle wallets to'
     66     ' withdraw blind-signed promotional tokens.';
     67   COMMENT ON COLUMN merchant_fountains.fountain_id IS
     68     'Public identifier of the fountain, included in the wallet'
     69     ' onboarding URI.';
     70   COMMENT ON COLUMN merchant_fountains.h_fountain_secret IS
     71     'Hash of the bearer credential; the secret itself is never stored.';
     72   COMMENT ON COLUMN merchant_fountains.description IS
     73     'Human-readable description, preferably an opaque campaign or'
     74     ' recipient reference of the institution.';
     75   COMMENT ON COLUMN merchant_fountains.poll_freq IS
     76     'How often wallets should re-poll the fountain information.';
     77 
     78   CREATE TABLE merchant_fountain_grants (
     79     fountain_grant_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
     80     fountain_serial INT8 NOT NULL,
     81     token_family_serial INT8 NOT NULL,
     82     tokens_per_period_limit INT8 NOT NULL,
     83     tokens_per_period_stash INT8 NOT NULL,
     84     key_window_size INT8 NOT NULL,
     85     CONSTRAINT merchant_fountain_grants_limit_check
     86       CHECK ((tokens_per_period_limit >= 0)),
     87     CONSTRAINT merchant_fountain_grants_stash_check
     88       CHECK ((tokens_per_period_stash BETWEEN 0 AND tokens_per_period_limit)),
     89     CONSTRAINT merchant_fountain_grants_window_check
     90       CHECK ((key_window_size >= 0)),
     91     CONSTRAINT merchant_fountain_grants_unique
     92       UNIQUE (fountain_serial, token_family_serial),
     93     CONSTRAINT merchant_fountain_grants_fountain_serial_fkey
     94       FOREIGN KEY (fountain_serial)
     95       REFERENCES merchant_fountains(fountain_serial) ON DELETE CASCADE,
     96     CONSTRAINT merchant_fountain_grants_token_family_serial_fkey
     97       FOREIGN KEY (token_family_serial)
     98       REFERENCES merchant_token_families(token_family_serial) ON DELETE CASCADE
     99   );
    100   COMMENT ON TABLE merchant_fountain_grants IS
    101     'Withdrawal rights of a fountain: which token families may be'
    102     ' withdrawn and at what rate.';
    103   COMMENT ON COLUMN merchant_fountain_grants.tokens_per_period_limit IS
    104     'Maximum number of tokens blind-signed per issue-key validity'
    105     ' period for this token family.';
    106   COMMENT ON COLUMN merchant_fountain_grants.tokens_per_period_stash IS
    107     'Number of tokens the wallet should aim to hold per period; a'
    108     ' client-side stocking target, at most tokens_per_period_limit.';
    109   COMMENT ON COLUMN merchant_fountain_grants.key_window_size IS
    110     'Number of issue-key slots ahead of the current one for which the'
    111     ' wallet may withdraw tokens.';
    112 
    113   CREATE TABLE merchant_fountain_withdrawals (
    114     fountain_withdrawal_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    115     fountain_serial INT8 NOT NULL,
    116     token_family_serial INT8 NOT NULL,
    117     slot_start INT8 NOT NULL,
    118     num_withdrawn INT8 DEFAULT 0 NOT NULL,
    119     CONSTRAINT merchant_fountain_withdrawals_unique
    120       UNIQUE (fountain_serial, token_family_serial, slot_start),
    121     CONSTRAINT merchant_fountain_withdrawals_fountain_serial_fkey
    122       FOREIGN KEY (fountain_serial)
    123       REFERENCES merchant_fountains(fountain_serial) ON DELETE CASCADE,
    124     CONSTRAINT merchant_fountain_withdrawals_token_family_serial_fkey
    125       FOREIGN KEY (token_family_serial)
    126       REFERENCES merchant_token_families(token_family_serial) ON DELETE CASCADE
    127   );
    128   COMMENT ON TABLE merchant_fountain_withdrawals IS
    129     'Per-period withdrawal counters enforcing the limits of the'
    130     ' fountain grants. Deliberately not referencing'
    131     ' merchant_fountain_grants: replacing the grants of a fountain'
    132     ' must not reset the quota already consumed.';
    133   COMMENT ON COLUMN merchant_fountain_withdrawals.slot_start IS
    134     'signature_validity_start of the issue-key validity period this'
    135     ' counter accounts for.';
    136   COMMENT ON COLUMN merchant_fountain_withdrawals.num_withdrawn IS
    137     'Number of tokens already blind-signed for this fountain, token'
    138     ' family and validity period.';
    139 
    140   CREATE TABLE merchant_fountain_withdraw_sigs (
    141     fountain_serial INT8 NOT NULL,
    142     h_request BYTEA NOT NULL,
    143     grant_index INT4 NOT NULL,
    144     token_index INT4 NOT NULL,
    145     token_family_slug TEXT NOT NULL,
    146     h_issue BYTEA NOT NULL CHECK (LENGTH(h_issue) = 64),
    147     signature_validity_end INT8 NOT NULL,
    148     token_blinded_signature BYTEA NOT NULL,
    149     CONSTRAINT merchant_fountain_withdraw_sigs_h_request_check
    150       CHECK ((LENGTH(h_request) = 64)),
    151     CONSTRAINT merchant_fountain_withdraw_sigs_pkey
    152       PRIMARY KEY (fountain_serial, h_request, grant_index, token_index),
    153     CONSTRAINT merchant_fountain_withdraw_sigs_fountain_serial_fkey
    154       FOREIGN KEY (fountain_serial)
    155       REFERENCES merchant_fountains(fountain_serial) ON DELETE CASCADE
    156   );
    157   COMMENT ON TABLE merchant_fountain_withdraw_sigs IS
    158     'Blind signatures handed out by a completed withdrawal, committed'
    159     ' together with the quota consumption and the issued tokens. A'
    160     ' repeated request is answered by rebuilding its response from'
    161     ' these rows instead of consuming quota a second time.';
    162   COMMENT ON COLUMN merchant_fountain_withdraw_sigs.h_request IS
    163     'Hash of the canonical JSON of the grants array of the request.'
    164     ' Neither the bearer credential nor unblinded token data is'
    165     ' stored.';
    166   COMMENT ON COLUMN merchant_fountain_withdraw_sigs.grant_index IS
    167     'Offset of the grant this signature belongs to in the request,'
    168     ' and hence in the response.';
    169   COMMENT ON COLUMN merchant_fountain_withdraw_sigs.token_index IS
    170     'Offset of the signature within the token_sigs array of that'
    171     ' grant, matching the order of the submitted envelopes.';
    172   COMMENT ON COLUMN merchant_fountain_withdraw_sigs.token_family_slug IS
    173     'Token family slug returned in the original withdrawal response.';
    174   COMMENT ON COLUMN merchant_fountain_withdraw_sigs.h_issue IS
    175     'Issue-key hash returned in the original withdrawal response.';
    176   COMMENT ON COLUMN merchant_fountain_withdraw_sigs.signature_validity_end IS
    177     'Issue-key expiry at withdrawal time. GC retains the complete request'
    178     ' until the latest expiry, independently of family or key deletion.';
    179 
    180   ALTER TABLE merchant_issued_tokens
    181     ALTER COLUMN h_contract_terms DROP NOT NULL;
    182   COMMENT ON COLUMN merchant_issued_tokens.h_contract_terms IS
    183     'This is no foreign key by design. NULL for tokens issued via a'
    184     ' fountain instead of as an output of an order.';
    185 
    186   SET LOCAL search_path TO merchant;
    187 END
    188 $OUTER$;
    189 
    190 INSERT INTO merchant.instance_fixups
    191   (migration_name, version)
    192   VALUES ('merchant_0048_init', 48);
    193 CALL merchant.fixup_instance_schema (48::INT8);
    194 
    195 COMMIT;