123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497# 1 "src/lib_signer_backends/unix/ledger.available.ml"(*****************************************************************************)(* *)(* Open Source License *)(* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. <contact@tezos.com> *)(* *)(* Permission is hereby granted, free of charge, to any person obtaining a *)(* copy of this software and associated documentation files (the "Software"),*)(* to deal in the Software without restriction, including without limitation *)(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *)(* and/or sell copies of the Software, and to permit persons to whom the *)(* Software is furnished to do so, subject to the following conditions: *)(* *)(* The above copyright notice and this permission notice shall be included *)(* in all copies or substantial portions of the Software. *)(* *)(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *)(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *)(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *)(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *)(* DEALINGS IN THE SOFTWARE. *)(* *)(*****************************************************************************)moduleEvents=Signer_eventsopenClient_keysmoduleBip32_path=structlethard=Int32.logor0x8000_0000lletunhard=Int32.logand0x7fff_fffflletis_hardn=Int32.logand0x8000_0000ln<>0llettezos_root=[hard44l;hard1729l]letpp_nodeppfnode=matchis_hardnodewith|true->Fmt.pfppf"%ldh"(unhardnode)|false->Fmt.pfppf"%ld"nodeletpp_path=Fmt.(list~sep:(constchar'/')pp_node)letstring_of_path=Fmt.to_to_stringpp_pathendtypeerror+=|LedgerErrorofLedgerwallet.Transport.error|Ledger_signing_hash_mismatchofstring*string|Ledger_msg_chunk_too_longofstringleterror_encoding=letopenData_encodinginconv(fune->Format.asprintf"%a"Ledgerwallet.Transport.pp_errore)(fun_->invalid_arg"Ledger error is not deserializable")(obj1(req"ledger-error"string))let()=register_error_kind`Permanent~id:"signer.ledger"~title:"Ledger error"~description:"Error communicating with a Ledger Nano device"~pp:(funppfe->Format.fprintfppf"@[Ledger %a@]"Ledgerwallet.Transport.pp_errore)error_encoding(functionLedgerErrore->Somee|_->None)(fune->LedgerErrore)let()=register_error_kind`Permanent~id:"signer.ledger.msg-chunk-too-big"~title:"Too long message chunk sent to a ledger"~description:"Too long message chunk sent to a ledger."~pp:(funppfmsg->Format.fprintfppf"Chunk of ledger message %s too long, the ledger app may not be up to \
date."msg)Data_encoding.(obj1(req"msg"string))(functionLedger_msg_chunk_too_longmsg->Somemsg|_->None)(funmsg->Ledger_msg_chunk_too_longmsg)let()=letdescriptionledger_hashcomputed_hash=letparenfmthash_opt=matchBase.Option.bind~f:Tezos_crypto.Blake2B.of_string_opthash_optwith|None->()|Somehash->Format.fprintffmt" (%a)"Tezos_crypto.Blake2B.pp_shorthashinFormat.asprintf"The ledger returned a hash%a which doesn't match the independently \
computed hash%a."parenledger_hashparencomputed_hashinregister_error_kind`Permanent~id:"signer.ledger.signing-hash-mismatch"~title:"Ledger signing-hash mismatch"~description:(descriptionNoneNone)~pp:(funppf(lh,ch)->Format.pp_print_stringppf(description(Somelh)(Somech)))Data_encoding.(obj2(req"ledger-hash"string)(req"computed-hash"string))(function|Ledger_signing_hash_mismatch(lh,ch)->Some(lh,ch)|_->None)(fun(lh,ch)->Ledger_signing_hash_mismatch(lh,ch))letpp_round_optfmt=function|None->()|Somex->Format.fprintffmt" (round: %ld)"x(** Wrappers around Ledger APDUs. *)moduleLedger_commands=structletwrap_ledger_cmdf=letopenLwt_result_syntaxinletbuf=Buffer.create100inletpp=Format.make_formatter(funsofslgth->Buffer.add_substringbufsofslgth)(fun()->Events.(emit__dont_wait__use_with_careLedger.communication)(Buffer.contentsbuf);Buffer.clearbuf)inletres=fppinmatchreswith|Error(Ledgerwallet.Transport.AppError{status=Ledgerwallet.Status.Incorrect_length_for_ins;msg})->tzfail(Ledger_msg_chunk_too_longmsg)|Errorerr->tzfail(LedgerErrorerr)|Okv->returnvletget_version~device_nameh=letopenLwt_result_syntaxinletbuf=Buffer.create100inletpp=Format.formatter_of_bufferbufinletversion=Ledgerwallet_tezos.get_version~pphinlet*!()=Events.(emitLedger.communication)(Buffer.contentsbuf)inmatchversionwith|Errore->let*!()=Events.(emitLedger.not_tezos)(device_name,Format.asprintf"@[Ledger %a@]"Ledgerwallet.Transport.pp_errore)inreturn_none|Okversion->let*()=if(version.major,version.minor)<(1,4)thenfailwith"Version %a of the ledger apps is not supported by this client"Ledgerwallet_tezos.Version.ppversionelsereturn_unitinlet*git_commit=wrap_ledger_cmd(funpp->Ledgerwallet_tezos.get_git_commit~pph)inlet*!()=Events.(emitLedger.found_application)(Format.asprintf"%a"Ledgerwallet_tezos.Version.ppversion,device_name,git_commit)inletcleaned_up=(* The ledger sends a NUL-terminated C-String: *)ifgit_commit.[String.lengthgit_commit-1]='\x00'thenString.subgit_commit0(String.lengthgit_commit-1)elsegit_commitinreturn_some(version,cleaned_up)letsecp256k1_ctx=Libsecp256k1.External.Context.create~sign:false~verify:false()letpublic_key_returning_instructionwhich?(prompt=false)hidapicurvepath=letopenLwt_result_syntaxinletpath=Bip32_path.tezos_root@pathinlet+pk=matchwhichwith|`Get_public_key->wrap_ledger_cmd(funpp->Ledgerwallet_tezos.get_public_key~prompt~pphidapicurvepath)|`Authorize_baking->wrap_ledger_cmd(funpp->Ledgerwallet_tezos.authorize_baking~pphidapicurvepath)|`Setup(main_chain_id,main_hwm,test_hwm)->wrap_ledger_cmd(funpp->Ledgerwallet_tezos.setup_baking~pphidapicurvepath~main_chain_id~main_hwm~test_hwm)inmatchcurvewith|Ed25519|Bip32_ed25519->letpk=Cstruct.to_bytespkinTzEndian.set_int8pk00;(* hackish, but works. *)Data_encoding.Binary.of_bytes_exnSignature.Public_key.encodingpk|Secp256k1->letopenLibsecp256k1.Externalinletbuf=Bigstring.create(Key.compressed_pk_bytes+1)inletpk=Key.read_pk_exnsecp256k1_ctx(Cstruct.to_bigarraypk)inEndianBigstring.BigEndian.set_int8buf01;let_nb_written=Key.writesecp256k1_ctx~pos:1bufpkinData_encoding.Binary.of_bytes_exnSignature.Public_key.encoding(Bigstring.to_bytesbuf)|Secp256r1->(letopenTezos_crypto.Hacl.P256inletbuf=Bytes.create(pk_size+1)inmatchpk_of_bytes(Cstruct.to_bytespk)with|None->Stdlib.failwith"Impossible to read P256 public key from Ledger"|Somepk->TzEndian.set_int8buf02;blit_to_bytespk~pos:1buf;Data_encoding.Binary.of_bytes_exnSignature.Public_key.encodingbuf)letget_public_key=public_key_returning_instruction`Get_public_keyletpkh_of_pk=Signature.Public_key.hashletpublic_key?(first_import:Client_context.io_walletoption)hidcurvepath=letopenLwt_result_syntaxinmatchfirst_importwith|Somecctxt->let*pk=get_public_key~prompt:falsehidcurvepathinletpkh=pkh_of_pkpkinlet*!()=cctxt#message"Please validate@ (and write down)@ the public key hash@ \
displayed@ on the Ledger,@ it should be equal@ to `%a`:"Signature.Public_key_hash.pppkhinget_public_key~prompt:truehidcurvepath|None->get_public_key~prompt:falsehidcurvepathletpublic_key_hash?first_importhidcurvepath=letopenLwt_result_syntaxinlet*pk=public_key?first_importhidcurvepathinreturn(pkh_of_pkpk,pk)letget_authorized_pathhidversion=letopenLwt_result_syntaxinletopenLedgerwallet_tezos.Versioninifversion.major<2thenlet+path=wrap_ledger_cmd(funpp->Ledgerwallet_tezos.get_authorized_key~pphid)in`Legacy_pathpathelselet*!r=wrap_ledger_cmd(funpp->Ledgerwallet_tezos.get_authorized_path_and_curve~pphid)inmatchrwith|Error(LedgerError(AppError{status=Ledgerwallet.Status.Referenced_data_not_found;_})::_)->return`No_baking_authorized|Error_ase->Lwt.returne|Ok(path,curve)->return(`Path_curve(path,curve))letsign?watermark~versionhidcurvepath(base_msg:Bytes.t)=letopenLwt_result_syntaxinletmsg=Option.foldwatermark~none:base_msg~some:(funwatermark->Bytes.cat(Signature.bytes_of_watermarkwatermark)base_msg)inletpath=Bip32_path.tezos_root@pathinlet*hash_opt,signature=wrap_ledger_cmd(funpp->let{Ledgerwallet_tezos.Version.major;minor;patch;_}=versioninletopenResult_syntaxinif(major,minor,patch)<=(2,0,0)thenlet*s=Ledgerwallet_tezos.sign~pphidcurvepath(Cstruct.of_bytesmsg)inOk(None,s)elselet*h,s=Ledgerwallet_tezos.sign_and_hash~pphidcurvepath(Cstruct.of_bytesmsg)inOk(Someh,s))inlet*()=matchhash_optwith|None->return_unit|Somehsh->lethash_msg=Tezos_crypto.Blake2B.hash_bytes[msg]inletledger_one=Tezos_crypto.Blake2B.of_bytes_exn(Cstruct.to_byteshsh)inifTezos_crypto.Blake2B.equalhash_msgledger_onethenreturn_unitelsetzfail(Ledger_signing_hash_mismatch(Tezos_crypto.Blake2B.to_stringledger_one,Tezos_crypto.Blake2B.to_stringhash_msg))inmatchcurvewith|Ed25519|Bip32_ed25519->letsignature=Signature.Ed25519.of_bytes_exn(Cstruct.to_bytessignature)inreturn(Signature.of_ed25519signature)|Secp256k1->(* Remove parity info *)Cstruct.(set_uint8signature0(get_uint8signature0land0xfe));letsignature=Cstruct.to_bigarraysignatureinletopenLibsecp256k1.Externalinletsignature=Sign.read_der_exnsecp256k1_ctxsignatureinletbytes=Sign.to_bytessecp256k1_ctxsignatureinletsignature=Signature.Secp256k1.of_bytes_exn(Bigstring.to_bytesbytes)inreturn(Signature.of_secp256k1signature)|Secp256r1->(* Remove parity info *)Cstruct.(set_uint8signature0(get_uint8signature0land0xfe));letsignature=Cstruct.to_bigarraysignatureinletopenLibsecp256k1.Externalin(* We use secp256r1 library to extract P256 DER signature. *)letsignature=Sign.read_der_exnsecp256k1_ctxsignatureinletbuf=Sign.to_bytessecp256k1_ctxsignatureinletsignature=Signature.P256.of_bytes_exn(Bigstring.to_bytesbuf)inreturn(Signature.of_p256signature)letget_deterministic_noncehidcurvepathmsg=letopenLwt_result_syntaxinletpath=Bip32_path.tezos_root@pathinlet*nonce=wrap_ledger_cmd(funpp->Ledgerwallet_tezos.get_deterministic_nonce~pphidcurvepath(Cstruct.of_bytesmsg))inreturn(Bigstring.of_bytes(Cstruct.to_bytesnonce))end(** Identification of a ledger's root key through crouching-tigers
(not the keys used for an account). *)moduleLedger_id=struct(**
The “ID” of the ledger is the animals (or pkh) corresponding to
["/ed25519/"] (first curve, no path).
*)typet=AnimalsofLedger_names.t|PkhofSignature.public_key_hashletanimals_of_pkhpkh=pkh|>Signature.Public_key_hash.to_string|>Ledger_names.crouching_tigerletcurve=Ledgerwallet_tezos.Ed25519letgethidapi=letopenLwt_result_syntaxinlet*pk=Ledger_commands.get_public_keyhidapicurve[]inletpkh=Signature.Public_key.hashpkinletanimals=animals_of_pkhpkhinreturn(Animalsanimals)letppppf=function|Animalsa->Ledger_names.ppppfa|Pkhpkh->Signature.Public_key_hash.ppppfpkhletto_animals=functionAnimalsa->a|Pkhpkh->animals_of_pkhpkhletequalab=to_animalsa=to_animalsblethash=Hashtbl.hashend(** An account is a given key-pair corresponding to a
[ledger + curve + derivation-path]. *)moduleLedger_account=structtypet={ledger:Ledger_id.t;curve:Ledgerwallet_tezos.curve;path:int32list;}end(** {!Ledger_uri.t} represents a parsed ["ledger://..."] URI which may
refer to a {!Ledger_id.t} or a full blown {!Ledger_account.t}. *)moduleLedger_uri=structtypet=[`LedgerofLedger_id.t|`Ledger_accountofLedger_account.t]letint32_of_path_element_exn~allow_weakx=letfailfppf=Printf.ksprintfStdlib.failwithppfinletlen=String.lengthxiniflen=0thenfailf"Empty path element"elsematchx.[len-1]with|'\''|'h'->(letintpart=String.subx0(len-1)inmatchInt32.of_string_optintpartwith|Somei->Bip32_path.hardi|None->failf"Path is not an integer: %S"intpart)|_whenallow_weak->(matchInt32.of_string_optxwith|Somei->i|None->failf"Path is not a non-hardened integer: %S"x)|_->failf"Non-hardened paths are not allowed for this derivation scheme (%S)"xletparse_animalsanimals=matchString.split_no_empty'-'animalswith|[c;t;h;d]->Some{Ledger_names.c;t;h;d}|_->Noneletderivation_supports_weak_paths=function|Ledgerwallet_tezos.Ed25519->false|Ledgerwallet_tezos.Secp256k1->true|Ledgerwallet_tezos.Secp256r1->true|Ledgerwallet_tezos.Bip32_ed25519->trueletparse?allow_weakuri:ttzresultLwt.t=letopenLwt_result_syntaxinlethost=Uri.hosturiinlet*ledger=matchOption.bindhostSignature.Public_key_hash.of_b58check_optwith|Somepkh->return(Ledger_id.Pkhpkh)|None->(matchOption.bindhostparse_animalswith|Someanimals->return(Ledger_id.Animalsanimals)|None->failwith"Cannot parse host of URI: %s"(Uri.to_stringuri))inletcomponents=String.split_no_empty'/'(Uri.pathuri)inmatchcomponentswith|s::tl->letcurve,more_path=matchLedgerwallet_tezos.curve_of_stringswith|Somecurve->(curve,tl)|None->(Ledger_id.curve,s::tl)inletactually_allow_weak=matchallow_weakwith|None->derivation_supports_weak_pathscurve|Somex->xinlet*bip32=tryreturn(List.map(int32_of_path_element_exn~allow_weak:actually_allow_weak)more_path)withFailures->failwith"Failed to parse Curve/BIP32 path from %s (%s): %s"(Uri.pathuri)(Uri.to_stringuri)sinreturn(`Ledger_accountLedger_account.{ledger;curve;path=bip32})|[]->return(`Ledgerledger)letledger_uri_or_alias_paramnext=letopenLwt_result_syntaxinletname="account-alias-or-ledger-uri"inletdesc="An imported ledger alias or a ledger URI (e.g. \
\"ledger://animal/curve/path\")."inletopenTezos_clicinparam~name~desc(parameter(funcctxtstr->let*uri=let*o=Public_key.find_optcctxtstrinmatchowith|Some((x:pk_uri),_)->return(x:>Uri.t)|None->(tryreturn(Uri.of_stringstr)withe->failwith"Error while parsing URI: %s"(Printexc.to_stringe))inparseuri))nextletpp:_->t->unit=funppf->Format.(function|`Ledgerlid->fprintfppf"ledger://%a"Ledger_id.pplid|`Ledger_account{Ledger_account.ledger;curve;path}->fprintfppf"ledger://%a/%a/%a"Ledger_id.ppledgerLedgerwallet_tezos.pp_curvecurveBip32_path.pp_pathpath)letif_matches(meta_uri:t)ledger_idcont=letopenLwt_result_syntaxinmatchmeta_uriwith|`Ledgerl->ifLedger_id.equallledger_idthencont()elsereturn_none|`Ledger_account{Ledger_account.ledger;_}->ifLedger_id.equalledgerledger_idthencont()elsereturn_noneletfull_account(ledger_uri:t)=letopenLwt_result_syntaxinmatchledger_uriwith|`Ledger_accountacc->returnacc|`Ledgerledger_id->failwith"Insufficient information: you need to provide a curve & BIP32 path \
(%a)."Ledger_id.ppledger_idend(** Filters allow early dismissal of HID devices/ledgers which
searching for a ledger. *)moduleFilter=structtypeversion_filter=Ledgerwallet_tezos.Version.t*string->booltypet=[`None|`Hid_pathofstring|`Versionofstring*version_filter]letversion_matches(t:t)version_commit=matchtwith`Version(_,f)->fversion_commit|_->trueletis_app:_->_->t=funmsgapp->`Version(msg,fun({Ledgerwallet_tezos.Version.app_class;_},_)->app=app_class)letis_baking=is_app"App = Baking"Ledgerwallet_tezos.Version.TezBakeletppppf(f:t)=letopenFormatinmatchfwith|`None->fprintfppf"None"|`Hid_paths->fprintfppf"HID-path: %s"s|`Version(s,_)->fprintfppf"%s"sendmoduleRequest_cache=Aches.Vache.Map(Aches.Vache.LRU_Precise)(Aches.Vache.Strong)(Ledger_id)typerequest_info={version:Ledgerwallet_tezos.Version.t;git_commit:string;ledger_id:Ledger_id.t;path:Ledgerwallet.Transport.path;}letcache=Request_cache.create7letpath_name(path:Ledgerwallet.Transport.path)=matchpathwith|Hidapi_pathdevice_info->device_info.path|Proxy_path_->"proxy"type'aledger_function=Ledgerwallet.Transport.t->Ledgerwallet_tezos.Version.t*string->device_path:Ledgerwallet.Transport.path->Ledger_id.t->('aoption,tztrace)resultLwt.tletuse_ledger?(filter:Filter.t=`None)(f:'aledger_function)=letopenLwt_result_syntaxinletledgers=Ledgerwallet.Transport.enumerate()inlet*!()=Events.(emitLedger.found)(List.lengthledgers,String.concat" -- "(List.map(fun(path:Ledgerwallet.Transport.path)->matchpathwith|Proxy_path_->"Proxy"|Hidapi_pathl->Printf.sprintf"(%04x, %04x)"l.vendor_idl.product_id)ledgers))inletselected(path:Ledgerwallet.Transport.path)=letid_ok=matchpathwith|Proxy_path_->true|Hidapi_pathdevice_info->(* HID interfaces get the number 0
(cf. https://github.com/LedgerHQ/ledger-nano-s/issues/48)
*BUT* on MacOSX the Hidapi library does not report the interface-number
so we look at the usage-page (which is even more unspecified but used by
prominent Ledger users:
https://github.com/LedgerHQ/ledgerjs/commit/333ade0d55dc9c59bcc4b451cf7c976e78629681).
*)device_info.Hidapi.interface_number=0||device_info.Hidapi.interface_number=-1&&device_info.Hidapi.usage_page=0xffa0inid_ok&&matchfilterwith|`None->true|`Version_->true|`Hid_pathhp->(matchpathwith|Proxy_path_->false|Hidapi_pathdevice_info->device_info.path=hp)inletprocess_device(path:Ledgerwallet.Transport.path)=letdevice_name=path_namepathinifnot(selectedpath)thenreturn_noneelselet*!()=Events.(emitLedger.processing)device_nameinmatchLedgerwallet.Transport.open_pathpathwith|None->return_none|Someh->Lwt.finalize(fun()->let*o=Ledger_commands.get_version~device_namehinmatchowith|Someversion_gitwhenFilter.version_matchesfilterversion_git->let*ledger_id=Ledger_id.gethinfhversion_git~device_path:pathledger_id|None|Some_->return_none)(fun()->Ledgerwallet.Transport.closeh;Lwt.return_unit)inletrecgo=function|[]->return_none|h::t->(let*o=process_devicehinmatchowithSomex->return_somex|None->got)ingoledgersletmin_version_of_derivation_scheme=function|Ledgerwallet_tezos.Ed25519->(1,3,0)|Ledgerwallet_tezos.Secp256k1->(1,3,0)|Ledgerwallet_tezos.Secp256r1->(1,3,0)|Ledgerwallet_tezos.Bip32_ed25519->(2,1,0)letis_derivation_scheme_supportedversioncurve=Ledgerwallet_tezos.Version.(let{major;minor;patch;_}=versionin(major,minor,patch)>=min_version_of_derivation_schemecurve)letuse_ledger_or_fail~ledger_uri?(filter=`None)?msg(f:'aledger_function)=letopenLwt_result_syntaxinletfhidapi(version,git_commit)~device_pathledger_id=letgo()=fhidapi(version,git_commit)~device_pathledger_idinmatchledger_uriwith|`Ledger_account{Ledger_account.curve;_}->ifis_derivation_scheme_supportedversioncurvethengo()elseLedgerwallet_tezos.(failwith"To use derivation scheme %a you need %a or later but you're \
using %a."pp_curvecurveVersion.pp(leta,b,c=min_version_of_derivation_schemecurvein{versionwithmajor=a;minor=b;patch=c})Version.ppversion)|_->go()inlet*o=letcheck_filter(transport_path,version,git_commit)=match(filter:Filter.t)with|`None->true|`Hid_pathpath->(match(transport_path:Ledgerwallet.Transport.path)with|Hidapi_pathdevice_info->device_info.path=path|Proxy_path_->false)|`Version(_,f)->f(version,git_commit)inletledger_id=matchledger_uriwith|`Ledgerid->id|`Ledger_account{ledger;_}->ledgerinmatchRequest_cache.find_optcacheledger_idwith|Some{path;version;git_commit;ledger_id}whencheck_filter(path,version,git_commit)->(matchLedgerwallet.Transport.(open_pathpath)with|None->return_none|Somehidapi->Lwt.finalize(fun()->fhidapi(version,git_commit)~device_path:pathledger_id)(fun()->Ledgerwallet.Transport.closehidapi;Lwt.return_unit))|Some_|None->use_ledger~filter(funhidapi(version,git_commit)~device_pathledger_id->Ledger_uri.if_matchesledger_uriledger_id(fun()->Request_cache.replacecacheledger_id{path=device_path;version;git_commit;ledger_id};fhidapi(version,git_commit)~device_pathledger_id))inmatchowith|Someo->returno|None->failwith"%aFound no ledger corresponding to %a%t."(Format.pp_print_option(funfmts->Format.fprintffmt"%s: "s))msgLedger_uri.ppledger_uri(funppf->Format.fprintfppf" with filter \"%a\""Filter.ppfilter)(** A global {!Hashtbl.t} which allows us to avoid calling
{!Signer_implementation.get_public_key} too often. *)moduleGlobal_cache:sigvalrecord:pk_uri->pk:Signature.public_key->pkh:Signature.public_key_hash->unitvalget:pk_uri->(Signature.public_key_hash*Signature.public_key)optionend=structletcache:(Signature.Public_key_hash.t*Signature.Public_key.t)Client_keys.Pk_uri_hashtbl.t=Client_keys.Pk_uri_hashtbl.create13letrecordpk_uri~pk~pkh=Client_keys.Pk_uri_hashtbl.replacecachepk_uri(pkh,pk)letgetpk_uri=Client_keys.Pk_uri_hashtbl.findcachepk_uriend(** The implementation of the “signer-plugin.” *)moduleSigner_implementation:Client_keys.SIGNER=structletscheme="ledger"lettitle="Built-in signer using a Ledger Nano device."letdescription=Printf.sprintf"Valid URIs are of the form\n\
\ - ledger://<animals>/<curve>[/<path>]\n\
where:\n\
\ - <animals> is the identifier of the ledger of the form \
'crouching-tiger-hidden-dragon' and can be obtained with the command \
`octez-client list connected ledgers` (which also provides full \
examples).\n\
- <curve> is the signing curve, e.g. `ed1551`\n\
- <path> is a BIP32 path anchored at m/%s. The ledger does not yet \
support non-hardened paths, so each node of the path must be hardened."Bip32_path.(string_of_pathtezos_root)includeClient_keys.Signature_typeletneuterize(sk:sk_uri)=letopenLwt_result_syntaxinlet*?v=make_pk_uri(sk:>Uri.t)inreturnvletpkh_of_pk=Signature.Public_key.hashletpublic_key_maybe_prompt?(first_import:Client_context.io_walletoption)(pk_uri:pk_uri)=letopenLwt_result_syntaxinmatchGlobal_cache.getpk_uriwith|Some(_,pk)->returnpk|None->(let*!r=let*ledger_uri=Ledger_uri.parse(pk_uri:>Uri.t)inlet*{curve;path;_}=Ledger_uri.full_accountledger_uriinuse_ledger_or_fail~ledger_uri(funhidapi(_version,_git_commit)~device_path:__ledger_id->let*pk=Ledger_commands.public_key?first_importhidapicurvepathinletpkh=pkh_of_pkpkinGlobal_cache.recordpk_uri~pkh~pk;return_somepk)inmatchrwith|Errorerr->failwith"%a"pp_print_traceerr|Okv->returnv)letpublic_key_hash_maybe_prompt?first_importpk_uri=letopenLwt_result_syntaxinmatchGlobal_cache.getpk_uriwith|Some(pkh,pk)->return(pkh,Somepk)|None->let*pk=public_key_maybe_prompt?first_importpk_uriinreturn(pkh_of_pkpk,Somepk)letpublic_key=public_key_maybe_prompt?first_import:Noneletpublic_key_hash=public_key_hash_maybe_prompt?first_import:Noneletimport_secret_key~iopk_uri=public_key_hash_maybe_prompt~first_import:iopk_uriletsign?watermark(sk_uri:sk_uri)msg=letopenLwt_result_syntaxinlet*ledger_uri=Ledger_uri.parse(sk_uri:>Uri.t)inlet*{curve;path;_}=Ledger_uri.full_accountledger_uriinuse_ledger_or_fail~ledger_uri(funhidapi(version,_git_commit)~device_path:__ledger_id->let*bytes=Ledger_commands.sign?watermark~versionhidapicurvepathmsginreturn_somebytes)letdeterministic_nonce(sk_uri:sk_uri)msg=letopenLwt_result_syntaxinlet*ledger_uri=Ledger_uri.parse(sk_uri:>Uri.t)inlet*{curve;path;_}=Ledger_uri.full_accountledger_uriinuse_ledger_or_fail~ledger_uri(funhidapi(_version,_git_commit)~device_path:__ledger_id->let*bytes=Ledger_commands.get_deterministic_noncehidapicurvepathmsginreturn_some(Bigstring.to_bytesbytes))letdeterministic_nonce_hash(sk:sk_uri)msg=letopenLwt_result_syntaxinlet*nonce=deterministic_nonceskmsginreturn(Tezos_crypto.Blake2B.to_bytes(Tezos_crypto.Blake2B.hash_bytes[nonce]))letsupports_deterministic_nonces_=Lwt_result_syntax.return_trueend(* The Ledger uses a special value 0x00000000 for the “any” chain-id: *)letpp_ledger_chain_idfmts=matchswith|"\x00\x00\x00\x00"->Format.fprintffmt"'Unspecified'"|other->Format.fprintffmt"%a"Chain_id.pp(Chain_id.of_string_exnother)(** Commands for both ledger applications. *)letgeneric_commandsgroup=letopenLwt_result_syntaxinTezos_clic.[command~group~desc:"List supported Ledger Nano devices connected."no_options(fixed["list";"connected";"ledgers"])(fun()(cctxt:Client_context.full)->let*_=use_ledger(fun_hidapi(version,git_commit)~device_pathledger_id->letopenHidapiinlet*!()=cctxt#message"%t"Format.(funppf->letintro=matchdevice_pathwith|Proxy_path_->asprintf"Found a %a (git-description: %S) application \
running on proxy server."Ledgerwallet_tezos.Version.ppversiongit_commit|Hidapi_pathdevice_info->asprintf"Found a %a (git-description: %S) application \
running on %s %s at [%s]."Ledgerwallet_tezos.Version.ppversiongit_commit(device_info.manufacturer_string|>Option.value~default:"NO-MANUFACTURER")(device_info.product_string|>Option.value~default:"NO-PRODUCT")device_info.pathinpp_open_vboxppf0;fprintfppf"## Ledger `%a`@,"Ledger_id.ppledger_id;pp_open_hovboxppf0;pp_print_textppfintro;pp_close_boxppf();pp_print_cutppf();pp_print_cutppf();pp_open_hovboxppf0;pp_print_textppf"To use keys at BIP32 path m/44'/1729'/0'/0' \
(default Tezos key path), use one of:";pp_close_boxppf();pp_print_cutppf();List.iter(funcurve->fprintfppf" octez-client import secret key ledger_%s \
\"ledger://%a/%a/0h/0h\""(Sys.getenv_opt"USER"|>Option.value~default:"user")Ledger_id.ppledger_idLedgerwallet_tezos.pp_curvecurve;pp_print_cutppf())(List.filter(is_derivation_scheme_supportedversion)[Ed25519;Secp256k1;Secp256r1;Bip32_ed25519]);pp_close_boxppf();pp_print_newlineppf())inreturn_none)inreturn_unit);Tezos_clic.command~group~desc:"Display version/public-key/address information for a Ledger URI"(args1(switch~doc:"Test signing operation"~long:"test-sign"()))(prefixes["show";"ledger"]@@Ledger_uri.ledger_uri_or_alias_param@@stop)(funtest_signledger_uri(cctxt:Client_context.full)->use_ledger_or_fail~ledger_uri(funhidapi(version,git_commit)~device_path_ledger_id->let*!()=cctxt#message"Found ledger corresponding to %a:"Ledger_uri.ppledger_uriinlet*!()=matchdevice_pathwith|Proxy_path_->Lwt.return_unit|Hidapi_pathdevice_info->let*!()=cctxt#message"* Manufacturer: %s"(Option.valuedevice_info.manufacturer_string~default:"NONE")inlet*!()=cctxt#message"* Product: %s"(Option.valuedevice_info.product_string~default:"NONE")inlet*!()=cctxt#message"* Application: %a (git-description: %S)"Ledgerwallet_tezos.Version.ppversiongit_commitinLwt.return_unitinlet*()=matchledger_uriwith|`Ledger_account{curve;path;_}->(let*!()=cctxt#message"* Curve: `%a`"Ledgerwallet_tezos.pp_curvecurveinletfull_path=Bip32_path.tezos_root@pathinlet*!()=cctxt#message"* Path: `%s` [%s]"(Bip32_path.string_of_pathfull_path)(String.concat"; "(List.map(Printf.sprintf"0x%lX")full_path))inlet*pkh,pk=Ledger_commands.public_key_hashhidapicurvepathinlet*!()=cctxt#message"* Public Key: %a"Signature.Public_key.pppkinlet*!()=cctxt#message"* Public Key Hash: %a@\n"Signature.Public_key_hash.pppkhinmatch(test_sign,version.app_class)with|true,Tezos->(letpkh_bytes=Signature.Public_key_hash.to_bytespkhin(* Signing requires validation on the device. *)let*!()=cctxt#message"@[Attempting a signature@ (of `%a`),@ please@ \
validate on@ the ledger.@]"Hex.pp(Hex.of_bytespkh_bytes)inlet*signature=Ledger_commands.sign~version~watermark:Generic_operationhidapicurvepathpkh_bytesinmatchSignature.check~watermark:Generic_operationpksignaturepkh_byteswith|false->failwith"Fatal: Ledger cannot sign with %a"Signature.Public_key_hash.pppkh|true->let*!()=cctxt#message"Tezos Wallet successfully signed:@ %a."Signature.ppsignatureinreturn_unit)|true,TezBake->failwith"Option --test-sign only works for the Tezos Wallet \
app."|false,_->return_unit)|`Ledger_whentest_sign->failwith"Option --test-sign only works with a full ledger \
URI/account (with curve/path)."|`Ledger_->let*!()=cctxt#message"* This is just a ledger URI."inreturn_unitinreturn_some()));](** Commands specific to the Baking app minus the high-water-mark ones
which get a specific treatment in {!high_water_mark_commands}. *)letbaking_commandsgroup=letopenLwt_result_syntaxinTezos_clic.[Tezos_clic.command~group~desc:"Query the path of the authorized key"no_options(prefixes["get";"ledger";"authorized";"path";"for"]@@Ledger_uri.ledger_uri_or_alias_param@@stop)(fun()ledger_uri(cctxt:Client_context.full)->use_ledger_or_fail~ledger_uri~filter:Filter.is_baking(funhidapi(version,_git_commit)~device_path:__ledger_id->let*authorized=Ledger_commands.get_authorized_pathhidapiversioninmatchauthorizedwith|`Legacy_pathp->let*!()=cctxt#message"@[<v 0>Authorized baking path (Legacy < 2.x.y): %a@]"Bip32_path.pp_pathpinreturn_some()|`No_baking_authorized->let*!()=cctxt#message"No baking key authorized at all."inreturn_some()|`Path_curve(ledger_path,ledger_curve)->(let*!()=cctxt#message"@[<v 0>Authorized baking path: %a@]"Bip32_path.pp_pathledger_pathinlet*!()=cctxt#message"@[<v 0>Authorized baking curve: %a@]"Ledgerwallet_tezos.pp_curveledger_curveinmatchledger_uriwith|`Ledger_->return_some()|`Ledger_account{curve;path;_}whencurve=ledger_curve&&Bip32_path.tezos_root@path=ledger_path->let*!()=cctxt#message"@[<v 0>Authorized baking URI: %a@]"Ledger_uri.ppledger_uriinreturn_some()|`Ledger_account{curve;path;_}->failwith"Path and curve do not match the ones specified in the \
command line: %a & %a"Ledgerwallet_tezos.pp_curvecurveBip32_path.pp_path(Bip32_path.tezos_root@path))));Tezos_clic.command~group~desc:"Authorize a Ledger to bake for a key (deprecated, use `setup ledger \
...` with recent versions of the Baking app)"no_options(prefixes["authorize";"ledger";"to";"bake";"for"]@@Ledger_uri.ledger_uri_or_alias_param@@stop)(fun()ledger_uri(cctxt:Client_context.full)->use_ledger_or_fail~ledger_uri~filter:Filter.is_baking(funhidapi(version,_git_commit)~device_path:__ledger_id->let*()=matchversionwith|{Ledgerwallet_tezos.Version.app_class=Tezos;_}->failwith"This command (`authorize ledger ...`) only works with \
the Tezos Baking app"|{Ledgerwallet_tezos.Version.app_class=TezBake;major;_}whenmajor>=2->failwith"This command (`authorize ledger ...`) is@ not \
compatible with@ this version of the Ledger@ Baking app \
(%a >= 2.0.0),@ please use the command@ `setup ledger \
to bake for ...`@ from now on."Ledgerwallet_tezos.Version.ppversion|_->let*!()=cctxt#message"This Ledger Baking app is outdated (%a)@ running@ in \
backwards@ compatibility mode."Ledgerwallet_tezos.Version.ppversioninreturn_unitinlet*{Ledger_account.curve;path;_}=Ledger_uri.full_accountledger_uriinlet*pk=Ledger_commands.public_key_returning_instruction`Authorize_bakinghidapicurvepathinletpkh=Signature.Public_key.hashpkinlet*!()=cctxt#message"@[<v 0>Authorized baking for address: %a@,\
Corresponding full public key: %a@]"Signature.Public_key_hash.pppkhSignature.Public_key.pppkinreturn_some()));Tezos_clic.command~group~desc:"Setup a Ledger to bake for a key"(lethwm_argkind=letdoc=Printf.sprintf"Use <HWM> as %s chain high watermark instead of asking the \
ledger."kindinletlong=kind^"-hwm"indefault_arg~doc~long~placeholder:"HWM"~default:"ASK-LEDGER"(parameter(fun_->function|"ASK-LEDGER"->return_none|s->(tryreturn_some(Int32.of_strings)with_->failwith"Parameter %S should be a 32-bits integer"s)))inargs3(default_arg~doc:"Use <ID> as main chain-id instead of asking the node."~long:"main-chain-id"~placeholder:"ID"~default:"ASK-NODE"(parameter(fun_->function|"ASK-NODE"->return`Ask_node|s->(tryreturn(`Int32(Int32.of_strings))with_->(tryreturn(`Chain_id(Chain_id.of_b58check_exns))with_->failwith"Parameter %S should be a 32-bits integer or a \
Tezos_crypto.Base58 chain-id"s)))))(hwm_arg"main")(hwm_arg"test"))(prefixes["setup";"ledger";"to";"bake";"for"]@@Ledger_uri.ledger_uri_or_alias_param@@stop)(fun(chain_id_opt,main_hwm_opt,test_hwm_opt)ledger_uri(cctxt:Client_context.full)->use_ledger_or_fail~ledger_uri~filter:Filter.is_baking(funhidapi(version,_git_commit)~device_path:__ledger_id->let*()=letopenLedgerwallet_tezos.Versioninmatchversionwith|{app_class=Tezos;_}->failwith"This command (`setup ledger ...`) only works with the \
Tezos Baking app"|{app_class=TezBake;major;_}whenmajor<2->failwith"This command (`setup ledger ...`)@ is not@ compatible@ \
with this version@ of the Ledger Baking app@ (%a < \
2.0.0),@ please upgrade@ your ledger@ or use the \
command@ `authorize ledger to bake for ...`"ppversion|_->return_unitinlet*{Ledger_account.curve;path;_}=Ledger_uri.full_accountledger_uriinletchain_id_of_int32i32=letopenInt32inletbyten=logand0xFFl(shift_righti32(n*8))|>Int32.to_int|>char_of_intinChain_id.of_string_exn(Stringext.of_array(Array.init4(funi->byte(3-i))))inlet*main_chain_id=matchchain_id_optwith|`Ask_node->Chain_services.chain_idcctxt()|`Int32s->return(chain_id_of_int32s)|`Chain_idchid->returnchidinlet*(`Main_hwm(current_mh,current_mhr_opt),`Test_hwm(current_th,current_thr_opt),`Chain_idcurrent_ci)=Ledger_commands.wrap_ledger_cmd(funpp->Ledgerwallet_tezos.get_all_high_watermarks~pphidapi)inletmain_hwm=Option.valuemain_hwm_opt~default:current_mhinlettest_hwm=Option.valuetest_hwm_opt~default:current_thinlet*!()=cctxt#message"Setting up the ledger:@.* Main chain ID: %a -> %a@.* Main \
chain High Watermark: %ld%a -> %ld%a@.* Test chain High \
Watermark: %ld%a -> %ld%a"pp_ledger_chain_idcurrent_ciChain_id.ppmain_chain_idcurrent_mhpp_round_optcurrent_mhr_optmain_hwmpp_round_opt(Option.map(fun_->0l)current_mhr_opt)current_thpp_round_optcurrent_thr_opttest_hwmpp_round_opt(Option.map(fun_->0l)current_thr_opt)inlet*pk=Ledger_commands.public_key_returning_instruction(`Setup(Chain_id.to_stringmain_chain_id,main_hwm,test_hwm))hidapicurvepathinletpkh=Signature.Public_key.hashpkinlet*!()=cctxt#message"@[<v 0>Authorized baking for address: %a@,\
Corresponding full public key: %a@]"Signature.Public_key_hash.pppkhSignature.Public_key.pppkinreturn_some()));Tezos_clic.command~group~desc:"Deauthorize Ledger from baking"no_options(prefixes["deauthorize";"ledger";"baking";"for"]@@Ledger_uri.ledger_uri_or_alias_param@@stop)(fun()ledger_uri(_cctxt:Client_context.full)->use_ledger_or_fail~ledger_uri~filter:Filter.is_baking(funhidapi(_version,_git_commit)~device_path:__ledger_id->let*()=Ledger_commands.wrap_ledger_cmd(funpp->Ledgerwallet_tezos.deauthorize_baking~pphidapi)inreturn_some()));](** Commands for high water mark of the Baking app. The
[watermark_spelling] argument is used to make 2 sets of commands: with
the old/wrong spelling “watermark” for backwards compatibility and
with the correct one “high water mark” (it's a mark of the highest
water level). *)lethigh_water_mark_commandsgroupwatermark_spelling=letopenLwt_result_syntaxinletmake_descdesc=ifCompare.List_length_with.(watermark_spelling=1)thendesc^" (legacy/deprecated spelling)"elsedescinTezos_clic.[Tezos_clic.command~group~desc:(make_desc"Get high water mark of a Ledger")(args1(switch~doc:"Prevent the fallback to the (deprecated) Ledger instructions \
(for 1.x.y versions of the Baking app)"~long:"no-legacy-instructions"()))(prefixes(["get";"ledger";"high"]@watermark_spelling@["for"])@@Ledger_uri.ledger_uri_or_alias_param@@stop)(funno_legacy_apduledger_uri(cctxt:Client_context.full)->use_ledger_or_fail~ledger_uri~filter:Filter.is_baking(funhidapi(version,_git_commit)~device_path:__ledger_id->matchversion.app_classwith|Tezos->failwith"Fatal: this operation is only valid with the Tezos Baking \
application"|TezBakewhen(notno_legacy_apdu)&&version.major<2->let*hwm,hwm_round_opt=Ledger_commands.wrap_ledger_cmd(funpp->Ledgerwallet_tezos.get_high_watermark~pphidapi)inlet*!()=cctxt#message"The high water mark for@ %a@ is %ld%a."Ledger_uri.ppledger_urihwmpp_round_opthwm_round_optinreturn_some()|TezBakewhenno_legacy_apdu&&version.major<2->failwith"Cannot get the high water mark with@ \
`--no-legacy-instructions` and version %a"Ledgerwallet_tezos.Version.ppversion|TezBake->let*`Main_hwm(mh,mr),`Test_hwm(th,tr),`Chain_idci=Ledger_commands.wrap_ledger_cmd(funpp->Ledgerwallet_tezos.get_all_high_watermarks~pphidapi)inlet*!()=cctxt#message"The high water mark values for@ %a@ are@ %ld%a for the \
main-chain@ (%a)@ and@ %ld%a for the test-chain."Ledger_uri.ppledger_urimhpp_round_optmrpp_ledger_chain_idcithpp_round_opttrinreturn_some()));Tezos_clic.command~group~desc:(make_desc"Set high water mark of a Ledger")no_options(prefixes(["set";"ledger";"high"]@watermark_spelling@["for"])@@Ledger_uri.ledger_uri_or_alias_param@@prefix"to"@@param~name:"high watermark"~desc:"High watermark"(parameter(fun_ctxs->tryreturn(Int32.of_strings)with_->failwith"%s is not an int32 value"s))@@stop)(fun()ledger_urihwm(cctxt:Client_context.full)->use_ledger_or_fail~ledger_uri~filter:Filter.is_baking(funhidapi(version,_git_commit)~device_path:__ledger_id->matchversion.app_classwith|Tezos->failwith"Fatal: this operation is only valid with TezBake"|TezBake->let*()=Ledger_commands.wrap_ledger_cmd(funpp->Ledgerwallet_tezos.set_high_watermark~pphidapihwm)inlet*new_hwm,new_hwm_round_opt=Ledger_commands.wrap_ledger_cmd(funpp->Ledgerwallet_tezos.get_high_watermark~pphidapi)inlet*!()=cctxt#message"@[<v 0>%a has now high water mark: %ld%a@]"Ledger_uri.ppledger_urinew_hwmpp_round_optnew_hwm_round_optinreturn_some()));]letcommands=letgroup={Tezos_clic.name="ledger";title="Commands for managing the connected Ledger Nano devices";}infun()->generic_commandsgroup@baking_commandsgroup@high_water_mark_commandsgroup["water";"mark"]@high_water_mark_commandsgroup["watermark"]