open Syntax open Time open Hash let () = Mirage_crypto_rng_unix.use_default () (* TODO monotonic time: add something for tests / broken monotonic time https://docs.taler.net/manpages/taler-exchange-offline.1.html#security-considerations *) let now_s () = let ns = Mtime_clock.now_ns () in Int64.unsigned_div ns 1_000_000_000L module Future_keys = struct open Api let verify offline_master_public_key FutureKeysResponse. { future_denoms; future_signkeys; master_pub; denom_secmod_public_key; signkey_secmod_public_key; } = let verify_future_denom ~sm_denom_pub FutureDenom. { section_name; value= _; stamp_start; stamp_expire_withdraw; stamp_expire_deposit= _; stamp_expire_legal= _; denom_pub; fee_withdraw= _; fee_deposit= _; fee_refresh= _; fee_refund= _; denom_secmod_sig; } = let rsa_pub = match denom_pub with DenominationKey.Rsa denom -> denom.rsa_pub in let h_denom_pub = DenominationHash.hash rsa_pub in let h_section_name = H64_cstring.hash section_name in let anchor_time = stamp_start in let duration_withdraw = Timestamp.diff stamp_start stamp_expire_withdraw in let open Signatures.DenominationKeyAnnouncement in verify sm_denom_pub denom_secmod_sig { h_denom_pub; h_section_name; anchor_time; duration_withdraw } in let verify_future_signkey ~sm_signkey_pub FutureSignKey. { key; stamp_start; stamp_expire; stamp_end= _; signkey_secmod_sig } = let exchange_pub = key in let anchor_time = stamp_start in let duration = Timestamp.diff stamp_start stamp_expire in let open Signatures.SigningKeyAnnouncement in verify sm_signkey_pub signkey_secmod_sig { exchange_pub; anchor_time; duration } in let* () = match master_pub = offline_master_public_key with | false -> Fmt.error_msg "master public key of the future key response does not match ours" | true -> Ok () in let* () = list_iter (verify_future_denom ~sm_denom_pub:denom_secmod_public_key) future_denoms in let* () = list_iter (verify_future_signkey ~sm_signkey_pub:signkey_secmod_public_key) future_signkeys in Ok () let make ~master_key FutureKeysResponse. { future_denoms; future_signkeys; master_pub= _; denom_secmod_public_key= _; signkey_secmod_public_key= _; } = let denom_signature FutureDenom. { section_name= _; value; stamp_start; stamp_expire_withdraw; stamp_expire_deposit; stamp_expire_legal; denom_pub; fee_withdraw; fee_deposit; fee_refresh; fee_refund; denom_secmod_sig= _; } = let rsa_pub = match denom_pub with DenominationKey.Rsa denom -> denom.rsa_pub in let h_denom_pub = DenominationHash.hash rsa_pub in let master_sig = let open Signatures.DenominationKeyValidity in let master = Eddsa.pub_of_priv master_key in signf (Eddsa.sign ~key:master_key) { master; start= stamp_start; expire_withdraw= stamp_expire_withdraw; expire_spend= stamp_expire_deposit; expire_legal= stamp_expire_legal; value; fee_withdraw; fee_deposit; fee_refresh; fee_refund; denom_hash= h_denom_pub; } in DenomSignature.{ h_denom_pub; master_sig } in let signkey_signature FutureSignKey. { key; stamp_start; stamp_expire; stamp_end; signkey_secmod_sig= _ } = let master_sig = let open Signatures.ExchangeSigningKeyValidity in signf (Eddsa.sign ~key:master_key) { start= stamp_start; expire= stamp_expire; end_= stamp_end; signkey_pub= key; } in SignKeySignature.{ key; master_sig } in let denom_sigs = List.map denom_signature future_denoms in let signkey_sigs = List.map signkey_signature future_signkeys in MasterSignatures.{ denom_sigs; signkey_sigs } end (* -- *) let read_file fname = Bos.OS.File.read (Fpath.v fname) let write_file fname content = Bos.OS.File.write (Fpath.v fname) content let write_json_file fname jsont v = let* content = Api.encode jsont v in write_file fname content let read_master_key_file filename = let* master_key = read_file filename in Eddsa.priv_of_octets master_key |> Result.map_error (fun e -> `Bin_decode e) let download ~output ~url = Result.unwrap_err @@ let open Bos in OS.Cmd.run Cmd.( v "curl" % "--silent" % "--show-error" % "--fail" % "-o" % output % "-X" % "GET" % url) let upload ~input ~url = Result.unwrap_err @@ let open Bos in OS.Cmd.run Cmd.( v "curl" % "--silent" % "--show-error" % "--fail" % "-i" % "-X" % "POST" % "-H" % "Content-Type: application/json" % "--data" % ("@" ^ input) % url) let generate_eddsa_key ~output ~output_pubkey = Result.unwrap_err @@ let priv, pub = Mirage_crypto_ec.Ed25519.generate () in let priv_data = Mirage_crypto_ec.Ed25519.priv_to_octets priv in let pub_data = Mirage_crypto_ec.Ed25519.pub_to_octets pub |> Crockford.encode in let* () = write_file output priv_data in let* () = write_file output_pubkey pub_data in Ok () let sign ~master_key ~input ~output = Result.unwrap_err @@ let* master_key = read_master_key_file master_key in let* input = read_file input in let master_pub = Eddsa.pub_of_priv master_key in let* future_keys_response = Api.decode Api.FutureKeysResponse.jsont input in let* () = Future_keys.verify master_pub future_keys_response in let master_signatures = Future_keys.make ~master_key future_keys_response in write_json_file output Api.MasterSignatures.jsont master_signatures let revoke_denom ~output ~master_key ~h_denom = Result.unwrap_err @@ let* key = read_master_key_file master_key in let* h_denom_pub = DenominationHash.of_crockford h_denom |> Result.map_error (fun e -> `Msg e) in let denom_revoke = let master_sig = let open Signatures.MasterDenominationKeyRevocation in signf (Eddsa.sign ~key) { h_denom_pub } in Api.DenomRevocationSignature.{ master_sig } in write_json_file output Api.DenomRevocationSignature.jsont denom_revoke let revoke_signkey ~output ~master_key ~signkey = Result.unwrap_err @@ let* key = read_master_key_file master_key in let signkey_revoke = let master_sig = let open Signatures.MasterSigningKeyRevocation in signf (Eddsa.sign ~key) { exchange_pub= signkey } in Api.SignkeyRevocationSignature.{ master_sig } in write_json_file output Api.SignkeyRevocationSignature.jsont signkey_revoke let global_fees ~output ~master_key ~start_date ~end_date ~history_fee ~account_fee ~purse_fee ~history_expiration ~purse_account_limit ~purse_timeout = Result.unwrap_err @@ let* key = read_master_key_file master_key in let* purse_account_limit = match purse_account_limit >= 0 && purse_account_limit <= Int32.to_int Int32.max_int with | false -> Fmt.error_msg "invalid purse_account_limit value" | true -> Ok (Int32.of_int purse_account_limit) in let master_sig = let open Signatures.GlobalFees in signf (Eddsa.sign ~key) { start_date; end_date; purse_timeout; history_expiration; history_fee; account_fee; purse_fee; purse_account_limit; } in let global_fees = Api.GlobalFees. { start_date; end_date; purse_timeout; history_expiration; history_fee; account_fee; purse_fee; purse_account_limit; master_sig; } in write_json_file output Api.GlobalFees.jsont global_fees let enable_auditor ~output ~master_key ~auditor_url ~auditor_name ~auditor_pub = Result.unwrap_err @@ let* key = read_master_key_file master_key in let validity_start = Timestamp.of_s (now_s ()) in let master_sig = let open Signatures.MasterAddAuditor in signf (Eddsa.sign ~key) { start_date= validity_start; auditor_pub; h_auditor_url= H64_cstring.hash auditor_url; } in let v = Api.AuditorSetupMessage. { auditor_url; auditor_name; auditor_pub; master_sig; validity_start } in write_json_file output Api.AuditorSetupMessage.jsont v let disable_auditor ~output ~master_key ~auditor_pub = Result.unwrap_err @@ let* key = read_master_key_file master_key in let validity_end = TimeAbsolute.of_s (now_s ()) 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 (Eddsa.sign ~key) { end_date= validity_end; auditor_pub } in let v = Api.AuditorTeardownMessage.{ master_sig; validity_end } in write_json_file output Api.AuditorTeardownMessage.jsont v let wire_fee ~output ~master_key ~wire_method ~fee_start ~fee_end ~closing_fee ~wire_fee = Result.unwrap_err @@ let* key = read_master_key_file master_key in let master_sig_wire = let open Signatures.MasterWireFee in signf (Eddsa.sign ~key) { h_wire_method= H64_cstring.hash wire_method; start_date= fee_start; end_date= fee_end; closing_fee; wire_fee; } in let v = Api.WireFeeSetupMessage. { wire_method; fee_start; fee_end; closing_fee; wire_fee; master_sig_wire; } in write_json_file output Api.WireFeeSetupMessage.jsont v let enable_wire ~output ~master_key ~payto_uri ~bank_label ~priority = Result.unwrap_err @@ (* TODO wire *) let conversion_url = None in let credit_restrictions = [] in let debit_restrictions = [] in let h_conversion_url = H64_cstring.hash ((* ?? *) Option.value ~default:"" conversion_url) in let h_credit_restrictions = H64_cstring.hash "" in let h_debit_restrictions = H64_cstring.hash "" in (* - *) let* key = read_master_key_file master_key in let validity_start = Timestamp.of_s (now_s ()) in let h_wire_details = FullPaytoHash.hash payto_uri in let master_sig_wire = let open Signatures.MasterWireDetails in signf (Eddsa.sign ~key) { h_wire_details; h_conversion_url; h_credit_restrictions; h_debit_restrictions; } in let master_sig_add = let open Signatures.MasterAddWire in signf (Eddsa.sign ~key) { start_date= validity_start; h_wire= h_wire_details; h_conversion_url; h_credit_restrictions; h_debit_restrictions; } in let v = Api.WireSetupMessage. { master_sig_wire; master_sig_add; payto_uri; conversion_url; credit_restrictions; debit_restrictions; validity_start; bank_label; priority; } in write_json_file output Api.WireSetupMessage.jsont v let disable_wire ~output ~master_key ~payto_uri = Result.unwrap_err @@ let* key = read_master_key_file master_key in let validity_end = TimeAbsolute.of_s (now_s ()) 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 = FullPaytoHash.hash payto_uri in let master_sig_del = let open Signatures.MasterDelWire in signf (Eddsa.sign ~key) { end_date= validity_end; h_wire } in let v = Api.WireTeardownMessage.{ payto_uri; master_sig_del; validity_end } in write_json_file output Api.WireTeardownMessage.jsont v let drain ~output ~master_key ~debit_account_section ~credit_payto_uri ~wtid_octets ~date ~amount = Result.unwrap_err @@ let* key = read_master_key_file master_key in let* wtid = Api.Bytes32.of_octets wtid_octets |> Result.map_error (fun e -> `Msg e) in let master_sig = let open Signatures.MasterDrainProfit in signf (Eddsa.sign ~key) { wtid; date; amount; h_section= H64_cstring.hash debit_account_section; h_payto= FullPaytoHash.hash credit_payto_uri; } in let v = Api.DrainProfitsMessage. { debit_account_section; credit_payto_uri; wtid; master_sig; date; amount; } in write_json_file output Api.DrainProfitsMessage.jsont v