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;