From c042dee0b1123d9055e6158d21bae3f5513d97e3 Mon Sep 17 00:00:00 2001 From: swrup Date: Fri, 27 Feb 2026 17:49:54 +0100 Subject: [PATCH] JJ: Description from the destination commit: wip validate response JJ: Description from source commit: + validate wire_fees JJ: Description from source commit: + validate signkeys JJ: Description from source commit: + validate exchange_pub JJ: Description from source commit: + validate exchange_sig JJ: Description from source commit: + validate denominations JJ: Description from source commit: + validate global-fees JJ: Description from source commit: + validate auditors --- src/api.ml | 8 +- src/denomination.ml | 57 +++++++++++ src/http_info.ml | 15 ++- test/dune | 5 + test/offline_management.sh | 2 +- test/validate_response.ml | 202 +++++++++++++++++++++++++++++++++++++ 6 files changed, 277 insertions(+), 12 deletions(-) create mode 100644 test/validate_response.ml diff --git a/src/api.ml b/src/api.ml index 592083fa..9f3b99d0 100644 --- a/src/api.ml +++ b/src/api.ml @@ -915,6 +915,10 @@ module SignKey = struct Signkey.{ pub; stamp_start; stamp_expire; stamp_end; master_sig } = { key= pub; stamp_start; stamp_expire; stamp_end; master_sig } + let to_signkey { key= pub; stamp_start; stamp_expire; stamp_end; master_sig } + = + Signkey.{ pub; stamp_start; stamp_expire; stamp_end; master_sig } + let jsont = let make key stamp_start stamp_expire stamp_end master_sig = { key; stamp_start; stamp_expire; stamp_end; master_sig } @@ -1261,7 +1265,7 @@ module ExchangeKeysResponse = struct stefan_lin: Float.t; asset_type: string; accounts: ExchangeWireAccount.t list; - wire_fees: AggregateTransferFee.t list Stdlib.Map.Make(Stdlib.String).t; + wire_fees: AggregateTransferFee.t list String_map.t; wads: ExchangePartnerListEntry.t list; kyc_enabled: bool; disable_direct_deposit: bool; @@ -1278,7 +1282,7 @@ module ExchangeKeysResponse = struct list_issue_date: Timestamp.t; auditors: AuditorKeys.t list; signkeys: SignKey.t list; - extensions: ExtensionManifest.t Stdlib.Map.Make(Stdlib.String).t option; + extensions: ExtensionManifest.t String_map.t option; (* Signature by the exchange master key of the SHA-256 hash of the normalized JSON-object of field extensions, if it was set. The signature has purpose TALER_SIGNATURE_MASTER_EXTENSIONS. *) diff --git a/src/denomination.ml b/src/denomination.ml index e6664d22..710f8921 100644 --- a/src/denomination.ml +++ b/src/denomination.ml @@ -110,3 +110,60 @@ let make_denom_group_sorted denominations = |> List.sort cmp_group in l + +let denoms_of_denomgroups l = + let open Api in + l + |> List.concat_map + (fun + (DenomGroup.Rsa + RsaDenomGroup. + { + denoms; + value; + fee_withdraw; + fee_deposit; + fee_refresh; + fee_refund; + }) + -> + let open RsaDenom in + denoms + |> List.map + (fun + { + rsa_pub; + master_sig; + stamp_start; + stamp_expire_withdraw; + stamp_expire_deposit; + stamp_expire_legal; + lost= _; + } + -> + let h_pub = + DenominationHash.hash (Crypto.RsaPublicKey.to_octets rsa_pub) + in + { + pub= rsa_pub; + value; + stamp_start; + stamp_expire_withdraw; + stamp_expire_deposit; + stamp_expire_legal; + fee_withdraw; + fee_deposit; + fee_refresh; + fee_refund; + age_mask= 0; + h_pub; + master_sig; + })) + +let hash_over_master_sigs denominations = + denominations + |> List.map (fun (Api.DenomGroup.Rsa g) -> g.Api.RsaDenomGroup.denoms) + |> List.concat_map (List.map (fun dn -> dn.Api.RsaDenom.master_sig)) + |> List.map Signatures.DenominationKeyValidity.to_octets + |> String.concat "" + |> Hash.H64.hash diff --git a/src/http_info.ml b/src/http_info.ml index 4a20888d..97b80e03 100644 --- a/src/http_info.ml +++ b/src/http_info.ml @@ -104,16 +104,13 @@ let mk_keys ~db_conn (module Keys : Keys.S) ~last_issue_date = (* ! depends on denominations order *) let exchange_sig = - let hc = - denominations - |> List.map (fun (DenomGroup.Rsa g) -> g.RsaDenomGroup.denoms) - |> List.concat_map (List.map (fun dn -> dn.RsaDenom.master_sig)) - |> List.map Signatures.DenominationKeyValidity.to_octets - |> String.concat "" - |> Hash.H64.hash - in let open Signatures.ExchangeKeySet in - signf (Keys.sign exchange_pub) R.{ list_issue_date; hc } + signf (Keys.sign exchange_pub) + R. + { + list_issue_date; + hc= Denomination.hash_over_master_sigs denominations; + } in let recoup = (* /recoup *) [] in diff --git a/test/dune b/test/dune index 7e8b7438..a906ce1d 100644 --- a/test/dune +++ b/test/dune @@ -7,3 +7,8 @@ (name test_crypto) (modules test_crypto) (libraries mte fmt)) + +(executable + (name validate_response) + (modules validate_response) + (libraries mte fmt bos)) diff --git a/test/offline_management.sh b/test/offline_management.sh index ccbef73b..606e6028 100755 --- a/test/offline_management.sh +++ b/test/offline_management.sh @@ -60,7 +60,7 @@ echo "[OK] /management/auditors/\$AUDITOR_PUB/disable" offline_tool wire-fee \ --master_key $master_key \ --output $b \ ---wire_method "xxx" \ +--wire_method "x-taler-bank" \ --fee_start 0 \ --fee_end 99999999 \ --closing_fee $zero_kudos \ diff --git a/test/validate_response.ml b/test/validate_response.ml new file mode 100644 index 00000000..c1b7795c --- /dev/null +++ b/test/validate_response.ml @@ -0,0 +1,202 @@ +open Syntax + +let keys content = + let open Api in + let open ExchangeKeysResponse in + let* v = Api.decode jsont content in + let* () = + if + Libtool_version.is_compatible + ~implementation:Libtool_version.mte_protocol_version v.version + then Ok () + else Fmt.error "version incompatible" + in + + (* TODO need hash over json.. + validate ExchangeWireAccount *) + let* () = + let open AggregateTransferFee in + v.wire_fees + |> String_map.bindings + |> List.concat_map (fun (wire_method, l) -> + List.map (fun item -> (wire_method, item)) l) + |> list_iter + (fun + (wire_method, { wire_fee; closing_fee; start_date; end_date; sig_ }) + -> + let open Signatures.MasterWireFee in + verify v.master_public_key sig_ + { + h_wire_method= Hash.Cstring.H64.hash wire_method; + start_date; + end_date; + wire_fee; + closing_fee; + }) + in + + let* () = + let open SignKey in + v.signkeys + |> list_iter + (fun { key; stamp_start; stamp_expire; stamp_end; master_sig } -> + let open Signatures.ExchangeSigningKeyValidity in + verify v.master_public_key master_sig + { + R.start= stamp_start; + expire= stamp_expire; + end_= stamp_end; + signkey_pub= key; + }) + in + + let* () = + let opt = + List.find_opt (fun sk -> sk.SignKey.key = v.exchange_pub) v.signkeys + in + match opt with + | None -> Fmt.error "exchange_pub is not in signkeys list" + | Some sk -> + let sk = SignKey.to_signkey sk in + (* TODO will need to fake time to validate expired data *) + let now = Timestamp.of_ptime (Ptime_clock.now ()) in + if Signkey.is_valid_at ~timestamp:now sk then Ok () + else Fmt.error "exchange_pub is not valid at the current time" + in + let* () = + let open Signatures.ExchangeKeySet in + verify v.exchange_pub v.exchange_sig + R. + { + list_issue_date= v.list_issue_date; + hc= Denomination.hash_over_master_sigs v.denominations; + } + in + + let denom_l = v.denominations |> Denomination.denoms_of_denomgroups in + let* () = + let open Denomination in + denom_l + |> list_iter (fun dn -> + let open Signatures.DenominationKeyValidity in + verify v.master_public_key dn.master_sig + { + R.master= v.master_public_key; + start= dn.stamp_start; + expire_withdraw= dn.stamp_expire_withdraw; + expire_spend= dn.stamp_expire_deposit; + expire_legal= dn.stamp_expire_legal; + value= dn.value; + fee_withdraw= dn.fee_withdraw; + fee_deposit= dn.fee_deposit; + fee_refresh= dn.fee_refresh; + fee_refund= dn.fee_refund; + denom_hash= dn.h_pub; + }) + in + let* () = + let last_issue_date = + denom_l + |> List.map (fun dn -> dn.Denomination.stamp_start) + |> List.fold_left Timestamp.max Timestamp.zero + in + if last_issue_date = v.list_issue_date then Ok () + else Fmt.error "list_issue_date is wrong" + in + + let* () = + v.global_fees + |> list_iter + (fun + GlobalFees. + { + start_date; + end_date; + history_fee; + account_fee; + purse_fee; + history_expiration; + purse_account_limit; + purse_timeout; + master_sig; + } + -> + let open Signatures.GlobalFees in + verify v.master_public_key master_sig + { + start_date; + end_date; + purse_timeout; + history_expiration; + history_fee; + account_fee; + purse_fee; + purse_account_limit; + }) + in + + let* () = + (* TODO O(n^2) *) + v.auditors + |> list_iter + (fun + AuditorKeys. + { auditor_pub; auditor_url; auditor_name= _; denomination_keys } + -> + let auditor_url_hash = Hash.H64.hash auditor_url in + denomination_keys + |> list_iter + (fun AuditorDenominationKey.{ denom_pub_h; auditor_sig } -> + let open Denomination in + let opt = + denom_l |> List.find_opt (fun dn -> dn.h_pub = denom_pub_h) + in + match opt with + | None -> Fmt.error "auditor denomination key not found" + | Some dn -> + let open Signatures.ExchangeKeyValidity in + verify auditor_pub auditor_sig + { + auditor_url_hash; + master= auditor_pub; + start= dn.stamp_start; + expire_withdraw= dn.stamp_expire_withdraw; + expire_spend= dn.stamp_expire_deposit; + expire_legal= dn.stamp_expire_legal; + value= dn.value; + fee_withdraw= dn.fee_withdraw; + fee_deposit= dn.fee_deposit; + fee_refresh= dn.fee_refresh; + denom_hash= denom_pub_h; + })) + in + + Ok () + +(* --- *) +open Cmdliner +open Cmdliner.Term.Syntax + +let keys_cmd = + let input = + let doc = "input file" in + Arg.(required & opt (some filepath) None & info [ "i"; "input" ] ~doc) + in + let doc = "validate a /keys JSON response" in + Cmd.make (Cmd.info "keys" ~doc) + @@ + let+ input = input in + let* content = + Result.bind (Fpath.of_string input) Bos.OS.File.read |> unwrap_err_msg + in + keys content + +let cli = + let info = + let doc = "Testing tool to validate exchange responses" in + Cmd.info "validate" ~doc + in + Cmd.group info [ keys_cmd ] + +let main () = Cmd.eval_result cli +let () = if !Sys.interactive then () else exit (main ())