From db66d410cd4b8cdf4e332342f62e04509af565aa Mon Sep 17 00:00:00 2001 From: Justin Berman Date: Sat, 6 Apr 2024 11:05:39 -0700 Subject: [PATCH] Submit review first pass (#29) --- docs/review_02.03.22/architecture.png | Bin 0 -> 30252 bytes docs/review_02.03.22/architecture.puml | 49 +++ docs/review_02.03.22/review_02.03.22.md | 546 ++++++++++++++++++++++++ 3 files changed, 595 insertions(+) create mode 100644 docs/review_02.03.22/architecture.png create mode 100644 docs/review_02.03.22/architecture.puml create mode 100644 docs/review_02.03.22/review_02.03.22.md diff --git a/docs/review_02.03.22/architecture.png b/docs/review_02.03.22/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..2c488d56ac82383c61727d98c56710d0091ebb0c GIT binary patch literal 30252 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 { + entity "monero-lws-daemon" as MoneroLWSDaemon { + * implements light wallet server REST API + * takes addresses and view keys and scans + for transactions in real-time + } + show MoneroLWSDaemon fields + entity "monero-lws-admin" as MoneroLWSAdmin { + * admin uses this program to perform various + administrative tasks managing the server, + such as accept or reject requests to + add an address to the server + } + show MoneroLWSAdmin fields + entity "monero-lws database" as MoneroLWSdb { + * uses LMDB, which reads/writes from/to + files on the host machine by default + } + show MoneroLWSdb fields + + MoneroLWSDaemon -up-> MoneroLWSdb + MoneroLWSAdmin -up-> MoneroLWSdb +} + +entity monerod { + * running Monero daemon +} +show monerod fields + +entity "Exchange rate API provider (optional)" as ExchangeRateProvider { + * currently cryptocompare.com + * disabled by default +} +show ExchangeRateProvider fields + +Client -right-> MoneroLWSDaemon +MoneroLWSDaemon -down-> monerod +MoneroLWSDaemon -down-> ExchangeRateProvider +@enduml diff --git a/docs/review_02.03.22/review_02.03.22.md b/docs/review_02.03.22/review_02.03.22.md new file mode 100644 index 0000000..b2e4b8c --- /dev/null +++ b/docs/review_02.03.22/review_02.03.22.md @@ -0,0 +1,546 @@ +## Overview + +This review is meant to serve as an informal audit of `monero-lws` as of +February 3, 2022. It is divided into 3 parts: + +1. How does `monero-lws` work? +2. The code +3. Suggestions + +It was put together as part of [a CCS proposal](https://ccs.getmonero.org/proposals/j-berman-3-months-full-time.html) +with the intention of getting `monero-lws` further along (and potentially +included in the [Monero project github](https://github.com/monero-project)). + +Note that this report does not contain documentation for all code in +`monero-lws`, and has to-do's for a number of folders and files. + +In my review, I did not find anything that would compromise `monero-lws` when +examining from the lens of the following threat model: + +``` +A user who is running `monerod` + `monero-lws` on a machine only the user has +access to does not leak any information about their Monero transactions to a 3rd +party through normal usage. Examples of leaks would be: + +- a backdoor that sends sensitive data from `monero-lws` out to a 3rd party +- `get_random_outs` responds with decoys that compromise the user's transactions +when stored on chain +- calls to the exchange rate API reveal information to the service provider +``` + +Disclaimer: I wouldn't call this audit definitively conclusive that the above +threat model is successfully defended, but I hope that it does inspire more +confidence in this fine piece of software, and serves as a useful document for +curious users, potential contributors, or any future auditors of `monero-lws`. +I reviewed (and tried to provide documentation for) nooks and crannies across +the code, and so at the very least can say with confidence there aren't any +obvious backdoors. + +## How does `monero-lws` work? + +Monero wallets need to scan all transactions in the blockchain to find which +ones belong to a user. `monero-lws` ("Monero light wallet server") is designed +to offload scanning for a user's transactions from the client to a server. As +stated in the [README.md](/README.md), it is an implementation of the +[Monero light wallet REST API](https://github.com/monero-project/meta/blob/master/api/lightwallet_rest.md), and is therefore compatible with e.g. a +[MyMonero frontend](https://github.com/mymonero/mymonero-app-js). A user may use +a MyMonero app (or any other compatible frontend) to connect with their own +running `monero-lws` instance, so that when the user revisits the app, the app +is immediately ready to use for spending. `monero-lws` works by taking in a +user's address and view key, and perpetually scanning for transactions received +and plausibly spent by the user. + +The primary risk to a user using `monero-lws` as their backend is that the +device where it runs is an additional target for someone malicious to compromise +a user's privacy. A user should assume that someone with access to wherever the +`monero-lws` backend is hosted will be able to determine all outputs received to +the user, and in most cases today, the outputs spent by the user as well. This +also includes **amounts** received to and spent by the user. If a `monero-lws` +instance is compromised, user funds are still SAFU since the private spend key +is never sent to `monero-lws`. Ongoing research may improve this risk profile +further, but the above should be users' default assumptions today. + +Whoever hosts a `monero-lws` instance can run the server via the +`monero-lws-daemon` as described in the [README.md](/README.md#running-monero-lws-daemon) +("Running monero-lws-daemon"). The daemon is expected to run 24/7 and +perpetually scan for transactions recevied to (and plausibly spent from) the +addresses known to the server in real-time. + +Whoever hosts a `monero-lws` instance can perform administrative tasks via the +`monero-lws-admin` program, as described in +[administration.md](/docs/administration.md). The `monero-lws-daemon` does not +need to be running in order for the admin to execute commands via +`monero-lws-admin`. + +Both `monero-lws-daemon` and `monero-lws-admin` read/write from/to an LMDB +database on the same machine hosting `monero-lws`. + +`monero-lws-daemon` can also fetch exchange rates from a 3rd party API provider +if an administrator wants. This is turned off by default. + +Here is a simplified diagram of the `monero-lws` architecture: + +
+

+
+ +In the [next section](#the-code), the code will be explained folder-by-folder, +file-by-file, and in some cases where appropriate, function-by-function. The +[final section](#areas-worth-exploring) will highlight suggestions to +potentially improve `monero-lws`. + +## The code + +### /src + +This is where the source code lives. The starter file for `monero-lws-daemon` is +[/src/server_main.cpp](/src/server_main.cpp). The starter file for +`monero-lws-admin` is [/src/admin_main.cpp](/src/admin_main.cpp). Both use +a lot of shared code across the repo. + +#### /src/server_main.cpp + +Starts in `main()` initializing logs and loading tools, then passes command line +args to a function `get_program()` to parse the args and construct the program +to run. The resulting program is then passed to `run()`. Failures are caught +gracefully. + +Passing `--help` to `monero-lws-daemon` is the one exception that does not run +a program, and instead outputs the following (note the configurable defaults): + +``` +Usage: [options] +Options: + --db-path arg (=~/.bitmonero/light_wallet_server) + Folder for LMDB files + --network arg (=main) <"main"|"stage"|"test"> - Blockchain + net type + --help Produce help message + --daemon arg (=tcp://127.0.0.1:18082) ://
: of a + monerod ZMQ RPC + --sub arg tcp://address:port or ipc://path of a + monerod ZMQ Pub + --rest-server arg (=https://0.0.0.0:8443) + [(https|http)://
:] for + incoming connections, multiple + declarations allowed + --rest-ssl-key arg to PEM formatted SSL key for + https REST server + --rest-ssl-certificate arg to PEM formatted SSL certificate + (chains supported) for https REST + server + --rest-threads arg (=1) Number of threads to process REST + connections + --scan-threads arg (=8) Maximum number of threads for account + scanning + --access-control-origin arg Specify a whitelisted HTTP control + origin domain + --confirm-external-bind Allow listening for external + connections + --create-queue-max arg (=10000) Set pending create account requests + maximum + --exchange-rate-interval arg (=0) Retrieve exchange rates in minute + intervals from cryptocompare.com if + greater than 0 + --log-level arg (=1) Log level [0-4] +``` + +Note that `scan-threads` defaults to the maxmimum number of available threads +on the host machine. + +Also note that the `rest-server` defaults to being served over https. + +Here are sample logs when you start the default program with `log-level` 4. + +``` +2022-01-11 07:56:42.713 I Using monerod ZMQ RPC at tcp://127.0.0.1:18082 +2022-01-11 07:56:42.713 I Starting blockchain sync with daemon +2022-01-11 07:56:42.715 I [PARSE URI] regex not matched for uri: ^((.*?)://)?(\[(.*)\](:(\d+))?)(.*)? +2022-01-11 07:56:42.715 I Binding on 0.0.0.0 (IPv4):8443 +2022-01-11 07:56:42.715 I Generating SSL certificate +2022-01-11 07:56:43.092 D start accept (IPv4) +2022-01-11 07:56:43.092 D Spawned connection #0 to 0.0.0.0 currently we have sockets count:1 +2022-01-11 07:56:43.092 D test, connection constructor set m_connection_type=1 +2022-01-11 07:56:43.092 I Run net_service loop( 1 threads)... +2022-01-11 07:56:43.092 D Run server thread name: NET +2022-01-11 07:56:43.092 D Reiniting OK. +2022-01-11 07:56:43.092 I Listening for REST clients at https://0.0.0.0:8443 +``` + +When `get_program()` calls `run()`, this creates the LMDB environment and +initializes all tables in the db, syncs `monero-lws` with the running `monerod` +daemon, subscribes to the monero ZMQ RPC, initializes an http client to query +for exchange rates from cryptocompare.com, initializes the REST server so that +it can start listening for requests, and starts the scanner so that it will +perpetually scan for users' transactions. + +#### /src/rest_server.cpp + /src/rest_server.h + +The REST server exposes the following 7 endpoints: + +- `/login` +- `/import_wallet_request` +- `/get_address_info` +- `/get_address_txs` +- `/get_random_outs` +- `/get_unspent_outs` +- `/submit_raw_tx` + +It handles requests via the function `handle_http_request()`, which does +standard validation such as whether the endpoint is implemented, if the request +is appropriately sized (each endpoint has sane default byte size allowances), +if the request is an HTTP POST request. Then executes the respective endpoint +passing in params included in the request, then handles any errors and responds +accordingly, or returns with a standard success response and body. + +##### Authentication + +Every endpoint that accepts an `address` and `view_key` as params +(`/login`, `/get_address_info`, `/get_address_txs`, `/get_unspent_outs`, and +`/import_wallet_request`) also enforces that the public view key included in +the `address` is derived from the private `view_key` provided in the request +(via the `key_check()` function). Additionally, addresses are indexed in the db +by the public view key. Thus, even if the public view key *is* derived from the +private view key, if the public view key is not stored in the db, then the +response will fail. This way if a malicious user provides an honest user's +public spend key, and also provides a public view key that is derived from some +random private view key, the account would not be found in the db. This +ensures that only the user with the correct `address` and `view_key` pair +can access their own data on the server through the REST API. + +##### Exchange Rates + +`/get_address_info` is the only endpoint that responds with exchange rates iff +the server has exchange rates enabled. The server serves the latest cached +exchange rates, rather than makes requests to the exchange rate API provider +every time exchange rates are included in a response. The server requests +exchange rates and caches them in intervals of `exchange-rate-interval`, which +is an arg the admin can set when starting the `monero-lws-daemon`. This caching +in set intervals strategy is a good privacy-respecting approach, otherwise the +exchange rate API provider could collect usage stats on how often a user calls +`/get_address_info`. + +##### /login + +> Check for the existence of an account or create a new one. + +The very first thing done is authentication on the provided `address` and +`view_key` as described above. On failure, it returns `bad_view_key`, which is +great. Next, it queries the db for the `account` with the given `address`. If +the account is found, it checks if the account has status `hidden`, which is +a status the admin can set via `monero-lws-admin`. If hidden, it will return +`account_not_found`. Otherwise, if it exists, it will simply return with +`create_account` set to false (account was already created), and with whatever +`generated_locally` flag was set at account creation. If the account does not +exist and the user is not attempting to `create_account`, or there was an error +reading the db, then it will return with an error. Finally, if the account does +not already exist, and the user is attempting to `create_account`, then the +server will write a "creation request" to the db, which is just a request to +create an account with that `address` and `view_key` pair (and provided flags). +The admin will need to then manually approve the creation request via +`monero-lws-admin` in order to add the address to the server. + +##### /import_wallet_request + +> Request an account scan from the genesis block. + +This first calls `open_account()`, which authenticates the `address` and +`view_key`, then retrieves the account from the db by address. Just like above, +this function returns with `account_not_found` if hidden or not found, or +returns with the account. If the account's starting scanning height +(`start_height`) is already 0, then the import request returns as fulfilled. +Otherwise, it checks for the existence of another import request. +If one already exists, then the endpoint returns saying `Waiting for Approval`. +If not, then it says `Accepted, waiting for approval`. + +##### /get_address_info + +> Returns the minimal set of information needed to calculate a wallet balance. The server cannot calculate when a spend occurs without the spend key, so a list of candidate spends is returned. + +First calls `open_account()`. Then, using the account's unique identifier in the +db, queries for all the user's received outputs and candidate spends. Then +checks the db for the latest height the server knows. Then starts constructing +height metadata for response. Then iterates over all received outputs, collects +spend metadata for each, and totals up `total_received` and `locked_funds`. +Then iterates over candidate spends, retrieves spend metadata collected in +prior step, and totals up `total_sent`. Finally, retrieves exchange rates +if the exchange rate interval is set. Then returns the constructed response. + +##### /get_address_txs + +> Returns information needed to show transaction history. The server cannot calculate when a spend occurs without the spend key, so a list of candidate spends is returned. + +First calls `open_account()`. Then queries db for received outputs, candidate +spends, and latest height data. Constructs the same height metadata as above. +Then, iterates over all received and candidates spends at one time, placing +candidate spends in an array inside each received output. + +Note there is some more work done before sending the final response over the +wire in `/src/rpc/light_wallet.cpp` that will be explained further later on. + +##### /get_random_outs + +> Selects random outputs to use in a ring signature of a new transaction. If the `amount` is `0` then the `monerod` RPC `get_output_distribution` should be used to locally select outputs using a gamma distribution as described in "An Empirical Analysis of Traceability in the Monero Blockchain". If the `amount` is not `0`, then the `monerod` RPC `get_output_histogram` should be used to locally select outputs using a triangular distribution (`uint64_t dummy_out = histogram.total * sqrt(float64(random_uint53) / float64(2^53))`). + +First, limits requests to a max mixin (ring size) of 50, or max amounts +requested (the number of different rings) to 20 to avoid overloading the server +with a single request. Sets up daemon RPC client to query for output data. Then +for all non-0 amounts, retrieives output histograms and places in an array. A +relevant query parameter to highlight here is the `recent_cutoff` when +requesting histograms; the recent cutoff was used in wallet2 to bias toward +recent outputs when selecting decoys for **pre**-RingCT outputs. However, +pre-RingCT outputs are infrequent enough today that it should no longer yield +practical privacy benefits to try and select more recent pre-RingCT outputs in +higher frequency. Thus, it is ok that `monero-lws` sets this `recent_cutoff` +param to 0, so that the query wastes no time in factoring in recency for +pre-RingCT outputs. + +After retrieving histograms for pre-RingCT outputs, retrieves the output +distribution of RingCT outputs across the whole chain (if 0 amount outputs +are requested). This distribution is later passed into a `lws:gamma_picker` +declaration, where it's used to select RingCT decoys. + +Then sets up a constructor to fetch output public keys over RPC (accepts output +id's as param). + +Final step, the function passes in all data (and fetch function) needed +to actually make decoy selections from the distribution/histograms. This +function is called `pick_random_outputs` and will be explained in greater +detail later. + +The response from `pick_random_outputs` is the final response to +`/get_random_outs`. + +Note: it should be noted here that any server that records the selected decoys +and cross-references them with the outputs included in a transaction can easily +tell which output is real (the one not included as a decoy in the response). +Thus, a user should expect that if they are using a light wallet server to +construct transactions, the server can trivially tell which outputs are +spent in the user's transactions, no heuristics needed. + +##### /get_unspent_outs + +> Returns a list of received outputs. The client must determine when the output was actually spent. + +First calls `open_account()`. Then makes an RPC request to `monerod` for the +dynamic fee estimate. A relevant query parameter to highlight here is +`num_grace_blocks`, which is the +["number of blocks we want the fee to be valid for"](https://github.com/monero-project/monero/blob/298c9a357f6e57eccf28db1f3e734eb6da080d9a/src/cryptonote_core/blockchain.h#L646). +It is set to `10`, which matches [wallet2](https://github.com/monero-project/monero/blob/319b831e65437f1c8e5ff4b4cb9be03f091f6fc6/src/wallet/wallet2.cpp#L118). +It is important this estimate matches what other Monero ecosystem users would +use, since if the fee were different, it would cause transactions constructed +with the different fee to be fingerprintable. Thus, matching `num_grace_blocks` +is good. + +The endpoint then gets all of a user's received outputs and for all outputs that +are >= requested `dust_threshold`, or are in tx's with ring sizes >= requested +`mixin_count`, gets their associated key images, then tacks the final `output` +object onto what will eventually become the `outputs` array in the response. + +Finally, calculates the `per_kb_fee` factoring in `size_scale` and `fee_mask` +returned by the daemon. Note that `per_kb_fee` has been deprecated in the +light wallet REST API, and this should be updated to `per_byte_fee`. See [this +issue](https://github.com/vtnerd/monero-lws/issues/25) for more. + +Note there is some more work done before sending the final response over the +wire in `/src/rpc/light_wallet.cpp` that will be explained further later on. + +##### /submit_raw_tx + +> Submit raw transaction to be relayed to monero network. + +Very simple. Just relays the transaction over the daemon RPC `send_raw_tx_hex` +endpoint. Returns error if not relayed successfully. + +#### /src/scanner.cpp + /src/scanner.h + +This code handles scanning the blockchain for users' transactions while +`monero-lws-daemon` is running. It makes sure `monero-lws` is in sync with the +chain, stays in sync, and just keeps scanning all the time. + +##### lws::scanner::stop() + +Stops the scanner, naturally. + +Sets the `monero-lws-daemon`'s `running` state to false. It is called from +`server_main.cpp`, which is listening for a SIGINT signal in order to trigger +`lws::scanner::stop()`. + +##### lws::scanner::sync() + +Syncs the running `monero-lws-daemon` with the blockchain, making sure +`monero-lws-daemon` knows what blocks still need to be scanned. + +First thing this function does is reads the `monero-lws` db to find the most +recent known block hashes. Then it requests block hashes from the running +`monerod` daemon via RPC endpoint `/get_hashes_fast`, providing the known hashes +as a param. Uses while loops to wait on daemon response and times out or exits +if admin signals to end the program via SIGINT. Once daemon returns the +response, if the daemon has already synced to the top of the chain, the `sync()` +function returns successfully. If not, writes the returned new hashes to the +`monero-lws` db, and rolls back/updates the chain if a reorg has been detected. +If there are still more hashes to request from the daemon, continues loop +requesting from the daemon. Otherwise, breaks and the function returns. + +##### lws::scanner::run() + +Runs the scanner in a perpetual loop, so that it continues scanning for users' +transactions while `monero-lws-daemon` is running. Also updates exchange rates +held in the cache. + +First retrieives all active accounts and their received outputs from the +`monero-lws` db. If no active accounts, sleeps for a bit. Otherwise, proceeds +to scan for transactions for all active accounts inside `check_loop()`. Then, +checks if the scanner has stopped and should exit `run()`, otherwise continues. +Next initializes an rpc client connection to the daemon if one already not +initialized. Then calls `lws::scanner::sync()` to make sure `monero-lws` is +still in sync with `monerod`. Finally, if there is a sync error that is not a +timeout, throws; if not timeout, warns in a log. Continues looping until +there are users to scan transactions for. + +###### check_loop() + +Relevant comments: + +> Launches `thread_count` threads to run `scan_loop`, and then polls for active account changes in background + +``` +The algorithm here is extremely basic. Users are divided evenly amongst +the configurable thread count, and grouped by scan height. If an old +account appears, some accounts (grouped on that thread) will be delayed +in processing waiting for that account to catch up. Its not the greatest, +but this "will have to do" for the first cut. +Its not expected that many people will be running +"enterprise level" of nodes where accounts are constantly added. + +Another "issue" is that each thread works independently instead of more +cooperatively for scanning. This requires a bit more synchronization, so +was left for later. Its likely worth doing to reduce the number of +transfers from the daemon, and the bottleneck on the writes into LMDB. + +If the active user list changes, all threads are stopped/joined, and +everything is re-started. +``` + +Exactly as described, the first thing it does is sorts all users by +`scan_height`, so that it can send batches of users at a time into threads +that execute `scan_loop()` over those users (so that users with similar heights +are processed together). Then it enters a while loop that updates exchange rates +over the exchange rate interval, and then perpetually checks for changes to +the users stored in the `monero-lws` db. If it finds that there is a change +in active users stored, `check_loop()` returns so that `lws::scanner::run()` +can do another loop, collecting all active users again and re-entering +`check_loop()` with the updated list. + +###### scan_loop() + +TO-DO + +#### /src/admin_main.cpp + +#### /src/CMakeLists.txt + +#### /src/config.cpp + +#### /src/config.h + +#### /src/error.cpp + +#### /src/error.h + +#### /src/fwd.h + +#### /src/options.h + +#### /src/wire.h + +#### /src/db + +##### /src/db/account.cpp + +##### /src/db/account.h + +##### /src/db/CMakeLists.txt + +##### /src/db/data.cpp + +##### /src/db/data.h + +##### /src/db/fwd.h + +##### /src/db/storage.cpp + +##### /src/db/storage.h + +##### /src/db/string.cpp + +##### /src/db/string.h + +#### /src/rpc + +##### /src/rpc/client.cpp + +##### /src/rpc/client.h + +##### /src/rpc/CMakeLists.txt + +##### /src/rpc/daemon_pub.cpp + +##### /src/rpc/daemon_pub.h + +##### /src/rpc/daemon_zmq.cpp + +##### /src/rpc/daemon_zmq.h + +##### /src/rpc/fwd.h + +##### /src/rpc/json.h + +##### /src/rpc/ligt_wallet.cpp + +##### /src/rpc/ligt_wallet.h + +##### /src/rpc/rates.cpp + +##### /src/rpc/rates.h + +#### /src/util + +#### /src/wire + +### /CMakeLists.txt + +## Suggestions + +- Consider indexing addresses by a hash of the private view key instead of just +public view key, so that timing analysis under perfect conditions cannot reveal +whether an address is stored. + +- See the current state of subaddress support +[over here](https://github.com/monero-project/meta/pull/647). + +- Share a guide on hosting `monero-lws` behind a .onion, and enable a frontend +to make calls to it. + +- The exchange rate API defaults to off. When turned on, it fetches prices in a +custom interval over the clearnet. `monero-lws` could be bundled with tor or +another anonymity network, so that queries for exchange rates are made over the +anonymity network by default. + +- Consider suggesting a particular interval to admins who wish to turn on the +exchange rate interval or requiring intervals at particular windows (e.g. 10 +seconds, 30 seconds, 60 seconds, etc.), to prevent an admin from fingerprinting +themselves via the exchange rate interval they choose to set. + +- Allow users to add addresses and import requests in a secure way without +needing manual input from the admin, for example, by using a special auth token +(or username/password combo) that allows a user to make as many requests as +they want (or comes with special rules). This minimizes need for out-of-band +solutions to automate this process in a potentially insecure way (e.g. +auto-accepting all requests). + +- Be mindful that even if an account has a `hidden` status, someone would be +able to still tell that account is stored on the server since `/login` will +always return `account_not_found`, even if trying to create the account. This +is not a big deal, because you'd need the `address` and `view_key` to be able +to reach this point. Just worth being mindful of.