Ledger Integration
We are evaluating writing a Sia app for the Ledger Nano S. Nothing is promised at this point. I'm creating this issue so that we can discuss design and track progress.
Plan
The current plan is to write a barebones Ledger app from scratch using the Nano S SDK that will support generating addresses and signing transactions. This app will talk to a client program on the user's computer -- preferably siac
, but possibly a separate standalone program.
Ledger provides Bitcoin equivalents for these programs (blue-btc-app for the device itself and ledger-wallet-chrome for the client program), and as far as I can tell, pretty much every altcoin Ledger app is a fork of these. It's feasible that we could do the same for Sia: just replace the Bitcoin-specific code with Sia-specific code. ("Just" is a bit misleading; we don't really know how difficult this would be in practice.) However, we are uncomfortable with both the size of these programs and the dependency on Chrome. The larger a program is, the more difficult it is to audit. My hope is that we can produce a Ledger app that is minimal, easy to understand, and easy to audit. I unfortunately cannot ascribe these qualities to any other Ledger app I've looked at.
UX
The UX of our Ledger app will not deviate substantially from blue-app-btc
. The flow for generating an address looks like this:
- User plugs Nano S into their computer and initializes it, generating a seed.
- User installs and opens Sia Ledger app.
- User runs
siac wallet ledger address
on their computer. - Nano S displays address; user confirms, and the address is recorded in the user's
siad
wallet.
The flow for signing transactions is more complicated. For now, it will not be very user friendly.
- User uses the
/wallet/unspent
endpoint (added by #2907) to get a list of outputs to spend. - User constructs an unsigned transaction using the outputs.
- User runs
siac wallet ledger sign [txn] [inputs to sign]
- Nano S displays each input being signed and the address + amount of each output.
- User confirms, and siac displays the signed transaction.
- User broadcasts signed transaction.
Implementation
One interesting thing I discovered is that the Nano S can only generate HD keys. There isn't a secure way to generate standard Sia keys using the Nano S's seed, because access to the seed is mediated by a few specialized functions.
This isn't a big deal; in practice, we'll do what other coins do: pick a Sia-specific derivation constant, and use the Sia "index" as the last part of the derivation path. This will all be invisible to the user. One potential pitfall is that the user may assume that they can import their Nano S seed into a standard siad
wallet -- this is not at all the case.
#2907 suggests this interface for a hardware wallet:
type HardwareWallet interface {
DeriveKey(index uint64) (pubkey [32]byte)
SignHash(hash [32]byte, index uint64) (signature [64]byte)
}
And indeed, this is sufficient to implement key derivation and transaction signing. In this model, the Ledger app does the bare minimum, making its code maximally simple to understand and verify. However, this approach is fundamentally flawed, because it blindly signs whatever hashes the client program provides without user validation. Even if the user did want to validate the hashes, how would they? They would need to compare each hash to a "trusted" hash, and unless you can do blake2b in your head, that means trusting another computer. Remember, the entire reason to use a hardware wallet is because you don't trust the software running on your PC. We must assume that the user may be running a malicious version of siac
. Which means that we must design the Ledger app such that even if it is talking to a malicious computer, the computer cannot trick the Ledger (or the user) into losing coins. At worst, the computer may be able to trick the user into thinking that they sent coins, when in reality the transaction was invalid and silently dropped.
So if users can't realistically validate hashes, what can they validate? Anything that appears in an explorer is probably fair game, so that means addresses, output IDs, and amounts. So a more secure interface would be:
type HardwareWallet interface {
DeriveKey(index uint64) (pubkey [32]byte)
Sign(txn types.Transaction, sigIndex int, keyIndex uint64) (signature [64]byte)
}
Sign
would basically do what addSignatures
does: calculate txn.SigHash(sigIndex)
and sign it with the key derived from keyIndex
. txn
will have to include a half-formed TransactionSignature
at sigIndex
; Sign
would only fill in the actual TransactionSignature.Signature
field. It would then display txn.TransactionSignatures[sigIndex].ParentID
, which would be the OutputID
of the output being spent. The user would be responsible for confirming that this is the output they intend to spend.
This takes care of the inputs, but the user will always want to verify the outputs of the transaction. The Nano S should display the address and amount of each transaction output. This prevents the client program from altering the outputs.
The downside of this approach is that it requires a lot more work on behalf of the Nano S. We would need to port much of the transaction serialization code to C. It's certainly doable, and maybe not even that hard, but it's annoying because it would be much easier to do client-side. It also requires some fancier UI code (to scroll through the inputs and outputs), and writing UI code is (at least for me) much trickier than writing transaction signing code.
Security
The client program can try to trick the Nano S (or the user) into doing something they didn't intend to do. For our purposes, this essentially can be reduced to "sending coins to any address not explicitly approved by the user," but it's also possible that the client program could exploit a flaw in the Sia Ledger app to cause it to malfunction. This is the motivation for keeping our app code as minimal as possible.
The client program has one primary means of attack: displaying one piece of data to the user while sending/receiving a different piece of data to/from the Nano S. We already saw how this attack can work if the Nano S only receives a hash. The client program can tell the user that a given transaction hash is x, when in reality the hash is y and x is the hash of a different transaction.
The new implementation should thwart any such attack, as long as the user performs validation correctly. Here I will enumerate the main attacks I can think of, and how they fail. Please suggest more attacks if you think of any.
- The client program modifies the outputs of the transaction before sending them to the Nano S. The user will see that the outputs are not what they expected, and refuse to sign.
- The client program modifies the outputs of the transaction after sending them to the Nano S. This causes the signatures added by the Nano S to become invalid.
- The client program modifies the
CoveredFields
field of theTransactionSignature
so that the signature only covers one input. This type of signature would not become invalid if other parts of the transaction were altered. The Sia Ledger app should either refuse to signCoveredFields
that do not set theWholeTransaction
flag, or display an extra warning to the user that non-standardCoveredFields
are being used. - The client program calls
DeriveKey
orSign
with invalid parameters, such as a malformedtxn
or an out-of-boundssigIndex
. The Sia Ledger app must be able to detect these and throw an error.
Status
I have implemented some basic routines for key derivation and signing. The next step is to start porting the transaction decoding code, so that the Nano S can parse transactions received from the client program and display their inputs/outputs to the user. No validation will be performed at this point. Once the Nano S can perform these actions in isolation, we can move on to writing client program code to pass data to and from the Nano S. For now, we will likely use Ledger's C or Python API to communicate with the Nano S, but later on we may want to port the API to Go so that we can integrate the client program code directly into siac
. If this proves infeasible, the client program will have to remain a standalone program.