From 35c0453267f256ab162c2d137049746d5eb6860b Mon Sep 17 00:00:00 2001 From: swrup Date: Thu, 26 Feb 2026 17:52:30 +0100 Subject: [PATCH] fix check for replay attack + revoke 1sec in the future --- src/api.ml | 3 +- src/http_management.ml | 77 +++++++++++++++++++++--------------------- src/pg.ml | 6 ++-- tools/offline.ml | 11 ++++-- tools/offline_impl.ml | 18 ++++++++-- 5 files changed, 67 insertions(+), 48 deletions(-) diff --git a/src/api.ml b/src/api.ml index 821aac27..b2e3506b 100644 --- a/src/api.ml +++ b/src/api.ml @@ -6,8 +6,7 @@ properly combine jsont for "interface DenomGroupRsa extends DenomGroupCommon" better types: - payto_uri - - uri - use of monotonic time for some validity_start/_end fields *) + - uri *) let protocol_version = "31:0:0" diff --git a/src/http_management.ml b/src/http_management.ml index 1203dce0..f1ef75b6 100644 --- a/src/http_management.ml +++ b/src/http_management.ml @@ -165,8 +165,7 @@ module Auditors = struct () | Some auditor -> if Timestamp.geq auditor.last_change validity_start then - Error - "database has more recent auditor data for this auditor public key" + Error "replay detected on enable-auditor" else let+ () = Pg.update_auditor db_conn auditor |> unwrap_err_caqti in Logs.info (fun m -> m "updated auditor"); @@ -200,27 +199,22 @@ module Auditors_disable = struct match opt with | None -> Error "auditor not found" | Some auditor -> ( - match Timestamp.geq auditor.last_change validity_end with - | true -> - Error - "database has more recent auditor data for this auditor public \ - key" - | false -> ( - match auditor.is_active with - | false -> - Logs.info (fun m -> m "auditor was already revoked"); - Ok () - | true -> - let auditor = - { auditor with last_change= validity_end; is_active= false } - in - let+ () = - Pg.update_auditor db_conn auditor |> unwrap_err_caqti - in - Logs.info (fun m -> - m "revoked auditor `%s`" - (Crypto.EddsaPublicKey.to_b32 auditor_pub)); - ())) + if Timestamp.geq auditor.last_change validity_end then + Error "replay detected on disable-auditor" + else + match auditor.is_active with + | false -> + Logs.info (fun m -> m "auditor was already revoked"); + Ok () + | true -> + let auditor = + { auditor with last_change= validity_end; is_active= false } + in + let+ () = Pg.update_auditor db_conn auditor |> unwrap_err_caqti in + Logs.info (fun m -> + m "revoked auditor `%s`" + (Crypto.EddsaPublicKey.to_b32 auditor_pub)); + ()) let jsont = AuditorTeardownMessage.jsont @@ -436,14 +430,6 @@ module Wire = struct } = let* opt = Pg.find_wire db_conn ~payto_uri |> unwrap_err_caqti in match opt with - | Some wire -> - let+ () = - Pg.update_wire db_conn ~is_active:true ~last_change:validity_start - wire - |> unwrap_err_caqti - in - Logs.info (fun m -> m "updated wire method"); - () | None -> let wire = ExchangeWireAccount. @@ -464,6 +450,17 @@ module Wire = struct in Logs.info (fun m -> m "added wire method"); () + | Some (wire, _is_active, last_change) -> + if Timestamp.geq last_change validity_start then + Error "replay detected on enable-wire" + else + let+ () = + Pg.update_wire db_conn ~is_active:true ~last_change:validity_start + wire + |> unwrap_err_caqti + in + Logs.info (fun m -> m "updated wire method"); + () let jsont = WireSetupMessage.jsont @@ -492,13 +489,17 @@ module Wire_disable = struct let* opt = Pg.find_wire db_conn ~payto_uri |> unwrap_err_caqti in match opt with | None -> Error "wire not found" - | Some wire -> - let+ () = - Pg.update_wire db_conn ~is_active:false ~last_change:validity_end wire - |> unwrap_err_caqti - in - Logs.info (fun m -> m "disabled wire method"); - () + | Some (wire, _is_active, last_change) -> + if Timestamp.geq last_change validity_end then + Error "replay detected on disable-wire" + else + let+ () = + Pg.update_wire db_conn ~is_active:false ~last_change:validity_end + wire + |> unwrap_err_caqti + in + Logs.info (fun m -> m "disabled wire method"); + () let jsont = WireTeardownMessage.jsont diff --git a/src/pg.ml b/src/pg.ml index ee45ab99..eb4c789e 100644 --- a/src/pg.ml +++ b/src/pg.ml @@ -257,10 +257,10 @@ let insert_global_fees = let find_wire = let req = - Caqti_type.(payto_uri ->? exchange_wire_account) + Caqti_type.(payto_uri ->? t3 exchange_wire_account bool time) "SELECT payto_uri, conversion_url, debit_restrictions::TEXT, \ - credit_restrictions::TEXT, master_sig, bank_label, priority FROM \ - wire_accounts WHERE payto_uri=$1" + credit_restrictions::TEXT, master_sig, bank_label, priority, is_active, \ + last_change FROM wire_accounts WHERE payto_uri=$1" in fun (module Conn : CONN) ~payto_uri -> Conn.find_opt req payto_uri diff --git a/tools/offline.ml b/tools/offline.ml index 595bcc9c..4aa05bd1 100644 --- a/tools/offline.ml +++ b/tools/offline.ml @@ -1,6 +1,11 @@ -(* not done: - /management/aml-officers (for /aml) - /management/partners (for /wads) *) +(* TODO + don't trust any public keys + keep trusted exchange/secmod keys + either from config or TOFU (Trust On First Use) + + not done: + /management/aml-officers (for /aml) + /management/partners (for /wads) *) open Cmdliner open Cmdliner.Term.Syntax open Offline_impl diff --git a/tools/offline_impl.ml b/tools/offline_impl.ml index 68e45ea7..6f0623c9 100644 --- a/tools/offline_impl.ml +++ b/tools/offline_impl.ml @@ -299,7 +299,14 @@ let enable_auditor ~output ~master_key ~auditor_url ~auditor_name ~auditor_pub = let disable_auditor ~output ~master_key ~auditor_pub = let* key = read_master_key_file master_key in let ns = Mtime_clock.now_ns () in - let validity_end = Timestamp.of_s @@ Int64.unsigned_div ns 1_000_000_000L in + let open Time in + let validity_end = + TimeAbsolute.of_s @@ Int64.unsigned_div ns 1_000_000_000L + in + (* hack for tests: +1sec to be sure it overwrite previous timestamp *) + let validity_end = + Timestamp.of_absolute (TimeAbsolute.add validity_end TimeRelative.(of_s 1L)) + in let master_sig = let open Signatures.MasterDelAuditor in signf (EddsaSignature.sign ~key) { end_date= validity_end; auditor_pub } @@ -397,7 +404,14 @@ let enable_wire ~output ~master_key ~payto_uri ~bank_label ~priority = let disable_wire ~output ~master_key ~payto_uri = let* key = read_master_key_file master_key in let ns = Mtime_clock.now_ns () in - let validity_end = Timestamp.of_s @@ Int64.unsigned_div ns 1_000_000_000L in + let open Time in + let validity_end = + TimeAbsolute.of_s @@ Int64.unsigned_div ns 1_000_000_000L + in + (* hack for tests: +1sec to be sure it overwrite previous timestamp *) + let validity_end = + Timestamp.of_absolute (TimeAbsolute.add validity_end TimeRelative.(of_s 1L)) + in let h_wire = Hash.FullPaytoHash.hash payto_uri in let master_sig_del = let open Signatures.MasterDelWire in