Signer: support plain HTTP (no S) and a few cosmetic fixes

This commit is contained in:
Benjamin Canou 2018-06-13 23:05:36 +02:00
parent 0bb12b0655
commit 7ad44a9af3
17 changed files with 322 additions and 148 deletions

View File

@ -59,7 +59,17 @@ let sign
let public_key (cctxt : #Client_context.wallet) pkh =
log "Request for public key %a"
Signature.Public_key_hash.pp pkh >>= fun () ->
Client_keys.get_public_key cctxt pkh >>=? fun (name, pk) ->
log "Found public key for hash %a (name: %s)"
Signature.Public_key_hash.pp pkh name >>= fun () ->
return pk
Client_keys.list_keys cctxt >>=? fun all_keys ->
match List.find (fun (_, h, _, _) -> Signature.Public_key_hash.equal h pkh) all_keys with
| exception Not_found ->
log "No public key found for hash %a"
Signature.Public_key_hash.pp pkh >>= fun () ->
Lwt.fail Not_found
| (_, _, None, _) ->
log "No public key found for hash %a"
Signature.Public_key_hash.pp pkh >>= fun () ->
Lwt.fail Not_found
| (name, _, Some pk, _) ->
log "Found public key for hash %a (name: %s)"
Signature.Public_key_hash.pp pkh name >>= fun () ->
return pk

View File

@ -0,0 +1,66 @@
(**************************************************************************)
(* *)
(* Copyright (c) 2014 - 2018. *)
(* Dynamic Ledger Solutions, Inc. <contact@tezos.com> *)
(* *)
(* All rights reserved. No warranty, explicit or implicit, provided. *)
(* *)
(**************************************************************************)
let log = Signer_logging.lwt_log_notice
let run (cctxt : #Client_context.wallet) ~hosts ?magic_bytes ~require_auth mode =
let dir = RPC_directory.empty in
let dir =
RPC_directory.register1 dir Signer_services.sign begin fun pkh signature data ->
Handler.sign cctxt { pkh ; data ; signature } ?magic_bytes ~require_auth
end in
let dir =
RPC_directory.register1 dir Signer_services.public_key begin fun pkh () () ->
Handler.public_key cctxt pkh
end in
let dir =
RPC_directory.register0 dir Signer_services.authorized_keys begin fun () () ->
if require_auth then
return None
else
Handler.Authorized_key.load cctxt >>=? fun keys ->
return (Some (keys |> List.split |> snd |> List.map Signature.Public_key.hash))
end in
Lwt.catch
(fun () ->
List.map
(fun host ->
let host = Ipaddr.V6.to_string host in
log "Listening on address %s" host >>= fun () ->
RPC_server.launch ~host mode dir
~media_types:Media_type.all_media_types
>>= fun _server ->
fst (Lwt.wait ()))
hosts |> Lwt.choose)
(function
| Unix.Unix_error(Unix.EADDRINUSE, "bind","") ->
failwith "Port already in use."
| exn -> Lwt.return (error_exn exn))
let run_https (cctxt : #Client_context.wallet) ~host ~port ~cert ~key ?magic_bytes ~require_auth =
Lwt_utils_unix.getaddrinfo ~passive:true ~node:host ~service:(string_of_int port) >>= function
| []->
failwith "Cannot resolve listening address: %S" host
| points ->
let hosts = fst (List.split points) in
log "Accepting HTTPS requests on port %d" port >>= fun () ->
let mode : Conduit_lwt_unix.server =
`TLS (`Crt_file_path cert, `Key_file_path key, `No_password, `Port port) in
run (cctxt : #Client_context.wallet) ~hosts ?magic_bytes ~require_auth mode
let run_http (cctxt : #Client_context.wallet) ~host ~port ?magic_bytes ~require_auth =
Lwt_utils_unix.getaddrinfo ~passive:true ~node:host ~service:(string_of_int port) >>= function
| [] ->
failwith "Cannot resolve listening address: %S" host
| points ->
let hosts = fst (List.split points) in
log "Accepting HTTP requests on port %d" port >>= fun () ->
let mode : Conduit_lwt_unix.server =
`TCP (`Port port) in
run (cctxt : #Client_context.wallet) ~hosts ?magic_bytes ~require_auth mode

View File

@ -7,9 +7,16 @@
(* *)
(**************************************************************************)
val run:
val run_https:
#Client_context.io_wallet ->
host:string -> port:int -> cert:string -> key:string ->
?magic_bytes: int list ->
require_auth: bool ->
'a tzresult Lwt.t
val run_http:
#Client_context.io_wallet ->
host:string -> port:int ->
?magic_bytes: int list ->
require_auth: bool ->
'a tzresult Lwt.t

View File

@ -1,44 +0,0 @@
(**************************************************************************)
(* *)
(* Copyright (c) 2014 - 2018. *)
(* Dynamic Ledger Solutions, Inc. <contact@tezos.com> *)
(* *)
(* All rights reserved. No warranty, explicit or implicit, provided. *)
(* *)
(**************************************************************************)
let log = Signer_logging.lwt_log_notice
let run (cctxt : #Client_context.wallet) ~host ~port ~cert ~key ?magic_bytes ~require_auth =
log "Accepting HTTPS requests on port %d" port >>= fun () ->
let mode : Conduit_lwt_unix.server =
`TLS (`Crt_file_path cert, `Key_file_path key, `No_password, `Port port) in
let dir = RPC_directory.empty in
let dir =
RPC_directory.register1 dir Signer_services.sign begin fun pkh signature data ->
Handler.sign cctxt { pkh ; data ; signature } ?magic_bytes ~require_auth
end in
let dir =
RPC_directory.register1 dir Signer_services.public_key begin fun pkh () () ->
Handler.public_key cctxt pkh
end in
let dir =
RPC_directory.register0 dir Signer_services.authorized_keys begin fun () () ->
if require_auth then
return None
else
Handler.Authorized_key.load cctxt >>=? fun keys ->
return (Some (keys |> List.split |> snd |> List.map Signature.Public_key.hash))
end in
Lwt.catch
(fun () ->
RPC_server.launch ~host mode dir
~media_types:Media_type.all_media_types
~cors: { allowed_origins = [ "*" ] ;
allowed_headers = [ "Content-Type" ] }
>>= fun _server ->
fst (Lwt.wait ()))
(function
| Unix.Unix_error(Unix.EADDRINUSE, "bind","") ->
failwith "Port already in use."
| exn -> Lwt.return (error_exn exn))

View File

@ -32,6 +32,16 @@ let default_https_port =
| None -> "443"
| Some port -> port
let default_http_host =
match Sys.getenv_opt "TEZOS_SIGNER_HTTP_HOST" with
| None -> "localhost"
| Some host -> host
let default_http_port =
match Sys.getenv_opt "TEZOS_SIGNER_HTTP_PORT" with
| None -> "6732"
| Some port -> port
open Clic
let group =
@ -98,6 +108,31 @@ let commands base_dir require_auth =
(fun (magic_bytes, path) cctxt ->
Tezos_signer_backends.Encrypted.decrypt_all cctxt >>=? fun () ->
Socket_daemon.run cctxt (Unix path) ?magic_bytes ~require_auth) ;
command ~group
~desc: "Launch a signer daemon over HTTP."
(args3
magic_bytes_arg
(default_arg
~doc: "listening address or host name"
~short: 'a'
~long: "address"
~placeholder: "host|address"
~default: default_http_host
(parameter (fun _ s -> return s)))
(default_arg
~doc: "listening HTTP port"
~short: 'p'
~long: "port"
~placeholder: "port number"
~default: default_http_port
(parameter
(fun _ x ->
try return (int_of_string x)
with Failure _ -> failwith "Invalid port %s" x))))
(prefixes [ "launch" ; "http" ; "signer" ] @@ stop)
(fun (magic_bytes, host, port) cctxt ->
Tezos_signer_backends.Encrypted.decrypt_all cctxt >>=? fun () ->
Http_daemon.run_http cctxt ~host ~port ?magic_bytes ~require_auth) ;
command ~group
~desc: "Launch a signer daemon over HTTPS."
(args3
@ -123,14 +158,22 @@ let commands base_dir require_auth =
param
~name:"cert"
~desc: "path to th TLS certificate"
(parameter (fun _ s -> return s)) @@
(parameter (fun _ s ->
if not (Sys.file_exists s) then
failwith "No such TLS certificate file %s" s
else
return s)) @@
param
~name:"key"
~desc: "path to th TLS key"
(parameter (fun _ s -> return s)) @@ stop)
(parameter (fun _ s ->
if not (Sys.file_exists s) then
failwith "No such TLS key file %s" s
else
return s)) @@ stop)
(fun (magic_bytes, host, port) cert key cctxt ->
Tezos_signer_backends.Encrypted.decrypt_all cctxt >>=? fun () ->
Https_daemon.run cctxt ~host ~port ~cert ~key ?magic_bytes ~require_auth) ;
Http_daemon.run_https cctxt ~host ~port ~cert ~key ?magic_bytes ~require_auth) ;
command ~group
~desc: "Authorize a given public key to perform signing requests."
(args1

View File

@ -193,13 +193,13 @@ let raw_get_key (cctxt : #Client_context.wallet) pkh =
let get_key cctxt pkh =
raw_get_key cctxt pkh >>=? function
| (pkh, Some pk, Some sk) -> return (pkh, pk, sk)
| (_pkh, _pk, None) -> failwith "... FIXME ... E"
| (_pkh, None, _sk) -> failwith "... FIXME ... F"
| (_pkh, _pk, None) -> failwith "Unknown secret key for %a" Signature.Public_key_hash.pp pkh
| (_pkh, None, _sk) -> failwith "Unknown public key for %a" Signature.Public_key_hash.pp pkh
let get_public_key cctxt pkh =
raw_get_key cctxt pkh >>=? function
| (pkh, Some pk, _sk) -> return (pkh, pk)
| (_pkh, None, _sk) -> failwith "... FIXME ... G"
| (_pkh, None, _sk) -> failwith "Unknown public key for %a" Signature.Public_key_hash.pp pkh
let get_keys (cctxt : #Client_context.wallet) =
Secret_key.load cctxt >>=? fun sks ->

View File

@ -110,7 +110,7 @@ let main select_commands =
(module Tezos_signer_backends.Encrypted.Make(struct
let cctxt = (client_config :> Client_context.prompter)
end)) ;
let module Remote_authenticator = struct
let module Remote_params = struct
let authenticate pkhs payload =
Client_keys.list_keys client_config >>=? fun keys ->
match List.filter_map
@ -125,17 +125,20 @@ let main select_commands =
| [] -> failwith
"remote signer expects authentication signature, \
but no authorized key was found in the wallet"
let logger = rpc_config.logger
end in
let module Https = Tezos_signer_backends.Https.Make(Remote_authenticator) in
let module Socket = Tezos_signer_backends.Socket.Make(Remote_authenticator) in
let module Https = Tezos_signer_backends.Https.Make(Remote_params) in
let module Http = Tezos_signer_backends.Http.Make(Remote_params) in
let module Socket = Tezos_signer_backends.Socket.Make(Remote_params) in
Client_keys.register_signer (module Https) ;
Client_keys.register_signer (module Http) ;
Client_keys.register_signer (module Socket.Unix) ;
Client_keys.register_signer (module Socket.Tcp) ;
Option.iter parsed_config_file.remote_signer ~f: begin fun signer ->
Client_keys.register_signer
(module Tezos_signer_backends.Remote.Make(struct
let default = signer
include Remote_authenticator
include Remote_params
end))
end ;
select_commands ctxt parsed_args >>=? fun commands ->

View File

@ -41,7 +41,7 @@ module Raw = struct
| None -> return None
| Some bytes ->
match Data_encoding.Binary.of_bytes Signature.Secret_key.encoding bytes with
| None -> failwith "... FIXME ... D" (* corrupted data *)
| None -> failwith "Corrupted wallet, deciphered key is invalid"
| Some sk -> return (Some sk)
end
@ -77,7 +77,7 @@ let rec noninteractice_decrypt_loop ~encrypted_sk = function
let decrypt_payload cctxt ?name encrypted_sk =
match Base58.safe_decode encrypted_sk with
| None -> failwith "... FIXME ... A"
| None -> failwith "Not a Base58 encoded encrypted key"
| Some encrypted_sk ->
let encrypted_sk = MBytes.of_string encrypted_sk in
noninteractice_decrypt_loop ~encrypted_sk !passwords >>=? function

View File

@ -0,0 +1,10 @@
(**************************************************************************)
(* *)
(* Copyright (c) 2014 - 2018. *)
(* Dynamic Ledger Solutions, Inc. <contact@tezos.com> *)
(* *)
(* All rights reserved. No warranty, explicit or implicit, provided. *)
(* *)
(**************************************************************************)
include Http_gen.Make(struct let scheme = "http" end)

View File

@ -0,0 +1,16 @@
(**************************************************************************)
(* *)
(* Copyright (c) 2014 - 2018. *)
(* Dynamic Ledger Solutions, Inc. <contact@tezos.com> *)
(* *)
(* All rights reserved. No warranty, explicit or implicit, provided. *)
(* *)
(**************************************************************************)
module Make(P : sig
val authenticate: Signature.Public_key_hash.t list -> MBytes.t -> Signature.t tzresult Lwt.t
val logger: RPC_client.logger
end)
: Client_keys.SIGNER
val make_base: string -> int -> Uri.t

View File

@ -0,0 +1,91 @@
(**************************************************************************)
(* *)
(* Copyright (c) 2014 - 2018. *)
(* Dynamic Ledger Solutions, Inc. <contact@tezos.com> *)
(* *)
(* All rights reserved. No warranty, explicit or implicit, provided. *)
(* *)
(**************************************************************************)
module Make(N : sig val scheme : string end) = struct
open Client_keys
let scheme = N.scheme
module Make(P : sig
val authenticate: Signature.Public_key_hash.t list -> MBytes.t -> Signature.t tzresult Lwt.t
val logger: RPC_client.logger
end) = struct
let scheme = scheme
let title =
"Built-in tezos-signer using remote signer through hardcoded " ^ scheme ^ " requests."
let description =
"Valid locators are of this form:\n"
^ " - " ^ scheme ^ "://host/tz1...\n"
^ " - " ^ scheme ^ "://host:port/path/to/service/tz1..."
let parse uri =
(* extract `tz1..` from the last component of the path *)
assert (Uri.scheme uri = Some scheme) ;
let path = Uri.path uri in
begin match String.rindex_opt path '/' with
| None ->
failwith "Invalid locator %a" Uri.pp_hum uri
| Some i ->
let pkh = String.sub path (i + 1) (String.length path - i - 1) in
let path = String.sub path 0 i in
return (Uri.with_path uri path, pkh)
end >>=? fun (base, pkh) ->
Lwt.return (Signature.Public_key_hash.of_b58check pkh) >>=? fun pkh ->
return (base, pkh)
let public_key uri =
parse (uri : pk_uri :> Uri.t) >>=? fun (base, pkh) ->
RPC_client.call_service
~logger: P.logger
Media_type.all_media_types
~base Signer_services.public_key ((), pkh) () ()
let neuterize uri =
return (Client_keys.make_pk_uri (uri : sk_uri :> Uri.t))
let public_key_hash uri =
public_key uri >>=? fun pk ->
return (Signature.Public_key.hash pk)
let sign ?watermark uri msg =
parse (uri : sk_uri :> Uri.t) >>=? fun (base, pkh) ->
let msg =
match watermark with
| None -> msg
| Some watermark ->
MBytes.concat "" [ Signature.bytes_of_watermark watermark ; msg ] in
RPC_client.call_service
~logger: P.logger
Media_type.all_media_types
~base Signer_services.authorized_keys () () () >>=? fun authorized_keys ->
begin match authorized_keys with
| Some authorized_keys ->
P.authenticate
authorized_keys
(Signer_messages.Sign.Request.to_sign ~pkh ~data:msg) >>=? fun signature ->
return (Some signature)
| None -> return None
end >>=? fun signature ->
RPC_client.call_service
~logger: P.logger
Media_type.all_media_types
~base Signer_services.sign ((), pkh)
signature
msg
end
let make_base host port =
Uri.make ~scheme ~host ~port ()
end

View File

@ -0,0 +1,20 @@
(**************************************************************************)
(* *)
(* Copyright (c) 2014 - 2018. *)
(* Dynamic Ledger Solutions, Inc. <contact@tezos.com> *)
(* *)
(* All rights reserved. No warranty, explicit or implicit, provided. *)
(* *)
(**************************************************************************)
module Make(N : sig val scheme : string end) : sig
module Make(P : sig
val authenticate: Signature.Public_key_hash.t list -> MBytes.t -> Signature.t tzresult Lwt.t
val logger: RPC_client.logger
end)
: Client_keys.SIGNER
val make_base: string -> int -> Uri.t
end

View File

@ -7,77 +7,4 @@
(* *)
(**************************************************************************)
open Client_keys
let scheme = "https"
module Make(P : sig
val authenticate: Signature.Public_key_hash.t list -> MBytes.t -> Signature.t tzresult Lwt.t
end) = struct
let scheme = scheme
let title =
"Built-in tezos-signer using remote signer through hardcoded https requests."
let description =
"Valid locators are of this form:\n\
\ - https://host/tz1...\n\
\ - https://host:port/path/to/service/tz1...\n"
let parse uri =
(* extract `tz1..` from the last component of the path *)
assert (Uri.scheme uri = Some scheme) ;
let path = Uri.path uri in
begin match String.rindex_opt path '/' with
| None ->
failwith "Invalid locator %a" Uri.pp_hum uri
| Some i ->
let pkh = String.sub path (i + 1) (String.length path - i - 1) in
let path = String.sub path 0 i in
return (Uri.with_path uri path, pkh)
end >>=? fun (base, pkh) ->
Lwt.return (Signature.Public_key_hash.of_b58check pkh) >>=? fun pkh ->
return (base, pkh)
let public_key uri =
parse (uri : pk_uri :> Uri.t) >>=? fun (base, pkh) ->
RPC_client.call_service
Media_type.all_media_types
~base Signer_services.public_key ((), pkh) () ()
let neuterize uri =
return (Client_keys.make_pk_uri (uri : sk_uri :> Uri.t))
let public_key_hash uri =
public_key uri >>=? fun pk ->
return (Signature.Public_key.hash pk)
let sign ?watermark uri msg =
parse (uri : sk_uri :> Uri.t) >>=? fun (base, pkh) ->
let msg =
match watermark with
| None -> msg
| Some watermark ->
MBytes.concat "" [ Signature.bytes_of_watermark watermark ; msg ] in
RPC_client.call_service
Media_type.all_media_types
~base Signer_services.authorized_keys () () () >>=? fun authorized_keys ->
begin match authorized_keys with
| Some authorized_keys ->
P.authenticate
authorized_keys
(Signer_messages.Sign.Request.to_sign ~pkh ~data:msg) >>=? fun signature ->
return (Some signature)
| None -> return None
end >>=? fun signature ->
RPC_client.call_service
Media_type.all_media_types
~base Signer_services.sign ((), pkh)
signature
msg
end
let make_base host port =
Uri.make ~scheme ~host ~port ()
include Http_gen.Make(struct let scheme = "https" end)

View File

@ -9,6 +9,7 @@
module Make(P : sig
val authenticate: Signature.Public_key_hash.t list -> MBytes.t -> Signature.t tzresult Lwt.t
val logger: RPC_client.logger
end)
: Client_keys.SIGNER

View File

@ -14,6 +14,7 @@ let scheme = "remote"
module Make(S : sig
val default : Uri.t
val authenticate: Signature.Public_key_hash.t list -> MBytes.t -> Signature.t tzresult Lwt.t
val logger: RPC_client.logger
end) = struct
let scheme = scheme
@ -25,18 +26,21 @@ module Make(S : sig
"Valid locators are of this form: remote://tz1...\n\
The key will be queried to current remote signer, which can be \
configured with the `--remote-signer` or `-R` options, \
or by defining the following environment variables:\n \
- $TEZOS_SIGNER_UNIX_PATH,\n\
- $TEZOS_SIGNER_TCP_HOST and $TEZOS_SIGNER_TCP_PORT (default: 7732),\n\
- $TEZOS_SIGNER_HTTPS_HOST and $TEZOS_SIGNER_HTTPS_PORT (default: 443)."
or by defining the following environment variables:\n\
\ - $TEZOS_SIGNER_UNIX_PATH,\n\
\ - $TEZOS_SIGNER_TCP_HOST and $TEZOS_SIGNER_TCP_PORT (default: 7732),\n\
\ - $TEZOS_SIGNER_HTTP_HOST and $TEZOS_SIGNER_HTTP_PORT (default: 6732),\n\
\ - $TEZOS_SIGNER_HTTPS_HOST and $TEZOS_SIGNER_HTTPS_PORT (default: 443)."
module Socket = Socket.Make(S)
module Http = Http.Make(S)
module Https = Https.Make(S)
let get_remote () =
match Uri.scheme S.default with
| Some "unix" -> (module Socket.Unix : SIGNER)
| Some "tcp" -> (module Socket.Tcp : SIGNER)
| Some "http" -> (module Http : SIGNER)
| Some "https" -> (module Https : SIGNER)
| _ -> assert false
@ -51,7 +55,7 @@ module Make(S : sig
(fun uri ->
let key = Uri.path uri in
Uri.with_path S.default key)
| Some "https" ->
| Some ("https" | "http") ->
(fun uri ->
let key = Uri.path uri in
match Uri.path S.default with
@ -89,11 +93,12 @@ let make_pk pk =
let read_base_uri_from_env () =
match Sys.getenv_opt "TEZOS_SIGNER_UNIX_PATH",
Sys.getenv_opt "TEZOS_SIGNER_TCP_HOST",
Sys.getenv_opt "TEZOS_SIGNER_HTTP_HOST",
Sys.getenv_opt "TEZOS_SIGNER_HTTPS_HOST" with
| None, None, None -> return None
| Some path, None, None ->
| None, None, None, None -> return None
| Some path, None, None, None ->
return (Some (Socket.make_unix_base path))
| None, Some host, None -> begin
| None, Some host, None, None -> begin
try
let port =
match Sys.getenv_opt "TEZOS_SIGNER_TCP_PORT" with
@ -103,7 +108,17 @@ let read_base_uri_from_env () =
with Invalid_argument _ ->
failwith "Failed to parse TEZOS_SIGNER_TCP_PORT.@."
end
| None, None, Some host -> begin
| None, None, Some host, None -> begin
try
let port =
match Sys.getenv_opt "TEZOS_SIGNER_HTTP_PORT" with
| None -> 6732
| Some port -> int_of_string port in
return (Some (Http.make_base host port))
with Invalid_argument _ ->
failwith "Failed to parse TEZOS_SIGNER_HTTP_PORT.@."
end
| None, None, None, Some host -> begin
try
let port =
match Sys.getenv_opt "TEZOS_SIGNER_HTTPS_PORT" with
@ -113,11 +128,12 @@ let read_base_uri_from_env () =
with Invalid_argument _ ->
failwith "Failed to parse TEZOS_SIGNER_HTTPS_PORT.@."
end
| _, _, _ ->
| _, _, _, _ ->
failwith
"Only one the following environment variable must be defined: \
TEZOS_SIGNER_UNIX_PATH, \
TEZOS_SIGNER_TCP_HOST, \
TEZOS_SIGNER_HTTP_HOST, \
TEZOS_SIGNER_HTTPS_HOST@."
type error += Invalid_remote_signer of string
@ -130,7 +146,13 @@ let () =
~description: "The provided remote signer is invalid."
~pp:
(fun ppf s ->
Format.fprintf ppf "Value '%s' is not a valid URI for a remote signer" s)
Format.fprintf ppf
"@[<v 0>Value '%s' is not a valid URI for a remote signer.@,\
Supported URIs for remote signers are of the form:@,\
\ - unix:///path/to/socket/file@,\
\ - tcp://host:port@,\
\ - http://host[:port][/prefix]@,\
\ - https://host[:port][/prefix]@]" s)
Data_encoding.(obj1 (req "uri" string))
(function Invalid_remote_signer s -> Some s | _ -> None)
(fun s -> Invalid_remote_signer s)
@ -140,6 +162,7 @@ let parse_base_uri s =
try
let uri = Uri.of_string s in
match Uri.scheme uri with
| Some "http" -> return uri
| Some "https" -> return uri
| Some "tcp" -> return uri
| Some "unix" -> return uri

View File

@ -10,6 +10,7 @@
module Make(S : sig
val default : Uri.t
val authenticate: Signature.Public_key_hash.t list -> MBytes.t -> Signature.t tzresult Lwt.t
val logger: RPC_client.logger
end) : Client_keys.SIGNER
val make_pk: Signature.public_key -> Client_keys.pk_uri

View File

@ -23,14 +23,14 @@ let sign =
~query
~input: Data_encoding.bytes
~output: Data_encoding.(obj1 (req "signature" Signature.encoding))
RPC_path.(root /: Signature.Public_key_hash.rpc_arg)
RPC_path.(root / "keys" /: Signature.Public_key_hash.rpc_arg)
let public_key =
RPC_service.get_service
~description: "Retrieve the public key of a given remote key"
~query: RPC_query.empty
~output: Data_encoding.(obj1 (req "public_key" Signature.Public_key.encoding))
RPC_path.(root /: Signature.Public_key_hash.rpc_arg)
RPC_path.(root / "keys" /: Signature.Public_key_hash.rpc_arg)
let authorized_keys =
RPC_service.get_service