diff --git a/.gitignore b/.gitignore index 4d441ddc..1030c38d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ bin debug -dep +/dep obj src/u2f_crypto_data.h src/glyphs.h diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..23501267 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,21 @@ +# Contributing + +## Hacking with [Nix](https://nixos.org/nix/) + +The `nix/` folder contains helper scripts for working with the ledger via Nix. + +### Developing +Use `nix/env.sh` to enter a shell where you can run `make` and it will just work. You can also pass a command instead, e.g. `nix/env.sh make clean`. + +For development, use `nix/watch.sh make APP=` to incrementally build on every change. Be sure to `nix/env.sh make clean` if you start watching a different `APP`. + +### Building +To do a full Nix build run `nix/build.sh`. You can pass `nix-build` arguments to this to build specific attributes, e.g. `nix/build.sh -A wallet`. + +### Installing +`nix/install.sh` will install both the wallet and baking apps. Use `nix/install.sh baking` to install just the baking app or `nix/install.sh wallet` to install just the wallet. + + +### Releasing + +`nix/build -A release.all` diff --git a/Makefile b/Makefile index f33fdf77..66a76b02 100644 --- a/Makefile +++ b/Makefile @@ -30,23 +30,38 @@ else ifeq ($(APP),tezos_wallet) APPNAME = "Tezos Wallet" endif APP_LOAD_PARAMS=--appFlags 0 --curve ed25519 --curve secp256k1 --curve prime256r1 --path "44'/1729'" $(COMMON_LOAD_PARAMS) -VERSION_TAG=$(shell git describe --tags | cut -f1 -d-) -APPVERSION_M=1 -APPVERSION_N=5 +VERSION_TAG ?= $(shell git describe --tags 2>/dev/null | cut -f1 -d-) +APPVERSION_M=2 +APPVERSION_N=0 APPVERSION_P=0 APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P) -ifneq (v$(APPVERSION), $(VERSION_TAG)) +# Only warn about version tags if specified/inferred +ifeq ($(VERSION_TAG),) + $(warning VERSION_TAG not checked) +else + ifneq (v$(APPVERSION), $(VERSION_TAG)) $(warning "Version-Tag Mismatch: v$(APPVERSION) version and $(VERSION_TAG) tag") + endif endif -COMMIT := $(shell git describe --abbrev=8 --always) +COMMIT ?= $(shell git describe --tags --abbrev=8 --always --long --dirty 2>/dev/null) +ifeq ($(COMMIT),) + $(error COMMIT not specified and could not be determined with git) +endif ICONNAME=icon.gif + ################ # Default rule # ################ -all: default +all: show-app default + + +.PHONY: show-app +show-app: + @echo ">>>>> Building $(APP) at commit $(COMMIT)" + ############ # Platform # diff --git a/README.md b/README.md index d00d0fb8..cd7aa6ce 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,16 @@ blocks. This repository contains two Ledger Nano S applications: - 1. The "Tezos Baking" application is for baking Tezos: signing new blocks, - endorsements, and denunciations. For more information about baking, see + 1. The "Tezos Baking" application is for baking: signing new blocks and + endorsements. For more information about baking, see [*Benefits and Risks of Home Baking*](https://medium.com/@tezos_91823/benefits-and-risks-of-home-baking-a631c9ca745). - 2. The "Tezos Wallet" application is for making XTZ transactions, and + 2. The "Tezos Wallet" application is for making XTZ transactions, originating contracts, delegation, and voting. Basically everything you might want to use the Ledger Nano S for on Tezos besides baking. It is possible to do all of these things without a hardware wallet, but using a hardware wallet provides you better security against key theft. -Currently, there is no GUI support, so everything in this document is +This documentation was originally written when there was no GUI support, so everything is tailored towards the command line. We recommend you read this entire document to understand the commands available, and which commands are most appropriate to your situation. This will require judgment on how @@ -38,10 +38,7 @@ help you understand that. This document is not a comprehensive guide to setting up Tezos software. While it covers some aspects of setting up and installing Tezos nodes and clients, especially as it interacts with the Ledger Nano S, -you should familiarize yourself with the Tezos community's own documentation, -including -[how to build a node](https://github.com/tezoscommunity/FAQ/blob/master/Compile_Betanet.md) -and the [Tezos technical FAQ](https://github.com/tezoscommunity/FAQ/wiki/Tezos-Technical-FAQ). +you should familiarize yourself with the [Tezos Documentation](https://tezos.gitlab.io/master/) and community resources such as Tezos Community's guide on [building a node](https://github.com/tezoscommunity/FAQ/blob/master/Compile_Mainnet.md). If you have questions, please ask them on the [Tezos Stack Exchange](https://tezos.stackexchange.com/). This document is also not a guide on how to use Linux. It assumes you know how to install and configure a Linux system to your general needs, @@ -94,11 +91,7 @@ can fall into the wrong hands. You will write it down on paper, along with your PIN, and store it. If you will have a large amount of money, consider putting your paper in a safe or safe deposit box, but at the very least keep it away from places where children, -dogs, housekeepers, obnoxious neighbors, could inadvertently destroy it. -Additionally, we recommend storing your PIN in a password manager. - -Finally, if you will not be baking yourself, consider disconnecting your Ledger device -and closing your password manager when not in use. +dogs, housekeepers, or obnoxious neighbors could inadvertently destroy it. ### Protecting Your Key -- Further Advanced Reading @@ -108,11 +101,11 @@ or plausible deniability features should look at Note that Ledger devices with different seeds will appear to `tezos-client` to be different hardware wallets. Note also that it can change what key is authorized in -the Baking App. When using these features in a Ledger hardware wallet used for baking, -please exit and re-start the Baking App right before baking is supposed to +Tezos Baking. When using these features in a Ledger hardware wallet used for baking, +please exit and re-start Tezos Baking right before baking is supposed to happen, and manually verify that it displays the key you expect to bake for. -The Wallet App does not require such extra steps, and so these extra +Tezos Wallet does not require such extra steps, and so these extra protections are more appropriate for keys used for transaction than they are for keys used for baking. If you do use these features, one technique is that your tez be stored in the passphrase-protected and @@ -124,9 +117,7 @@ way, the baking account won't actually store the vast majority of the tez. To use these apps, you must be sure to have [up-to-date firmware](https://support.ledgerwallet.com/hc/en-us/articles/360002731113) on the Ledger device. This code was tested with version -1.4.2. Please use Ledger's tools, including their [Chrome -app](https://www.ledgerwallet.com/apps/manager), to do this. Note that -you can't talk to the Ledger device's OS itself while an application is running. +1.5.5. Please use [Ledger Live](https://www.ledger.com/pages/ledger-live) to do this. ### udev rules (Linux only) @@ -186,17 +177,25 @@ Once you have added this, run `sudo nixos-rebuild switch` to activate the configuration, and unplug your Ledger device and plug it in again for the changes to take effect. -## Obtaining the Ledger Nano S apps +## Installing the Applications with Ledger Live + +The easiest way to obtain and install the Tezos Ledger Nano S apps is to download them +from [Ledger Live](https://www.ledger.com/pages/ledger-live). Tezos Wallet is readily available +in Ledger Live's Manager. To download Tezos Baking, you'll need to enable 'Developer Mode' in Settings. + +If you've used Ledger Live for app installation, you can skip ahead to [Registering the Ledger Nano S with the node](#registering-the-ledger-nano-s-with-the-node). + +## Obtaining the Ledger Nano S apps without Ledger Live If you are using the [Nix package manager](https://nixos.org/nix/), you can skip this section and the next one; go directly to -[Tezos baking platform](https://gitlab.com/obsidian.systems/tezos-baking-platform) -for simpler Nix-based installation, where documentation should be in `ledger/BUILDING.md`. +[Tezos Baking Platform](https://gitlab.com/obsidian.systems/tezos-baking-platform) +for simpler Nix-based installation, where documentation should be in `ledger/README.md`. Then return to this document and continue reading at *Using the Ledger Nano S apps*. -The easiest way to obtain the Tezos Ledger Nano S apps is to download the `.hex` files +The second easiest way to obtain both applications (after Ledger Live) is to download `.hex` files from the [releases](https://github.com/obsidiansystems/ledger-app-tezos/releases) -page. After doing so, skip ahead to *Installing the apps onto your Ledger device*. +page of this repo. After doing so, skip ahead to *Installing the apps onto your Ledger device*. You will need to expand the releases tarball somewhere and copy the baking.hex and wallet.hex files into the ledger-app-tezos directory. If you want to compile the applications yourself, keep reading this section. @@ -250,7 +249,7 @@ $ export BOLOS_ENV=$PWD/bolos_env To build the Tezos Wallet app: ``` -$ env BAKING_APP= make +$ APP=tezos_wallet make $ mv bin/app.hex wallet.hex ``` If this results in an error message that includes this line (possibly repeatedly): @@ -269,27 +268,23 @@ Note that if you build *both* apps, you need to run `make clean` before building the second one. So, to build both apps run: ``` -$ env BAKING_APP= make +$ APP=tezos_wallet make $ mv bin/app.hex wallet.hex $ make clean -$ env BAKING_APP=Y make +$ APP=tezos_baking make $ mv bin/app.hex baking.hex ``` To build just the Tezos Baking App: ``` -$ env BAKING_APP=Y make +$ APP=tezos_baking make $ mv bin/app.hex baking.hex ``` -## Installing the apps onto your Ledger device +### Installing the apps onto your Ledger device without Ledger Live -Ledger's primary interface for loading an app onto a Ledger device is a Chrome -app called [Ledger Manager](https://www.ledgerwallet.com/apps/manager). However, -it only supports a limited set of officially-supported apps like Bitcoin and -Ethereum, and Tezos is not yet among these. So you will need to use a -command-line tool called the +Manually installing the apps requires a command-line tool called the [BOLOS Python Loader](https://ledger.readthedocs.io/projects/blue-loader-python/en/0.1.16/index.html). ### Installing BOLOS Python Loader @@ -377,34 +372,12 @@ as you continue. You may want to read the rest of these instructions before you begin installing, as you will need to confirm and verify a few things during the process. -Still within the virtualenv, run the `./install.sh` command. This script is in -the root directory of this very repo, which means that in order to have it, you -must clone this repo and `cd` into the resulting directory: - -``` -$ git clone https://github.com/obsidiansystems/ledger-app-tezos.git -$ cd ledger-app-tezos/ -``` - -This `./install.sh` script takes two parameters, the first of which is -the *name* of the application you are installing, and the second is the -path to the `app.hex` file. (Up to date `app.hex` files can be found in -the releases for this repo, and can be unpacked in the same directory that -contains `./install.sh`): - -* If you are installing the baking app, we recommend using the name "Tezos - Baking". - - ``` - $ ./install.sh "Tezos Baking" baking.hex - ``` - -* If you are installing the transaction app, we recommend using the name "Tezos - Wallet". +Still within the virtualenv, run the `./install.sh` command included in the `release.tar.gz` +that you downloaded. - ``` - $ ./install.sh "Tezos Wallet" wallet.hex - ``` +This `./install.sh` script takes the path to an app directory. Two such directories +were included in the downloaded `release.tar.gz`. +Install both apps like this: `./install.sh wallet baking`. The first thing that should come up in your terminal is a message that looks like this: @@ -446,10 +419,7 @@ If you'd like to remove your app, you can do this. In the virtualenv described in the last sections, run this command: ``` -$ python \ - -m ledgerblue.deleteApp \ - --targetId 0x31100003 \ - --appName "Tezos" +$ python -m ledgerblue.deleteApp --targetId 0x31100004 --appName 'Tezos Wallet' ``` Replace the `appName` parameter "Tezos" with whatever app name you used when you @@ -460,7 +430,7 @@ Then follow the prompts on the Ledger Nano S screen. ### Confirming the Installation Worked You should now have two apps, `Tezos Baking` and `Tezos Wallet`. The `Tezos -Baking` app should display also a `0` on the screen, which is the highest block +Baking` app should display a `0` on the screen, which is the highest block level baked so far (`0` in case of no blocks). The `Tezos Wallet` app will just display `Tezos`. @@ -474,7 +444,7 @@ Currently there are two other ways to do this: 1. If you have the Nix package manager, use the [Tezos baking platform](https://gitlab.com/obsidian.systems/tezos-baking-platform). - 2. Build tezos from the tezos repo with [these instructions](http://doc.tzalpha.net/introduction/howto.html#get-the-sources). + 2. Build tezos from the tezos repo with [these instructions](http://tezos.gitlab.io/mainnet/introduction/howtoget.html#build-from-sources). Depending on how you build it, you might need to prefix `./` to your commands, and the names of some of the binaries might be different. @@ -697,8 +667,7 @@ in the section on the baking app below. To delegate tez controlled by a Ledger device to someone else, you must first originate an account. Please read more -about this in the Tezos document, [How to bake on the -alphanet](http://doc.tzalpha.net/introduction/alphanet.html#how-to-bake-on-the-alphanet), to +about this in the Tezos documentation, [How to use Tezos](https://tezos.gitlab.io/master/introduction/howtouse.html), to understand why this is necessary and the semantics of delegation. To originate an account, the command is: @@ -706,7 +675,7 @@ To originate an account, the command is: $ tezos-client originate account for transferring from --delegatable ``` - * `NEW` is the alias you will now give to the originated account. Only originted accounts can + * `NEW` is the alias you will now give to the originated account. Only originated accounts can be delegated, and even then only if originated with this `--delegatable` flag. * `MGR` is the name of the key you will use to manage the account. If you want to manage it with a Ledger device, it should be an existing imported key from the Ledger hardware wallet. @@ -724,7 +693,7 @@ Originated accounts have names beginning with `KT1` rather than `tz1`, `tz2` or ### Proposals and Voting -To submit (or upvote) a proposal, open the Wallet app on your ledger and run +To submit (or upvote) a proposal during the Proposal Period, open the Wallet app on your ledger and run ``` $ tezos-client submit proposals for @@ -734,7 +703,7 @@ The Wallet app will then ask you to confirm the various details of the proposal **Note:** While `tezos-client` will let you submit multiple proposals at once with this command, submitting more than one will cause the Wallet app to show "Sign Unverified?" instead of showing each field of each proposal for your confirmation. Signing an operation that you can't confirm is not safe and it is highly recommended that you simply submit each proposal one at a time so you can properly confirm the fields on the ledger device. -Voting for a proposal also requires that you have the Wallet app open. You can then run +Voting for a proposal during the Exploration or Promotion Vote Period also requires that you have the Wallet app open. You can then run ``` $ tezos-client submit ballot for @@ -742,15 +711,17 @@ $ tezos-client submit ballot for The Wallet app will ask you to confirm the details of your vote. -Keep in mind that only registered delegate accounts can submit proposals and vote. Each account can submit up to 20 proposals per proposal period and vote only once per voting period. For a full description of how voting works, refer to the [Tezos documentation](https://gitlab.com/tezos/tezos/blob/master/docs/whitedoc/voting.rst). +Keep in mind that only registered delegate accounts can submit proposals and vote. Each account can submit up to 20 proposals per proposal period and vote only once per voting period. For a more detailed post on participating during each phase of the amendment process, see this [Medium post](https://medium.com/@obsidian.systems/voting-on-tezos-with-your-ledger-nano-s-8d75f8c1f076). For a full description of how voting works, refer to the [Tezos documentation](https://gitlab.com/tezos/tezos/blob/master/docs/whitedoc/voting.rst). ## Using the Tezos Baking Application -The Tezos Baking Application supports 3 operations: +The Tezos Baking Application supports the following operations: - 1. Authorize/get public key - 2. Reset high watermark - 3. Sign + 1. Get public key + 2. Setup ledger for baking + 3. Reset high watermark + 4. Get high watermark + 5. Sign (blocks and endorsements) It will only sign block headers and endorsements, as the purpose of the baking app is that it cannot be co-opted to perform other types of operations (like @@ -767,11 +738,9 @@ Wallet application on the same Ledger Nano S should suffice. ### Start the baking daemon ``` -$ tezos-baker-alpha run with local node ~/.tezos-node ledger_<...>_ed_0_0 +$ tezos-baker-003-PsddFKi3 run with local node ~/.tezos-node ledger_<...>_ed_0_0 ``` -Alternatively, it might be called `tezos-alpha-baker`. - This won't actually be able bake successfully yet until you run the rest of these setup steps. This will run indefinitely, so you might want to do it in a dedicated terminal or in a `tmux` or `screen` session. @@ -779,32 +748,28 @@ a dedicated terminal or in a `tmux` or `screen` session. You will also want to start the endorser and accuser daemons: ``` -$ tezos-endorser-alpha run ledger_<...>_ed_0_0 -$ tezos-accuser-alpha run +$ tezos-endorser-003-PsddFKi3 run ledger_<...>_ed_0_0 +$ tezos-accuser-003-PsddFKi3 run ``` -Alternatively, they might might be called `tezos-alpha-endorser` and `tezos-alpha-accuser`. Again, each of these will run indefinitely, and each should be in its own terminal `tmux`, or `screen` window. -### Authorize public key +*Note*: The binaries shown above all correspond to current Tezos mainnet protocol. When the Tezos protocol upgrades, the binaries shown above will update to, for instance, `tezos-baker-004-********`. + +### Setup ledger device to bake and endorse You need to run a specific command to authorize a key for baking. Once a key is authorized for baking, the user will not have to approve this command again. If a key is not authorized for baking, signing endorsements and block headers with that key will be rejected. This authorization data is persisted across runs of -the application. Only one key can be authorized for baking per Ledger hardware wallet at a +the application, but not across app installations. Only one key can be authorized for baking per Ledger hardware wallet at a time. -In order to authorize a public key for baking, you can do either of the -following: - - * Use the APDU for authorizing a public key. - - The command for this is as follows: +In order to authorize a public key for baking, use the APDU for setting up the ledger device to bake: ``` - $ tezos-client authorize ledger to bake for + $ tezos-client setup ledger to bake for ``` This only authorizes the key for baking on the Ledger Nano S, but does @@ -812,10 +777,10 @@ following: be necessary if you re-install the app, or if you have a different paired Ledger device that you are using to bake for the first time. - * Have the baking app sign a self-delegation. This is explained in the next section. - ### Registering as a Delegate +*Note: The ledger device will not sign this operation unless you have already setup the device to bake using the command in the previous section.* + In order to bake from the Ledger Nano S account you need to register the key as a delegate. This is formally done by delegating the account to itself. As a non-originated account, an account directly stored on the Ledger device can only @@ -824,41 +789,20 @@ delegate to itself. Open the Tezos Baking Application on the device, and then run this: ``` -$ tezos-client register key ledger_<...>_ed_0_0 as delegate +$ tezos-client register key as delegate ``` This command is intended to inform the blockchain itself of your intention to -bake with this key. It can be signed with either the Wallet App or the Baking -App, but if you sign it with the Baking App, it also implies to the Ledger Nano S -that you want to authorize that key for baking on the device as well. - -Authorizing a key for baking on a specific Ledger Nano S and authorizing it for -baking in general on the blockchain are two distinct authorizations. This -command will do both of them if signed with the appropriate baking app, -whereas the `authorize ledger` command in the previous section will -only do it for the in question, which is appropriate if it is already -authorized to bake on the blockchain. - -The `register key` command is equivalent to: - -``` -$ tezos-client set delegate for ledger_<...>_ed_0_0 to ledger_<...>_ed_0_0 -``` - -The Baking App only signs self-delegations; the Wallet App is needed to sign -delegations of originated accounts controlled by a hardware wallet. The Baking App -also only signs delegations with fees less than 0.05 ęś©; to sign those with -more, you must use the Wallet App to authorize baking for the blockchain, -and the command in the previous section to authorize baking for the Ledger Nano S, -which is always available as an alternative to signing this with the Baking App. +bake with this key. It can be signed with either Tezos Wallet or Tezos Baking, however +Tezos Baking can only sign self-delegations. ### Sign The sign operation is for signing block headers and endorsements. -Block headers must have monotonically strictly increasing levels; that is, each +Block headers must have monotonically increasing levels; that is, each block must have a higher level than all previous blocks signed with the Ledger device. -This is intended to prevent double baking at the device level, as a security +This is intended to prevent double baking and double endorsing at the device level, as a security measure against potential vulnerabilities where the computer might be tricked into double baking. This feature will hopefully be a redundant precaution, but it's implemented at the device level because the point of the Ledger hardware wallet is to not @@ -875,71 +819,63 @@ self-delegation). With the exception of self-delegations, as long as the key is configured and the high watermark constraint is followed, there is no user prompting required for -signing. The baking app will only ever sign without prompting or reject an -attempt at signing; this operation is designed to be used unsupervised. +signing. Tezos Baking will only ever sign without prompting or reject an +attempt at signing; this operation is designed to be used unsupervised. As mentioned, + the only exception to this is self-delegation. -As mentioned, the only exception to this is self-delegation. This will prompt -and display the public key hash of the account. This will authorize the key for -baking on the hardware wallet. This is independent of what the signed self-delegation -will then do on the blockchain. If you are baking on a new device, or have -reinstalled the app, you might have to sign a self-delegation to authorize the -key on the Ledger Nano S, even if you are already registered for baking on the -block-chain. This will also have to be done if you have previously signed this -with the wallet app. +### Reset High Watermark -### Reset +When updating the version of Tezos Baking you are using or if you are switching baking to + a new ledger device, we recommend setting the HWM to the current head block level of the blockchain. +This can be accomplished with the reset command. The following command requires an explicit +confirmation from the user: -There is some possibility that the HWM will need to be reset. This could be due -to the computer somehow being tricked into sending a block with a high level to -the Ledger Nano S (either due to an attack or a bug on the computer side), or because -the owner of the device wants to move from the real Tezos network to a test -network, or to a different test network. This can be accomplished with the reset -command. - -This is intended as a testing/debug tool and as a possible recovery from -attempted attacks or bugs, not as a day to day command. It requires an explicit -confirmation from the user, and there is no specific utility to send this -command. To send it manually, you can use the following command line: +``` +$ tezos-client set ledger high watermark for "ledger:///" to +``` -`tezos-client set ledger high watermark for "ledger:///" to ` +`` indicates the new high watermark to reset to. Both the main and test chain HWMs will be +simultaneously changed to this value. -`` indicates the new high watermark to reset to. If you are joining a new -test network, `0` is a fitting number, as then all blocks will be allowed again. -You can also set it to the most recently baked level on that test net. +If you would like to know the current high watermark of the ledger device, you can run: -If you are not physically present at your computer, but are curious what the -Ledger device's high watermark is, you can run: +``` +$ tezos-client get ledger high watermark for "ledger:///" +``` -`tezos-client get ledger high watermark for "ledger:///"` +While the ledger device's UI displays the HWM of the main chain it is signing on, it will not +display the HWM of a test chain it may be signing on during the 3rd period of the Tezos Amendment Process. +Running this command will return both HWMs as well as the chain ID of the main chain. ## Upgrading When you want to upgrade to a new version, whether you built it yourself from source or whether it's a new release of the `app.hex` files, use the same commands as you did to originally install it. As the keys are generated from the device's seeds and the -derivation paths, you will have the same keys with every version of this Ledger Nano S app, +derivation paths, you will have the same keys with every version of this Ledger hardware wallet app, so there is no need to re-import the keys with `tezos-client`. ### Special Upgrading Considerations for Bakers -If you've already been baking on an old version of the Baking App, the new version will +If you've already been baking on an old version of Tezos Baking, the new version will not remember which key you are baking with nor the High Watermark. You will have to re-run -this command to remind the hardware wallet what key you intend to authorize for baking: +this command to remind the hardware wallet what key you intend to authorize for baking. As shown, it can +also set the HWM: ``` -$ tezos-client authorize ledger to bake for +$ tezos-client setup ledger to bake for --main-hwm ``` -You can also set the High Watermark to the level of the most recently baked block with: +Alternatively, you can also set the High Watermark to the level of the most recently baked block with a separate command: ``` -tezos-client set ledger high watermark for "ledger:///" to +$ tezos-client set ledger high watermark for "ledger:///" to ``` -This will require the correct URL for the Ledger Nano S acquired from: +The later will require the correct URL for the Ledger device acquired from: ``` -tezos-client list connected ledgers +$ tezos-client list connected ledgers ``` ## Troubleshooting @@ -965,6 +901,10 @@ We need as much context as possible to help troubleshoot. If you run `script ` it opens a new shell where everything output and typed is also output to that file, giving you a transcript of your terminal session. +### Importing a Fundraiser Account to a Ledger Device + +You currently cannot directly import a fundraiser account to the Ledger device. Instead, you'll first need to import your fundraiser account to a non-hardware wallet address from which you can send the funds to an address on the ledger. You can do so with wallet providers such as [Galleon](https://galleon-wallet.tech/) or [TezBox](https://tezbox.com/). + ### Two Ledger Devices at the Same Time Two Ledger devices with the same seed should not ever be plugged in at the same time. This confuses @@ -1012,7 +952,7 @@ If the Ledger Nano S app crashes when you load it, there are two primary causes: the operation in question, cancel it from the device before pressing Ctrl-C, otherwise you might have to restart the Ledger Nano S. * Out of date firmware: If the Ledger Nano S app doesn't work at all, make sure you are running firmware - version 1.4.2. + version 1.5.5. ### Contact Us You can email us at tezos@obsidian.systems and request to join our Slack. diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..7ef131a7 --- /dev/null +++ b/default.nix @@ -0,0 +1,82 @@ +{ pkgs ? import nix/nixpkgs.nix {}, commit, ... }: +let + + fetchThunk = p: + if builtins.pathExists (p + /git.json) + then pkgs.fetchgit { inherit (builtins.fromJSON (builtins.readFile (p + /git.json))) url rev sha256; } + else if builtins.pathExists (p + /github.json) + then pkgs.fetchFromGitHub { inherit (builtins.fromJSON (builtins.readFile (p + /github.json))) owner repo rev sha256; } + else p; + + fhs = pkgs.callPackage nix/fhs.nix {}; + bolosEnv = pkgs.callPackage nix/bolos-env.nix {}; + bolosSdk = fetchThunk nix/dep/nanos-secure-sdk; + src = pkgs.lib.sources.sourceFilesBySuffices (pkgs.lib.sources.cleanSource ./.) [".c" ".h" ".gif" "Makefile"]; + + app = bakingApp: pkgs.runCommand "ledger-app-tezos-${if bakingApp then "baking" else "wallet"}" {} '' + set -Eeuo pipefail + + cp -a '${src}'/* . + chmod -R u+w . + + '${fhs}/bin/enter-fhs' <>>> Application size: <<<<" + '${pkgs.binutils-unwrapped}/bin/size' "$out/bin/app.elf" + ''; + + mkRelease = short_name: name: appDir: pkgs.runCommand "${short_name}-release-dir" {} '' + mkdir -p "$out" + + cp '${appDir + /bin/app.hex}' "$out/app.hex" + + cat > "$out/app.manifest" </dev/null) +echo >&2 "Git commit: $commit" +exec nix-build --no-out-link --argstr commit "$commit" "$@" diff --git a/nix/dep/nanos-secure-sdk/default.nix b/nix/dep/nanos-secure-sdk/default.nix new file mode 100644 index 00000000..7a047786 --- /dev/null +++ b/nix/dep/nanos-secure-sdk/default.nix @@ -0,0 +1,7 @@ +# DO NOT HAND-EDIT THIS FILE +import ((import {}).fetchFromGitHub ( + let json = builtins.fromJSON (builtins.readFile ./github.json); + in { inherit (json) owner repo rev sha256; + private = json.private or false; + } +)) diff --git a/nix/dep/nanos-secure-sdk/github.json b/nix/dep/nanos-secure-sdk/github.json new file mode 100644 index 00000000..1878dd54 --- /dev/null +++ b/nix/dep/nanos-secure-sdk/github.json @@ -0,0 +1,7 @@ +{ + "owner": "LedgerHQ", + "repo": "nanos-secure-sdk", + "branch": "master", + "rev": "f9e1c7b8904df2eee0ae7e603f552b876c169334", + "sha256": "1wzra32zkqw521a5cmvqsblpkbkfzgx2pbngqpymcgf8db20p50i" +} diff --git a/nix/env.sh b/nix/env.sh new file mode 100755 index 00000000..e5cdf57b --- /dev/null +++ b/nix/env.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +commit=$(git describe --tags --abbrev=8 --always --long --dirty 2>/dev/null) +echo >&2 "Git commit: $commit" +shell_dir="$(nix-build -A env-shell --no-out-link --argstr commit "$commit" "${NIX_BUILD_ARGS:-}")" +shell="$shell_dir/bin/env-shell" + +if [ $# -eq 0 ]; then + echo >&2 "Entering via $shell" + exec "$shell" +else + exec "$shell" <` to get the SHA256 hash of the contents.* +# 4. Update the URL and SHA256 values below. + +import (builtins.fetchTarball { + url = "https://releases.nixos.org/nixpkgs/nixpkgs-19.03pre167327.11cf7d6e1ff/nixexprs.tar.xz"; + sha256 = "0y0fs0j6pb9p9hzv1zagcavvpv50z2anqnbri6kq5iy1j4yqaric"; +}) diff --git a/nix/watch.sh b/nix/watch.sh new file mode 100755 index 00000000..35cd7bd8 --- /dev/null +++ b/nix/watch.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -uo pipefail + +root="$(git rev-parse --show-toplevel)" + +fail() { unset ___empty; : "${___empty:?$1}"; } + +[ -z "${1:-}" ] && fail "No command given; try running $0 make" + +watchdirs=("$root/default.nix" "$root/nix" "$root/Makefile" "$root/src") + +inotifywait="$(nix-build '' -A inotify-tools --no-out-link)/bin/inotifywait" +while true; do + "$root/nix/env.sh" < #include -unsigned int handle_apdu_error(uint8_t __attribute__((unused)) instruction) { +size_t provide_pubkey(uint8_t *const io_buffer, cx_ecfp_public_key_t const *const pubkey) { + size_t tx = 0; + io_buffer[tx++] = pubkey->W_len; + memmove(io_buffer + tx, pubkey->W, pubkey->W_len); + tx += pubkey->W_len; + return finalize_successful_send(tx); +} + +size_t handle_apdu_error(uint8_t __attribute__((unused)) instruction) { THROW(EXC_INVALID_INS); } -unsigned int handle_apdu_version(uint8_t __attribute__((unused)) instruction) { - int tx = 0; +size_t handle_apdu_version(uint8_t __attribute__((unused)) instruction) { memcpy(G_io_apdu_buffer, &version, sizeof(version_t)); - tx += sizeof(version_t); - G_io_apdu_buffer[tx++] = 0x90; - G_io_apdu_buffer[tx++] = 0x00; - return tx; + size_t tx = sizeof(version_t); + return finalize_successful_send(tx); } -unsigned int handle_apdu_git(uint8_t __attribute__((unused)) instruction) { - static char commit[] = COMMIT; +size_t handle_apdu_git(uint8_t __attribute__((unused)) instruction) { + static const char commit[] = COMMIT; memcpy(G_io_apdu_buffer, commit, sizeof(commit)); - - uint32_t tx = sizeof(commit); - G_io_apdu_buffer[tx++] = 0x90; - G_io_apdu_buffer[tx++] = 0x00; - return tx; + size_t tx = sizeof(commit); + return finalize_successful_send(tx); } #define CLA 0x80 __attribute__((noreturn)) -void main_loop(apdu_handler handlers[INS_MAX]) { - volatile uint32_t rx = io_exchange(CHANNEL_APDU, 0); +void main_loop(apdu_handler const *const handlers, size_t const handlers_size) { + volatile size_t rx = io_exchange(CHANNEL_APDU, 0); while (true) { BEGIN_TRY { TRY { + // Process APDU of size rx + if (rx == 0) { // no apdu received, well, reset the session, and reset the // bootloader configuration THROW(EXC_SECURITY); } - if (G_io_apdu_buffer[0] != CLA) { + if (G_io_apdu_buffer[OFFSET_CLA] != CLA) { THROW(EXC_CLASS); } @@ -53,14 +57,12 @@ void main_loop(apdu_handler handlers[INS_MAX]) { THROW(EXC_WRONG_LENGTH); } - uint8_t instruction = G_io_apdu_buffer[OFFSET_INS]; - apdu_handler cb; - if (instruction >= INS_MAX) { - cb = handle_apdu_error; - } else { - cb = handlers[instruction]; - } - uint32_t tx = cb(instruction); + uint8_t const instruction = G_io_apdu_buffer[OFFSET_INS]; + apdu_handler const cb = instruction >= handlers_size + ? handle_apdu_error + : handlers[instruction]; + + size_t const tx = cb(instruction); rx = io_exchange(CHANNEL_APDU, tx); } CATCH(ASYNC_EXCEPTION) { @@ -73,11 +75,13 @@ void main_loop(apdu_handler handlers[INS_MAX]) { sw = 0x6800 | (e & 0x7FF); // FALL THROUGH case 0x6000 ... 0x6FFF: - case 0x9000 ... 0x9FFF: - G_io_apdu_buffer[0] = sw >> 8; - G_io_apdu_buffer[1] = sw; - rx = io_exchange(CHANNEL_APDU, 2); - break; + case 0x9000 ... 0x9FFF: { + size_t tx = 0; + G_io_apdu_buffer[tx++] = sw >> 8; + G_io_apdu_buffer[tx++] = sw; + rx = io_exchange(CHANNEL_APDU, tx); + break; + } } } FINALLY { diff --git a/src/apdu.h b/src/apdu.h index f38c06bf..14233d04 100644 --- a/src/apdu.h +++ b/src/apdu.h @@ -2,8 +2,9 @@ #include "exception.h" #include "keys.h" - +#include "types.h" #include "ui.h" + #include "os.h" #include @@ -14,35 +15,47 @@ #endif #define OFFSET_CLA 0 -#define OFFSET_INS 1 -#define OFFSET_P1 2 +#define OFFSET_INS 1 // instruction code +#define OFFSET_P1 2 // user-defined 1-byte parameter #define OFFSET_CURVE 3 -#define OFFSET_LC 4 -#define OFFSET_CDATA 5 +#define OFFSET_LC 4 // length of CDATA +#define OFFSET_CDATA 5 // payload +// Instruction codes #define INS_VERSION 0x00 +#define INS_AUTHORIZE_BAKING 0x01 +#define INS_GET_PUBLIC_KEY 0x02 +#define INS_PROMPT_PUBLIC_KEY 0x03 +#define INS_SIGN 0x04 +#define INS_SIGN_UNSAFE 0x05 // Data that is already hashed. +#define INS_RESET 0x06 +#define INS_QUERY_AUTH_KEY 0x07 +#define INS_QUERY_MAIN_HWM 0x08 #define INS_GIT 0x09 -#define INS_MAX 0x0B - -// Return number of bytes to transmit (tx) -typedef uint32_t (*apdu_handler)(uint8_t instruction); +#define INS_SETUP 0x0A +#define INS_QUERY_ALL_HWM 0x0B +#define INS_DEAUTHORIZE 0x0C +#define INS_QUERY_AUTH_KEY_WITH_CURVE 0x0D __attribute__((noreturn)) -void main_loop(apdu_handler handlers[INS_MAX]); +void main_loop(apdu_handler const *const handlers, size_t const handlers_size); -static inline void return_ok(void) { - THROW(EXC_NO_ERROR); +static inline size_t finalize_successful_send(size_t tx) { + G_io_apdu_buffer[tx++] = 0x90; + G_io_apdu_buffer[tx++] = 0x00; + return tx; } // Send back response; do not restart the event loop -static inline void delayed_send(uint32_t tx) { +static inline void delayed_send(size_t tx) { io_exchange(CHANNEL_APDU | IO_RETURN_AFTER_TX, tx); } static inline bool delay_reject(void) { - G_io_apdu_buffer[0] = EXC_REJECT >> 8; - G_io_apdu_buffer[1] = EXC_REJECT & 0xFF; - delayed_send(2); + size_t tx = 0; + G_io_apdu_buffer[tx++] = EXC_REJECT >> 8; + G_io_apdu_buffer[tx++] = EXC_REJECT & 0xFF; + delayed_send(tx); return true; } @@ -52,16 +65,8 @@ static inline void require_hid(void) { } } -uint32_t handle_apdu_error(uint8_t instruction); -uint32_t handle_apdu_version(uint8_t instruction); -uint32_t handle_apdu_git(uint8_t instruction); +size_t provide_pubkey(uint8_t *const io_buffer, cx_ecfp_public_key_t const *const pubkey); -extern uint32_t app_stack_canary; - -extern void *stack_root; -static inline void throw_stack_size() { - uint8_t st; - // uint32_t tmp1 = (uint32_t)&st - (uint32_t)&app_stack_canary; - uint32_t tmp2 = (uint32_t)stack_root - (uint32_t)&st; - THROW(0x9000 + tmp2); -} +size_t handle_apdu_error(uint8_t instruction); +size_t handle_apdu_version(uint8_t instruction); +size_t handle_apdu_git(uint8_t instruction); diff --git a/src/apdu_baking.c b/src/apdu_baking.c index 1a62293a..665076d7 100644 --- a/src/apdu_baking.c +++ b/src/apdu_baking.c @@ -1,33 +1,31 @@ +#include "apdu_baking.h" + #include "apdu.h" #include "baking_auth.h" -#include "apdu_baking.h" -#include "cx.h" -#include "os.h" +#include "globals.h" +#include "os_cx.h" #include "protocol.h" #include "to_string.h" #include "ui_prompt.h" #include -static level_t reset_level; +#define G global.u.baking static bool reset_ok(void); -unsigned int handle_apdu_reset(__attribute__((unused)) uint8_t instruction) { +size_t handle_apdu_reset(__attribute__((unused)) uint8_t instruction) { uint8_t *dataBuffer = G_io_apdu_buffer + OFFSET_CDATA; uint32_t dataLength = G_io_apdu_buffer[OFFSET_LC]; if (dataLength != sizeof(int)) { THROW(EXC_WRONG_LENGTH_FOR_INS); } - level_t lvl = READ_UNALIGNED_BIG_ENDIAN(level_t, dataBuffer); - - if (!is_valid_level(lvl)) { - THROW(EXC_PARSE_ERROR); - } + level_t const lvl = READ_UNALIGNED_BIG_ENDIAN(level_t, dataBuffer); + if (!is_valid_level(lvl)) THROW(EXC_PARSE_ERROR); - reset_level = lvl; + G.reset_level = lvl; - number_to_string(get_value_buffer(0), reset_level); + register_ui_callback(0, number_to_string_indirect32, &G.reset_level); static const char *const reset_prompts[] = { PROMPT("Reset HWM"), @@ -37,18 +35,19 @@ unsigned int handle_apdu_reset(__attribute__((unused)) uint8_t instruction) { } bool reset_ok(void) { - write_highest_level(reset_level, false); // We have not yet had an endorsement at this level - - uint32_t tx = 0; - G_io_apdu_buffer[tx++] = 0x90; - G_io_apdu_buffer[tx++] = 0x00; + UPDATE_NVRAM(ram, { + ram->hwm.main.highest_level = G.reset_level; + ram->hwm.main.had_endorsement = false; + ram->hwm.test.highest_level = G.reset_level; + ram->hwm.test.had_endorsement = false; + }); // Send back the response, do not restart the event loop - delayed_send(tx); + delayed_send(finalize_successful_send(0)); return true; } -uint32_t send_word_big_endian(uint32_t tx, uint32_t word) { +size_t send_word_big_endian(size_t tx, uint32_t word) { char word_bytes[sizeof(word)]; memcpy(word_bytes, &word, sizeof(word)); @@ -62,28 +61,56 @@ uint32_t send_word_big_endian(uint32_t tx, uint32_t word) { return tx + i; } -unsigned int handle_apdu_hwm(__attribute__((unused)) uint8_t instruction) { - uint32_t tx = 0; - - level_t level = N_data.highest_level; - tx = send_word_big_endian(tx, level); +size_t handle_apdu_all_hwm(__attribute__((unused)) uint8_t instruction) { + size_t tx = 0; + tx = send_word_big_endian(tx, N_data.hwm.main.highest_level); + tx = send_word_big_endian(tx, N_data.hwm.test.highest_level); + tx = send_word_big_endian(tx, N_data.main_chain_id.v); + return finalize_successful_send(tx); +} - G_io_apdu_buffer[tx++] = 0x90; - G_io_apdu_buffer[tx++] = 0x00; - return tx; +size_t handle_apdu_main_hwm(__attribute__((unused)) uint8_t instruction) { + size_t tx = 0; + tx = send_word_big_endian(tx, N_data.hwm.main.highest_level); + return finalize_successful_send(tx); } -unsigned int handle_apdu_query_auth_key(__attribute__((unused)) uint8_t instruction) { - uint32_t tx = 0; - uint8_t length = N_data.path_length; +size_t handle_apdu_query_auth_key(__attribute__((unused)) uint8_t instruction) { + uint8_t const length = N_data.baking_key.bip32_path.length; + + size_t tx = 0; G_io_apdu_buffer[tx++] = length; for (uint8_t i = 0; i < length; ++i) { - tx = send_word_big_endian(tx, N_data.bip32_path[i]); + tx = send_word_big_endian(tx, N_data.baking_key.bip32_path.components[i]); + } + + return finalize_successful_send(tx); +} + +size_t handle_apdu_query_auth_key_with_curve(__attribute__((unused)) uint8_t instruction) { + uint8_t const curve_code = curve_to_curve_code(N_data.baking_key.curve); + if (curve_code == TEZOS_NO_CURVE) THROW(EXC_REFERENCED_DATA_NOT_FOUND); + + uint8_t const length = N_data.baking_key.bip32_path.length; + + size_t tx = 0; + G_io_apdu_buffer[tx++] = curve_code; + G_io_apdu_buffer[tx++] = length; + for (uint8_t i = 0; i < length; ++i) { + tx = send_word_big_endian(tx, N_data.baking_key.bip32_path.components[i]); } - G_io_apdu_buffer[tx++] = 0x90; - G_io_apdu_buffer[tx++] = 0x00; - return tx; + return finalize_successful_send(tx); +} + +size_t handle_apdu_deauthorize(__attribute__((unused)) uint8_t instruction) { + if (READ_UNALIGNED_BIG_ENDIAN(uint8_t, &G_io_apdu_buffer[OFFSET_P1]) != 0) THROW(EXC_WRONG_PARAM); + if (READ_UNALIGNED_BIG_ENDIAN(uint8_t, &G_io_apdu_buffer[OFFSET_LC]) != 0) THROW(EXC_PARSE_ERROR); + UPDATE_NVRAM(ram, { + memset(&ram->baking_key, 0, sizeof(ram->baking_key)); + }); + + return finalize_successful_send(0); } diff --git a/src/apdu_baking.h b/src/apdu_baking.h index 1d60df1e..afff5f32 100644 --- a/src/apdu_baking.h +++ b/src/apdu_baking.h @@ -1,11 +1,11 @@ #pragma once +#include #include -#define INS_RESET 0x06 -#define INS_QUERY_AUTH_KEY 0x07 -#define INS_QUERY_HWM 0x08 - -unsigned int handle_apdu_reset(uint8_t instruction); -unsigned int handle_apdu_query_auth_key(uint8_t instruction); -unsigned int handle_apdu_hwm(uint8_t instruction); +size_t handle_apdu_reset(uint8_t instruction); +size_t handle_apdu_query_auth_key(uint8_t instruction); +size_t handle_apdu_query_auth_key_with_curve(uint8_t instruction); +size_t handle_apdu_main_hwm(uint8_t instruction); +size_t handle_apdu_all_hwm(uint8_t instruction); +size_t handle_apdu_deauthorize(uint8_t instruction); diff --git a/src/apdu_pubkey.c b/src/apdu_pubkey.c index ffeecda9..3105fbe1 100644 --- a/src/apdu_pubkey.c +++ b/src/apdu_pubkey.c @@ -2,79 +2,59 @@ #include "apdu.h" #include "baking_auth.h" -#include "keys.h" - #include "cx.h" +#include "globals.h" +#include "keys.h" #include "ui.h" #include -static cx_ecfp_public_key_t public_key; -static cx_curve_t curve; - - -static int provide_pubkey(void) { - int tx = 0; - G_io_apdu_buffer[tx++] = public_key.W_len; - os_memmove(G_io_apdu_buffer + tx, - public_key.W, - public_key.W_len); - tx += public_key.W_len; - G_io_apdu_buffer[tx++] = 0x90; - G_io_apdu_buffer[tx++] = 0x00; - return tx; -} +#define G global.u.pubkey static bool pubkey_ok(void) { - int tx = provide_pubkey(); - delayed_send(tx); + delayed_send(provide_pubkey(G_io_apdu_buffer, &G.public_key)); return true; } #ifdef BAKING_APP static bool baking_ok(void) { - authorize_baking(curve, bip32_path, bip32_path_length); + authorize_baking(G.key.curve, &G.key.bip32_path); pubkey_ok(); return true; } #endif -unsigned int handle_apdu_get_public_key(uint8_t instruction) { +size_t handle_apdu_get_public_key(uint8_t instruction) { uint8_t *dataBuffer = G_io_apdu_buffer + OFFSET_CDATA; - if (G_io_apdu_buffer[OFFSET_P1] != 0) - THROW(EXC_WRONG_PARAM); + if (READ_UNALIGNED_BIG_ENDIAN(uint8_t, &G_io_apdu_buffer[OFFSET_P1]) != 0) THROW(EXC_WRONG_PARAM); - // do not expose pks without prompt over U2F - if (instruction == INS_GET_PUBLIC_KEY) { - require_hid(); - } + // do not expose pks without prompt over U2F (browser support) + if (instruction == INS_GET_PUBLIC_KEY) require_hid(); - curve = curve_code_to_curve(G_io_apdu_buffer[OFFSET_CURVE]); + uint8_t const curve_code = READ_UNALIGNED_BIG_ENDIAN(uint8_t, &G_io_apdu_buffer[OFFSET_CURVE]); + G.key.curve = curve_code_to_curve(curve_code); + + size_t const cdata_size = READ_UNALIGNED_BIG_ENDIAN(uint8_t, &G_io_apdu_buffer[OFFSET_LC]); #ifdef BAKING_APP - if (G_io_apdu_buffer[OFFSET_LC] == 0 && instruction == INS_AUTHORIZE_BAKING) { - curve = N_data.curve; - bip32_path_length = N_data.path_length; - memcpy(bip32_path, N_data.bip32_path, sizeof(*bip32_path) * bip32_path_length); + if (cdata_size == 0 && instruction == INS_AUTHORIZE_BAKING) { + copy_bip32_path_with_curve(&G.key, &N_data.baking_key); } else { #endif - bip32_path_length = read_bip32_path(G_io_apdu_buffer[OFFSET_LC], bip32_path, dataBuffer); + read_bip32_path(&G.key.bip32_path, dataBuffer, cdata_size); #ifdef BAKING_APP - if (bip32_path_length == 0) { - THROW(EXC_WRONG_LENGTH_FOR_INS); - } + if (G.key.bip32_path.length == 0) THROW(EXC_WRONG_LENGTH_FOR_INS); } #endif - struct key_pair *pair = generate_key_pair(curve, bip32_path_length, bip32_path); - os_memset(&pair->private_key, 0, sizeof(pair->private_key)); - memcpy(&public_key, &pair->public_key, sizeof(public_key)); + cx_ecfp_public_key_t const *const pubkey = generate_public_key(G.key.curve, &G.key.bip32_path); + memcpy(&G.public_key, pubkey, sizeof(G.public_key)); if (instruction == INS_GET_PUBLIC_KEY) { - return provide_pubkey(); + return provide_pubkey(G_io_apdu_buffer, &G.public_key); } else { // instruction == INS_PROMPT_PUBLIC_KEY || instruction == INS_AUTHORIZE_BAKING - callback_t cb; + ui_callback_t cb; bool bake; #ifdef BAKING_APP if (instruction == INS_AUTHORIZE_BAKING) { @@ -88,6 +68,6 @@ unsigned int handle_apdu_get_public_key(uint8_t instruction) { #ifdef BAKING_APP } #endif - prompt_address(bake, curve, &public_key, cb, delay_reject); + prompt_address(bake, G.key.curve, &G.public_key, cb, delay_reject); } } diff --git a/src/apdu_pubkey.h b/src/apdu_pubkey.h index ebda2972..ac58d796 100644 --- a/src/apdu_pubkey.h +++ b/src/apdu_pubkey.h @@ -2,8 +2,4 @@ #include "apdu.h" -#define INS_AUTHORIZE_BAKING 0x01 -#define INS_GET_PUBLIC_KEY 0x02 -#define INS_PROMPT_PUBLIC_KEY 0x03 - -unsigned int handle_apdu_get_public_key(uint8_t instruction); +size_t handle_apdu_get_public_key(uint8_t instruction); diff --git a/src/apdu_setup.c b/src/apdu_setup.c new file mode 100644 index 00000000..bcc9ac30 --- /dev/null +++ b/src/apdu_setup.c @@ -0,0 +1,89 @@ +#include "apdu_setup.h" + +#include "apdu.h" +#include "cx.h" +#include "globals.h" +#include "keys.h" +#include "to_string.h" +#include "ui_prompt.h" +#include "ui.h" + +#include + +#define G global.u.setup + +struct setup_wire { + uint32_t main_chain_id; + struct { + uint32_t main; + uint32_t test; + } hwm; + struct bip32_path_wire bip32_path; +} __attribute__((packed)); + +static bool ok(void) { + UPDATE_NVRAM(ram, { + copy_bip32_path_with_curve(&ram->baking_key, &G.key); + ram->main_chain_id = G.main_chain_id; + ram->hwm.main.highest_level = G.hwm.main; + ram->hwm.main.had_endorsement = false; + ram->hwm.test.highest_level = G.hwm.test; + ram->hwm.test.had_endorsement = false; + }); + + cx_ecfp_public_key_t const *const pubkey = generate_public_key(G.key.curve, &G.key.bip32_path); + delayed_send(provide_pubkey(G_io_apdu_buffer, pubkey)); + return true; +} + +__attribute__((noreturn)) static void prompt_setup( + ui_callback_t const ok_cb, + ui_callback_t const cxl_cb) +{ + static const size_t TYPE_INDEX = 0; + static const size_t ADDRESS_INDEX = 1; + static const size_t CHAIN_INDEX = 2; + static const size_t MAIN_HWM_INDEX = 3; + static const size_t TEST_HWM_INDEX = 4; + + static const char *const prompts[] = { + PROMPT("Setup"), + PROMPT("Address"), + PROMPT("Chain"), + PROMPT("Main Chain HWM"), + PROMPT("Test Chain HWM"), + NULL, + }; + + REGISTER_STATIC_UI_VALUE(TYPE_INDEX, "Baking?"); + register_ui_callback(ADDRESS_INDEX, bip32_path_with_curve_to_pkh_string, &G.key); + register_ui_callback(CHAIN_INDEX, chain_id_to_string_with_aliases, &G.main_chain_id); + register_ui_callback(MAIN_HWM_INDEX, number_to_string_indirect32, &G.hwm.main); + register_ui_callback(TEST_HWM_INDEX, number_to_string_indirect32, &G.hwm.test); + + ui_prompt(prompts, NULL, ok_cb, cxl_cb); +} + +__attribute__((noreturn)) size_t handle_apdu_setup(__attribute__((unused)) uint8_t instruction) { + if (READ_UNALIGNED_BIG_ENDIAN(uint8_t, &G_io_apdu_buffer[OFFSET_P1]) != 0) THROW(EXC_WRONG_PARAM); + + uint32_t const buff_size = READ_UNALIGNED_BIG_ENDIAN(uint8_t, &G_io_apdu_buffer[OFFSET_LC]); + if (buff_size < sizeof(struct setup_wire)) THROW(EXC_WRONG_LENGTH_FOR_INS); + + uint8_t const curve_code = READ_UNALIGNED_BIG_ENDIAN(uint8_t, &G_io_apdu_buffer[OFFSET_CURVE]); + G.key.curve = curve_code_to_curve(curve_code); + + { + struct setup_wire const *const buff_as_setup = (struct setup_wire const *)&G_io_apdu_buffer[OFFSET_CDATA]; + + size_t consumed = 0; + G.main_chain_id.v = CONSUME_UNALIGNED_BIG_ENDIAN(consumed, uint32_t, (uint8_t const *)&buff_as_setup->main_chain_id); + G.hwm.main = CONSUME_UNALIGNED_BIG_ENDIAN(consumed, uint32_t, (uint8_t const *)&buff_as_setup->hwm.main); + G.hwm.test = CONSUME_UNALIGNED_BIG_ENDIAN(consumed, uint32_t, (uint8_t const *)&buff_as_setup->hwm.test); + consumed += read_bip32_path(&G.key.bip32_path, (uint8_t const *)&buff_as_setup->bip32_path, buff_size - consumed); + + if (consumed != buff_size) THROW(EXC_WRONG_LENGTH); + } + + prompt_setup(ok, delay_reject); +} diff --git a/src/apdu_setup.h b/src/apdu_setup.h new file mode 100644 index 00000000..fd2d2d0a --- /dev/null +++ b/src/apdu_setup.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +size_t handle_apdu_setup(uint8_t instruction); diff --git a/src/apdu_sign.c b/src/apdu_sign.c index f7110907..6ad6d8ce 100644 --- a/src/apdu_sign.c +++ b/src/apdu_sign.c @@ -4,91 +4,72 @@ #include "baking_auth.h" #include "base58.h" #include "blake2.h" +#include "globals.h" #include "keys.h" +#include "memory.h" #include "protocol.h" #include "to_string.h" +#include "ui.h" #include "ui_prompt.h" +// Order matters #include "cx.h" -#include "ui.h" #include -// Where does this number come from? -#ifdef BAKING_APP -# define TEZOS_BUFSIZE 512 -#else -# define TEZOS_BUFSIZE 256 -#endif +#define G global.u.sign #define SIGN_HASH_SIZE 32 #define B2B_BLOCKBYTES 128 -static uint8_t message_data[TEZOS_BUFSIZE]; -static uint32_t message_data_length; -static cx_curve_t curve; - -static bool is_hash_state_inited; -static uint8_t magic_number; -static bool hash_only; - static void conditional_init_hash_state(void) { - if (!is_hash_state_inited) { - b2b_init(&hash_state, SIGN_HASH_SIZE); - is_hash_state_inited = true; + if (!G.is_hash_state_inited) { + b2b_init(&global.blake2b.hash_state, SIGN_HASH_SIZE); + G.is_hash_state_inited = true; } } static void hash_buffer(void) { - const uint8_t *current = message_data; - while (message_data_length > B2B_BLOCKBYTES) { + const uint8_t *current = G.message_data; + while (G.message_data_length > B2B_BLOCKBYTES) { conditional_init_hash_state(); - b2b_update(&hash_state, current, B2B_BLOCKBYTES); - message_data_length -= B2B_BLOCKBYTES; + b2b_update(&global.blake2b.hash_state, current, B2B_BLOCKBYTES); + G.message_data_length -= B2B_BLOCKBYTES; current += B2B_BLOCKBYTES; } // TODO use circular buffer at some point - memmove(message_data, current, message_data_length); + memmove(G.message_data, current, G.message_data_length); } static void finish_hashing(uint8_t *hash, size_t hash_size) { hash_buffer(); conditional_init_hash_state(); - b2b_update(&hash_state, message_data, message_data_length); - b2b_final(&hash_state, hash, hash_size); - message_data_length = 0; - is_hash_state_inited = false; + b2b_update(&global.blake2b.hash_state, G.message_data, G.message_data_length); + b2b_final(&global.blake2b.hash_state, hash, hash_size); + G.message_data_length = 0; + G.is_hash_state_inited = false; } static int perform_signature(bool hash_first); -#ifdef BAKING_APP -static bool bake_auth_ok(void) { - authorize_baking(curve, bip32_path, bip32_path_length); - int tx = perform_signature(true); - delayed_send(tx); - return true; -} -#else static bool sign_ok(void) { - int tx = perform_signature(true); - delayed_send(tx); + delayed_send(perform_signature(true)); return true; } +#ifndef BAKING_APP static bool sign_unsafe_ok(void) { - int tx = perform_signature(false); - delayed_send(tx); + delayed_send(perform_signature(false)); return true; } #endif static void clear_data(void) { - bip32_path_length = 0; - message_data_length = 0; - is_hash_state_inited = false; - magic_number = 0; - hash_only = false; + G.key.bip32_path.length = 0; + G.message_data_length = 0; + G.is_hash_state_inited = false; + G.magic_number = 0; + G.hash_only = false; } static bool sign_reject(void) { @@ -98,13 +79,37 @@ static bool sign_reject(void) { } #ifdef BAKING_APP + +__attribute__((noreturn)) static void prompt_register_delegate( + ui_callback_t const ok_cb, + ui_callback_t const cxl_cb +) { + static const size_t TYPE_INDEX = 0; + static const size_t ADDRESS_INDEX = 1; + static const size_t FEE_INDEX = 2; + + static const char *const prompts[] = { + PROMPT("Register"), + PROMPT("Address"), + PROMPT("Fee"), + NULL, + }; + + REGISTER_STATIC_UI_VALUE(TYPE_INDEX, "as delegate?"); + register_ui_callback(ADDRESS_INDEX, bip32_path_with_curve_to_pkh_string, &G.key); + register_ui_callback(FEE_INDEX, microtez_to_string_indirect, &G.ops.total_fee); + + ui_prompt(prompts, NULL, ok_cb, cxl_cb); +} + uint32_t baking_sign_complete(void) { // Have raw data, can get insight into it - switch (magic_number) { + switch (G.magic_number) { case MAGIC_BYTE_BLOCK: case MAGIC_BYTE_BAKING_OP: - guard_baking_authorized(curve, message_data, message_data_length, - bip32_path, bip32_path_length); + guard_baking_authorized( + G.key.curve, G.message_data, G.message_data_length, + &G.key.bip32_path); return perform_signature(true); case MAGIC_BYTE_UNSAFE_OP: @@ -115,22 +120,21 @@ uint32_t baking_sign_complete(void) { allow_operation(&allowed, OPERATION_TAG_DELEGATION); allow_operation(&allowed, OPERATION_TAG_REVEAL); - struct parsed_operation_group *ops = - parse_operations(message_data, message_data_length, curve, - bip32_path_length, bip32_path, allowed); + parse_operations( + &G.ops, + G.message_data, G.message_data_length, + G.key.curve, &G.key.bip32_path, allowed); - // With < nickel fee - if (ops->total_fee > 50000) THROW(EXC_PARSE_ERROR); + // Must be self-delegation signed by the *authorized* baking key + if (bip32_path_with_curve_eq(&G.key, &N_data.baking_key) && - // Must be self-delegation signed by the same key - if (memcmp(&ops->operation.source, &ops->signing, sizeof(ops->signing))) { - THROW(EXC_PARSE_ERROR); - } - if (memcmp(&ops->operation.destination, &ops->signing, sizeof(ops->signing))) { - THROW(EXC_PARSE_ERROR); + // ops->signing is generated from G.bip32_path and G.curve + COMPARE(&G.ops.operation.source, &G.ops.signing) == 0 && + COMPARE(&G.ops.operation.destination, &G.ops.signing) == 0 + ) { + prompt_register_delegate(sign_ok, sign_reject); } - - prompt_contract_for_baking(&ops->signing, bake_auth_ok, sign_reject); + THROW(EXC_SECURITY); } case MAGIC_BYTE_UNSAFE_OP2: case MAGIC_BYTE_UNSAFE_OP3: @@ -150,14 +154,14 @@ const char *const insecure_values[] = { #define MAX_NUMBER_CHARS (MAX_INT_DIGITS + 2) // include decimal point and terminating null -#define SET_STATIC_UI_VALUE(index, str) strcpy(get_value_buffer(index), STATIC_UI_VALUE(str)) - // Return false if the transaction isn't easily parseable, otherwise prompt with given callbacks // and do not return, but rather throw ASYNC. static bool prompt_transaction(const void *data, size_t length, cx_curve_t curve, - size_t path_length, uint32_t *bip32_path, - callback_t ok, callback_t cxl) { - struct parsed_operation_group *ops; + bip32_path_t const *const bip32_path, + ui_callback_t ok, ui_callback_t cxl) { + check_null(data); + check_null(bip32_path); + struct parsed_operation_group *const ops = &G.ops; #ifndef TEZOS_DEBUG BEGIN_TRY { // TODO: Eventually, "unsafe" operations will be another APDU, @@ -176,7 +180,7 @@ static bool prompt_transaction(const void *data, size_t length, cx_curve_t curve allow_operation(&allowed, OPERATION_TAG_TRANSACTION); // TODO: Add still other operations - ops = parse_operations(data, length, curve, path_length, bip32_path, allowed); + parse_operations(ops, data, length, curve, bip32_path, allowed); #ifndef TEZOS_DEBUG } CATCH_OTHER(e) { @@ -208,15 +212,11 @@ static bool prompt_transaction(const void *data, size_t length, cx_curve_t curve NULL, }; - if (!parsed_contract_to_string(get_value_buffer(SOURCE_INDEX), VALUE_WIDTH, - &ops->operation.source)) return false; - if (!number_to_string(get_value_buffer(PERIOD_INDEX), - ops->operation.proposal.voting_period)) return false; - - if (!protocol_hash_to_string(get_value_buffer(PROTOCOL_HASH_INDEX), VALUE_WIDTH, - ops->operation.proposal.protocol_hash)) return false; + register_ui_callback(SOURCE_INDEX, parsed_contract_to_string, &ops->operation.source); + register_ui_callback(PERIOD_INDEX, number_to_string_indirect32, &ops->operation.proposal.voting_period); + register_ui_callback(PROTOCOL_HASH_INDEX, protocol_hash_to_string, ops->operation.proposal.protocol_hash); - SET_STATIC_UI_VALUE(TYPE_INDEX, "Proposal"); + REGISTER_STATIC_UI_VALUE(TYPE_INDEX, "Proposal"); ui_prompt(proposal_prompts, NULL, ok, cxl); } @@ -235,22 +235,20 @@ static bool prompt_transaction(const void *data, size_t length, cx_curve_t curve NULL, }; - if (!parsed_contract_to_string(get_value_buffer(SOURCE_INDEX), VALUE_WIDTH, - &ops->operation.source)) return false; - if (!protocol_hash_to_string(get_value_buffer(PROTOCOL_HASH_INDEX), VALUE_WIDTH, - ops->operation.ballot.protocol_hash)) return false; - if (!number_to_string(get_value_buffer(PERIOD_INDEX), - ops->operation.ballot.voting_period)) return false; + register_ui_callback(SOURCE_INDEX, parsed_contract_to_string, &ops->operation.source); + register_ui_callback(PROTOCOL_HASH_INDEX, protocol_hash_to_string, + ops->operation.ballot.protocol_hash); + register_ui_callback(PERIOD_INDEX, number_to_string_indirect32, &ops->operation.ballot.voting_period); switch (ops->operation.ballot.vote) { case BALLOT_VOTE_YEA: - SET_STATIC_UI_VALUE(TYPE_INDEX, "Yea"); + REGISTER_STATIC_UI_VALUE(TYPE_INDEX, "Yea"); break; case BALLOT_VOTE_NAY: - SET_STATIC_UI_VALUE(TYPE_INDEX, "Nay"); + REGISTER_STATIC_UI_VALUE(TYPE_INDEX, "Nay"); break; case BALLOT_VOTE_PASS: - SET_STATIC_UI_VALUE(TYPE_INDEX, "Pass"); + REGISTER_STATIC_UI_VALUE(TYPE_INDEX, "Pass"); break; } @@ -267,12 +265,12 @@ static bool prompt_transaction(const void *data, size_t length, cx_curve_t curve static const uint32_t DELEGATE_INDEX = 5; static const uint32_t STORAGE_INDEX = 6; - if (!parsed_contract_to_string(get_value_buffer(SOURCE_INDEX), VALUE_WIDTH, - &ops->operation.source)) return false; - if (!parsed_contract_to_string(get_value_buffer(DESTINATION_INDEX), VALUE_WIDTH, - &ops->operation.destination)) return false; - microtez_to_string(get_value_buffer(FEE_INDEX), ops->total_fee); - number_to_string(get_value_buffer(STORAGE_INDEX), ops->total_storage_limit); + register_ui_callback(SOURCE_INDEX, parsed_contract_to_string, &ops->operation.source); + register_ui_callback(DESTINATION_INDEX, parsed_contract_to_string, + &ops->operation.destination); + register_ui_callback(FEE_INDEX, microtez_to_string_indirect, &ops->total_fee); + register_ui_callback(STORAGE_INDEX, number_to_string_indirect64, + &ops->total_storage_limit); static const char *const origination_prompts_fixed[] = { PROMPT("Confirm"), @@ -307,26 +305,26 @@ static bool prompt_transaction(const void *data, size_t length, cx_curve_t curve if (!(ops->operation.flags & ORIGINATION_FLAG_SPENDABLE)) return false; - SET_STATIC_UI_VALUE(TYPE_INDEX, "Origination"); - microtez_to_string(get_value_buffer(AMOUNT_INDEX), ops->operation.amount); + REGISTER_STATIC_UI_VALUE(TYPE_INDEX, "Origination"); + register_ui_callback(AMOUNT_INDEX, microtez_to_string_indirect, &ops->operation.amount); const char *const *prompts; bool delegatable = ops->operation.flags & ORIGINATION_FLAG_DELEGATABLE; bool has_delegate = ops->operation.delegate.curve_code != TEZOS_NO_CURVE; if (delegatable && has_delegate) { prompts = origination_prompts_delegatable; - if (!parsed_contract_to_string(get_value_buffer(DELEGATE_INDEX), VALUE_WIDTH, - &ops->operation.delegate)) return false; + register_ui_callback(DELEGATE_INDEX, parsed_contract_to_string, + &ops->operation.delegate); } else if (delegatable && !has_delegate) { prompts = origination_prompts_delegatable; - SET_STATIC_UI_VALUE(DELEGATE_INDEX, "Any"); + REGISTER_STATIC_UI_VALUE(DELEGATE_INDEX, "Any"); } else if (!delegatable && has_delegate) { prompts = origination_prompts_fixed; - if (!parsed_contract_to_string(get_value_buffer(DELEGATE_INDEX), VALUE_WIDTH, - &ops->operation.delegate)) return false; + register_ui_callback(DELEGATE_INDEX, parsed_contract_to_string, + &ops->operation.delegate); } else if (!delegatable && !has_delegate) { prompts = origination_prompts_undelegatable; - SET_STATIC_UI_VALUE(DELEGATE_INDEX, "Disabled"); + REGISTER_STATIC_UI_VALUE(DELEGATE_INDEX, "Disabled"); } ui_prompt(prompts, NULL, ok, cxl); @@ -339,12 +337,13 @@ static bool prompt_transaction(const void *data, size_t length, cx_curve_t curve static const uint32_t DESTINATION_INDEX = 3; static const uint32_t STORAGE_INDEX = 4; - if (!parsed_contract_to_string(get_value_buffer(SOURCE_INDEX), VALUE_WIDTH, - &ops->operation.source)) return false; - if (!parsed_contract_to_string(get_value_buffer(DESTINATION_INDEX), VALUE_WIDTH, - &ops->operation.destination)) return false; - microtez_to_string(get_value_buffer(FEE_INDEX), ops->total_fee); - number_to_string(get_value_buffer(STORAGE_INDEX), ops->total_storage_limit); + register_ui_callback(SOURCE_INDEX, parsed_contract_to_string, + &ops->operation.source); + register_ui_callback(DESTINATION_INDEX, parsed_contract_to_string, + &ops->operation.destination); + register_ui_callback(FEE_INDEX, microtez_to_string_indirect, &ops->total_fee); + register_ui_callback(STORAGE_INDEX, number_to_string_indirect64, + &ops->total_storage_limit); static const char *const withdrawal_prompts[] = { PROMPT("Withdraw"), @@ -363,7 +362,7 @@ static bool prompt_transaction(const void *data, size_t length, cx_curve_t curve NULL, }; - SET_STATIC_UI_VALUE(TYPE_INDEX, "Delegation"); + REGISTER_STATIC_UI_VALUE(TYPE_INDEX, "Delegation"); bool withdrawal = ops->operation.destination.originated == 0 && ops->operation.destination.curve_code == TEZOS_NO_CURVE; @@ -390,16 +389,15 @@ static bool prompt_transaction(const void *data, size_t length, cx_curve_t curve NULL, }; - if (!parsed_contract_to_string(get_value_buffer(SOURCE_INDEX), VALUE_WIDTH, - &ops->operation.source)) return false; - if (!parsed_contract_to_string(get_value_buffer(DESTINATION_INDEX), VALUE_WIDTH, - &ops->operation.destination)) return false; - microtez_to_string(get_value_buffer(FEE_INDEX), ops->total_fee); - number_to_string(get_value_buffer(STORAGE_INDEX), ops->total_storage_limit); - - SET_STATIC_UI_VALUE(TYPE_INDEX, "Transaction"); + register_ui_callback(SOURCE_INDEX, parsed_contract_to_string, &ops->operation.source); + register_ui_callback(DESTINATION_INDEX, parsed_contract_to_string, + &ops->operation.destination); + register_ui_callback(FEE_INDEX, microtez_to_string_indirect, &ops->total_fee); + register_ui_callback(STORAGE_INDEX, number_to_string_indirect64, + &ops->total_storage_limit); + register_ui_callback(AMOUNT_INDEX, microtez_to_string_indirect, &ops->operation.amount); - microtez_to_string(get_value_buffer(AMOUNT_INDEX), ops->operation.amount); + REGISTER_STATIC_UI_VALUE(TYPE_INDEX, "Transaction"); ui_prompt(transaction_prompts, NULL, ok, cxl); } @@ -410,10 +408,8 @@ static bool prompt_transaction(const void *data, size_t length, cx_curve_t curve static const uint32_t FEE_INDEX = 2; static const uint32_t STORAGE_INDEX = 3; - if (!parsed_contract_to_string(get_value_buffer(SOURCE_INDEX), VALUE_WIDTH, - &ops->operation.source)) return false; - - microtez_to_string(get_value_buffer(FEE_INDEX), ops->total_fee); + register_ui_callback(SOURCE_INDEX, parsed_contract_to_string, &ops->operation.source); + register_ui_callback(FEE_INDEX, microtez_to_string_indirect, &ops->total_fee); // Parser function guarantees this has a reveal static const char *const reveal_prompts[] = { @@ -424,10 +420,10 @@ static bool prompt_transaction(const void *data, size_t length, cx_curve_t curve NULL, }; - SET_STATIC_UI_VALUE(TYPE_INDEX, "To Blockchain"); - number_to_string(get_value_buffer(STORAGE_INDEX), ops->total_storage_limit); - if (!parsed_contract_to_string(get_value_buffer(SOURCE_INDEX), VALUE_WIDTH, - &ops->operation.source)) return false; + REGISTER_STATIC_UI_VALUE(TYPE_INDEX, "To Blockchain"); + register_ui_callback(STORAGE_INDEX, number_to_string_indirect64, + &ops->total_storage_limit); + register_ui_callback(SOURCE_INDEX, parsed_contract_to_string, &ops->operation.source); ui_prompt(reveal_prompts, NULL, ok, cxl); } @@ -449,17 +445,18 @@ uint32_t wallet_sign_complete(uint8_t instruction) { }; ui_prompt(prehashed_prompts, insecure_values, sign_unsafe_ok, sign_reject); } else { - switch (magic_number) { + switch (G.magic_number) { case MAGIC_BYTE_BLOCK: case MAGIC_BYTE_BAKING_OP: default: THROW(EXC_PARSE_ERROR); case MAGIC_BYTE_UNSAFE_OP: - if (is_hash_state_inited) { + if (G.is_hash_state_inited) { goto unsafe; } - if (!prompt_transaction(message_data, message_data_length, curve, - bip32_path_length, bip32_path, sign_ok, sign_reject)) { + if (!prompt_transaction( + G.message_data, G.message_data_length, G.key.curve, + &G.key.bip32_path, sign_ok, sign_reject)) { goto unsafe; } case MAGIC_BYTE_UNSAFE_OP2: @@ -477,7 +474,7 @@ uint32_t wallet_sign_complete(uint8_t instruction) { #define P1_HASH_ONLY_NEXT 0x03 // You only need it once #define P1_LAST_MARKER 0x80 -unsigned int handle_apdu_sign(uint8_t instruction) { +size_t handle_apdu_sign(uint8_t instruction) { uint8_t p1 = G_io_apdu_buffer[OFFSET_P1]; uint8_t *dataBuffer = G_io_apdu_buffer + OFFSET_CDATA; uint32_t dataLength = G_io_apdu_buffer[OFFSET_LC]; @@ -486,19 +483,19 @@ unsigned int handle_apdu_sign(uint8_t instruction) { switch (p1 & ~P1_LAST_MARKER) { case P1_FIRST: clear_data(); - os_memset(message_data, 0, sizeof(message_data)); - message_data_length = 0; - bip32_path_length = read_bip32_path(dataLength, bip32_path, dataBuffer); - curve = curve_code_to_curve(G_io_apdu_buffer[OFFSET_CURVE]); - return_ok(); + memset(G.message_data, 0, sizeof(G.message_data)); + G.message_data_length = 0; + read_bip32_path(&G.key.bip32_path, dataBuffer, dataLength); + G.key.curve = curve_code_to_curve(G_io_apdu_buffer[OFFSET_CURVE]); + return finalize_successful_send(0); #ifndef BAKING_APP case P1_HASH_ONLY_NEXT: // This is a debugging Easter egg - hash_only = true; + G.hash_only = true; // FALL THROUGH #endif case P1_NEXT: - if (bip32_path_length == 0) { + if (G.key.bip32_path.length == 0) { THROW(EXC_WRONG_LENGTH_FOR_INS); } break; @@ -506,8 +503,8 @@ unsigned int handle_apdu_sign(uint8_t instruction) { THROW(EXC_WRONG_PARAM); } - if (instruction != INS_SIGN_UNSAFE && magic_number == 0) { - magic_number = get_magic_byte(dataBuffer, dataLength); + if (instruction != INS_SIGN_UNSAFE && G.magic_number == 0) { + G.magic_number = get_magic_byte(dataBuffer, dataLength); } #ifndef BAKING_APP @@ -516,15 +513,15 @@ unsigned int handle_apdu_sign(uint8_t instruction) { } #endif - if (message_data_length + dataLength > sizeof(message_data)) { + if (G.message_data_length + dataLength > sizeof(G.message_data)) { THROW(EXC_PARSE_ERROR); } - os_memmove(message_data + message_data_length, dataBuffer, dataLength); - message_data_length += dataLength; + os_memmove(G.message_data + G.message_data_length, dataBuffer, dataLength); + G.message_data_length += dataLength; if (!last) { - return_ok(); + return finalize_successful_send(0); } #ifdef BAKING_APP @@ -536,11 +533,11 @@ unsigned int handle_apdu_sign(uint8_t instruction) { static int perform_signature(bool hash_first) { uint8_t hash[SIGN_HASH_SIZE]; - uint8_t *data = message_data; - uint32_t datalen = message_data_length; + uint8_t *data = G.message_data; + uint32_t datalen = G.message_data_length; #ifdef BAKING_APP - update_high_water_mark(message_data, message_data_length); + update_high_water_mark(G.message_data, G.message_data_length); #endif if (hash_first) { @@ -550,21 +547,18 @@ static int perform_signature(bool hash_first) { datalen = SIGN_HASH_SIZE; #ifndef BAKING_APP - if (hash_only) { + if (G.hash_only) { memcpy(G_io_apdu_buffer, data, datalen); - uint32_t tx = datalen; - - G_io_apdu_buffer[tx++] = 0x90; - G_io_apdu_buffer[tx++] = 0x00; - return tx; + size_t tx = datalen; + return finalize_successful_send(tx); } #endif } - struct key_pair *pair = generate_key_pair(curve, bip32_path_length, bip32_path); + struct key_pair *const pair = generate_key_pair(G.key.curve, &G.key.bip32_path); - uint32_t tx; - switch (curve) { + size_t tx = 0; + switch (G.key.curve) { case CX_CURVE_Ed25519: { tx = cx_eddsa_sign(&pair->private_key, 0, @@ -599,12 +593,9 @@ static int perform_signature(bool hash_first) { THROW(EXC_WRONG_PARAM); // This should not be able to happen. } - os_memset(&pair->private_key, 0, sizeof(pair->private_key)); - - G_io_apdu_buffer[tx++] = 0x90; - G_io_apdu_buffer[tx++] = 0x00; + memset(&pair->private_key, 0, sizeof(pair->private_key)); clear_data(); - return tx; + return finalize_successful_send(tx); } diff --git a/src/apdu_sign.h b/src/apdu_sign.h index dac7a31f..8c3b8583 100644 --- a/src/apdu_sign.h +++ b/src/apdu_sign.h @@ -2,7 +2,4 @@ #include "apdu.h" -#define INS_SIGN 0x04 -#define INS_SIGN_UNSAFE 0x05 // Data that is already hashed. - -unsigned int handle_apdu_sign(uint8_t instruction); +size_t handle_apdu_sign(uint8_t instruction); diff --git a/src/baking_auth.c b/src/baking_auth.c index d2d7a401..cb8b78c2 100644 --- a/src/baking_auth.c +++ b/src/baking_auth.c @@ -1,97 +1,84 @@ -#include "apdu.h" #include "baking_auth.h" + +#include "apdu.h" +#include "globals.h" #include "keys.h" +#include "memory.h" #include "protocol.h" -#include "ui_prompt.h" #include "to_string.h" +#include "ui_prompt.h" -#include "os.h" -#include "cx.h" +#include "os_cx.h" #include -WIDE nvram_data N_data_real; // TODO: What does WIDE actually mean? -static nvram_data new_data; // Staging area for setting N_data - bool is_valid_level(level_t lvl) { return !(lvl & 0xC0000000); } -void write_highest_level(level_t lvl, bool is_endorsement) { - if (!is_valid_level(lvl)) return; - memcpy(&new_data, &N_data, sizeof(new_data)); - new_data.highest_level = lvl; - new_data.had_endorsement = is_endorsement; - nvm_write((void*)&N_data, &new_data, sizeof(N_data)); - change_idle_display(N_data.highest_level); +static void write_high_watermark(parsed_baking_data_t const *const in) { + check_null(in); + if (!is_valid_level(in->level)) THROW(EXC_WRONG_VALUES); + UPDATE_NVRAM(ram, { + // If the chain matches the main chain *or* the main chain is not set, then use 'main' HWM. + high_watermark_t *const dest = select_hwm_by_chain(in->chain_id, ram); + dest->highest_level = MAX(in->level, dest->highest_level); + dest->had_endorsement = in->is_endorsement; + }); } -void authorize_baking(cx_curve_t curve, uint32_t *bip32_path, uint8_t path_length) { - if (bip32_path == NULL || path_length > MAX_BIP32_PATH || path_length == 0) { - return; - } +void authorize_baking(cx_curve_t const curve, bip32_path_t const *const bip32_path) { + check_null(bip32_path); + if (bip32_path->length > NUM_ELEMENTS(N_data.baking_key.bip32_path.components) || bip32_path->length == 0) return; - new_data.highest_level = N_data.highest_level; - new_data.had_endorsement = N_data.had_endorsement; - new_data.curve = curve; - memcpy(new_data.bip32_path, bip32_path, path_length * sizeof(*bip32_path)); - new_data.path_length = path_length; - nvm_write((void*)&N_data, &new_data, sizeof(N_data)); - change_idle_display(N_data.highest_level); + UPDATE_NVRAM(ram, { + ram->baking_key.curve = curve; + copy_bip32_path(&ram->baking_key.bip32_path, bip32_path); + }); } -bool is_level_authorized(level_t level, bool is_endorsement) { - if (!is_valid_level(level)) return false; - if (level > N_data.highest_level) return true; +static bool is_level_authorized(parsed_baking_data_t const *const baking_info) { + check_null(baking_info); + if (!is_valid_level(baking_info->level)) return false; + high_watermark_t const *const hwm = select_hwm_by_chain(baking_info->chain_id, &N_data); + return baking_info->level > hwm->highest_level - // Levels are tied. In order for this to be OK, this must be an endorsement, and we must not - // have previously seen an endorsement. - return is_endorsement && !N_data.had_endorsement; + // Levels are tied. In order for this to be OK, this must be an endorsement, and we must not + // have previously seen an endorsement. + || (baking_info->level == hwm->highest_level + && baking_info->is_endorsement && !hwm->had_endorsement); } -bool is_path_authorized(cx_curve_t curve, uint32_t *bip32_path, uint8_t path_length) { - return bip32_path != NULL && - path_length != 0 && - path_length == N_data.path_length && - curve == N_data.curve && - memcmp(bip32_path, N_data.bip32_path, path_length * sizeof(*bip32_path)) == 0; +bool is_path_authorized(cx_curve_t curve, bip32_path_t const *const bip32_path) { + check_null(bip32_path); + return + curve == N_data.baking_key.curve && + bip32_path->length > 0 && + bip32_paths_eq(bip32_path, &N_data.baking_key.bip32_path); } -void guard_baking_authorized(cx_curve_t curve, void *data, int datalen, uint32_t *bip32_path, - uint8_t path_length) { - if (!is_path_authorized(curve, bip32_path, path_length)) THROW(EXC_SECURITY); - - struct parsed_baking_data baking_info; - if (!parse_baking_data(data, datalen, &baking_info)) THROW(EXC_PARSE_ERROR); +void guard_baking_authorized(cx_curve_t curve, void *data, int datalen, bip32_path_t const *const bip32_path) { + check_null(data); + check_null(bip32_path); + if (!is_path_authorized(curve, bip32_path)) THROW(EXC_SECURITY); - if (!is_level_authorized(baking_info.level, baking_info.is_endorsement)) THROW(EXC_WRONG_VALUES); + parsed_baking_data_t baking_info; + if (!parse_baking_data(&baking_info, data, datalen)) THROW(EXC_PARSE_ERROR); + if (!is_level_authorized(&baking_info)) THROW(EXC_WRONG_VALUES); } void update_high_water_mark(void *data, int datalen) { - struct parsed_baking_data baking_info; - if (!parse_baking_data(data, datalen, &baking_info)) { + check_null(data); + parsed_baking_data_t baking_info; + if (!parse_baking_data(&baking_info, data, datalen)) { return; // Must be signing a delegation } - - write_highest_level(baking_info.level, baking_info.is_endorsement); + write_high_watermark(&baking_info); } -void update_auth_text(void) { - if (N_data.path_length == 0) { - strcpy(baking_auth_text, "No Key Authorized"); - } else { - struct key_pair *pair = generate_key_pair(N_data.curve, N_data.path_length, N_data.bip32_path); - os_memset(&pair->private_key, 0, sizeof(pair->private_key)); - pubkey_to_pkh_string(baking_auth_text, sizeof(baking_auth_text), N_data.curve, - &pair->public_key); - } -} - -static char address_display_data[VALUE_WIDTH]; - static const char *const pubkey_values[] = { "Public Key", - address_display_data, + global.baking_auth.address_display_data, NULL, }; @@ -108,16 +95,14 @@ static char const * const * get_baking_prompts() { static const char *const baking_values[] = { "With Public Key?", - address_display_data, + global.baking_auth.address_display_data, NULL, }; // TODO: Unshare code with next function -void prompt_contract_for_baking(struct parsed_contract *contract, callback_t ok_cb, callback_t cxl_cb) { - if (!parsed_contract_to_string(address_display_data, sizeof(address_display_data), contract)) { - THROW(EXC_WRONG_VALUES); - } - +void prompt_contract_for_baking(struct parsed_contract *contract, ui_callback_t ok_cb, ui_callback_t cxl_cb) { + parsed_contract_to_string( + global.baking_auth.address_display_data, sizeof(global.baking_auth.address_display_data), contract); ui_prompt(get_baking_prompts(), baking_values, ok_cb, cxl_cb); } #endif @@ -127,11 +112,10 @@ void prompt_address( __attribute__((unused)) #endif bool baking, - cx_curve_t curve, const cx_ecfp_public_key_t *key, callback_t ok_cb, - callback_t cxl_cb) { - if (!pubkey_to_pkh_string(address_display_data, sizeof(address_display_data), curve, key)) { - THROW(EXC_WRONG_VALUES); - } + cx_curve_t curve, const cx_ecfp_public_key_t *key, ui_callback_t ok_cb, + ui_callback_t cxl_cb) { + pubkey_to_pkh_string( + global.baking_auth.address_display_data, sizeof(global.baking_auth.address_display_data), curve, key); #ifdef BAKING_APP if (baking) { @@ -149,36 +133,36 @@ void prompt_address( #endif } -struct __attribute__((packed)) block { - char magic_byte; +struct block_wire { + uint8_t magic_byte; uint32_t chain_id; - level_t level; + uint32_t level; uint8_t proto; // ... beyond this we don't care -}; +} __attribute__((packed)); -struct __attribute__((packed)) endorsement { +struct endorsement_wire { uint8_t magic_byte; uint32_t chain_id; uint8_t branch[32]; uint8_t tag; uint32_t level; -}; +} __attribute__((packed)); -bool parse_baking_data(const void *data, size_t length, struct parsed_baking_data *out) { +bool parse_baking_data(parsed_baking_data_t *const out, void const *const data, size_t const length) { switch (get_magic_byte(data, length)) { case MAGIC_BYTE_BAKING_OP: - if (length != sizeof(struct endorsement)) return false; - const struct endorsement *endorsement = data; - // TODO: Check chain ID + if (length != sizeof(struct endorsement_wire)) return false; + struct endorsement_wire const *const endorsement = data; out->is_endorsement = true; - out->level = READ_UNALIGNED_BIG_ENDIAN(level_t, &endorsement->level); + out->chain_id.v = READ_UNALIGNED_BIG_ENDIAN(uint32_t, &endorsement->chain_id); + out->level = READ_UNALIGNED_BIG_ENDIAN(uint32_t, &endorsement->level); return true; case MAGIC_BYTE_BLOCK: - if (length < sizeof(struct block)) return false; - // TODO: Check chain ID + if (length < sizeof(struct block_wire)) return false; + struct block_wire const *const block = data; out->is_endorsement = false; - const struct block *block = data; + out->chain_id.v = READ_UNALIGNED_BIG_ENDIAN(uint32_t, &block->chain_id); out->level = READ_UNALIGNED_BIG_ENDIAN(level_t, &block->level); return true; case MAGIC_BYTE_INVALID: diff --git a/src/baking_auth.h b/src/baking_auth.h index 9f72856e..245bfc07 100644 --- a/src/baking_auth.h +++ b/src/baking_auth.h @@ -1,42 +1,28 @@ #pragma once -#include -#include - #include "apdu.h" -#include "protocol.h" #include "operations.h" +#include "protocol.h" +#include "types.h" -#define MAX_BIP32_PATH 10 - -typedef struct { - cx_curve_t curve; - level_t highest_level; - bool had_endorsement; - uint8_t path_length; - uint32_t bip32_path[MAX_BIP32_PATH]; -} nvram_data; -extern WIDE nvram_data N_data_real; -#define N_data (*(WIDE nvram_data*)PIC(&N_data_real)) +#include +#include -void authorize_baking(cx_curve_t curve, uint32_t *bip32_path, uint8_t pathLength); -void guard_baking_authorized(cx_curve_t curve, void *data, int datalen, uint32_t *bip32_path, - uint8_t path_length); -bool is_path_authorized(cx_curve_t curve, uint32_t *bip32_path, uint8_t path_length); +void authorize_baking(cx_curve_t const curve, bip32_path_t const *const bip32_path); +void guard_baking_authorized(cx_curve_t curve, void *data, int datalen, bip32_path_t const *const bip32_path); +bool is_path_authorized(cx_curve_t curve, bip32_path_t const *const bip32_path); void update_high_water_mark(void *data, int datalen); -void write_highest_level(level_t level, bool is_endorsement); -bool is_level_authorized(level_t level, bool is_endorsement); bool is_valid_level(level_t level); -void update_auth_text(void); -void prompt_contract_for_baking(struct parsed_contract *contract, callback_t ok_cb, callback_t cxl_cb); +void prompt_contract_for_baking(struct parsed_contract *contract, ui_callback_t ok_cb, ui_callback_t cxl_cb); void prompt_address(bool bake, cx_curve_t curve, const cx_ecfp_public_key_t *key, - callback_t ok_cb, callback_t cxl_cb) __attribute__((noreturn)); + ui_callback_t ok_cb, ui_callback_t cxl_cb) __attribute__((noreturn)); -struct parsed_baking_data { +typedef struct { + chain_id_t chain_id; bool is_endorsement; level_t level; -}; +} parsed_baking_data_t; // Return false if it is invalid -bool parse_baking_data(const void *data, size_t length, struct parsed_baking_data *out); +bool parse_baking_data(parsed_baking_data_t *const out, void const *const data, size_t const length); diff --git a/src/blake2.h b/src/blake2.h index 11cab5ec..0dae3fdb 100644 --- a/src/blake2.h +++ b/src/blake2.h @@ -151,5 +151,4 @@ extern "C" { } #endif -extern b2b_state hash_state; #endif diff --git a/src/blake2b-ref.c b/src/blake2b-ref.c index ccf94d48..5adf08fb 100644 --- a/src/blake2b-ref.c +++ b/src/blake2b-ref.c @@ -20,8 +20,6 @@ #include "blake2.h" #include "blake2-impl.h" -b2b_state hash_state; - static const uint64_t b2b_IV[8] = { 0x6a09e667f3bcc908ULL, 0xbb67ae8584caa73bULL, diff --git a/src/boot.c b/src/boot.c index 3eced2cb..53b5cafb 100644 --- a/src/boot.c +++ b/src/boot.c @@ -17,9 +17,12 @@ #include "ui.h" +// Order matters #include "os.h" #include "cx.h" +#include "globals.h" + __attribute__((noreturn)) void app_main(void); @@ -30,6 +33,10 @@ __attribute__((section(".boot"))) int main(void) { // ensure exception will work as planned os_boot(); + uint8_t tag; + init_globals(); + global.stack_root = &tag; + for (;;) { BEGIN_TRY { TRY { diff --git a/src/exception.h b/src/exception.h index ddd59292..cb2cd9e5 100644 --- a/src/exception.h +++ b/src/exception.h @@ -5,17 +5,20 @@ // Throw this to indicate prompting #define ASYNC_EXCEPTION 0x2000 +// Standard APDU error codes: +// https://www.eftlab.co.uk/index.php/site-map/knowledge-base/118-apdu-response-list + #define EXC_WRONG_PARAM 0x6B00 #define EXC_WRONG_LENGTH 0x6C00 #define EXC_INVALID_INS 0x6D00 #define EXC_WRONG_LENGTH_FOR_INS 0x917E #define EXC_REJECT 0x6985 #define EXC_PARSE_ERROR 0x9405 +#define EXC_REFERENCED_DATA_NOT_FOUND 0x6A88 #define EXC_WRONG_VALUES 0x6A80 #define EXC_SECURITY 0x6982 #define EXC_HID_REQUIRED 0x6983 #define EXC_CLASS 0x6E00 -#define EXC_NO_ERROR 0x9000 #define EXC_MEMORY_ERROR 0x9200 // Crashes can be harder to debug than exceptions and latency isn't a big concern diff --git a/src/globals.c b/src/globals.c new file mode 100644 index 00000000..a78fb597 --- /dev/null +++ b/src/globals.c @@ -0,0 +1,55 @@ +#include "globals.h" + +#include "exception.h" +#include "to_string.h" + +#include + + +// WARNING: *************************************************** +// Non-const globals MUST NOT HAVE AN INITIALIZER. +// +// Providing an initializer will cause the application to crash +// if you write to it. +// ************************************************************ + + +globals_t global; + +// These are strange variables that the SDK relies on us to define but uses directly itself. +ux_state_t ux; +unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; + +void init_globals(void) { + memset(&global, 0, sizeof(global)); + memset(&ux, 0, sizeof(ux)); + memset(G_io_seproxyhal_spi_buffer, 0, sizeof(G_io_seproxyhal_spi_buffer)); +} + +// #ifdef BAKING_APP +// DO NOT TRY TO INIT THIS. This can only be written via an system call. +// The "N_" is *significant*. It tells the linker to put this in NVRAM. +WIDE nvram_data N_data_real; // TODO: What does WIDE actually mean? + +high_watermark_t *select_hwm_by_chain(chain_id_t const chain_id, nvram_data *const ram) { + check_null(ram); + return chain_id.v == ram->main_chain_id.v || ram->main_chain_id.v == 0 + ? &ram->hwm.main + : &ram->hwm.test; +} + +void update_baking_idle_screens(void) { + number_to_string(global.ui.baking_idle_screens.hwm, N_data.hwm.main.highest_level); + + if (N_data.baking_key.bip32_path.length == 0) { + STRCPY(global.ui.baking_idle_screens.pkh, "No Key Authorized"); + } else { + cx_ecfp_public_key_t const *const pubkey = generate_public_key(N_data.baking_key.curve, &N_data.baking_key.bip32_path); + pubkey_to_pkh_string( + global.ui.baking_idle_screens.pkh, sizeof(global.ui.baking_idle_screens.pkh), + N_data.baking_key.curve, pubkey); + } + + chain_id_to_string_with_aliases(global.ui.baking_idle_screens.chain, sizeof(global.ui.baking_idle_screens.chain), &N_data.main_chain_id); +} +//#endif diff --git a/src/globals.h b/src/globals.h new file mode 100644 index 00000000..e7fca542 --- /dev/null +++ b/src/globals.h @@ -0,0 +1,140 @@ +#pragma once + +#include "blake2.h" +#include "types.h" + +#include + + +void init_globals(void); + +// Where does this number come from? +#ifdef BAKING_APP +# define TEZOS_BUFSIZE 512 +#else +# define TEZOS_BUFSIZE 256 +#endif + +#define PRIVATE_KEY_DATA_SIZE 32 + +struct priv_generate_key_pair { + uint8_t privateKeyData[PRIVATE_KEY_DATA_SIZE]; + struct key_pair res; +}; + +typedef struct { + void *stack_root; + apdu_handler handlers[INS_MAX + 1]; + + union { + struct { + level_t reset_level; + } baking; + + struct { + bip32_path_with_curve_t key; + cx_ecfp_public_key_t public_key; + } pubkey; + + struct { + bip32_path_with_curve_t key; + + uint8_t message_data[TEZOS_BUFSIZE]; + uint32_t message_data_length; + + bool is_hash_state_inited; + uint8_t magic_number; + bool hash_only; + + struct parsed_operation_group ops; + } sign; + + struct { + bip32_path_with_curve_t key; + chain_id_t main_chain_id; + struct { + level_t main; + level_t test; + } hwm; + } setup; + } u; + + struct { + ui_callback_t ok_callback; + ui_callback_t cxl_callback; + + uint32_t ux_step; + uint32_t ux_step_count; + + uint32_t timeout_cycle_count; + + struct { + char hwm[MAX_INT_DIGITS + 1]; // with null termination + char pkh[PKH_STRING_SIZE]; + char chain[CHAIN_ID_BASE58_STRING_SIZE]; + } baking_idle_screens; + + struct { + string_generation_callback callbacks[MAX_SCREEN_COUNT]; + const void *callback_data[MAX_SCREEN_COUNT]; + char active_prompt[PROMPT_WIDTH + 1]; + char active_value[VALUE_WIDTH + 1]; + + // This will and must always be static memory full of constants + const char *const *prompts; + } prompt; + } ui; + + struct { + nvram_data new_data; // Staging area for setting N_data + + char address_display_data[VALUE_WIDTH]; + } baking_auth; + + struct { + b2b_state hash_state; + } blake2b; // TODO: Use blake2b from SDK + + struct { + struct priv_generate_key_pair generate_key_pair; + + struct { + cx_ecfp_public_key_t compressed; + } public_key_hash; + } priv; +} globals_t; + +extern globals_t global; + +extern unsigned int app_stack_canary; // From SDK + +// Used by macros that we don't control. +extern ux_state_t ux; +extern unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; + +static inline void throw_stack_size() { + uint8_t st; + // uint32_t tmp1 = (uint32_t)&st - (uint32_t)&app_stack_canary; + uint32_t tmp2 = (uint32_t)global.stack_root - (uint32_t)&st; + THROW(0x9000 + tmp2); +} + +// #ifdef BAKING_APP +extern WIDE nvram_data N_data_real; // TODO: What does WIDE actually mean? + +#define N_data (*(WIDE nvram_data*)PIC(&N_data_real)) + +void update_baking_idle_screens(void); +high_watermark_t *select_hwm_by_chain(chain_id_t const chain_id, nvram_data *const ram); + +// Properly updates NVRAM data to prevent any clobbering of data. +// 'out_param' defines the name of a pointer to the nvram_data struct +// that 'body' can change to apply updates. +#define UPDATE_NVRAM(out_name, body) ({ \ + nvram_data *const out_name = &global.baking_auth.new_data; \ + memcpy(&global.baking_auth.new_data, &N_data, sizeof(global.baking_auth.new_data)); \ + body; \ + nvm_write((void*)&N_data, &global.baking_auth.new_data, sizeof(N_data)); \ + update_baking_idle_screens(); \ +}) +//#endif diff --git a/src/keys.c b/src/keys.c index 2b8342d3..c7f97da2 100644 --- a/src/keys.c +++ b/src/keys.c @@ -18,83 +18,94 @@ #include "keys.h" #include "apdu.h" -#include "os.h" #include "blake2.h" +#include "globals.h" +#include "memory.h" #include "protocol.h" +#include "types.h" #include #include -// The following need to be persisted for baking app -uint8_t bip32_path_length; -uint32_t bip32_path[MAX_BIP32_PATH]; +size_t read_bip32_path(bip32_path_t *const out, uint8_t const *const in, size_t const in_size) { + struct bip32_path_wire const *const buf_as_bip32 = (struct bip32_path_wire const *)in; -uint32_t read_bip32_path(uint32_t bytes, uint32_t *bip32_path, const uint8_t *buf) { - uint32_t path_length = *buf; - if (bytes < path_length * sizeof(uint32_t) + 1) THROW(EXC_WRONG_LENGTH_FOR_INS); + if (in_size < sizeof(buf_as_bip32->length)) THROW(EXC_WRONG_LENGTH_FOR_INS); - buf++; + size_t ix = 0; + out->length = CONSUME_UNALIGNED_BIG_ENDIAN(ix, uint8_t, &buf_as_bip32->length); - if (path_length == 0 || path_length > MAX_BIP32_PATH) { - screen_printf("Invalid path\n"); - THROW(EXC_WRONG_VALUES); - } + if (in_size - ix < out->length * sizeof(*buf_as_bip32->components)) THROW(EXC_WRONG_LENGTH_FOR_INS); + if (out->length == 0 || out->length > NUM_ELEMENTS(out->components)) THROW(EXC_WRONG_VALUES); - for (size_t i = 0; i < path_length; i++) { - bip32_path[i] = READ_UNALIGNED_BIG_ENDIAN(uint32_t, (uint32_t*)buf); - buf += 4; + for (size_t i = 0; i < out->length; i++) { + out->components[i] = CONSUME_UNALIGNED_BIG_ENDIAN(ix, uint32_t, &buf_as_bip32->components[i]); } - return path_length; + return ix; } -struct key_pair *generate_key_pair(cx_curve_t curve, uint32_t path_length, uint32_t *bip32_path) { - static uint8_t privateKeyData[32]; - static struct key_pair res; +struct key_pair *generate_key_pair(cx_curve_t const curve, bip32_path_t const *const bip32_path) { + check_null(bip32_path); + struct priv_generate_key_pair *const priv = &global.priv.generate_key_pair; + #if CX_APILEVEL > 8 if (curve == CX_CURVE_Ed25519) { - os_perso_derive_node_bip32_seed_key(HDW_ED25519_SLIP10, curve, bip32_path, path_length, - privateKeyData, NULL, NULL, 0); + os_perso_derive_node_bip32_seed_key( + HDW_ED25519_SLIP10, curve, bip32_path->components, bip32_path->length, + priv->privateKeyData, NULL, NULL, 0); } else { #endif - os_perso_derive_node_bip32(curve, bip32_path, path_length, privateKeyData, NULL); + os_perso_derive_node_bip32(curve, bip32_path->components, bip32_path->length, priv->privateKeyData, NULL); #if CX_APILEVEL > 8 } #endif - cx_ecfp_init_private_key(curve, privateKeyData, 32, &res.private_key); - cx_ecfp_generate_pair(curve, &res.public_key, &res.private_key, 1); + cx_ecfp_init_private_key(curve, priv->privateKeyData, sizeof(priv->privateKeyData), &priv->res.private_key); + cx_ecfp_generate_pair(curve, &priv->res.public_key, &priv->res.private_key, 1); if (curve == CX_CURVE_Ed25519) { - cx_edward_compress_point(curve, res.public_key.W, res.public_key.W_len); - res.public_key.W_len = 33; + cx_edward_compress_point( + curve, + priv->res.public_key.W, + priv->res.public_key.W_len); + priv->res.public_key.W_len = 33; } - os_memset(privateKeyData, 0, sizeof(privateKeyData)); - return &res; + memset(priv->privateKeyData, 0, sizeof(priv->privateKeyData)); + return &priv->res; +} + +cx_ecfp_public_key_t const *generate_public_key(cx_curve_t const curve, bip32_path_t const *const bip32_path) { + check_null(bip32_path); + struct key_pair *const pair = generate_key_pair(curve, bip32_path); + memset(&pair->private_key, 0, sizeof(pair->private_key)); + return &pair->public_key; } -cx_ecfp_public_key_t *public_key_hash(uint8_t output[HASH_SIZE], cx_curve_t curve, - const cx_ecfp_public_key_t *restrict public_key) { - static cx_ecfp_public_key_t compressed; +cx_ecfp_public_key_t const *public_key_hash( + uint8_t output[HASH_SIZE], cx_curve_t curve, + cx_ecfp_public_key_t const *const restrict public_key) +{ + cx_ecfp_public_key_t *const compressed = &global.priv.public_key_hash.compressed; switch (curve) { case CX_CURVE_Ed25519: { - compressed.W_len = public_key->W_len - 1; - memcpy(compressed.W, public_key->W + 1, compressed.W_len); + compressed->W_len = public_key->W_len - 1; + memcpy(compressed->W, public_key->W + 1, compressed->W_len); break; } case CX_CURVE_SECP256K1: case CX_CURVE_SECP256R1: { - memcpy(compressed.W, public_key->W, public_key->W_len); - compressed.W[0] = 0x02 + (public_key->W[64] & 0x01); - compressed.W_len = 33; + memcpy(compressed->W, public_key->W, public_key->W_len); + compressed->W[0] = 0x02 + (public_key->W[64] & 0x01); + compressed->W_len = 33; break; } default: THROW(EXC_WRONG_PARAM); } - b2b_init(&hash_state, HASH_SIZE); - b2b_update(&hash_state, compressed.W, compressed.W_len); - b2b_final(&hash_state, output, HASH_SIZE); - return &compressed; + b2b_init(&global.blake2b.hash_state, HASH_SIZE); + b2b_update(&global.blake2b.hash_state, compressed->W, compressed->W_len); + b2b_final(&global.blake2b.hash_state, output, HASH_SIZE); + return compressed; } diff --git a/src/keys.h b/src/keys.h index 76e1ac2d..ec6763cf 100644 --- a/src/keys.h +++ b/src/keys.h @@ -3,33 +3,25 @@ #include #include #include -#include "os.h" -#include "cx.h" #include "exception.h" +#include "os_cx.h" +#include "types.h" -#define MAX_BIP32_PATH 10 +struct bip32_path_wire { + uint8_t length; + uint32_t components[0]; +} __attribute__((packed)); -// The following need to be persisted for baking app -extern uint8_t bip32_path_length; -extern uint32_t bip32_path[MAX_BIP32_PATH]; +// throws +size_t read_bip32_path(bip32_path_t *const out, uint8_t const *const in, size_t const in_size); -// Throws upon error -uint32_t read_bip32_path(uint32_t bytes, uint32_t *bip32_path, const uint8_t *buf); +struct key_pair *generate_key_pair(cx_curve_t const curve, bip32_path_t const *const bip32_path); +cx_ecfp_public_key_t const *generate_public_key(cx_curve_t const curve, bip32_path_t const *const bip32_path); -struct key_pair { - cx_ecfp_public_key_t public_key; - cx_ecfp_private_key_t private_key; -}; - -struct key_pair *generate_key_pair(cx_curve_t curve, uint32_t path_size, uint32_t *bip32_path); - -// TODO: Rename to KEY_HASH_SIZE -#define HASH_SIZE 20 -#define PKH_STRING_SIZE 40 - -cx_ecfp_public_key_t *public_key_hash(uint8_t output[HASH_SIZE], cx_curve_t curve, - const cx_ecfp_public_key_t *public_key); +cx_ecfp_public_key_t const *public_key_hash( + uint8_t output[HASH_SIZE], cx_curve_t curve, + cx_ecfp_public_key_t const *const restrict public_key); enum curve_code { TEZOS_ED, @@ -47,7 +39,7 @@ static inline uint8_t curve_to_curve_code(cx_curve_t curve) { case CX_CURVE_SECP256R1: return TEZOS_SECP256R1; default: - THROW(EXC_MEMORY_ERROR); + return TEZOS_NO_CURVE; } } diff --git a/src/main.c b/src/main.c index ef02a945..4de5e543 100644 --- a/src/main.c +++ b/src/main.c @@ -15,36 +15,36 @@ * limitations under the License. ********************************************************************************/ -#include "apdu.h" +#include "apdu_baking.h" #include "apdu_pubkey.h" +#include "apdu_setup.h" #include "apdu_sign.h" -#include "apdu_baking.h" - -void *stack_root; +#include "apdu.h" +#include "globals.h" +#include "memory.h" __attribute__((noreturn)) void app_main(void) { - static apdu_handler handlers[INS_MAX]; - uint8_t tag; - stack_root = &tag; - // TODO: Consider using static initialization of a const, instead of this - for (size_t i = 0; i < INS_MAX; i++) { - handlers[i] = handle_apdu_error; + for (size_t i = 0; i < NUM_ELEMENTS(global.handlers); i++) { + global.handlers[i] = handle_apdu_error; } - handlers[INS_VERSION] = handle_apdu_version; - handlers[INS_GET_PUBLIC_KEY] = handle_apdu_get_public_key; - handlers[INS_PROMPT_PUBLIC_KEY] = handle_apdu_get_public_key; + global.handlers[APDU_INS(INS_VERSION)] = handle_apdu_version; + global.handlers[APDU_INS(INS_GET_PUBLIC_KEY)] = handle_apdu_get_public_key; + global.handlers[APDU_INS(INS_PROMPT_PUBLIC_KEY)] = handle_apdu_get_public_key; + global.handlers[APDU_INS(INS_SIGN)] = handle_apdu_sign; + global.handlers[APDU_INS(INS_GIT)] = handle_apdu_git; #ifdef BAKING_APP - handlers[INS_AUTHORIZE_BAKING] = handle_apdu_get_public_key; - handlers[INS_RESET] = handle_apdu_reset; - handlers[INS_QUERY_AUTH_KEY] = handle_apdu_query_auth_key; - handlers[INS_QUERY_HWM] = handle_apdu_hwm; -#endif - handlers[INS_SIGN] = handle_apdu_sign; -#ifndef BAKING_APP - handlers[INS_SIGN_UNSAFE] = handle_apdu_sign; + global.handlers[APDU_INS(INS_AUTHORIZE_BAKING)] = handle_apdu_get_public_key; + global.handlers[APDU_INS(INS_RESET)] = handle_apdu_reset; + global.handlers[APDU_INS(INS_QUERY_AUTH_KEY)] = handle_apdu_query_auth_key; + global.handlers[APDU_INS(INS_QUERY_MAIN_HWM)] = handle_apdu_main_hwm; + global.handlers[APDU_INS(INS_SETUP)] = handle_apdu_setup; + global.handlers[APDU_INS(INS_QUERY_ALL_HWM)] = handle_apdu_all_hwm; + global.handlers[APDU_INS(INS_DEAUTHORIZE)] = handle_apdu_deauthorize; + global.handlers[APDU_INS(INS_QUERY_AUTH_KEY_WITH_CURVE)] = handle_apdu_query_auth_key_with_curve; +#else + global.handlers[APDU_INS(INS_SIGN_UNSAFE)] = handle_apdu_sign; #endif - handlers[INS_GIT] = handle_apdu_git; - main_loop(handlers); + main_loop(global.handlers, sizeof(global.handlers)); } diff --git a/src/memory.h b/src/memory.h new file mode 100644 index 00000000..5ae024a9 --- /dev/null +++ b/src/memory.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include "exception.h" + + +#define COMPARE(a, b) ({ \ + _Static_assert(sizeof(*a) == sizeof(*b), "Size mismatch in COMPARE"); \ + check_null(a); \ + check_null(b); \ + memcmp(a, b, sizeof(*a)); \ +}) +#define NUM_ELEMENTS(a) (sizeof(a)/sizeof(*a)) + + +// Macro that produces a compile-error showing the sizeof the argument. +#define ERROR_WITH_NUMBER_EXPANSION(x) char (*__kaboom)[x] = 1; +#define SIZEOF_ERROR(x) ERROR_WITH_NUMBER_EXPANSION(sizeof(x)); diff --git a/src/operations.c b/src/operations.c index 5484c194..9799ede4 100644 --- a/src/operations.c +++ b/src/operations.c @@ -1,8 +1,10 @@ #include "operations.h" #include "apdu.h" -#include "ui.h" +#include "globals.h" +#include "memory.h" #include "to_string.h" +#include "ui.h" #include #include @@ -103,19 +105,28 @@ static inline uint64_t parse_z(const void *data, size_t *ix, size_t length, uint val; \ }) -static inline void compute_pkh(cx_curve_t curve, size_t path_length, uint32_t *bip32_path, - struct parsed_operation_group *out) { - struct key_pair *pair = generate_key_pair(curve, path_length, bip32_path); - os_memset(&pair->private_key, 0, sizeof(pair->private_key)); - - cx_ecfp_public_key_t *key = public_key_hash(out->signing.hash, curve, &pair->public_key); - memcpy(&out->public_key, key, sizeof(out->public_key)); - out->signing.curve_code = curve_to_curve_code(curve); - out->signing.originated = 0; +static inline void compute_pkh( + cx_ecfp_public_key_t *const compressed_pubkey_out, + struct parsed_contract *const contract_out, + cx_curve_t const curve, + bip32_path_t const *const bip32_path +) { + check_null(bip32_path); + check_null(compressed_pubkey_out); + check_null(contract_out); + cx_ecfp_public_key_t const *const pubkey = generate_public_key(curve, bip32_path); + cx_ecfp_public_key_t const *const compressed_pubkey = public_key_hash(contract_out->hash, curve, pubkey); + contract_out->curve_code = curve_to_curve_code(curve); + if (contract_out->curve_code == TEZOS_NO_CURVE) THROW(EXC_MEMORY_ERROR); + contract_out->originated = 0; + + memcpy(compressed_pubkey_out, compressed_pubkey, sizeof(*compressed_pubkey_out)); } -static inline void parse_implicit(struct parsed_contract *out, uint8_t curve_code, - const uint8_t hash[HASH_SIZE]) { +static inline void parse_implicit( + struct parsed_contract *out, uint8_t curve_code, + const uint8_t hash[HASH_SIZE] +) { out->originated = 0; out->curve_code = curve_code; memcpy(out->hash, hash, sizeof(out->hash)); @@ -132,17 +143,22 @@ static inline void parse_contract(struct parsed_contract *out, const struct cont } } -struct parsed_operation_group *parse_operations(const void *data, size_t length, cx_curve_t curve, - size_t path_length, uint32_t *bip32_path, - allowed_operation_set ops) { - static struct parsed_operation_group out; +void parse_operations( + struct parsed_operation_group *const out, + void const *const data, + size_t length, + cx_curve_t curve, + bip32_path_t const *const bip32_path, + allowed_operation_set ops +) { + check_null(out); check_null(data); check_null(bip32_path); - memset(&out, 0, sizeof(out)); + memset(out, 0, sizeof(*out)); - out.operation.tag = OPERATION_TAG_NONE; + out->operation.tag = OPERATION_TAG_NONE; - compute_pkh(curve, path_length, bip32_path, &out); // sets up "signing" and "public_key" members + compute_pkh(&out->public_key, &out->signing, curve, bip32_path); size_t ix = 0; @@ -152,7 +168,7 @@ struct parsed_operation_group *parse_operations(const void *data, size_t length, // Start out with source = signing, for reveals // TODO: This is slightly hackish - memcpy(&out.operation.source, &out.signing, sizeof(out.signing)); + memcpy(&out->operation.source, &out->signing, sizeof(out->signing)); while (ix < length) { const enum operation_tag tag = NEXT_BYTE(data, &ix, length); // 1 byte is always aligned @@ -162,55 +178,55 @@ struct parsed_operation_group *parse_operations(const void *data, size_t length, if (tag == OPERATION_TAG_PROPOSAL || tag == OPERATION_TAG_BALLOT) { // These tags don't have the "originated" byte so we have to parse PKH differently. const struct implicit_contract *implicit_source = NEXT_TYPE(struct implicit_contract); - out.operation.source.originated = 0; - out.operation.source.curve_code = READ_UNALIGNED_BIG_ENDIAN(uint8_t, &implicit_source->curve_code); - memcpy(out.operation.source.hash, implicit_source->pkh, sizeof(out.operation.source.hash)); + out->operation.source.originated = 0; + out->operation.source.curve_code = READ_UNALIGNED_BIG_ENDIAN(uint8_t, &implicit_source->curve_code); + memcpy(out->operation.source.hash, implicit_source->pkh, sizeof(out->operation.source.hash)); } else { const struct contract *source = NEXT_TYPE(struct contract); - parse_contract(&out.operation.source, source); + parse_contract(&out->operation.source, source); - out.total_fee += PARSE_Z(data, &ix, length); // fee + out->total_fee += PARSE_Z(data, &ix, length); // fee PARSE_Z(data, &ix, length); // counter PARSE_Z(data, &ix, length); // gas limit - out.total_storage_limit += PARSE_Z(data, &ix, length); // storage limit + out->total_storage_limit += PARSE_Z(data, &ix, length); // storage limit } - // out.operation.source IS NORMALIZED AT THIS POINT + // out->operation.source IS NORMALIZED AT THIS POINT if (tag == OPERATION_TAG_REVEAL) { // Public key up next! Ensure it matches signing key. // Ignore source :-) and do not parse it from hdr. // We don't much care about reveals, they have very little in the way of bad security // implications and any fees have already been accounted for - if (NEXT_BYTE(data, &ix, length) != out.signing.curve_code) PARSE_ERROR(); + if (NEXT_BYTE(data, &ix, length) != out->signing.curve_code) PARSE_ERROR(); - size_t klen = out.public_key.W_len; + size_t klen = out->public_key.W_len; advance_ix(&ix, length, klen); - if (memcmp(out.public_key.W, data + ix - klen, klen) != 0) PARSE_ERROR(); + if (memcmp(out->public_key.W, data + ix - klen, klen) != 0) PARSE_ERROR(); - out.has_reveal = true; + out->has_reveal = true; continue; } - if (out.operation.tag != OPERATION_TAG_NONE) { + if (out->operation.tag != OPERATION_TAG_NONE) { // We are only currently allowing one non-reveal operation PARSE_ERROR(); } // This is the one allowable non-reveal operation per set - out.operation.tag = (uint8_t)tag; + out->operation.tag = (uint8_t)tag; // If the source is an implicit contract,... - if (out.operation.source.originated == 0) { + if (out->operation.source.originated == 0) { // ... it had better match our key, otherwise why are we signing it? - if (memcmp(&out.operation.source, &out.signing, sizeof(out.signing))) return false; + if (COMPARE(&out->operation.source, &out->signing) != 0) PARSE_ERROR(); } // OK, it passes muster. // This should by default be blanked out - out.operation.delegate.curve_code = TEZOS_NO_CURVE; - out.operation.delegate.originated = 0; + out->operation.delegate.curve_code = TEZOS_NO_CURVE; + out->operation.delegate.originated = 0; switch (tag) { case OPERATION_TAG_PROPOSAL: @@ -221,8 +237,8 @@ struct parsed_operation_group *parse_operations(const void *data, size_t length, const size_t payload_size = READ_UNALIGNED_BIG_ENDIAN(int32_t, &proposal_data->num_bytes); if (payload_size != PROTOCOL_HASH_SIZE) PARSE_ERROR(); // We only accept exactly 1 proposal hash. - out.operation.proposal.voting_period = READ_UNALIGNED_BIG_ENDIAN(int32_t, &proposal_data->period); - memcpy(out.operation.proposal.protocol_hash, proposal_data->hash, sizeof(out.operation.proposal.protocol_hash)); + out->operation.proposal.voting_period = READ_UNALIGNED_BIG_ENDIAN(int32_t, &proposal_data->period); + memcpy(out->operation.proposal.protocol_hash, proposal_data->hash, sizeof(out->operation.proposal.protocol_hash)); } break; case OPERATION_TAG_BALLOT: @@ -230,19 +246,19 @@ struct parsed_operation_group *parse_operations(const void *data, size_t length, const struct ballot_contents *ballot_data = NEXT_TYPE(struct ballot_contents); if (ix != length) PARSE_ERROR(); - out.operation.ballot.voting_period = READ_UNALIGNED_BIG_ENDIAN(int32_t, &ballot_data->period); - memcpy(out.operation.ballot.protocol_hash, ballot_data->proposal, sizeof(out.operation.ballot.protocol_hash)); + out->operation.ballot.voting_period = READ_UNALIGNED_BIG_ENDIAN(int32_t, &ballot_data->period); + memcpy(out->operation.ballot.protocol_hash, ballot_data->proposal, sizeof(out->operation.ballot.protocol_hash)); const int8_t ballot_vote = READ_UNALIGNED_BIG_ENDIAN(int8_t, &ballot_data->ballot); switch (ballot_vote) { case 0: - out.operation.ballot.vote = BALLOT_VOTE_YEA; + out->operation.ballot.vote = BALLOT_VOTE_YEA; break; case 1: - out.operation.ballot.vote = BALLOT_VOTE_NAY; + out->operation.ballot.vote = BALLOT_VOTE_NAY; break; case 2: - out.operation.ballot.vote = BALLOT_VOTE_PASS; + out->operation.ballot.vote = BALLOT_VOTE_PASS; break; default: PARSE_ERROR(); @@ -254,11 +270,11 @@ struct parsed_operation_group *parse_operations(const void *data, size_t length, uint8_t delegate_present = NEXT_BYTE(data, &ix, length); if (delegate_present) { const struct delegation_contents *dlg = NEXT_TYPE(struct delegation_contents); - parse_implicit(&out.operation.destination, dlg->curve_code, dlg->hash); + parse_implicit(&out->operation.destination, dlg->curve_code, dlg->hash); } else { // Encode "not present" - out.operation.destination.originated = 0; - out.operation.destination.curve_code = TEZOS_NO_CURVE; + out->operation.destination.originated = 0; + out->operation.destination.curve_code = TEZOS_NO_CURVE; } } break; @@ -270,28 +286,28 @@ struct parsed_operation_group *parse_operations(const void *data, size_t length, } __attribute__((packed)); const struct origination_header *hdr = NEXT_TYPE(struct origination_header); - parse_implicit(&out.operation.destination, hdr->curve_code, hdr->hash); - out.operation.amount = PARSE_Z(data, &ix, length); + parse_implicit(&out->operation.destination, hdr->curve_code, hdr->hash); + out->operation.amount = PARSE_Z(data, &ix, length); if (NEXT_BYTE(data, &ix, length) != 0) { - out.operation.flags |= ORIGINATION_FLAG_SPENDABLE; + out->operation.flags |= ORIGINATION_FLAG_SPENDABLE; } if (NEXT_BYTE(data, &ix, length) != 0) { - out.operation.flags |= ORIGINATION_FLAG_DELEGATABLE; + out->operation.flags |= ORIGINATION_FLAG_DELEGATABLE; } if (NEXT_BYTE(data, &ix, length) != 0) { // Has delegate const struct delegation_contents *dlg = NEXT_TYPE(struct delegation_contents); - parse_implicit(&out.operation.delegate, dlg->curve_code, dlg->hash); + parse_implicit(&out->operation.delegate, dlg->curve_code, dlg->hash); } if (NEXT_BYTE(data, &ix, length) != 0) PARSE_ERROR(); // Has script } break; case OPERATION_TAG_TRANSACTION: { - out.operation.amount = PARSE_Z(data, &ix, length); + out->operation.amount = PARSE_Z(data, &ix, length); const struct contract *destination = NEXT_TYPE(struct contract); - parse_contract(&out.operation.destination, destination); + parse_contract(&out->operation.destination, destination); uint8_t params = NEXT_BYTE(data, &ix, length); if (params) PARSE_ERROR(); // TODO: Support params @@ -302,9 +318,7 @@ struct parsed_operation_group *parse_operations(const void *data, size_t length, } } - if (out.operation.tag == OPERATION_TAG_NONE && !out.has_reveal) { + if (out->operation.tag == OPERATION_TAG_NONE && !out->has_reveal) { PARSE_ERROR(); // Must have at least one op } - - return &out; // Success! } diff --git a/src/operations.h b/src/operations.h index d1a4a9fa..ce5f7ab3 100644 --- a/src/operations.h +++ b/src/operations.h @@ -8,42 +8,7 @@ #include "protocol.h" #include "cx.h" - -struct parsed_contract { - uint8_t originated; - uint8_t curve_code; // TEZOS_NO_CURVE in originated case - // An implicit contract with TEZOS_NO_CURVE means not present - uint8_t hash[HASH_SIZE]; -}; - -#define PROTOCOL_HASH_SIZE 32 - -struct parsed_proposal { - int32_t voting_period; - uint8_t protocol_hash[PROTOCOL_HASH_SIZE]; -}; - -enum ballot_vote { - BALLOT_VOTE_YEA, - BALLOT_VOTE_NAY, - BALLOT_VOTE_PASS, -}; - -struct parsed_ballot { - int32_t voting_period; - uint8_t protocol_hash[PROTOCOL_HASH_SIZE]; - enum ballot_vote vote; -}; - -enum operation_tag { - OPERATION_TAG_NONE = -1, // Sentinal value, as 0 is possibly used for something - OPERATION_TAG_PROPOSAL = 5, - OPERATION_TAG_BALLOT = 6, - OPERATION_TAG_REVEAL = 7, - OPERATION_TAG_TRANSACTION = 8, - OPERATION_TAG_ORIGINATION = 9, - OPERATION_TAG_DELEGATION = 10, -}; +#include "types.h" typedef uint32_t allowed_operation_set; @@ -59,34 +24,15 @@ static inline void clear_operation_set(allowed_operation_set *ops) { *ops = 0; } -#define ORIGINATION_FLAG_SPENDABLE 1 -#define ORIGINATION_FLAG_DELEGATABLE 2 - -struct parsed_operation { - enum operation_tag tag; - struct parsed_contract source; - struct parsed_contract destination; - struct parsed_contract delegate; // For originations only - struct parsed_proposal proposal; // For proposals only - struct parsed_ballot ballot; // For ballots only - uint64_t amount; // 0 where inappropriate - uint32_t flags; // Interpretation depends on operation type -}; - -struct parsed_operation_group { - cx_ecfp_public_key_t public_key; // compressed - uint64_t total_fee; - uint64_t total_storage_limit; - bool has_reveal; - struct parsed_contract signing; - struct parsed_operation operation; -}; // Throws upon invalid data. // Allows arbitrarily many "REVEAL" operations but only one operation of any other type, // which is the one it puts into the group. - -// Returns pointer to static data -- non-reentrant as hell. -struct parsed_operation_group * -parse_operations(const void *data, size_t length, cx_curve_t curve, size_t path_length, - uint32_t *bip32_path, allowed_operation_set ops); +void parse_operations( + struct parsed_operation_group *const out, + void const *const data, + size_t length, + cx_curve_t curve, + bip32_path_t const *const bip32_path, + allowed_operation_set ops +); diff --git a/src/os_cx.h b/src/os_cx.h new file mode 100644 index 00000000..c1e92cb1 --- /dev/null +++ b/src/os_cx.h @@ -0,0 +1,5 @@ +#pragma once + +// Order matters +#include "os.h" +#include "cx.h" diff --git a/src/protocol.h b/src/protocol.h index 82b9a5f5..892ef81e 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -6,6 +6,7 @@ #include "os.h" #include "cx.h" +#include "types.h" #define MAGIC_BYTE_INVALID 0x00 #define MAGIC_BYTE_BLOCK 0x01 @@ -14,8 +15,6 @@ #define MAGIC_BYTE_UNSAFE_OP2 0x04 #define MAGIC_BYTE_UNSAFE_OP3 0x05 -typedef uint32_t level_t; - static inline uint8_t get_magic_byte(const uint8_t *data, size_t length) { if (data == NULL || length == 0) return MAGIC_BYTE_INVALID; else return *data; @@ -23,7 +22,7 @@ static inline uint8_t get_magic_byte(const uint8_t *data, size_t length) { #define READ_UNALIGNED_BIG_ENDIAN(type, in) \ ({ \ - const uint8_t *bytes = (uint8_t*)in; \ + uint8_t const *bytes = (uint8_t const *)in; \ uint8_t out_bytes[sizeof(type)]; \ type res; \ \ @@ -34,3 +33,7 @@ static inline uint8_t get_magic_byte(const uint8_t *data, size_t length) { \ res; \ }) + +// Same as READ_UNALIGNED_BIG_ENDIAN but helps keep track of how many bytes +// have been read by adding sizeof(type) to the given counter. +#define CONSUME_UNALIGNED_BIG_ENDIAN(counter, type, addr) ({ counter += sizeof(type); READ_UNALIGNED_BIG_ENDIAN(type, addr); }) diff --git a/src/to_string.c b/src/to_string.c index a72e2304..83033a94 100644 --- a/src/to_string.c +++ b/src/to_string.c @@ -8,38 +8,65 @@ #define NO_CONTRACT_STRING "None" -int parsed_contract_to_string(char *buff, uint32_t buff_size, const struct parsed_contract *contract) { +#define TEZOS_HASH_CHECKSUM_SIZE 4 + +static void pkh_to_string(char *buff, const size_t buff_size, const cx_curve_t curve, const uint8_t hash[HASH_SIZE]); + +// These functions output terminating null bytes, and return the ending offset. +static size_t microtez_to_string(char *dest, uint64_t number); + +void parsed_contract_to_string(char *buff, uint32_t buff_size, const struct parsed_contract *contract) { if (contract->originated == 0 && contract->curve_code == TEZOS_NO_CURVE) { - if (buff_size < sizeof(NO_CONTRACT_STRING)) return 0; + if (buff_size < sizeof(NO_CONTRACT_STRING)) THROW(EXC_WRONG_LENGTH); strcpy(buff, NO_CONTRACT_STRING); - return buff_size; + return; } - cx_curve_t curve; - if (contract->originated != 0) { - curve = CX_CURVE_NONE; - } else { - curve = curve_code_to_curve(contract->curve_code); - } - return pkh_to_string(buff, buff_size, curve, contract->hash); + cx_curve_t const curve = contract->originated != 0 + ? CX_CURVE_NONE + : curve_code_to_curve(contract->curve_code); + pkh_to_string(buff, buff_size, curve, contract->hash); } -int pubkey_to_pkh_string(char *buff, uint32_t buff_size, cx_curve_t curve, - const cx_ecfp_public_key_t *public_key) { +void pubkey_to_pkh_string( + char *const out, + size_t const out_size, + cx_curve_t const curve, + cx_ecfp_public_key_t const *const public_key +) { + check_null(out); + check_null(public_key); + uint8_t hash[HASH_SIZE]; public_key_hash(hash, curve, public_key); - return pkh_to_string(buff, buff_size, curve, hash); + pkh_to_string(out, out_size, curve, hash); } -// TODO: this should return size_t -int pkh_to_string(char *buff, const size_t buff_size, const cx_curve_t curve, const uint8_t hash[HASH_SIZE]) { +void bip32_path_with_curve_to_pkh_string(char *const out, size_t const out_size, bip32_path_with_curve_t const *const key) { + check_null(out); + check_null(key); + + cx_ecfp_public_key_t const *const pubkey = generate_public_key(key->curve, &key->bip32_path); + pubkey_to_pkh_string(out, out_size, key->curve, pubkey); +} + + +void compute_hash_checksum(uint8_t out[TEZOS_HASH_CHECKSUM_SIZE], void const *const data, size_t size) { + uint8_t checksum[32]; + cx_hash_sha256(data, size, checksum, sizeof(checksum)); + cx_hash_sha256(checksum, sizeof(checksum), checksum, sizeof(checksum)); + memcpy(out, checksum, TEZOS_HASH_CHECKSUM_SIZE); +} + +void pkh_to_string(char *buff, const size_t buff_size, const cx_curve_t curve, + const uint8_t hash[HASH_SIZE]) { if (buff_size < PKH_STRING_SIZE) THROW(EXC_WRONG_LENGTH); // Data to encode struct __attribute__((packed)) { - char prefix[3]; + uint8_t prefix[3]; uint8_t hash[HASH_SIZE]; - char checksum[4]; + uint8_t checksum[TEZOS_HASH_CHECKSUM_SIZE]; } data; // prefix @@ -70,45 +97,66 @@ int pkh_to_string(char *buff, const size_t buff_size, const cx_curve_t curve, co // hash memcpy(data.hash, hash, sizeof(data.hash)); - - // checksum -- twice because them's the rules - uint8_t checksum[32]; - cx_hash_sha256((void*)&data, sizeof(data) - sizeof(data.checksum), checksum, sizeof(checksum)); - cx_hash_sha256(checksum, sizeof(checksum), checksum, sizeof(checksum)); - memcpy(data.checksum, checksum, sizeof(data.checksum)); + compute_hash_checksum(data.checksum, &data, sizeof(data) - sizeof(data.checksum)); size_t out_size = buff_size; if (!b58enc(buff, &out_size, &data, sizeof(data))) THROW(EXC_WRONG_LENGTH); - return out_size; } -size_t protocol_hash_to_string(char *buff, const size_t buff_size, const uint8_t hash[PROTOCOL_HASH_SIZE]) { +void protocol_hash_to_string(char *buff, const size_t buff_size, const uint8_t hash[PROTOCOL_HASH_SIZE]) { if (buff_size < PROTOCOL_HASH_BASE58_STRING_SIZE) THROW(EXC_WRONG_LENGTH); // Data to encode struct __attribute__((packed)) { - char prefix[2]; + uint8_t prefix[2]; uint8_t hash[PROTOCOL_HASH_SIZE]; - char checksum[4]; + uint8_t checksum[TEZOS_HASH_CHECKSUM_SIZE]; } data = { .prefix = {2, 170} }; memcpy(data.hash, hash, sizeof(data.hash)); + compute_hash_checksum(data.checksum, &data, sizeof(data) - sizeof(data.checksum)); - // checksum -- twice because them's the rules - // Hash the input (prefix + hash) - // Hash that hash. - // Take the first 4 bytes of that - // Voila: a checksum. - uint8_t checksum[32]; - cx_hash_sha256((void*)&data, sizeof(data) - sizeof(data.checksum), checksum, sizeof(checksum)); - cx_hash_sha256(checksum, sizeof(checksum), checksum, sizeof(checksum)); - memcpy(data.checksum, checksum, sizeof(data.checksum)); + size_t out_size = buff_size; + if (!b58enc(buff, &out_size, &data, sizeof(data))) THROW(EXC_WRONG_LENGTH); +} + +void chain_id_to_string(char *const buff, size_t const buff_size, chain_id_t const chain_id) { + if (buff_size < CHAIN_ID_BASE58_STRING_SIZE) THROW(EXC_WRONG_LENGTH); + + // Data to encode + struct __attribute__((packed)) { + uint8_t prefix[3]; + int32_t chain_id; + uint8_t checksum[TEZOS_HASH_CHECKSUM_SIZE]; + } data = { + .prefix = {87, 82, 0}, + + // Must hash big-endian data so treating little endian as big endian just flips + .chain_id = READ_UNALIGNED_BIG_ENDIAN(uint32_t, &chain_id.v) + }; + + compute_hash_checksum(data.checksum, &data, sizeof(data) - sizeof(data.checksum)); size_t out_size = buff_size; if (!b58enc(buff, &out_size, &data, sizeof(data))) THROW(EXC_WRONG_LENGTH); - return out_size; +} + +#define STRCPY_OR_THROW(buff, size, x, exc) ({ \ + if (size < sizeof(x)) THROW(exc); \ + strcpy(buff, x); \ +}) + +void chain_id_to_string_with_aliases(char *const out, size_t const out_size, chain_id_t const *const chain_id) { + check_null(chain_id); + if (chain_id->v == 0) { + STRCPY_OR_THROW(out, out_size, "any", EXC_WRONG_LENGTH); + } else if (chain_id->v == mainnet_chain_id.v) { + STRCPY_OR_THROW(out, out_size, "mainnet", EXC_WRONG_LENGTH); + } else { + chain_id_to_string(out, out_size, *chain_id); + } } // These functions do not output terminating null bytes. @@ -128,6 +176,21 @@ static inline size_t convert_number(char dest[MAX_INT_DIGITS], uint64_t number, return 0; } +void number_to_string_indirect64(char *dest, size_t buff_size, const uint64_t *number) { + if (buff_size < MAX_INT_DIGITS + 1) THROW(EXC_WRONG_LENGTH); // terminating null + number_to_string(dest, *number); +} + +void number_to_string_indirect32(char *dest, size_t buff_size, const uint32_t *number) { + if (buff_size < MAX_INT_DIGITS + 1) THROW(EXC_WRONG_LENGTH); // terminating null + number_to_string(dest, *number); +} + +void microtez_to_string_indirect(char *dest, size_t buff_size, const uint64_t *number) { + if (buff_size < MAX_INT_DIGITS + 1) THROW(EXC_WRONG_LENGTH); // + terminating null + decimal point + microtez_to_string(dest, *number); +} + size_t number_to_string(char *dest, uint64_t number) { char tmp[MAX_INT_DIGITS]; size_t off = convert_number(tmp, number, false); @@ -171,3 +234,10 @@ size_t microtez_to_string(char *dest, uint64_t number) { dest[off] = '\0'; return off; } + +void copy_string(char *const dest, size_t const buff_size, char const *const src) { + char const *const src_in = (char const *)PIC(src); + // I don't care that we will loop through the string twice, latency is not an issue + if (strlen(src_in) >= buff_size) THROW(EXC_WRONG_LENGTH); + strcpy(dest, src_in); +} diff --git a/src/to_string.h b/src/to_string.h index 1e6f09f7..a250f263 100644 --- a/src/to_string.h +++ b/src/to_string.h @@ -5,19 +5,34 @@ #include "keys.h" #include "operations.h" - -#include "os.h" -#include "cx.h" +#include "os_cx.h" +#include "types.h" #include "ui.h" -int pubkey_to_pkh_string(char *buff, uint32_t buff_size, cx_curve_t curve, - const cx_ecfp_public_key_t *public_key); -int pkh_to_string(char *buff, const size_t buff_size, const cx_curve_t curve, const uint8_t hash[HASH_SIZE]); -size_t protocol_hash_to_string(char *buff, const size_t buff_size, const uint8_t hash[PROTOCOL_HASH_SIZE]); -int parsed_contract_to_string(char *buff, uint32_t buff_size, const struct parsed_contract *contract); - +void pubkey_to_pkh_string( + char *const out, + size_t const out_size, + cx_curve_t const curve, + cx_ecfp_public_key_t const *const public_key +); +void bip32_path_with_curve_to_pkh_string( + char *const out, + size_t const out_size, + bip32_path_with_curve_t const *const key +); +void protocol_hash_to_string(char *buff, const size_t buff_size, const uint8_t hash[PROTOCOL_HASH_SIZE]); +void parsed_contract_to_string(char *buff, uint32_t buff_size, const struct parsed_contract *contract); +void chain_id_to_string(char *buff, size_t const buff_size, chain_id_t const chain_id); +void chain_id_to_string_with_aliases(char *const out, size_t const out_size, chain_id_t const *const chain_id); -// These functions output terminating null bytes, and return the ending offset. -#define MAX_INT_DIGITS 20 +// dest must be at least MAX_INT_DIGITS size_t number_to_string(char *dest, uint64_t number); -size_t microtez_to_string(char *dest, uint64_t number); + +// These take their number parameter through a pointer and take a length +void number_to_string_indirect64(char *dest, uint32_t buff_size, const uint64_t *number); +void number_to_string_indirect32(char *dest, uint32_t buff_size, const uint32_t *number); +void microtez_to_string_indirect(char *dest, uint32_t buff_size, const uint64_t *number); + +// This is designed to be called with potentially unrelocated pointers from rodata tables +// for the src argument, so performs PIC on src. +void copy_string(char *const dest, size_t const buff_size, char const *const src); diff --git a/src/types.h b/src/types.h new file mode 100644 index 00000000..5e8f8859 --- /dev/null +++ b/src/types.h @@ -0,0 +1,217 @@ +#pragma once + +#include "exception.h" +#include "os.h" +#include "os_io_seproxyhal.h" + +#include +#include + +// Type-safe versions of true/false +#undef true +#define true ((bool)1) +#undef false +#define false ((bool)0) + +// Return number of bytes to transmit (tx) +typedef size_t (*apdu_handler)(uint8_t instruction); + +typedef uint32_t level_t; + +#define CHAIN_ID_BASE58_STRING_SIZE sizeof("NetXdQprcVkpaWU") + +#define MAX_INT_DIGITS 20 + +typedef struct { + uint32_t v; +} chain_id_t; + +// Mainnet Chain ID: NetXdQprcVkpaWU +static chain_id_t const mainnet_chain_id = { .v = 0x7A06A770 }; + +// UI +typedef bool (*ui_callback_t)(void); // return true to go back to idle screen + +// Uses K&R style declaration to avoid being stuck on const void *, to avoid having to cast the +// function pointers. +typedef void (*string_generation_callback)(/* char *buffer, size_t buffer_size, const void *data */); + +// Keys +struct key_pair { + cx_ecfp_public_key_t public_key; + cx_ecfp_private_key_t private_key; +}; + +// Baking Auth +#define MAX_BIP32_PATH 10 + +typedef struct { + uint8_t length; + uint32_t components[MAX_BIP32_PATH]; +} bip32_path_t; + +static inline void copy_bip32_path(bip32_path_t *const out, bip32_path_t const *const in) { + check_null(out); + check_null(in); + memcpy(out->components, in->components, in->length * sizeof(*in->components)); + out->length = in->length; +} + +static inline bool bip32_paths_eq(bip32_path_t const *const a, bip32_path_t const *const b) { + return a == b || ( + a != NULL && + b != NULL && + a->length == b->length && + memcmp(a->components, b->components, a->length * sizeof(*a->components)) == 0 + ); +} + + +typedef struct { + bip32_path_t bip32_path; + cx_curve_t curve; +} bip32_path_with_curve_t; + +static inline void copy_bip32_path_with_curve(bip32_path_with_curve_t *const out, bip32_path_with_curve_t const *const in) { + check_null(out); + check_null(in); + copy_bip32_path(&out->bip32_path, &in->bip32_path); + out->curve = in->curve; +} + +static inline bool bip32_path_with_curve_eq(bip32_path_with_curve_t const *const a, bip32_path_with_curve_t const *const b) { + return a == b || ( + a != NULL && + b != NULL && + bip32_paths_eq(&a->bip32_path, &b->bip32_path) && + a->curve == b->curve + ); +} + + +typedef struct { + level_t highest_level; + bool had_endorsement; +} high_watermark_t; + +typedef struct { + chain_id_t main_chain_id; + struct { + high_watermark_t main; + high_watermark_t test; + } hwm; + bip32_path_with_curve_t baking_key; +} nvram_data; + + +#define PKH_STRING_SIZE 40 // includes null byte // TODO: use sizeof for this. +#define PROTOCOL_HASH_BASE58_STRING_SIZE sizeof("ProtoBetaBetaBetaBetaBetaBetaBetaBetaBet11111a5ug96") + +#define MAX_SCREEN_COUNT 7 // Current maximum usage +#define PROMPT_WIDTH 16 +#define VALUE_WIDTH PROTOCOL_HASH_BASE58_STRING_SIZE + +// Macros to wrap a static prompt and value strings and ensure they aren't too long. +#define PROMPT(str) ({ \ + _Static_assert(sizeof(str) <= PROMPT_WIDTH + 1/*null byte*/ , str " won't fit in the UI prompt."); \ + str; \ +}) + +#define STATIC_UI_VALUE(str) ({ \ + _Static_assert(sizeof(str) <= VALUE_WIDTH + 1/*null byte*/, str " won't fit in the UI.".); \ + str; \ +}) + + +// Operations +#define PROTOCOL_HASH_SIZE 32 + +// TODO: Rename to KEY_HASH_SIZE +#define HASH_SIZE 20 + +struct parsed_contract { + uint8_t originated; // a lightweight bool + uint8_t curve_code; // TEZOS_NO_CURVE in originated case + // An implicit contract with TEZOS_NO_CURVE means not present + uint8_t hash[HASH_SIZE]; +}; + +struct parsed_proposal { + uint32_t voting_period; + // TODO: Make 32 bit version of number_to_string_indirect + uint8_t protocol_hash[PROTOCOL_HASH_SIZE]; +}; + +enum ballot_vote { + BALLOT_VOTE_YEA, + BALLOT_VOTE_NAY, + BALLOT_VOTE_PASS, +}; + +struct parsed_ballot { + uint32_t voting_period; + uint8_t protocol_hash[PROTOCOL_HASH_SIZE]; + enum ballot_vote vote; +}; + +enum operation_tag { + OPERATION_TAG_NONE = -1, // Sentinal value, as 0 is possibly used for something + OPERATION_TAG_PROPOSAL = 5, + OPERATION_TAG_BALLOT = 6, + OPERATION_TAG_REVEAL = 7, + OPERATION_TAG_TRANSACTION = 8, + OPERATION_TAG_ORIGINATION = 9, + OPERATION_TAG_DELEGATION = 10, +}; + +// TODO: Make this an enum. +// Flags for parsed_operation.flag +#define ORIGINATION_FLAG_SPENDABLE 1 +#define ORIGINATION_FLAG_DELEGATABLE 2 + +struct parsed_operation { + enum operation_tag tag; + struct parsed_contract source; + struct parsed_contract destination; + struct parsed_contract delegate; // For originations only + struct parsed_proposal proposal; // For proposals only + struct parsed_ballot ballot; // For ballots only + uint64_t amount; // 0 where inappropriate + uint32_t flags; // Interpretation depends on operation type +}; + +struct parsed_operation_group { + cx_ecfp_public_key_t public_key; // compressed + uint64_t total_fee; + uint64_t total_storage_limit; + bool has_reveal; + struct parsed_contract signing; + struct parsed_operation operation; +}; + +// Maximum number of APDU instructions +#define INS_MAX 0x0D + +#define APDU_INS(x) ({ \ + _Static_assert(x <= INS_MAX, "APDU instruction is out of bounds"); \ + x; \ +}) + +#define STRCPY(buff, x) ({ \ + _Static_assert(sizeof(buff) >= sizeof(x) && sizeof(*x) == sizeof(char), "String won't fit in buffer"); \ + strcpy(buff, x); \ +}) + +#undef MAX +#define MAX(a, b) ({ \ + __typeof__(a) ____a_ = (a); \ + __typeof__(b) ____b_ = (b); \ + ____a_ > ____b_ ? ____a_ : ____b_; \ +}) + +#undef MIN +#define MIN(a, b) ({ \ + __typeof__(a) ____a_ = (a); \ + __typeof__(b) ____b_ = (b); \ + ____a_ < ____b_ ? ____a_ : ____b_; \ +}) diff --git a/src/ui.c b/src/ui.c index 3c8b8f77..69076574 100644 --- a/src/ui.c +++ b/src/ui.c @@ -1,29 +1,22 @@ #include "ui.h" #include "ui_menu.h" +#include "ui_prompt.h" #include "baking_auth.h" +#include "globals.h" #include "keys.h" +#include "memory.h" #include "to_string.h" #include #include -ux_state_t ux; -unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B]; - -static callback_t ok_callback; -static callback_t cxl_callback; +#define G global.ui static unsigned button_handler(unsigned button_mask, unsigned button_mask_counter); -static uint32_t ux_step, ux_step_count; - #define PROMPT_CYCLES 3 -static uint32_t timeout_cycle_count; - -static char idle_text[16]; -char baking_auth_text[PKH_STRING_SIZE]; void require_pin(void) { bolos_ux_params_t params; @@ -33,7 +26,7 @@ void require_pin(void) { } #ifdef BAKING_APP -const bagl_element_t ui_idle_screen[] = { +static const bagl_element_t ui_idle_screen[] = { // type userid x y w h str rad // fill fg bg fid iid txt touchparams... ] {{BAGL_RECTANGLE, 0x00, 0, 0, 128, 32, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, @@ -71,7 +64,7 @@ const bagl_element_t ui_idle_screen[] = { {{BAGL_LABELINE, 0x01, 0, 26, 128, 12, 0, 0, 0, 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 0}, - idle_text, + G.baking_idle_screens.hwm, 0, 0, 0, @@ -91,13 +84,34 @@ const bagl_element_t ui_idle_screen[] = { {{BAGL_LABELINE, 0x02, 23, 26, 82, 12, 0x80 | 10, 0, 0, 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 26}, - baking_auth_text, + G.baking_idle_screens.pkh, 0, 0, 0, NULL, NULL, NULL}, + + {{BAGL_LABELINE, 0x03, 0, 12, 128, 12, 0, 0, 0, 0xFFFFFF, 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 0}, + "Chain", + 0, + 0, + 0, + NULL, + NULL, + NULL}, + + {{BAGL_LABELINE, 0x03, 23, 26, 82, 12, 0x80 | 10, 0, 0, 0xFFFFFF, 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 26}, + G.baking_idle_screens.chain, + 0, + 0, + 0, + NULL, + NULL, + NULL}, + }; static bool do_nothing(void) { @@ -107,35 +121,32 @@ static bool do_nothing(void) { static void ui_idle(void) { #ifdef BAKING_APP - update_auth_text(); - ui_display(ui_idle_screen, sizeof(ui_idle_screen)/sizeof(*ui_idle_screen), - do_nothing, exit_app, 2); + update_baking_idle_screens(); + ui_display(ui_idle_screen, NUM_ELEMENTS(ui_idle_screen), + do_nothing, exit_app, 3); #else + G.cxl_callback = exit_app; main_menu(); #endif } -void change_idle_display(uint32_t new) { - number_to_string(idle_text, new); - update_auth_text(); -} - void ui_initial_screen(void) { #ifdef BAKING_APP - change_idle_display(N_data.highest_level); + update_baking_idle_screens(); #endif + clear_ui_callbacks(); ui_idle(); } static bool is_idling(void) { - return cxl_callback == exit_app; + return G.cxl_callback == exit_app; } static void timeout(void) { if (is_idling()) { // Idle app timeout - update_auth_text(); - timeout_cycle_count = 0; + update_baking_idle_screens(); + G.timeout_cycle_count = 0; UX_REDISPLAY(); } else { // Prompt timeout -- simulate cancel button @@ -143,57 +154,57 @@ static void timeout(void) { } } -unsigned button_handler(unsigned button_mask, __attribute__((unused)) unsigned button_mask_counter) { - callback_t callback; +static unsigned button_handler(unsigned button_mask, __attribute__((unused)) unsigned button_mask_counter) { + ui_callback_t callback; switch (button_mask) { case BUTTON_EVT_RELEASED | BUTTON_LEFT: - callback = cxl_callback; + callback = G.cxl_callback; break; case BUTTON_EVT_RELEASED | BUTTON_RIGHT: - callback = ok_callback; + callback = G.ok_callback; break; default: return 0; } if (callback()) { + clear_ui_callbacks(); ui_idle(); } return 0; } const bagl_element_t *prepro(const bagl_element_t *element) { - // Always display elements with userid 0 - if (element->component.userid == 0) return element; + if (element->component.userid == BAGL_STATIC_ELEMENT) return element; + static const uint32_t pause_millis = 1500; uint32_t min = 2000; - uint32_t div = 2; + static const uint32_t div = 2; if (is_idling()) { min = 4000; } - if (ux_step == element->component.userid - 1) { + if (G.ux_step == element->component.userid - 1 || element->component.userid == BAGL_SCROLLING_ELEMENT) { // timeouts are in millis - if (ux_step_count > 1) { - UX_CALLBACK_SET_INTERVAL(MAX(min, - (1500 + bagl_label_roundtrip_duration_ms(element, 7)) / div)); - } else { - UX_CALLBACK_SET_INTERVAL(30000 / PROMPT_CYCLES); - } + UX_CALLBACK_SET_INTERVAL(MAX(min, + (pause_millis + bagl_label_roundtrip_duration_ms(element, 7)) / div)); return element; } else { return NULL; } } -void ui_display(const bagl_element_t *elems, size_t sz, callback_t ok_c, callback_t cxl_c, +void ui_display(const bagl_element_t *elems, size_t sz, ui_callback_t ok_c, ui_callback_t cxl_c, uint32_t step_count) { // Adapted from definition of UX_DISPLAY in header file - timeout_cycle_count = 0; - ux_step = 0; - ux_step_count = step_count; - ok_callback = ok_c; - cxl_callback = cxl_c; + G.timeout_cycle_count = 0; + G.ux_step = 0; + G.ux_step_count = step_count; + G.ok_callback = ok_c; + G.cxl_callback = cxl_c; + if (!is_idling()) { + switch_screen(0); + } ux.elements = elems; ux.elements_count = sz; ux.button_push_handler = button_handler; @@ -224,12 +235,15 @@ unsigned char io_event(__attribute__((unused)) unsigned char channel) { ux.callback_interval_ms -= MIN(ux.callback_interval_ms, 100); if (ux.callback_interval_ms == 0) { // prepare next screen - ux_step = (ux_step + 1) % ux_step_count; + G.ux_step = (G.ux_step + 1) % G.ux_step_count; + if (!is_idling()) { + switch_screen(G.ux_step); + } // check if we've timed out - if (ux_step == 0) { - timeout_cycle_count++; - if (timeout_cycle_count == PROMPT_CYCLES) { + if (G.ux_step == 0) { + G.timeout_cycle_count++; + if (G.timeout_cycle_count == PROMPT_CYCLES) { timeout(); break; // timeout() will often display a new screen } diff --git a/src/ui.h b/src/ui.h index a7c4d878..1bdeb5c5 100644 --- a/src/ui.h +++ b/src/ui.h @@ -2,23 +2,19 @@ #include "os_io_seproxyhal.h" -#include +#include "types.h" #include "keys.h" -#define PROTOCOL_HASH_BASE58_STRING_SIZE 52 // e.g. "ProtoBetaBetaBetaBetaBetaBetaBetaBetaBet11111a5ug96" plus null byte - -typedef bool (*callback_t)(void); // return true to go back to idle screen +#define BAGL_STATIC_ELEMENT 0 +#define BAGL_SCROLLING_ELEMENT 100 // Arbitrary value chosen to connect data structures with prepro func void ui_initial_screen(void); void ui_init(void); __attribute__((noreturn)) bool exit_app(void); // Might want to send it arguments to use as callback -void ui_display(const bagl_element_t *elems, size_t sz, callback_t ok_c, callback_t cxl_c, +void ui_display(const bagl_element_t *elems, size_t sz, ui_callback_t ok_c, ui_callback_t cxl_c, uint32_t step_count); unsigned char io_event(unsigned char channel); void io_seproxyhal_display(const bagl_element_t *element); -void change_idle_display(uint32_t new); - -extern char baking_auth_text[PKH_STRING_SIZE]; // TODO: Is this the right name? diff --git a/src/ui_prompt.c b/src/ui_prompt.c index d0ee7172..f148df64 100644 --- a/src/ui_prompt.c +++ b/src/ui_prompt.c @@ -1,38 +1,14 @@ #include "ui_prompt.h" #include "exception.h" +#include "globals.h" +#include "memory.h" +#include "to_string.h" #include -static char prompts[MAX_SCREEN_COUNT][PROMPT_WIDTH + 1]; // Additional bytes init'ed to null, -static char values[MAX_SCREEN_COUNT][VALUE_WIDTH + 1]; // and null they shall remain. - // TODO: Get rid of +1 - -#define SCREEN_FOR(SCREEN_ID) \ - {{BAGL_LABELINE, SCREEN_ID + 1, 0, 12, 128, 12, 0, 0, 0, 0xFFFFFF, 0x000000, \ - BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 0}, \ - prompts[SCREEN_ID], \ - 0, \ - 0, \ - 0, \ - NULL, \ - NULL, \ - NULL}, \ - {{BAGL_LABELINE, SCREEN_ID + 1, 23, 26, 82, 12, 0x80 | 10, 0, 0, 0xFFFFFF, 0x000000, \ - BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 26}, \ - values[SCREEN_ID], \ - 0, \ - 0, \ - 0, \ - NULL, \ - NULL, \ - NULL} - -#define INITIAL_ELEMENTS_COUNT 3 -#define ELEMENTS_PER_SCREEN 2 - static const bagl_element_t ui_multi_screen[] = { - {{BAGL_RECTANGLE, 0x00, 0, 0, 128, 32, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, + {{BAGL_RECTANGLE, BAGL_STATIC_ELEMENT, 0, 0, 128, 32, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL, 0, @@ -42,7 +18,7 @@ static const bagl_element_t ui_multi_screen[] = { NULL, NULL}, - {{BAGL_ICON, 0x00, 3, 12, 7, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, + {{BAGL_ICON, BAGL_STATIC_ELEMENT, 3, 12, 7, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, BAGL_GLYPH_ICON_CROSS}, NULL, 0, @@ -52,7 +28,7 @@ static const bagl_element_t ui_multi_screen[] = { NULL, NULL}, - {{BAGL_ICON, 0x00, 117, 13, 8, 6, 0, 0, 0, 0xFFFFFF, 0x000000, 0, + {{BAGL_ICON, BAGL_STATIC_ELEMENT, 117, 13, 8, 6, 0, 0, 0, 0xFFFFFF, 0x000000, 0, BAGL_GLYPH_ICON_CHECK}, NULL, 0, @@ -62,39 +38,67 @@ static const bagl_element_t ui_multi_screen[] = { NULL, NULL}, - SCREEN_FOR(0), - SCREEN_FOR(1), - SCREEN_FOR(2), - SCREEN_FOR(3), - SCREEN_FOR(4), - SCREEN_FOR(5), - SCREEN_FOR(6), + {{BAGL_LABELINE, BAGL_STATIC_ELEMENT, 0, 12, 128, 12, 0, 0, 0, 0xFFFFFF, 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 0}, + global.ui.prompt.active_prompt, + 0, + 0, + 0, + NULL, + NULL, + NULL}, + + {{BAGL_LABELINE, BAGL_SCROLLING_ELEMENT, 23, 26, 82, 12, 0x80 | 10, 0, 0, 0xFFFFFF, 0x000000, + BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 26}, + global.ui.prompt.active_value, + 0, + 0, + 0, + NULL, + NULL, + NULL}, }; -char *get_value_buffer(uint32_t which) { +void switch_screen(uint32_t which) { if (which >= MAX_SCREEN_COUNT) THROW(EXC_MEMORY_ERROR); - return values[which]; + const char *label = (const char*)PIC(global.ui.prompt.prompts[which]); + + strncpy(global.ui.prompt.active_prompt, label, sizeof(global.ui.prompt.active_prompt)); + if (global.ui.prompt.callbacks[which] == NULL) THROW(EXC_MEMORY_ERROR); + global.ui.prompt.callbacks[which]( + global.ui.prompt.active_value, sizeof(global.ui.prompt.active_value), + global.ui.prompt.callback_data[which]); +} + +void register_ui_callback(uint32_t which, string_generation_callback cb, const void *data) { + if (which >= MAX_SCREEN_COUNT) THROW(EXC_MEMORY_ERROR); + global.ui.prompt.callbacks[which] = cb; + global.ui.prompt.callback_data[which] = data; +} + +void clear_ui_callbacks(void) { + for (int i = 0; i < MAX_SCREEN_COUNT; ++i) { + global.ui.prompt.callbacks[i] = NULL; + } } __attribute__((noreturn)) -void ui_prompt(const char *const *labels, const char *const *data, callback_t ok_c, callback_t cxl_c) { +void ui_prompt(const char *const *labels, const char *const *data, ui_callback_t ok_c, ui_callback_t cxl_c) { check_null(labels); + global.ui.prompt.prompts = labels; size_t i; for (i = 0; labels[i] != NULL; i++) { const char *label = (const char *)PIC(labels[i]); if (i >= MAX_SCREEN_COUNT || strlen(label) > PROMPT_WIDTH) THROW(EXC_MEMORY_ERROR); - strncpy(prompts[i], label, sizeof(prompts[i])); if (data != NULL) { - const char *value = (const char *)PIC(data[i]); - if (strlen(value) > VALUE_WIDTH) THROW(EXC_MEMORY_ERROR); - strncpy(values[i], value, sizeof(values[i])); + register_ui_callback(i, copy_string, data[i]); } } size_t screen_count = i; - ui_display(ui_multi_screen, INITIAL_ELEMENTS_COUNT + screen_count * ELEMENTS_PER_SCREEN, + ui_display(ui_multi_screen, NUM_ELEMENTS(ui_multi_screen), ok_c, cxl_c, screen_count); THROW(ASYNC_EXCEPTION); } diff --git a/src/ui_prompt.h b/src/ui_prompt.h index 25c82eb2..54861fec 100644 --- a/src/ui_prompt.h +++ b/src/ui_prompt.h @@ -1,27 +1,21 @@ #pragma once #include "ui.h" - -#define MAX_SCREEN_COUNT 7 // Current maximum usage -#define PROMPT_WIDTH 16 -#define VALUE_WIDTH PROTOCOL_HASH_BASE58_STRING_SIZE - -// Macros to wrap a static prompt and value strings and ensure they aren't too long. -#define PROMPT(str) \ - ({ \ - _Static_assert(sizeof(str) <= PROMPT_WIDTH + 1/*null byte*/ , str " won't fit in the UI prompt."); \ - str; \ - }) - -#define STATIC_UI_VALUE(str) \ - ({ \ - _Static_assert(sizeof(str) <= VALUE_WIDTH + 1/*null byte*/, str " won't fit in the UI.".); \ - str; \ - }) +#include "types.h" // Displays labels (terminated with a NULL pointer) associated with data -// If data is NULL, assume we've filled it in directly with get_value_buffer +// labels must be completely static string constants +// data may be dynamic +// Alternatively, if data is NULL, assume we've registered appropriate callbacks to generate the data +// All pointers may be unrelocated __attribute__((noreturn)) -void ui_prompt(const char *const *labels, const char *const *data, callback_t ok_c, callback_t cxl_c); +void ui_prompt(const char *const *labels, const char *const *data, ui_callback_t ok_c, ui_callback_t cxl_c); + +// This is called by internal UI code to implement buffering +void switch_screen(uint32_t which); +// This is called by internal UI code to prevent callbacks from sticking around +void clear_ui_callbacks(void); -char *get_value_buffer(uint32_t which); +// This function registers how a value is to be produced +void register_ui_callback(uint32_t which, string_generation_callback cb, const void *data); +#define REGISTER_STATIC_UI_VALUE(index, str) register_ui_callback(index, copy_string, STATIC_UI_VALUE(str)) diff --git a/test/apdu.sh b/test/apdu.sh index 1c3c0d01..df7647b6 100755 --- a/test/apdu.sh +++ b/test/apdu.sh @@ -1,9 +1,5 @@ #!/usr/bin/env bash -set -eu +set -Eeuo pipefail -DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -cd "$DIR" - -if ! nix-shell ledger-blue-shell.nix --pure --run 'python -m ledgerblue.runScript --apdu'; then - python -m ledgerblue.runScript --apdu -fi +root="$(git rev-parse --show-toplevel)" +nix-shell "$root/nix/ledgerblue.nix" -A shell --pure --run 'python -m ledgerblue.runScript --apdu' diff --git a/test/ledger-blue-shell.nix b/test/ledger-blue-shell.nix deleted file mode 100644 index 7fb7c441..00000000 --- a/test/ledger-blue-shell.nix +++ /dev/null @@ -1,11 +0,0 @@ -let - # TODO: Upstream this change and use ../nixpkgs.nix as the pin. - pkgs = import ((import {}).fetchFromGitHub { - owner = "NixOS"; - repo = "nixpkgs"; - rev = "e3be0e49f082df2e653d364adf91b835224923d9"; - sha256 = "15xcs10fdc3lxvasd7cszd6a6vip1hs9r9r1qz2z0n9zkkfv3rrq"; - }) {}; -in pkgs.runCommand "ledger-blue-shell" { - buildInputs = with pkgs.python36Packages; [ ecpy hidapi pycrypto python-u2flib-host requests ledgerblue pillow pkgs.hidapi ]; -} ""