This commit is contained in:
swrup 2026-02-18 17:26:53 +01:00
parent 1e30753ba9
commit 84472e93dd
160 changed files with 18460 additions and 90 deletions

View file

@ -0,0 +1,127 @@
(* TODO
have a currency agnostic amount_lib.ml
and specialize amount.ml to Config.currency??
have safe amount arithmetic *)
type sign =
| Sign_plus
| Sign_minus
type t = {
sign: sign option;
currency: string;
value: Int64.t;
fraction: Int32.t;
}
let value_upper_bound = Int64.of_float @@ Float.pow 2. 52.
(* TODO
the constraint is on the number of digits,
so this wrong if leading 0s
this depends on currency..? *)
let fraction_upper_bound = Int32.of_int 100_000_000
let make ~sign ~currency ~value ~fraction =
if value < Int64.zero then Error "value is negative"
else if fraction < Int32.zero then Error "fraction is negative"
else if value > value_upper_bound then Error "value is greater than 2^52-1"
else if fraction >= fraction_upper_bound then
Error "fraction has more than 8 decimal digits"
else Ok { sign; currency; value; fraction }
module Parse = struct
open Angstrom
let sign =
char '+' *> return (Some Sign_plus)
<|> char '-' *> return (Some Sign_minus)
<|> return None
let currency =
take_while1 (function 'a' .. 'z' | 'A' .. 'Z' -> true | _ -> false)
>>= fun s ->
match String.length s < 12 with
| false -> fail "currency is more than 11 characters"
| true -> return s
let int64 =
take_while1 (function '0' .. '9' -> true | _ -> false)
>>| Int64.of_string_opt
>>= function
| None -> fail "value is not a valid int64"
| Some n when n >= value_upper_bound -> fail "value is greater than 2^52-1"
| Some n -> return n
let int32 =
take_while1 (function '0' .. '9' -> true | _ -> false)
>>| Int32.of_string_opt
>>= function
| None -> fail "fraction is not a valid int32"
| Some n when n >= fraction_upper_bound ->
fail "fraction is greater than 10^8-1"
| Some n -> return n
let amount =
lift4
(fun sign currency value fraction ->
make ~sign ~currency ~value ~fraction)
sign currency
(char ':' *> int64)
(char '.' *> int32 <|> return 0_l)
<* end_of_input
let f s = parse_string ~consume:Consume.All amount s |> Result.join
end
let of_string = Parse.f
let pp =
let open Fmt in
let pp_sign ppf = function
| Sign_plus -> char ppf '+'
| Sign_minus -> char ppf '-'
in
fun ppf { sign; currency; value; fraction } ->
(* TODO
depends on the currency's number of fraction digits
assumes value and fraction are in bounds *)
pf ppf "%a%s:%Ld.%02ld" (Fmt.option pp_sign) sign currency value fraction
let to_string = Fmt.str "%a" pp
(* - *)
let jsont = Jsont.of_of_string ~kind:"Amount" of_string ~enc:to_string
(* byte length of currency string *)
let currency_len = 12
let pad_currency_string s =
let len = String.length s in
assert (len < currency_len);
let b = Bytes.make 12 '\x00' in
Bytes.blit_string s 0 b 0 len;
Bytes.to_string b
(* binary decoding unused? *)
let make_exn value fraction currency =
match make ~sign:None ~currency ~value ~fraction with
| Error _ -> Fmt.failwith "Amount of binary data failure"
| Ok v -> v
let bin =
let open Bin in
record make_exn
|+ field neint64 (fun t -> t.value)
|+ field neint32 (fun t -> t.fraction)
|+ field (bytes currency_len) (fun t -> pad_currency_string t.currency)
|> sealr
let bin_nbo =
let open Bin in
record make_exn
|+ field beint64 (fun t -> t.value)
|+ field beint32 (fun t -> t.fraction)
|+ field (bytes currency_len) (fun t -> pad_currency_string t.currency)
|> sealr

View file

@ -0,0 +1,27 @@
type sign =
| Sign_plus
| Sign_minus
type t = private {
sign: sign option;
currency: string;
value: Int64.t;
fraction: Int32.t;
}
val make :
sign:sign option ->
currency:string ->
value:Int64.t ->
fraction:Int32.t ->
(t, string) result
val pp : Format.formatter -> t -> unit
val to_string : t -> string
val of_string : string -> (t, string) result
(* - *)
val jsont : t Jsont.t
val bin : t Bin.t
val bin_nbo : t Bin.t
(* [caqti] is in pg_type.ml *)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,163 @@
(* https://docs.taler.net/design-documents/003-tos-rendering.html
must support `text/plain` and `text/markdown` *)
type t =
| Terms
| Privacy
module Assets_config = struct
(* hardcoded config just for static assets *)
let default_lang = "en"
let default_mimetype = ("text", "plain")
let default_extension = ".txt"
let default_encoding : [< `Identity | `DEFLATE | `Gzip ] = `Identity
let base_dir = function Terms -> "terms" | Privacy -> "privacy"
(* TODO this should be in the config like terms_etag *)
let terms_legal_version = "0"
end
let etag k =
match k with Terms -> Config.terms_etag | Privacy -> Config.privacy_etag
let supported_lang_arr, supported_ext_arr =
let aux t =
let prefix = Fpath.v (Assets_config.base_dir t) in
let path_l = List.map Fpath.v Assets_crunch.file_list in
let path_l = List.filter_map (Fpath.rem_prefix prefix) path_l in
let ext_l =
path_l |> List.map Fpath.get_ext |> List.sort_uniq String.compare
in
let lang_l =
List.map
(fun path ->
match Fpath.segs path with
| [] -> assert false
| [ dir; _file ] -> dir
| _l ->
Fmt.failwith "invalid folder structure, file `%s` is misplaced"
(Fpath.to_string Fpath.(prefix // path)))
path_l
in
let lang_l = List.sort_uniq String.compare lang_l in
let etag = (etag t).value in
List.iter
(fun path ->
let etag' = Fpath.to_string (Fpath.rem_ext (Fpath.base path)) in
if not @@ String.equal etag etag' then
Fmt.failwith
"filename of file `%s` does not match configuration ETAG value `%s`"
(Fpath.to_string Fpath.(prefix // path))
etag)
path_l;
if List.is_empty lang_l then Fmt.failwith "no language supported";
if List.is_empty ext_l then Fmt.failwith "no mimetype supported";
if not @@ List.mem Assets_config.default_lang lang_l then
Fmt.failwith "default language `%s` files not found"
Assets_config.default_lang;
if not @@ List.mem ".txt" ext_l then
Fmt.failwith "plain text file not found";
if not @@ List.mem ".md" ext_l then Fmt.failwith "markdown file not found";
List.iter
(fun dir ->
if String.length dir <> 2 then
Fmt.failwith "language directory with invalid name: `%s`" dir)
lang_l;
if List.length path_l <> List.length ext_l * List.length lang_l then
Fmt.failwith
"invalid folder structure, all supported language must provide the \
same set of file mimetype"
else (lang_l, ext_l)
in
let lang_l, ext_l = aux Terms in
let lang_l', ext_l' = aux Privacy in
match
List.equal String.equal lang_l lang_l'
&& List.equal String.equal ext_l ext_l'
with
| false ->
Fmt.failwith
"invalid folder structure, /terms and /privacy must support the same \
set of languages and mimetypes"
| true -> (Array.of_list lang_l, Array.of_list ext_l)
module Mimetype = struct
type t = string * string
let pp fmt mime = Fmt.pf fmt "%s/%s" (fst mime) (snd mime)
let assoc =
List.filter
(fun (_mime, ext) -> Array.mem ext supported_ext_arr)
[
(("text", "plain"), ".txt");
(("text", "markdown"), ".md");
(("text", "html"), ".html");
(("text", "html"), ".htm");
(("application", "pdf"), ".pdf");
(("image", "jpeg"), ".jpg");
(("image", "jpeg"), ".jpeg");
(("image", "png"), ".png");
(("image", "gif"), ".gif");
]
let arr =
let all_supported, all_supported_ext = List.split assoc in
match
Array.find_opt
(fun ext -> not @@ List.exists (( = ) ext) all_supported_ext)
supported_ext_arr
with
| Some ext -> Fmt.failwith "extension `%s` unsupported" ext
| None -> Array.of_list all_supported
let default =
match
List.mem
(Assets_config.default_mimetype, Assets_config.default_extension)
assoc
with
| false ->
Fmt.failwith "default content type `%a` not supported" pp
Assets_config.default_mimetype
| true -> Assets_config.default_mimetype
let of_cohttp = function
| Cohttp.Accept.MediaType (m, m_sub) ->
Array.find_opt (( = ) (m, m_sub)) arr
| AnyMediaSubtype m -> Array.find_opt (fun (m', _) -> String.equal m m') arr
| AnyMedia -> Some default
let to_extension_exn t =
match List.assoc_opt t assoc with
| None -> Fmt.failwith "Mimetype.to_extension failure: `%a` unknown" pp t
| Some ext -> ext
end
module Language = struct
type t = string
let arr = supported_lang_arr
let default = Assets_config.default_lang
let of_cohttp = function
| Cohttp.Accept.AnyLanguage -> Some default
| Language language_range -> (
(* ignore language subtags (e.g. "en-US" -> "en") *)
match language_range with
| [] -> assert false
| lang :: _ when Array.mem lang supported_lang_arr -> Some lang
| _ -> None)
end
(* ! lang and mime must be supported *)
let get_content ~lang ~mime t =
let ext = Mimetype.to_extension_exn mime in
let path =
Fpath.to_string
Fpath.((v (Assets_config.base_dir t) / lang / (etag t).value) + ext)
in
match Assets_crunch.read path with
| None -> Fmt.failwith "static file not found: `%s`" path
| Some data -> data

View file

@ -0,0 +1,38 @@
(* Crockford's variant of Base32
http://www.crockford.com/wrmg/base32.html
except that:
- 'U' is not excluded but also decodes to 'V'
- '-' is not allowed
- checksum is not allowed *)
(* 'I' 'L' 'O' 'U' excluded
no '=' padding in encoded string *)
type t = string
let alphabet = Base32.make_alphabet "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
let encode s =
let s = Base32.encode_string ~alphabet s in
(* remove '=' padding *)
match String.index_opt s '=' with
| None -> s
| Some i -> String.sub s 0 i
let decode s =
let s =
String.map
(fun c ->
match Char.uppercase_ascii c with
| 'O' -> '0'
| 'I' | 'L' -> '1'
| 'U' -> 'V'
| c -> c)
s
in
(* restore padding for base32 lib *)
let n = 8 - (String.length s mod 8) in
let pad = String.make n '=' in
let s = s ^ pad in
match Base32.decode ~alphabet ~off:0 ~len:(String.length s) s with
| Error (`Msg e) -> Error e
| Ok v -> Ok v

View file

@ -0,0 +1,203 @@
open Parse_config
let config_filename = "mte.conf"
let secrets_dir = Fpath.v "secrets"
let secmod_dir = Fpath.(secrets_dir / "secmod")
let config_data =
match Assets_crunch.read config_filename with
| None -> fail "static file not found: `%s`" config_filename
| Some data ->
let v = Config_section.parse data in
v
module Exchange = struct
let get_opt field = get_opt config_data ~section:"exchange" ~field
let get field = get config_data ~section:"exchange" ~field
(* - *)
let currency = (* todo: constraint on currency string *) get "currency"
let currency_round_unit = get "currency_round_unit" |> amount
let db = get "db" |> const_value "postgres"
let attribute_encryption_key = get "attribute_encryption_key"
let port = get "port" |> int
let bind_to = get "bind_to"
let master_public_key = get "master_public_key" |> ed25519
(* TODO Defaults to 0.0 if not specified. *)
let stefan_abs = get "stefan_abs" |> amount
let stefan_log = get "stefan_log" |> amount
let stefan_lin =
get_opt "stefan_lin" |> Option.map float |> Option.value ~default:0.0
let aggregator_idle_sleep_interval =
get "aggregator_idle_sleep_interval" |> duration
let closer_idle_sleep_interval = get "closer_idle_sleep_interval" |> duration
let transfer_idle_sleep_interval =
get "transfer_idle_sleep_interval" |> duration
let wirewatch_idle_sleep_interval =
get "wirewatch_idle_sleep_interval" |> duration
let signkey_legal_duration = get "signkey_legal_duration" |> duration
let max_keys_caching = get "max_keys_caching" |> duration
let enable_kyc = get "enable_kyc" |> yes_no
let terms_etag = get "terms_etag" |> etag
let privacy_etag = get "privacy_etag" |> etag
let base_url = get "base_url"
let shopping_url = get_opt "shopping_url"
let open_banking_gateway_url = get_opt "open_banking_gateway_url"
let bank_compliance_language = get_opt "bank_compliance_language"
let aml_spa_dialect = get_opt "aml_spa_dialect"
let toplevel_redirect_url = get_opt "toplevel_redirect_url"
let tiny_amount = get_opt "tiny_amount" |> Option.map amount
(* not implemented or not relevant to MTE:
let max_requests = get "max_requests" |> int
aggregator_shard_size
serve
unixpath
unixpath_mode
terms_dir
privacy_dir *)
end
module Exchangedb = struct
let get field = get config_data ~section:"exchangedb" ~field
(* - *)
let idle_reserve_expiration_time =
get "idle_reserve_expiration_time" |> duration
let legal_reserve_expiration_time =
get "legal_reserve_expiration_time" |> duration
let aggregator_shift = get "aggregator_shift" |> duration
let max_aml_program_runtime = get "max_aml_program_runtime" |> duration
let default_purse_limit = get "default_purse_limit" |> int
end
module Exchangedb_postgres = struct
let config =
get config_data ~section:"exchangedb-postgres" ~field:"config" |> uri
end
module Currency = struct
type t = {
enabled: [ `YES | `NO ];
code: string;
name: string;
fractional_input_digits: int;
fractional_normal_digits: int;
fractional_trailing_zero_digits: int;
alt_unit_names: (int * string) list;
}
let currency_sections =
List.filter
(fun v -> String.starts_with ~prefix:"currency-" v.header)
config_data
let parse_currency section =
let get field = get config_data ~section:section.header ~field in
{
enabled= get "enabled" |> yes_no;
code= get "code";
name= get "name";
fractional_input_digits= get "fractional_input_digits" |> int;
fractional_normal_digits= get "fractional_normal_digits" |> int;
fractional_trailing_zero_digits=
get "fractional_trailing_zero_digits" |> int;
alt_unit_names=
get "alt_unit_names" |> Alt_unit_names.decode |> Parse_config.unwrap;
}
let all_currencies = List.map parse_currency currency_sections
(* I think the exchange only handle one currency *)
let v =
match
List.find_opt (fun v -> v.code = Exchange.currency) all_currencies
with
| None ->
fail "section `[currency-%s]` not found, currency `%s` is not defined"
Exchange.currency Exchange.currency
| Some v -> (
match v.enabled = `YES with
| false -> fail "currency `%s` is not enabled" Exchange.currency
| true -> v)
end
module Coin = struct
type t = {
section_name: string;
value: Amount.t;
duration_withdraw: Time.Relative.t;
duration_spend: Time.Relative.t;
duration_legal: Time.Relative.t;
fee_withdraw: Amount.t;
fee_deposit: Amount.t;
fee_refresh: Amount.t;
fee_refund: Amount.t;
cipher: [ (* `CS |*) `RSA ];
rsa_keysize: int; (* : int option (only if `RSA) *)
age_restricted: [ (*`YES|*) `NO ];
}
let coin_sections =
List.filter
(fun v ->
(* note: here its a '_' not '-' *)
String.starts_with ~prefix:"coin_" v.header)
config_data
let parse_coin section =
let get field = get config_data ~section:section.header ~field in
let section_name =
String.sub section.header 5 (String.length section.header - 5)
in
{
section_name;
value= get "value" |> amount;
duration_withdraw= get "duration_withdraw" |> duration;
duration_spend= get "duration_spend" |> duration;
duration_legal= get "duration_legal" |> duration;
fee_withdraw= get "fee_withdraw" |> amount;
fee_deposit= get "fee_deposit" |> amount;
fee_refresh= get "fee_refresh" |> amount;
fee_refund= get "fee_refund" |> amount;
cipher= (get "cipher" |> const_value "RSA" |> fun _s -> `RSA);
rsa_keysize= get "rsa_keysize" |> int;
age_restricted=
( get "age_restricted" |> yes_no |> function
| `NO -> `NO
| `YES -> fail "`age_restricted = YES` is not supported" );
}
let all_coins = List.map parse_coin coin_sections
end
module Exchange_secmod_rsa = struct
let get field =
let section = "taler-exchange-secmod-" ^ "rsa" in
get config_data ~section ~field
let lookahead_sign = get "lookahead_sign" |> duration
let overlap_duration = get "overlap_duration" |> duration
(* not relevant: sm_priv_key key_dir unixpath *)
end
module Exchange_secmod_eddsa = struct
let get field =
let section = "taler-exchange-secmod-" ^ "eddsa" in
get config_data ~section ~field
let lookahead_sign = get "lookahead_sign" |> duration
let overlap_duration = get "overlap_duration" |> duration
end
(* -- *)
include Exchange

View file

@ -0,0 +1,371 @@
open Syntax
module Binary_format_rsa = struct
(* RSA public key binary format
https://www.gnupg.org/documentation/manuals/gcrypt/MPI-formats.html
:= { uint16_be: n size; uint16_be: e size; n; e}
integer in big-endian format (MSB first)
leading zeroes are stripped unless they are required to keep a value positive
no 0-termination *)
let z_array_to_octets (arr : Z.t array) =
let nb = Array.length arr in
let bits_arr = Array.map Mirage_crypto_pk.Z_extra.to_octets_be arr in
let len_arr = Array.map String.length bits_arr in
let len = (2 * nb) + Array.fold_left ( + ) 0 len_arr in
let b = Bytes.make len '\x00' in
let pos = ref 0 in
Array.iter
(fun len ->
Bytes.set_uint16_be b !pos len;
pos := !pos + 2)
len_arr;
Array.iteri
(fun i bits ->
let len = len_arr.(i) in
Bytes.blit_string bits 0 b !pos len;
pos := !pos + len)
bits_arr;
Bytes.unsafe_to_string b
let z_array_of_octets ~nb s =
let s_len = String.length s in
if s_len <= 2 * nb then Error "rsa of_octets error"
else
let pos = ref 0 in
let len_arr =
Array.init nb (fun _i ->
let len = String.get_uint16_be s !pos in
pos := !pos + 2;
len)
in
let len = (2 * nb) + Array.fold_left ( + ) 0 len_arr in
if s_len <> len then Error "rsa of_octets error"
else
let z_arr =
Array.init nb (fun i ->
let len = len_arr.(i) in
let s = String.sub s !pos len in
let z = Mirage_crypto_pk.Z_extra.of_octets_be s in
pos := !pos + len;
z)
in
Ok z_arr
let pub_to_octets ({ n; e } : Mirage_crypto_pk.Rsa.pub) =
z_array_to_octets [| n; e |]
let pub_of_octets s =
let* arr = z_array_of_octets ~nb:2 s in
match arr with
| [| n; e |] ->
let+ pub = Mirage_crypto_pk.Rsa.pub ~n ~e |> unwrap_err_msg in
pub
| _ -> assert false
(* custom private key binary format <> than gcrypt *)
let priv_to_octets ({ e; d; n; p; q; dp; dq; q' } : Mirage_crypto_pk.Rsa.priv)
=
z_array_to_octets [| e; d; n; p; q; dp; dq; q' |]
let priv_of_octets s =
let* arr = z_array_of_octets ~nb:8 s in
match arr with
| [| e; d; n; p; q; dp; dq; q' |] ->
let+ priv =
Mirage_crypto_pk.Rsa.priv ~e ~d ~n ~p ~q ~dp ~dq ~q' |> unwrap_err_msg
in
priv
| _ -> assert false
end
module EddsaPublicKey = struct
open Mirage_crypto_ec.Ed25519
type t = pub
let to_octets t = pub_to_octets t
let of_octets t =
pub_of_octets t |> function
| Error e -> Fmt.error "%a" Mirage_crypto_ec.pp_error e
| Ok v -> Ok v
let bin =
let of_octets_exn t = of_octets t |> Result.get_ok in
Bin.map (Bin.bytes 32) of_octets_exn to_octets
let of_b32 s =
let* octets = B32.decode s in
let* pub = of_octets octets in
Ok pub
let to_b32 t = B32.encode (to_octets t)
let jsont = Jsont.of_of_string ~kind:"EddsaPublicKey" of_b32 ~enc:to_b32
let caqti =
Caqti_type.custom
~encode:(fun v -> Ok (to_octets v))
~decode:(fun v -> of_octets v)
Caqti_type.octets
end
module EddsaPrivateKey = struct
(* EdDSA and ECDHE public keys always point on Curve25519
and represented using the standard 256 bits Ed25519 compact format,
converted to Crockford Base32. *)
open Mirage_crypto_ec.Ed25519
type t = priv
let pub_of_priv = pub_of_priv
let to_octets t = priv_to_octets t
let of_octets t =
priv_of_octets t |> function
| Error err ->
let err = Fmt.str "%a" Mirage_crypto_ec.pp_error err in
Error err
| Ok v -> Ok v
let bin =
let of_octets_exn t = of_octets t |> Result.get_ok in
Bin.map (Bin.bytes 32) of_octets_exn to_octets
let jsont =
let of_b32 s =
let* octets = B32.decode s in
of_octets octets
in
let to_b32 t = B32.encode (to_octets t) in
Jsont.of_of_string ~kind:"EddsaPrivateKey" of_b32 ~enc:to_b32
end
module EddsaSignature : sig
type t
val sign : key:EddsaPrivateKey.t -> string -> t
(* Ok () on verification success *)
val verify : key:EddsaPublicKey.t -> t -> msg:string -> (unit, string) result
val to_octets : t -> string
val of_octets : string -> (t, string) result
val jsont : t Jsont.t
val bin : t Bin.t
val caqti : t Caqti_type.t
end = struct
(* transmitted as 64-bytes base32
binary-encoded objects with just the R and S values *)
type t = string
(* mirage_crypto:
"The result is the concatenation of r and s, as specified in RFC 8032." *)
let sign ~key s = Mirage_crypto_ec.Ed25519.sign ~key s
let verify ~key s ~msg =
let b = Mirage_crypto_ec.Ed25519.verify ~key s ~msg in
match b with
| false -> Error "EddsaSignature verification: invalid signature"
| true -> Ok ()
let to_octets t = t
let check_size t =
match String.length t = 64 with
| false -> Error "EddsaSignature of_octets: data is not 64 bytes."
| true -> Ok ()
let of_octets v =
let+ () = check_size v in
v
let bin =
let of_octets_exn t = of_octets t |> Result.get_ok in
Bin.map (Bin.bytes 64) of_octets_exn to_octets
let jsont =
let of_b32 s =
let* t = B32.decode s in
of_octets t
in
let to_b32 = B32.encode in
Jsont.of_of_string ~kind:"EddsaSignature" of_b32 ~enc:to_b32
let caqti =
Caqti_type.custom
~encode:(fun v -> Ok (to_octets v))
~decode:(fun s -> of_octets s)
Caqti_type.octets
end
module RsaPublicKey = struct
open Mirage_crypto_pk
type t = Rsa.pub
let to_octets = Binary_format_rsa.pub_to_octets
let of_octets = Binary_format_rsa.pub_of_octets
let jsont =
let of_b32 s =
let* s = B32.decode s in
let+ v = of_octets s in
v
in
let to_b32 t = B32.encode (to_octets t) in
Jsont.of_of_string ~kind:"RsaPublicKey" of_b32 ~enc:to_b32
let caqti : t Caqti_type.t =
Caqti_type.custom
~encode:(fun v -> Ok (to_octets v))
~decode:(fun v -> of_octets v)
Caqti_type.octets
end
module RsaPrivateKey = struct
open Mirage_crypto_pk.Rsa
type t = priv
let generate ~bits () =
let priv = generate ~bits () in
let pub = pub_of_priv priv in
(priv, pub)
let pub_of_priv = pub_of_priv
let of_octets = Binary_format_rsa.priv_of_octets
let to_octets = Binary_format_rsa.priv_to_octets
let jsont =
let of_b32 s =
let* s = B32.decode s in
let+ v = of_octets s in
v
in
let to_b32 t = B32.encode (to_octets t) in
Jsont.of_of_string ~kind:"RsaPrivateKey" of_b32 ~enc:to_b32
end
module RsaSignature : sig
type t
val jsont : t Jsont.t
end = struct
type t = string
let jsont =
let of_b32 s = B32.decode s in
let to_b32 t = B32.encode t in
Jsont.of_of_string ~kind:"RsaSignature" of_b32 ~enc:to_b32
end
(* some type aliases, just for prettier .mli *)
type eddsa_priv = EddsaPrivateKey.t
type eddsa_pub = EddsaPublicKey.t
type eddsa_sig = EddsaSignature.t
type rsa_priv = RsaPrivateKey.t
type rsa_pub = RsaPublicKey.t
type rsa_sig = RsaSignature.t
type denom_hash = Hash.DenominationHash.t
(* WIP *)
module FDH_RSA = struct
open Mirage_crypto_pk
module Kdf = struct
module XTR = Hkdf.Make (Digestif.SHA512)
module PRF = Hkdf.Make (Digestif.SHA256)
let kdf =
fun ~xts ~ikm ~ctx ~len ->
let prk = XTR.extract ~salt:xts ikm in
let okm = PRF.expand ~prk ~info:ctx len in
okm
let kdf_mod_n ~n ~xts ~ikm ~ctx =
let nbits = Z.numbits n in
let len = ((nbits - 1) / 8) + 1 in
assert (8 * len = nbits);
let rec go ctr =
(* cat ctx ctr_be *)
let ctx =
let ctx_len = String.length ctx in
let b = Bytes.create (ctx_len + 2) in
Bytes.blit_string ctx 0 b 0 ctx_len;
Bytes.set_uint16_be b ctx_len ctr;
Bytes.unsafe_to_string b
in
let okm = kdf ~xts ~ikm ~ctx ~len in
assert (String.length okm = len);
let r = Z_extra.of_octets_be okm in
if Z.gt r n then go (succ ctr) else r
in
go 0
end
let gcd_validate r n =
match Z.equal (Z.gcd r n) Z.one with
| true -> ()
| false -> Fmt.failwith "RSA key is malicious"
let rsa_full_domain_hash pub msg =
let xts = RsaPublicKey.to_octets pub in
let ctx = "RSA-FDA FTpsW!" in
let r = Kdf.kdf_mod_n ~n:pub.n ~xts ~ikm:msg ~ctx in
gcd_validate r pub.n; r
let rsa_blinding_key_derive (pub : RsaPublicKey.t) bks =
let xts = "Blinding KDF extractor HMAC key" in
let ctx = "Blinding KDF" in
let r = Kdf.kdf_mod_n ~n:pub.n ~xts ~ikm:bks ~ctx in
gcd_validate r pub.n; r
let rsa_blind pub ~bks ~msg =
let data = rsa_full_domain_hash pub msg in
let bkey = rsa_blinding_key_derive pub bks in
(* can we just use [powm] here instead? *)
let r_e = Z.powm_sec bkey pub.e pub.n in
let data_r_e = Z.rem (Z.mul data r_e) pub.n in
Z_extra.to_octets_be data_r_e
(* -- WIP crypto -- *)
(* TODO crypto
not sure about signature scheme used by taler
libgnunetutil crypto_rsa.c use "(flags raw)" => no padding *)
(* decrypt <=> sign *)
let rsa_sign_z priv r =
let data = Z_extra.to_octets_be r in
Rsa.decrypt ~crt_hardening:true ~key:priv data
(* TODO crypto
look into mirage-crypto for this
use Eqaf for constant time string compare *)
let rsa_verify_z pub r sig_ =
let data = Z_extra.to_octets_be r in
let sig_' = Rsa.encrypt ~key:pub data in
match String.equal sig_ sig_' with
| false -> Fmt.error "RSA signature verification failed"
| true -> Ok ()
let rsa_sign_fdh priv msg =
let pub = Rsa.pub_of_priv priv in
let r = rsa_full_domain_hash pub msg in
rsa_sign_z priv r
let rsa_unblind pub ~bks ~sig_ =
let bkey = rsa_blinding_key_derive pub bks in
let r_inv =
try Z.invert bkey pub.n
with Division_by_zero ->
(* => gcd(r,n) <> 1, should be already checked for *)
assert false
in
let ubsig = Z.rem (Z.mul sig_ r_inv) pub.n in
ubsig
let rsa_verify pub ~msg ~sig_ =
let r = rsa_full_domain_hash pub msg in
rsa_verify_z pub r sig_
end

View file

@ -0,0 +1,18 @@
open Crypto
type t = {
pub: rsa_pub;
value: Amount.t;
stamp_start: Timestamp.t;
stamp_expire_withdraw: Timestamp.t;
stamp_expire_deposit: Timestamp.t;
stamp_expire_legal: Timestamp.t;
fee_withdraw: Amount.t;
fee_deposit: Amount.t;
fee_refresh: Amount.t;
fee_refund: Amount.t;
age_mask: int;
h_pub: denom_hash;
master_sig: Signatures.DenominationKeyValidity.t;
revoked_sig: Signatures.MasterDenominationKeyRevocation.t option;
}

View file

@ -0,0 +1,26 @@
type env = {
caqti_switch: Caqti_miou.Switch.t;
db_uri: Uri.t;
}
let db_connection : (env, Caqti_miou.connection) Vif.Device.device =
let finally (module Conn : Caqti_miou.CONNECTION) = Conn.disconnect () in
Vif.Device.v ~name:"db_connection" ~finally []
@@ fun { caqti_switch; db_uri } ->
match Caqti_miou_unix.connect ~sw:caqti_switch db_uri with
| Error err ->
Fmt.failwith "Database connection failure: %a." Caqti_error.pp err
| Ok conn -> (
match Pg.preflight conn with
| Error err ->
Fmt.failwith "Database preflight failure: %a." Caqti_error.pp err
| Ok () ->
Logs.info (fun m -> m "database connection initialized");
conn)
let keys =
let finally _key = () in
Vif.Device.v ~name:"keys" ~finally [ Vif.Device.value db_connection ]
@@ fun (module Conn : Pg.CONN) (_env : env) ->
let sm : (module Keys.S) = (module Keys.Make (Conn)) in
sm

View file

@ -0,0 +1,45 @@
(executable
(public_name mte)
(name mte)
(modules mte)
(libraries mte))
(library
(name mte)
(wrapped false)
(modules :standard \ mte b32)
(libraries
b32
;
caqti
caqti-miou
caqti-miou.unix
caqti-driver-pgx
bin
mirage-crypto
kdf.hkdf
digestif
duration
vif
fmt
jsont
cohttp
ptime
logs
logs.fmt
logs.threaded
fmt.tty))
(library ; crockford base32
(name b32)
(modules b32)
(libraries base32))
(rule
(target assets_crunch.ml)
(deps
(source_tree ../assets))
(action
(with-stdout-to
%{null}
(run ocaml-crunch -m plain ../assets -o %{target}))))

View file

@ -0,0 +1,103 @@
open Digestif
module type S = sig
type t
val bin : t Bin.t
val caqti : t Caqti_type.t
val jsont : t Jsont.t
val hash : string -> t
val of_octets : string -> t
val to_octets : t -> string
val of_b32 : B32.t -> (t, string) result
end
module H32 = struct
type t = SHA256.t
let hash s = SHA256.(digest_string s)
let of_octets s =
match SHA256.of_raw_string_opt s with
| None -> Fmt.failwith "H32.of_octets failure"
| Some t -> t
let to_octets = SHA256.to_raw_string
let of_b32 s = Result.map of_octets (B32.decode s)
let bin =
let open Bin in
map (bytes 32) of_octets to_octets
(* hashes are not b32 encoded in the database *)
let caqti =
let open Caqti_type in
custom
~encode:(fun v -> Ok (to_octets v))
~decode:(fun v -> Ok (of_octets v))
octets
let jsont =
let enc v = B32.encode (to_octets v) in
Jsont.of_of_string ~kind:"Hash 32" of_b32 ~enc
end
module H64 = struct
type t = SHA512.t
let hash s = SHA512.(digest_string s)
let of_octets s =
match SHA512.of_raw_string_opt s with
| None -> Fmt.failwith "H64.of_octets failure"
| Some t -> t
let to_octets = SHA512.to_raw_string
let of_b32 s = Result.map of_octets (B32.decode s)
let bin =
let open Bin in
map (bytes 64) of_octets to_octets
(* hashes are not b32 encoded in the database *)
let caqti =
let open Caqti_type in
custom
~encode:(fun v -> Ok (to_octets v))
~decode:(fun v -> Ok (of_octets v))
octets
let jsont =
let enc v = B32.encode (to_octets v) in
Jsont.of_of_string ~kind:"Hash 64" of_b32 ~enc
end
(* C-terminated strings
some strings need to be hashed with a '\0' termination char *)
module Cstring = struct
module H32 = struct
include H32
let hash s = hash (s ^ "\x00")
end
module H64 = struct
include H64
let hash s = hash (s ^ "\x00")
end
end
(* TODO
check which hash algorithm to use for each hash type *)
module FullPaytoHash : S = H32
module NormalizedPaytoHash : S = H32
module DenominationHash : S = H64
module PrivateContractHash : S = H64
module ExtensionsPolicyHash : S = H64
module MerchantWireHash : S = H64
module AgeCommitmentHash : S = H64
module BlindedCoinHash : S = H64
module CoinPubHash : S = H64
module OutputCommitmentHash : S = H64
module HashPlanchetsP : S = H64

View file

@ -0,0 +1,44 @@
let accept_header_value =
Fmt.str "%a"
(Fmt.array ~sep:(Fmt.any ", ") Assets.Mimetype.pp)
Assets.Mimetype.arr
let avail_languages_header_value =
Fmt.str "%a" (Fmt.array ~sep:(Fmt.any ", ") Fmt.string) Assets.Language.arr
(* TODO Cohttp raises on invalid *)
let select_mimetype headers =
let opt = Vif.Headers.get headers "accept" in
Cohttp.Accept.media_ranges opt
|> Cohttp.Accept.qsort
|> List.find_map (fun (_q, (m, _p)) -> Assets.Mimetype.of_cohttp m)
|> function
| None -> Assets.Mimetype.default
| Some mime -> mime
let select_language headers =
let opt = Vif.Headers.get headers "accept-language" in
Cohttp.Accept.languages opt
|> Cohttp.Accept.qsort
|> List.map snd
|> List.find_map Assets.Language.of_cohttp
|> function
| None -> Assets.Language.default
| Some lang -> lang
let select_encoding headers =
let opt = Vif.Headers.get headers "accept-encoding" in
Cohttp.Accept.encodings opt
|> Cohttp.Accept.qsort
|> List.map snd
|> List.find_map (function
| Cohttp.Accept.Identity -> Some `Identity
| Deflate -> Some `DEFLATE
| Gzip -> Some `Gzip
| AnyEncoding -> Some Assets.Assets_config.default_encoding
| Encoding _ | Compress -> (* unsupported *) None)
|> function
| None -> None
| Some `Identity -> None
| Some `DEFLATE -> Some `DEFLATE
| Some `Gzip -> Some `Gzip

View file

@ -0,0 +1,69 @@
module Etag = struct
(* https://httpwg.org/specs/rfc9110.html#field.etag *)
type t = {
weak: bool;
value: string;
}
let pp ppf { weak; value } =
match weak with
| false -> Fmt.pf ppf {|"%s"|} value
| true -> Fmt.pf ppf {|W/"%s"|} value
let to_field_string t = Fmt.str "%a" pp t
let angstrom =
let open Angstrom in
let is_valid_char c =
let n = Char.code c in
(n >= 0x21 && n <= 0x7E && n <> 0x22) || (n >= 0x80 && n <= 0xFF)
in
let quoted_string = char '"' *> take_while is_valid_char <* char '"' in
lift2
(fun weak value -> { weak; value })
(option false (string "W/" *> return true))
quoted_string
let parse s =
match Angstrom.parse_string ~consume:Angstrom.Consume.All angstrom s with
| Error _e -> Fmt.error "invalid etag: `%s`" s
| Ok v -> Ok v
end
module If_none_match = struct
type t =
| Any
| List of Etag.t list
let pp ppf = function
| Any -> Fmt.pf ppf {|*|}
| List l -> Fmt.pf ppf {|%a|} (Fmt.list ~sep:(Fmt.any ", ") Etag.pp) l
let angstrom =
let open Angstrom in
let ows = skip_while (function ' ' | '\t' -> true | _ -> false) in
let comma = ows *> char ',' *> ows in
(* A recipient MUST parse and ignore a reasonable number of empty list elements *)
let etag_opt = Etag.angstrom >>| Option.some <|> return None in
let etags =
etag_opt >>= fun hd ->
many (comma *> etag_opt) >>= fun tl ->
let l = List.filter_map Fun.id (hd :: tl) in
match l with [] -> fail "empty etag list" | l -> return (List l)
in
let any = char '*' *> return Any in
any <|> etags
let parse s =
match Angstrom.parse_string ~consume:Angstrom.Consume.All angstrom s with
| Error _e -> Fmt.error "invalid if-none-match field: `%s`" s
| Ok v -> Ok v
let evaluate etag t =
match t with
| Any -> false
| List l ->
not
@@ List.exists (fun e -> String.equal etag.Etag.value e.Etag.value) l
end

View file

@ -0,0 +1,254 @@
open Syntax
open Api
module String_map = Stdlib.Map.Make (Stdlib.String)
(* TODO mirage-crypto
is this ok?
maybe don't use the same RNG-initialization as the one used to generate keys *)
let seed req _server _env =
Logs.info (fun m -> m "GET /seed");
(* RNG is initialized by Vif.run *)
let s = Mirage_crypto_rng.generate 64 in
let open Vif.Response in
let open Syntax in
let* () = add ~field:"content-type" "application/octet-stream" in
let* () = with_string req s in
respond `OK
let config req _server _env =
Logs.info (fun m -> m "GET /config");
let s = Api.(encode_exn ExchangeVersionResponse.jsont config) in
Respond.ok s req
(* TODO
for now we only have one item in each "denom group"
change this once we have denom/signkey rotation *)
let denomgroup_of_denomdata
Denomination.
{
pub;
value;
stamp_start;
stamp_expire_withdraw;
stamp_expire_deposit;
stamp_expire_legal;
fee_withdraw;
fee_deposit;
fee_refresh;
fee_refund;
age_mask= _;
h_pub= _;
master_sig;
revoked_sig= _;
} =
let denoms =
[
RsaDenom.
{
rsa_pub= pub;
master_sig;
stamp_start;
stamp_expire_withdraw;
stamp_expire_deposit;
stamp_expire_legal;
lost= None;
};
]
in
DenomGroup.Rsa
RsaDenomGroup.
{ denoms; value; fee_withdraw; fee_deposit; fee_refresh; fee_refund }
let mk_keys ~db_conn (module Keys : Keys.S) ~last_issue_date =
let version = Api.protocol_version in
let base_url = Config.base_url in
let currency = Config.currency in
let shopping_url = Config.shopping_url in
let open_banking_gateway = Config.open_banking_gateway_url in
let bank_compliance_language = Config.bank_compliance_language in
let currency_specification =
let v = Config.Currency.v in
let alt_unit_names =
Parse_config.Alt_unit_names.encode_exn v.alt_unit_names
in
CurrencySpecification.
{
name= v.name;
num_fractional_input_digits= v.fractional_input_digits;
num_fractional_normal_digits= v.fractional_normal_digits;
num_fractional_trailing_zero_digits= v.fractional_trailing_zero_digits;
alt_unit_names;
common_amounts= [];
}
in
let tiny_amount = Config.tiny_amount in
let stefan_abs = Config.stefan_abs in
let stefan_log = Config.stefan_log in
let stefan_lin = Config.stefan_lin in
(* todo asset_type
Type of the asset. "fiat", "crypto", "regional" or "stock". *)
let asset_type = "xxx" in
let* accounts = Pg.get_wire_accounts db_conn |> unwrap_err_caqti in
let* wire_fees =
(* todo
where does wire_methods comes from? *)
let wire_method = "xxx" in
let+ wire_fees =
Pg.get_wire_fees db_conn ~wire_method |> unwrap_err_caqti
in
String_map.singleton wire_method wire_fees
in
let wads =
(* TODO wads *)
[]
in
let rewards_allowed = false in
let kyc_enabled = false in
let disable_direct_deposit = (* todo *) false in
let master_public_key = Config.master_public_key in
let reserve_closing_delay = Config.Exchangedb.idle_reserve_expiration_time in
(* todo *)
let wallet_balance_limit_without_kyc = None in
let hard_limits = [] in
let zero_limits = [] in
let dn_l =
(*Pg.get_denominations db_conn |> unwrap_err_caqti *)
Keys.get_denominations ()
|>
(* reverse chronological order *)
List.sort (fun a b ->
Stdlib.compare b.Denomination.stamp_start a.stamp_start)
in
let list_issue_date =
match dn_l with
| [] -> Timestamp.never
| dn :: _ -> dn.Denomination.stamp_start
in
let denominations =
(* if `?last_issue_date` query param does not exactly match the `stamp_start`
of one of the denomination keys, all keys are returned *)
let open Denomination in
let l =
match last_issue_date with
| None -> dn_l
| Some last_issue_date -> (
match
List.find_opt
(fun v -> Timestamp.compare v.stamp_start last_issue_date = 0)
dn_l
with
| None -> dn_l
| Some _ ->
List.filter
(fun v ->
Time.Timestamp.compare v.stamp_start last_issue_date >= 0)
dn_l)
in
List.map denomgroup_of_denomdata l
in
let signkeys =
(*
let now = Ptime_clock.now () |> Option.some in
let+ signkey_data_l = Pg.get_active_signkeys db_conn ~now |> unwrap_err_caqti in*)
Keys.get_signkeys ()
|> List.sort (fun a b ->
let open Signkey in
Stdlib.compare b.stamp_start a.stamp_start)
|> List.map Api.SignKey.of_signkey
in
let exchange_pub =
(* the eddsa pub key used to sign exchange_sig *)
match signkeys with
| [] -> Fmt.failwith "exchange has no active signkey"
| sk :: _ -> sk.SignKey.key
in
let exchange_sig =
(* Compact EdDSA signature (binary-only) over the
contatentation of all of the master_sigs (in reverse
chronological order by group) in the arrays under "denominations". *)
let hc =
dn_l
|> List.map (fun dn -> dn.Denomination.master_sig)
|> List.map Signatures.DenominationKeyValidity.to_octets
|> String.concat ""
|> Hash.H64.hash
in
let open Signatures.ExchangeKeySet in
sign_f
~f:(Keys.sign_with_signkey ~pub:exchange_pub)
R.{ list_issue_date; hc }
in
let recoup = (* TODO /recoup *) [] in
let* global_fees =
Pg.get_global_fees db_conn ~start_date:Timestamp.zero |> unwrap_err_caqti
in
let* auditors =
(* TODO /auditors/$AUDITOR_PUB/$H_DENOM_PUB *)
(* does not contains auditor_keys with empty denomination_keys *)
Pg.get_auditor_keys db_conn
in
let extensions = None in
let extensions_sig = None in
Ok
ExchangeKeysResponse.
{
version;
base_url;
currency;
shopping_url;
open_banking_gateway;
bank_compliance_language;
currency_specification;
tiny_amount;
stefan_abs;
stefan_log;
stefan_lin;
asset_type;
accounts;
wire_fees;
wads;
rewards_allowed;
kyc_enabled;
disable_direct_deposit;
master_public_key;
reserve_closing_delay;
wallet_balance_limit_without_kyc;
hard_limits;
zero_limits;
denominations;
exchange_sig;
exchange_pub;
recoup;
global_fees;
list_issue_date;
auditors;
signkeys;
extensions;
extensions_sig;
}
let jsont = ExchangeKeysResponse.jsont
let keys req server _env =
Logs.info (fun m -> m "GET /keys");
let db_conn = Vif.Server.device Devices.db_connection server in
let keys = Vif.Server.device Devices.keys server in
let res =
let* last_issue_date =
match Vif.Queries.get req "last_issue_date" with
| [] -> Ok None
| v :: _ -> (
match int_of_string_opt v with
| None ->
Error
"invalid `?last_issue_date` query param, int_of_string failure"
| Some n -> Ok (Some (Time.Timestamp.of_s (Int64.of_int n))))
in
let* v = mk_keys ~db_conn keys ~last_issue_date in
let s = Api.encode_exn jsont v in
Ok s
in
Respond.result res req

View file

@ -0,0 +1,637 @@
open Syntax
open Api
open Hash
module Keys_get = struct
let mk_future_keys_response (module Keys : Keys.S) =
let future_signkeys = Keys.get_future_signkeys () in
let future_denoms = Keys.get_future_denominations () in
let master_pub = Config.Exchange.master_public_key in
let denom_secmod_public_key = Keys.sm_pubkey in
let signkey_secmod_public_key = Keys.sm_pubkey in
FutureKeysResponse.
{
future_denoms;
future_signkeys;
master_pub;
denom_secmod_public_key;
signkey_secmod_public_key;
}
let jsont = FutureKeysResponse.jsont
let f req server _env =
Logs.info (fun m -> m "GET /management/keys/");
let sm = Vif.Server.device Devices.secmod server in
let res =
let v = mk_future_keys_response sm in
let s = Api.encode_exn jsont v in
Ok s
in
Respond.result res req
end
module Keys_post = struct
let error_key_unknown =
"404 not found, One of the keys for which a signature was provided is \
unknown to the exchange."
let verify_denom_signature (module Keys : Keys.S)
DenomSignature.{ h_denom_pub; master_sig } =
let* denom =
Keys.find_future_denomination h_denom_pub
|> Option.to_result ~none:error_key_unknown
in
let open Signatures.DenominationKeyValidity in
let r : r =
{
master= Config.master_public_key;
start= denom.stamp_start;
expire_withdraw= denom.stamp_expire_withdraw;
expire_spend= denom.stamp_expire_deposit;
expire_legal= denom.stamp_expire_legal;
value= denom.value;
fee_withdraw= denom.fee_withdraw;
fee_deposit= denom.fee_deposit;
fee_refresh= denom.fee_refresh;
fee_refund= denom.fee_refund;
denom_hash= h_denom_pub;
}
in
verify_f ~f:Keys.verify_with_master_key master_sig r
let verify_signkey_signature (module Keys : Keys.S)
SignKeySignature.{ key; master_sig } =
let* signkey =
Keys.find_future_signkey key |> Option.to_result ~none:error_key_unknown
in
let open Signatures.ExchangeSigningKeyValidity in
let r : r =
{
start= signkey.stamp_start;
expire= signkey.stamp_expire;
end_= signkey.stamp_end;
signkey_pub= signkey.key;
}
in
verify_f ~f:Keys.verify_with_master_key master_sig r
let verify sm MasterSignatures.{ denom_sigs; signkey_sigs } =
let* () = list_iter (verify_denom_signature sm) denom_sigs in
let* () = list_iter (verify_signkey_signature sm) signkey_sigs in
Ok ()
let do_ ~db_conn:_ (module Keys : Keys.S)
MasterSignatures.{ denom_sigs; signkey_sigs } =
let* () =
list_iter
(fun SignKeySignature.{ key; master_sig } ->
Keys.certify_future_signkey key ~master_sig)
signkey_sigs
in
let* () =
list_iter
(fun DenomSignature.{ h_denom_pub; master_sig } ->
Keys.certify_future_denomination h_denom_pub ~master_sig)
denom_sigs
in
let* () = Keys.save () in
Ok ()
let jsont = MasterSignatures.jsont
let f req server _env =
Logs.info (fun m -> m "POST /management/keys/");
let db_conn = Vif.Server.device Devices.db_connection server in
let sm = Vif.Server.device Devices.secmod server in
let res =
let* v = Vif.Request.of_json req |> unwrap_err_msg in
let* () = verify sm v in
let* () = do_ ~db_conn sm v in
Ok ""
in
Respond.result res req
end
module Denom_revoke = struct
let verify (module Keys : Keys.S) h_denom_pub
DenomRevocationSignature.{ master_sig } =
let open Signatures.MasterDenominationKeyRevocation in
verify_f ~f:Keys.verify_with_master_key master_sig { h_denom_pub }
let do_ ~db_conn (module Keys : Keys.S) h_denom_pub
DenomRevocationSignature.{ master_sig } =
let* () = Keys.revoke_denomination h_denom_pub master_sig in
let+ () =
Pg.insert_denomination_revocation db_conn h_denom_pub master_sig
|> unwrap_err_caqti
in
()
let jsont = DenomRevocationSignature.jsont
let f req h_denom_pub server _env =
Logs.info (fun m -> m "POST /management/denominations/$H_DENOM_PUB/revoke/");
let db_conn = Vif.Server.device Devices.db_connection server in
let sm = Vif.Server.device Devices.secmod server in
let res =
let* h_denom_pub = DenominationHash.of_b32 h_denom_pub in
let* v = Vif.Request.of_json req |> unwrap_err_msg in
let* () = verify sm h_denom_pub v in
let* () = do_ ~db_conn sm h_denom_pub v in
Ok ""
in
Respond.result res req
end
module Signkey_revoke = struct
let verify (module Keys : Keys.S) exchange_pub
SignkeyRevocationSignature.{ master_sig } =
let open Signatures.MasterSigningKeyRevocation in
verify_f ~f:Keys.verify_with_master_key master_sig { exchange_pub }
let do_ ~db_conn (module Keys : Keys.S) exchange_pub
SignkeyRevocationSignature.{ master_sig } =
let* () = Keys.revoke_signkey exchange_pub master_sig in
let+ () =
Pg.insert_signkey_revocation db_conn exchange_pub master_sig
|> unwrap_err_caqti
in
()
let jsont = SignkeyRevocationSignature.jsont
let f req exchange_pub server _env =
Logs.info (fun m -> m "POST /management/signkeys/$EXCHANGE_PUB/revoke/");
let db_conn = Vif.Server.device Devices.db_connection server in
let sm = Vif.Server.device Devices.secmod server in
let res =
let* exchange_pub = Crypto.EddsaPublicKey.of_b32 exchange_pub in
let* v = Vif.Request.of_json req |> unwrap_err_msg in
let* () = verify sm exchange_pub v in
let* () = do_ ~db_conn sm exchange_pub v in
Ok ""
in
Respond.result res req
end
module Auditors = struct
let verify (module Keys : Keys.S)
AuditorSetupMessage.
{
auditor_url;
auditor_name= _;
auditor_pub;
master_sig;
validity_start;
} =
let open Signatures.MasterAddAuditor in
verify_f ~f:Keys.verify_with_master_key master_sig
{
start_date= validity_start;
auditor_pub;
h_auditor_url= Hash.Cstring.H64.hash auditor_url;
}
(* TODO monotonic time *)
let do_ ~db_conn v =
let auditor_pub = v.AuditorSetupMessage.auditor_pub in
let validity_start = v.AuditorSetupMessage.validity_start in
let* last_date_opt =
Pg.get_auditor_timestamp db_conn auditor_pub |> unwrap_err_caqti
in
match last_date_opt with
| None ->
let+ () = Pg.insert_auditor db_conn v |> unwrap_err_caqti in
Logs.info (fun m -> m "enabled auditor");
()
| Some last_date ->
if Timestamp.compare last_date validity_start > 0 then
Error
"database has more recent auditor data for this auditor public key"
else
let+ () = Pg.update_auditor db_conn v |> unwrap_err_caqti in
Logs.info (fun m -> m "updated auditor");
()
let jsont = AuditorSetupMessage.jsont
let f req server _env =
Logs.info (fun m -> m "POST /management/auditors/");
let sm = Vif.Server.device Devices.secmod server in
let db_conn = Vif.Server.device Devices.db_connection server in
let res =
let* v = Vif.Request.of_json req |> unwrap_err_msg in
let* () = verify sm v in
let* () = do_ ~db_conn v in
Ok ""
in
Respond.result res req
end
module Auditors_disable = struct
let verify (module Keys : Keys.S) auditor_pub
AuditorTeardownMessage.{ master_sig; validity_end } =
let open Signatures.MasterDelAuditor in
verify_f ~f:Keys.verify_with_master_key master_sig
{ end_date= validity_end; auditor_pub }
let do_ ~db_conn auditor_pub
AuditorTeardownMessage.{ master_sig= _; validity_end } =
let* last_date_opt =
Pg.get_auditor_timestamp db_conn auditor_pub |> unwrap_err_caqti
in
match last_date_opt with
| None -> Error "auditor not found"
| Some last_date ->
if Timestamp.compare last_date validity_end > 0 then
Error
"database has more recent auditor data for this auditor public key"
else
let+ () =
Pg.disable_auditor db_conn ~auditor_pub ~change_date:validity_end
|> unwrap_err_caqti
in
()
let jsont = AuditorTeardownMessage.jsont
let f req auditor_pub server _env =
Logs.info (fun m -> m "POST /management/auditors/$AUDITOR_PUB/revoke/");
let sm = Vif.Server.device Devices.secmod server in
let db_conn = Vif.Server.device Devices.db_connection server in
let res =
let* auditor_pub = Crypto.EddsaPublicKey.of_b32 auditor_pub in
let* v = Vif.Request.of_json req |> unwrap_err_msg in
let* () = verify sm auditor_pub v in
let* () = do_ ~db_conn auditor_pub v in
Ok ""
in
Respond.result res req
end
module Wire_fee = struct
let verify (module Keys : Keys.S)
WireFeeSetupMessage.
{
wire_method;
master_sig_wire;
fee_start;
fee_end;
closing_fee;
wire_fee;
} =
let open Signatures.MasterWireFee in
verify_f ~f:Keys.verify_with_master_key master_sig_wire
{
h_wire_method= Hash.Cstring.H64.hash wire_method;
start_date= fee_start;
end_date= fee_end;
wire_fee;
closing_fee;
}
let do_ ~db_conn (v : WireFeeSetupMessage.t) =
let* wire_fees =
Pg.get_wire_fees_by_time db_conn ~wire_method:v.wire_method
~start_date:v.fee_start ~end_date:v.fee_end
|> unwrap_err_caqti
in
match wire_fees with
| [] ->
let+ () = Pg.insert_wire_fee db_conn v |> unwrap_err_caqti in
Logs.info (fun m -> m "added wire fee");
()
| [ vv ] -> (
match v.master_sig_wire = vv.sig_ with
| false ->
Error "a different wire-fee was already setup for this time frame"
| true ->
Logs.info (fun m -> m "an identical wire-fee was already setup");
Ok ())
| _ ->
Error
"invalid database state, multiple wire-fee found in database for \
this time frame"
let jsont = WireFeeSetupMessage.jsont
let f req server _env =
Logs.info (fun m -> m "POST /management/wire-fee/");
let sm = Vif.Server.device Devices.secmod server in
let db_conn = Vif.Server.device Devices.db_connection server in
let res =
let* v = Vif.Request.of_json req |> unwrap_err_msg in
let* () = verify sm v in
let* () = do_ ~db_conn v in
Ok ""
in
Respond.result res req
end
module Global_fees = struct
let verify (module Keys : Keys.S)
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_f ~f:Keys.verify_with_master_key master_sig
{
start_date;
end_date;
purse_timeout;
history_expiration;
history_fee;
account_fee;
purse_fee;
purse_account_limit;
}
let do_ ~db_conn v =
let* global_fees =
let start_date = v.GlobalFees.start_date in
let end_date = v.GlobalFees.end_date in
Pg.get_global_fees_by_time db_conn ~start_date ~end_date
|> unwrap_err_caqti
in
match global_fees with
| [] ->
let+ () = Pg.insert_global_fees db_conn v |> unwrap_err_caqti in
Logs.info (fun m -> m "added global fees");
()
| [ vv ] -> (
match v.master_sig = vv.master_sig with
| false ->
Error
"a different global-fees was already setup for this time frame"
| true ->
Logs.info (fun m -> m "an identical global-fees was already setup");
Ok ())
| _ ->
Error
"invalid database state, multiple global-fees found in database for \
this time frame"
let jsont = GlobalFees.jsont
(* TODO better global_fees
ensure it is defined for the current time.
there should be only one global_fees for each moment in time
and once set for a timeframe, it should not change. *)
let f req server _env =
Logs.info (fun m -> m "POST /management/global-fees/");
let sm = Vif.Server.device Devices.secmod server in
let db_conn = Vif.Server.device Devices.db_connection server in
let res =
let* v = Vif.Request.of_json req |> unwrap_err_msg in
let* () = verify sm v in
let* () = do_ ~db_conn v in
Ok ""
in
Respond.result res req
end
module Wire = struct
let verify (module Keys : Keys.S)
WireSetupMessage.
{
payto_uri;
master_sig_wire;
master_sig_add;
validity_start;
bank_label= _;
priority= _;
} =
(* TODO are those read from payto_uri? *)
let conversion_url = "" in
let credit_restrictions = "" in
let debit_restrictions = "" in
let* () =
let open Signatures.MasterWireDetails in
verify_f ~f:Keys.verify_with_master_key master_sig_wire
{
h_wire_details= FullPaytoHash.hash payto_uri;
h_conversion_url= Hash.Cstring.H64.hash conversion_url;
h_credit_restrictions= Hash.Cstring.H64.hash credit_restrictions;
h_debit_restrictions= Hash.Cstring.H64.hash debit_restrictions;
}
in
let* () =
let open Signatures.MasterAddWire in
verify_f ~f:Keys.verify_with_master_key master_sig_add
{
start_date= validity_start;
h_wire= FullPaytoHash.hash payto_uri;
h_conversion_url= Hash.Cstring.H64.hash conversion_url;
h_credit_restrictions= Hash.Cstring.H64.hash credit_restrictions;
h_debit_restrictions= Hash.Cstring.H64.hash debit_restrictions;
}
in
Ok ()
let do_ ~db_conn v =
let* last_change_opt =
let payto_uri = v.WireSetupMessage.payto_uri in
Pg.get_wire_timestamp db_conn ~payto_uri |> unwrap_err_caqti
in
match last_change_opt with
| Some _ -> Error "wire already setup"
| None ->
let r =
ExchangeWireAccount.
{
payto_uri= v.payto_uri;
conversion_url= None;
debit_restrictions= [];
credit_restrictions= [];
master_sig= v.master_sig_wire;
bank_label= v.bank_label;
priority= v.priority;
}
in
let+ () =
Pg.insert_wire db_conn ~last_change:v.validity_start r
|> unwrap_err_caqti
in
()
let jsont = WireSetupMessage.jsont
let f req server _env =
Logs.info (fun m -> m "POST /management/wire/");
let sm = Vif.Server.device Devices.secmod server in
let db_conn = Vif.Server.device Devices.db_connection server in
let res =
let* v = Vif.Request.of_json req |> unwrap_err_msg in
let* () = verify sm v in
let* () = do_ ~db_conn v in
Ok ""
in
Respond.result res req
end
module Wire_disable = struct
let verify (module Keys : Keys.S)
WireTeardownMessage.{ payto_uri; master_sig_del; validity_end } =
let open Signatures.MasterDelWire in
verify_f ~f:Keys.verify_with_master_key master_sig_del
{ end_date= validity_end; h_wire= FullPaytoHash.hash payto_uri }
let do_ ~db_conn
WireTeardownMessage.{ payto_uri; master_sig_del= _; validity_end } =
let* last_change_opt =
Pg.get_wire_timestamp db_conn ~payto_uri |> unwrap_err_caqti
in
match last_change_opt with
| None -> Error "wire not found"
| Some _ ->
let+ () =
Pg.disable_wire db_conn ~payto_uri ~validity_end |> unwrap_err_caqti
in
()
let jsont = WireTeardownMessage.jsont
let f req server _env =
Logs.info (fun m -> m "POST /management/wire/disable/");
let sm = Vif.Server.device Devices.secmod server in
let db_conn = Vif.Server.device Devices.db_connection server in
let res =
let* v = Vif.Request.of_json req |> unwrap_err_msg in
let* () = verify sm v in
let* () = do_ ~db_conn v in
Ok ""
in
Respond.result res req
end
module Drain = struct
let verify (module Keys : Keys.S)
DrainProfitsMessage.
{
debit_account_section;
credit_payto_uri;
wtid;
master_sig;
date;
amount;
} =
let open Signatures.MasterDrainProfit in
verify_f ~f:Keys.verify_with_master_key master_sig
{
wtid;
date;
amount;
h_section= Hash.Cstring.H64.hash debit_account_section;
h_payto= FullPaytoHash.hash credit_payto_uri;
}
let do_ ~db_conn v =
let+ () = Pg.insert_drain_profit db_conn v |> unwrap_err_caqti in
()
let jsont = DrainProfitsMessage.jsont
let f req server _env =
Logs.info (fun m -> m "POST /management/drain/");
let sm = Vif.Server.device Devices.secmod server in
let db_conn = Vif.Server.device Devices.db_connection server in
let res =
let* v = Vif.Request.of_json req |> unwrap_err_msg in
let* () = verify sm v in
let* () = do_ ~db_conn v in
Ok ""
in
Respond.result res req
end
module AmlOfficer = struct
let verify (module Keys : Keys.S)
AmlOfficerSetup.
{
officer_pub;
officer_name;
is_active;
read_only= _;
master_sig;
change_date;
} =
let open Signatures.MasterAmlOfficerStatus in
let is_active = match is_active with true -> 1_l | false -> 0_l in
verify_f ~f:Keys.verify_with_master_key master_sig
{
change_date;
officer_pub;
h_officer_name= Hash.Cstring.H64.hash officer_name;
is_active;
}
let do_ ~db_conn v =
let+ _last_change = Pg.insert_aml_officer db_conn v |> unwrap_err_caqti in
()
let jsont = AmlOfficerSetup.jsont
let f req server _env =
Logs.info (fun m -> m "POST /management/aml-officers/");
let sm = Vif.Server.device Devices.secmod server in
let db_conn = Vif.Server.device Devices.db_connection server in
let res =
let* v = Vif.Request.of_json req |> unwrap_err_msg in
let* () = verify sm v in
let* () = do_ ~db_conn v in
Ok ""
in
Respond.result res req
end
module Partners = struct
let verify (module Keys : Keys.S)
ExchangePartnerSetupRequest.
{
partner_base_url;
partner_pub;
wad_frequency;
master_sig;
start_date;
end_date;
wad_fee;
} =
let open Signatures.PartnerConfiguration in
verify_f ~f:Keys.verify_with_master_key master_sig
{
partner_pub;
start_date;
end_date;
wad_frequency;
wad_fee;
h_url= Hash.Cstring.H64.hash partner_base_url;
}
let do_ ~db_conn v =
let+ () = Pg.insert_partner db_conn v |> unwrap_err_caqti in
()
let jsont = ExchangePartnerSetupRequest.jsont
let f req server _env =
Logs.info (fun m -> m "POST /management/partners/");
let sm = Vif.Server.device Devices.secmod server in
let db_conn = Vif.Server.device Devices.db_connection server in
let res =
let* v = Vif.Request.of_json req |> unwrap_err_msg in
let* () = verify sm v in
let* () = do_ ~db_conn v in
Ok ""
in
Respond.result res req
end

View file

@ -0,0 +1,49 @@
(* /terms + /privacy *)
let aux asset req _server _env =
let etag = Assets.etag asset in
let headers = Vif.Request.headers req in
let has_matching_etag =
match Vif.Headers.get headers "if-none-match" with
| None -> Ok false
| Some s ->
Headers_lib.If_none_match.parse s
|> Result.map (Headers_lib.If_none_match.evaluate etag)
in
match has_matching_etag with
| Error e -> Respond.bad_request ~hint:e req
| Ok true -> Respond.not_modified ()
| Ok false ->
let mime = Headers.select_mimetype headers in
let lang = Headers.select_language headers in
let compression = Headers.select_encoding headers in
let data = Assets.get_content ~mime ~lang asset in
(* -- *)
let open Vif.Response in
let open Syntax in
let* () = with_string ?compression req data in
let* () =
let etag_field_value = Headers_lib.Etag.to_field_string etag in
add ~field:"etag" etag_field_value
in
let* () =
add ~field:"taler-terms-version"
Assets.Assets_config.terms_legal_version
in
let* () =
add ~field:"avail-languages" Headers.avail_languages_header_value
in
let* () =
let content_type = Fmt.str "%a" Assets.Mimetype.pp mime in
add ~field:"content-type" content_type
in
let* () = add ~field:"content-language" lang in
respond `OK
let terms req _server _env =
Logs.info (fun m -> m "GET /terms");
aux Assets.Terms req _server _env
let privacy req _server _env =
Logs.info (fun m -> m "GET /privacy");
aux Assets.Privacy req _server _env

View file

@ -0,0 +1,476 @@
module type S = sig
open Crypto
val sm_pubkey : eddsa_pub
val sign_with_sm_key : string -> eddsa_sig
val sign_with_signkey : pub:eddsa_pub -> string -> eddsa_sig
val verify_with_master_key : eddsa_sig -> msg:string -> (unit, string) result
val verify_with_sm_key : eddsa_sig -> msg:string -> (unit, string) result
val verify_with_signkey :
pub:eddsa_pub -> eddsa_sig -> msg:string -> (unit, string) result
val get_signkeys : unit -> Signkey.t list
val get_denominations : unit -> Denomination.t list
val get_future_signkeys : unit -> Api.FutureSignKey.t list
val get_future_denominations : unit -> Api.FutureDenom.t list
val find_signkey : eddsa_pub -> Signkey.t option
val find_denomination : denom_hash -> Denomination.t option
val find_future_signkey : eddsa_pub -> Api.FutureSignKey.t option
val find_future_denomination : denom_hash -> Api.FutureDenom.t option
val certify_future_signkey :
eddsa_pub ->
master_sig:Signatures.ExchangeSigningKeyValidity.t ->
(unit, string) result
val certify_future_denomination :
denom_hash ->
master_sig:Signatures.DenominationKeyValidity.t ->
(unit, string) result
val revoke_signkey :
eddsa_pub ->
Signatures.MasterSigningKeyRevocation.t ->
(unit, string) result
val revoke_denomination :
denom_hash ->
Signatures.MasterDenominationKeyRevocation.t ->
(unit, string) result
val save : unit -> (unit, string) result
end
module Make (Conn : Pg.CONN) = struct
open Syntax
open Crypto
module DenominationHash = Hash.DenominationHash
let read fname = Bos.OS.File.read fname |> unwrap_err_msg
let write fname s = Bos.OS.File.write fname s |> unwrap_err_msg
let write_eddsa fname priv = write fname (EddsaPrivateKey.to_octets priv)
let write_rsa fname priv = write fname (RsaPrivateKey.to_octets priv)
let read_eddsa fname =
let* data = read fname in
EddsaPrivateKey.of_octets data
let read_rsa fname =
let* data = read fname in
RsaPrivateKey.of_octets data
type sk = Signkey.t
type future_sk = Api.FutureSignKey.t
type dn = Denomination.t
type future_dn = Api.FutureDenom.t
(* TODO ! use lock *)
(* not sure what to do with coin section_name, rm if possible *)
type t = {
sm_key: eddsa_priv;
sm_pubkey: eddsa_pub;
sk_ht: (eddsa_pub, sk) Hashtbl.t;
dn_ht: (denom_hash, dn) Hashtbl.t;
sk_key_ht: (eddsa_pub, eddsa_priv) Hashtbl.t;
dn_key_ht: (denom_hash, rsa_priv) Hashtbl.t;
future_sk_ht: (eddsa_pub, future_sk) Hashtbl.t;
future_dn_ht: (denom_hash, future_dn) Hashtbl.t;
future_sk_key_ht: (eddsa_pub, eddsa_priv) Hashtbl.t;
future_dn_key_ht: (denom_hash, rsa_priv) Hashtbl.t;
dn_section_name_ht: (denom_hash, string) Hashtbl.t;
}
let conn = (module Conn : Pg.CONN)
let sm_key_fname = Fpath.(Config.secmod_dir / "sm_key")
let sk_fname i = Fpath.(Config.secmod_dir / Fmt.str "sk_%d" i)
let dn_fname section_name =
Fpath.(Config.secmod_dir / Fmt.str "dn_%s" section_name)
let sign_with_sm_key t s = EddsaSignature.sign ~key:t.sm_key s
let make_future_sk t =
let start = Time.Absolute.of_ptime (Ptime_clock.now ()) in
let expire =
Time.Absolute.add start Config.Exchange.signkey_legal_duration
in
let stamp_start = Timestamp.of_absolute start in
let stamp_expire = Timestamp.of_absolute expire in
let stamp_end = stamp_expire in
let priv, pub = Mirage_crypto_ec.Ed25519.generate () in
let signkey_secmod_sig =
let open Signatures.SigningKeyAnnouncement in
let exchange_pub = pub in
let anchor_time = stamp_start in
let duration = Timestamp.diff stamp_start stamp_expire in
sign_f ~f:(sign_with_sm_key t) { exchange_pub; anchor_time; duration }
in
let future_sk =
Api.FutureSignKey.
{ key= pub; stamp_start; stamp_expire; stamp_end; signkey_secmod_sig }
in
Hashtbl.replace t.future_sk_ht pub future_sk;
Hashtbl.replace t.future_sk_key_ht pub priv;
()
let make_future_dn t
Config.Coin.
{
section_name;
value;
duration_withdraw;
duration_spend;
duration_legal;
fee_withdraw;
fee_deposit;
fee_refresh;
fee_refund;
cipher;
rsa_keysize;
age_restricted= _;
} =
assert (cipher = `RSA);
let start = Time.Absolute.of_ptime (Ptime_clock.now ()) in
let stamp_start = Timestamp.of_absolute start in
let stamp_expire_withdraw =
Timestamp.of_absolute @@ Time.Absolute.add start duration_withdraw
in
let stamp_expire_deposit =
Timestamp.of_absolute @@ Time.Absolute.add start duration_spend
in
let stamp_expire_legal =
Timestamp.of_absolute @@ Time.Absolute.add start duration_legal
in
let priv, pub = RsaPrivateKey.generate ~bits:rsa_keysize () in
let open Api in
let rsa_denomination_key =
RsaDenominationKey.{ age_mask= 0; rsa_pub= pub }
in
let denom_pub = DenominationKey.Rsa rsa_denomination_key in
let h_pub = DenominationHash.hash (RsaPublicKey.to_octets pub) in
let denom_secmod_sig =
let open Signatures.DenominationKeyAnnouncement in
let h_denom_pub = h_pub in
let h_section_name = Hash.Cstring.H64.hash section_name in
let anchor_time = stamp_start in
let duration_withdraw =
Timestamp.diff stamp_start stamp_expire_withdraw
in
sign_f ~f:(sign_with_sm_key t)
{ h_denom_pub; h_section_name; anchor_time; duration_withdraw }
in
let future_dn =
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;
}
in
Hashtbl.replace t.future_dn_ht h_pub future_dn;
Hashtbl.replace t.future_dn_key_ht h_pub priv;
()
let make_new () =
let sm_key, sm_pubkey = Mirage_crypto_ec.Ed25519.generate () in
let t =
{
sm_key;
sm_pubkey;
sk_ht= Hashtbl.create 0xff;
dn_ht= Hashtbl.create 0xff;
sk_key_ht= Hashtbl.create 0xff;
dn_key_ht= Hashtbl.create 0xff;
future_sk_ht= Hashtbl.create 0xff;
future_dn_ht= Hashtbl.create 0xff;
future_sk_key_ht= Hashtbl.create 0xff;
future_dn_key_ht= Hashtbl.create 0xff;
dn_section_name_ht= Hashtbl.create 0xff;
}
in
make_future_sk t;
List.iter (make_future_dn t) Config.Coin.all_coins;
t
let database_find_sk conn pub =
let* opt = Pg.find_signkey conn pub |> unwrap_err_caqti in
match opt with
| None -> Fmt.error "Keys: signkey data not found in database"
| Some sk_data -> Ok sk_data
let database_find_dn conn h_pub =
let* opt = Pg.find_denom conn h_pub |> unwrap_err_caqti in
match opt with
| None -> Fmt.error "Keys: denomination data not found in database"
| Some dn_data -> Ok dn_data
let list_to_ht l = Hashtbl.of_seq (List.to_seq l)
let load () =
let* sm_key = read_eddsa sm_key_fname in
let sm_pubkey = EddsaPrivateKey.pub_of_priv sm_key in
let* sk_keys = list_map read_eddsa (List.init 1 sk_fname) in
let* sk_l =
list_map
(fun priv ->
let pub = EddsaPrivateKey.pub_of_priv priv in
let+ sk = database_find_sk conn pub in
((pub, sk), (pub, priv)))
sk_keys
in
let sk_ht, sk_key_ht =
match List.split sk_l with l1, l2 -> (list_to_ht l1, list_to_ht l2)
in
let dn_section_name_ht = Hashtbl.create 0xff in
let* dn_keys =
list_map
(fun coin ->
let section_name = coin.Config.Coin.section_name in
let+ priv = read_rsa (dn_fname section_name) in
(section_name, priv))
Config.Coin.all_coins
in
let* dn_l =
list_map
(fun (section_name, priv) ->
let h_pub =
priv
|> RsaPrivateKey.pub_of_priv
|> RsaPublicKey.to_octets
|> DenominationHash.hash
in
(* fill dn_section_name_ht *)
Hashtbl.replace dn_section_name_ht h_pub section_name;
let+ dn = database_find_dn conn h_pub in
((h_pub, dn), (h_pub, priv)))
dn_keys
in
let dn_ht, dn_key_ht =
match List.split dn_l with l1, l2 -> (list_to_ht l1, list_to_ht l2)
in
(* future keys are not stored anywhere until they are certified with a master_sig
so we don't have any future key to load *)
let t =
{
sm_key;
sm_pubkey;
sk_ht;
dn_ht;
sk_key_ht;
dn_key_ht;
future_sk_ht= Hashtbl.create 0xff;
future_dn_ht= Hashtbl.create 0xff;
future_sk_key_ht= Hashtbl.create 0xff;
future_dn_key_ht= Hashtbl.create 0xff;
dn_section_name_ht;
}
in
Ok t
let init () =
let dir = Config.secmod_dir in
let* b = Bos.OS.Dir.create ~mode:0o700 dir |> unwrap_err_msg in
if b then Logs.info (fun m -> m "Keys: created directory `%a`" Fpath.pp dir);
let* l =
Bos.OS.Dir.contents ~dotfiles:false ~rel:false dir |> unwrap_err_msg
in
match List.is_empty l with
| true ->
Logs.info (fun m -> m "Keys: empty storage, generating fresh keys");
let t = make_new () in
Ok t
| false ->
Logs.info (fun m -> m "Keys: loading keys from storage");
load ()
let t =
match init () with
| Error e -> Fmt.failwith "Keys: initialization failure: `%s`." e
| Ok t ->
Logs.info (fun m -> m "Keys: initialized");
t
let sm_pubkey = t.sm_pubkey
let sign_with_sm_key s = sign_with_sm_key t s
let sign_with_signkey ~pub s =
match Hashtbl.find_opt t.sk_key_ht pub with
| None -> Fmt.failwith "Keys sign_with_signkey failure: not found."
| Some priv -> EddsaSignature.sign ~key:priv s
let verify_with_sm_key s ~msg = EddsaSignature.verify ~key:t.sm_pubkey s ~msg
let verify_with_master_key =
EddsaSignature.verify ~key:Config.master_public_key
let verify_with_signkey ~pub s ~msg =
match Hashtbl.find_opt t.sk_ht pub with
| None -> Fmt.failwith "Keys verify_with_signkey failure: not found."
| Some _sk -> EddsaSignature.verify ~key:pub s ~msg
let get_signkeys () = t.sk_ht |> Hashtbl.to_seq_values |> List.of_seq
let get_denominations () = t.dn_ht |> Hashtbl.to_seq_values |> List.of_seq
let get_future_signkeys () =
t.future_sk_ht |> Hashtbl.to_seq_values |> List.of_seq
let get_future_denominations () =
t.future_dn_ht |> Hashtbl.to_seq_values |> List.of_seq
let find_signkey pub = Hashtbl.find_opt t.sk_ht pub
let find_denomination h_pub = Hashtbl.find_opt t.dn_ht h_pub
let find_future_signkey pub = Hashtbl.find_opt t.future_sk_ht pub
let find_future_denomination h_pub = Hashtbl.find_opt t.future_dn_ht h_pub
let certify_future_signkey pub ~master_sig =
match
( Hashtbl.find_opt t.future_sk_ht pub,
Hashtbl.find_opt t.future_sk_key_ht pub )
with
| None, _ | _, None ->
Error "Keys certify_future_signkey: future signkey not found."
| Some future_sk, Some priv -> (
match Hashtbl.find_opt t.sk_ht pub with
| Some _sk -> Error "Keys certify_future_signkey: already certified"
| None ->
let Api.FutureSignKey.
{
key;
stamp_start;
stamp_expire;
stamp_end;
signkey_secmod_sig= _;
} =
future_sk
in
let sk =
Signkey.
{
pub= key;
stamp_start;
stamp_expire;
stamp_end;
master_sig;
revoked_sig= None;
}
in
Hashtbl.replace t.sk_ht pub sk;
Hashtbl.replace t.sk_key_ht pub priv;
Hashtbl.remove t.future_sk_ht pub;
Hashtbl.remove t.future_sk_key_ht pub;
let* () = Pg.insert_signkey (module Conn) sk |> unwrap_err_caqti in
Ok ())
let certify_future_denomination h_pub ~master_sig =
match
( Hashtbl.find_opt t.future_dn_ht h_pub,
Hashtbl.find_opt t.future_dn_key_ht h_pub )
with
| None, _ | _, None ->
Error "Keys certify_future_denomination: future denomination not found."
| Some future_dn, Some priv -> (
match Hashtbl.find_opt t.dn_ht h_pub with
| Some _dn ->
Error "Keys certify_future_denomination: already certified"
| None ->
let Api.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= _;
} =
future_dn
in
let rsa_pub =
match denom_pub with
| Rsa Api.RsaDenominationKey.{ age_mask= _; rsa_pub } -> rsa_pub
in
let dn =
Denomination.
{
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;
revoked_sig= None;
}
in
Hashtbl.replace t.dn_ht h_pub dn;
Hashtbl.replace t.dn_key_ht h_pub priv;
Hashtbl.replace t.dn_section_name_ht h_pub section_name;
Hashtbl.remove t.future_dn_ht h_pub;
Hashtbl.remove t.future_dn_key_ht h_pub;
let* () = Pg.insert_denom (module Conn) dn |> unwrap_err_caqti in
Ok ())
let revoke_signkey pub revoked_sig =
match Hashtbl.find_opt t.sk_ht pub with
| None -> Error "Keys revoke_signkey: signkey not found."
| Some sk ->
let sk = { sk with revoked_sig= Some revoked_sig } in
Hashtbl.replace t.sk_ht pub sk;
(* TODO revoke, apply to database *)
Ok ()
let revoke_denomination pub revoked_sig =
match Hashtbl.find_opt t.dn_ht pub with
| None -> Error "Keys revoke_denomination: denomination not found."
| Some dn ->
let dn = { dn with revoked_sig= Some revoked_sig } in
Hashtbl.replace t.dn_ht pub dn;
(* TODO revoke, apply to database *)
Ok ()
let save () =
let* () = write_eddsa sm_key_fname t.sm_key in
let* () =
Hashtbl.to_seq_values t.sk_key_ht
|> List.of_seq
|> List.mapi (fun i priv -> write_eddsa (sk_fname i) priv)
|> list_iter Fun.id
in
let* () =
Hashtbl.to_seq t.dn_key_ht
|> List.of_seq
|> list_iter (fun (h_pub, priv) ->
match Hashtbl.find_opt t.dn_section_name_ht h_pub with
| None -> Error "Keys save: invalid state, section_name not found"
| Some section_name -> write_rsa (dn_fname section_name) priv)
in
Logs.info (fun m -> m "saved private keys data");
Ok ()
end

View file

@ -0,0 +1,45 @@
module type S = sig
open Crypto
val sm_pubkey : eddsa_pub
val sign_with_sm_key : string -> eddsa_sig
val sign_with_signkey : pub:eddsa_pub -> string -> eddsa_sig
val verify_with_master_key : eddsa_sig -> msg:string -> (unit, string) result
val verify_with_sm_key : eddsa_sig -> msg:string -> (unit, string) result
val verify_with_signkey :
pub:eddsa_pub -> eddsa_sig -> msg:string -> (unit, string) result
val get_signkeys : unit -> Signkey.t list
val get_denominations : unit -> Denomination.t list
val get_future_signkeys : unit -> Api.FutureSignKey.t list
val get_future_denominations : unit -> Api.FutureDenom.t list
val find_signkey : eddsa_pub -> Signkey.t option
val find_denomination : denom_hash -> Denomination.t option
val find_future_signkey : eddsa_pub -> Api.FutureSignKey.t option
val find_future_denomination : denom_hash -> Api.FutureDenom.t option
val certify_future_signkey :
eddsa_pub ->
master_sig:Signatures.ExchangeSigningKeyValidity.t ->
(unit, string) result
val certify_future_denomination :
denom_hash ->
master_sig:Signatures.DenominationKeyValidity.t ->
(unit, string) result
val revoke_signkey :
eddsa_pub ->
Signatures.MasterSigningKeyRevocation.t ->
(unit, string) result
val revoke_denomination :
denom_hash ->
Signatures.MasterDenominationKeyRevocation.t ->
(unit, string) result
val save : unit -> (unit, string) result
end
module Make (_ : Pg.CONN) : S

View file

@ -0,0 +1,83 @@
(* MTE - the MirageOS Taler Exchange
Copyright (C) 2025 Olivier Pierre <swrup@protonmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. *)
let hello req _server _env =
let open Vif.Response in
let open Syntax in
let* () = with_string req "Hello~~\n" in
let* () = add ~field:"content-type" "text/plain" in
respond `OK
let routes =
let open Vif.Uri in
let open Vif.Route in
let get path = get (path /?? any) in
let post path jsont = post (Vif.Type.json_encoding jsont) (path /?? any) in
let v s = rel / s in
let tos =
[
get rel --> hello;
get (v "terms") --> Http_terms.terms;
get (v "privacy") --> Http_terms.privacy;
]
in
let status_info =
[
get (v "seed") --> Http_information.seed;
get (v "config") --> Http_information.config;
get (v "keys") --> Http_information.keys;
]
in
let management =
let open Http_management in
let v s = v "management" / s in
[
get (v "keys") --> Keys_get.f;
post (v "keys") Keys_post.jsont --> Keys_post.f;
post (v "denominations" /% string `Path / "revoke") Denom_revoke.jsont
--> Denom_revoke.f;
post (v "signkeys" /% string `Path / "revoke") Signkey_revoke.jsont
--> Signkey_revoke.f;
post (v "auditors") Auditors.jsont --> Auditors.f;
post (v "auditors" /% string `Path / "disable") Auditors_disable.jsont
--> Auditors_disable.f;
post (v "wire-fee") Wire_fee.jsont --> Wire_fee.f;
post (v "global-fees") Global_fees.jsont --> Global_fees.f;
post (v "wire") Wire.jsont --> Wire.f;
post (v "wire" / "disable") Wire_disable.jsont --> Wire_disable.f;
post (v "drain") Drain.jsont --> Drain.f;
post (v "aml-officers") AmlOfficer.jsont --> AmlOfficer.f;
post (v "partners") Partners.jsont --> Partners.f;
]
in
tos @ status_info @ management
let () =
Util.Log_reporter.setup ();
let cfg =
let port = Config.Exchange.port in
let sockaddr = Unix.(ADDR_INET (inet_addr_loopback, port)) in
Vif.config ~reporter:Util.Log_reporter.reporter sockaddr
in
Miou_unix.run @@ fun () ->
Caqti_miou.Switch.run @@ fun caqti_switch ->
let env : Devices.env =
{ caqti_switch; db_uri= Config.Exchangedb_postgres.config }
in
let devices = Vif.Devices.[ Devices.db_connection; Devices.keys ] in
let middlewares = Vif.Middlewares.[] in
Logs.info (fun m ->
m ~tags:(Util.Log_reporter.detail "...") "Starting MTE server");
Vif.run ~cfg ~devices ~middlewares routes env

View file

@ -0,0 +1,232 @@
(* rudimentary configuration file parser (INI-like)
https://docs.taler.net/manpages/taler-exchange.conf.5.html
do not support "$"-path expansion *)
open Angstrom
type item = {
key: string;
value: string;
}
type section = {
header: string;
items: item list;
}
let fail fmt =
let k _ppf = exit 1 in
Fmt.kpf k Fmt.stderr ("Configuration failure: " ^^ fmt ^^ ".@.")
let is_eol = function '\n' | '\r' -> true | _ -> false
let is_whitespace = function ' ' | '\t' -> true | _ -> false
let blanks = skip_while is_whitespace
module Config_section = struct
type t =
| Blank
| Comment of string
| Header of string
| Item of item
let id =
let ident_char = function
| 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '_' | '-' -> true
| _ -> false
in
take_while1 ident_char >>| String.lowercase_ascii
let line = take_till is_eol <* end_of_line
let blank_line = blanks <* end_of_line >>| fun () -> Blank
let comment = blanks *> (char '#' <|> char '%') *> line >>| fun s -> Comment s
let header =
blanks *> char '[' *> id <* char ']' <* blanks <* end_of_line >>| fun s ->
Header s
let item_value =
let unquoted_value =
take_while1 (fun c -> not (is_whitespace c || is_eol c))
<* blanks
<* end_of_line
in
let quoted_value =
char '"' *> take_till is_eol <* end_of_line >>= fun s ->
match String.ends_with ~suffix:"\"" s && s <> "\"" with
| false -> fail "invalid quoted value"
| true ->
let value = String.sub s 0 (String.length s - 1) in
return value
in
quoted_value <|> unquoted_value
let item =
lift2
(fun key value -> Item { key; value })
id
(blanks *> char '=' *> blanks *> item_value)
let config =
many (choice [ blank_line; comment; header; item ]) <* end_of_input
let fold_sections l =
let rec loop section_l item_l l =
match l with
| [] ->
if List.is_empty item_l then section_l
else fail "invalid configuration structure"
| Blank :: tl | Comment _ :: tl -> loop section_l item_l tl
| Item item :: tl -> loop section_l (item :: item_l) tl
| Header header :: tl ->
let section = { header; items= item_l } in
loop (section :: section_l) [] tl
in
loop [] [] (List.rev l)
let parse s =
match parse_string ~consume:All config s with
| Error msg -> fail "parse error `%s`" msg
| Ok v -> fold_sections v
end
module Config_duration = struct
type duration_element = {
number: int;
dunit: [ `Year | `Week | `Day | `Hour | `Minute | `Second ];
}
let integer =
take_while1 (function '0' .. '9' -> true | _ -> false) >>= fun s ->
match int_of_string_opt s with
| None -> fail "expected integer, got `%s`" s
| Some i -> return i
let duration_element =
let number = blanks *> integer in
let dunit =
blanks *> take_while1 (fun c -> not (is_whitespace c || is_eol c))
>>= function
| "year" | "years" -> return `Year
| "week" | "weeks" -> return `Week
| "day" | "days" -> return `Day
| "hour" | "hours" -> return `Hour
| "minute" | "minutes" -> return `Minute
| "second" | "seconds" | "s" -> return `Second
| s -> fail "expected a duration unit, got `%s`" s
in
lift2 (fun number dunit -> { number; dunit }) number dunit
let duration = many1 duration_element <* end_of_input
(* TODO put this in Time.Relative *)
let dunit_to_seconds u =
let rec f = function
| `Year -> 365 * f `Day
| `Week -> 7 * f `Day
| `Day -> 24 * f `Hour
| `Hour -> 60 * f `Minute
| `Minute -> 60 * f `Second
| `Second -> 1
in
f u
let to_time_span t =
List.fold_left
(fun acc { number; dunit } -> acc + (number * dunit_to_seconds dunit))
0 t
|> Int64.of_int
|> Time.Relative.of_s
let parse s : duration_element list =
match parse_string ~consume:All duration s with
| Error msg -> fail "duration parse error `%s`" msg
| Ok v -> v
end
let unwrap = function Error e -> fail "`%s`." e | Ok v -> v
let get_opt t ~section ~field =
match List.find_opt (fun v -> v.header = section) t with
| None -> None
| Some v -> (
match List.find_opt (fun item -> item.key = field) v.items with
| None -> None
| Some item -> Some item.value)
let get t ~section ~field =
match get_opt t ~section ~field with
| None -> fail "option `[%s].%s` not found" section field
| Some v -> v
let int s =
match int_of_string_opt s with
| None -> fail "expected int value, got `%s`" s
| Some v -> v
let float s =
match float_of_string_opt s with
| None -> fail "expected float value, got `%s`" s
| Some v -> v
let const_value a b =
match a = b with false -> fail "unexpected value `%s`" b | true -> a
let yes_no = function
| "NO" -> `NO
| "YES" -> `YES
| s -> fail "expected `YES`/`NO` value, got `%s`" s
let uri s = Uri.of_string s
let amount s = s |> Amount.of_string |> unwrap
let duration s = Config_duration.(s |> parse |> to_time_span)
let ed25519 s =
s
|> B32.decode
|> unwrap
|> Mirage_crypto_ec.Ed25519.pub_of_octets
|> Result.map_error (fun e -> Fmt.str "%a" Mirage_crypto_ec.pp_error e)
|> unwrap
let etag s =
match Headers_lib.Etag.parse s with
| Ok etag -> etag
| Error e -> (
(* retry with quotes if needed *)
match Headers_lib.Etag.parse (Fmt.str "\"%s\"" s) with
| Ok etag -> etag
| Error _ -> fail "could not parse etag `%s`: %s" s e)
(* TODO
move to another module
can we type the json as a Int_map directly? *)
module Alt_unit_names = struct
open Syntax
module String_map = Map.Make (String)
let string_map_jsont = Jsont.Object.as_string_map Jsont.string
let decode s =
let* string_map = Jsont_bytesrw.decode_string string_map_jsont s in
let l = String_map.to_list string_map in
let* l =
list_map
(fun (k, v) ->
match int_of_string_opt k with
| None -> Error "alt_unit_names has a non-integer key"
| Some k -> Ok (k, v))
l
in
match List.find_opt (fun (i, _) -> i = 0) l with
| None -> Error "alt_unit_names with no entry for base value \"0\""
| Some _ -> Ok l
let encode l =
let l = List.map (fun (k, v) -> (string_of_int k, v)) l in
let string_map = String_map.of_list l in
let+ s = Jsont_bytesrw.encode_string string_map_jsont string_map in
s
let encode_exn l = encode l |> Result.get_ok
end

View file

@ -0,0 +1,364 @@
(* TODO
check signed/unsigned ints
check endianness
can we avoid amount tuple boilerplate?
clean up caqti error type
transaction
GNU Taler use of db-events?
it seems caqti/pgx does not support it *)
module type CONN = Caqti_miou.CONNECTION
module Caqti_type = struct
include Caqti_type
include Pg_type
include Caqti_request.Infix
end
open Crypto
open Api
let preflight =
let l =
List.map
Caqti_type.(unit ->. unit)
[
"SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL \
SERIALIZABLE;";
"SET enable_sort=OFF;";
"SET enable_seqscan=OFF;";
"SET enable_mergejoin=OFF;";
"SET search_path TO exchange;";
]
in
fun (module Conn : CONN) -> Syntax.list_iter (fun p -> Conn.exec p ()) l
let find_signkey =
let find_signkey =
Caqti_type.(eddsa_pub ->? signkey_data)
"SELECT esk.exchange_pub, esk.valid_from, esk.expire_sign, \
esk.expire_legal, esk.master_sig, skr.master_sig FROM \
exchange_sign_keys AS esk LEFT JOIN signkey_revocations AS skr ON \
esk.esk_serial = skr.esk_serial WHERE esk.exchange_pub=$1"
in
fun (module Conn : CONN) (exchange_pub : EddsaPublicKey.t) ->
Conn.find_opt find_signkey exchange_pub
let get_active_signkeys =
let get_active_signkeys =
Caqti_type.(time ->* signkey_data)
"SELECT esk.exchange_pub, esk.valid_from, esk.expire_sign, \
esk.expire_legal, esk.master_sig, NULL FROM exchange_sign_keys esk \
WHERE expire_sign > $1 AND NOT EXISTS (SELECT esk_serial FROM \
signkey_revocations AS skr WHERE esk.esk_serial = skr.esk_serial)"
in
fun (module Conn : CONN) ~now -> Conn.collect_list get_active_signkeys now
(* note: does not update revocation *)
let insert_signkey =
let insert_signkey =
Caqti_type.(signkey_data ->. unit)
"INSERT INTO exchange_sign_keys (exchange_pub, valid_from, expire_sign, \
expire_legal, master_sig) VALUES ($1, $2, $3, $4, $5)"
in
fun (module Conn : CONN) v -> Conn.exec insert_signkey v
let find_denom =
let find_denom =
Caqti_type.(denom_hash ->? denom_data)
"SELECT dn.denom_pub, (dn.coin).*, dn.valid_from, dn.expire_withdraw, \
dn.expire_deposit, dn.expire_legal, (dn.fee_withdraw).*, \
(dn.fee_deposit).*, (dn.fee_refresh).*, (dn.fee_refund).*, dn.age_mask, \
dn.denom_pub_hash, dn.master_sig, dnr.master_sig FROM denominations AS \
dn LEFT JOIN denomination_revocations AS dnr ON dn.denominations_serial \
= dnr.denominations_serial WHERE dn.denom_pub_hash=$1"
in
fun (module Conn : CONN) h_denom_pub -> Conn.find_opt find_denom h_denom_pub
let get_denominations =
let get_denominations =
Caqti_type.(unit ->* denom_data)
"SELECT dn.denom_pub, (dn.coin).*, dn.valid_from, dn.expire_withdraw, \
dn.expire_deposit, dn.expire_legal, (dn.fee_withdraw).*, \
(dn.fee_deposit).*, (dn.fee_refresh).*, (dn.fee_refund).*, dn.age_mask, \
dn.denom_pub_hash, dn.master_sig, dnr.master_sig FROM denominations AS \
dn LEFT JOIN denomination_revocations AS dnr ON dn.denominations_serial \
= dnr.denominations_serial"
in
fun (module Conn : CONN) -> Conn.collect_list get_denominations ()
(* note: does not update revocation *)
let insert_denom =
let insert_denom =
Caqti_type.(denom_data ->. unit)
"INSERT INTO denominations (denom_pub, coin, valid_from, \
expire_withdraw, expire_deposit, expire_legal, fee_withdraw, \
fee_deposit, fee_refresh, fee_refund, age_mask, denom_pub_hash, \
master_sig) VALUES ($1, ($2, $3), $4, $5, $6, $7, ($8,$9), ($10,$11), \
($12,$13), ($14,$15), $16, $17, $18)"
in
fun (module Conn : CONN) v -> Conn.exec insert_denom v
let insert_denomination_revocation =
let denomination_revocation_insert =
let master_sig = Signatures.MasterDenominationKeyRevocation.caqti in
Caqti_type.(t2 denom_hash master_sig ->. unit)
"INSERT INTO denomination_revocations (denominations_serial, master_sig) \
SELECT denominations_serial, $2 FROM denominations WHERE \
denom_pub_hash=$1"
in
fun (module Conn : CONN) h_denom_pub master_sig ->
Conn.exec denomination_revocation_insert (h_denom_pub, master_sig)
let insert_signkey_revocation =
let signkey_revocation_insert =
let master_sig = Signatures.MasterSigningKeyRevocation.caqti in
Caqti_type.(t2 eddsa_pub master_sig ->. unit)
"INSERT INTO signkey_revocations (esk_serial, master_sig) SELECT \
esk_serial, $2 FROM exchange_sign_keys WHERE exchange_pub=$1"
in
fun (module Conn : CONN) exchange_pub master_sig ->
Conn.exec signkey_revocation_insert (exchange_pub, master_sig)
let get_auditor_timestamp =
let get_auditor_timestamp =
Caqti_type.(eddsa_pub ->? time)
"SELECT last_change FROM auditors WHERE auditor_pub=$1"
in
fun (module Conn : CONN) auditor_pub ->
Conn.find_opt get_auditor_timestamp auditor_pub
let insert_auditor =
let insert_auditor =
Caqti_type.(t4 eddsa_pub string string time ->. unit)
"INSERT INTO auditors (auditor_pub, auditor_name, auditor_url, \
is_active, last_change) VALUES ($1, $2, $3, true, $4)"
in
fun (module Conn : CONN)
AuditorSetupMessage.
{ auditor_url; auditor_name; auditor_pub; master_sig= _; validity_start }
->
Conn.exec insert_auditor
(auditor_pub, auditor_name, auditor_url, validity_start)
let update_auditor =
let update_auditor =
Caqti_type.(t5 eddsa_pub string string bool time ->. unit)
"UPDATE auditors SET auditor_url=$2, auditor_name=$3, is_active=$4, \
last_change=$5 WHERE auditor_pub=$1"
in
fun (module Conn : CONN)
AuditorSetupMessage.
{ auditor_url; auditor_name; auditor_pub; master_sig= _; validity_start }
->
Conn.exec update_auditor
(auditor_pub, auditor_url, auditor_name, true, validity_start)
let disable_auditor =
let update_auditor =
Caqti_type.(t5 eddsa_pub string string bool time ->. unit)
"UPDATE auditors SET auditor_url=$2, auditor_name=$3, is_active=$4, \
last_change=$5 WHERE auditor_pub=$1"
in
fun (module Conn : CONN) ~auditor_pub ~change_date ->
Conn.exec update_auditor (auditor_pub, "", "", false, change_date)
let insert_auditor_denom_sig =
let insert_auditor_denom_sig =
let auditor_sig = Signatures.ExchangeKeyValidity.caqti in
Caqti_type.(t3 eddsa_pub denom_hash auditor_sig ->. unit)
"WITH ax AS (SELECT auditor_uuid FROM auditors WHERE auditor_pub=$1) \
INSERT INTO auditor_denom_sigs (auditor_uuid, denominations_serial, \
auditor_sig) SELECT ax.auditor_uuid, denominations_serial, $3 FROM \
denominations CROSS JOIN ax WHERE denom_pub_hash=$2 ON CONFLICT DO \
NOTHING"
in
fun (module Conn : CONN) ~auditor_pub ~h_denom_pub ~auditor_sig ->
Conn.exec insert_auditor_denom_sig (auditor_pub, h_denom_pub, auditor_sig)
(* todo auditors
maybe check that url and name are unique/same for each auditor_pub
and do the ht logic out of pg.ml? *)
(* this does not return auditors that are not auditing any denom *)
let get_auditor_keys =
let get_auditor_keys =
let auditor_sig = Signatures.ExchangeKeyValidity.caqti in
Caqti_type.(unit ->* t5 eddsa_pub string string denom_hash auditor_sig)
"SELECT a.auditor_pub, a.auditor_url, a.auditor_name, dn.denom_pub_hash, \
ads.auditor_sig FROM auditor_denom_sigs AS ads JOIN auditors AS a USING \
(auditor_uuid) JOIN denominations AS dn USING (denominations_serial) \
WHERE a.is_active"
in
fun (module Conn : CONN) ->
let open Syntax in
let* l = Conn.collect_list get_auditor_keys () |> unwrap_err_caqti in
let ht = Hashtbl.create 0xff in
List.iter
(fun (pub, url, name, denom_pub_h, auditor_sig) ->
let k = (pub, url, name) in
match Hashtbl.find_opt ht k with
| None -> Hashtbl.replace ht k [ (denom_pub_h, auditor_sig) ]
| Some l -> Hashtbl.replace ht k ((denom_pub_h, auditor_sig) :: l))
l;
let l = Hashtbl.to_seq ht |> List.of_seq in
let l =
List.map
(fun ((auditor_pub, auditor_url, auditor_name), auditor_denoms) ->
let denomination_keys =
List.map
(fun (denom_pub_h, auditor_sig) ->
AuditorDenominationKey.{ denom_pub_h; auditor_sig })
auditor_denoms
in
AuditorKeys.
{ auditor_pub; auditor_url; auditor_name; denomination_keys })
l
in
Ok l
let insert_wire_fee =
let insert_wire_fee =
let master_sig = Signatures.MasterWireFee.caqti in
Caqti_type.(t6 wire_method time time amount amount master_sig ->. unit)
"INSERT INTO wire_fee (wire_method, start_date, end_date, wire_fee, \
closing_fee, master_sig) VALUES ($1, $2, $3, ($4,$5), ($6,$7), $8)"
in
fun (module Conn : CONN)
WireFeeSetupMessage.
{
wire_method;
master_sig_wire;
fee_start;
fee_end;
closing_fee;
wire_fee;
}
->
Conn.exec insert_wire_fee
(wire_method, fee_start, fee_end, wire_fee, closing_fee, master_sig_wire)
let get_wire_fees_by_time =
let get_wire_fee_by_time =
Caqti_type.(t3 wire_method time time ->* aggregate_transfer_fee)
"SELECT (wire_fee).*, (closing_fee).*, start_date, end_date, master_sig \
FROM wire_fee WHERE wire_method=$1 AND end_date > $2 AND start_date < \
$3"
in
fun (module Conn : CONN) ~wire_method ~start_date ~end_date ->
Conn.collect_list get_wire_fee_by_time (wire_method, start_date, end_date)
let get_wire_fees =
let get_wire_fees =
Caqti_type.(string ->* aggregate_transfer_fee)
"SELECT (wire_fee).*, (closing_fee).*, start_date, end_date, master_sig \
FROM wire_fee WHERE wire_method=$1"
in
fun (module Conn : CONN) ~wire_method ->
Conn.collect_list get_wire_fees wire_method
let get_global_fees =
let get_global_fees =
Caqti_type.(time ->* global_fee)
"SELECT start_date, end_date, (history_fee).*, (account_fee).*, \
(purse_fee).*, history_expiration, purse_account_limit, purse_timeout, \
master_sig FROM global_fee WHERE start_date >= $1"
in
fun (module Conn : CONN) ~start_date ->
Conn.collect_list get_global_fees start_date
let get_global_fees_by_time =
let get_global_fees_by_time =
Caqti_type.(t2 time time ->* global_fee)
"SELECT start_date, end_date, (history_fee).*, (account_fee).*, \
(purse_fee).*, history_expiration, purse_account_limit, purse_timeout, \
master_sig FROM global_fee WHERE start_date >= $1 AND end_date <= $2"
in
fun (module Conn : CONN) ~start_date ~end_date ->
Conn.collect_list get_global_fees_by_time (start_date, end_date)
let insert_global_fees =
let insert_global_fees =
Caqti_type.(global_fee ->. unit)
"INSERT INTO global_fee (start_date, end_date, history_fee, account_fee, \
purse_fee, history_expiration, purse_account_limit, purse_timeout, \
master_sig) VALUES ($1, $2, ($3,$4), ($5,$6), ($7,$8), $9, $10, $11, \
$12)"
in
fun (module Conn : CONN) v -> Conn.exec insert_global_fees v
let get_wire_timestamp =
let get_wire_timestamp =
Caqti_type.(payto_uri ->? time)
"SELECT last_change FROM wire_accounts WHERE payto_uri=$1"
in
fun (module Conn : CONN) ~payto_uri ->
Conn.find_opt get_wire_timestamp payto_uri
let insert_wire =
let insert_wire =
Caqti_type.(t3 exchange_wire_account bool time ->. unit)
"INSERT INTO wire_accounts (payto_uri, conversion_url, \
credit_restrictions, debit_restrictions, master_sig, bank_label, \
priority, is_active, last_change) VALUES \
($1,$2,$3::TEXT::JSONB,$4::TEXT::JSONB,$5,$6,$7,true,$8)"
in
fun (module Conn : CONN) ~last_change v ->
let is_active = true in
Conn.exec insert_wire (v, is_active, last_change)
let update_wire =
let update_wire =
Caqti_type.(t3 exchange_wire_account bool time ->. unit)
"UPDATE wire_accounts SET conversion_url=$2, \
debit_restrictions=$3::TEXT::JSONB, \
credit_restrictions=$4::TEXT::JSONB, master_sig=$5, bank_label=$6, \
priority=$7, is_active=$8, last_change=$9 WHERE payto_uri=$1"
in
fun (module Conn : CONN) ~is_active ~last_change v ->
Conn.exec update_wire (v, is_active, last_change)
let disable_wire =
let disable_wire =
Caqti_type.(t2 payto_uri time ->. unit)
"UPDATE wire_accounts SET conversion_url=NULL, debit_restrictions=NULL, \
credit_restrictions=NULL, master_sig=NULL, bank_label=NULL, \
priority=NULL, is_active=FALSE, last_change=$2 WHERE payto_uri=$1"
in
fun (module Conn : CONN) ~payto_uri ~validity_end ->
Conn.exec disable_wire (payto_uri, validity_end)
let get_wire_accounts =
let get_wire_accounts =
Caqti_type.(unit ->* exchange_wire_account)
"SELECT payto_uri, conversion_url, debit_restrictions::TEXT, \
credit_restrictions::TEXT, master_sig, bank_label, priority FROM \
wire_accounts WHERE is_active"
in
fun (module Conn : CONN) -> Conn.collect_list get_wire_accounts ()
let insert_drain_profit =
let insert_drain_profit =
Caqti_type.(drain_profit_message ->. unit)
"INSERT INTO profit_drains (wtid, account_section, payto_uri, \
trigger_date, amount, master_sig) VALUES ($1, $2, $3, $4, ($5,$6), $7)"
in
fun (module Conn : CONN) v -> Conn.exec insert_drain_profit v
let insert_aml_officer =
let exchange_do_insert_aml_officer =
Caqti_type.(aml_officer_setup ->! time)
"SELECT out_last_change FROM exchange_do_insert_aml_officer ($1, $2, $3, \
$4, $5, $6)"
in
fun (module Conn : CONN) v -> Conn.find exchange_do_insert_aml_officer v
let insert_partner =
let insert_partner =
Caqti_type.(exchange_partner_setup ->. unit)
"INSERT INTO partners (partner_master_pub, start_date, end_date, \
wad_frequency, wad_fee, master_sig, partner_base_url) VALUES ($1, $2, \
$3, $4, ($5,$6), $7, $8) ON CONFLICT DO NOTHING"
in
fun (module Conn : CONN) v -> Conn.exec insert_partner v

View file

@ -0,0 +1,360 @@
(* this module defines caqti encoding/decodings *)
open Caqti_type
open Crypto
open Api
let amount : Amount.t t =
let open Amount in
custom
~encode:(fun amount -> Ok (amount.value, amount.fraction))
~decode:(fun (value, fraction) ->
Amount.make ~sign:None ~currency:Config.currency ~value ~fraction)
(t2 int64 int32)
(* we want to use int64 timestamps,
not postgresql built-in timestamp type *)
let ptime : unit t = Caqti_type.unit
let time = Timestamp.caqti
let time_span = Time.Relative.caqti
let age_mask : int t = Caqti_type.int
let rsa_pub = RsaPublicKey.caqti
let eddsa_pub = EddsaPublicKey.caqti
let eddsa_sig = EddsaSignature.caqti
(* todo: enum type for wire_method? *)
let wire_method = Caqti_type.string
let payto_uri = Caqti_type.string
let b32 = B32.caqti
include struct
(* alias for hash *)
open Hash
let fullpayto_hash = FullPaytoHash.caqti
let nomalizaedpayto_hash = NormalizedPaytoHash.caqti
let denom_hash = DenominationHash.caqti
let privatecontract_hash = PrivateContractHash.caqti
let extensionspolicy_hash = ExtensionsPolicyHash.caqti
let merchantwire_hash = MerchantWireHash.caqti
let agecommitment_hash = AgeCommitmentHash.caqti
let blindedcoin_hash = BlindedCoinHash.caqti
let coinpub_hash = CoinPubHash.caqti
let outputcommitment_hash = OutputCommitmentHash.caqti
let planchets_hash = HashPlanchetsP.caqti
end
let signkey_data =
let master_sig = Signatures.ExchangeSigningKeyValidity.caqti in
let revoked_sig = option Signatures.MasterSigningKeyRevocation.caqti in
custom
~encode:(fun
Signkey.
{ pub; stamp_start; stamp_expire; stamp_end; master_sig; revoked_sig }
->
Ok (pub, stamp_start, stamp_expire, stamp_end, master_sig, revoked_sig))
~decode:(fun
(pub, stamp_start, stamp_expire, stamp_end, master_sig, revoked_sig) ->
Ok { pub; stamp_start; stamp_expire; stamp_end; master_sig; revoked_sig })
(t6 eddsa_pub time time time master_sig revoked_sig)
let denom_data =
let master_sig = Signatures.DenominationKeyValidity.caqti in
let revoked_sig = option Signatures.MasterDenominationKeyRevocation.caqti in
custom
~encode:(fun
Denomination.
{
pub;
value;
stamp_start;
stamp_expire_withdraw;
stamp_expire_deposit;
stamp_expire_legal;
fee_withdraw;
fee_deposit;
fee_refresh;
fee_refund;
age_mask;
h_pub;
master_sig;
revoked_sig;
}
->
Ok
( pub,
value,
stamp_start,
stamp_expire_withdraw,
stamp_expire_deposit,
stamp_expire_legal,
fee_withdraw,
fee_deposit,
fee_refresh,
fee_refund,
age_mask,
(h_pub, master_sig, revoked_sig) ))
~decode:(fun
( pub,
value,
stamp_start,
stamp_expire_withdraw,
stamp_expire_deposit,
stamp_expire_legal,
fee_withdraw,
fee_deposit,
fee_refresh,
fee_refund,
age_mask,
(h_pub, master_sig, revoked_sig) )
->
Ok
{
pub;
value;
stamp_start;
stamp_expire_withdraw;
stamp_expire_deposit;
stamp_expire_legal;
fee_withdraw;
fee_deposit;
fee_refresh;
fee_refund;
age_mask;
h_pub;
master_sig;
revoked_sig;
})
(t12 rsa_pub amount time time time time amount amount amount amount int
(t3 denom_hash master_sig revoked_sig))
let account_restrictions =
custom
~encode:(fun l -> Api.encode (Jsont.list AccountRestriction.jsont) l)
~decode:(fun s -> Api.decode (Jsont.list AccountRestriction.jsont) s)
string
let global_fee =
let master_sig = Signatures.GlobalFees.caqti in
custom
~encode:(fun
GlobalFees.
{
start_date;
end_date;
history_fee;
account_fee;
purse_fee;
history_expiration;
purse_account_limit;
purse_timeout;
master_sig;
}
->
Ok
( start_date,
end_date,
history_fee,
account_fee,
purse_fee,
history_expiration,
purse_account_limit,
purse_timeout,
master_sig ))
~decode:(fun
( start_date,
end_date,
history_fee,
account_fee,
purse_fee,
history_expiration,
purse_account_limit,
purse_timeout,
master_sig )
->
Ok
{
start_date;
end_date;
history_fee;
account_fee;
purse_fee;
history_expiration;
purse_account_limit;
purse_timeout;
master_sig;
})
Caqti_type.(
t9 time time amount amount amount time_span int32 time_span master_sig)
let aggregate_transfer_fee =
let master_sig = Signatures.MasterWireFee.caqti in
Caqti_type.custom
~encode:(fun
AggregateTransferFee.
{ wire_fee; closing_fee; start_date; end_date; sig_ }
-> Ok (wire_fee, closing_fee, start_date, end_date, sig_))
~decode:(fun (wire_fee, closing_fee, start_date, end_date, sig_) ->
Ok
AggregateTransferFee.
{ wire_fee; closing_fee; start_date; end_date; sig_ })
Caqti_type.(t5 amount amount time time master_sig)
let exchange_wire_account =
let master_sig = Signatures.MasterWireDetails.caqti in
Caqti_type.custom
~encode:(fun
ExchangeWireAccount.
{
payto_uri;
conversion_url;
debit_restrictions;
credit_restrictions;
master_sig;
bank_label;
priority;
}
->
Ok
( payto_uri,
conversion_url,
debit_restrictions,
credit_restrictions,
master_sig,
bank_label,
priority ))
~decode:(fun
( payto_uri,
conversion_url,
debit_restrictions,
credit_restrictions,
master_sig,
bank_label,
priority )
->
Ok
{
payto_uri;
conversion_url;
debit_restrictions;
credit_restrictions;
master_sig;
bank_label;
priority;
})
Caqti_type.(
t7 payto_uri (option string) account_restrictions account_restrictions
master_sig (option string) (option int))
let drain_profit_message =
let master_sig = Signatures.MasterDrainProfit.caqti in
Caqti_type.custom
~encode:(fun
DrainProfitsMessage.
{
wtid;
debit_account_section;
credit_payto_uri;
date;
amount;
master_sig;
}
->
Ok
(wtid, debit_account_section, credit_payto_uri, date, amount, master_sig))
~decode:(fun
(wtid, debit_account_section, credit_payto_uri, date, amount, master_sig)
->
Ok
{
wtid;
debit_account_section;
credit_payto_uri;
date;
amount;
master_sig;
})
Caqti_type.(t6 b32 string string time amount master_sig)
let aml_officer_setup =
let master_sig = Signatures.MasterAmlOfficerStatus.caqti in
Caqti_type.custom
~encode:(fun
AmlOfficerSetup.
{
officer_pub;
master_sig;
officer_name;
is_active;
read_only;
change_date;
}
->
Ok
( officer_pub,
master_sig,
officer_name,
is_active,
read_only,
change_date ))
~decode:(fun
( officer_pub,
master_sig,
officer_name,
is_active,
read_only,
change_date )
->
Ok
{
officer_pub;
master_sig;
officer_name;
is_active;
read_only;
change_date;
})
Caqti_type.(t6 eddsa_pub master_sig string bool bool time)
let exchange_partner_setup =
let master_sig = Signatures.PartnerConfiguration.caqti in
Caqti_type.custom
~encode:(fun
ExchangePartnerSetupRequest.
{
partner_pub;
start_date;
end_date;
wad_frequency;
wad_fee;
master_sig;
partner_base_url;
}
->
Ok
( partner_pub,
start_date,
end_date,
wad_frequency,
wad_fee,
master_sig,
partner_base_url ))
~decode:(fun
( partner_pub,
start_date,
end_date,
wad_frequency,
wad_fee,
master_sig,
partner_base_url )
->
Ok
{
partner_base_url;
partner_pub;
wad_frequency;
master_sig;
start_date;
end_date;
wad_fee;
})
Caqti_type.(t7 eddsa_pub time time time_span amount master_sig string)

View file

@ -0,0 +1,39 @@
(* TODO response
use ErrorDetail *)
let respond_json req content status =
let open Vif.Response in
let open Syntax in
let* () = add ~field:"content-type" "application/json" in
let* () = with_string req content in
respond status
let mk_error_content ?hint _status =
let open Api in
let code = -1 in
let err = ErrorDetail.make ?hint code in
encode_exn ErrorDetail.jsont err
let error ~hint req =
Logs.err (fun m -> m "internal server error: %s" hint);
let body = mk_error_content ~hint `Internal_server_error in
respond_json req body `Internal_server_error
let bad_request ?hint req =
Logs.err (fun m -> m "bad request");
let body = mk_error_content ?hint `Bad_request in
respond_json req body `Bad_request
let ok content req =
Logs.debug (fun m -> m "ok");
respond_json req content `OK
let not_modified () =
Logs.debug (fun m -> m "not modified");
let open Vif.Response in
let open Syntax in
let* () = empty in
respond `Not_modified
let result res req =
match res with Error hint -> error ~hint req | Ok content -> ok content req

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
open Crypto
(* TODO replace by Api.SignKey.t instead? (no revoked_sig) *)
type t = {
pub: eddsa_pub;
stamp_start: Timestamp.t;
stamp_expire: Timestamp.t;
stamp_end: Timestamp.t;
master_sig: Signatures.ExchangeSigningKeyValidity.t;
revoked_sig: Signatures.MasterSigningKeyRevocation.t option;
}

View file

@ -0,0 +1,51 @@
let ( let* ) o f = match o with Ok v -> f v | Error _ as e -> e
let ( let+ ) o f = match o with Ok v -> Ok (f v) | Error _ as e -> e
(* TODO use polymorphic variant for errors *)
let unwrap_err_msg o = match o with Error (`Msg e) -> Error e | Ok v -> Ok v
let unwrap_err_caqti o =
match o with Error err -> Fmt.error "%a" Caqti_error.pp err | Ok v -> Ok v
let list_iter f l =
let err = ref None in
try
List.iter
(fun v ->
match f v with
| Error _e as e ->
err := Some e;
raise Exit
| Ok () -> ())
l;
Ok ()
with Exit -> ( match !err with None -> assert false | Some v -> v)
let list_map f l =
let err = ref None in
try
Ok
(List.map
(fun v ->
match f v with
| Error _e as e ->
err := Some e;
raise Exit
| Ok v -> v)
l)
with Exit -> ( match !err with None -> assert false | Some v -> v)
let list_fold_left f acc l =
List.fold_left
(fun acc v ->
let* acc = acc in
f acc v)
(Ok acc) l
let opt_list l =
match (List.for_all Option.is_none l, List.for_all Option.is_some l) with
| _, true ->
let l = List.map Option.get l in
Ok (Some l)
| true, _ -> Ok None
| _, _ -> Error ()

View file

@ -0,0 +1,280 @@
(* This file was generated from the GANA database:
https://git-www.gnunet.org/gana.git/tree/gnunet-signatures/registry.rec *)
(** Initialize or update the status of an AML key for an AML officer *)
let master_aml_key : int32 = 1017_l
(** Affirm wiring of exchange profits to operator account. *)
let master_drain_profit : int32 = 1018_l
(** Signature affirming a partner configuration for wads. *)
let master_partner_details : int32 = 1019_l
(** The given revocation key was revoked and must no longer be used. *)
let master_signing_key_revoked : int32 = 1020_l
(** Add payto URI to the list of our wire methods. *)
let master_add_wire : int32 = 1021_l
(** Signature over global set of fees charged by the exchange. *)
let master_global_fees : int32 = 1022_l
(** Remove payto URI from the list of our wire methods. *)
let master_del_wire : int32 = 1023_l
(** Purpose for signing public keys signed by the exchange master key. *)
let master_signing_key_validity : int32 = 1024_l
(** Purpose for denomination keys signed by the exchange master key. *)
let master_denomination_key_validity : int32 = 1025_l
(** Add an auditor to the list of our auditors. *)
let master_add_auditor : int32 = 1026_l
(** Remove an auditor from the list of our auditors. *)
let master_del_auditor : int32 = 1027_l
(** Fees charged per (aggregate) wire transfer to the merchant. *)
let master_wire_fees : int32 = 1028_l
(** The given revocation key was revoked and must no longer be used. *)
let master_denomination_key_revoked : int32 = 1029_l
(** Signature where the Exchange confirms its IBAN details in the /wire
response. *)
let master_wire_details : int32 = 1030_l
(** Set the configuration of an extension (age-restriction or peer2peer) *)
let master_extension : int32 = 1031_l
(** Purpose for the state of a reserve, signed by the exchange's signing key. *)
let exchange_reserve_status : int32 = 1032_l
(** Signature where the Exchange confirms a deposit request. *)
let exchange_confirm_deposit : int32 = 1033_l
(** Signature where the exchange (current signing key) confirms the no-reveal
index for cut-and-choose and the validity of the melted coins. *)
let exchange_confirm_melt : int32 = 1034_l
(** Signature where the Exchange confirms the full /keys response set. *)
let exchange_key_set : int32 = 1035_l
(** Signature where the Exchange confirms the /track/transaction response. *)
let exchange_confirm_wire : int32 = 1036_l
(** Signature where the Exchange confirms the /wire/deposit response. *)
let exchange_confirm_wire_deposit : int32 = 1037_l
(** Signature where the Exchange confirms a refund request. *)
let exchange_confirm_refund : int32 = 1038_l
(** Signature where the Exchange confirms a recoup. *)
let exchange_confirm_recoup : int32 = 1039_l
(** Signature where the Exchange confirms it closed a reserve. *)
let exchange_reserve_closed : int32 = 1040_l
(** Signature where the Exchange confirms a recoup-refresh operation. *)
let exchange_confirm_recoup_refresh : int32 = 1041_l
(** Signature where the Exchange confirms that it does not know a denomination
(hash). *)
let exchange_affirm_denom_unknown : int32 = 1042_l
(** Signature where the Exchange confirms that it does not consider a
denomination valid for the given operation at this time. *)
let exchange_affirm_denom_expired : int32 = 1043_l
(** Signature by which the exchange affirms that a purse was created with a
certain amount deposited into it. *)
let exchange_confirm_purse_creation : int32 = 1045_l
(** Signature by which the exchange affirms that a purse was merged into a
reserve with a certain amount in it. *)
let exchange_confirm_purse_merged : int32 = 1046_l
(** Purpose for the state of a purse, signed by the exchange's signing key. *)
let exchange_purse_status : int32 = 1047_l
(** Signature by which the exchange attests identity attributes of a particular
reserve owner. *)
let exchange_reserve_attest_details : int32 = 1048_l
(** Signature by which the exchange confirms that a purse expired and a coin was
refunded. *)
let exchange_confirm_purse_refund : int32 = 1049_l
(** Signature where the Exchange confirms an (age-)withdraw. *)
let exchange_confirm_withdraw : int32 = 1050_l
(** Signature where the auditor confirms that he is aware of certain
denomination keys from the exchange. *)
let auditor_exchange_keys : int32 = 1064_l
(** Signature where the merchant confirms a contract (to the customer). *)
let merchant_contract : int32 = 1101_l
(** Signature where the merchant confirms a refund (of a coin). *)
let merchant_refund : int32 = 1102_l
(** Signature where the merchant confirms that he needs the wire transfer
identifier for a deposit operation. *)
let merchant_track_transaction : int32 = 1103_l
(** Signature where the merchant confirms that the payment was successful *)
let merchant_payment_ok : int32 = 1104_l
(** Signature where the merchant confirms its own (salted) wire details (not yet
really used). *)
let merchant_wire_details : int32 = 1107_l
(** Signature where the merchant issues a token by blindly signing it. Signed
with the token issue private key. *)
let merchant_token_issue : int32 = 1108_l
(** Signature where the reserve key confirms a withdraw request. Signed with the
reserve private key. *)
let wallet_reserve_withdraw : int32 = 1200_l
(** Signature made by the wallet of a user to confirm a deposit of a coin. *)
let wallet_coin_deposit : int32 = 1201_l
(** Signature using a coin key confirming the melting of a coin. Signed with the
coin's private key. *)
let wallet_coin_melt : int32 = 1202_l
(** Signature using a coin key requesting recoup. Signed with the coin's private
key. *)
let wallet_coin_recoup : int32 = 1203_l
(** Signature using a coin key authenticating link data. Signed with the old
coin's private key. *)
let wallet_coin_link : int32 = 1204_l
(** Signature using a reserve key by which a wallet requests a payment target
UUID for itself. Signs over just a purpose (no body), as the signature only
serves to demonstrate that the request comes from the wallet controlling the
private key, and not some third party. *)
let wallet_account_setup : int32 = 1205_l
(** Signature using a coin key requesting recoup-refresh. Signed with the coin
private key. *)
let wallet_coin_recoup_refresh : int32 = 1206_l
(** Signature using a age restriction key for attestation of a particular
age/age-group. *)
let wallet_age_attestation : int32 = 1207_l
(** Request full or partial reserve history. Signed with the reserve private
key. *)
let wallet_reserve_history : int32 = 1208_l
(** Request full or partial coin history. Signed with the coin private key. *)
let wallet_coin_history : int32 = 1209_l
(** Request purse creation (without reserve). Signed by the purse private key.
*)
let wallet_purse_create : int32 = 1210_l
(** Request coin to be deposited into a purse. Signed with the coin private key.
*)
let wallet_purse_deposit : int32 = 1211_l
(** Request purse status. Signed with the purse private key. *)
let wallet_purse_status : int32 = 1212_l
(** Request purse to be merged with a reserve. Signed with the purse private
key. *)
let wallet_purse_merge : int32 = 1213_l
(** Request purse to be merged with a reserve. Signed by the reserve private
key. *)
let wallet_account_merge : int32 = 1214_l
(** Request account to be closed. Signed with the reserve private key. *)
let wallet_reserve_close : int32 = 1215_l
(** Associates encrypted contract with a purse. Signed with the purse private
key. *)
let wallet_purse_econtract : int32 = 1216_l
(** Request reserve to be kept open. Signed with the reserve private key. *)
let wallet_reserve_open : int32 = 1217_l
(** Request coin to be used to pay for reserve to be kept open. Signed with the
coin private key. *)
let wallet_reserve_open_deposit : int32 = 1218_l
(** Request attestation about reserve owner. Signed by the reserve private key.
*)
let wallet_reserve_attest_details : int32 = 1219_l
(** Signature by which a wallet requests a purse to be deleted. *)
let wallet_purse_delete : int32 = 1220_l
(** Signature where the reserve key confirms an age-withdraw request. Signed
with the reserve private key. *)
let wallet_reserve_age_withdraw : int32 = 1221_l
(** Signature where the token use key confirms the usage of a token on a pay
request. Signed with the token use private key. *)
let wallet_token_use : int32 = 1222_l
(** Signature used to unclaim an order, allowing other wallets to claim it.
Signed with the private key of the claim nonce. *)
let wallet_order_unclaim : int32 = 1223_l
(** Signature on a denomination key announcement. *)
let sm_rsa_denomination_key : int32 = 1250_l
(** Signature on an exchange message signing key announcement. *)
let sm_signing_key : int32 = 1251_l
(** Signature on a denomination key announcement. *)
let sm_cs_denomination_key : int32 = 1252_l
(** EdDSA test signature. *)
let client_test_eddsa : int32 = 1302_l
(** EdDSA test signature. *)
let exchange_test_eddsa : int32 = 1303_l
(** Signature by which an AML officer signs an AML decision. *)
let aml_decision : int32 = 1350_l
(** Signature by which an AML officer requests AML data. *)
let aml_query : int32 = 1351_l
(** Signature by which an account owner authorizes access to a KYC operation. *)
let kyc_auth : int32 = 1360_l
(** EdDSA signature for a policy upload. *)
let anastasis_policy_upload : int32 = 1400_l
(** EdDSA signature for a backup upload. *)
let sync_backup_upload : int32 = 1450_l
(** The signature is done by the Donau. The Donau signes over the total amount
of the corresponding year, the corresponding year and the donation
identifier of a specific donor. The statement confirms that the donor made
this total in donations for the given year. *)
let donau_donation_statement : int32 = 1500_l
(** The signature is made by a charity and shows that the charity is in
agreement with the donation request which it sends to the Donau. The charity
signs over all blinded identifiers and key pairs which it has received from
the donor. The signature affirms that the charity wants the donation
receipts to be issued on its behalf. *)
let charity_donation_confirmation : int32 = 1501_l
(** The signature is made by a charity to request information about its status
from a Donau. It is not over anything in particular and is just there for
access control. *)
let charity_get_info : int32 = 1502_l
(** Signature over messages to delete in the mailbox service *)
let mailbox_messages_delete : int32 = 1551_l
(** Signature for mailbox registration request *)
let mailbox_register : int32 = 1552_l

View file

@ -0,0 +1,151 @@
let uint64_max = Int64.minus_one
module Relative = struct
type t = Int64.t
let forever = uint64_max
let zero = 0L
let compare = Int64.unsigned_compare
let min a b = if compare a b < 0 then a else b
let max a b = if compare a b > 0 then a else b
(* forever if either argument is forever or on overflow; otherwise a + b *)
let add a b =
if a = forever || b = forever then forever
else
let v = Int64.add a b in
if compare v a < 0 then forever else v
(* zero if a <= b, or forever if a is forever; otherwise a - b *)
let sub a b =
if compare a b <= 0 then zero
else if a = forever then forever
else Int64.sub a b
let of_s s =
let v = Int64.mul s 1_000_000L in
if Int64.unsigned_div v 1_000_000L <> s then forever else v
let bin = Bin.neint64
let bin_nbo = Bin.beint64
(* TODO should be in NBO here? *)
let caqti =
let encode v = Ok v in
let decode v = Ok v in
Caqti_type.custom ~encode ~decode Caqti_type.int64
(* TODO
reject negative / non-integer values
cap value at 2^53 - 1 inclusive *)
let jsont =
let jsont =
let forever_jsont =
let dec s =
match s with
| "forever" -> forever
| _ -> Jsont.Error.msg Jsont.Meta.none "unexpected string value"
in
let enc _t = "forever" in
Jsont.map ~dec ~enc Jsont.string
in
let num_jsont = Jsont.int64 in
let enc t = if t = forever then forever_jsont else num_jsont in
Jsont.any ~dec_string:forever_jsont ~dec_number:num_jsont ~enc ()
in
Jsont.Object.map ~kind:"RelativeTime" Fun.id
|> Jsont.Object.mem "d_us" jsont ~enc:Fun.id
|> Jsont.Object.finish
end
module Absolute = struct
type t = Int64.t
let never = uint64_max
let zero = 0L
let compare = Int64.unsigned_compare
let min a b = if compare a b < 0 then a else b
let max a b = if compare a b > 0 then a else b
(* zero if a >= b; never if b=never; otherwise b - a *)
let diff a b =
if compare a b >= 0 then zero
else if b = never then never
else Int64.sub b a
(* never if either argument is never/forever or on overflow; otherwise t + d *)
let add t d =
if t = never || d = never then never
else
let v = Int64.add t d in
if compare v t < 0 then never else v
(* zero if t <= d, or never if t is never; otherwise t - d *)
let sub t d =
if compare t d <= 0 then zero
else if t = never then never
else Int64.sub t d
let of_s s =
let v = Int64.mul s 1_000_000L in
if Int64.unsigned_div v 1_000_000L <> s then never else v
let of_ptime v = v |> Ptime.to_float_s |> Int64.of_float |> of_s
end
module Timestamp = struct
type t = Int64.t
let never = uint64_max
let zero = 0L
let compare = Int64.unsigned_compare
(* zero if a >= b; never if b=never; otherwise b - a *)
let diff a b =
if compare a b >= 0 then zero
else if b = never then never
else Int64.sub b a
let of_s s =
let v = Int64.mul s 1_000_000L in
if Int64.unsigned_div v 1_000_000L <> s then never else v
let to_s t =
if t = never then None else Some (Int64.unsigned_div t 1_000_000L)
let of_absolute a =
if a = never then never else Int64.sub a (Int64.unsigned_rem a 1_000_000L)
let of_ptime v = v |> Absolute.of_ptime |> of_absolute
let bin = Bin.neint64
let bin_nbo = Bin.beint64
let caqti =
let encode v = Ok v in
let decode v = Ok v in
Caqti_type.custom ~encode ~decode Caqti_type.int64
let jsont =
let jsont =
let never_jsont =
let dec s =
match s with
| "never" -> never
| _ -> Jsont.Error.msg Jsont.Meta.none "unexpected string value"
in
let enc _t = "never" in
Jsont.map ~dec ~enc Jsont.string
in
let num_jsont =
Jsont.map
~dec:(fun n -> of_s n)
~enc:(fun t -> match to_s t with None -> assert false | Some s -> s)
Jsont.int64
in
let enc t = if t = never then never_jsont else num_jsont in
Jsont.any ~dec_string:never_jsont ~dec_number:num_jsont ~enc ()
in
Jsont.Object.map ~kind:"Timestamp" Fun.id
|> Jsont.Object.mem "t_s" jsont ~enc:Fun.id
|> Jsont.Object.finish
end

View file

@ -0,0 +1,54 @@
module Relative : sig
type t
val forever : t
val zero : t
val compare : t -> t -> int
val min : t -> t -> t
val max : t -> t -> t
val add : t -> t -> t
val sub : t -> t -> t
val of_s : int64 -> t
(* - *)
val bin : t Bin.t
val bin_nbo : t Bin.t
val caqti : t Caqti_type.t
val jsont : t Jsont.t
end
module Absolute : sig
type t
val never : t
val zero : t
val compare : t -> t -> int
val min : t -> t -> t
val max : t -> t -> t
val diff : t -> t -> Relative.t
val add : t -> Relative.t -> t
val sub : t -> Relative.t -> t
val of_s : int64 -> t
val of_ptime : Ptime.t -> t
end
module Timestamp : sig
type t
val never : t
val zero : t
val compare : t -> t -> int
val diff : t -> t -> Relative.t
val of_s : int64 -> t
(* none if t = never *)
val to_s : t -> int64 option
val of_absolute : Absolute.t -> t
val of_ptime : Ptime.t -> t
(* - *)
val bin : t Bin.t
val bin_nbo : t Bin.t
val caqti : t Caqti_type.t
val jsont : t Jsont.t
end

View file

@ -0,0 +1 @@
include Time.Timestamp

View file

@ -0,0 +1,62 @@
module Log_reporter = struct
let detail_tag : string Logs.Tag.def =
Logs.Tag.def "Detail tag" ~doc:"" Fmt.string
let detail s = Logs.Tag.(empty |> add detail_tag s)
let time_anchor = Ptime_clock.now () |> Ptime.to_span
let color_of_log_level = function
| Logs.App -> `White
| Error -> `Red
| Warning -> `Yellow
| Info -> `Blue
| Debug -> `Magenta
let reporter : Logs.reporter =
let open Fmt in
let pp_timestamp = styled `Faint (styled (`Fg `White) (fmt "%04.02f")) in
let pp_header ppf v =
let color = color_of_log_level (fst v) in
let pp = styled (`Fg color) Logs.pp_header in
pf ppf "%a" pp v
in
let pp_src_name =
let pp = using Logs.Src.name (styled `Cyan (fmt "%s: ")) in
fun ppf v -> if not @@ Logs.Src.equal Logs.default v then pp ppf v
in
let pp_detail = option (styled `Green (fmt " (%s)")) in
let report src lvl ~over k msgf =
let ppf =
match lvl with
| Logs.App -> stdout
| Error | Warning | Info | Debug -> stderr
in
let k _ppf = over (); k () in
let with_detail h tags k user_fmt =
let detail = Option.bind tags (Logs.Tag.find detail_tag) in
let timestamp =
Ptime.sub_span (Ptime_clock.now ()) time_anchor
|> Option.map Ptime.to_float_s
|> Option.value ~default:0.
in
let k ppf = kpf k ppf "%a@." pp_detail detail in
let k ppf = kpf k ppf user_fmt in
kpf k ppf "%a %a %a" pp_timestamp timestamp pp_header (lvl, h)
pp_src_name src
in
msgf @@ fun ?header ?tags fmt -> with_detail header tags k fmt
in
{ report }
(* TODO logs
- vif shouldn't use/set the default reporter
- Log.err all `Internal_server_error response *)
let setup () =
let level = Some Logs.Info in
Logs.set_level ~all:false level;
Fmt_tty.setup_std_outputs ~style_renderer:`Ansi_tty ~utf_8:true ();
Logs.Src.set_level Logs.default level;
Logs_threaded.enable ();
Logs.set_reporter reporter;
()
end