This commit is contained in:
Swrup 2025-09-13 15:56:19 +02:00
commit 099cd80670
48 changed files with 4951 additions and 0 deletions

119
src/amount.ml Normal file
View file

@ -0,0 +1,119 @@
(* TODO
have safe amount arithmetic
make type private
redefine an Amount module with currency enforced to be Config.currency? *)
(* Amounts of currency, serialized as `<Currency>:<IntegerPart>.<FractionalPart>`
Fixed-precision numbers with 8 decimal places.
- <Currency> must be at most 11 characters long
only consist of ASCII letters (a-zA-Z).
- integer part of <DecimalAmount> may be at most 2^52.
- fractional part of <DecimalAmount> may contain at most 8 decimal digits.
Prefixed with '+' or '-' in certain contexts.
When no sign is present, the amount is assumed to be positive. *)
type sign =
| Sign_plus
| Sign_minus
type t = {
sign: sign option;
currency: string;
value: Int64.t;
fraction: Int32.t;
}
let ( let* ) o f = match o with Ok v -> f v | Error _ as e -> e
let check_not msg = function false -> Ok () | true -> Error msg
let value_upper_bound = Z.(pow (of_int 2) 52) |> Z.to_int64
let fraction_upper_bound = Int32.of_int 100_000_000
let make ~sign ~currency ~value ~fraction =
let* () = check_not "value is negative" (value < Int64.zero) in
let* () = check_not "fraction is negative" (fraction < Int32.zero) in
let* () =
check_not "value is greater than 2^52" (value > value_upper_bound)
in
let* () =
check_not "fraction has more than 8 decimal digits"
(fraction >= fraction_upper_bound)
in
Ok { sign; currency; value; fraction }
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 } ->
pf ppf "%a%s:%Ld.%02ld" (Fmt.option pp_sign) sign currency value fraction
let to_string = Fmt.str "%a" pp
let of_string =
let open Angstrom in
let parse_sign =
choice
[
char '+' *> return (Some Sign_plus);
char '-' *> return (Some Sign_minus);
return None;
]
in
let parse_currency =
(* TODO currency string constraint/format *)
take_while1 (function
| 'a' .. 'z' | 'A' .. 'Z' -> true
| _ -> false)
in
let parse_int64 =
take_while1 (function '0' .. '9' -> true | _ -> false)
>>| Int64.of_string_opt
>>= function
| None -> fail "invalid integer"
| Some n -> return n
in
let parse_int32 =
take_while1 (function '0' .. '9' -> true | _ -> false)
>>| Int32.of_string_opt
>>= function
| None -> fail "invalid integer"
| Some n -> return n
in
let parse_t =
lift4
(fun sign currency value fraction ->
make ~sign ~currency ~value ~fraction)
parse_sign parse_currency
(char ':' *> parse_int64)
(char '.' *> parse_int32)
in
fun s -> parse_string ~consume:Consume.All parse_t s |> Result.join
let jsont = Jsont.of_of_string ~kind:"Amount" of_string ~enc:to_string
let currency_len = 12
let pad_currency s =
let len = String.length s in
assert (len <= 11);
let b = Bytes.make 12 '\x00' in
Bytes.blit_string s 0 b 0 len;
Bytes.to_string b
let bin =
let open Bin in
record (fun _value _fraction _currency ->
(* no need to decode amount? *)
assert false)
|+ field neint64 (fun t -> t.value)
|+ field neint32 (fun t -> t.fraction)
|+ field (bytes currency_len) (fun t -> pad_currency t.currency)
|> sealr
let bin_nbo =
let open Bin in
record (fun _value _fraction _currency -> assert false)
|+ field beint64 (fun t -> t.value)
|+ field beint32 (fun t -> t.fraction)
|+ field (bytes currency_len) (fun t -> pad_currency t.currency)
|> sealr

27
src/amount.mli Normal file
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 currency_len : int
val jsont : t Jsont.t
(* only for encoding *)
val bin : t Bin.t
val bin_nbo : t Bin.t

317
src/api.ml Normal file
View file

@ -0,0 +1,317 @@
(* TODO
number
- number is "float", but we probably want int everywhere instead
- numeric values capped at 2^53 -1 inclusive because json
time
- better types
- issues with "never" = uint64_max *)
open Crypto
let encode_exn jsont v = Jsont_bytesrw.encode_string jsont v |> Result.get_ok
let encode jsont v = Jsont_bytesrw.encode_string jsont v
let decode jsont v = Jsont_bytesrw.decode_string jsont v
module ErrorDetail = struct
(* TODO GANA error codes
https://git.gnunet.org/gana.git/tree/gnu-taler-error-codes/registry.rec *)
type t = {
code: int;
hint: string option;
}
let jsont =
let make code hint = { code; hint } in
let enc_code v = v.code in
let enc_hint v = v.hint in
Jsont.Object.map ~kind:"ErrorDetail" make
|> Jsont.Object.mem "code" Jsont.int ~enc:enc_code
|> Jsont.Object.opt_mem "hint" Jsont.(string) ~enc:enc_hint
|> Jsont.Object.finish
end
module HashCode : sig
type t
val hash : string -> t
val jsont : t Jsont.t
end = struct
type t = B32.t
let hash s =
let open Digestif.SHA512 in
s |> digest_string |> to_raw_string
let jsont = Jsont.of_of_string ~kind:"HashCode" B32.decode ~enc:B32.encode
end
module RelativeTime = struct
type t =
| Microseconds of float
| Forever
let number_or_forever_jsont =
let forever =
let dec s =
match s with
| "forever" -> Forever
| _ -> Jsont.Error.msg Jsont.Meta.none "unexpected string value"
in
let enc = function Forever -> "forever" | _ -> assert false in
Jsont.map ~dec ~enc Jsont.string
in
let number =
let dec n = Microseconds n in
let enc = function Microseconds n -> n | _ -> assert false in
Jsont.map ~dec ~enc Jsont.number
in
let enc = function Forever -> forever | Microseconds _ -> number in
Jsont.any ~dec_string:forever ~dec_number:number ~enc ()
let jsont =
let make t_s = t_s in
Jsont.Object.map ~kind:"RelativeTime" make
|> Jsont.Object.mem "t_s" number_or_forever_jsont ~enc:Fun.id
|> Jsont.Object.finish
end
module RsaDenominationKey = struct
type t = {
age_mask: int;
rsa_pub: RsaPublicKey.t;
}
let jsont =
let make age_mask rsa_pub = { age_mask; rsa_pub } in
let age_mask v = v.age_mask in
let rsa_pub v = v.rsa_pub in
Jsont.Object.map ~kind:"RsaDenominationKey" make
|> Jsont.Object.mem "age_mask" Jsont.int ~enc:age_mask
|> Jsont.Object.mem "rsa_pub" RsaPublicKey.jsont ~enc:rsa_pub
|> Jsont.Object.finish
end
(* ! Clause Schnorr cipher is not supported *)
module CSDenominationKey = struct
type t = {
age_mask: int;
cs_pub: B32.t;
}
(* TODO B32.jsont *)
let b32_jsont = Jsont.of_of_string ~kind:"B32" B32.decode ~enc:B32.encode
let jsont =
let make age_mask cs_pub = { age_mask; cs_pub } in
let age_mask v = v.age_mask in
let cs_pub v = v.cs_pub in
Jsont.Object.map ~kind:"CSDenominationKey" make
|> Jsont.Object.mem "age_mask" Jsont.int ~enc:age_mask
|> Jsont.Object.mem "cs_pub" b32_jsont ~enc:cs_pub
|> Jsont.Object.finish
end
module DenominationKey = struct
type t = Rsa of RsaDenominationKey.t
(*| CS of CSDenominationKey.t*)
let to_octets = function Rsa denom -> RsaPublicKey.to_octets denom.rsa_pub
let of_cs _v =
Jsont.Error.msg Jsont.Meta.none "CSDenominationKey are not supported"
let of_rsa v = Rsa v
let jsont =
let open Jsont.Object in
let rsa = Case.map "RSA" RsaDenominationKey.jsont ~dec:of_rsa in
let cs = Case.map "CS" CSDenominationKey.jsont ~dec:of_cs in
let enc_case = function Rsa v -> Case.value rsa v in
let cases = Case.[ make rsa; make cs ] in
map ~kind:"DenominationKey" Fun.id
|> case_mem "cipher" Jsont.string ~enc:Fun.id ~enc_case cases
|> finish
end
module FutureSignKey = struct
type t = {
key: EddsaPublicKey.t;
stamp_start: Timestamp.t;
stamp_expire: Timestamp.t;
stamp_end: Timestamp.t;
signkey_secmod_sig: EddsaSignature.t;
}
let jsont =
let make key stamp_start stamp_expire stamp_end signkey_secmod_sig =
{ key; stamp_start; stamp_expire; stamp_end; signkey_secmod_sig }
in
let key v = v.key in
let stamp_start v = v.stamp_start in
let stamp_expire v = v.stamp_expire in
let stamp_end v = v.stamp_end in
let signkey_secmod_sig v = v.signkey_secmod_sig in
let open Jsont.Object in
map ~kind:"FutureSignKey" make
|> mem "key" EddsaPublicKey.jsont ~enc:key
|> mem "stamp_start" Timestamp.jsont ~enc:stamp_start
|> mem "stamp_expire" Timestamp.jsont ~enc:stamp_expire
|> mem "stamp_end" Timestamp.jsont ~enc:stamp_end
|> mem "signkey_secmod_sig" EddsaSignature.jsont ~enc:signkey_secmod_sig
|> finish
end
module FutureDenom = struct
type t = {
section_name: string;
value: Amount.t;
stamp_start: Timestamp.t;
stamp_expire_withdraw: Timestamp.t;
stamp_expire_deposit: Timestamp.t;
stamp_expire_legal: Timestamp.t;
denom_pub: DenominationKey.t;
fee_withdraw: Amount.t;
fee_deposit: Amount.t;
fee_refresh: Amount.t;
fee_refund: Amount.t;
denom_secmod_sig: Bin_signature.DenominationKeyAnnouncementPS.Sig.t;
}
let jsont =
let make 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 =
{
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
let section_name t = t.section_name in
let value t = t.value in
let stamp_start t = t.stamp_start in
let stamp_expire_withdraw t = t.stamp_expire_withdraw in
let stamp_expire_deposit t = t.stamp_expire_deposit in
let stamp_expire_legal t = t.stamp_expire_legal in
let denom_pub t = t.denom_pub in
let fee_withdraw t = t.fee_withdraw in
let fee_deposit t = t.fee_deposit in
let fee_refresh t = t.fee_refresh in
let fee_refund t = t.fee_refund in
let denom_secmod_sig t = t.denom_secmod_sig in
let open Jsont.Object in
map ~kind:"FutureDenom" make
|> mem "section_name" Jsont.string ~enc:section_name
|> mem "value" Amount.jsont ~enc:value
|> mem "stamp_start" Timestamp.jsont ~enc:stamp_start
|> mem "stamp_expire_withdraw" Timestamp.jsont ~enc:stamp_expire_withdraw
|> mem "stamp_expire_deposit" Timestamp.jsont ~enc:stamp_expire_deposit
|> mem "stamp_expire_legal" Timestamp.jsont ~enc:stamp_expire_legal
|> mem "denom_pub" DenominationKey.jsont ~enc:denom_pub
|> mem "fee_withdraw" Amount.jsont ~enc:fee_withdraw
|> mem "fee_deposit" Amount.jsont ~enc:fee_deposit
|> mem "fee_refresh" Amount.jsont ~enc:fee_refresh
|> mem "fee_refund" Amount.jsont ~enc:fee_refund
|> mem "denom_secmod_sig"
Bin_signature.DenominationKeyAnnouncementPS.Sig.jsont
~enc:denom_secmod_sig
|> finish
end
module FutureKeysResponse = struct
type t = {
future_denoms: FutureDenom.t list;
future_signkeys: FutureSignKey.t list;
master_pub: EddsaPublicKey.t;
denom_secmod_public_key: EddsaPublicKey.t;
signkey_secmod_public_key: EddsaPublicKey.t;
}
let jsont =
let make future_denoms future_signkeys master_pub denom_secmod_public_key
signkey_secmod_public_key =
{
future_denoms;
future_signkeys;
master_pub;
denom_secmod_public_key;
signkey_secmod_public_key;
}
in
let future_denoms t = t.future_denoms in
let future_signkeys t = t.future_signkeys in
let master_pub t = t.master_pub in
let denom_secmod_public_key t = t.denom_secmod_public_key in
let signkey_secmod_public_key t = t.signkey_secmod_public_key in
let open Jsont.Object in
map ~kind:"FutureKeysResponse" make
|> mem "future_denoms" (Jsont.list FutureDenom.jsont) ~enc:future_denoms
|> mem "future_signkeys"
(Jsont.list FutureSignKey.jsont)
~enc:future_signkeys
|> mem "master_pub" EddsaPublicKey.jsont ~enc:master_pub
|> mem "denom_secmod_public_key" EddsaPublicKey.jsont
~enc:denom_secmod_public_key
|> mem "signkey_secmod_public_key" EddsaPublicKey.jsont
~enc:signkey_secmod_public_key
|> finish
end
module SignKeySignature = struct
type t = {
key: EddsaPublicKey.t;
master_sig: EddsaSignature.t;
}
let jsont =
let make key master_sig = { key; master_sig } in
let key v = v.key in
let master_sig v = v.master_sig in
let open Jsont.Object in
map ~kind:"SignKeySignature" make
|> mem "key" EddsaPublicKey.jsont ~enc:key
|> mem "master_sig" EddsaSignature.jsont ~enc:master_sig
|> finish
end
module DenomSignature = struct
type t = {
h_denom_pub: HashCode.t;
master_sig: EddsaSignature.t;
}
let jsont =
let make h_denom_pub master_sig = { h_denom_pub; master_sig } in
let h_denom_pub v = v.h_denom_pub in
let master_sig v = v.master_sig in
let open Jsont.Object in
map ~kind:"DenomSignature" make
|> mem "h_denom_pub" HashCode.jsont ~enc:h_denom_pub
|> mem "master_sig" EddsaSignature.jsont ~enc:master_sig
|> finish
end
module MasterSignatures = struct
type t = {
denom_sigs: DenomSignature.t list;
signkey_sigs: SignKeySignature.t list;
}
let jsont =
let make denom_sigs signkey_sigs = { denom_sigs; signkey_sigs } in
let denom_sigs v = v.denom_sigs in
let signkey_sigs v = v.signkey_sigs in
let open Jsont.Object in
map ~kind:"MasterSignatures" make
|> mem "denom_sigs" (Jsont.list DenomSignature.jsont) ~enc:denom_sigs
|> mem "signkey_sigs" (Jsont.list SignKeySignature.jsont) ~enc:signkey_sigs
|> finish
end

150
src/assets.ml Normal file
View file

@ -0,0 +1,150 @@
(* TODO clean up *)
(* docs: https://docs.taler.net/manpages/taler-exchange.conf.5.html
https://docs.taler.net/design-documents/003-tos-rendering.html *)
(* hardcoded config just for static assets *)
let default_lang = "en"
let default_encoding : [< `Identity | `DEFLATE | `Gzip ] = `Identity
(* todo: Taler documentation markdown mimetype should be the prefered one, and be
supported, according to DD we take text/plain as default instead for now *)
let default_mimetype = ("text", "plain")
let default_extension = ".txt"
let terms_legal_version = "1"
let privacy_legal_version = "1"
module Mimetype = struct
let mimetype_extension_assoc =
[
(("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 mimetype_l, _ = List.split mimetype_extension_assoc
let of_cohttp_media = function
| Cohttp.Accept.MediaType (m, m_sub) ->
List.find_opt (( = ) (m, m_sub)) mimetype_l
| AnyMediaSubtype m ->
List.find_opt (fun (m', _) -> String.equal m m') mimetype_l
| AnyMedia -> Some default_mimetype
let to_extension (m, m_sub) =
assert (m <> "*");
assert (m_sub <> "*");
List.assoc_opt (m, m_sub) mimetype_extension_assoc
let of_extension ext =
List.find_map
(fun (mime, ext') ->
match String.equal ext ext' with false -> None | true -> Some mime)
mimetype_extension_assoc
let pp_mime fmt mime = Fmt.pf fmt "%s/%s" (fst mime) (snd mime)
end
(* TODO config *)
type t =
| Terms
| Privacy
let etag k =
Result.get_ok
@@ Headers_lib.Etag.of_crockford32
@@ match k with Terms -> "0" | Privacy -> "0"
let legal_version = function
| Terms -> terms_legal_version
| Privacy -> privacy_legal_version
let base_dir = function
| Terms -> Fpath.v "terms"
| Privacy -> Fpath.v "privacy"
(* does some checks on assets/ folder content and infer the set of supported languages and mimetype *)
let supported_lang_arr, supported_ext_arr =
let open Syntax in
let path_l =
Assets_crunch.file_list |> list_map Fpath.of_string |> function
| Error (`Msg e) -> Fmt.failwith "%s" e
| Ok x -> x
in
let aux t =
let prefix = base_dir t in
let path_l = path_l |> List.filter_map (Fpath.rem_prefix prefix) in
let ext_l =
path_l |> List.map Fpath.get_ext |> List.sort_uniq String.compare
in
let lang_l =
path_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 path))
|> List.sort_uniq String.compare
in
let () =
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 default_lang lang_l then
Fmt.failwith "default language `%s` files not found" 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"
in
(lang_l, ext_l)
in
let lang_l, ext_l = aux Terms in
let lang_l', ext_l' = aux Privacy in
let () =
if
not
@@ (List.equal String.equal lang_l lang_l'
&& List.equal String.equal ext_l ext_l')
then
Fmt.failwith
"invalid folder structure, /terms and /privacy must support the same \
set of languages and mimetypes";
()
in
(Array.of_list lang_l, Array.of_list ext_l)
let supported_mimetype_arr =
Array.map Mimetype.of_extension supported_ext_arr |> Array.map Option.get
let is_supported_lang lang = Array.mem lang supported_lang_arr
let is_supported_ext ext = Array.mem ext supported_ext_arr
let is_supported_mimetype mime = Array.mem mime supported_mimetype_arr
(* ! lang and mime must be supported *)
let get_content ~lang ~mime t =
let etag = Headers_lib.Etag.to_raw_string (etag t) in
let ext =
match Mimetype.to_extension mime with
| None -> Fmt.failwith "mimetype `%s/%s` unknown" (fst mime) (snd mime)
| Some ext -> ext
in
let path = Fpath.to_string Fpath.((base_dir t / lang / etag) + ext) in
match Assets_crunch.read path with
| None -> Fmt.failwith "static file not found: `%s`" path
| Some data -> data

28
src/b32.ml Normal file
View file

@ -0,0 +1,28 @@
(* TODO test *)
(* 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 *)
type t = string
let alphabet = Base32.make_alphabet "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
let encode s = Base32.encode_string ~alphabet s
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
match Base32.decode ~alphabet ~off:0 ~len:(String.length s) s with
| Error (`Msg e) -> Error e
| Ok v -> Ok v

801
src/bin_signature.ml Normal file
View file

@ -0,0 +1,801 @@
(* Packed Signature *)
open Bin_type
open Bin_type.Aliases
(* EccSignaturePurpose *)
module Purpose = struct
type t = {
size: int32;
purpose: int32;
}
let bin =
let open Bin in
record (fun size purpose -> { size; purpose })
|+ field beint32 (fun t -> t.size)
|+ field beint32 (fun t -> t.purpose)
|> sealr
let make ~size purpose = { size= Int32.of_int size; purpose }
let dummy = make ~size:0 0_l
(* helper function to make ['signature Bin.t]
to compute [t.size], we first build a bin with a dummy purpose *)
let make_bin =
let get_size f =
let open Bin in
match Size.of_value (Size.size_of (f dummy)) with
| Dynamic _ | Unknown ->
Fmt.failwith "size_of failure: size is not Static"
| Static n -> n
in
fun code f -> make ~size:(get_size f) code |> f
let field purpose = Bin.field bin (fun _t -> purpose)
end
module WithdrawRequestPS = struct
(* Purpose is #TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW *)
type t = {
amount: Amount.t;
fee: Amount.t;
h_planchets: HashPlanchetsP.t;
blinding_seed: BlindingMasterSecret.t;
max_age_group: int32;
mask: AgeMask.t;
}
let bin =
let open Bin in
Purpose.make_bin Taler_signatures.wallet_reserve_withdraw @@ fun purpose ->
record
(fun _purpose amount fee h_planchets blinding_seed max_age_group mask ->
{ amount; fee; h_planchets; blinding_seed; max_age_group; mask })
|+ Purpose.field purpose
|+ field Amount.bin (fun t -> t.amount)
|+ field Amount.bin (fun t -> t.fee)
|+ field HashPlanchetsP.bin (fun t -> t.h_planchets)
|+ field BlindingMasterSecret.bin (fun t -> t.blinding_seed)
|+ field beint32 (fun t -> t.max_age_group)
|+ field AgeMask.bin (fun t -> t.mask)
|> sealr
end
module WithdrawConfirmationPS = struct
(* Purpose is #TALER_SIGNATURE_EXCHANGE_CONFIRM_WITHDRAW.
Signed by a `struct TALER_ExchangePrivateKeyP` using EdDSA. *)
type t = {
(* TODO TALER doc
missing TALER_HashBlindedPlanchetsP*)
h_planchets: HashPlanchetsP.t;
noreveal_index: int32;
}
let bin =
let open Bin in
Purpose.make_bin Taler_signatures.exchange_confirm_withdraw
@@ fun purpose ->
record (fun _purpose h_planchets noreveal_index ->
{ h_planchets; noreveal_index })
|+ Purpose.field purpose
|+ field HashPlanchetsP.bin (fun t -> t.h_planchets)
|+ field beint32 (fun t -> t.noreveal_index)
|> sealr
end
module DenominationKeyAnnouncementPS = struct
(* TODO taler_signatures purpose
we use TALER_SIGNATURE_SM_RSA_DENOMINATION_KEY instead here *)
(* purpose.purpose = TALER_SIGNATURE_SM_DENOMINATION_KEY *)
type r = {
h_denom_pub: DenominationHash.t;
h_section_name: Hash_64_cstr.t;
anchor_time: TimeAbsoluteNBO.t;
duration_withdraw: TimeRelativeNBO.t;
}
let bin =
let open Bin in
Purpose.make_bin Taler_signatures.sm_rsa_denomination_key @@ fun purpose ->
record
(fun _purpose h_denom_pub h_section_name anchor_time duration_withdraw ->
{ h_denom_pub; h_section_name; anchor_time; duration_withdraw })
|+ Purpose.field purpose
|+ field DenominationHash.bin (fun t -> t.h_denom_pub)
|+ field Hash_64_cstr.bin (fun t -> t.h_section_name)
|+ field TimeAbsoluteNBO.bin (fun t -> t.anchor_time)
|+ field TimeRelativeNBO.bin (fun t -> t.duration_withdraw)
|> sealr
module type SIG = sig
type t
val sign : (string -> string) -> r -> t
val verify : (string -> msg:string -> bool) -> t -> r -> bool
val jsont : t Jsont.t
end
module Sig : SIG = struct
type t = string
let sign f r = f @@ Bin.to_string bin r
let verify f s r = f s ~msg:(Bin.to_string bin r)
(* TODO B32.jsont, for others types too *)
let jsont = Jsont.string
end
end
module SigningKeyAnnouncementPS = struct
(* purpose.purpose = TALER_SIGNATURE_SM_SIGNING_KEY *)
type t = {
exchange_pub: ExchangePublicKeyP.t;
anchor_time: TimeAbsoluteNBO.t;
duration: TimeRelativeNBO.t;
}
let bin =
let open Bin in
Purpose.make_bin Taler_signatures.sm_signing_key @@ fun purpose ->
record (fun _purpose exchange_pub anchor_time duration ->
{ exchange_pub; anchor_time; duration })
|+ Purpose.field purpose
|+ field ExchangePublicKeyP.bin (fun t -> t.exchange_pub)
|+ field TimeAbsoluteNBO.bin (fun t -> t.anchor_time)
|+ field TimeRelativeNBO.bin (fun t -> t.duration)
|> sealr
end
module DenominationKeyValidityPS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_DENOMINATION_KEY_VALIDITY *)
type t = {
master: MasterPublicKeyP.t;
start: TimeAbsoluteNBO.t;
expire_withdraw: TimeAbsoluteNBO.t;
expire_spend: TimeAbsoluteNBO.t;
expire_legal: TimeAbsoluteNBO.t;
value: AmountNBO.t;
fee_withdraw: AmountNBO.t;
fee_deposit: AmountNBO.t;
fee_refresh: AmountNBO.t;
denom_hash: DenominationHash.t;
}
let bin =
let open Bin in
Purpose.make_bin Taler_signatures.master_denomination_key_validity
@@ fun purpose ->
record
(fun
_purpose
master
start
expire_withdraw
expire_spend
expire_legal
value
fee_withdraw
fee_deposit
fee_refresh
denom_hash
->
{
master;
start;
expire_withdraw;
expire_spend;
expire_legal;
value;
fee_withdraw;
fee_deposit;
fee_refresh;
denom_hash;
})
|+ Purpose.field purpose
|+ field MasterPublicKeyP.bin (fun t -> t.master)
|+ field TimeAbsoluteNBO.bin (fun t -> t.start)
|+ field TimeAbsoluteNBO.bin (fun t -> t.expire_withdraw)
|+ field TimeAbsoluteNBO.bin (fun t -> t.expire_spend)
|+ field TimeAbsoluteNBO.bin (fun t -> t.expire_legal)
|+ field AmountNBO.bin (fun t -> t.value)
|+ field AmountNBO.bin (fun t -> t.fee_withdraw)
|+ field AmountNBO.bin (fun t -> t.fee_deposit)
|+ field AmountNBO.bin (fun t -> t.fee_refresh)
|+ field DenominationHash.bin (fun t -> t.denom_hash)
|> sealr
end
module ExchangeSigningKeyValidityPS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_SIGNING_KEY_VALIDITY *)
type t = {
start: TimeAbsoluteNBO.t;
expire: TimeAbsoluteNBO.t;
end_: TimeAbsoluteNBO.t; (* "end" renamed to end_ *)
signkey_pub: ExchangePublicKeyP.t;
}
let bin =
let open Bin in
Purpose.make_bin Taler_signatures.master_signing_key_validity
@@ fun purpose ->
record (fun _purpose start expire end_ signkey_pub ->
{ start; expire; end_; signkey_pub })
|+ Purpose.field purpose
|+ field TimeAbsoluteNBO.bin (fun t -> t.start)
|+ field TimeAbsoluteNBO.bin (fun t -> t.expire)
|+ field TimeAbsoluteNBO.bin (fun t -> t.end_)
|+ field ExchangePublicKeyP.bin (fun t -> t.signkey_pub)
|> sealr
end
(* ### BIN IMPL END ### *)
module SingleWithdrawRequestPS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW *)
type t = {
amount_with_fee: AmountNBO.t;
h_denomination_pub: DenominationHash.t;
h_coin_envelope: BlindedCoinHash.t;
}
end
module DepositRequestPS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_COIN_DEPOSIT *)
type t = {
h_contract_terms: PrivateContractHash.t;
h_age_commitment: AgeCommitmentHash.t;
h_policy: ExtensionsPolicyHash.t;
h_wire: MerchantWireHash.t;
h_denom_pub: DenominationHash.t;
timestamp: TimeAbsoluteNBO.t;
refund_deadline: TimeAbsoluteNBO.t;
amount_with_fee: AmountNBO.t;
deposit_fee: AmountNBO.t;
merchant: MerchantPublicKeyP.t;
wallet_data_hash: Hash_64_cstr.t;
}
end
module DepositConfirmationPS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_CONFIRM_DEPOSIT *)
type t = {
h_contract_terms: PrivateContractHash.t;
h_wire: MerchantWireHash.t;
h_policy: ExtensionsPolicyHash.t;
timestamp: TimeAbsoluteNBO.t;
refund_deadline: TimeAbsoluteNBO.t;
amount_without_fee: AmountNBO.t;
coin_pub: CoinSpendPublicKeyP.t;
merchant: MerchantPublicKeyP.t;
}
end
module RefreshMeltCoinAffirmationPS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_COIN_MELT *)
type t = {
session_hash: RefreshCommitmentP.t;
h_denom_pub: DenominationHash.t;
h_age_commitment: AgeCommitmentHash.t;
amount_with_fee: AmountNBO.t;
melt_fee: AmountNBO.t;
}
end
module RefreshMeltConfirmationPS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_CONFIRM_MELT *)
type t = {
session_hash: RefreshCommitmentP.t;
noreveal_index: int; (* uint16_t mapped to OCaml int *)
}
end
module ExchangeKeySetPS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_KEY_SET *)
type t = {
list_issue_date: TimeAbsoluteNBO.t;
hc: Hash_64_cstr.t;
}
end
module MasterWireDetailsPS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_WIRE_DETAILS *)
type t = {
h_wire_details: FullPaytoHash.t;
h_conversion_url: Hash_64_cstr.t;
h_credit_restrictions: Hash_64_cstr.t;
h_debit_restrictions: Hash_64_cstr.t;
}
end
module MasterWireFeePS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_WIRE_FEES *)
type t = {
h_wire_method: Hash_64_cstr.t;
start_date: TimeAbsoluteNBO.t;
end_date: TimeAbsoluteNBO.t;
wire_fee: AmountNBO.t;
closing_fee: AmountNBO.t;
}
end
module GlobalFeesPS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_GLOBAL_FEES *)
type t = {
start_date: TimeAbsoluteNBO.t;
end_date: TimeAbsoluteNBO.t;
purse_timeout: TimeRelativeNBO.t;
kyc_timeout: TimeRelativeNBO.t;
history_expiration: TimeRelativeNBO.t;
history_fee: AmountNBO.t;
kyc_fee: AmountNBO.t;
account_fee: AmountNBO.t;
purse_fee: AmountNBO.t;
purse_account_limit: int; (* uint32_t → int *)
}
end
module MasterDrainProfitPS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_DRAIN_PROFITS *)
type t = {
wtid: WireTransferIdentifierRawP.t;
date: TimeAbsoluteNBO.t;
amount: AmountNBO.t;
h_section: Hash_64_cstr.t;
h_payto: FullPaytoHash.t;
}
end
module DepositTrackPS = struct
(* purpose.purpose = TALER_SIGNATURE_MERCHANT_TRACK_TRANSACTION *)
type t = {
h_contract_terms: PrivateContractHash.t;
h_wire: MerchantWireHash.t;
coin_pub: CoinSpendPublicKeyP.t;
}
end
module WireDepositDetailP = struct
type t = {
h_contract_terms: PrivateContractHash.t;
execution_time: TimeAbsoluteNBO.t;
coin_pub: CoinSpendPublicKeyP.t;
deposit_value: AmountNBO.t;
deposit_fee: AmountNBO.t;
}
end
module WireDepositDataPS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE_DEPOSIT *)
type t = {
total: AmountNBO.t;
wire_fee: AmountNBO.t;
merchant_pub: MerchantPublicKeyP.t;
h_wire: MerchantWireHash.t;
h_details: Hash_64_cstr.t;
}
end
module ExchangeKeyValidityPS = struct
(* purpose.purpose = TALER_SIGNATURE_AUDITOR_EXCHANGE_KEYS *)
type t = {
auditor_url_hash: Hash_64_cstr.t;
master: MasterPublicKeyP.t;
start: TimeAbsoluteNBO.t;
expire_withdraw: TimeAbsoluteNBO.t;
expire_spend: TimeAbsoluteNBO.t;
expire_legal: TimeAbsoluteNBO.t;
value: AmountNBO.t;
fee_withdraw: AmountNBO.t;
fee_deposit: AmountNBO.t;
fee_refresh: AmountNBO.t;
denom_hash: DenominationHash.t;
}
end
module PaymentResponsePS = struct
(* purpose.purpose = TALER_SIGNATURE_MERCHANT_PAYMENT_OK *)
type t = { h_contract_terms: PrivateContractHash.t }
end
module ContractPS = struct
(* purpose.purpose = TALER_SIGNATURE_MERCHANT_CONTRACT *)
type t = { h_contract_terms: PrivateContractHash.t }
end
module ConfirmWirePS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE *)
type t = {
h_wire: MerchantWireHash.t;
h_contract_terms: PrivateContractHash.t;
wtid: WireTransferIdentifierRawP.t;
coin_pub: CoinSpendPublicKeyP.t;
execution_time: TimeAbsoluteNBO.t;
coin_contribution: AmountNBO.t;
}
end
module RefundConfirmationPS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND *)
type t = {
h_contract_terms: PrivateContractHash.t;
coin_pub: CoinSpendPublicKeyP.t;
merchant: MerchantPublicKeyP.t;
rtransaction_id: int64;
refund_amount: AmountNBO.t;
}
end
module DepositTrackPS2 = struct
(* purpose.purpose = TALER_SIGNATURE_MERCHANT_TRACK_TRANSACTION *)
type t = {
h_contract_terms: PrivateContractHash.t;
h_wire: MerchantWireHash.t;
merchant: MerchantPublicKeyP.t;
coin_pub: CoinSpendPublicKeyP.t;
}
end
module RefundRequestPS = struct
(* purpose.purpose = TALER_SIGNATURE_MERCHANT_REFUND *)
type t = {
h_contract_terms: PrivateContractHash.t;
coin_pub: CoinSpendPublicKeyP.t;
rtransaction_id: int64;
refund_amount: AmountNBO.t;
refund_fee: AmountNBO.t;
}
end
module MerchantRefundConfirmationPS = struct
(* purpose.purpose = TALER_SIGNATURE_MERCHANT_REFUND_OK *)
(* Hash of the order ID (a string), hashed without the 0-termination. *)
type t = { h_order_id: Hash_64_cstr.t }
end
module RecoupRequestPS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_COIN_RECOUP or TALER_SIGNATURE_WALLET_COIN_RECOUP_REFRESH *)
type t = {
h_denom_pub: DenominationHash.t;
coin_blind: DenominationBlindingKeyP.t;
}
end
module RecoupRefreshConfirmationPS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH *)
type t = {
timestamp: TimeAbsoluteNBO.t;
recoup_amount: AmountNBO.t;
coin_pub: CoinSpendPublicKeyP.t;
old_coin_pub: CoinSpendPublicKeyP.t;
}
end
module RecoupConfirmationPS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP *)
type t = {
timestamp: TimeAbsoluteNBO.t;
recoup_amount: AmountNBO.t;
coin_pub: CoinSpendPublicKeyP.t;
reserve_pub: ReservePublicKeyP.t;
}
end
module DenominationUnknownAffirmationPS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_UNKNOWN *)
type t = {
timestamp: TimeAbsoluteNBO.t;
h_denom_pub: DenominationHash.t;
}
end
module DenominationExpiredAffirmationPS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_GENERIC_DENOMINATIN_EXPIRED *)
type t = {
timestamp: TimeAbsoluteNBO.t;
operation: string; (* char[8] → string *)
h_denom_pub: DenominationHash.t;
}
end
module ReserveCloseConfirmationPS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_RESERVE_CLOSED *)
type t = {
timestamp: TimeAbsoluteNBO.t;
closing_amount: AmountNBO.t;
reserve_pub: ReservePublicKeyP.t;
h_wire: FullPaytoHash.t;
}
end
module CoinLinkSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_COIN_LINK *)
type t = {
h_denom_pub: DenominationHash.t;
old_coin_pub: CoinSpendPublicKeyP.t;
transfer_pub: TransferPublicKeyP.t;
coin_envelope_hash: BlindedCoinHash.t;
}
end
module RefreshNonceSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_COIN_LINK *)
type t = { nonce: PublicRefreshCoinNonceP.t }
end
module ReserveStatusRequestSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_RESERVE_STATUS_REQUEST *)
type t = { request_timestamp: TimeAbsoluteNBO.t }
end
module ReserveHistoryRequestSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_RESERVE_HISTORY_REQUEST *)
type t = {
history_fee: AmountNBO.t;
request_timestamp: TimeAbsoluteNBO.t;
}
end
module PurseStatusRequestSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_PURSE_STATUS_REQUEST *)
type t = unit
end
module PurseStatusResponseSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_PURSE_STATUS_RESPONSE *)
type t = {
total_purse_amount: AmountNBO.t;
total_deposit_amount: AmountNBO.t;
max_deposit_fees: AmountNBO.t;
purse_expiration: TimeAbsoluteNBO.t;
status_timestamp: TimeAbsoluteNBO.t;
h_contract_terms: PrivateContractHash.t;
}
end
module ReserveCloseRequestSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_RESERVE_CLOSE *)
type t = unit
end
module PurseRequestSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_PURSE_CREATE *)
type t = {
purse_expiration: TimeAbsoluteNBO.t;
merge_value_after_fees: AmountNBO.t;
h_contract_terms: PrivateContractHash.t;
min_age: int;
}
end
module PurseDepositSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_PURSE_DEPOSIT *)
type t = {
coin_contribution: AmountNBO.t;
h_denom_pub: DenominationHash.t;
h_age_commitment: AgeCommitmentHash.t;
purse_pub: PursePublicKey.t;
h_exchange_base_url: Hash_64_cstr.t;
}
end
module PurseDepositSignaturePS2 = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_RESERVE_OPEN_DEPOSIT *)
type t = {
reserve_sig: ReserveSignatureP.t;
coin_contribution: AmountNBO.t;
}
end
module PurseDepositConfirmedSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_PURSE_DEPOSIT_CONFIRMED *)
type t = {
total_purse_amount: AmountNBO.t;
total_deposit_fees: AmountNBO.t;
purse_pub: PursePublicKey.t;
purse_expiration: TimeAbsoluteNBO.t;
h_contract_terms: PrivateContractHash.t;
}
end
module PurseMergeSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_PURSE_MERGE *)
type t = {
merge_timestamp: TimeAbsoluteNBO.t;
h_wire: NormalizedPaytoHash.t;
}
end
module AccountMergeSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_ACCOUNT_MERGE *)
type t = {
reserve_pub: ReservePublicKeyP.t;
purse_pub: PursePublicKey.t;
merge_amount_after_fees: AmountNBO.t;
merge_timestamp: TimeAbsoluteNBO.t;
purse_expiration: TimeAbsoluteNBO.t;
h_contract_terms: PrivateContractHash.t;
min_age: int;
}
end
module AccountSetupRequestSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_ACCOUNT_SETUP *)
type t = { threshold: AmountNBO.t }
end
module PurseMergeSuccessSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_PURSE_MERGE_SUCCESS *)
type t = {
reserve_pub: ReservePublicKeyP.t;
purse_pub: PursePublicKey.t;
merge_amount_after_fees: AmountNBO.t;
contract_time: TimeAbsoluteNBO.t;
h_contract_terms: PrivateContractHash.t;
h_wire: NormalizedPaytoHash.t;
min_age: int;
}
end
module WadDataSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_WAD_DATA *)
type t = {
wad_execution_time: TimeAbsoluteNBO.t;
total_amount: AmountNBO.t;
h_items: Hash_64_cstr.t;
wad_id: WadId.t;
}
end
module WadPartnerSignaturePS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_PARTNER_DETAILS *)
type t = {
h_partner_base_url: Hash_64_cstr.t;
master_public_key: MasterPublicKeyP.t;
start_date: TimeAbsoluteNBO.t;
end_date: TimeAbsoluteNBO.t;
wad_fee: AmountNBO.t;
wad_frequency: TimeRelativeNBO.t;
}
end
module P2PFeesPS = struct
(* purpose.purpose = TALER_SIGNATURE_P2P_FEES *)
type t = {
start_date: TimeAbsoluteNBO.t;
end_date: TimeAbsoluteNBO.t;
kyc_fee: AmountNBO.t;
purse_fee: AmountNBO.t;
account_history_fee: AmountNBO.t;
account_annual_fee: AmountNBO.t;
account_kyc_timeout: TimeRelativeNBO.t;
purse_timeout: TimeRelativeNBO.t;
purse_account_limit: int;
}
end
module CoinPurseRefundConfirmationPS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_CONFIRM_PURSE_REFUND *)
type t = {
purse_pub: PursePublicKey.t;
coin_pub: CoinSpendPublicKeyP.t;
refunded_amount: AmountNBO.t;
refund_fee: AmountNBO.t;
}
end
module MasterDenominationKeyRevocationPS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_DENOMINATION_KEY_REVOKED *)
type t = { h_denom_pub: DenominationHash.t }
end
module MasterSigningKeyRevocationPS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_SIGNING_KEY_REVOKED *)
type t = { exchange_pub: ExchangePublicKeyP.t }
end
module MasterAddAuditorPS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_ADD_AUDITOR *)
type t = {
start_date: TimeAbsoluteNBO.t;
auditor_pub: AuditorPublicKeyP.t;
h_auditor_url: Hash_64_cstr.t;
}
end
module MasterDelAuditorPS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_DEL_AUDITOR *)
type t = {
end_date: TimeAbsoluteNBO.t;
auditor_pub: AuditorPublicKeyP.t;
}
end
module MasterAddWirePS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_ADD_WIRE *)
type t = {
start_date: TimeAbsoluteNBO.t;
h_wire: FullPaytoHash.t;
h_conversion_url: Hash_64_cstr.t;
h_credit_restrictions: Hash_64_cstr.t;
h_debit_restrictions: Hash_64_cstr.t;
}
end
module MasterDelWirePS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_DEL_WIRE *)
type t = {
end_date: TimeAbsoluteNBO.t;
h_wire: FullPaytoHash.t;
}
end
module MasterAmlOfficerStatusPS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_AML_KEY *)
type t = {
change_date: TimestampNBO.t;
officer_pub: AmlOfficerPublicKeyP.t;
h_officer_name: Hash_64_cstr.t;
is_active: int;
}
end
module AmlDecisionPS = struct
(* purpose.purpose = TALER_SIGNATURE_AML_DECISION *)
type t = {
h_justification: Hash_64_cstr.t;
decision_time: TimestampNBO.t;
new_threshold: AmountNBO.t;
h_payto: NormalizedPaytoHash.t;
h_kyc_requirements: Hash_64_cstr.t;
new_state: int;
}
end
module PartnerConfigurationPS = struct
(* purpose.purpose = TALER_SIGNATURE_MASTER_PARNTER_DETAILS *)
type t = {
partner_pub: MasterPublicKeyP.t;
start_date: TimestampNBO.t;
end_date: TimestampNBO.t;
wad_frequency: TimeRelativeNBO.t;
wad_fee: AmountNBO.t;
h_url: Hash_64_cstr.t;
}
end
module ReserveOpenPS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_RESERVE_OPEN *)
type t = {
reserve_payment: AmountNBO.t;
request_timestamp: TimestampNBO.t;
reserve_expiration: TimestampNBO.t;
purse_limit: int;
}
end
module ReserveClosePS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_RESERVE_CLOSE *)
type t = {
request_timestamp: TimestampNBO.t;
target_account_h_payto: FullPaytoHash.t;
}
end
module ReserveAttestRequestPS = struct
(* purpose.purpose = TALER_SIGNATURE_WALLET_ATTEST_REQUEST *)
type t = {
request_timestamp: TimestampNBO.t;
h_details: Hash_64_cstr.t;
}
end
module ExchangeAttestPS = struct
(* purpose.purpose = TALER_SIGNATURE_EXCHANGE_RESERVE_ATTEST_DETAILS *)
type t = {
attest_timestamp: TimestampNBO.t;
expiration_time: TimestampNBO.t;
reserve_pub: ReservePublicKeyP.t;
h_attributes: Hash_64_cstr.t;
}
end

269
src/bin_type.ml Normal file
View file

@ -0,0 +1,269 @@
(* https://docs.taler.net/core/api-common.html#binary-formats
numeric values are in network byte order (big endian) *)
(* structs that are 'packed' and do not contain pointers and are
thus suitable for hashing or similar operations are distinguished
by adding a 'P' at the end of the name.
(NEW) Note that this convention does not hold for the GNUnet-structs (yet).
structs that are used with a purpose for signatures,
additionally get an 'S' at the end of the name.
(from https://docs.taler.net/taler-developer-manual.html) *)
(* TODO
- correctly handle endianness
- check that our struct are well packed
- it looks like Bin only define packed structs
- we don't need to worry about struct having "P" suffix
remove them
- test them
- union: not sure what to do of them
not needed or relevant i think
- some purpose (`TALER_SIGNATURE_XXX`) are missing
- exchange and gana master branch are not in sync
and we should use a specific git tag instead
- outdated doc(?)
- some missing struct documentation
- better way to have module aliases?
*)
(* TODO hash and C(ancer)-terminated strings
- "A JSON object is canonicalized by converting it to an ASCII byte array
with the algorithm specified in RFC 8785. The resulting bytes are
terminated with a single 0-byte and then hashed with SHA512."
- from the code it looks like its the same for all stringy-strings
! not strings that are raw-bytes-data-like
? only for "HashCode" and "ShortHashCode"
*)
module Taler_signatures = Include.Taler_signatures
open Crypto
let int32_size = 4
let int64_size = 8
module Bytes_32 = struct
type t = string
let bin = Bin.bytes 32
end
module Bytes_64 = struct
type t = string
let bin = Bin.bytes 64
end
(* -- Time -- *)
module type Time_S = sig
type t = Timestamp.t
val bin : t Bin.t
end
module TIME : Time_S = struct
type t = Timestamp.t
let bin = Timestamp.bin
end
module TIME_NBO : Time_S = struct
type t = Timestamp.t
let bin = Timestamp.bin_nbo
end
module TimeAbsolute : Time_S = TIME
module TimeAbsoluteNBO : Time_S = TIME_NBO
module TimeRelative : Time_S = TIME
module TimeRelativeNBO : Time_S = TIME_NBO
(* -- Cryptographic primitives -- *)
(* Hashes *)
module Hash_32 = struct
type t = Digestif.SHA256.t
let hash s = Digestif.SHA256.(digest_string s)
let of_octets s =
match String.length s = 32 with
| false -> Fmt.failwith "Hash.of_octets failure: data is not 32 bytes"
| true -> Digestif.SHA256.of_raw_string s
let to_octets = Digestif.SHA256.to_raw_string
let bin =
let open Bin in
map (bytes 32) of_octets to_octets
let caqti : t Caqti_type.t =
let open Caqti_type in
custom
~encode:(fun v -> Ok (to_octets v))
~decode:(fun v -> Ok (of_octets v))
octets
end
module Hash_64 = struct
type t = Digestif.SHA512.t
let hash s = Digestif.SHA512.(digest_string s)
let of_octets s =
match String.length s = 64 with
| false -> Fmt.failwith "Hash.of_octets failure: data is not 64 bytes"
| true -> Digestif.SHA512.of_raw_string s
let to_octets v =
let s = Digestif.SHA512.to_raw_string v in
match String.length s = 64 with
| false -> Fmt.failwith "Hash.to_octets failure: data is not 64 bytes"
| true -> s
let bin =
let open Bin in
map (bytes 64) of_octets to_octets
let caqti : t Caqti_type.t =
let open Caqti_type in
custom
~encode:(fun v -> Ok (to_octets v))
~decode:(fun v -> Ok (of_octets v))
octets
end
(* Hash over string + '\0' *)
module Hash_32_cstr = struct
include Hash_32
let hash s =
let s = s ^ "\x00" in
Digestif.SHA256.(digest_string s)
end
module Hash_64_cstr = struct
include Hash_64
let hash s =
let s = s ^ "\x00" in
Digestif.SHA512.(digest_string s)
end
module type Hash_S = sig
type t
val bin : t Bin.t
val caqti : t Caqti_type.t
val hash : string -> t
val of_octets : string -> t
val to_octets : t -> string
end
module FullPaytoHash : Hash_S = Hash_32
module NormalizedPaytoHash : Hash_S = Hash_32
module DenominationHash : Hash_S = Hash_64
module PrivateContractHash : Hash_S = Hash_64
module ExtensionsPolicyHash : Hash_S = Hash_64
module MerchantWireHash : Hash_S = Hash_64
module AgeCommitmentHash : Hash_S = Hash_64
module BlindedCoinHash : Hash_S = Hash_64
module CoinPubHash : Hash_S = Hash_64
module OutputCommitmentHash : Hash_S = Hash_64
module HashPlanchetsP : Hash_S = Hash_64
(* --- Various --- *)
module TransferSecretP = Bytes_64
module LinkSecretP = Bytes_64
module EncryptedLinkSecretP = Bytes_64
module BlindingMasterSeed = Bytes_32
module BlindingMasterSecret = Bytes_32
module WireTransferIdentifierRawP = Bytes_32
module PublicRefreshCoinNonceP = Bytes_64
(* TODO ? need to use/save a specific nonce for cryptographic blinding *)
(* Secret for blinding/unblinding.
An RSA blinding secret, which is basically
a 256-bit nonce, converted to Crockford `Base32`.
type DenominationBlindingKeyP = string; *)
module DenominationBlindingKeyP = Bytes_32
module RefreshCommitmentP = Bytes_64
(* -- TODO better: -- *)
module UUID = struct
(* uint32t value[4]; *)
type t = { value: string }
let size = 4 * int32_size
let bin =
let open Bin in
record (fun value -> { value })
|+ field (bytes size) (fun t -> t.value)
|> sealr
end
module WadId = struct
(* uint32t value[6]; *)
type t = { raw: string }
let size = 6 * int32_size
let bin =
let open Bin in
record (fun raw -> { raw }) |+ field (bytes size) (fun t -> t.raw) |> sealr
end
module AgeMask = struct
type t = { mask: int32 }
let bin =
let open Bin in
record (fun mask -> { mask }) |+ field beint32 (fun t -> t.mask) |> sealr
end
(* TODO keep this?
some of those are actuall ecdhe, or union of eddsa|ecdhe *)
module Aliases = struct
module TimestampNBO : Time_S = struct
type t = Timestamp.t
let bin = Timestamp.bin_nbo
end
module AmountNBO = struct
type t = Amount.t
let bin = Amount.bin_nbo
end
(* - Keys - *)
module PursePublicKey = EddsaPublicKey
module AuditorPublicKeyP = EddsaPublicKey
module ReservePublicKeyP = EddsaPublicKey
module MerchantPublicKeyP = EddsaPublicKey
module TransferPublicKeyP = EddsaPublicKey
module AmlOfficerPublicKeyP = EddsaPublicKey
module ExchangePublicKeyP = EddsaPublicKey
module MasterPublicKeyP = EddsaPublicKey
module CoinSpendPublicKeyP = EddsaPublicKey
module TokenPublicKeyP = EddsaPublicKey
module ReservePrivateKeyP = EddsaPrivateKey
module MerchantPrivateKeyP = EddsaPrivateKey
module TransferPrivateKeyP = EddsaPrivateKey
module AmlOfficerPrivateKeyP = EddsaPrivateKey
module ExchangePrivateKeyP = EddsaPrivateKey
module MasterPrivateKeyP = EddsaPrivateKey
module CoinSpendPrivateKeyP = EddsaPrivateKey
module MasterSignatureP = EddsaSignature
module ReserveSignatureP = EddsaSignature
module ExchangeSignatureP = EddsaSignature
module CoinSpendSignatureP = EddsaSignature
end

196
src/config.ml Normal file
View file

@ -0,0 +1,196 @@
open Parse_config
let config_data =
let path = Fpath.to_string (Fpath.v "default.config") in
match Assets_crunch.read path with
| None -> fail "static file not found: `%s`" path
| Some data ->
let v = Parse_data.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
let stefan_abs = get "stefan_abs" |> amount
let stefan_log = get "stefan_log" |> amount
let stefan_lin = get_opt "stefan_lin" |> Option.map float
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"
let privacy_etag = get "privacy_etag"
(* optional:
tiny_amount
shopping_url
open_banking_gateway_url
aml_spa_dialect
bank_compliance_language
toplevel_redirect_url *)
(* not implemented or not relevant to MTE:
let max_requests = get "max_requests" |> int
base_url
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" |> Parse_alt_unit_names.parse;
}
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: Ptime.Span.t;
duration_spend: Ptime.Span.t;
duration_legal: Ptime.Span.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

159
src/crypto.ml Normal file
View file

@ -0,0 +1,159 @@
(* TODO key format
- check what is the exact format in GNUNET
- endianess issue? *)
module EddsaPublicKey = 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 = pub
let to_octets t = pub_to_octets t
let of_octets t = pub_of_octets t |> Result.get_ok
let bin = Bin.map (Bin.bytes 32) of_octets to_octets
let of_b32 s =
let open Syntax in
let* octets = B32.decode s in
match pub_of_octets octets with
| Error e -> Fmt.error "%a" Mirage_crypto_ec.pp_error e
| Ok pub -> Ok pub
let to_b32 t = B32.encode (to_octets t)
let jsont = Jsont.of_of_string ~kind:"EddsaPublicKey" of_b32 ~enc:to_b32
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 |> Result.get_ok
let bin = Bin.map (Bin.bytes 32) of_octets to_octets
let of_b32 s =
let open Syntax in
let* octets = B32.decode s in
match priv_of_octets octets with
| Error e -> Fmt.error "%a" Mirage_crypto_ec.pp_error e
| Ok priv -> Ok priv
let to_b32 t = B32.encode (to_octets t)
let jsont = Jsont.of_of_string ~kind:"EddsaPrivateKey" of_b32 ~enc:to_b32
end
module EddsaSignature : sig
type t
val sign : key:Mirage_crypto_ec.Ed25519.priv -> string -> t
val sign_as_string : key:Mirage_crypto_ec.Ed25519.priv -> string -> string
val of_b32 : string -> (t, string) result
val to_b32 : t -> string
val to_octets : t -> string
val of_octets : string -> t
val jsont : t Jsont.t
val bin : t Bin.t
end = struct
(* TODO key format
endianess issue? *)
(* EdDSA signatures are transmitted as 64-bytes base32
binary-encoded objects with just the R and S values (base32_ binary-only).
They are signature over a c-struct like `TALER_xxxPS` + with a purpose *)
type t = string
let to_octets t = t
let of_octets v =
match String.length v = 64 with
| false ->
Fmt.failwith "EddsaSignature.of_octets failure: data is not 64 bytes."
| true -> v
let bin = Bin.map (Bin.bytes 64) of_octets to_octets
let sign ~key s =
(* mirage_crypto: "The result is the concatenation of r and s, as specified in RFC 8032." *)
Mirage_crypto_ec.Ed25519.sign ~key s
let sign_as_string ~key s = sign ~key s
let check_size t =
match String.length t = 64 with
| false -> Error "EddsaSignature: invalid string length"
| true -> Ok ()
let of_b32 s =
let open Syntax in
let* t = B32.decode s in
let+ () = check_size t in
t
let to_b32 = B32.encode
let jsont = Jsont.of_of_string ~kind:"EddsaSignature" of_b32 ~enc:to_b32
end
module RsaPublicKey = struct
open Mirage_crypto_pk
type t = Rsa.pub
let of_octets = Util.Bin_rsa.pub_of_octets
let to_octets = Util.Bin_rsa.pub_to_octets
let of_b32 s =
let open Syntax in
let* s = B32.decode s in
let v = Util.Bin_rsa.pub_of_octets s in
let+ v = Rsa.pub ~n:v.n ~e:v.e |> unwrap_err_msg in
v
let to_b32 t = B32.encode (to_octets t)
let jsont = Jsont.of_of_string ~kind:"RsaPublicKey" of_b32 ~enc:to_b32
end
module RsaPrivateKey = struct
open Mirage_crypto_pk.Rsa
type t = priv
let pub_of_priv = pub_of_priv
let of_octets = Util.Bin_rsa.priv_of_octets
let to_octets = Util.Bin_rsa.priv_to_octets
let of_b32 s =
let open Syntax in
let* s = B32.decode s in
Ok (of_octets s)
let to_b32 t = B32.encode (to_octets t)
let jsont = Jsont.of_of_string ~kind:"RsaPrivateKey" of_b32 ~enc:to_b32
end
module RsaSignature : sig
type t
val to_octets : t -> string
val sign : key:Mirage_crypto_pk.Rsa.priv -> string -> t
val of_b32 : string -> (t, string) result
val to_b32 : t -> string
val jsont : t Jsont.t
end = struct
type t = string
let to_octets t = t
(* TODO rsa sign *)
let sign ~key s =
Mirage_crypto_pk.Rsa.decrypt ~crt_hardening:true ~mask:`Yes ~key s
let of_b32 s = B32.decode s
let to_b32 t = B32.encode t
let jsont = Jsont.of_of_string ~kind:"RsaSignature" of_b32 ~enc:to_b32
end

93
src/data_file.ml Normal file
View file

@ -0,0 +1,93 @@
open Bos.OS
open Syntax
open Crypto
let read fname =
let* b = File.exists fname in
match b with
| false -> Ok None
| true ->
let+ content = File.read fname in
Some content
let read_eddsa fname =
let+ content_opt = read fname in
Option.map EddsaPrivateKey.of_octets content_opt
let read_rsa fname =
let+ content_opt = read fname in
Option.map RsaPrivateKey.of_octets content_opt
let write_eddsa fname priv = EddsaPrivateKey.to_octets priv |> File.write fname
let write_rsa fname priv = RsaPrivateKey.to_octets priv |> File.write fname
let load_signkey conn fname =
let* opt = read_eddsa fname in
match opt with
| None -> Ok None
| Some priv -> (
let pub = EddsaPrivateKey.pub_of_priv priv in
let* opt = Pg.lookup_signing_key conn pub in
match opt with
| None ->
Fmt.error_msg
"load_signkey error no associated metadata found in database for \
signkey `%s`."
(Fpath.to_string fname)
| Some (stamp_start, stamp_expire, stamp_end) ->
(* TODO master_sig *)
let master_sig = None in
let v =
Signkey.
{ pub; priv; stamp_start; stamp_expire; stamp_end; master_sig }
in
Ok (Some v))
let load_denom conn ~section_name fname =
let* opt = read_rsa fname in
match opt with
| None -> Ok None
| Some priv -> (
let pub = RsaPrivateKey.pub_of_priv priv in
let h_pub = Bin_type.DenominationHash.hash (RsaPublicKey.to_octets pub) in
let* opt = Pg.lookup_denomination_key conn h_pub in
match opt with
| None ->
Fmt.error_msg
"load_denom error no associated metadata found in database for \
denom `%s`."
(Fpath.to_string fname)
| Some
( stamp_start,
stamp_expire_withdraw,
stamp_expire_deposit,
stamp_expire_legal,
value,
fee_withdraw,
fee_deposit,
fee_refresh,
fee_refund,
age_mask ) ->
(* TODO master_sig *)
let master_sig = None in
let v =
Denomination.
{
pub;
priv;
section_name;
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;
}
in
Ok (Some v))

33
src/database.ml Normal file
View file

@ -0,0 +1,33 @@
(* TODO
- GNU Taler db-events?
it seems caqti/pgx does not support it *)
let on_ok req () =
let open Vif.Response.Syntax in
let* () =
Vif.Response.add ~field:"content-type" "text/plain; charset= utf-8"
in
let* () =
Vif.Response.with_string req (Fmt.str "activate_signing_key done~~@.")
in
Vif.Response.respond `OK
let on_error req err =
(* TODO be sure to not leak private data in error messages *)
let open Vif.Response.Syntax in
let str = Fmt.str "Database error: %a." Caqti_error.pp err in
Logs.err (fun m -> m "%s" str);
let* () = Vif.Response.with_string req str in
Vif.Response.respond `Internal_server_error
(* TODO master_sig *)
let dummy_master_sig =
Option.some @@ Crypto.EddsaSignature.of_octets (String.make 64 '\x00')
let test_activate req server _ =
let db_conn = Vif.Server.device Devices.db_connection server in
let secmod_signkey = Vif.Server.device Devices.secmod_signkey server in
let sm_key = secmod_signkey.sm_key in
let sm_key = { sm_key with master_sig= dummy_master_sig } in
let res = Pg.activate_signing_key db_conn sm_key in
Result.fold ~ok:(on_ok req) ~error:(on_error req) res

71
src/denomination.ml Normal file
View file

@ -0,0 +1,71 @@
open Crypto
type t = {
pub: RsaPublicKey.t;
priv: RsaPrivateKey.t;
section_name: string;
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: Bin_type.DenominationHash.t;
master_sig: EddsaSignature.t option;
}
let make
({
section_name;
value;
duration_withdraw;
duration_spend;
duration_legal;
fee_withdraw;
fee_deposit;
fee_refresh;
fee_refund;
cipher;
rsa_keysize;
age_restricted= _;
} :
Config.Coin.t) =
assert (cipher = `RSA);
let stamp_start = Ptime_clock.now () |> Option.some in
let stamp_expire_withdraw =
Timestamp.add_span_exn stamp_start (Some duration_withdraw)
in
let stamp_expire_deposit =
Timestamp.add_span_exn stamp_start (Some duration_spend)
in
let stamp_expire_legal =
Timestamp.add_span_exn stamp_start (Some duration_legal)
in
let open Mirage_crypto_pk.Rsa in
let priv = generate ~bits:rsa_keysize () in
let pub = pub_of_priv priv in
let h_pub = Bin_type.DenominationHash.hash (RsaPublicKey.to_octets pub) in
let master_sig = None in
{
pub;
priv;
section_name;
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;
}

148
src/devices.ml Normal file
View file

@ -0,0 +1,148 @@
open Syntax
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)
module Secmod_signkey = struct
(* TODO
- key rotation
- how many signkey to use?
we just use 1 for now *)
type t = {
sm_key: Signkey.t;
keys: Signkey.t list;
}
let dir = Fpath.(v "data" / "secmod_signkey")
let store_secmod_data t =
let* () = Data_file.write_eddsa Fpath.(dir / "sm_key") t.sm_key.priv in
t.keys
|> List.mapi (fun i key ->
let fname = Fpath.(dir / string_of_int i) in
(fname, key.Signkey.priv))
|> list_iter (fun (fname, key) -> Data_file.write_eddsa fname key)
let load conn =
let error_invalid_state =
Fmt.error_msg "secmod_signkey load error: invalid store state."
in
let* sm_key = Data_file.load_signkey conn Fpath.(dir / "sm_key") in
let* keys =
let l = List.init 1 (fun i -> Fpath.(dir / string_of_int i)) in
let* l =
Syntax.list_map (fun fname -> Data_file.load_signkey conn fname) l
in
match Syntax.opt_list l with
| Error () -> error_invalid_state
| Ok opt -> Ok opt
in
match (sm_key, keys) with
| None, None -> Ok None
| Some sm_key, Some keys -> Ok (Some { sm_key; keys })
| _, _ -> error_invalid_state
let generate_fresh_secmod_data () =
let sm_key = Signkey.generate () in
let keys = [ Signkey.generate () ] in
{ sm_key; keys }
let v =
let finally _key = () in
Vif.Device.v ~name:"secmod_signkey" ~finally
[ Vif.Device.value db_connection ]
@@ fun conn (_env : env) ->
match load conn with
| Ok None ->
let t = generate_fresh_secmod_data () in
Logs.info (fun m -> m "secmod_signkey initialized with fresh keys");
t
| Ok (Some v) ->
Logs.info (fun m -> m "secmod_signkey initialized from storage");
v
| Error _ ->
(* TODO error: pretty print *)
Fmt.failwith "secmod_signkey init failure."
end
module Secmod_denom = struct
type t = {
sm_key: Signkey.t;
keys: Denomination.t list;
}
let dir = Fpath.(v "data" / "secmod_signkey")
let store_secmod_data t =
let* () = Data_file.write_eddsa Fpath.(dir / "sm_key") t.sm_key.priv in
t.keys
|> List.mapi (fun i key ->
let fname = Fpath.(dir / string_of_int i) in
(fname, key.Denomination.priv))
|> list_iter (fun (fname, key) -> Data_file.write_rsa fname key)
let load conn =
let error_invalid_state =
Fmt.error_msg "secmod_denom load error: invalid store state."
in
let* sm_key = Data_file.load_signkey conn Fpath.(dir / "sm_key") in
let* keys =
let* l =
let open Config.Coin in
Syntax.list_map
(fun coin ->
let section_name = coin.section_name in
let fname = Fpath.(dir / section_name) in
(* todo: could check that coin config match db values *)
Data_file.load_denom conn ~section_name fname)
all_coins
in
match Syntax.opt_list l with
| Error () -> error_invalid_state
| Ok opt -> Ok opt
in
match (sm_key, keys) with
| None, None -> Ok None
| Some sm_key, Some keys -> Ok (Some { sm_key; keys })
| _, _ -> error_invalid_state
let generate_fresh_secmod_data () =
let sm_key = Signkey.generate () in
let keys = List.map Denomination.make Config.Coin.all_coins in
{ sm_key; keys }
let v =
let finally _key = () in
Vif.Device.v ~name:"secmod_denom" ~finally
[ Vif.Device.value db_connection ]
@@ fun conn (_env : env) ->
match load conn with
| Ok None ->
let t = generate_fresh_secmod_data () in
Logs.info (fun m -> m "secmod_denom initialized with fresh keys");
t
| Ok (Some v) ->
Logs.info (fun m -> m "secmod_denom initialized from storage");
v
| Error _ -> Fmt.failwith "secmod_denom init failure."
end
let secmod_signkey = Secmod_signkey.v
let secmod_denom = Secmod_denom.v

45
src/dune Normal file
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
include
;
caqti
caqti-miou
caqti-miou.unix
caqti-driver-pgx
bin
mirage-crypto
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 ../data/assets/))
(action
(with-stdout-to
%{null}
(run ocaml-crunch -m plain ../data/assets -o %{target}))))

52
src/headers.ml Normal file
View file

@ -0,0 +1,52 @@
let pp_array pp_item item = Fmt.array ~sep:(Fmt.any ", ") pp_item item
let accept_header_value =
let s =
Fmt.str "%a"
(pp_array Assets.Mimetype.pp_mime)
Assets.supported_mimetype_arr
in
s
let avail_languages_header_value =
let s = Fmt.str "%a" (pp_array Fmt.string) Assets.supported_lang_arr in
s
let select_mimetype headers =
let accept = Vif.Headers.get headers "accept" in
Cohttp.Accept.media_ranges accept
|> Cohttp.Accept.qsort
|> List.filter_map (fun (_q, (m, _p)) -> Assets.Mimetype.of_cohttp_media m)
|> List.find_opt Assets.is_supported_mimetype
let select_language headers =
let accept_language = Vif.Headers.get headers "accept-language" in
Cohttp.Accept.languages accept_language
|> Cohttp.Accept.qsort
|> List.map (fun (_q, lang) -> lang)
|> List.map (function
| Cohttp.Accept.AnyLanguage -> Assets.default_lang
| Language language_range -> (
(* ignore language subtags (e.g. "en-US" -> "en") *)
match language_range with
| [] -> assert false
| primary_tag :: _ -> primary_tag))
|> List.find_opt Assets.is_supported_lang
|> Option.value ~default:Assets.default_lang
let select_encoding headers =
Vif.Headers.get headers "accept-encoding"
|> Cohttp.Accept.encodings
|> Cohttp.Accept.qsort
|> List.map snd
|> List.filter_map (function
| Cohttp.Accept.Identity -> Some `Identity
| Deflate -> Some `DEFLATE
| Gzip -> Some `Gzip
| AnyEncoding -> Some Assets.default_encoding
| Encoding _ | Compress -> (* unsupported *) None)
|> function
| [] -> assert false
| `Identity :: _ -> None
| `DEFLATE :: _ -> Some `DEFLATE
| `Gzip :: _ -> Some `Gzip

86
src/headers_lib.ml Normal file
View file

@ -0,0 +1,86 @@
(* independent library for headers fields value *)
(* TODO - test - can still bypass this module and directly set Etag header, but
its fine *)
module Etag : sig
(* module to parse etags header fields used by If-Match and If-None-Match
headers
https://httpwg.org/specs/rfc9110.html#field.etag *)
type t
type header_value
val parse : string -> (header_value, string) result
val of_crockford32 : string -> (t, string) result
val to_raw_string : t -> string
val to_field_value : t -> string
val evaluate : t -> header_value -> bool
end = struct
(* raw etag *)
type t = string
(* type for the value of header field *)
type header_etag_item = {
weak: bool;
value: string;
}
type header_value =
| Any_etag
| Etag_list of header_etag_item list
let pp_header_etag_item ppf { weak; value } =
if weak then Fmt.pf ppf {|W/"%s"|} value else Fmt.pf ppf {|"%s"|} value
let to_raw_string t = t
let to_field_value t =
(* always weak comparison for If-None-Match header *)
let v = { weak= true; value= t } in
Fmt.str "%a" pp_header_etag_item v
let is_valid_char c =
let n = Char.code c in
(n >= 0x21 && n <= 0x7E && n <> 0x22) || (n >= 0x80 && n <= 0xFF)
let has_valid_charset s = String.for_all is_valid_char s
let of_crockford32 s =
match has_valid_charset s with
| false -> Error "invalid etag"
| true -> Ok s
let parse =
let open Angstrom in
let ws = skip_while (function ' ' -> true | _ -> false) in
let quoted_string =
char '"' *> take_till (fun c -> c = '"') <* char '"' >>= fun s ->
if String.for_all is_valid_char s then return s
else fail "found illegal char"
in
let item =
ws
*> lift2
(fun weak value -> { weak; value })
(option false (string "W/" *> return true))
quoted_string
<* ws
in
let comma = ws *> char ',' *> ws in
let list_of_items = sep_by1 comma item in
let parse_header_value =
char '*' *> return Any_etag
<|> (list_of_items >>| fun items -> Etag_list items)
<* end_of_input
in
fun s ->
match parse_string ~consume:Consume.All parse_header_value s with
| Error e -> Fmt.error "invalid etag: %s" e
| Ok v -> Ok v
let evaluate t header_value =
match header_value with
| Any_etag -> false
| Etag_list l ->
not @@ List.exists (fun { weak= _; value } -> String.equal t value) l
end

112
src/management.ml Normal file
View file

@ -0,0 +1,112 @@
open Api
open Devices
let mk_future_denom denom_key_signf
({
pub;
priv= _;
section_name;
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= _;
} :
Denomination.t) =
let denom_pub =
DenominationKey.of_rsa RsaDenominationKey.{ age_mask; rsa_pub= pub }
in
let denom_secmod_sig =
let open Bin_signature.DenominationKeyAnnouncementPS in
let h_denom_pub = h_pub in
let h_section_name = Bin_type.Hash_64_cstr.hash section_name in
let anchor_time = stamp_start in
let duration_withdraw =
Timestamp.diff stamp_start stamp_expire_withdraw |> Timestamp.of_span_exn
in
Sig.sign denom_key_signf
{ h_denom_pub; h_section_name; anchor_time; duration_withdraw }
in
FutureDenom.
{
section_name;
value;
stamp_start;
stamp_expire_withdraw;
stamp_expire_deposit;
stamp_expire_legal;
denom_pub;
fee_withdraw;
fee_deposit;
fee_refresh;
fee_refund;
denom_secmod_sig;
}
let mk_future_signkey signkey_signf
({ pub; priv= _; stamp_start; stamp_expire; stamp_end; master_sig= _ } :
Signkey.t) =
let signkey_secmod_sig =
let open Bin_signature.SigningKeyAnnouncementPS in
let exchange_pub = pub in
let anchor_time = stamp_start in
let duration =
Timestamp.diff stamp_start stamp_expire |> Timestamp.of_span_exn
in
{ exchange_pub; anchor_time; duration }
|> Bin.to_string bin
|> signkey_signf
in
FutureSignKey.
{ key= pub; stamp_start; stamp_expire; stamp_end; signkey_secmod_sig }
let mk_future_keys_response (secmod_signkey : Secmod_signkey.t)
(secmod_denom : Secmod_denom.t) =
let future_denoms =
secmod_denom.keys
|> List.filter (fun k -> Option.is_none k.Denomination.master_sig)
|> List.map (fun denom ->
let signf s =
Crypto.EddsaSignature.sign_as_string
~key:secmod_denom.sm_key.Signkey.priv s
in
mk_future_denom signf denom)
in
let future_signkeys =
secmod_signkey.keys
|> List.filter (fun k -> Option.is_none k.Signkey.master_sig)
|> List.map (fun signkey ->
let signf s =
Crypto.EddsaSignature.sign ~key:secmod_denom.sm_key.Signkey.priv s
in
mk_future_signkey signf signkey)
in
let master_pub = Config.Exchange.master_public_key in
let denom_secmod_public_key = secmod_denom.sm_key.pub in
let signkey_secmod_public_key = secmod_signkey.sm_key.pub in
FutureKeysResponse.
{
future_denoms;
future_signkeys;
master_pub;
denom_secmod_public_key;
signkey_secmod_public_key;
}
let keys_get req server _env =
let open Vif.Response in
let open Syntax in
let secmod_signkey = Vif.Server.device Devices.secmod_signkey server in
let secmod_denom = Vif.Server.device Devices.secmod_denom server in
let v = mk_future_keys_response secmod_signkey secmod_denom in
let s = Api.encode_exn Api.FutureKeysResponse.jsont v in
let* () = with_string req s in
let* () = add ~field:"content-type" "application/json" in
respond `OK

53
src/mte.ml Normal file
View file

@ -0,0 +1,53 @@
(* 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 open Vif.Type in*)
[
get (rel /?? nil) --> hello;
get (rel / "terms" /?? nil) --> Static.terms;
get (rel / "privacy" /?? nil) --> Static.privacy;
get (rel / "management" / "keys" /?? nil) --> Management.keys_get;
]
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.secmod_signkey; Devices.secmod_denom ]
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

270
src/parse_config.ml Normal file
View file

@ -0,0 +1,270 @@
(* parse config file
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 whitespace = skip_while is_whitespace
module Parse_data = struct
type line =
| 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 take_till_end_of_line =
take_till is_eol >>= fun s -> return s <* end_of_line
let blank = whitespace <* end_of_line >>| fun () -> Blank
let comment =
whitespace *> (char '#' <|> char '%') *> take_till_end_of_line >>| fun s ->
Comment s
let header = char '[' *> id <* char ']' <* end_of_line >>| fun s -> Header s
let item_value =
let unquoted_value =
take_while1 (fun c -> not (is_whitespace c || is_eol c))
in
let quoted_value =
char '"' *> take_till is_eol >>= fun s ->
match String.ends_with ~suffix:"\"" 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
(whitespace *> char '=' *> whitespace *> item_value)
<* end_of_line
let config = many (choice [ blank; 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 Pp_debug = struct
let pp_item ppf { key; value } =
let open Fmt in
match String.contains value '"' || String.contains value ' ' with
| false -> pf ppf "%s = %s" key value
| true -> pf ppf "%s = \"%s\"" key value
let pp_line ppf line =
let open Fmt in
let open Parse_data in
match line with
| Blank -> Fmt.nop ppf ()
| Comment s -> pf ppf "#%s" s
| Header s -> pf ppf "[%s]" s
| Item item -> pf ppf "%a" pp_item item
let _pp_lines ppf raw_line_l =
let open Fmt in
pf ppf "%a" (list ~sep:(any "\n") pp_line) raw_line_l
let pp_section ppf { header; items } =
let open Fmt in
pf ppf "[%s]@\n%a" header (list ~sep:(any "\n") pp_item) items
let pp_config ppf l =
let open Fmt in
pf ppf "%a" (list ~sep:(any "\n") pp_section) l
end
[@@ocaml.warning "-32"]
module Parse_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 = whitespace *> integer in
let dunit =
whitespace *> 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
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 ptime_span_of_int64 i =
match Ptime.Span.of_float_s (Int64.to_float i) with
| None ->
fail "ptime_span_of_int64 error: `%Ld` is not a valid ptime span" i
| Some ts -> ts
let to_ptime_span t =
let acc =
List.fold_left
(fun acc { number; dunit } -> acc + (number * dunit_to_seconds dunit))
0 t
in
let acc = Int64.of_int acc in
let ptime = ptime_span_of_int64 acc in
ptime
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_res = 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_res
let duration s = Parse_duration.(s |> parse |> to_ptime_span)
let ed25519 s =
s
|> B32.decode
|> unwrap_res
|> Mirage_crypto_ec.Ed25519.pub_of_octets
|> Result.map_error (fun e -> Fmt.str "%a" Mirage_crypto_ec.pp_error e)
|> unwrap_res
module Parse_alt_unit_names = struct
let rm_brackets s =
let s = String.trim s in
match
String.starts_with ~prefix:"{" s && String.ends_with ~suffix:"}" s
with
| false -> fail "expected json, got `%s`" s
| true ->
let s = String.sub s 1 (String.length s - 2) in
s
let rm_quotes s =
let s = String.trim s in
match
String.starts_with ~prefix:"\"" s && String.ends_with ~suffix:"\"" s
with
| false -> fail "expected quoted string, got `%s`" s
| true ->
let s = String.sub s 1 (String.length s - 2) in
s
let parse s =
let s = rm_brackets s in
String.split_on_char ',' s
|> List.map (String.split_on_char ':')
|> List.map (function
| [ k; v ] -> (k, v)
| _ -> fail "invalid json key-value map")
|> List.map (fun (k, v) ->
let k = rm_quotes k in
let v = rm_quotes v in
let k =
match int_of_string_opt k with
| None ->
fail "invalid json key-value map, expected integer key, got `%s`"
k
| Some k -> k
in
(k, v))
end

164
src/pg.ml Normal file
View file

@ -0,0 +1,164 @@
open Crypto
module Caqti_type = struct
include Caqti_type
(* TODO add a dune stanza like for prelude:
"(flags (:standard -open Prelude))" *)
(* we want to use int64 timestamps,
not postgresql built-in timestamp type *)
let ptime : Ptime.t option t = Timestamp.caqti
let time = Timestamp.caqti
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)
let age_mask : int t = Caqti_type.int
let rsa_public : RsaPublicKey.t t =
let open RsaPublicKey in
custom
~encode:(fun v -> Ok (to_octets v))
~decode:(fun v -> Ok (of_octets v))
octets
let eddsa_public : EddsaPublicKey.t t =
let open EddsaPublicKey in
custom
~encode:(fun v -> Ok (to_octets v))
~decode:(fun v -> Ok (of_octets v))
octets
let eddsa_signature : EddsaSignature.t t =
let open EddsaSignature in
custom
~encode:(fun v -> Ok (to_octets v))
~decode:(fun s -> Ok (of_octets s))
octets
include struct
(* alias for hash *)
open Bin_type
let fullpayto_hash = FullPaytoHash.caqti
let nomalizaedpayto_hash = NormalizedPaytoHash.caqti
let denomination_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
include Caqti_request.Infix
end
module type CONN = Caqti_miou.CONNECTION
open Bin_type
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 : Caqti_miou.CONNECTION) ->
Syntax.list_iter (fun p -> Conn.exec p ()) l
let lookup_signing_key =
let lookup_signing_key =
Caqti_type.(eddsa_public ->? t3 time time time)
"SELECT valid_from, expire_sign, expire_legal FROM exchange_sign_keys \
WHERE exchange_pub=$1"
in
fun (module Conn : CONN) (exchange_pub : EddsaPublicKey.t) ->
Conn.find_opt lookup_signing_key exchange_pub
let activate_signing_key =
let insert_signkey =
Caqti_type.(t5 eddsa_public time time time eddsa_signature ->. 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)
Signkey.{ pub; priv= _; stamp_start; stamp_expire; stamp_end; master_sig }
->
(* TODO master_sig *)
let master_sig = master_sig |> Option.get in
Conn.exec insert_signkey
(pub, stamp_start, stamp_expire, stamp_end, master_sig)
let lookup_denomination_key =
let lookup_denomination_key =
Caqti_type.(
denomination_hash
->? t10 time time time time amount amount amount amount amount age_mask)
"SELECT valid_from, expire_withdraw, expire_deposit, expire_legal, coin, \
fee_withdraw, fee_deposit, fee_refresh, fee_refund, age_mask FROM \
denominations WHERE denom_pub_hash=$1"
in
fun (module Conn : CONN) (h_denom_pub : DenominationHash.t) ->
Conn.find_opt lookup_denomination_key h_denom_pub
let add_denomination_key =
let denomination_insert =
Caqti_type.(
t12 denomination_hash rsa_public eddsa_signature time time time time
amount amount amount amount (t2 amount age_mask)
->. unit)
"INSERT INTO denominations (denom_pub_hash, denom_pub, master_sig, \
valid_from, expire_withdraw, expire_deposit, expire_legal, coin, \
fee_withdraw, fee_deposit, fee_refresh, fee_refund, age_mask) VALUES \
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)"
in
fun (module Conn : CONN)
Denomination.
{
pub;
priv= _;
section_name= _;
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;
}
->
(* TODO master_sig *)
let master_sig = master_sig |> Option.get in
Conn.exec denomination_insert
( h_pub,
pub,
master_sig,
stamp_start,
stamp_expire_withdraw,
stamp_expire_deposit,
stamp_expire_legal,
value,
fee_withdraw,
fee_deposit,
fee_refresh,
(fee_refund, age_mask) )

29
src/signkey.ml Normal file
View file

@ -0,0 +1,29 @@
open Crypto
(* TODO master_sig
not sur how to handle master_sig initialization *)
type t = {
pub: EddsaPublicKey.t;
priv: EddsaPrivateKey.t;
stamp_start: Timestamp.t;
stamp_expire: Timestamp.t;
stamp_end: Timestamp.t;
(* signature of this key by offline master key *)
master_sig: EddsaSignature.t option;
}
let generate () =
(* TODO
- look if it exists
- if not, create it (TOFU initialization scheme)
- write it *)
let stamp_start = Ptime_clock.now () |> Option.some in
let stamp_expire =
Timestamp.add_span_exn stamp_start
(Some Config.Exchange.signkey_legal_duration)
in
let stamp_end = stamp_expire in
let priv, pub = Mirage_crypto_ec.Ed25519.generate () in
let master_sig = None in
{ pub; priv; stamp_start; stamp_expire; stamp_end; master_sig }

83
src/static.ml Normal file
View file

@ -0,0 +1,83 @@
(* TODO check for mathcing ETAG with a middleware instead? *)
(* /terms + /privacy
- try to find a response with an acceptable mime-type
- pick the version in the most preferred language of the user
- apply compression if that is allowed by the client
- set ETAG header
- If it did not change, a "304 Not Modified" response will be returned
- A "Taler-Terms-Version" header is generated to indicate the legal version of the terms
- When returning a full response (not a "304 Not Modified"),
include a "Avail-Languages" header: a comma-separated list of the languages available *)
module Respond_with = struct
open Vif.Response
open Syntax
open struct
let error_detail ?hint _status =
let open Api in
let code = -1 in
let s = encode_exn ErrorDetail.jsont { code; hint } in
s
end
let bad_request ?hint req =
let body = error_detail ?hint `Bad_request in
let* () = with_string ?compression:None req body in
respond `Bad_request
let not_modified () =
let* () = empty in
respond `Not_modified
let unsupported_media_type req =
let body =
error_detail ~hint:"no acceptable mimetype" `Unsupported_media_type
in
let* () = with_string ?compression:None req body in
let* () = add ~field:"accept" Headers.accept_header_value in
respond `Unsupported_media_type
end
let aux kind req _server _env =
let etag = Assets.etag kind 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.Etag.parse s |> Result.map (Headers_lib.Etag.evaluate etag)
in
match has_matching_etag with
| Error e -> Respond_with.bad_request ~hint:e req
| Ok true -> Respond_with.not_modified ()
| Ok false -> (
match Headers.select_mimetype headers with
| None -> Respond_with.unsupported_media_type req
| Some mime ->
let lang = Headers.select_language headers in
let compression = Headers.select_encoding headers in
let data = Assets.get_content ~mime ~lang kind 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_value etag in
add ~field:"etag" etag_field_value
in
let* () =
(* todo: is it "taler-privacy-version" for /policy ? *)
add ~field:"taler-terms-version" Assets.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 mime in
add ~field:"content-type" content_type
in
respond `OK)
let terms req _server _env = aux Assets.Terms req _server _env
let privacy req _server _env = aux Assets.Privacy req _server _env

48
src/syntax.ml Normal file
View file

@ -0,0 +1,48 @@
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 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 ()

69
src/timestamp.ml Normal file
View file

@ -0,0 +1,69 @@
type t = Ptime.t option
type span = Ptime.Span.t option
let diff a b =
match (a, b) with
| None, _ | _, None -> None
| Some a, Some b -> Some (Ptime.diff a b)
let add_span_exn t span =
match (t, span) with
| None, _ | _, None -> None
| Some t, Some span -> (
match Ptime.add_span t span with
| None -> Fmt.failwith "add_span_exn: not in the range [min;max]"
| Some v -> Some v)
let of_span_exn = function
| None -> None
| Some span -> (
match Ptime.of_span span with
| None -> Fmt.failwith "of_span_exn: not in the range [min;max]"
| Some p -> Some p)
(* -- *)
(* microseconds since the UNIX Epoch, or "never" if None *)
let jsont =
let number_or_never_jsont =
let never =
let dec s =
match s with
| "never" -> None
| _ -> Jsont.Error.msg Jsont.Meta.none "unexpected string value"
in
let enc = function None -> "never" | _ -> assert false in
Jsont.map ~dec ~enc Jsont.string
in
let number =
let dec n = Ptime.of_float_s n in
let enc = function Some n -> Ptime.to_float_s n | _ -> assert false in
Jsont.map ~dec ~enc Jsont.number
in
let enc = function None -> never | Some _ -> number in
Jsont.any ~dec_string:never ~dec_number:number ~enc ()
in
let make t = t in
Jsont.Object.map ~kind:"Timestamp" make
|> Jsont.Object.mem "t_s" number_or_never_jsont ~enc:Fun.id
|> Jsont.Object.finish
(* TODO exn *)
let ptime_to_int64 ptime = ptime |> Ptime.to_float_s |> Int64.of_float
let ptime_of_int64 i =
match Ptime.of_float_s (Int64.to_float i) with
| None -> Fmt.failwith "ptime_of_int64 error: `%Ld` is not a valid ptime" i
| Some ts -> ts
let encode_int64 = function None -> Int64.max_int | Some p -> ptime_to_int64 p
let decode_int64 i = if i = Int64.max_int then None else Some (ptime_of_int64 i)
(* UINT64_MAX represents "never". *)
let bin = Bin.map Bin.neint64 decode_int64 encode_int64
let bin_nbo = Bin.map Bin.beint64 decode_int64 encode_int64
let caqti : Ptime.t option Caqti_type.t =
let encode v = Ok (encode_int64 v) in
let decode v = Ok (decode_int64 v) in
Caqti_type.custom ~encode ~decode Caqti_type.int64

15
src/timestamp.mli Normal file
View file

@ -0,0 +1,15 @@
(* TODO time
not sure about this *)
type t = Ptime.t option
type span = Ptime.Span.t option
val diff : t -> t -> span
val add_span_exn : t -> span -> t
val of_span_exn : span -> t
(* - *)
val jsont : t Jsont.t
val bin : t Bin.t
val bin_nbo : t Bin.t
val caqti : Ptime.t option Caqti_type.t

177
src/util.ml Normal file
View file

@ -0,0 +1,177 @@
(* TODO bin
- no [Bin.of_string] ? *)
let bin_of_string bin s =
let v = Bin.decode bin s (ref 0) in
Ok v
module Bin_rsa = struct
(* TODO
- need to strip leading zeros?
- endianess ok?
- tests *)
(* 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 *)
(* RSA private key custom format is inspired by the public rsa key format
used by secmod to save private key to file *)
open Syntax
module Internal = struct
let rev_string len s = String.init len (fun i -> s.[len - 1 - i])
(* we need reverse bytes because Z.of_bits reads bytes in little endian *)
let z_of_bits_be src pos len =
String.sub src pos len |> rev_string len |> Z.of_bits
let z_to_bits_be z =
let bits = Z.to_bits z in
rev_string (String.length bits) bits
let check = function false -> Error (`Msg "invalid data") | true -> Ok ()
let z_array_to_octets (arr : Z.t array) =
let nb = Array.length arr in
let bits_arr = Array.map z_to_bits_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
let* () = check (s_len > 2 * nb) in
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* () =
let len = (2 * nb) + Array.fold_left ( + ) 0 len_arr in
check (s_len = len)
in
let z_arr =
Array.init nb (fun i ->
let len = len_arr.(i) in
let z = z_of_bits_be s !pos len in
pos := !pos + len;
z)
in
Ok z_arr
end
open Internal
let pub_to_octets ({ n; e } : Mirage_crypto_pk.Rsa.pub) =
z_array_to_octets [| n; e |]
let pub_of_octets s =
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 in
Ok pub
| _ -> assert false
in
match pub_of_octets s with
| Error (`Msg e) -> Fmt.failwith "rsa pub_of_octets failure: %s@." e
| Ok v -> v
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 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' in
Ok priv
| _ -> assert false
in
match priv_of_octets s with
| Error (`Msg e) -> Fmt.failwith "rsa priv_of_octets failure: %s@." e
| Ok v -> v
end
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